Web dashboard step 7: docs, users page, polish
Users page, README and config.example updates, implementation notes, the rollout runbook, site-layout 404/500 pages, human-readable download sizes, dark-mode and narrow-screen polish, and a smoke test over every dashboard route; also removes zdiff3 ancestor markers left by earlier merges. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NHyYupFdBiR4VfoUM7NjSM
This commit is contained in:
@@ -221,3 +221,59 @@ implementer needs that are easy to get wrong:
|
||||
`assessment_reuse_days`, `--rescore` ignores it, and `admit::hygiene` never treats it as a low
|
||||
score. Because a recovered article's row carries the editor's model, reuse accepts rows whose
|
||||
`model` is either configured model (`triage::reusable_models`).
|
||||
|
||||
## Web dashboard (2026-09-03)
|
||||
|
||||
Verified on the production host and against the implemented dependency graph on 2026-09-03:
|
||||
|
||||
- **Host authorization and units:** polkit 124 supports JavaScript rules. The installed rule must
|
||||
grant user `daily-epub` only `org.freedesktop.systemd1.manage-units`, verb `start`, for units
|
||||
matching `^daily-epub-job@[a-z0-9-]+\.service$`. `daily-epub.service` now needs
|
||||
`SupplementaryGroups=systemd-journal` to read job logs and `/etc/daily-epub` in
|
||||
`ReadWritePaths` to atomically replace `config.toml`. The job template deliberately omits
|
||||
`MemoryDenyWriteExecute` because generate jobs can launch Node's JIT; the server retains it.
|
||||
- **Settings writes:** direct `toml_edit 0.25` performs typed, comment/order-preserving updates.
|
||||
A candidate file is loaded through `Config::load` before its permissions are copied and it is
|
||||
renamed over the original. The process therefore needs write access to the containing
|
||||
directory, not only the file. Every changed dotted key is written to `config_changes`.
|
||||
- **Authentication stack:** `axum-login` is pinned to git revision
|
||||
`151c72d7a1b4646830f86b4332e6bd6e34d719a7`, whose graph contains `tower-sessions 0.15`.
|
||||
The local `SqliteSessionStore` uses this crate's existing sqlx 0.9 pool because the published
|
||||
tower SQLx store is incompatible. `password-auth 1.0` supplies Argon2id PHC hashes;
|
||||
`tower_governor 0.8` throttles login POSTs. Its smart IP extractor trusts forwarded headers,
|
||||
so production must bind to loopback and accept traffic only from the configured reverse proxy.
|
||||
- **Test environment:** router tests use `tower::ServiceExt::oneshot`, temp SQLite databases and
|
||||
`MockRunner`; they do not bind or invoke systemd. This sandbox forbids loopback listeners, so
|
||||
the four `curate::llm::tests::anthropic_*` tests, the three OpenAI tests using the same fake
|
||||
listener, `extract::tests::relative_urls_resolve_against_the_url_we_landed_on`, the five
|
||||
listener-based `server::tests::*`, and `tests/m7_server.rs` are filtered only for sandbox runs.
|
||||
The complete suite is expected to run outside the sandbox.
|
||||
|
||||
Implementation-time decisions recorded while landing dashboard steps 1–7:
|
||||
|
||||
- `/files/*` remains public when Basic auth is not configured, preserving existing OPDS
|
||||
acquisition behavior. With Basic auth configured, either valid Basic credentials or any valid
|
||||
web session authorizes a download. This intentionally resolves the plan's conflicting request
|
||||
to redirect unauthenticated downloads in favor of its compatibility acceptance criterion.
|
||||
- Final reports are attached to an issue after `finish_run`, when publish timing and status are
|
||||
complete. Issue snapshots omit article bodies and rehydrate them from `articles`; old rows use
|
||||
the reduced fallback renderer. The CLI password prompt uses `rpassword` after it became
|
||||
available to the orchestrator.
|
||||
- Flash handlers extract the exact tower session installed by the auth layer through
|
||||
`Extension<Session>`. Dashboard `down` forms persist the established `not_for_me` label.
|
||||
Ratings-page feed credit includes rating decay because that is what `signals::feed_rates`
|
||||
actually uses; the stored prompt verdict count is inferred from the prompt text because
|
||||
`TasteProfile.verdicts` is not persisted.
|
||||
- Dynamic list SQL is assembled only from fixed fragments and allow-listed sort/filter names,
|
||||
wrapped in sqlx 0.9's `AssertSqlSafe`; all user values remain bound parameters. Funnel bars
|
||||
count rows that reached each stage because a row stores the stage where it stopped. SVG/meter
|
||||
attributes replace inline styles under the CSP. Article pages omit the nominal extract method
|
||||
because database reconstruction currently hard-codes it and would display misleading data.
|
||||
- Shipped providers cannot be removed: deleting one would cause `Config::default()` to restore
|
||||
it. They remain editable and may be unreferenced; custom providers are removable. An absent
|
||||
setting already equal to its submitted default stays absent. Settings that are captured while
|
||||
building the server, session, or throttle layers carry restart notices.
|
||||
- A job start reloads a hand-edited config before inserting and starting the unit, retaining the
|
||||
last-good config if reload fails. The offline lifecycle test uses `features-prune`; generate
|
||||
run-id linkage is covered separately. CLI duration statistics retain finished dry runs for
|
||||
byte-identical output, while dashboard run series and overview sparklines exclude them.
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
# Step 7 handoff — docs, users page, and polish
|
||||
|
||||
## Landed
|
||||
|
||||
- Added the admin-only, read-only `/dashboard/users` page with username, role, enabled/disabled
|
||||
status, created time, last login, and unexpired session count. The page directs edits to the
|
||||
existing `daily-epub users` CLI.
|
||||
- Expanded the README with the public/user/admin boundary, user lifecycle commands, complete
|
||||
route table, systemd+polkit Jobs setup, comment-preserving Settings behavior, environment
|
||||
locks, shipped-provider removal limitation, public `/files/*` behavior without Basic auth,
|
||||
and the `X-Forwarded-For`/loopback trust boundary. Updated `[server]` comments in
|
||||
`config.example.toml` to match.
|
||||
- Added the dated Web dashboard section to the implementation notes: polkit 124 and its narrow
|
||||
rule, service-unit changes, the `toml_edit` write path, the pinned auth/session/password/
|
||||
throttle stack, sandbox test caveats, and the implementation-time decisions from steps 1–6.
|
||||
- Added `docs/runbooks/web-dashboard-rollout.md`, with the plan's nine production rollout steps,
|
||||
commands, permissions, public-files compatibility note, and smoke checks.
|
||||
- Completed layout polish: Atom discovery and favicon remain in `<head>`, Users remains in the
|
||||
admin nav, both navs now wrap cleanly, narrow cards/forms/rating controls collapse, dark-mode
|
||||
form controls use the site colors, focus states are visible, and all wide tables (including
|
||||
the nested signals table and new users table) sit in `.scroll-x`.
|
||||
- Unknown routes and template-render failures now use the site-layout 404/500 pages. Issue
|
||||
download buttons show human-readable B/KB/MB/GB sizes.
|
||||
- Removed stale diff3 ancestor markers from the merged CSS/JavaScript and restored the missing
|
||||
table-filter closure; `node --check src/web/static/app.js` passes.
|
||||
- Added the users guard/content test, file-size tests, site-layout error test, and one
|
||||
fixture-backed router smoke test that renders every dashboard route template.
|
||||
|
||||
## Acceptance criteria walk-through
|
||||
|
||||
1. **Public issue and Atom boundary — met.** `PublicIssue` cannot carry generated/body fields;
|
||||
existing fixture tests assert titles/authors/sources/comment links/metadata are present and
|
||||
private Brief/summary/why/body/comment/World text is absent. The feed parses as XML and uses
|
||||
the same public view.
|
||||
2. **Complete signed-in issue — met.** Existing router tests cover Brief, summaries, why lines,
|
||||
article bodies/discussion, World and Behind pages, reduced pre-snapshot fallback, and
|
||||
existence-gated downloads. All three artifact slots share the same loader; sizes are now
|
||||
human-readable.
|
||||
3. **Admin-only ratings — met.** Whole-router role guards and rating tests cover anonymous,
|
||||
user, and admin outcomes, attributed `source = 'dashboard'` events, clear events, and the
|
||||
unchanged HMAC path.
|
||||
4. **Article history/explainability — met.** The articles list/detail routes expose the latest
|
||||
stage plus full run history, reasons, signals, assessments/facets, utility/cluster/admission,
|
||||
editor why, issue appearances, neighbours, embedding metadata, ratings, and text explain;
|
||||
allow-listed query tests and fixture rendering pass.
|
||||
5. **Run history — met.** Run list/detail renders the cumulative funnel and reasons, admission,
|
||||
preference state, timings, provider costs, warnings, feeds, near misses, config diff, and
|
||||
candidate table; seeded counts, sorts, filters, links, and sparklines are tested.
|
||||
6. **Ratings contributions/history — met.** Current and events tabs cover decay, decayed feed
|
||||
credit, neighbour weight, prompt/rebuild membership, last-run use, append-only edits/clear,
|
||||
annotations, filters, and superseded history with hand-checked tests.
|
||||
7. **Settings — met.** The schema/default/help/env/secret tests cover every config leaf. Writer
|
||||
tests cover typed changes, comments/order/mode preservation, validation before rename,
|
||||
history, providers, and reload-on-mtime. The README records why shipped providers cannot be
|
||||
removed.
|
||||
8. **Profile — met.** Save/restore/version tests cover the atomic editor and parsed preview;
|
||||
the page shows OPML interests, prompt, learned adjustments/staleness, and the rebuild job.
|
||||
9. **Jobs — met.** Fixed-name parsing, polkit/unit files, MockRunner starts/status/logs, duplicate
|
||||
refusal, failure persistence, lifecycle/run linkage, journal tail, reload-before-start, and
|
||||
overview links are present and tested. Pipeline work remains in the separate job unit.
|
||||
10. **Session/security stack — met.** The pinned axum-login/tower-sessions store,
|
||||
password-auth hashes, cookie properties, session invalidation, origin checks, login throttle,
|
||||
CSP/security headers, safe redirect checks, and secret redaction have focused tests.
|
||||
11. **Existing routes — met.** HMAC, OPDS, files, health, and reports remain wired; issue reports
|
||||
are populated. The reviewed compatibility decision keeps files public only when Basic auth
|
||||
is absent, while configured Basic auth or a valid session still gates them.
|
||||
12. **Test coverage — met, with sandbox execution caveat.** All new router tests use `oneshot`;
|
||||
no new test binds or invokes systemd. The filtered full run passed every runnable target.
|
||||
The orchestrator should run the complete suite outside this listener-restricted sandbox.
|
||||
|
||||
## Prior handoff follow-ups
|
||||
|
||||
- Step 1's `rpassword` and direct `toml_edit 0.25` follow-ups were already resolved by the
|
||||
orchestrator; its public-files deviation is now documented.
|
||||
- Step 2's raw download sizes are resolved here.
|
||||
- Step 3's Jobs/ratings targets and overview sparklines, Step 4's settings anchors and profile
|
||||
rebuild action, and Steps 5–6's config reload plus combined unit changes are all present after
|
||||
the merged steps and were included in the smoke/audit pass.
|
||||
- Nothing remains open against §20. Production deployment itself is intentionally left to the
|
||||
operator following the new runbook. Plan §21's product deferrals remain intentional: per-user
|
||||
personalization, OPML editing, body FTS, SSE logs, passkeys, public why lines, old snapshot
|
||||
backfill, and charts beyond the current sparklines.
|
||||
|
||||
## Verification
|
||||
|
||||
- `cargo fmt`: pass.
|
||||
- `cargo clippy --all-targets -- -D warnings`: pass.
|
||||
- `node --check src/web/static/app.js`: pass.
|
||||
- Focused new router/size/error tests: pass.
|
||||
- Raw `cargo test`: **426 passed, 13 failed** before Cargo stopped at the library target; every
|
||||
failure was `PermissionDenied` while binding a loopback listener (the four documented
|
||||
Anthropic tests, three OpenAI tests using the same helper, the extractor test, and five server
|
||||
tests).
|
||||
- `cargo test` with those 13 listener tests plus the two `tests/m7_server.rs` TCP tests filtered:
|
||||
**460 passed, 0 failed, 15 filtered out** across library, binary, integration, and doc targets.
|
||||
|
||||
Reference in New Issue
Block a user