# 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](https://codna.me/docs/adr/0036-a-page-can-be-a-dashboard.md)). 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.

````markdown
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](https://codna.me/docs/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](#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](https://codna.me/docs/automations.md#writing-a-page)):

```js
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](https://codna.me/docs/reference/tools.md)). 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.
