# Recipe: a live dashboard

Publish a dashboard once, then push new numbers to it on a schedule. People who have it open see the numbers change without reloading.

You need a DocPin workspace and either an API key (**Connections → API keys**) or DocPin connected to Claude or ChatGPT.

## 1. The page

A complete page. It reads everything from [page data](/docs/page-data.md), handles missing values, and re-checks every minute.

```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sales dashboard</title>
<style>
  body { font: 16px/1.5 system-ui, sans-serif; margin: 0; padding: 32px; background: #f7f6f3; color: #1d1c1a }
  h1 { margin: 0 0 4px; font-size: 28px }
  .asof { color: #6b6762; margin: 0 0 24px }
  .cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px }
  .card { background: #fff; border: 1px solid #e8e5df; border-radius: 14px; padding: 18px }
  .label { color: #6b6762; font-size: 14px }
  .value { font-size: 36px; font-weight: 700; margin-top: 4px }
  table { width: 100%; border-collapse: collapse; margin-top: 24px; background: #fff; border-radius: 14px; overflow: hidden }
  td, th { text-align: left; padding: 10px 14px; border-top: 1px solid #e8e5df }
  th { background: #f3f1ec; border-top: 0 }
  .empty { color: #6b6762 }
</style>
</head>
<body>
  <h1>Sales dashboard</h1>
  <p class="asof" id="asof">Loading…</p>
  <div class="cards">
    <div class="card"><div class="label">Revenue this month</div><div class="value" id="revenue">–</div></div>
    <div class="card"><div class="label">New customers</div><div class="value" id="customers">–</div></div>
  </div>
  <table>
    <thead><tr><th>Region</th><th>Revenue</th></tr></thead>
    <tbody id="regions"><tr><td class="empty" colspan="2">No regions yet</td></tr></tbody>
  </table>
<script>
  const money = (n) => typeof n === "number" ? "$" + n.toLocaleString() : "–";
  const text = (id, value) => { document.getElementById(id).textContent = value; };

  function render(data) {
    text("revenue", money(data.revenue));
    text("customers", typeof data.customers === "number" ? data.customers.toLocaleString() : "–");
    text("asof", data.asOf ? "As of " + new Date(data.asOf).toLocaleString() : "No data yet");
    const rows = Array.isArray(data.regions) ? data.regions : [];
    const body = document.getElementById("regions");
    body.replaceChildren(...(rows.length ? rows : [null]).map((r) => {
      const tr = document.createElement("tr");
      if (!r) { tr.innerHTML = '<td class="empty" colspan="2">No regions yet</td>'; return tr; }
      const name = document.createElement("td"); name.textContent = String(r.name ?? "");
      const value = document.createElement("td"); value.textContent = money(r.revenue);
      tr.append(name, value);
      return tr;
    }));
  }

  if (window.docpin) docpin.onData(render, { every: 60 });
  else render({});
</script>
</body>
</html>
```

It uses `textContent`, not `innerHTML`, for data values, so text in the data can't inject markup.

## 2. Publish it

**From the API:**

```bash
curl https://api.docpin.app/v1/pages \
  -H "Authorization: Bearer $DOCPIN_API_KEY" \
  -F file=@dashboard.html -F slug=sales
```

The response includes `url` (the link to share) and `doc.id`. Save `doc.id` as `PAGE_ID`.

**From Claude or ChatGPT:** "Publish this to DocPin as sales."

## 3. Push numbers

**From the API**, from cron, CI or any scheduler:

```bash
curl -X PATCH https://api.docpin.app/v1/pages/$PAGE_ID/data \
  -H "Authorization: Bearer $DOCPIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "revenue": 128400,
    "customers": 37,
    "regions": [{"name": "New Zealand", "revenue": 51200}, {"name": "Australia", "revenue": 77200}],
    "asOf": "2026-11-06T09:00:00Z"
  }'
```

**From Node.js:**

```js
const res = await fetch(`https://api.docpin.app/v1/pages/${process.env.PAGE_ID}/data`, {
  method: "PATCH",
  headers: { Authorization: `Bearer ${process.env.DOCPIN_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({ revenue: 128400, customers: 37, asOf: new Date().toISOString() }),
});
if (!res.ok) throw new Error((await res.json()).error);
```

**From Python:**

```python
import os, datetime, requests

r = requests.patch(
    f"https://api.docpin.app/v1/pages/{os.environ['PAGE_ID']}/data",
    headers={"Authorization": f"Bearer {os.environ['DOCPIN_API_KEY']}"},
    json={"revenue": 128400, "customers": 37, "asOf": datetime.datetime.now(datetime.timezone.utc).isoformat()},
)
r.raise_for_status()
```

**From Claude or ChatGPT:** "Update the data on the sales dashboard: revenue 128,400, 37 customers." The app calls `set_page_data`.

## 4. Check it

- Open the page's link: the numbers show, and anyone with it open sees updates within a minute.
- In the DocPin app, the page's **Data** tab shows the current JSON.
- `GET /v1/pages/$PAGE_ID/data` returns it too.

## Variations

- **Private dashboard:** make the page password-protected or people-only (Pro). The data is then just as private.
- **Several sources:** have each job `PATCH` only its own keys (`sales`, `support`, `uptime`), so they never overwrite each other.
- **History or charts:** keep a short array in the data, e.g. the last 30 daily values, and replace it each run. Store long history elsewhere: the limit is 1 MB per page.
