CoDNA Book a call
Contents

Docs How it works

Dashboards

A dashboard is a page that draws its numbers: charts, figures and tables, made by an AI tool and shared like any other page (adr/0036). Claude makes dashboards in claude.ai and in Claude Code; asked to put one on CoDNA, it publishes it here, where the people it is for can open it, and an automation can keep its numbers current.

What a dashboard is

A page, in markdown, like every other: what the dashboard shows, the line that shows it, and the data it draws.

Spend for each campaign over the last 30 days, from the campaign database.

![Campaigns](campaigns.files/dashboard.html)

## Data

```json stats
[
{"day":"2026-11-05","campaign":"Autumn","spend":120.5,"clicks":3400},
{"day":"2026-11-05","campaign":"Welcome","spend":80,"clicks":2100}
]
```

```json target
{
  "spend": 100
}
```
  • The drawing is an HTML file of the page, campaigns.files/dashboard.html, beside it as its images are (files.md). The image line shows it where it is, as wide as the page's column and as tall as what it draws. Writing the page, its place and name stand for it; it is drawn when the page is read.
  • The data is each fenced block whose info string is json and a name: a dataset. Its body is one JSON value, most often rows as an array of objects. A drawing reads its own page's datasets and nothing else. Reading the page, each folds away under its name and size.

Because the data is in the page:

  • an AI tool reading the page reads the numbers, and can answer from them;
  • every change to them is a version in the page's history, and a diff shows which rows changed;
  • a number corrected by hand in the editor is the number the dashboard draws.

Everything else is the page's: who can see it, its folder, search, the trash, export. A dashboard is shared, moved and deleted as a page is.

Publishing one

An AI tool publishes with publish_dashboard over MCP; anything with a token can POST /api/dashboards. Both take:

  • document: the page. An address for a new one, such as output:dashboards/campaigns, or an existing page's address or id.
  • title: what people call it. Needed for a new page.
  • description: markdown, saying what it shows and where its numbers come from. Needed for a new page.
  • html: the drawing, at most 1 MB. Needed for a new page.
  • data: datasets by name (letters, digits, - and _, up to 64). null takes one out.
  • change_summary and expected_version, as for any edit.

A new page is made as the person, private to them until it is shared. Publishing to a page that is there is an edit, by someone who can edit it, and a new version:

  • With a description, the page is written whole: the description, the drawing, then ## Data and its datasets.
  • Without one, only what is given changes and the page's own text stays. A new drawing is added (dashboard-2.html, and on) and the image line points at it, so older versions still show the drawing they had. A dataset given replaces the block of that name, or is added at the end; one not given stays as the page has it.

output is for a dashboard an automation refreshes, as it is for reports; wiki for one kept by hand. A dataset should be what the dashboard shows, summed up (30 days of daily totals, not every event): every version is kept.

Writing the drawing

The drawing is a whole HTML document or a fragment. CoDNA puts a script before its own that provides:

  • codna.data(name): a dataset, as the page has it, a fresh copy each time. undefined for a name the page does not have.
  • codna.datasets: the names of the page's datasets, in its order.
  • codna.page: the page's title, address, version and updated_at.
  • codna.theme: light or dark, as the page around it is shown.
  • codna.problems: datasets whose block is not JSON, by name, with why.

and CSS variables in CoDNA's colours for that theme, as plain values a chart drawn on a canvas can read: --codna-fg, --codna-muted, --codna-bg, --codna-panel, --codna-border, --codna-accent, --codna-ok, --codna-warn, --codna-bad, --codna-series-1 to --codna-series-8, --codna-font and --codna-font-mono. The page's text colour and font are its defaults, and its background is the page's.

What it may load and do is in Serving the drawing. In short: scripts and styles inline or from cdnjs, jsDelivr or unpkg at an exact version, fonts from Google Fonts, images as data: URLs, and nothing else from the network. Draw to the width it is given, which is the page's column and narrow on a phone, and let its height follow what it draws: a drawing sized to the window's height has nothing to grow into. Links open in a new tab.

Keeping it current

An automation writes a dashboard's datasets with actions.write and data in place of content (automations.md):

export function inputs() {
  return {
    stats: source('campaign-db', {
      sql: "select day, campaign, sum(spend) as spend from daily_stats where day > now() - interval '30 days' group by 1, 2 order by 1, 2",
    }),
  }
}

export default function ({ inputs, actions }) {
  if (inputs.stats.error) return actions.log(inputs.stats.error)
  actions.write({ into: 'output:dashboards/campaigns', data: { stats: inputs.stats.rows } })
}

Run on a schedule, it keeps the numbers current. It changes the datasets it names and nothing else, in a page that must be there and in output. Data the page already holds makes no version, and the run says so.

The page says what keeps it current. Under the dashboard, everyone who can read the page sees when it was last refreshed and by which automation: the later of the last version the automation wrote and its last run that went through. The automation's owner also sees:

  • Refresh now, on a scheduled automation, which runs it as a tick. The new numbers are drawn when its version arrives.
  • When its last run failed, that it did and why, with a link to the automation.

Automations are their owner's, so nobody else is offered either. The numbers come through the owner's connections, so everyone who can read the page sees the same ones.

Serving the drawing

A drawing is HTML and script someone else wrote, shown to people signed in to CoDNA. It is served from GET /api/dashboards/<space>/<path>/<slug>.files/<name>.html, with its page's datasets put in, to whoever can read its page; to anyone else it is the same 404 as a page that is not there. ?theme=dark serves it in dark colours. Its file's own route, /api/files/…, downloads it, inert.

It is served under this policy:

sandbox allow-scripts allow-popups allow-popups-to-escape-sandbox allow-downloads;
default-src 'none';
script-src 'unsafe-inline' 'unsafe-eval' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://unpkg.com;
style-src 'unsafe-inline' https://fonts.googleapis.com https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://unpkg.com;
font-src https://fonts.gstatic.com data:;
img-src data: blob:;
connect-src 'none'; form-action 'none'; base-uri 'none'; frame-ancestors 'self'

with Referrer-Policy: no-referrer, and shown in an <iframe> with the same sandbox:

  • An origin of its own. Sandboxed without allow-same-origin, it matches no site. It cannot read CoDNA's cookies or storage, its requests to CoDNA's API carry no session, and it cannot reach into the page around it. That holds whatever opens its address, because the policy comes with the response.
  • Only in its page. A browser that says it is opening the address as a page of its own (Sec-Fetch-Dest: document) is refused. Otherwise a drawing could be made to look like anything, a sign-in form for one, at an address on the instance, with nothing of CoDNA around it. In a page, it sits in the column, under the page's title, with what refreshes it under it.
  • No network but libraries. It cannot fetch, show an outside image, or submit a form, so what it shows is what its page holds, and nobody learns who opened it. Libraries come from the three CDNs at the versions it names, and the CDN is not told which page asked.
  • Its own page only. A drawing is handed its own page's data, which whoever wrote it could already read, since they could edit the page. It is never handed anything else the reader can see.
  • As tall as it draws. The script CoDNA adds tells the page its height as it changes, and the page sizes the frame to it, between 120 and 20,000 pixels. A framed drawing does not scroll.

Agents

  • publish_dashboard publishes (reference). Its description is the contract above, for the model.
  • get_document returns the page with its datasets, and lists its drawing among its files.
  • get_file hands an agent a drawing as its HTML, to change and publish again.

Claude rebuilding a dashboard it made in claude.ai turns each live query into a dataset, and each attached file into a dataset of its rows. A claude.ai dashboard's queries run with each reader's own access; a CoDNA dashboard's data is the page's, the same for everyone who can read it, kept current by an automation.

Not yet

  • Numbers that depend on who is looking.
  • A drawing that reads other pages.
  • Public links: nobody who is not signed in sees a dashboard, as nobody sees a page.
  • Uploading a drawing, or drawing one, in the web UI.