Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NHyYupFdBiR4VfoUM7NjSM
7.6 KiB
7.6 KiB
Step 1 — Foundation
Read docs/plans/briefs/web-dashboard/00-shared.md first, then the plan
docs/plans/2026-09-03-web-dashboard.md. This step is plan §19 item 1. Sections
that govern it: §2 (verified crate facts — read carefully, especially the
axum-login git rev API), §3 (routes and access levels), §4 (architecture, state,
templates, design system), §5 (migration and data model), §6 (auth, sessions,
CSRF, throttle), §7 (public site), §15 (config keys and dependencies), §16
(security checklist), §17 (tests for the parts you build).
Deliverables
- Dependencies (§15):
axum-loginfrom git at rev151c72d7a1b4646830f86b4332e6bd6e34d719a7,password-auth,tower_governor(featureaxum),time,async-trait,rpassword,toml_edit,toml(promote to direct), devtowerwithutil. Usecargo add; do not hand-pin old versions; do not addtower-sessionsorargon2separately. Confirmcargo tree -dshows a single sqlx and a singlelibsqlite3-sys. - Migration
migrations/0004_web.sqlexactly as §5.1 (users, sessions + indexes,rating_events.user_id,config_changes,profile_versions,jobs,runs.report_json,issues.issue_json), plus the indexidx_candidate_runs_article_run ON candidate_runs(article_id, run_id DESC)from §9.3 (put it in this migration so step 3 needs no new file). - Data model changes (§5.2–§5.4):
RatingEvent.user_id: Option<i64>(bound byappend_rating_event, read back bycurrent_ratings*/rated_article_from_row);db::finish_runwritesruns.report_json;pipeline::record_issuewritesissues.report_json(fixing the never-written column) andissues.issue_json(theIssuewith everypick.article.content_htmlemptied);GenerateOutcome.run_id: i64. Add thedbaccessors the loader needs (issue_by_date,issue_dates/listing for the archive,latest_issue_date). src/web/skeleton (§4):mod.rs(WebState,Html<T>,WebError,Pagelayout context, pagination + time helpers),session.rs(SqliteSessionStore,Backend,Credentials,AuthSessionalias,Viewer,require_same_originmiddleware),users.rs(Role, User with redacting Debug, hash/verify wrappers, username/password rules, the CLI operations),public.rs(PublicIssue+/,/issues,/issues/{date}public branch,/feed.xml,/robots.txt),issue.rs(theIssueViewloader withissue_json→ row fallback per §5.2; the full page template is step 2 — for now a signed-in viewer on/and/issues/{date}may see the public rendering plus the downloads list, and the loader must be complete and tested),static/{app.css,app.js,favicon.svg}served from/static/{file}with sha256 ETag / 304 andCache-Control: public, max-age=86400, and the templateslayout.html,error.html,login.html,account.html,issue_public.html,issue_list.html,feed_entry.html,_pagination.html.app.cssimplements the §4.4 design tokens, masthead, reading column, the dashboard table/badge/kv/funnel/spark/rating classes (so later steps only add to it), light and dark.app.jscan be minimal now (thedata-confirmand<details>persistence bits); the rating fetch enhancement is step 2.AppStaterefactor (§4.2):config: Arc<RwLock<Arc<Config>>>,config_path: Option<PathBuf>,web: Arc<WebState>,AppState::config();server::serve(config, config_path, db);main::servepassesConfig::resolve_path(cli.config). Existing handlers callstate.config().WebState { jobs: Arc<dyn JobRunner>, started_at, config_mtime }— define a minimalJobRunnertrait +DisabledRunner/MockRunnerinsrc/web/mod.rsor a smallsrc/web/dashboard/jobs.rsstub now so the state shape is final (step 6 fills inSystemdRunnerand the pages).- Auth (§6): session layer, auth layer, governor on
POST /login(into_make_service_with_connect_info::<SocketAddr>inserve; a periodicretain_recenttask; a periodicdelete_expiredtask),/loginGET/POST,/logout,/account(change password, sign out everywhere), the origin-check middleware on every POST except/r/*,login_required!/permission_required!route layers on the sub-routers (a/dashboardstub router with an admin-only placeholder overview page is fine so the guard tests in §17 can run now), and the 403 → site error pagemap_response./files/*accepts a session or Basic auth per §8's last paragraph. - Security headers (§16) on every app response: CSP, nosniff, referrer
policy;
Cache-Control: no-storeon/dashboard/*;Vary: Cookieon HTML; public pagespublic, max-age=300only without a session cookie. - CLI
daily-epub users add|passwd|role|disable|enable|list|logout(§6.1), none of which take the run lock;users add --adminbootstraps. - Config (§15): the new
[server]keys with validation,Config::default(),config.example.toml, README table (the key-for-key test must pass). - README: the route table gains the new public routes and the
session-or-Basic note on
/files/*; a sentence thatSmartIpKeyExtractortrustsX-Forwarded-Forbecause only nginx reaches the bind address. - Tests from §17: migration, users/passwords, session store, login flow
(cookie flags, disabled user, password change logs out other sessions,
logout, anonymous
/sets no cookie,nextvalidation), guards, origin check, throttle (burst 3 test config), public rendering (public_issue_carries_no_generated_text, comment links, no World Briefing, feed is valid XML with one entry per issue, robots.txt, cache headers), issue_json round trip + fallback loader +runs.report_json/issues.report_jsonwritten and/issues.jsonreturning real reports, and the/files/epubsession-or-Basic behaviour. The binary-level test intests/m7_server.rs(serves/,/issues,/feed.xml,/login;/dashboardredirects;users addthen a real login over TCP) — add it, but remember it cannot run inside your sandbox.
Notes and traps
- Read the axum-login source at the pinned rev under
~/.cargo/git/checkoutsaftercargo addto confirm theRequirebuilder,AuthSession::user().await,login(&self, &user), the macros' signatures, and the session data key"axum-login.data". §2 describes what was verified; trust the source over the plan's sketch if they differ, and record the difference in your handoff. tower-sessions0.15SessionStorestill usesasync_trait; axum-login's own traits do not.password_auth::verify_passwordblocks — run it inspawn_blocking, and use a fixed dummy hash for unknown usernames so timing does not reveal existence.- The governor's
per_secondtakes seconds per token:login_window_minutes * 60 / login_attempts. - The
sessions.user_idcolumn is denormalized best-effort fromrecord.data["axum-login.data"]["user_id"]— verify the actual shape axum-login stores (it may be a struct withuser_idandauth_hash). PublicIssue::from(&Issue)is the only constructor and carries no generated text; the no-leak test rendersfixtures::issue()publicly and asserts the Brief, every summary, everywhy, article body sentences and comment strings are absent.- The
serve_fileBasic-auth fallback must keep the existing OPDS client behaviour byte for byte when no session cookie is present. - Keep
server.rsas the router root:router()mergesweb::router()sub-routers; do not move the existing handlers.