Restyle every template under src/web/templates/dashboard/ plus the shared partials _candidate_row.html, _signals_table.html and _pagination.html with Tailwind utilities and the design system step 2 built, so the dashboard reads as the sans-serif instrument-panel half of the same publication as the reader pages: a page header on every page, stat tiles, compact tables with sticky heads inside .scroll-x, tinted badges, one inline filter form per page, settings groups with a sticky save bar, job cards, a scrolling journal block and a rating widget that fits inside a table cell. tailwind.css gains the dashboard vocabulary (.page-head/.page-desc/ .page-actions, .tiles/.tile*, .cell-wrap, .scroll-x.tall, .filters, .pager/.tabs, .setting*/.save-bar, .disclosure, .sparklines/.spark-figure, td .rating overrides, tr.superseded) plus a styled <meter> and a disabled control state. Renames class="inline" to form-inline: Tailwind emits an .inline display utility that was beating form.inline's layout. Three small backend changes: job cards show the status and time of their last run (attach_last_runs, no extra query, unit-tested); the Stats page passes "stats" as its nav key so the admin nav no longer highlights Overview; and the settings load-error banner drops its "! " prefix, with its assertion updated. cargo test: 477 passed. npm run css:check clean. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NHyYupFdBiR4VfoUM7NjSM
8.8 KiB
Step 3 handoff — dashboard restyle
Branch v2-dashboard, worktree /home/thallada/workspace/the-daily-epub-v2c, one commit
on top of the merged v2-frontend work (step 1 + step 2).
Landed
Every template under src/web/templates/dashboard/ plus the shared partials
_candidate_row.html, _signals_table.html and _pagination.html is restyled with
Tailwind utilities and the component layer step 2 built. src/web/static/app.css is
rebuilt and committed; npm run css:check is clean.
New/changed pieces in src/web/tailwind.css
@layer base gained: a disabled state for every control, summary { cursor:pointer },
and a styled <meter> (webkit + moz pseudo-elements, --rule track, --good value,
--down when .over) for the overview budget bars.
@layer components gained the dashboard vocabulary the templates now use:
| class | what it is |
|---|---|
.page-head, .page-desc, .page-actions, .page-eyebrow |
the page header on all 13 pages: eyebrow/breadcrumb, h1, one-line description, right-aligned actions, hairline under it |
.tiles, .tile, .tile-label, .tile-num, .tile-delta |
stat tiles (overview, stats, run detail) |
.cell-wrap, .cell-tight |
long text cells (message, editor_why, notes, reasons) wrap with overflow-wrap:anywhere and a max width; dates/badges stay on one line |
.scroll-x.tall |
opt-in max-h-[70vh] overflow-y-auto, which is what actually makes the base layer's thead { sticky top-0 bg-paper } do something. Applied to the long tables (runs, candidates, articles, ratings, jobs history, users, settings history, stats) and deliberately not to the small tables inside cards |
.filters / .filter-actions / .form-inline |
one inline filter form per page: rule-bounded row, tiny uppercase labels above each control, buttons grouped at the end |
.pager, .tabs, .pagination |
pagination row and the ratings tab strip (accent underline on .active) |
.setting, .setting-label, .setting-input, .help, .default, .secret, button.reset, .save-bar |
settings: two-column rows collapsing to one on phones, monospace keys, disabled env-locked fields with a locked badge and a lock note, sticky save bar with a soft top shadow |
.disclosure (details.disclosure + .disclosure-body) |
the framed <details> blocks (ratings "How ratings enter the algorithm", stats "As text", article "Explain") |
.sparklines, .spark-figure, .spark-axis, .spark-legend |
sparkline cards; the SVG classes (.spark, .s0….s5, .line) are untouched |
.funnel, .funnel-cell |
the funnel bar gets a bg-paper-2 track; class="funnel" is unchanged because a test asserts it verbatim |
td .rating / .picks .rating overrides |
the rating widget compacted for table cells and the overview picks list: prompt hidden, min-h-8 buttons, small note field, capped width |
tr.superseded |
muted row (the class stays exactly superseded; a test counts it) |
.badge default |
bare <span class="badge"> now has a neutral tint, plus .badge.restart / .badge.warn |
.dashboard gained a vertical rhythm (> * + * { mt-4 }), section headings as tracked
uppercase labels on a rule (.dashboard > h2), muted underlined table links, and
code chips on bg-paper-2.
Per page
- overview — five stat tiles (last run, warnings, verdicts this week, unrated picks, active jobs) above the existing cards; budget rows are now provider + spend + a meter bar; jobs/picks lists are hairline-separated with ink links.
- runs — page header with Stats/Jobs actions, inline status filter with a Reset that only appears when a filter is set, numeric columns right-aligned, empty-state row.
- run — breadcrumb + status badge header, prev/issue/next as buttons in the header, four tiles (date, duration, cost, warnings), funnel with a track, cards for the report, empty-state row for candidates.
- articles / article — filter form, wrapped title cells; the article page splits its facts into "Article" and "Provenance" cards, gives the rating widget its own "Your verdict" section and keeps everything else as tables.
- ratings — description as the page header, tab strip, per-tab filter form, the in-cell rating widget, muted superseded rows.
- profile — editor/preview split (
lg:grid-cols-2, stacked on phones), monospace textarea, previews as cappedpre.previewblocks, history table,.kvtheme list. - stats — window buttons in the header,
summaryrendered as six stat tiles, sparkline cards, cards, two filterable tables, text output in a disclosure. - jobs / job — job cards now carry a status badge and a "last started" line linking
to that job (see the one backend change below); the job page is breadcrumb + two cards
- a scrolling monospace journal.
- settings / settings history / users — as described in the table above.
Backend changes (three, all small)
JobCardgainedlast_status/last_requested/last_id, filled by a newattach_last_runsfrom the job rows the page already loads (matchingnameorname-<date>for the datedgeneratecard). No extra query. Unit-tested.stats::statspassed"dashboard"as its nav key, so the admin nav highlighted Overview on the Stats page; it now passes"stats".settings.htmldropped the!prefix in front of the "config.toml on disk does not load" banner (the.errorbar already says it); the assertion insettings_pages_render_save_and_show_hand_editswas updated with it. That is the only test touched.
Deviations
class="inline"was a latent bug, the same shape as step 2's.blockcollision: Tailwind emits an.inline { display:inline }utility, which beatsform.inline's@apply flexin the components layer, so those forms were laid out inline. The class is renamed toform-inlineinjobs.html,profile.htmlandsettings.htmland intailwind.css. Nothing asserted on it.- Sticky table heads need a scroll box.
thead { sticky top-0 }from step 2 is inert inside a plainoverflow-x:autocontainer, so the long tables opt into.scroll-x.tall(max-h-[70vh]). Small tables inside cards keep the plain.scroll-x. - The brief asks for "last run" on the job cards; the data was not in
JobCard, so it was added (item 1 above) rather than skipped. _rating_widget.htmlitself is untouched — two tests assert its exact attribute order (value="loved" data-label="loved" class="active"), so the table-cell treatment is done purely withtd .rating …rules..dashboard > h2renders section headings as small tracked uppercase labels on a rule rather than as large sans headings;.card h2/h3stay normal-weighttext-base.
Hooks and asserted classes (all preserved)
form.rating, button[data-label], .active + aria-pressed, form[data-confirm],
details[id] (ratings-how, profile-prompt, plus new stats-text and article-explain),
table[data-filter], .table-filter, button[data-reset]/data-default, [data-refresh],
.scroll-x, .spark/.s0–.s5/.line, class="funnel", class="superseded",
badge <state>, <h1>Users</h1>, every form action/name/id and every aria-*.
Verification
npm run css+npm run css:check— clean.cargo fmt,cargo clippy --all-targets -- -D warnings— clean.cargo test— 477 passed, 0 failed (441 lib + 9 + 2 + 3 + 4 + 7 + 9 + 2 integration, one binary with 0 tests). Run outside the Codex sandbox, so the socket-binding tests ran.- Dev server on
127.0.0.1:3601(port 3599/3602 belong to other agents, 3499 to production). Every dashboard route returned 200, with no console errors and no asset 404s in the server log.
Screenshots
/tmp/claude-1000/-home-thallada-workspace-the-daily-epub/e963db53-510f-4312-ac87-460291a92781/scratchpad/step3-shots/
— 58 PNGs, all signed in as admin: /dashboard, /dashboard/runs, /dashboard/runs/2,
/dashboard/articles, /dashboard/articles/1, /dashboard/ratings,
/dashboard/ratings?tab=events, /dashboard/profile, /dashboard/stats,
/dashboard/jobs, /dashboard/jobs/1, /dashboard/settings,
/dashboard/settings/history, /dashboard/users × {light, dark} × {1280px desktop,
390px mobile}, full page, plus viewport shots of the settings save bar
(*-top.png, desktop light/dark and mobile).
The throwaway ./dev database was extended by hand for the shoot (four jobs rows, eight
rating_events, four config_changes) so the jobs, ratings and settings-history pages had
content; dev/ is git-ignored and examples/seed_dev_db.rs was not changed. A future
design pass may want those rows in the seeder.
Left for later
- Step 4 adds the collapsing table-of-contents sidebar to the signed-in issue pages.
examples/seed_dev_db.rsstill seeds no jobs, ratings or config changes.