Files
the-daily-epub/docs/plans/briefs/web-dashboard/handoff-step3.md
T
thalladaandClaude Fable 5.1 63eee6bbd3 Web dashboard step 3: overview, runs and articles pages
Fill in the dashboard read pages of the web dashboard plan (§9): the
overview card set (last run from report_json, budget today per provider,
ratings this week, unrated picks with the rating widget, jobs summary,
config-on-disk warnings), the runs list and run detail (funnel, admission
mix, preference state, timings, provider usage, warnings, top feeds,
config diff against the previous non-dry run, near misses, and the
candidates table with allow-listed filters/sorts and per-row signals),
and the articles list and article detail (assessments including
provider rejections, run history, neighbours and interests, embedding
metadata, rating events, and the text explain).

Dynamic list queries go through AssertSqlSafe with SQL built only from
constants and allow-listed fragments; every user value is bound. Adds
the shared signals-table, candidate-row and pager partials, a CSS block
and the data-filter table behaviour in app.js, plus the step 3 handoff.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NHyYupFdBiR4VfoUM7NjSM
2026-09-03 05:26:52 +00:00

5.9 KiB

Step 3 handoff — dashboard reads: overview, runs, articles

Landed

  • src/web/dashboard/mod.rs: the overview (§9.1) — last-run card built from runs.report_json via RunReport::info_block() (legacy-counter fallback when the report is missing or unparseable), budget-today <meter> per referenced provider plus voyage from db::provider_spend_for_utc_day(now), ratings this week by label, "Unrated picks" (last three issues' picks with no explicit event, each with the step 2 rating widget posting back to /dashboard), a jobs summary read straight from the jobs table (active rows plus the last five finished; the pages themselves are step 6), and the ! lines of Config::check_report. Sparklines are a marked HTML comment for step 6. The module also carries the helpers the read pages share: Pager, SignalsView (the parsed signals_json behind _signals_table.html), allow_listed, like_pattern, Bind/bind_all/ dynamic_query, formatting helpers, and the STAGES/REASONS/ RETRIEVERS allow-lists.
  • src/web/dashboard/runs.rs (§9.2): /dashboard/runs with ?status= and pagination (50), and /dashboard/runs/{id} with header (issue link when the issue exists, previous/next run by id), the funnel from candidate_runs grouped by stage and reason, admission mix, preference state, timings, provider usage, warnings (#warnings anchor used by the overview and the list), top-20 feeds, the config diff (flatten_json + config_diff) against the previous non-dry run that recorded a config, ten near misses via telemetry::near_misses, and the candidates table (100 per page) with the stage/reason/admitted_by/q/flag filters and utility (default)/ rank/triage/quality/fit/title sorts. Each row's title is a <details> that expands _signals_table.html.
  • src/web/dashboard/articles.rs (§9.3): /dashboard/articles (50 per page) joining the best entry, the latest candidate_runs row through the MAX(run_id) subquery (the index idx_candidate_runs_article_run is in migration 0004 — confirmed), both assessments, the current explicit rating and the latest publication; every listed filter and sort; /dashboard/articles/{id} with the six blocks in the plan's order, provider_rejected rows called out as such, run history rows expanding their signals, neighbours/interests from the latest row, embedding metadata only (never the vector), all rating events with source/user/note/value, the rating widget with show_note = true, and "Explain (text)" wrapping telemetry::render_explain verbatim in <pre>.
  • Templates: dashboard/overview.html, runs.html, run.html, articles.html, article.html, partials _signals_table.html, _candidate_row.html and dashboard/_pager.html. Every table sits in .scroll-x; badges per stage/reason/label/status; tables carry data-filter.
  • app.css / app.js: one appended block each (/* step 3 … */): cards, filters, funnel bar, pager, signals details, diff colours, and the filter-as-you-type behaviour for table[data-filter] (inserts a search input before the table's .scroll-x wrapper, hides non-matching rows on the current page only).

Deviations and notes

  • sqlx 0.9 dynamic SQL. sqlx::query only accepts &'static str or AssertSqlSafe, so the list queries go through dashboard::dynamic_query, which wraps the assembled string in AssertSqlSafe. The audit holds because the string is built only from constants and allow-listed fragments (CANDIDATE_SORTS/ARTICLE_SORTS map names to fixed ORDER BY text); every user value is bound through bind_all. An unknown sort or filter value falls back to the default / is dropped, never an error.
  • Funnel semantics. stage records where a row stopped, so the bars show the cumulative "reached" count (rows at this stage or a later one, so the first bar is everything considered) with "stopped here" and the reason breakdown beside it. Widths are <svg width="N%"> children of .funnel because the CSP (style-src 'self') forbids inline style attributes; the budget card uses <meter> for the same reason.
  • Prev/next run links go to the neighbouring run ids overall (the header already links the date's issue); a rerun of the same date is therefore the immediate neighbour.
  • Extract method is not shown on the article page: db::article_from_row hard-codes ExtractMethod::Miniflux for every loaded article, so the value would be meaningless. excerpt_only is shown instead. Source kinds render via their Debug names (Feed, Scour, …).
  • admitted_by filter is allow-listed against the six retriever names and still applied as the plan's prefix LIKE on json_extract(admitted_by, '$[0]').
  • The _pagination.html partial from step 1 (text only) was left alone; the dashboard uses its own dashboard/_pager.html with prev/next links that preserve the other query parameters.
  • Only additive edits outside my files: two appended blocks in app.css and app.js. No changes to web/mod.rs, db.rs, rate.rs or telemetry.rs (a private first_retriever in telemetry was reimplemented as admitted_by_parts rather than made pub).

Left for later steps

  • Step 6: overview sparklines (placeholder comment in overview.html), and the /dashboard/jobs pages the overview's job links point at.
  • Step 4: /dashboard/ratings, linked from the overview's ratings card.
  • The overview's ratings-this-week card counts all explicit events in the last seven days by label (a cleared is listed but not counted in the total).

Verification (outside the sandbox)

  • cargo fmt: pass.
  • cargo clippy --all-targets -- -D warnings: pass.
  • cargo test web::dashboard: 11 passed, 0 failed.
  • cargo test (full suite): 387 lib tests passed, 0 failed, plus every integration test binary green (7, 2, 3, 4, 7, 9, 2), nothing skipped.