Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NHyYupFdBiR4VfoUM7NjSM
3.4 KiB
3.4 KiB
Step 3 — Dashboard reads: overview, runs, articles
Read 00-shared.md, the plan (§3, §4, §9 in full, §16, §17 "Dashboard
queries"), the curation v2 plan sections it cites
(docs/plans/2026-09-02-personalized-curation-v2.md §7 tables, §9–§13
signals/admission/utility/editor, §15 explain/stats) and the handoffs for
steps 1–2. This is plan §19 item 3. Study src/curate/telemetry.rs
(ExplainRow, resolve_run, explain_row, near_misses, render_explain,
SignalsJson) — the dashboard renders the same data as HTML and must reuse
those loaders/types where they fit rather than re-deriving them.
Deliverables
src/web/dashboard/mod.rs— the overview (§9.1): last-run card fromruns.report_json(counters fallback), budget-today card per provider (config.referenced_providers()+ voyage,db::provider_spend_for_utc_day), ratings this week by label, "Unrated picks" (last three issues' picks with no explicit event, with the inline rating widget from step 2), jobs summary (thejobstable; the pages come in step 6), config-on-disk!lines fromConfig::check_report. Sparklines are step 6 — leave a clearly marked placeholder section or omit.src/web/dashboard/runs.rs(§9.2): the list with?status=filter and pagination; the detail with header + prev/next run, the funnel (fromcandidate_runsgrouped by stage/reason, in pipeline order, bars via the.funnelCSS), admission mix, preference state, timings, provider usage, warnings, per-feed top 20, the config diff against the previous non-dry run (flatten bothconfig_jsondocuments to dotted keys), near misses viatelemetry::near_misses, and the candidates table with the allow-listed filters (stage,reason,admitted_byviajson_extract(admitted_by,'$[0]'),q,flag) and sorts (utility default, rank, triage, quality, fit, title), 100 per page, a<details>per row with_signals_table.htmlbuilt fromsignals_json.src/web/dashboard/articles.rs(§9.3): the list with the latestcandidate_runsrow join (the indexidx_candidate_runs_article_runexists from migration 0004 — confirm), all listed filters and sorts allow-listed, 50 per page; the detail with the six blocks in the plan's order (article facts, assessments incl.provider_rejected, run history with expandable signals, neighbours/interests, embedding metadata never the vector, rating events). The "Explain (text)"<details>wrapstelemetry::render_explainoutput in<pre>. The rating widget here carries the note field.- Templates:
dashboard/overview.html,dashboard/runs.html,dashboard/run.html,dashboard/articles.html,dashboard/article.html, partials_signals_table.html,_candidate_row.html. Every table inside.scroll-x; badges per stage/reason/label; sticky thead; thedata-filterclient-side filter from §4.4 inapp.js. - Sort/filter parameters are validated against allow-lists and never interpolated into SQL; an unknown sort falls back to the default, never errors.
- Tests (§17 "Dashboard queries"): funnel counts against a seeded
candidate_runsset; candidate filters/sorts allow-listed; config diff finds changed dotted keys and ignores unchanged; articles list filters; article detail shows assessments, run history and rating events. Router-level tests for the admin guard on each new route.