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
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
# 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 `kind`s 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.
|
||||
Reference in New Issue
Block a user