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:06 AM PDT ยท auto-refresh 120s
โ† All docsยทdocs/origin-build-log.md

> Origin document. This is the complete build log of the original Woodwiki Operator Dashboard โ€” the single-file implementation that the AI Operations Dashboard product was built from. It is preserved here as an architectural reference: every design decision, data contract, UI convention, and incident lesson from the original is captured below. The canonical source lives at woodwiki/docs/operator-dashboard-build-log.md; this copy is the product team's reference.


Woodwiki Operator Dashboard โ€” Complete Build Log

> Purpose of this document. A fully self-contained reference for recreating the Woodwiki > Operator Dashboard in a new codebase. Every section answers three questions: Why was this > built? How was it built? What did it produce? Design rationale, UI decisions, data > contracts, incident lessons, and current state are all captured here.


Table of Contents

  1. Origin & Purpose
  2. Technical Architecture
  3. Security & Privacy
  4. Data Layer โ€” All Sources
  5. Feature Timeline (Chronological)
  6. Tab Reference โ€” Why / How / What
  7. Component Inventory
  8. Prebuild Audit Pipeline
  9. Incidents & Postmortems
  10. Current State (2026-06-04)
  11. Key Conventions & Rules

1. Origin & Purpose

1.1 Why It Was Built

The dashboard was conceived during the initial SEO audit on 2026-05-01 as "Phase 5 โ€” Measurement." The audit revealed that every other SEO phase (fix, content, infrastructure, outreach) would drift without a dedicated monitoring surface. The design goal was captured in one sentence from the very first session log:

> "One screen for every operator question. Where is the SEO master plan? What shipped today? > Which spokes are live, which are stuck? What's the long-tail strategy and how far along are we? > What audits exist? What's left to do?"

Before the dashboard existed, answering any of these questions required reading multiple scattered markdown files by hand. The operator (Ahmed) would need to open plans/seo-master-plan/seo-master-plan-roadmap.md, plans/seo-master-plan/execution-status.md, docs/operator-log.md, and several JSON files just to understand the current project state.

The dashboard replaced that cognitive load with a single auto-refreshing page.

1.2 Core Design Philosophy (Decided on Day 1)

Four principles were established at the start and never changed:

| Principle | What it means | Why it was chosen | |---|---|---| | One screen | Every operator question has an answer on this page | Eliminate context-switching between markdown files | | Live, not cached | Every section reads from the filesystem at request time | No stale snapshots โ€” the page is always current | | Self-updating by rule | CLAUDE.md mandates docs/operator-log.md update after every substantive session | The dashboard stays accurate without manual page edits | | Privacy-by-obscurity | Route is in /preview/, excluded from robots.txt/sitemap, noindex, nofollow | Internal tool โ€” not public, not indexed, not shared |

1.3 Relationship to the Content Pipeline

The dashboard is not just a reporting surface โ€” it is the control plane for the entire SEO content pipeline. Every pipeline runner (cornerstone, long-tail, guide, diagram, densify-links) writes its status to files the dashboard reads. The PENDING tab is the canonical task queue. The Activity tab is the commit history. The Postmortems tab is the institutional memory. The Usage tab is the cost watchdog.

The dashboard makes it possible to run the pipeline unattended (via Cowork scheduled tasks) and check in once a day to understand what happened.


2. Technical Architecture

2.1 Stack

| Layer | Choice | Why | |---|---|---| | Framework | Next.js 16 App Router | Project-wide choice; App Router enables server-side filesystem reads at request time | | Rendering | export const dynamic = "force-dynamic" | Every page load re-reads all files โ€” no stale data ever | | Language | TypeScript (strict) | Project-wide standard | | Styling | Tailwind CSS 4 (CSS-first, no tailwind.config.js) | Project-wide standard | | Auto-refresh | <meta http-equiv="refresh" content="120"> | Passive refresh without JavaScript; works even if the tab is left open | | Auth | HTTP Basic Auth via Next.js middleware.ts | Simple, robust, no auth library needed for an internal tool |

2.2 File Location

app/
  preview/
    operator/
      page.tsx           โ† 3,534 lines โ€” the entire dashboard
    seo-monitor/
      page.tsx           โ† 743 lines โ€” older, simpler sprint-only monitor (predates dashboard)

The seo-monitor page was the original prototype. The operator dashboard (page.tsx) replaced it as the primary surface. Both still exist; the operator dashboard links back to seo-monitor in its footer.

2.3 Component Architecture

The page uses a hybrid model:

  • All tab layout functions are defined inline in page.tsx (they are server components โ€”

they read files and return JSX, but they are not exported as separate files)

  • Client components (those needing useState, useEffect, or interactive chart rendering)

are extracted into components/:

| Component | Location | What it does | |---|---|---| | OperatorDocsTable | components/operator-docs-table.tsx | Filterable/sortable docs table | | OperatorPendingList | components/operator-pending-list.tsx | Interactive pending task list | | OperatorSubTaskLedger | components/operator-sub-task-ledger.tsx | Sortable sub-task ledger | | PipelineMonitor | components/pipeline-monitor.tsx | Live pipeline run status | | ScoreTimeline | components/score-timeline.tsx | SEO audit score sparklines | | PsiTimeline | components/psi-timeline.tsx | PageSpeed Insights diff cards + sparklines | | OperatorCommitTimeline | components/operator-commit-timeline.tsx | Searchable/filterable commit feed | | PostmortemsTab | components/postmortem-tab.tsx | Collapsible severity-tinted incident cards |

Decision rationale: Keeping tab functions inline avoids file proliferation while client components are extracted when interactivity requires "use client". The rule of thumb: if a tab only reads data and renders markup, it stays inline. If it needs browser APIs or user interaction, it becomes a component.

2.4 Rendering Flow

Request arrives at /preview/operator?tab=<name>
       โ†“
middleware.ts โ€” Basic Auth check (OPERATOR_USER + OPERATOR_PASSWORD env vars)
       โ†“
page.tsx โ€” force-dynamic server render
       โ†“
Parallel filesystem reads:
  - plans/seo-master-plan/*.md         (Master Plan, Sprints tabs)
  - cornerstone-spokes/ tree           (Spokes tab)
  - git log (or data/recent-commits.json fallback)  (Activity tab)
  - data/operator-pending.json         (Pending tab)
  - data/scheduled-jobs.json           (Daily SEO tab)
  - data/anthropic-api-usage.json      (Usage tab, top banner)
  - pipeline/runs/gsc-weekly/*.json    (Daily SEO tab, Trends tab)
  - data/disavow.txt                   (Daily SEO tab)
  - data/spoke-backlog-ranking.json    (Spokes tab)
  - data/psi-history.json              (Trends tab)
  - data/score-history.json            (Trends tab)
  - data/pipeline-runs-snapshot.json   (Pipelines tab)
       โ†“
Render HTML with <meta refresh="120">
       โ†“
Response sent โ€” no client-side data fetching

2.5 Auto-Refresh Strategy

Why `<meta http-equiv="refresh">` instead of `setInterval`/polling?

Decision made on 2026-05-07 during the Pipelines tab build. Options considered:

  1. setInterval with fetch() โ€” requires JavaScript, state management, loading states
  2. <meta refresh> โ€” pure HTML, works in any browser state, works on mobile, zero JavaScript
  3. WebSocket/SSE โ€” massive overkill for an internal tool

<meta refresh> was chosen because the dashboard is read-only monitoring. A full page reload every 120 seconds is acceptable for an operator check-in screen; it gives a fresh server render with all updated data.


3. Security & Privacy

3.1 Why It Needed Auth

The operator dashboard exposes business-sensitive information: sprint status, pending tasks, backlink strategy, SEO scores, API spend, pipeline state. It's a /preview/ route that should never be accidentally indexed or discovered.

3.2 How Auth Was Implemented (2026-05-06)

Session: 2026-05-06-preview-routes-basic-auth-gate-activated

The auth code had been written in middleware.ts from the beginning but the env vars were not set. The 2026-05-06 session activated it:

  1. OPERATOR_USER and OPERATOR_PASSWORD set in Vercel (Production + Preview scopes, marked

Sensitive)

  1. Triggered a Vercel redeploy
  2. Verified live: https://www.woodwiki.org/preview/operator returns 401 without creds, 200

with creds

  1. data/operator-pending.json auth-gate item removed (count: 15 โ†’ 14)

Why Basic Auth over a proper session/OAuth?

Decision: Basic Auth is sufficient for an internal single-operator tool. The complexity of implementing OAuth or session management is not justified. The route is also excluded from robots.txt and the sitemap, and carries noindex, nofollow meta tags โ€” discovery is extremely unlikely.

3.3 Watchdog (2026-05-06, same session)

Immediately after activating auth, a GitHub Actions watchdog was added: .github/workflows/operator-gate-watchdog.yml

It runs at 9:00 UTC and 21:00 UTC and asserts:

  • curl without credentials returns 401
  • curl with credentials returns 200
  • The public homepage returns 200 (regression guard)

Why a watchdog? A misconfigured Vercel env var or middleware regression could silently expose the dashboard. The watchdog catches this within 12 hours and pages via GHA failure notification.

3.4 Privacy Layers

| Layer | Mechanism | |---|---| | Not indexed | robots.txt excludes /preview/ | | Not in sitemap | Sitemap generation skips /preview/ routes | | Not linked from public pages | No public-facing HTML links to /preview/* | | noindex + nofollow | <meta name="robots" content="noindex, nofollow"> on all preview pages | | HTTP Basic Auth | middleware.ts gate on all /preview/* routes | | GHA watchdog | Twice-daily assertion that the 401 still fires |


4. Data Layer โ€” All Sources

Every data source the dashboard reads, what feeds it, and what panel consumes it.

4.1 docs/operator-log.md

What: Chronological work log, newest entry first. 1,437+ lines (66+ entries).

Why: The self-updating discipline. After every substantive session, the operator (Ahmed) appends an entry. The dashboard reads this file at request time, parses the entries, and surfaces the most recent ones in the Overview tab's "What shipped recently" panel.

Format contract:

## YYYY-MM-DD HH:MM โ€” title
**Why:** ...
**What shipped:** ...
**Tags:** TAG1 TAG2 TAG3
**Next:** ...

Who writes it: Every Claude Code session that ships substantive work. CLAUDE.md mandates it as non-negotiable. The rule: "Operator-log discipline is non-negotiable. Skipping it stales the live dashboard."

Parsing: The page does simple line-by-line parsing โ€” splits on ## headers, extracts the date/title, then scans for **Why:**, **What shipped:**, **Tags:**, **Next:** fields.


4.2 plans/seo-master-plan/seo-master-plan-roadmap.md

What: High-level strategic plan. Sprint sections become sprint cards; sub-task tables become the ledger.

Why: The "Master Plan" tab and "Sprints" tab both read this file. It contains the canonical list of all sprints (Era 1: Sprints 0โ€“7, archived; Era 2: Sprints 8โ€“15, current).

Format contract: Sprint sections use ## Sprint N โ€” Title headers. Sub-task tables use pipe-delimited markdown.

Who writes it: Only when sprint scope itself changes โ€” new sub-task added, sprint introduced, sprint marked complete and archived.


4.3 plans/seo-master-plan/execution-status.md

What: Live execution state. 286 lines. Updated by every work session.

Why: The hero panel at the top of the Master Plan tab reads three fields from this file: **Last update:**, **Current state:**, and **Next sub-task on resume:**. These three lines tell Ahmed exactly where work left off.

Format contract:

  • **Last update:** โ€” date + summary
  • **Current state:** โ€” one to three sentences on current situation
  • **Next sub-task on resume:** โ€” the literal next task to start
  • ## Sprint progress โ€” pipe-delimited table (Sprint | Status | Notes)
  • ## Sub-task ledger โ€” pipe-delimited table (ID | Sub-task | Date | Commit | Status)
  • ## Decision log โ€” pipe-delimited table (Date | Decision | Rationale)

Sub-task ledger schema (enforced in prebuild since 2026-05-08):

| ID | Sub-task | Date | Commit | Status |

ID shapes: S<n>.<n>, S<n>.<n><letter>, S<n>.<n>v<version>, S<n>-<ALPHA>, BATCH-YYYY-MM-DD-<n>, SPRINT-<n>.

Status markers: โœ… (shipped), ๐ŸŸก queued, ๐ŸŸก in progress, โธ (deferred), partial, ๐Ÿ batch, ๐Ÿ”ต recurring.


4.4 data/operator-pending.json

What: 26 pending items (as of 2026-06-04) across Sprints S3, S4, S8, S9, S10, S13, S15.

Why: The Pending tab reads this JSON file directly. It replaced a hardcoded array in page.tsx. The extraction happened on 2026-05-05 during the "park Marcel" session, where the pending list needed to be rebuilt from scratch without a full page rewrite.

Schema per item:

{
  "title": "Imperative title",
  "priority": "high|medium|low",
  "doer": "claude|human|mixed",
  "why": "One sentence rationale",
  "ref": "optional/file/path.md",
  "sprintId": "S15.68",
  "dateAdded": "2026-05-08",
  "steps": ["Step 1", "Step 2"]
}

`steps` field: Added for human and mixed items so Ahmed knows exactly what to click, paste, or sign up for. Rendered as a numbered list in the UI.

Lifecycle: Add item when work is identified. Delete item when shipped. Never mark as "done" and keep โ€” delete it.


4.5 data/scheduled-jobs.json

What: 11 jobs โ€” GitHub Actions crons, Cowork SKILLs, git hooks, Vercel builds, manual recurring tasks.

Why: Added during Sprint 15 Wave 2 (2026-05-12) as part of the scheduled-jobs dashboard panel on the Daily SEO tab. Before this file existed, scheduled jobs were documented only in .github/workflows/ filenames and memory โ€” there was no single "what is automated?" surface.

Schema per job:

{
  "_schema": { ... field docs ... },
  "jobs": [
    {
      "id": "external-link-rot-scan",
      "name": "External-link-rot scan",
      "type": "gha|cowork|git-hook|vercel|manual-recurring",
      "schedule": "Weekly Sunday 04:00 PT",
      "status": "active|decommissioned",
      "description": "...",
      "lastRun": "ISO datetime or null",
      "nextRun": "ISO datetime or null",
      "notes": "..."
    }
  ]
}

4.6 data/anthropic-api-usage.json

What: Self-instrumented API cost tracking. Updated by two mechanisms: daily Cowork SKILL scrape (manualHistorical[] + manualByModel{}), and in-repo API-mode scripts that call recordUsage() from lib/anthropic-billing.mjs.

Why: Anthropic's Admin API (the only programmatic billing source) is gated to Team/ Enterprise plans. The Max plan has no programmatic billing access. So this file is populated by scraping console.anthropic.com (Cowork SKILL runs daily at 04:00 PT) and by self-instrumentation in scripts.

Why this matters to the dashboard: A $93+ incident on 2026-05-07 (the woodwiki-extraction API key left in shell env, parallel runners billing the API instead of the Max plan) was the forcing function. The watchdog banner at the top of every tab shows: TODAY $X.XX, JUNE 2026 $X.XX, Q2 2026 $X.XX. Color thresholds: green = $0, yellow = <$10, red = โ‰ฅ$10 per day. The full Usage tab shows per-model breakdown and daily spend chart.

Current values (2026-06-04): Today $0.00, June $0.00, Q2 $201.27 (the $201.27 is historical including the $112.25 incident day on 2026-05-07).


4.7 data/recent-commits.json

What: Frozen git log snapshot โ€” last 90 days of commits.

Why: Vercel builds on a shallow clone (fetch-depth: 1), so git log at request time returns at most one commit. The pre-push hook (scripts/pre-push-freeze.mjs) regenerates this file before every push so the Activity tab always shows full history.

Fallback: In local dev, page.tsx tries git log directly first. Only on Vercel (where git is unavailable at request time) does it fall back to this file.

Format: Array of commit objects: { hash, date, message, author, files[] }.

Pre-push hook behavior: If data/recent-commits.json changed after regeneration, the hook auto-creates a [freeze] data/recent-commits.json refresh before push commit so both the work commit and the freeze commit ship together. This causes the well-known "two pushes required" behavior โ€” first push triggers the freeze commit, second push sends it.


4.8 data/spoke-backlog-ranking.json

What: Priority-ranked view of all unwritten cornerstone spokes. 60 spokes total (59 router, 1 table-saws). Score range 0โ€“100.

Why: The Spokes tab shows the backlog in priority order so Ahmed can pick the highest-ROI spoke to write next without manually consulting multiple files.

Scoring formula:

  • Structural importance (0โ€“40): by article_shape (Cornerstone Hub > Deep Spoke > Standard

Spoke > Reference > Quick Answer) + 15 bonus if parent cornerstone >100 impressions/week

  • Topical demand (0โ€“30): GSC query-token overlap with the slug, impressions-weighted
  • Assignment age (0โ€“20): >60 days in backlog gets the boost
  • Stage (0โ€“10): 02-ready-to-start over 01-backlog

Regenerated by: pnpm spoke-backlog (NOT wired into prebuild โ€” too slow). Manually run before consulting the Spokes tab for fresh rankings.


4.9 data/score-history.json

What: 8 SEO audit composite-score snapshots (v1 through v8), 2026-05-02 to 2026-05-05. Composite scores: 71 โ†’ 70 โ†’ 73 โ†’ 75 โ†’ 75 โ†’ 84 โ†’ 86 โ†’ 88.

Why: The Trends tab shows a sparkline chart of the SEO audit score over time. Each snapshot records: composite, technical, content, onPage, schema, perf, geo, visual, SXO, backlinks, longTailScore.

Latest (v8): composite 88, technical 88, content 90, onPage 88, schema 91, perf 84, geo 91, visual 93, SXO 72, backlinks 18, longTailScore 8.


4.10 data/psi-history.json

What: PageSpeed Insights snapshots for the Woodwiki homepage (mobile).

Why: Added during the Sprint 15 performance cleanup session on 2026-05-12. Two snapshots exist: before cleanup (perf 90, LCP 3.3s) and after cleanup (perf 94, LCP 2.9s). The Trends tab shows these as diff cards + sparklines.

Format per snapshot:

{
  "date": "2026-05-12",
  "label": "Baseline (before cleanup)",
  "performance": 90,
  "accessibility": 100,
  "bestPractices": 100,
  "seo": 100,
  "lcp": 3.3,
  "fcp": 1.2,
  "tbt": 40,
  "cls": 0
}

4.11 data/pipeline-runs-snapshot.json

What: Snapshot of active/recent pipeline run states.

Why: The Pipelines tab needs to work on Vercel (no filesystem access to .claude/ pipeline-logs/ which is local-only). Solution: pipeline runners write heartbeat updates to this file every ~60 seconds; the page reads it as a fallback. Local dev reads the live log files directly.

Generated by: scripts/write-pipeline-snapshot.mjs (called by pipeline runners at heartbeat intervals).


4.12 data/disavow-gsc-upload-state.json

What: Last successful GSC disavow upload state: timestamp, run ID, total domains uploaded.

Why: The DisavowStatusPanel on the Daily SEO tab needs to know when the last successful disavow upload happened. The Cowork SKILL that performs the upload writes this file after each successful submission.

Current value: Last upload 2026-06-02T11:47:56.182Z, 238 total domains.


4.13 pipeline/runs/gsc-weekly/<date>.json

What: Weekly GSC performance snapshots. Used by the priority queue scorer and the Daily SEO tab's "Weekly GSC snapshot" panel.

Why critical: The validate-gsc-weekly-freshness.mjs prebuild validator (strict-flipped 2026-06-02) blocks Vercel builds if the newest tracked snapshot is more than 10 days old OR if there is an on-disk-but-untracked snapshot (the S15.84 failure mode where a stale git add deleted the file from git).

Generated by: GitHub Actions cron (gsc-weekly-snapshot.yml) โ€” runs weekly Monday 02:00 PT.


4.14 data/gsc-priority-pinned.json

What: Manually pinned URLs that always appear at the top of the GSC priority queue, regardless of rank score.

Why: Some canonical-fragmentation campaigns need sustained daily GSC submission beyond what the score-based queue would naturally surface. Pins let Ahmed force-prioritize specific URLs for a defined period.

Current state: pinned: [] โ€” empty after the S15.80 audit retired 4 completed slugs.


5. Feature Timeline (Chronological)

5.1 Phase 0 โ€” Concept (2026-05-01)

Session: 2026-05-01-seo-audit.md

Why: The v5 full-site SEO audit identified measurement as the missing layer. The plan table described: "Phase 5: Measurement โ€” weekly health check, GSC trend, GEO citation tracking. ~2 days eng. 3โ€“5 days then ongoing."

What existed: Nothing. The /preview/operator route did not exist.


5.2 Phase 1 โ€” First Commit (2026-05-02 era)

Commit: 148cf791 โ€” Operator dashboard at /preview/operator + live operator-log + project rule

Why: Ahmed needed a single page to check current sprint status, what shipped recently, and what was left to do.

What shipped:

  • The route /preview/operator with initial tabs: Overview, Master Plan, Content, Activity,

Docs, Pending

  • docs/operator-log.md created as the self-updating work log
  • CLAUDE.md updated with the "commit and push at session end" rule

Design choices at launch:

  • Server-side rendering with no client-side fetching (filesystem reads at request time)
  • All content from markdown files (no database)
  • noindex, nofollow + excluded from robots.txt + sitemap

5.3 Phase 2 โ€” Overhaul & GSC Integration (2026-05-05)

Sessions: 2026-05-05-sprint-3-overnight-quickwins, 2026-05-05-park-marcel, multiple v5 audit sessions

Why: The initial dashboard was functional but rough. Multiple tabs needed fixing. The PENDING list needed to become a JSON-driven data structure (not hardcoded in the TSX). The Daily SEO tab needed a GSC data source.

What shipped:

  • Commit `5978632e`: Operator dashboard overhaul โ€” master-plan/content/activity/docs tabs

all fixed

  • Commit `59ce763d`: Hub descriptions evergreen + master-plan archive collapsible
  • Weekly GSC snapshot card added to Daily SEO tab (Sprint 3 Batch 1) โ€” KPI cells, trend

arrows, collapsible top-query/page tables

  • PENDING list extracted to `data/operator-pending.json` โ€” decoupled from the TSX so it

can be edited without a rebuild

  • PENDING list rebuilt to include backlinks recovery, Wikidata, long-tail phases

Decision: PENDING list โ†’ JSON file

> Why: The pending list was being modified every session (items added, items completed, > statuses updated). Keeping it hardcoded in the TSX meant every change required a code edit > and a full rebuild. Extracting to JSON let the Cowork scheduled tasks and Claude Code sessions > update it without touching production code.


5.4 Phase 3 โ€” Security + Score History (2026-05-06)

Session: 2026-05-06-preview-routes-basic-auth-gate-activated

Why: The operator dashboard contained sensitive business data. The auth gate code existed in middleware.ts but env vars were not set.

What shipped:

  • Basic Auth gate activated
  • OPERATOR_USER + OPERATOR_PASSWORD set in Vercel
  • GHA watchdog workflow (operator-gate-watchdog.yml) added

Also shipped (same era):

  • Commit `97cc5cba`: Trends tab added โ€” SEO + long-tail + backlinks score timeline

(reads data/score-history.json)


5.5 Phase 4 โ€” Pipelines, Activity Redesign, Usage (2026-05-07)

Sessions: 2026-05-07-gsc-pipelines-svg-bundle, 2026-05-07-post-crash-recovery, 2026-05-07-anthropic-usage-scrape

This was the biggest single-day expansion of the dashboard.

Pipelines Tab (commit f54c15ff)

Why: Ahmed needed to monitor multi-hour pipeline runs (cornerstone runner, missing-SVGs runner) from his phone. The only existing mechanism was SSHing into a terminal and watching log files โ€” impractical for a mobile check-in.

How:

  • New lib/pipeline-status.ts + app/api/pipeline-status/route.ts โ€” reads `.claude/

pipeline-logs/ (local) or data/pipeline-runs-snapshot.json` (Vercel fallback)

  • New components/pipeline-monitor.tsx โ€” client component with live worker status cards,

stage-dot progress visualization, success/failure/in-progress classification

  • scripts/write-pipeline-snapshot.mjs โ€” called by runners on heartbeat intervals
  • Workers classified by: heartbeat sentinel > PID heuristic

Iteration log: The Pipelines tab was rebuilt 4 times in a single session based on Ahmed's feedback:

  1. V1 โ€” Silent workers (no output shown)
  2. V2 โ€” Dual-run misclassification fixed
  3. V3 โ€” Stage-dot rendering corrected
  4. V4 โ€” "stage unknown" UX improved, final form

Decision: Snapshot fallback for Vercel

> Why: Vercel's serverless runtime has no access to the filesystem's .claude/ > pipeline-logs/ directory which is local-only. The solution: pipeline runners write a > committed snapshot file every ~60 seconds. The Pipelines tab reads this on Vercel (worst-case > staleness: ~90 seconds). Local dev reads the live files directly. Ahmed can monitor from > his phone via the deployed dashboard.

Activity Tab Redesign (commits a0486cc6, 944b29a5, 40f1a884, e535ce84)

Why: The original activity heat map used arbitrary colors. Multiple design iterations happened to find the right visual hierarchy.

Heat map evolution:

  1. Initial: Basic commit history list
  2. 944b29a5 โ€” 16-week heat map added (first version)
  3. 40f1a884 โ€” Redesign: 26 weeks, full-width responsive grid
  4. e535ce84 โ€” Revert to compact GSC-tracker style (Ahmed preferred original) + 30/60/90/

180-day selector added

  1. efcf85bc โ€” Single horizontal row + 8-tier palette
  2. a0486cc6 โ€” Final redesign: heat map stays + commit timeline with search/filter/PT timestamps
  3. b01a8997 โ€” Palette change: green โ†’ yellow โ†’ red โ†’ purple (replaces brown)
  4. 014e3b25 โ€” Hover tooltip on every cell (instant, no delay)

Decision: Pacific Time for all timestamps

> Why: Ahmed is in Pacific Time. The dashboard was originally showing UTC timestamps, > which caused confusion when reading "2026-05-07 07:45" and needing to mentally convert. > Commit c694c38b added PT rendering globally; 0d707b33 matched the status-file timestamp > format to the page-rendered format.

API Spend Watchdog (commits 39f010e5, b5cfe465)

Why: The 2026-05-07 API billing incident ($112.25 in one day from parallel runners that inherited ANTHROPIC_API_KEY) required a permanent cost watchdog.

How:

  • data/anthropic-api-usage.json created as the spend tracking source of truth
  • Banner added at the top of every tab (not just Usage tab) showing TODAY/MONTH/QUARTER spend
  • Color thresholds: green ($0), yellow (<$10/day), red (โ‰ฅ$10/day)
  • Full Usage tab built with per-model breakdown, daily spend chart, last-sync timestamp
  • v2: 3-tier daily banner + Usage tab + hourly auto-sync โ€” the banner was upgraded from

simple text to a structured three-tier card


5.6 Phase 5 โ€” Activity Freeze Hook + GSC Priority Queue (2026-05-07)

Session: 2026-05-07-gsc-pipelines (same marathon session)

Commit: 49fa8bb1 โ€” Activity tab freshness: pre-push freeze hook + restore GSC-tracker heat map style

Why: The Activity tab was showing only 10 commits in production because Vercel's shallow clone (fetch-depth: 1) only fetches one commit. The pre-push freeze hook was the fix.

How:

  • scripts/pre-push-freeze.mjs โ€” regenerates data/recent-commits.json before every push
  • simple-git-hooks pre-push event triggers the script
  • If the file changed, auto-creates a [freeze] commit so both commits ship together
  • Defense: the hook refuses to overwrite a richer committed dataset (e.g., if a Vercel build

somehow regenerated a truncated version, the hook's committed file wins)


5.7 Phase 6 โ€” Postmortems Tab + Validation Strategy (2026-05-08)

Session: 2026-05-08-sprint-15-pause-validation-approved.md

Commits: 6154e8de, 1b888d76

Why: The Sprint 15 overnight session produced 14 postmortem entries covering systemic failures: orphan pages, RSC payload bloat, missing metadata, sitemap errors, spam backlinks. These needed a permanent, structured surface on the dashboard โ€” not just a Markdown file.

What shipped:

  • New knowledge/postmortems/ directory (moved from knowledge/seo-audits/)
  • lib/postmortems.ts (287 LOC) โ€” parser handling all three postmortem doc shapes with

markdown fallback

  • components/postmortem-tab.tsx (360 LOC) โ€” server component, native <details> collapse-

on-click (no JavaScript needed), severity-tinted cards

  • Postmortems tab added to operator dashboard (?tab=postmortems)
  • 11 collapsible severity-tinted fix cards from Sprint 15 postmortem + 3 from spam-monitoring

+ 6 per-page from site-audit-pro followup + cross-cutting prevention table

Decision: Postmortems โ†’ Own folder + Own tab

> Why: Postmortems are a distinct document class (root-cause + prevention) from audits > (assessment + recommendations). Mixing them in knowledge/seo-audits/ made both folders > harder to navigate. The dedicated tab makes the dashboard the canonical "what have we > learned, what still needs to land" surface. Every PREVENTION recommendation in a postmortem > becomes a PENDING item in data/operator-pending.json.

Also in this session:

  • data/operator-pending.json expanded from 15 โ†’ 39 entries (every postmortem prevention

recommendation became its own actionable item)

  • CLAUDE.md updated with ## Postmortems โ€” every regression gets root-cause + prevention

section


5.8 Phase 7 โ€” Sprint 15 Wave: Scheduled Jobs, DisavowPanel, PsiTimeline (2026-05-12)

Three major sessions on 2026-05-12 each added significant dashboard features.

Scheduled Jobs Panel (commit 17c5d375)

Session: 2026-05-12-sprint-15-mid-day-parallel-agent-wave

Why: No single place to see "what is automated?" โ€” jobs were scattered across GHA YAMLs, Cowork SKILL descriptions, and git hook scripts. Ahmed needed an at-a-glance calendar of all recurring automation.

What shipped:

  • data/scheduled-jobs.json โ€” 11 jobs with type, schedule, status, last/next run
  • ScheduledJobsPanel inline component in page.tsx โ€” card grid showing job type badges,

schedule text, status indicators

  • Wired into Daily SEO tab

DisavowStatusPanel (commit 1f607347)

Session: 2026-05-12-disavow-cowork-phase-2-js-file-api-injection

Why: The disavow GSC upload is a daily automated task that can silently fail (wrong credentials, quota exhausted, GSC API error). The Daily SEO tab needed a health indicator showing the last upload status so Ahmed could catch failures without reading logs.

How:

  • data/disavow-gsc-upload-log.jsonl โ€” append-only log of every upload attempt (written by

Cowork SKILL after each run)

  • data/disavow-gsc-upload-state.json โ€” last-successful-upload state
  • DisavowStatusPanel component: green/amber/red/gray banner + last-7-runs table + latest-

delta detail + external links to GSC tool, GitHub file, GHA runs, issue label

Architecture:

Dashboard (/preview/operator?tab=daily)
  โ†“ reads data/disavow-gsc-upload-log.jsonl (last 30 entries)
  โ†“ reads data/disavow-gsc-upload-state.json
  โ†“ renders banner + run table

Known gap noted in session log: "GHA workflow status NOT on the panel. Currently shows Cowork upload-log only. A future enhancement could parse recent [disavow] commits to show 'GHA: ran 6h ago, +1 domain' right next to 'Cowork: uploaded 5h ago'."

PsiTimeline in Trends Tab (commit d566ffcc)

Session: 2026-05-12-perf-bundle-cleanup

Why: The performance cleanup session reduced homepage LCP from 3.3s โ†’ 2.9s and perf score from 90 โ†’ 94. This needed to be captured as a before/after datapoint and rendered in the Trends tab so the improvement is visible on the dashboard over time.

What shipped:

  • data/psi-history.json โ€” new structured PSI-snapshot file, two snapshots seeded
  • components/psi-timeline.tsx โ€” explainer header + 4 diff cards (perf, LCP, FCP, CLS) +

3 sparkline charts + expandable all-snapshots table

  • Wired into TrendsTab below <ScoreTimeline />

5.9 Phase 8 โ€” Sub-task Ledger ID Schema (2026-05-08, codified in 16855815)

Why: The sub-task ledger was originally a 3-column table (Sub-task | Commit | Status). Multiple sessions had added rows with inconsistent ID formats, some blank. The validator validate-subtask-ledger.mjs was added to prebuild to enforce the schema.

What changed:

  • Schema flipped to 5-column: | ID | Sub-task | Date | Commit | Status |
  • validate-subtask-ledger.mjs added to prebuild โ€” FAILS build on missing IDs, missing

statuses, malformed ID shapes

  • OperatorSubTaskLedger component became sortable (commit c374abaf)

5.10 Phase 9 โ€” Master Plan Split (2026-06-02)

Commit: 18cedc9c โ€” feat(operator): split Master Plan into roadmap + dedicated Sprints tab

Why: The Master Plan tab was getting overcrowded โ€” it combined the strategic roadmap view (high-level sprint descriptions) with the operational execution view (sub-task ledger, current state, decision log). These serve different purposes and have different audiences.

What changed:

  • "Master Plan" tab: now shows only the high-level roadmap (sprint cards, era summaries)
  • New "Sprints" tab: shows execution state (current state hero panel, sprint progress board,

sub-task ledger, decision log)

  • Tab nav updated: now 14 tabs total

6. Tab Reference โ€” Why / How / What

6.1 Overview Tab

Why built: The entry point. Shows the dashboard's purpose, live hero metrics, and the most recent operator log entries. Every new session starts here.

What it shows:

  • Hero strip: Sprint Progress (72/115 sub-tasks, 63%), Cornerstone Spokes Live (29, 0 in

flight), Long-Tail Spokes Shipped (29/363, 8%), Total Guides on Site (237), Commits 90d (1038)

  • "What this dashboard is, in one paragraph" โ€” the four design principles
  • "What shipped recently" โ€” live from docs/operator-log.md, last 5 entries with Why/What/Tags/Next

Design note: The hero strip is persistent โ€” it appears at the top of every tab, not just Overview. This was an explicit decision: "Tabs, not sections. Click a tab in the header to focus. The hero strip stays visible on every tab so the headline numbers never disappear."


6.2 Trends Tab

Why built: The SEO score composite grew from 71 โ†’ 88 over 3 weeks of Sprint 15 fixes. This progress was invisible without a chart. The Trends tab makes the trajectory visible.

What it shows:

  • <ScoreTimeline /> โ€” sparkline charts for composite, technical, content, schema, geo,

perf, SXO, backlinks scores across audit snapshots v1โ€“v8

  • <PsiTimeline /> โ€” PageSpeed Insights before/after diff cards + sparklines for performance,

LCP, FCP, CLS

Data sources: data/score-history.json, data/psi-history.json


6.3 Master Plan Tab

Why built: The strategic roadmap needs to be visible without opening files in a text editor. Sprint cards give Ahmed (and collaborating AI sessions) instant context on what era of work is active and what each sprint targets.

What it shows:

  • Era 1 (Sprints 0โ€“7) โ€” archived, collapsible
  • Era 2 (Sprints 8โ€“15) โ€” active, each sprint as a card
  • Per-sprint: title, goal, status badge, sub-task count

Data source: plans/seo-master-plan/seo-master-plan-roadmap.md


6.4 Sprints Tab

Why built: Split from Master Plan tab on 2026-06-02. The execution view (what's in progress, what's next, the full sub-task ledger) was crowding the roadmap view.

What it shows:

  • Hero panel: Last update, Current state, Next sub-task on resume (from execution-status.md)
  • Sprint progress board: table of all sprints with status
  • Sub-task ledger: sortable <OperatorSubTaskLedger /> โ€” all 200+ rows
  • Decision log: table of architectural decisions with rationale

Data source: plans/seo-master-plan/execution-status.md


6.5 Spokes Tab

Why built: The cornerstone-spoke architecture has 363 planned spokes. Without a priority view, it's impossible to pick "what to write next" efficiently.

What it shows:

  • Live cornerstone stage matrix (how many spokes per cornerstone are in each pipeline stage)
  • SpokeBacklogPanel โ€” ranked list from data/spoke-backlog-ranking.json
  • Per-spoke: slug, score breakdown, cornerstone parent, article shape, assignment stage,

liveSlug if different from slug (with link to live URL)

Known issue noted in session log: The liveSlug display was added as an emergency fix after the 2026-05-06 incident where 11 long-form router slug renames were deployed without adding 301 redirects to next.config.mjs. The Spokes tab now shows the liveSlug field so Claude Code sessions can see immediately whether a slug rename requires a redirect.


6.6 Content Tab

Why built: Shows the full corpus โ€” all published guides organized by category, discipline, and topic. Used to spot coverage gaps and verify cross-linking completeness.

What it shows:

  • Guide count by category
  • List of all disciplines + topics
  • Coverage stats (guides per topic, orphan topics)

6.7 Long-Tail Tab

Why built: Sprint 8 involves writing ~360 long-tail spokes. The Long-Tail tab tracks progress against the v2 content strategy (363 planned, 29 shipped = 8%).

What it shows:

  • Phase progress (Phases 1a/1b, 2a/2b, etc.)
  • Published vs planned count per cornerstone
  • Link to long-tail strategy doc

6.8 Activity Tab

Why built: Commit history as a visual monitoring surface. Ahmed can scan the heat map and see at a glance whether the pipeline has been active.

What it shows:

  • Commit heat map: weeks ร— day-of-week grid, colored by commit count per day

(8-tier palette: empty โ†’ green โ†’ yellow โ†’ red โ†’ purple for high-volume days)

  • <OperatorCommitTimeline /> โ€” searchable, filterable commit feed with PT timestamps and

category badges

Data source: data/recent-commits.json (on Vercel), git log (local dev)

Heat map palette decision: The final palette (green โ†’ yellow โ†’ red โ†’ purple) replaced an earlier brown-based palette after multiple iterations. The reasoning: "green = healthy activity, yellow = elevated, red = high-urgency, purple = exceptional" maps to intuition better than arbitrary colors.

30/60/90/180-day selector: Added after the initial full-history view made it hard to focus on recent work. The selector lets Ahmed filter to the timeframe relevant for the current sprint.


6.9 Pipelines Tab

Why built: Multi-hour pipeline runs need remote monitoring. Ahmed uses an iPad from the shop to check whether a cornerstone-spoke run or missing-SVGs run is still in progress or has finished.

What it shows:

  • <PipelineMonitor /> โ€” live worker cards (worker ID, current stage, success/failure

indicators, last heartbeat time)

  • Stage-dot progress bar per worker
  • Run summary (started, workers, completed, failed)

Data source: .claude/pipeline-logs/ (local) or data/pipeline-runs-snapshot.json (Vercel fallback)


6.10 Usage Tab

Why built: After the $112.25 billing incident on 2026-05-07, a dedicated cost visibility surface was required.

What it shows:

  • Per-month and per-quarter API spend totals
  • Daily spend chart (last 30 days)
  • Per-model breakdown (claude-sonnet-4-6 vs claude-opus-4-7)
  • topKey (which API key is being billed)
  • Last sync timestamp from the daily Cowork scrape
  • Per-script instrumented usage from data/anthropic-call-log.jsonl

Top-of-every-tab banner: A trimmed version of the spend summary appears at the top of every tab. This was intentional โ€” the cost watchdog should be impossible to miss. Color thresholds are relative to TODAY's spend (green $0, yellow <$10, red โ‰ฅ$10) not the monthly total.


6.11 Docs Tab

Why built: The docs/ directory contains 15+ reference files. The dashboard needed a searchable index so docs could be found without knowing filenames.

What it shows:

  • <OperatorDocsTable /> โ€” filterable grid of all docs with title, category, description
  • Links to each file on GitHub and on the local filesystem

6.12 Postmortems Tab

Why built: Postmortems are the institutional memory. Every regression that escapes the audit pipeline gets a postmortem. The tab makes root causes and prevention actions visible so future sessions don't repeat mistakes.

What it shows:

  • <PostmortemsTab /> โ€” severity-tinted cards (P0 red, P1 orange, High yellow, Medium gray)
  • Per-postmortem: WHAT, ROOT CAUSE, FIX (with commit hash), PREVENTION (concrete steps)
  • Cross-cutting prevention table: which validators were added as a result

Data source: All .md files in knowledge/postmortems/ โ€” parsed by lib/postmortems.ts

Design: native `<details>` collapse

> Why: The postmortem cards use native HTML <details>/<summary> for collapse, not a > React state-driven accordion. Reason: server component โ€” no useState available. Native > <details> handles the expand/collapse without any JavaScript, works identically on all > browsers, and preserves the open/closed state across tab switches.


6.13 Pending Tab

Why built: The canonical task queue. Before this tab existed, pending tasks were a mix of hardcoded TSX, mental notes, and scattered TODO comments in docs.

What it shows:

  • <OperatorPendingList /> โ€” sortable, filterable list of all pending items
  • Per-item: title, priority badge (high/medium/low), doer badge (claude/human/mixed), why,

sprint ID, date added

  • steps[] rendered as numbered list for human/mixed items

"Execution sequence" panel at top: Added in commit 10e5b8e5. When a resuming session opens the Pending tab, the execution-sequence panel (read from execution-status.md) appears first so there's a clear "start here" instruction before the full item list.


6.14 Daily SEO Tab

Why built: The GSC daily indexing submission workflow generates data (ranked URLs, submission history, disavow upload state) that needs a monitoring surface separate from the main dashboard but linked to it.

What it shows:

  • Link to /preview/gsc-daily.html (the dedicated GSC tracker, separated 2026-05-05)
  • GscWeeklyPanel โ€” latest weekly GSC snapshot: KPI cells (clicks, impressions, CTR,

position) with trend arrows, collapsible top-queries/top-pages tables

  • DisavowStatusPanel โ€” last upload banner + 7-run history table + delta detail
  • ScheduledJobsPanel โ€” all 11 scheduled jobs with type/schedule/status

Decision: GSC tracker as separate HTML page

> Why: The original GSC tracker was built as part of the operator dashboard. On 2026-05-05 > it was extracted to /preview/gsc-daily.html for two reasons: (1) it is a focused tool > with a single job โ€” daily indexing โ€” and mixing it with the dashboard created a sprawling > page, and (2) the tracker used localStorage for user state (checked/unchecked URLs), which > would conflict with the dashboard's server-render model. The tracker still lives under the > same Basic Auth gate (same /preview/ path).


7. Component Inventory

Server-Side Inline Functions (in page.tsx)

These are defined as functions inside page.tsx and called directly. They are server components that read files and return JSX.

| Function | Tab | What it renders | |---|---|---| | OverviewTab | Overview | Hero metrics + recent operator log | | TrendsTab | Trends | ScoreTimeline + PsiTimeline wrappers | | MasterPlanTab | Master Plan | Sprint cards from roadmap.md | | SprintsTab | Sprints | Execution state from execution-status.md | | ActivityTab | Activity | Heat map + OperatorCommitTimeline wrapper | | SpokesTab | Spokes | Stage matrix + SpokeBacklogPanel | | ContentTab | Content | Guide corpus stats | | LongTailTab | Long-Tail | Phase progress | | DocsTab | Docs | OperatorDocsTable wrapper | | PendingTab | Pending | OperatorPendingList wrapper | | UsageTab | Usage | Anthropic spend breakdown | | DailyTab | Daily SEO | GSC + Disavow + ScheduledJobs panels | | ScheduledJobsPanel | Daily SEO | Jobs grid (inline function) | | SpokeBacklogPanel | Spokes | Priority-ranked backlog (inline function) | | GscWeeklyPanel | Daily SEO | GSC KPIs + tables (inline function) | | DisavowStatusPanel | Daily SEO | Upload health banner (inline function) | | OperatorLogPanel | Overview | Log entry cards (inline function) | | PipelineStagesPanel | Pipelines | Stage progress grid (inline function) | | CornerstoneStageMatrix | Spokes | Per-cornerstone pipeline-stage counts (inline) |

Extracted Client Components (components/)

| File | Type | What it does | Why extracted | |---|---|---|---| | operator-docs-table.tsx | "use client" | Filterable/sortable docs table | Needs useState for filter input | | operator-pending-list.tsx | "use client" | Interactive pending task list | Needs useState for sort/filter | | operator-sub-task-ledger.tsx | "use client" | Sortable sub-task ledger | Needs useState for sort column | | pipeline-monitor.tsx | "use client" | Live pipeline run status | Needs useEffect for auto-refresh | | score-timeline.tsx | "use client" | SEO score sparkline charts | Needs canvas/chart rendering | | psi-timeline.tsx | "use client" | PSI diff cards + sparklines | Needs canvas/chart rendering | | operator-commit-timeline.tsx | "use client" | Searchable commit feed | Needs useState for search input | | postmortem-tab.tsx | Server | Severity-tinted incident cards | Complex rendering but no interaction; uses native <details> |

Supporting Library Files

| File | Purpose | |---|---| | lib/pipeline-status.ts | Pipeline log parsing logic (local + snapshot modes) | | lib/postmortems.ts | Postmortem markdown parser (287 LOC, handles 3 doc shapes) | | app/api/pipeline-status/route.ts | API route for live pipeline status (used by PipelineMonitor) | | scripts/write-pipeline-snapshot.mjs | Runner-called script that commits pipeline state snapshot | | scripts/pre-push-freeze.mjs | Pre-push hook that regenerates data/recent-commits.json | | scripts/freeze-recent-commits.mjs | Same as above, manual invocation (pnpm freeze:commits) | | lib/anthropic-billing.mjs | recordUsage() โ€” appends to data/anthropic-call-log.jsonl | | scripts/aggregate-anthropic-usage.mjs | Rolls JSONL into data/anthropic-api-usage.json |


8. Prebuild Audit Pipeline

The pnpm prebuild chain runs before every Vercel deploy. Many stages were added as direct results of dashboard-surfaced issues. The table below shows all stages that are relevant to the operator dashboard's health monitoring.

| Stage | Added | Why | Dashboard surface | |---|---|---|---| | validate-subtask-ledger.mjs | 2026-05-08 | Enforce ID + Status on every ledger row | Sprints tab | | validate-gsc-weekly-freshness.mjs --strict | 2026-06-02 | Catch stale/untracked GSC snapshots (S15.84 class) | Daily SEO tab | | aggregate-anthropic-usage.mjs | 2026-05-07 | Roll JSONL into dashboard-readable JSON | Usage tab + top banner | | freeze-recent-commits.mjs | 2026-05-07 | Capture git log for Activity tab (Vercel can't run git) | Activity tab | | validate-runner-coverage.mjs | 2026-05-12 | Every runner must have the validator-agent gate marker | Pipelines tab | | validate-api-instrumentation.mjs | 2026-05-07 | Every API-mode script must call recordUsage() | Usage tab | | generate-llms-txt.mjs | 2026-05-07 | Regenerates llms.txt (shown in Docs tab) | Docs tab | | build-search-index.mjs | early | Regenerates search index (linked from dashboard) | Docs tab |

The full prebuild chain has 30+ stages. See CLAUDE.md ยง "Audit pipeline" for the complete table.


9. Incidents & Postmortems

These four incidents directly shaped dashboard features, data contracts, and audit validators.


9.1 API Billing Incident (2026-05-07)

Severity: High โ€” $112.25 charged in one day

What happened: Parallel pipeline runners (.claude/parallel/*.sh) inherited ANTHROPIC_API_KEY from the shell environment. Every claude -p invocation billed the API instead of the Max plan.

Root cause: Runners did not unset ANTHROPIC_API_KEY before invoking the Claude CLI. The variable was present in ~/.zshrc (intentional for manual API-mode scripts) and leaked into all child processes.

Fix (commit `03624308`): Added to every .claude/parallel/*.sh runner immediately after set -euo pipefail:

if [ -n "${ANTHROPIC_API_KEY:-}${ANTHROPIC_AUTH_TOKEN:-}" ]; then
    echo "[WARN] ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN detected โ€” unsetting." >&2
fi
unset ANTHROPIC_API_KEY ANTHROPIC_AUTH_TOKEN

Dashboard impact:

  • Usage tab built (b5cfe465)
  • API spend watchdog banner added to every tab (39f010e5)
  • data/anthropic-api-usage.json created as spend source of truth
  • validate-api-instrumentation.mjs added to prebuild (blocks build if any API-mode script

skips recordUsage())

  • Daily Cowork SKILL woodwiki-anthropic-usage-scrape set up for 04:00 PT scrape

9.2 Activity Tab Staleness (2026-05-07)

What happened: Production Activity tab showed only 10 commits. Vercel's shallow clone (fetch-depth: 1) means git log returns 1 commit; the page had a fallback that limited output to 10 commits.

Root cause: No mechanism to persist full git history to Vercel's serverless runtime.

Fix: Pre-push freeze hook + data/recent-commits.json + Vercel-aware fallback logic.

Dashboard impact: Activity tab redesigned to use frozen data. pnpm freeze:commits command added. The pre-push hook means every pushed branch includes an up-to-date snapshot.


9.3 Dashboard Staleness from Cowork Non-Push (2026-05-15)

What happened: Ahmed opened /preview/gsc-daily.html and saw it showing 2-day-old data. The Cowork SKILL had run successfully but never committed or pushed the generated files.

Root cause: The SKILL's finalize step wrote sidecar files to disk but had no git add / commit / push steps. Vercel only redeploys on push, so the dashboard was always reading whatever was in the last manual push.

Fix: SKILL.md updated with a ## Commit & push to deploy section. The runner now stages 4 files, commits with SKIP_PREPUSH_FREEZE=1, pushes. Skips silently on idempotent re-runs.

Dashboard impact: Every Cowork SKILL that writes dashboard-feeding files now auto-commits and auto-pushes at the end of each run. The validate-gsc-weekly-freshness.mjs validator was added as the build-time enforcement.


9.4 Cowork Dirty-Index Deletion (2026-05-26, discovered 2026-05-27)

Severity: High โ€” silent data loss + 2 days of stale GSC rankings

What happened: A Cowork SKILL ran git commit (no pathspec) and swept pre-staged deletions from a dirty index left by a crashed prior run. This silently deleted pipeline/runs/gsc-weekly/2026-05-25.json from git while the file remained on disk as untracked. For 2 days, the priority queue scored URLs against a 9-day-stale snapshot. A stale .git/index.lock blocked all subsequent commits for 2 days.

Root cause: Two compounding failures:

  1. Cowork SKILL used git commit without pathspec โ€” swept garbage from a dirty index
  2. .git/index.lock was left by a killed git process and never cleared

Fix (S15.84):

  • All 4 repo-committing Cowork SKILLs got a git reset -q HEAD preamble before git add
  • All 4 SKILLs got rm -f .git/index.lock in their hygiene preamble
  • Permission-failure escalation: if rm can't clear the lock, STOP and surface to Ahmed

Dashboard impact:

  • validate-gsc-weekly-freshness.mjs added and strict-flipped (2026-06-02)

โ€” blocks Vercel builds if GSC snapshot is >10 days old OR on-disk-but-untracked

  • New postmortem at knowledge/postmortems/sprint-15-cowork-dirty-index-deletion-postmortem-2026-05-27.md

9.5 OG Image Runtime 404 (2026-05-01 โ†’ 2026-05-13) โ€” P0

Severity: P0 โ€” 213+ guide pages with broken OG images for 12 days

What happened: Every social share for guide/wood/tool/tag/discipline pages rendered text-only with no thumbnail. Ahrefs flagged 291 pages with og_tags_image_url_invalid.

Root cause: 5 opengraph-image.tsx files used generateImageMetadata({ id: "default" }). Next.js builds the asset at .../opengraph-image/default when this is used, but page.tsx hardcoded the bare .../opengraph-image URL. All static validators passed (they check source-level field presence, not runtime URL resolution).

Fix (S15.76, `1f607347`): Removed generateImageMetadata from all 5 [slug]/opengraph-image.tsx files. Next.js now builds at the bare URL.

Dashboard impact:

  • validate-og-runtime.mjs added (not prebuild โ€” needs deployed host; aliased

pnpm validate:og-runtime)

  • validate-og-route-shape.mjs added to prebuild (strict) โ€” two rules: (1) no

opengraph-image.tsx exports generateImageMetadata, (2) every [slug]/page.tsx with a sibling opengraph-image.tsx sets explicit openGraph.images: []

  • data/operator-pending.json S15.77 entry added: wire validate-og-runtime --prod into

Vercel postbuild or daily GHA

  • New postmortem at knowledge/postmortems/sprint-15-og-runtime-404-postmortem-2026-05-13.md

9.6 GSC Daily Cron Freelance + Quota Waste (2026-05-15)

Severity: Medium โ€” dashboard staleness + silent quota waste, no data loss

What happened: Three compounding failures:

  1. Cron ran but never pushed โ€” dashboard showed 2-day-old data
  2. Cowork submitted already-indexed URLs in the same run, burning ~50% of daily quota
  3. 18 pin entries (S15.56 canonical-fragmentation campaign) consumed 100% of daily quota,

leaving 153 scored candidates with zero submissions

Fixes:

  1. SKILL auto-commit/push added (see ยง9.3)
  2. scripts/gsc-record-submission.mjs guards: refuses to write submitted row if

already-indexed exists for same (runId, url); override: GSC_FORCE_RESUBMIT=1

  1. Pin list audited: 4 slugs retired, 18 โ†’ 10 entries

Dashboard impact:

  • data/gsc-priority-pinned.json now empty after cleanup
  • Pin lifecycle rule codified: "Add pins only after daily-quota math; set realistic until

dates (60-day ceiling); audit retirement candidates on weekly snapshot"


10. Current State (2026-06-04)

10.1 Dashboard State

| Tab | Status | |---|---| | Overview | Live โ€” 66+ operator log entries | | Trends | Live โ€” 8 score snapshots + 2 PSI snapshots | | Master Plan | Live โ€” Era 1 archived, Era 2 (Sprints 8โ€“15) active | | Sprints | Live โ€” 200+ sub-task ledger rows, S15.85 as most recent | | Spokes | Live โ€” 60 spokes ranked, spoke-backlog-ranking.json from 2026-05-12 | | Content | Live โ€” 237 guides | | Long-Tail | Live โ€” 29/363 spokes shipped (8%) | | Activity | Live โ€” 1,038 commits (90d) | | Pipelines | Live โ€” reads pipeline-runs-snapshot.json on Vercel | | Usage | Live โ€” Today $0, June $0, Q2 $201.27 | | Docs | Live โ€” filterable docs table | | Postmortems | Live โ€” 4 postmortem files, 20+ incident entries | | Pending | Live โ€” 26 items | | Daily SEO | Live โ€” DisavowStatusPanel, GscWeeklyPanel, ScheduledJobsPanel |

10.2 Open Items in Pending (Selected High-Priority)

| Sprint | Title | Doer | Priority | |---|---|---|---| | S15.61 | Wire validator-agent gate into 8 runners | claude | high | | S15.72 | Ahmed records 50 "From my shop" anecdote seeds | human | high | | S15.68 | 12 deep guides on top link-magnet topics | mixed | high | | S15.69 | Send 6โ€“7 broken-link reclamation pitches | human | high | | S15.71 | AI humanization anecdote injection (phases 4+7) | claude | high | | S15.77 | Wire validate-og-runtime into GHA or Vercel postbuild | claude | medium | | S15.15 | Monthly Site Audit Pro 3-agent re-run (~2026-06-07) | human | medium | | S15.48e | Editorial-split worst RSC offenders | claude | medium | | S15.85-verify | Cross-uid .git/*.lock investigation | claude | medium |

10.3 Scheduled Jobs (Active)

| Job | Type | Schedule | |---|---|---| | External-link-rot scan | GitHub Actions | Weekly Sunday 04:00 PT | | Daily disavow refresh | GitHub Actions | Daily 04:00 PT | | IndexNow ping | GitHub Actions | On push (content/**) | | GSC weekly snapshot | GitHub Actions | Weekly Monday 02:00 PT | | Operator gate watchdog | GitHub Actions | 09:00 + 21:00 UTC | | Anthropic usage scrape | Cowork SKILL | Daily 04:00 PT | | Pre-commit validation | git-hook | On every commit | | Pre-push freeze | git-hook | On every push | | Vercel prebuild chain | Vercel | On every deploy | | Monthly Ahrefs RD spot-check | Manual | Monthly | | Site Audit Pro 3-agent re-run | Manual | ~Monthly (next ~2026-06-07) |


11. Key Conventions & Rules

11.1 Operator Log Protocol (Non-Negotiable)

After every substantive work session, append to docs/operator-log.md:

## YYYY-MM-DD HH:MM โ€” title
**Why:** one-line rationale
**What shipped:** 3-5 bullet items
**Tags:** COMMA SEPARATED TAGS
**Next:** follow-up, or "None"

Newest entry on top. The dashboard reads this file at request time. Skipping it stales the live dashboard. "Substantive" = at least one of: 3+ commits, user-facing change, schema/API change, new subsystem, or decision future sessions need to know.

11.2 Pending List Protocol

  • Add item: append to data/operator-pending.json
  • Close item: DELETE from data/operator-pending.json (do not mark as done and keep)
  • Schema is strict โ€” see ยง4.4 above
  • Validate: node -e "JSON.parse(require('fs').readFileSync('data/operator-pending.json','utf8'))"

11.3 Postmortem Protocol

Every regression that escapes the audit pipeline gets a postmortem in knowledge/postmortems/. Required sections: WHAT, ROOT CAUSE, FIX (with commit hash), PREVENTION (concrete steps). Every PREVENTION step becomes its own entry in data/operator-pending.json.

11.4 Canonical Host (Always www)

All URLs in code, configs, markdown, and JSON must use https://www.woodwiki.org (with www). The bare host 308-redirects to www. Every hop is a cost. The validate-canonical-host.mjs pre-commit hook and prebuild step enforce this.

11.5 Max Plan vs API Billing

Every .claude/parallel/*.sh runner must unset ANTHROPIC_API_KEY immediately after set -euo pipefail. Every Node script that spawns claude via spawnSync must delete the key from childEnv. Violation = billable API charges against the woodwiki-extraction key.

11.6 Session Documentation Requirement

After any substantive work session (3+ commits, user-facing change, schema/API change, new subsystem, or decision future sessions need to know):

  1. Append to docs/operator-log.md
  2. Append to CHANGELOG.md
  3. Write session log at docs/sessions/YYYY-MM-DD-<topic>.md
  4. Add one-line entry to docs/sessions/INDEX.md
  5. Commit and push (so /preview/operator reflects the work in production)

11.7 Data File Freshness

The dashboard reads committed files. Cowork SKILLs that generate dashboard-feeding data MUST auto-commit and auto-push at the end of each run. A SKILL that writes to disk but doesn't push leaves the dashboard stale (the 2026-05-15 incident).

11.8 Two-Push Pattern

The pre-push hook regenerates data/recent-commits.json before every push. If it changed, the hook creates a [freeze] commit. This causes two commits to need pushing โ€” the first push triggers the freeze, the second push sends both. This is normal behavior and expected. Bypass with SKIP_PREPUSH_FREEZE=1 git push in emergencies only.


*Document generated 2026-06-04. Sources: 48 session logs, 48 git commits to page.tsx, 4 postmortems, docs/operator-log.md (66+ entries), CHANGELOG.md (2,324 lines), all data JSON files, plans/seo-master-plan/, docs/PIPELINE.md.*

Woodwiki Operator Dashboard โ€” Complete Build Log