Operator dashboard

AI Ops Dashboard

Where it all comes together

โš™ Tabs

Show or hide any dashboard tab.

How: Toggle tabs on/off and pick the default tab. Hidden tabs stay installed and can be turned back on anytime โ€” nothing is deleted.

Change the dashboard's color theme.

How: Pick any of the 8 themes โ€” your choice is saved in this browser only (it doesn't change the shared default).

Data updated 10/19/2018, 6:46 PM PDT
Rendered 6/20/2026, 12:11 AM PDT ยท auto-refresh 120s
โ† AI Operations Dashboard docsยทhelp/data-schemas.md

Data schemas

This is the reference for every data file AI Operations Dashboard reads. If your dashboard panel doesn't render or npm run check:data is failing, this is the page that tells you what shape the file expects.

Every schema below is enforced by `scripts/validate-data.mjs`. That validator runs as part of npm run build via the check:data step โ€” so a malformed data file blocks the build. To run it on its own:

npm run check:data

The script prints a โœ“ or โœ— per file, lists exact errors, and exits 1 if anything failed.


data/pending.json

What's open. Editable in any text editor. The Pending panel renders one card per item, sorted by priority then dateAdded.

interface PendingFile {
  items: PendingItem[]
}

interface PendingItem {
  /** Required. The card headline. */
  title: string
  /** Optional. Stable slug for cross-referencing. */
  id?: string
  /** Optional. One short paragraph: why this matters. */
  why?: string
  /** Optional. high | medium | low. Drives sort order + tint. */
  priority?: "high" | "medium" | "low"
  /** Optional. Who's executing. */
  doer?: "you" | "ai" | "team" | "external" | "claude" | "human" | "mixed"
  /** Optional. Repo path the item lives under. */
  ref?: string
  /** Optional. YYYY-MM-DD. */
  dateAdded?: string
  /** Optional. Ordered checklist rendered as a numbered list. */
  steps?: string[]
  /** Optional. Free-form filter tags. */
  tags?: string[]
  /** Optional. Link back to a sprint sub-task id (e.g. "S3.1"). */
  sprintId?: string
}

Example:

{
  "items": [
    {
      "id": "wire-stripe",
      "title": "Wire up Stripe for paid tier",
      "why": "Free signups have no path to convert. Need checkout + webhook before launch.",
      "priority": "high",
      "doer": "you",
      "dateAdded": "2026-05-15",
      "steps": [
        "Create Stripe account (test mode)",
        "Add STRIPE_SECRET_KEY + STRIPE_WEBHOOK_SECRET to .env",
        "Build /api/checkout/session route",
        "Build /api/checkout/webhook route"
      ],
      "tags": ["billing", "launch-blocker"]
    }
  ]
}

Common mistakes

  • Forgetting title. The validator rejects items without a non-empty title.
  • Using priority: "urgent" or priority: "p1". Only high / medium / low are accepted.
  • Putting steps as a single string instead of an array of strings.

data/usage.json

AI spend rollup. Powers the watchdog banner at the top of the dashboard and the Usage panel. Regenerate via npm run data:aggregate-usage (rolls data/usage-log.jsonl into this file).

interface UsageFile {
  /** Required. Spend (USD) since 00:00 local today. */
  spendToday: number
  /** Required. Spend (USD) for the current calendar month. */
  spendThisMonth: number
  /** Required. Spend (USD) for the current quarter. */
  spendThisQuarter: number

  /** Optional. Human labels for the banner ("May 2026", "2026-Q2"). */
  monthLabel?: string
  quarterLabel?: string
  /** Optional. ISO-8601 timestamp of the last aggregation. */
  lastUpdated?: string

  /** Optional. Banner color thresholds in USD. */
  thresholds?: {
    dailyYellow?: number
    dailyRed?: number
    [k: string]: number | undefined
  }

  /** Optional. Per-day rollup for the spend sparkline. */
  daily?: Array<{
    date: string // YYYY-MM-DD
    spend: number
    tokens?: number
  }>

  /** Optional. Per-model rollup. Two accepted shapes. */
  byModel?:
    | Array<{ model: string; spend: number; tokens?: number }>
    | Record<string, number | { spend: number; tokens?: number }>
}

Example:

{
  "spendToday": 0.42,
  "spendThisMonth": 19.374,
  "spendThisQuarter": 30.809,
  "monthLabel": "May 2026",
  "quarterLabel": "2026-Q2",
  "lastUpdated": "2026-05-23T16:11:35.839Z",
  "thresholds": { "dailyYellow": 1, "dailyRed": 10 },
  "daily": [
    { "date": "2026-05-22", "spend": 1.08, "tokens": 92400 },
    { "date": "2026-05-23", "spend": 0.42, "tokens": 38100 }
  ],
  "byModel": [
    { "model": "claude-opus-4-7", "spend": 17.463, "tokens": 893200 },
    { "model": "claude-sonnet-4-6", "spend": 12.121, "tokens": 2214700 }
  ]
}

Common mistakes

  • Writing dollar amounts as strings ("19.37") instead of numbers (19.37). The validator only accepts finite numbers.
  • Putting thresholds at the top level instead of nested. It must be thresholds: { dailyRed: 10 }, not dailyRed: 10.
  • Mixing the two byModel shapes within a single file. Pick one and stick with it.

data/scheduled-jobs.json

Every recurring job that touches the project โ€” cron, git hooks, scheduled agents, manual reviews. The Activity panel renders this file unchanged.

interface ScheduledJobsFile {
  jobs: ScheduledJob[]
}

interface ScheduledJob {
  /** Required. Stable slug. Must be unique across jobs. */
  id: string
  /** Required. Human-readable display name. */
  name: string
  /** Required. When it fires ("daily 04:00 PT", "every git push", "weekly Sunday"). */
  cadence: string
  /** Required. One sentence โ€” what the job does + why it exists. */
  purpose: string

  /** Optional. cron | git-hook | manual | gha | etc. */
  kind?: string
  /** Optional. ISO-8601 timestamp of the most recent run. */
  lastRun?: string
  /** Optional. ok | error | skipped. */
  lastStatus?: string
}

Common mistakes

  • Duplicate id across two entries โ€” the validator flags this; the dashboard would render only one card.
  • Forgetting purpose. If you can't write one sentence about why this job exists, you don't need the job.

data/recent-commits.json

Frozen git log snapshot. The Activity panel reads this file because serverless platforms don't ship .git. Regenerate via npm run data:freeze-commits.

interface RecentCommitsFile {
  /** Optional. ISO-8601 timestamp of the freeze. */
  generatedAt?: string
  /** Optional. The `--since` window used (e.g. "180 days ago"). */
  since?: string
  commits: Commit[]
}

interface Commit {
  hash: string         // full sha
  shortHash: string    // 7-char sha
  date: string         // YYYY-MM-DD
  timestamp: string    // ISO-8601
  author: string
  subject: string      // first line of the commit message
}

Common mistakes

  • Committing a stale recent-commits.json from a shallow clone. If you have a pre-push hook that runs data:freeze-commits, the local file is always richer than the CI clone โ€” the freeze script should refuse to overwrite a richer dataset.
  • Missing shortHash because you generated the file by hand. Use the script.

data/connected-repos.json

Registry of repositories linked into AI Operations Dashboard. Each entry describes one external repo that emits signals (pending items, usage logs, commit freezes, etc.) into the AI Operations Dashboard. The Connected Repos panel renders one card per entry.

interface ConnectedReposFile {
  repos: ConnectedRepo[]
}

interface ConnectedRepo {
  /** Required. Stable slug. Must be unique across the file. */
  id: string
  /** Required. Display name shown on the card. */
  name: string
  /** Required. Absolute filesystem path on the host machine. Must start with "/". */
  path: string
  /** Required. ISO-8601 date string โ€” when the repo was linked. */
  linkedAt: string
  /** Required. List of signal kinds the repo emits (e.g. "pending", "usage", "commits"). */
  emitSignals: string[]

  /** Optional. ISO-8601 timestamp of the most recent emission. May be null. */
  lastReportedAt?: string | null
  /** Optional. Non-negative integer โ€” how many writes this repo has produced. */
  writeCount?: number
  /** Optional. Path to a frozen commits snapshot for this repo. May be null. */
  commitsSnapshotPath?: string | null
}

Example:

{
  "repos": [
    {
      "id": "woodwiki",
      "name": "Woodwiki",
      "path": "/Users/ahamade/Documents/GitHub/woodwiki",
      "linkedAt": "2026-06-02T14:30:00Z",
      "emitSignals": ["pending", "usage", "commits"],
      "lastReportedAt": "2026-06-02T18:11:42Z",
      "writeCount": 47,
      "commitsSnapshotPath": "/Users/ahamade/Documents/GitHub/woodwiki/data/recent-commits.json"
    }
  ]
}

Common mistakes

  • Using a relative path (./woodwiki or ../woodwiki). Paths must be absolute and start with / โ€” the validator rejects anything else. AI Operations Dashboard runs from a different working directory than its connected repos, so relative paths can't be resolved consistently.
  • Forgetting `emitSignals`. Even if the list is empty for now, the field must be present as an array. The dashboard uses it to decide which panels to surface for the repo.
  • Writing `linkedAt` as a unix timestamp (1717340400) instead of an ISO date string ("2026-06-02T14:30:00Z"). The validator only accepts ISO-8601 / YYYY-MM-DD form.
  • Duplicate `id` across two entries. The validator flags this; the dashboard would render only one card.
  • `writeCount` as a string ("47") or negative number. Must be a non-negative integer.

docs/operator-log.md

Chronological work log, newest entry on top. Parsed live by the Operator Log panel.

Required heading shape (per entry):

## YYYY-MM-DD HH:MM โ€” short title

**Why:** one sentence.

**What shipped:** what landed.

**Tags:** comma-separated tags

**Next:** next concrete step.

The validator checks that at least one entry exists (a ## YYYY-MM-DD โ€ฆ heading). A file with zero entries emits a warning, not an error โ€” but the panel will render empty.

Common mistakes

  • Using ### (level-3) headings. The parser only counts ##.
  • Hyphen instead of em-dash. Either works for the file to parse, but โ€” is the convention.
  • Entry order. Newest goes on top, always.

knowledge/postmortems/*.md

One file per incident. The Postmortems panel walks this directory, parses frontmatter, and renders severity-tinted cards.

Required shape (frontmatter + body):

---
title: AI spend leaked through env var; $40 burned overnight
date: 2026-05-12
severity: p1
---

# AI spend leaked through env var; $40 burned overnight

## What happened
โ€ฆ

## Root cause
โ€ฆ

## Fix
- Commit hash + summary

## Prevention
- Concrete steps (each should become a Pending item)

| Field | Required? | Notes | |---|---|---| | title | yes | The card headline. | | date | yes | YYYY-MM-DD. | | severity | recommended | p1 / p2 / p3. Validator warns if missing. |

Common mistakes

  • Forgetting the closing --- on the frontmatter. The parser silently fails to read frontmatter at all โ†’ validator marks the file as missing title and date.
  • Putting the title in the body but not the frontmatter. The panel reads frontmatter.

plans/execution-status.md

Live execution state for the Plan panel. Hero panel reads **Last update:**, **Current state:**, **Next sub-task on resume:**. The sub-task ledger table drives the per-sprint boards.

Required shape (excerpt):

# Execution status

**Last update:** 2026-05-18 11:00 PDT
**Current state:** v0.1 scaffold complete ยท docs + landing in flight
**Next sub-task on resume:** S1.7 โ€” record 90-second demo video

## Sub-task ledger

| ID | Sub-task | Date | Commit | Status |
|---|---|---|---|---|
| S1.1 | Folder scaffold | 2026-05-16 | a1b2c3cf | โœ“ shipped |

The validator checks:

  • **Last update:** exists (error if missing โ€” the dashboard hero relies on it).
  • A markdown table with header ID | Sub-task | โ€ฆ | Status exists (warning if missing).

Common mistakes

  • Renaming the field to **Updated:** or **Last updated:**. The dashboard parses the exact string Last update.
  • Dropping the ID column from the ledger table. The 5-column form is required.

data/savings.json

Generated by `scripts/build-savings.mjs` (the Token Savings collector). It reads the global claude-mem SQLite DB (via the sqlite3 CLI) and per-repo Graphify (graphify benchmark + graphify-out/cost.json), computes token/dollar savings, and writes a committed, sanitized snapshot. Derived โ€” never hand-edit. Refresh with npm run data:collect-savings, the data:build chain, or the auto-installed SessionStart hook.

type SavingsData = {
  generatedAt: string          // UTC ISO with Z
  generatedAtLocal: string
  config: {
    dollarsPerMillionTokens: number
    recallTokensPerObservation: number
    graphifyAssumedQueriesPerDay: number
    scopeMode: string          // "global" | "projects"
    scopeProjects: string[]
    scopeExclude: string[]     // deny-list / privacy control
  }
  combined: { savedTokens: number; savedDollars: number }   // claude-mem only (the hard number)
  cloudMem: {
    observations: number; workTokens: number; distinctDays: number
    recallTokensPerObservation: number; recallTokens: number
    savedTokens: number; savedDollars: number; efficiencyPct: number   // capped 99.9, never 100
    byProject: { project: string; observations: number; workTokens: number; inRepo: boolean }[]
    daily: { day: string; observations: number; workTokens: number; cumulativeWorkTokens: number }[]
    dbFound: boolean
  } | null                     // null when the claude-mem DB is absent
  graphify: {
    corpusWords?: number|null; corpusTokensNaive?: number|null; nodes?: number|null; edges?: number|null
    avgQueryTokens?: number|null; reductionFactor?: number|null
    savedTokensPerQuery: number|null; savedDollarsPerQuery: number|null
    build: { totalInputTokens: number; totalOutputTokens: number; runs: number; lastBuilt: string|null } | null
    buildTokens: number|null; buildCostDollars: number|null
    notDetected?: boolean; reason?: string
  } | null
  graphifyHistory: { day: string; reductionFactor: number|null; corpusTokensNaive: number|null;
                     avgQueryTokens: number|null; nodes: number|null; edges: number|null; buildTokens: number|null }[]
  errors: string[]             // collector is fail-safe: problems land here, exit code stays 0
}

Privacy: the snapshot is machine-global by default โ€” it lists every project name in the claude-mem DB (this is a private repo, so that's accepted; see the operator decision). Trim specific projects with scope.exclude in .ai-ops-dashboard/savings.config.json. The absolute DB path is never written โ€” only cloudMem.dbFound (a boolean).

Common mistakes

  • Hand-editing the numbers โ€” it's regenerated; edit .ai-ops-dashboard/savings.config.json instead.
  • Letting efficiencyPct reach 100 โ€” it is capped at 99.9 by design (the validator rejects โ‰ฅ100).
  • Adding a config.dbPath โ€” forbidden (privacy leak); the validator rejects it.
  • Treating a missing file as fatal โ€” the collector needs the local claude-mem DB, so on CI/Vercel the

file simply may be stale; validateSavings reports a warning, not an error.

data/savings-history.json

A bare JSON array (one row per day) of Graphify reduction snapshots that accrues over time โ€” the only savings file that keeps history (claude-mem daily is rebuilt from the DB each run). Upserted by day, sorted ascending. Same row shape as SavingsData.graphifyHistory[]. Derived โ€” never hand-edit.


Adding a new schema

When you ship a new dashboard panel that reads a data file:

  1. Add the file shape to this doc (interface block + example + common mistakes).
  2. Add a validateX() function to `scripts/validate-data.mjs`.
  3. Register it in the validators array inside main().
  4. Run npm run check:data to confirm both the happy path and an intentionally-broken file are caught.

The convention is one validator function per file or per directory, returning a result object with errors[] + warnings[]. Don't reach for a third-party JSON-schema library โ€” the validator is meant to be readable and edited by anyone with five minutes of Node experience.

Data schemas