UXS-006: Dashboard¶
- Status: Active
- Authors: Podcast Scraper Team
-
Parent UXS: UXS-001: GI/KG Viewer -- shared tokens, typography, layout, states
-
Normative layout & charts: see ## Dashboard implementation specification below (full product spec merged into this UXS).
- Related PRDs:
- PRD-025: Corpus Intelligence Dashboard
- Related RFCs:
- RFC-071: Corpus Intelligence Dashboard
- RFC-062: GI/KG viewer v2
- Implementation paths:
web/gi-kg-viewer/src/components/dashboard/DashboardView.vueweb/gi-kg-viewer/src/components/dashboard/BriefingCard.vueweb/gi-kg-viewer/src/components/dashboard/IndexStatusCard.vueweb/gi-kg-viewer/src/components/dashboard/TopicClustersStatusBlock.vueweb/gi-kg-viewer/src/utils/chartRegister.tsweb/gi-kg-viewer/src/stores/indexStats.ts,web/gi-kg-viewer/src/stores/dashboardNav.ts- Shell IA: VIEWER_IA.md — canonical shell layout, navigation axes, subject rail, status bar, first-run behavior
Summary¶
For shell layout, the three navigation axes, subject rail persistence and clearing, status bar, and first-run empty corpus behavior, see VIEWER_IA.md. This document specifies the Dashboard main tab only (briefing card, Coverage / Intelligence / Pipeline sub-tabs, charts, and layout below).
The Dashboard main tab is briefing + three tabs only: Coverage (default),
Intelligence, Pipeline. Corpus artifact picking (List, All / None,
Load into graph) lives on the status bar (List opens
data-testid="artifact-list-dialog"). Deep links to Library, Digest, and
Graph use dashboardNav handoffs consumed when those tabs activate. Chart.js uses
shared Tufte-style defaults from chartRegister.ts.
Layout¶
- Briefing card (
data-testid="briefing-card") — last run / health / short actions; always above tabs. - Tablist
aria-label="Dashboard tabs"— Coverage | Intelligence | Pipeline. - Coverage — coverage by month, feed coverage table, artifact activity (from listed artifacts), Index status (
data-testid="index-status-card") with Update index and Full rebuild (index-status-update,index-status-full-rebuild). - Intelligence — digest snapshot, Topic clusters status (
topic-clusters-status-block), topic landscape, top voices (when API available). Topic momentum / emerging connections omitted until RFC-073 data ships (no placeholder UI). - Pipeline — run history strip, duration trend, stage timings, numeric outcomes, episodes per run; optional per-feed run heatmap only when server exposes stable per-feed fields.
The legacy CorpusDataWorkspace / Pipeline | Content intelligence split on Dashboard is removed; those components are not part of this surface.
Corpus artifacts (status bar)¶
List (when health is ok and a corpus path is set) fetches GET /api/artifacts and opens the Corpus artifacts dialog. Load into graph switches the main tab to Graph when load succeeds (same behavior as the former workspace).
E2E contract¶
E2E surface map — Dashboard tab,
briefing-card, Dashboard tabs tablist, Index status card, artifact-list-dialog;
openCorpusDataWorkspace only switches to Dashboard and waits for the briefing card.
Revision history¶
| Date | Change |
|---|---|
| 2026-04-06 | Initial content (in UXS-001) |
| 2026-04-13 | Extracted from UXS-001 into standalone UXS-006 |
| 2026-04-19 | Corpus workspace on Dashboard; left rail query-only IA |
| 2026-04-19 | Dashboard: briefing + tabs; artifacts via status bar dialog |
| 2026-04-20 | §6.0 jobs: operator --config + optional profile: merge (#593) |
| 2026-04-20 | §6.0 jobs (cont.): feeds via --feeds-spec when feeds.spec.yaml exists |
| 2026-04-21 | §6.0: Operator YAML = Config; shallow PUT validation (feed + secret keys) |
Dashboard implementation specification¶
Status: Ready for implementation
Author: Design session (Marko + Claude), April 2026
Repo: chipi/podcast_scraper
Target area: web/gi-kg-viewer/src/components/dashboard/
Related docs: UXS-006, RFC-071, TUFTE_CHART_CRITIQUE.md, UXS-001
Replaces: Current UXS-006 content (rewrite, not amendment)
1. Overview¶
The Dashboard is restructured around two principles:
5-second answers. The primary question — "is everything OK, and what should I do?" — is answered by a permanent briefing card above the tabs, before any chart is read.
Actionability. Every data point either has an attached action or is explicitly decorative context. Observations without a "so what?" belong in depth charts, not on the primary surface.
Structure:
├── BRIEFING CARD (permanent, always visible, above tabs)
│ Last run · Corpus health · Action items
│
└── TABS
├── Coverage — what fraction of my corpus has intelligence coverage?
├── Intelligence — what is my corpus telling me?
└── Pipeline — how is my pipeline performing?
```yaml
Three tabs instead of the current two. The old "Status" concept becomes
the briefing card. The old "Content Intelligence" becomes Coverage +
Intelligence split by purpose.
---
### 2. Global Chart Rules (Tufte)
These apply to every chart in the dashboard without exception. They
are not per-chart suggestions — they are design system constraints.
#### 2.1 Non-negotiable rules
**No legends.** Every series is labelled directly — end-of-line for
line charts, end-of-bar for bar charts, positioned annotation for
everything else. No legend box anywhere in the dashboard.
**Title = insight.** Every chart title states the finding, not the
variable name. "Coverage by month" is a label. "N months below average
GI coverage" is a title. Mandatory, not optional.
**No gridlines.** Remove all gridlines from all charts. Reference
lines (average, target) are a single `border`-token hairline annotated
directly — not a grid.
**No dual y-axes.** If two series have different scales, they get
separate charts. No exceptions.
**Remove top and right spines.** Only left and bottom axis lines on
all Chart.js charts.
**Y-axis starts at 0 on all bar charts.** No truncated axes.
**Mandatory insight line.** Remove the word "optional" from UXS-001
and UXS-006. Every chart has an insight line below it. If the data
does not support a clear takeaway, the chart does not belong on the
dashboard.
**Charts earn their place.** Before adding a chart, ask: does this
need visual encoding, or is a number or table sufficient? Three numbers
are not a chart. If the answer is "a table," build a table.
**No chartjunk.** No 3D effects, no gradient fills, no drop shadows
on chart elements, no decorative borders around chart areas.
#### 2.2 Chart.js global config
Apply these defaults in `chartRegister.ts` so every chart component
inherits them without per-component overrides:
```typescript
Chart.defaults.plugins.legend.display = false
Chart.defaults.plugins.tooltip.enabled = true
Chart.defaults.scales.linear = {
...Chart.defaults.scales.linear,
grid: { display: false },
border: { display: false },
ticks: { maxTicksLimit: 5, color: 'var(--ps-muted)' }
}
Chart.defaults.scales.category = {
...Chart.defaults.scales.category,
grid: { display: false },
border: { display: false },
ticks: { color: 'var(--ps-muted)' }
}
2.3 Direct end-label plugin¶
A shared Chart.js afterDraw plugin for direct series labelling.
Register once in chartRegister.ts. Used by all multi-series line
charts:
const endLabelPlugin = {
id: 'endLabel',
afterDraw(chart: Chart) {
const ctx = chart.ctx
chart.data.datasets.forEach((dataset, i) => {
const meta = chart.getDatasetMeta(i)
if (!meta.hidden && meta.data.length > 0) {
const lastPoint = meta.data[meta.data.length - 1]
ctx.fillStyle = dataset.borderColor as string
ctx.font = '10px Inter, system-ui'
ctx.textAlign = 'left'
ctx.fillText(
dataset.label ?? '',
lastPoint.x + 6,
lastPoint.y + 3
)
}
})
}
}
Chart.register(endLabelPlugin)
```yaml
---
### 3. Briefing Card
#### 3.1 Purpose
Permanent section at the top of the Dashboard page, above the tab bar.
Always visible regardless of which tab is active. Answers the three
primary questions in under 5 seconds:
- "Did the last run succeed?" (Last run section)
- "Is my corpus healthy?" (Corpus health section)
- "What should I do?" (Action items section)
#### 3.2 Layout
```text
│ CORPUS 213 episodes · 67% GI · 82% indexed · 3 feeds │
│ ────────────────────────────────────────────────────────────── │
│ → 71 episodes have no GI artifacts [View in Library] │
│ → Index last rebuilt 14 days ago [Rebuild now] │
│ → 2 episodes failed in last run [View failures] │
└──────────────────────────────────────────────────────────────────┘
- Background:
elevatedtoken (lifts the card above the page surface) - Border:
bordertoken,rounded-sm - Padding:
p-4 - Divider between health and action items:
border-t border-border mt-3 pt-3 -
Section labels ("LAST RUN", "CORPUS"):
text-[10px] font-semibold tracking-wider mutedsmall caps -
Data values:
text-sm surface-foreground
3.3 Last run section¶
Sources: GET /api/corpus/runs/summary most recent item.
Format: ● [status] · [N] episodes · [duration] · [age] [Details →]
Status badge:
● Success—successtoken dot● Partial—warningtoken dot (some feeds failed in multi-feed run)● Failed—dangertoken dot
Age: relative time — "2 hours ago", "yesterday", "3 days ago". Not a raw timestamp. Computed client-side from run timestamp.
Duration: human-readable — "3m 24s", not "204s".
Multi-feed: "3 of 4 feeds succeeded" replaces episode count when
multi_feed_summary is present and any feed failed.
"Details →" link: navigates to Pipeline tab.
Empty state (no runs found): muted text — "No pipeline runs
found. Run podcast scrape to begin."
data-testid="briefing-last-run"
3.4 Corpus health section¶
Sources: GET /api/corpus/coverage (NEW endpoint — see Section 8) +
GET /api/index/stats.
Format: [total] episodes · [gi]% GI · [indexed]% indexed · [feeds] feeds
Each metric is a link:
- Episode count → Library tab
- GI% → Coverage tab (scrolls to coverage chart)
- Indexed% → Coverage tab (scrolls to index section)
- Feed count → Library tab (feed filter)
Warning threshold: if GI% < 50% or indexed% < 60%, that metric
renders in warning token colour. Immediate visual signal without
reading action items.
Thresholds are tunable parameters (UXS-001 table):
| Parameter | Default | Status |
|---|---|---|
| GI coverage warning threshold | 50% | Open |
| Index coverage warning threshold | 60% | Open |
data-testid="briefing-corpus-health"
3.5 Action items section¶
Max 3 items. Triage order: pipeline failures first (blocking), coverage gaps second, operational staleness third.
Sources: assembled client-side from coverage data + index stats + run summary.
Item format: → [plain-language finding] [primary action link] [secondary action link]
Action item rules:
-
Plain language, present tense: "71 episodes have no GI artifacts" not "GI coverage: 67%"
-
Primary action link: the most direct next step
- Max one secondary action link
- Never says "run enrich" or references RFC-073 enrichers — out of scope
Possible items (ordered by priority):
| Condition | Text | Actions |
|---|---|---|
| Last run has failures | "N episodes failed in last run" | [View failures] → Library |
| Last run failed entirely | "Last run failed — no episodes processed" | [View in Pipeline] |
| GI coverage < 50% | "N episodes have no GI artifacts" | [View in Library] |
| Index not built | "Vector index has not been built" | [Build index] |
| Index stale > 7 days | "Index last rebuilt N days ago" | [Rebuild now] |
| Feed not indexed | "Feed X is not in the vector index" | [View coverage] |
| No runs in > 7 days | "No pipeline runs in N days" | [View in Pipeline] |
| Topic clusters missing | "Topic clusters not built" | [View in Coverage] |
"Rebuild now" and "Build index" trigger POST /api/index/rebuild
directly (existing endpoint). All others are navigation links.
All-clear state: When no items qualify: a single line —
● Everything looks good — using success token dot. This positive
confirmation is as important as finding items — it tells you to stop
looking.
data-testid="briefing-action-items"
data-testid="briefing-action-item" (per item)
data-testid="briefing-all-clear" (empty state)
4. Coverage Tab¶
Answers: "What fraction of my corpus has intelligence coverage, and where are the gaps?"
Four sections, top to bottom.
4.1 GI coverage by month¶
Chart type: Single-series bar chart.
What it shows: GI coverage percentage per publish month (0–100%). One bar per month. Bars are the primary story — what fraction of episodes published that month have GI artifacts.
Not a stacked chart. Not episode counts. Coverage rate is the signal, not volume.
Tufte compliance:
- Single series, no legend needed
- Y-axis: 0–100%, labelled "Coverage %", max 5 tick labels
- X-axis: month labels,
text-[10px] -
Reference line: horizontal hairline at corpus average coverage %, annotated directly — "avg 67%" placed at the right end of the line
-
Bars below average:
warningtoken fill - Bars at or above average:
gitoken fill - No gridlines, no top/right spines
- Direct annotation on the lowest bar: "Lowest: [month] — N%"
- Insight line below chart (mandatory): computed from data, e.g. "3 months below average — [month], [month], [month] need attention"
or "Coverage improving: up [X]pp since [earliest month]"
Interaction: clicking any bar navigates to Library filtered to
episodes from that month without GI artifacts (since + end of
month + topic_cluster_only=false + filter for no-GI).
Source: GET /api/corpus/coverage → by_month array (NEW).
Component: CoverageByMonthChart.vue (new, replaces
VerticalBarChart.vue for this use case)
Empty state: "No episode metadata found. Run the pipeline to generate corpus data."
data-testid="coverage-by-month-chart"
4.2 Feed breakdown¶
Not a chart — a table with inline data bars.
What it shows: Per-feed coverage summary. Each row = one feed. Columns: Feed name | Episodes | GI coverage | KG coverage | Indexed.
GI coverage and KG coverage columns show a 40px inline progress bar (CSS, not Chart.js) plus the percentage number. This gives visual comparison without a separate chart.
Tufte note: 40px inline bars are proportional (0–100% always), direct labelled (number next to bar), and scannable. No legend, no axis, no gridlines.
Sort order: ascending by GI coverage — worst-covered feeds at top, since those are the action targets.
Row interaction: clicking a row navigates to Library filtered to that feed.
Insight line: "Feed [name] has lowest GI coverage at [N]% — [M] episodes without GI artifacts."
Source: GET /api/corpus/coverage → by_feed array (NEW).
Component: FeedCoverageTable.vue (new)
data-testid="feed-coverage-table"
data-testid="feed-coverage-row" (per row)
4.3 Artifact activity¶
Chart type: Grouped bar chart — new artifacts per day, last 30 days.
Replaces: The current cumulative GI+KG line chart.
Why: Cumulative lines always trend upward and look healthy even when work has stopped. A recency chart makes silence visually obvious.
What it shows: For each day in the last 30 days: count of new GI artifacts + count of new KG artifacts written that day.
Tufte compliance:
- Two series:
gitoken bars andkgtoken bars, grouped per day -
Series labelled directly: "GI" and "KG" as
text-[10px]text positioned above the first bar of each series (not a legend) -
X-axis: day labels, only show every 7th label to avoid clutter
- Y-axis: starts at 0, labelled "New artifacts"
- No gridlines, no spines except left + bottom
- Silence is visible: days with no bars are visually obvious gaps
-
If last 14 consecutive days have no bars: mandatory insight reads "No new artifacts in 14 days — pipeline may not be running"
-
Otherwise insight: "Last GI: [date] · Last KG: [date]"
- Annotate the most recent active day: small dot + date label
Source: GET /api/artifacts → mtime_utc values per artifact,
bucketed by day client-side. Existing artifactMtimeBuckets.ts —
extend to produce daily counts rather than cumulative.
Component: ArtifactActivityChart.vue (new — replaces
CategoryLineChart.vue for this purpose)
data-testid="artifact-activity-chart"
4.4 Index status¶
Not a chart — a card.
The question "is my index current?" doesn't need a chart. It needs three facts:
Feeds in index: 3 of 3
```python
- "⚠ Rebuild recommended" appears when index staleness heuristic
from `index/stats` is true. Uses `warning` token.
- "Rebuild now" button when stale (triggers existing endpoint).
- "Last rebuild error: [message]" in `danger` token when
`rebuild_last_error` is present.
- Disabled while `rebuild_in_progress` is true (shows "Rebuilding…"
spinner inline).
No Chart.js involved. Pure card with MetricsPanel pattern.
**Source:** `GET /api/index/stats` (existing).
`data-testid="index-status-card"`
---
### 5. Intelligence Tab
Answers: "What is my corpus telling me?"
Five sections. Some require enricher data and degrade gracefully.
Enricher triggers ("run enrich") are out of scope — degraded states
show what's missing, not how to generate it.
#### 5.1 Corpus snapshot
**Always available.**
An expanded version of the digest compact view. Shows the rolling
window summary (default 7d) with a little more breathing room than
the Digest tab's compact layout.
Content:
- Window line: "Last 7 days — 12 new episodes across 3 feeds"
- Top 3 topic bands from the digest, one line each: topic label +
episode count ("AI policy — 4 episodes")
- Each topic band line clickable → Digest tab (which loads that topic)
- "Open Digest →" link at bottom
Not a chart. A structured text summary using the digest API data.
**Source:** `GET /api/corpus/digest` with `window=7d`, `compact=false`,
`max_rows=3`.
`data-testid="intelligence-snapshot"`
#### 5.2 Topic landscape
**Available when topic clusters have been built.**
A compact grid of topic clusters. Each cluster card:
- Cluster canonical label (`text-sm font-semibold`)
- Member topic count badge ("4 topics")
- Episode count (summed from `members[].episode_ids`, deduplicated)
- Clicking → Graph tab, focused on that TopicCluster compound node
Grid: `repeat(auto-fit, minmax(min(100%, 12rem), 1fr))` — same card-width logic as Digest topic bands
but cards are smaller and denser — no hit rows, just cluster identity.
**Insight line:** "N topic clusters covering M distinct topics."
**Degraded state** (404 from topic-clusters endpoint): `muted` text —
"Topic clusters not yet built for this corpus." No action prompt.
**Source:** `GET /api/corpus/topic-clusters` (existing).
`data-testid="intelligence-topic-landscape"`
#### 5.3 Top voices
**Available when `GET /api/corpus/persons/top` exists (NEW endpoint —
see Section 8).**
Top 5 persons by insight count. Each person card:
- Display name (`text-sm font-semibold`)
- Episode count badge: "23 episodes"
- Insight count badge: "67 insights" (`gi` token)
- Top 3 topic chips (`kg` token border) — topics from the endpoint
- "View profile →" link → Person Landing (UXS-010) in subject rail
Cards in a horizontal scrollable row on narrow viewports, 2-column
grid on wide viewports.
**Insight line:** "N speakers with grounded insights — [top person]
leads with N insights across M episodes."
**Degraded state** (endpoint not available or 0 persons): `muted` text —
"No speaker intelligence found for this corpus."
**Source:** `GET /api/corpus/persons/top?limit=5` (NEW).
`data-testid="intelligence-top-voices"`
#### 5.4 Topic momentum
**Enricher-gated — degrades gracefully.**
Available when `temporal_velocity` enricher data is present (RFC-073).
Top 5 topics by recent activity. Each row:
- Topic name (`text-sm`)
- Sparkline: 8-point mini line chart, last 8 weeks of mention
frequency. `series-1` token stroke, no fill. No axes, no labels —
the shape is the signal.
- Trend badge: accelerating (`success`) / stable (`muted`) /
declining (`warning`) — same as UXS-007
- Episode count: "N episodes" in `muted`
- Clicking → Topic Entity View (UXS-007) in subject rail
**Tufte note on sparklines:** y-scale normalises to each topic's own
range (0 to its max). This shows *relative* trend shape, not absolute
volume. Volume is shown as the episode count number. This is acceptable
for a "relative momentum" display — the spec must note this so
implementers don't try to share y-scales (which would make
low-volume topics invisible).
**Insight line:** "N topics accelerating in the last 30 days."
**Degraded state:** `muted` text — "Topic momentum data not available
for this corpus." No enricher prompt.
**Source:** RFC-073 enricher output (not yet available).
`data-testid="intelligence-topic-momentum"`
#### 5.5 Emerging connections
**Enricher-gated — degrades gracefully.**
Available when `topic_cooccurrence` enricher data is present (RFC-073).
Top 4 topic pairs by unexpected co-occurrence strength. Each row:
- "Topic A ↔ Topic B" (`text-sm`)
- Episode count: "14 episodes" (`muted`)
- Clicking → Graph tab, both topic nodes highlighted + focused,
subgraph showing their shared connections
**Insight line:** "N topic pairs co-occur more than expected given
their individual frequency."
**Degraded state:** `muted` text — "Topic co-occurrence data not
available for this corpus." No enricher prompt.
**Source:** RFC-073 enricher output (not yet available).
`data-testid="intelligence-emerging-connections"`
---
### 6. Pipeline Tab
Answers: "How is my pipeline performing?"
Now freed from being the default tab (briefing card handles the quick
answer), the Pipeline tab can be dense and detailed.
#### 6.0 HTTP pipeline jobs (RFC-077)
When **`jobs_api`** is true on health, the tab includes **`PipelineJobsCard`**
(`data-testid="pipeline-jobs-card"`): list **HTTP-triggered** pipeline jobs for
the current corpus, **Run** (enqueue), **Reconcile**, **Refresh**, per-row **Cancel**
when status is `queued` or `running`, and inline help when the API is off. This
surface is **in addition to** run-history charts fed from **`GET /api/corpus/runs/summary`**
(see below). Spec: [RFC-077](../rfc/RFC-077-viewer-feeds-and-serve-pipeline-jobs.md).
**Config a job uses:** the subprocess passes **`--config`** to the server-resolved operator YAML path (same file as status bar **Operator YAML** / **Config**). That file may contain **`profile: <preset>`** — effective pipeline settings are **packaged preset defaults merged with explicit keys** in the file ([GitHub #593](https://github.com/chipi/podcast_scraper/issues/593)). **Feeds** for the job come from corpus **`feeds.spec.yaml`** via **`--feeds-spec`** when that file exists, not from duplicate root keys in operator YAML (those keys are rejected on **`PUT /api/operator-config`** — **top-level** denylist only, same as secrets). Any **Run** / help copy in this tab should say **operator file + optional profile**, not “edit feeds here.”
#### 6.1 Run history strip
**Not a chart — a row of status dots.**
Last 10 runs rendered as coloured dots in a horizontal row. No axes,
no labels — maximum information density.
- `●` success token = run succeeded
- `●` warning token = partial success (some feeds failed)
- `●` danger token = run failed
Dots are ordered left-to-right oldest to newest. Most recent dot is
slightly larger (12px vs 8px) to draw the eye to current state.
Native `title` tooltip per dot: "Run [date] · [N] episodes ·
[duration] · [status]"
Clicking a dot: expands that run's detail inline below the strip,
replacing the current "latest run detail" section with the selected
run. Clicking again collapses. Only one run expanded at a time.
**Insight line:** "Last 10 runs: [N] success, [M] partial, [P] failed"
— or simply "All 10 runs succeeded" in `success` token if clean.
**Source:** `GET /api/corpus/runs/summary` (existing).
`data-testid="pipeline-run-history-strip"`
`data-testid="pipeline-run-dot"` (per dot)
#### 6.2 Duration trend
**Chart type:** Bar chart, last 5 runs' durations.
**What it shows:** Run-to-run duration variability. Is the pipeline
getting slower?
**Tufte compliance:**
- 5 bars — simple, no legend needed
- Y-axis: starts at 0, labelled in minutes, max 4 ticks
- X-axis: run dates, `text-[10px]`
- Reference line: horizontal hairline at 5-run average, directly
labelled "avg [X]m [Y]s"
- Latest bar: `primary` fill. Others: `surface-foreground` at 60%
opacity. Draws eye to the current run vs history.
- If latest bar > average + 20%: `warning` fill on latest bar
- No gridlines, no top/right spines
- Direct bar labels: duration value above each bar ("3m 24s") in
`text-[10px] muted`
- **Title = insight** (mandatory): "Latest run [X]% [faster/slower]
than 5-run average" — computed. If within 10% either way: "Run
duration stable — avg [X]m [Y]s"
**Source:** `GET /api/corpus/runs/summary` last 5 items sorted by
timestamp.
**Component:** Reuse/update existing chart component.
`data-testid="pipeline-duration-trend"`
#### 6.3 Latest run breakdown
Two sections side-by-side on wide viewports, stacked on narrow.
**Stage timings (left):**
**Chart type:** Horizontal bar chart, one bar per pipeline stage,
sorted by duration descending.
**What it shows:** Where time was spent in the most recently selected
run (from the history strip — defaults to latest).
**Tufte compliance:**
- Horizontal bars — stage names are text, need space to breathe
- Sorted descending: longest bar at top, shortest at bottom
- Direct value labels: duration ("1m 23s") printed at the right end
of each bar — **no x-axis needed** once values are labelled
- Bar fill: `primary` token for the longest bar (the bottleneck),
`surface-foreground` at 60% opacity for all others. The bottleneck
is visually distinct without any annotation.
- No gridlines at all — bar length communicates value
- No spines — labels make axes redundant
- **Title = insight** (mandatory): "[Stage name]: [X]% of total run
time" — always names the bottleneck
**Episode outcomes (right):**
**Not a chart.** Three numbers are not a chart.
```text
✗ 2 failed [View failures →]
text-sm numbers, intent token colours for each count. "View
failures →" link appears only when failures > 0, navigates to
Library filtered for that run's failed episodes.
Source: GET /api/corpus/runs/summary for the selected run
(or run detail endpoint if stage timings need fuller data — see
open questions in Section 9).
data-testid="pipeline-stage-timings"
data-testid="pipeline-episode-outcomes"
6.4 Episodes per run¶
Chart type: Bar chart, all runs in summary (up to 150, capped).
What it shows: Episode count processed per run over time. Shows pipeline activity and cadence.
Tufte compliance:
- Single series, no legend needed
- Y-axis: starts at 0, "Episodes" label, max 5 ticks
- X-axis: run dates (sparse labelling — every Nth label)
- No gridlines, no top/right spines
- Total corpus as text annotation below chart: "Total: [N] episodes across [M] runs" — not a cumulative overlay on the same axis
(that was the dual-axis lie)
- Title = insight: "Average [N] episodes per run" or "Processing volume declining — last 3 runs below average" if trend is negative
Source: GET /api/corpus/runs/summary (existing).
Component: Reuse VerticalBarChart.vue with updated config.
data-testid="pipeline-episodes-per-run"
6.5 Feed processing history¶
Chart type: Heatmap grid (small multiples).
Only shown for multi-feed corpora. Hidden for single-feed.
What it shows: Per-feed run success/partial/failure over the last 5 runs. Rows = feeds, columns = runs (left-to-right oldest to newest).
Tufte compliance:
- Cell colour:
successfill /warningfill /dangerfill -
Cell glyph: ✓ / ⚠ / ✗ inside each cell — colour alone is not sufficient (accessibility + Tufte "don't rely on colour only")
-
Cell size: ~28px × 28px. Gap spacing between cells instead of borders (whitespace is cleaner than lines)
-
Feed names: left-aligned row headers,
text-xs - Run dates: top-aligned column headers,
text-[10px] - No legend — ✓/⚠/✗ glyphs are self-explanatory
- Insight line (mandatory): "Feed [name] failed [N] of last 5 runs" — triggered when any feed has ≥ 2 failures in the window.
If all clean: "All feeds succeeded in last 5 runs" (success
token).
Source: GET /api/corpus/runs/summary per-feed breakdown
(verify field availability in CorpusRunSummaryItem — see Section 9).
Component: FeedRunHistoryGrid.vue (new)
data-testid="pipeline-feed-history-grid"
7. Tab Order and Defaults¶
Coverage is the default tab (not Pipeline as today) because it answers the most frequent ongoing question — "how complete is my intelligence coverage?" — whereas Pipeline is consulted reactively after running.
The briefing card's "Details →" link on the Last run section jumps to the Pipeline tab.
8. New API Endpoints Required¶
8.1 GET /api/corpus/coverage¶
Priority: CRITICAL. Unblocks briefing card scorecard + Coverage tab.
Single catalog scan checking GI/KG artifact presence per episode,
grouped by month and feed. Same implementation pattern as
/api/corpus/stats.
Response schema:
class CoverageByMonthItem(BaseModel):
month: str # "YYYY-MM"
total: int
with_gi: int
with_kg: int
with_both: int
class CoverageFeedItem(BaseModel):
feed_id: str
display_title: str
total: int
with_gi: int
with_kg: int
class CorpusCoverageResponse(BaseModel):
total_episodes: int
with_gi: int
with_kg: int
with_both: int
with_neither: int
by_month: List[CoverageByMonthItem]
by_feed: List[CoverageFeedItem]
Implementation: One filesystem scan. For each episode metadata
file: check sibling *.gi.json and *.kg.json existence, group by
publish_date month and feed_id. No GI/KG file reading — just
existence checks. Fast.
Test: tests/unit/podcast_scraper/server/test_viewer_corpus_coverage.py
8.2 GET /api/corpus/persons/top¶
Priority: MEDIUM. Enables Intelligence tab Top Voices section. Intelligence tab degrades gracefully without it.
Partial GI artifact scan: reads Person node counts from loaded GI artifacts, aggregates by person canonical id, returns top N by insight count.
Response schema:
class TopPersonItem(BaseModel):
person_id: str # "person:{slug}"
display_name: str
episode_count: int
insight_count: int
top_topics: List[str] # canonical topic ids, max 3
class CorpusTopPersonsResponse(BaseModel):
persons: List[TopPersonItem]
total_persons: int # total distinct persons in corpus
```yaml
**Implementation:** Requires reading GI artifacts. Two options:
(a) scan all `*.gi.json` files, extract Person node counts — full scan,
slower; (b) use the loaded graph in memory if available — faster but
state-dependent. Prefer (a) for correctness — this is a background
fetch, latency is acceptable.
**Test:** `tests/unit/podcast_scraper/server/test_viewer_corpus_persons.py`
---
### 9. Open Questions
These are not blocking Phase 1–2 but need resolution before the
implementation is complete.
**Stage timings granularity:** The current `CorpusRunSummaryItem`
schema provides "compact metrics per run." Verify whether it includes
per-stage timing breakdown or only total duration. If stage timings
are only in the full `run.json` file and not in the compact summary,
a `GET /api/corpus/runs/{run_id}` detail endpoint may be needed for
the stage timings chart. Check the existing `corpus_run_summary.json`
schema before adding a new endpoint.
**Feed per-run breakdown:** `FeedRunHistoryGrid` requires per-feed
success/failure per run. Verify whether `CorpusRunSummaryItem` includes
per-feed breakdown. If not, this chart silently hides (it is conditional
on multi-feed corpus anyway) and is tracked as a follow-up.
**`GET /api/corpus/coverage` performance:** If the corpus has thousands
of episodes, the existence-check scan may be slow. Add a
`Cache-Control: max-age=60` header and consider a lightweight cache
in the route handler (invalidated on corpus path change). Profile with
a real large corpus during implementation.
---
### 10. Files to Touch
#### New components
```text
web/gi-kg-viewer/src/components/dashboard/ArtifactActivityChart.vue
web/gi-kg-viewer/src/components/dashboard/FeedRunHistoryGrid.vue
web/gi-kg-viewer/src/components/dashboard/IntelligenceSnapshot.vue
web/gi-kg-viewer/src/components/dashboard/TopicLandscape.vue
web/gi-kg-viewer/src/components/dashboard/TopVoices.vue
web/gi-kg-viewer/src/components/dashboard/TopicMomentum.vue
web/gi-kg-viewer/src/components/dashboard/EmergingConnections.vue
Modified components¶
web/gi-kg-viewer/src/components/dashboard/DashboardView.vue
— briefing card always rendered above tabs
web/gi-kg-viewer/src/utils/chartRegister.ts
— global Chart.js defaults (no legends, no gridlines)
— endLabelPlugin registered globally
web/gi-kg-viewer/src/utils/artifactMtimeBuckets.ts
— extend to produce daily new-artifact counts (not just cumulative)
New API modules¶
web/gi-kg-viewer/src/api/corpusPersonsApi.ts
— fetchTopPersons(limit)
New server routes¶
src/podcast_scraper/server/schemas.py
— CorpusCoverageResponse, CoverageByMonthItem, CoverageFeedItem
— CorpusTopPersonsResponse, TopPersonItem
Add coverage and persons/top routes to app.py.
New server tests¶
Add or extend integration tests for the new coverage and persons routes (mirror patterns used for existing corpus API tests).
UXS amendments (after implementation)¶
docs/uxs/UXS-006-dashboard.md
— Full rewrite (current file is thin; this spec replaces it)
— Remove API/Data left panel section (moved to status bar)
— Add briefing card spec
— Add three-tab structure
— Add Tufte compliance rules (cross-reference TUFTE_CHART_CRITIQUE.md)
docs/uxs/UXS-001-gi-kg-viewer.md
— Remove "optional" from insight line rule — mandatory
— Add to tunable parameters:
GI coverage warning threshold 50% Open
Index coverage warning threshold 60% Open
Action items max 3 Open
Top voices limit 5 Open
docs/guides/SERVER_GUIDE.md
— Add GET /api/corpus/coverage and GET /api/corpus/persons/top
to the API reference table
E2E surface map¶
text
— coverage-by-month-chart, feed-coverage-table, artifact-activity-chart
— index-status-card
— intelligence-snapshot, intelligence-topic-landscape
— intelligence-top-voices, intelligence-topic-momentum
— intelligence-emerging-connections
— pipeline-run-history-strip, pipeline-run-dot
— pipeline-duration-trend, pipeline-stage-timings
— pipeline-episode-outcomes, pipeline-episodes-per-run
— pipeline-feed-history-gridyaml
11. Implementation Phases¶
Phase 1 — Briefing card + Coverage tab¶
Goal: The two most impactful surfaces. Requires GET
/api/corpus/coverage new endpoint.
Steps:
- Implement
GET /api/corpus/coverageserver route + tests BriefingCard.vue— all three sections wired to coverage + runs/summary- Coverage tab:
CoverageByMonthChart.vue,FeedCoverageTable.vue - Apply global Chart.js Tufte defaults in
chartRegister.ts ArtifactActivityChart.vue(replaces cumulative line)IndexStatusCardsection (from existing index/stats data)
Checkpoint: Briefing card shows last run + corpus health + action items. Coverage tab shows month chart, feed table, activity chart, index card. No regressions in existing Pipeline tab.
Phase 2 — Pipeline tab restructure¶
Goal: Improved Pipeline tab with Tufte-compliant charts.
Steps:
RunHistoryStrip.vuewith click-to-expand per runDurationTrendChart.vuewith insight title + reference line- Stage timings chart with direct value labels + bottleneck highlight
- Episode outcomes → inline numbers in run card (remove chart)
EpisodesPerRunChart.vue(remove cumulative overlay)FeedRunHistoryGrid.vue(multi-feed only)
Checkpoint: Pipeline tab fully restructured. All Tufte rules applied. Episode outcomes is no longer a chart.
Phase 3 — Intelligence tab¶
Goal: Intelligence tab with available data + graceful degradation.
Steps:
IntelligenceSnapshot.vue(digest API, existing data)TopicLandscape.vue(topic-clusters API, existing data)- Implement
GET /api/corpus/persons/top+TopVoices.vue -
TopicMomentum.vuestub with degraded state (enricher not yet available) -
EmergingConnections.vuestub with degraded state
Checkpoint: Intelligence tab shows snapshot + topic landscape immediately. Top voices shows when persons/top endpoint ships. Momentum and connections show degraded states (no enricher prompts).
Phase 4 — UXS + E2E updates¶
- Rewrite
UXS-006-dashboard.mdto match implementation -
Update
UXS-001-gi-kg-viewer.mdtunable params + mandatory insight line rule -
Update
SERVER_GUIDE.mdAPI table - Update
E2E_SURFACE_MAP.mdwith all new testids - Run
make test-ui-e2e
12. What This Does Not Change¶
- Chart.js library itself — same version, same registration pattern
indexStats.tsstore — unchanged- Digest and Library tabs — unchanged
- Subject rail — unchanged
- Token system — all chart colours use existing
--ps-*variables DashboardOverviewSection.vueis absorbed intoBriefingCard.vueand can be removed after migration