Conversation
The viewer now surfaces ingestion state without gaining any ability to change it, closing the watch-from-the-browser phase of the #26 design. Server (src/bin/web.rs): - GET /api/status: merges the read-only ingest_runs view (active_run, last_run, mixed_sources), a flock probe of the Phase A lockfile (open + try-lock + release, never truncating, never holding), db readiness as ready|missing|empty, owns_active_run (always false this phase), and viewer-derived rate_per_sec/eta_seconds from an in-memory sliding window of successive observations - nothing is ever persisted. Also carries the per-process CSRF token (32 CSPRNG bytes, hex, minted at startup) that Phase C's mutating routes will require; no endpoint consumes it yet. Never 5xxs for a missing or empty database so onboarding always renders; a missing ingest_runs table means no runs. - GET /api/runs?limit=N: run history straight from ingest_runs, newest first, limit clamped to 1..=100 (default 20); missing table or file is an empty list. - Both routes sit behind the existing Host guard + CSP/nosniff/ no-referrer middleware; no CORS headers anywhere; the pool stays read_only + query_only. GMAIL_STATS_DB now points the viewer at a database elsewhere (default ./stats.db unchanged). Shared code: src/lib.rs now exposes the ingest and mbox modules as a library so the viewer reuses Phase A's lockfile naming and migrations instead of duplicating them; src/main.rs consumes them via the crate. Front-end (web/): - Onboarding cards (db missing/empty, idle): OAuth scan and Takeout import with copyable CLI commands, the Advanced Protection policy_enforced callout, and the ~2-day Takeout export warning. Launch buttons are deliberately absent until Phase C. - Observe-only progress panel: source badge, state chip, seen/new counts, msg/s, determinate bar + ETA when a total exists (bytes for imports, total_estimate for scans), indeterminate sweep otherwise, display-only stalled hint when the heartbeat is older than 15s, and a started-from-the-terminal note for non-owned runs (all of them, this phase). - Header pill during active runs; /api/summary re-fetched every ~3.5s while a run is active, preserving all client state (search, page, page size, hidden senders); busy 503 keeps the previous table and dims the freshness stamp. - History strip from /api/runs plus the interim mixed-sources caution. - Polling: 1s while active, decaying to 10s idle, paused while the tab is hidden. - Static/demo hosting degrades cleanly: when /api/status 404s the entire ingestion UI hides and status polling stops. All demo build anchors are intact and every dynamic string still renders via createElement/textContent only. Tests: endpoint tests over the real router (tower oneshot) for missing/ empty/pre-Phase-A databases, active+finished runs, mixed sources, the non-truncating lock probe, runs ordering/limits, security headers and no-CORS on the new routes, host guard, CSRF token stability, and the rate window/ETA math.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #28
Phase B of the #26 design ("watch from the browser"): the viewer surfaces ingestion state while staying strictly read-only — no DB writes, no state-changing endpoints, no CORS, zero new attack surface.
Server
GET /api/status— the single cheap poll target:db: ready|missing|empty(drives onboarding; a database file whose tables were never created reads asempty). Never 5xx for a missing/empty DB; onlybusy/unexpected errors use the existing 503/500 contract.active_run/last_runfrom the read-onlyingest_runsview; a missingingest_runstable (pre-Phase-A database) is simply "no runs".ingest::ingest_lock_path, reused via a new thinsrc/lib.rsrather than duplicated): open withcreate(true).truncate(false)exactly like the ingester, non-blocking try-lock, immediate release. Never truncates, never holds, detects "something is running" even before the first run row exists (verified by a test that plants sentinel bytes in the lockfile and locks around the probe).owns_active_run: falsealways (the viewer spawns nothing this phase),mixed_sources(interim caution, retired by Phase D).rate_per_sec/eta_seconds: computed in viewer memory from a 30s sliding window of successive status observations, reset on run change — never persisted anywhere. ETA prefers byte progress (exact for imports) and falls back tototal_estimatefor scans.csrf_token: 32 bytes from the OS CSPRNG, hex, minted at startup, per the Design proposal: drive scans and imports from the web UI #26 CSRF design. No endpoint consumes it until Phase C; shipping the slot now means it is only ever obtainable via this same-origin GET.GET /api/runs?limit=20— history straight fromingest_runs, newest first, limit clamped to 1..=100; missing table/file ⇒{"runs": []}.Both routes sit behind the unchanged Host guard + CSP
default-src 'self'+ nosniff + no-referrer middleware; tests assert the headers are present on the new routes and that noAccess-Control-*header is ever emitted. The pool staysread_only(true)+query_only=ON; a test asserts/api/statusagainst a nonexistent path does not create the file. The viewer also honorsGMAIL_STATS_DB(default./stats.dbunchanged), matching Phase A's path flexibility.Front-end (all new DOM via
createElement/textContent; CSP untouched)policy_enforcedcallout, and Takeout import with the ~2-day export warning — each with the exact CLI command and a Copy button. No launch buttons (Phase C).<details>): source badge, state chip, seen/new counts, msg/s, determinate bar + ETA when a total exists (bytes for imports,total_estimatefor scans), indeterminate sweep otherwise; display-only "stalled?" hint when the heartbeat is older than 15s (compared against servernow_unix, not the client clock); non-owned runs say "started from the terminal — cancel it with Ctrl-C there"./api/summaryre-fetched every ~3.5s during active runs plus once on completion, preserving search/page/page-size/hidden-senders (all client-side);busy503 keeps the previous table and dims the freshness stamp./api/runs(failed runs showerror_kind: erroras inert text) and the interimmixed_sourcescaution banner.document.hidden(immediate poll on return)./api/statushides every piece of ingestion UI and stops status polling for good;demo/build_demo.pybuilds with all anchors intact — thefetch("/api/summary")anchor still occurs exactly once, and the new/api/status+/api/runsfetches are separate call sites.src/lib.rsexposesingest/mboxas a library (the extraction #26 anticipated) so the viewer reuses Phase A's lock naming and DDL/migrations instead of copying them;src/main.rsnow consumes them through the crate — no behavior change.Testing
cargo build/cargo test(66 tests: 22 lib, 31 scanner, 13 viewer) /cargo clippy --all-targets -- -D warnings/cargo fmt --checkall clean;python3 demo/build_demo.py <tmp>succeeds.tower::ServiceExt::oneshotagainst temp databases created with Phase A's actualingest::migrateDDL: status for missing DB (200,db:"missing"), empty DB, pre-Phase-A DB withoutingest_runs, populated DB with an activerunningrow + a finishedfailedrow (field-level assertions incl.error_kind),mixed_sourcesflip, the non-truncating lock probe under a real held flock,/api/runsordering/limit/garbage-limit/missing-DB, security headers + no-CORS on both new routes, host-guard 403s, CSRF token stability/uniqueness, and unit tests for the rate-window/ETA math.db:"missing"(cards, commands, callouts; stats/panel/pill hidden); an active mbox run renders the pill ("Importing · 12,004 messages"), determinate 61% bar, "840.5 msg/s", "~23s left", running chip, terminal-origin note, mixed-sources banner, and history incl. a failed run whose<script>-bearing error string renders inert; a stalled heartbeat shows the display-only hint; the built Pages demo (no/api/status) shows stats fromsummary.jsonwith every ingestion element hidden. 28/28 assertions pass.db:"missing", lock free), then rangmail_stats importfrom a terminal-style background process. Polling/api/statusobserved the run 36 times:bytes_doneadvanced monotonically 0 → 12,099,390 withingest_lock_held:truethroughout and viewer-derived rates (~2,500–3,800 msg/s) and ETAs; final status showedactive_run:null,last_run.state:"done", 60,000 seen/new, lock released, and/api/summaryreported 400 senders / 60,000 messages. A live headless-Chrome DOM dump mid-import showed the pill ("Importing · 44,924 messages · 75%"), the running chip, the 74.9% bar, and the terminal-origin note.Deferred / adjusted (relative to #26's full UI-flow text)
awaiting_authauthorize-link flow, resume/retry buttons, remediation buttons on failures) is Phase C by design; failed runs surface here via the history strip witherror_kind+ error text.resume_tokenandmbox_fingerprintare internal resume bookkeeping and are not exposed in the API payloads; everything else on the run row is.<details>element rather than custom JS.Part of the incremental plan in #26 (Phase B).