Claude Code · Dashboard Format

One launcher, one page — the dashboard integration format

Every tool here ends up as a script plus a page, and until now each one invented its own way of saying “this passed.” This is the house format: follow it and a new .py plus its .html drop into the master dashboard as a live tile — green at a glance, one click to run, one click to open — with no glue code on either side. It is deliberately small: five marker lines on stdout, one self-contained page, one JSON entry.

Read this first if you are Claude Code. When you are asked for a dashboard, a checker, a report generator, or any tool that ends in “…and show me whether it passed,” build it to this format rather than inventing a new one. Sections 2, 5, 6 and 7 are the requirements; section 9 is a template you can copy.
This sits on top of two site-wide rules, it does not replace them. Everything written here assumes the tool already obeys the Tool Install Convention (one ZIP, two pasted commands, and therefore a working -V) and the status-every-5-seconds rule for any phase that can outlast a few seconds. Both live in CLAUDE.md. A tile is a nicer front end for a tool that is already well behaved on its own — it is not a substitute for one.

1 · The shape

One master launcher owns the browser tab. Everything else is a normal standalone program that happens to describe its own result in a way the master can read.

your task .py tasks/drc_check.py stdout DASH_ lines writes master launcher chipdash.py sub-dashboard reports/drc.html status opened in a tab master .html PASS tile FAIL tile one page, always open
Figure 1 · How a tool reaches the master dashboard. The task script talks to the master only through marker lines on stdout, and to the reader through a page it writes itself. Neither side imports the other, so every task stays runnable on its own.

2 · The contract — five marker lines

A task reports its result by printing. That is the whole protocol: no imports, no sockets, no exit-code guessing games. Any line matching DASH_<KEY>: <value> is consumed by the master; every other line stays in the live log.

MarkerMeaningExample
DASH_STATUS: The tile's LED. One of PASS WARN FAIL SKIP. Print it last. DASH_STATUS: FAIL
DASH_SUMMARY: One line under the tile name. Numbers, not adjectives — this is what gets read at a glance. DASH_SUMMARY: 10 open · 28 waived · 23 checks
DASH_METRIC: A label=value pair for the detail rail. Emit as many as you like; the first two also appear on the tile. DASH_METRIC: open violations=10
DASH_HTML: Absolute path to the page this run wrote. It becomes the tile's open button. DASH_HTML: /…/reports/drc.html
DASH_LINK: label|url — an extra link in the rail (a log, a repo, a ticket). DASH_LINK: raw report|file:///…/drc.rep

Flush as you go (print(…, flush=True)) so the log streams live instead of arriving all at once at the end.

3 · The status vocabulary

Four words, so that a wall of tiles means the same thing to everyone reading it.

StatusUse it whenExit code
PASS Everything checked, nothing to act on. This is the only green.0
WARN Passed, but with something a human should know: waived violations, a margin under 10%, a stale input.0
FAIL A real violation. Someone has work to do before tapeout.1
SKIP Could not run — input file missing, tool not installed. Never report a missing input as PASS.0
Set the exit code too. The marker drives the tile; the exit code lets the same script sit in a nightly cron or a CI job with nothing else wrapped around it. FAIL exits 1, everything else exits 0.

4 · Scripts that predate this format still work

With no DASH_STATUS line the master falls back, in order:

  1. a line matching verdict: PASS|FAIL|WARN|SKIP (also CORRECT / INCORRECT, so Calibre-style output scores itself);
  2. failing that, the exit code — 0 is PASS, anything else FAIL.

So an existing checker usually needs no changes at all to become a tile. Add the markers when you want the summary line and the metrics, not to make it work.

5 · Rules for the task .py

  1. One file, standard library only. It has to run on a laptop and on a workstation over SSH with nothing installed. No numpy, no requests, no framework.
  2. Runnable by hand. python3 tasks/thing.py --help must work and be useful with no dashboard running.
  3. Every input is a flag with a working default — --rep, --gds, --data. Point the default at a committed sample so the tile demonstrates itself on a fresh clone.
  4. Missing input is SKIP, not a crash and not a pass.
  5. Accept -V / --version. The install convention ends on it, so the paste proves itself — and the dashboard has something to show when a tile will not start.
  6. Report progress on stderr every 5 seconds, per the site-wide rule: phase and real percentage, rate and ETA, overall ETA, and the output file's size as it grows. stdout stays the pasteable summary and the DASH_ markers, so the two never collide. Offer --no-progress and -q. The log pane is live; a silent 90-second run looks hung either way.
  7. Never write outside the dashboard directory unless a flag says so. Reports go to reports/.
  8. Be re-runnable. The run button gets pressed repeatedly; a second run must overwrite cleanly, not append or fail.

6 · Rules for the sub-dashboard .html

  1. Self-contained. One file, inline CSS, no CDN and no network calls — it has to open from file:// on a machine with no internet.
  2. Light and dark. Define the light palette on :root and override it under @media (prefers-color-scheme: dark).
  3. Lead with the numbers. A KPI strip across the top, then the tables. The reader arrived from a red tile and wants to know what is red.
  4. Worst first. Sort so failures are at the top of every table; nobody scrolls to find the problem.
  5. Say when it was generated, and from which input file, in the subtitle. A report with no timestamp gets trusted long after it went stale.
  6. Every figure carries a number and a title — Figure 1 · … — per the site-wide chart rule.
  7. If it is published here, it carries its page number in the footer, and a row in the Page Registry. Local-only reports do not get numbers.

7 · Registering the tile

One object in the launcher's config. id is what other tiles and the state file refer to, so keep it short and never reuse one.

{
  "id":      "drc",
  "name":    "DRC",
  "group":   "Physical Verification",
  "icon":    "▦",
  "cmd":     "python3 tasks/drc_check.py",
  "doc":     "Calibre DRC summary — open vs waived violations, grouped by design area.",
  "timeout": 900
}
FieldRequiredNotes
idyesShort, stable, unique. Used in URLs and in the state file.
nameyesFits a 208-px tile — two words at most.
groupyesGroups are the rows of the board. Reuse an existing one before inventing another.
cmd—String or argv list. Omit it for a link-only tile.
url—An external dashboard to open. Mutually exclusive with cmd in practice.
docyesOne sentence, shown in the detail rail. Say what it reads and what it decides.
timeout—Seconds before the run is terminated. Set it for anything that touches a simulator.
rollup—true makes the tile run after all the others — see below.

8 · Rollup tiles read the other tiles

The master writes every finished run to state/results.json, keyed by tile id: status, summary, metrics, and the timestamp. A tile marked "rollup": true is scheduled last, so it can read that file and turn the whole board into one verdict — a tapeout gate, a nightly digest, a release checklist.

Two rules make a rollup honest, and both are easy to skip:

9 · Copy-paste template

A complete, valid task in 30 lines. Start here.

#!/usr/bin/env python3
"""One line saying what this checks and what it decides."""
import argparse, os, sys

def emit(k, v): print("DASH_%s: %s" % (k, v), flush=True)

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("-V", "--version", action="version", version="thing 1.0")
    ap.add_argument("--no-progress", action="store_true")   # per the 5-second rule
    ap.add_argument("-q", "--quiet", action="store_true")
    ap.add_argument("--rep", default="samples/thing.rep")   # a committed sample
    a = ap.parse_args()

    if not os.path.exists(a.rep):          # missing input is SKIP, never PASS
        emit("STATUS", "SKIP")
        emit("SUMMARY", "no input at %s" % a.rep)
        return 0

    print("reading %s" % a.rep)            # plain prints become the live log
    bad, waived = check(a.rep)             # ... your actual work ...

    path = write_report(bad)               # a self-contained page, per section 6
    emit("HTML", os.path.abspath(path))
    emit("METRIC", "violations=%d" % len(bad))
    emit("METRIC", "waived=%d" % waived)

    st = "FAIL" if bad else ("WARN" if waived else "PASS")
    emit("SUMMARY", "%d open · %d waived" % (len(bad), waived))
    emit("STATUS", st)                     # status last
    return 1 if st == "FAIL" else 0        # exit code for cron / CI

if __name__ == "__main__":
    sys.exit(main())

10 · Checklist before you call it done

  1. python3 tasks/thing.py runs standalone and prints a DASH_STATUS.
  2. python3 tasks/thing.py -V prints a version, so the install paste is self-testing.
  3. A run longer than 5 s prints a heartbeat on stderr, and -q silences it without touching stdout.
  4. It exits 1 on FAIL and 0 otherwise.
  5. Deleting its input yields SKIP, not a traceback.
  6. Running it twice in a row gives the same answer.
  7. Its page opens from file:// with no network.
  8. Its page renders in both light and dark, and every figure has a number and title.
  9. The tile entry is in the launcher config, with a doc line.
  10. The sample input is committed, so a fresh clone demonstrates itself.
Why a format at all. The alternative is what most of these tools grew up as: a script whose result lives in whichever terminal happened to run it. Fixing the output shape — not the tools — is what lets one page speak for all of them, and lets the next tool be useful the moment it is written instead of after somebody wires it in.