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
5.9 KiB
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 fromruns.report_jsonviaRunReport::info_block()(legacy-counter fallback when the report is missing or unparseable), budget-today<meter>per referenced provider plus voyage fromdb::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 thejobstable (active rows plus the last five finished; the pages themselves are step 6), and the!lines ofConfig::check_report. Sparklines are a marked HTML comment for step 6. The module also carries the helpers the read pages share:Pager,SignalsView(the parsedsignals_jsonbehind_signals_table.html),allow_listed,like_pattern,Bind/bind_all/dynamic_query, formatting helpers, and theSTAGES/REASONS/RETRIEVERSallow-lists.src/web/dashboard/runs.rs(§9.2):/dashboard/runswith?status=and pagination (50), and/dashboard/runs/{id}with header (issue link when the issue exists, previous/next run by id), the funnel fromcandidate_runsgrouped by stage and reason, admission mix, preference state, timings, provider usage, warnings (#warningsanchor 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 viatelemetry::near_misses, and the candidates table (100 per page) with thestage/reason/admitted_by/q/flagfilters andutility(default)/rank/triage/quality/fit/titlesorts. 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 latestcandidate_runsrow through theMAX(run_id)subquery (the indexidx_candidate_runs_article_runis 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_rejectedrows 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 withshow_note = true, and "Explain (text)" wrappingtelemetry::render_explainverbatim in<pre>.- Templates:
dashboard/overview.html,runs.html,run.html,articles.html,article.html, partials_signals_table.html,_candidate_row.htmlanddashboard/_pager.html. Every table sits in.scroll-x; badges per stage/reason/label/status; tables carrydata-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 fortable[data-filter](inserts a search input before the table's.scroll-xwrapper, hides non-matching rows on the current page only).
Deviations and notes
- sqlx 0.9 dynamic SQL.
sqlx::queryonly accepts&'static strorAssertSqlSafe, so the list queries go throughdashboard::dynamic_query, which wraps the assembled string inAssertSqlSafe. The audit holds because the string is built only from constants and allow-listed fragments (CANDIDATE_SORTS/ARTICLE_SORTSmap names to fixedORDER BYtext); every user value is bound throughbind_all. An unknown sort or filter value falls back to the default / is dropped, never an error. - Funnel semantics.
stagerecords 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.funnelbecause the CSP (style-src 'self') forbids inlinestyleattributes; 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_rowhard-codesExtractMethod::Minifluxfor every loaded article, so the value would be meaningless.excerpt_onlyis shown instead. Sourcekinds render via theirDebugnames (Feed,Scour, …). admitted_byfilter is allow-listed against the six retriever names and still applied as the plan's prefixLIKEonjson_extract(admitted_by, '$[0]').- The
_pagination.htmlpartial from step 1 (text only) was left alone; the dashboard uses its owndashboard/_pager.htmlwith prev/next links that preserve the other query parameters. - Only additive edits outside my files: two appended blocks in
app.cssandapp.js. No changes toweb/mod.rs,db.rs,rate.rsortelemetry.rs(a privatefirst_retrieverin telemetry was reimplemented asadmitted_by_partsrather than madepub).
Left for later steps
- Step 6: overview sparklines (placeholder comment in
overview.html), and the/dashboard/jobspages 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
clearedis 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.