# Secure health-dashboard UI and write-path workflow

Use this reference when evolving a health dashboard from read-only HTML to mobile/touch input.

## Architecture

1. Preserve the approved data and Missingness contracts before changing presentation. UI polish must never turn missing values into zero, “normal”, symptom-free, or safe.
2. Keep the network-facing dashboard process read-only for the health DB and reports.
3. For touch input, validate in the HTTP process, then atomically enqueue a private action file (`0600`) in a dedicated state directory.
4. Consume actions with a separate short-lived, network-isolated worker. Revalidate the exact schema, date range, field allowlist, score ranges, note length, regular-file type, size, and no-symlink condition before calling the existing tested domain workflow.
5. Use a systemd `.path` unit to trigger the one-shot worker. The server gets write access only to the queue; the worker gets the minimum health-data write scope and no network. This also serializes writes from a threaded HTTP server.
6. Make queue processing idempotent. Delete successful actions; quarantine invalid/failed actions without logging health payloads. Regenerate dashboard/report HTML through a same-directory temporary file plus atomic `os.replace()`, never direct truncating writes.

## Browser security

- Bind to `127.0.0.1` by default. Remote private access must use an explicit Tailnet address or authenticated local reverse proxy—never an unauthenticated `0.0.0.0` default.
- Validate every GET/HEAD/POST `Host` against a configured allowlist before routing; never derive trust from the client-supplied Host itself. Keep exact same-Origin checks for writes.
- For document/report routes, use resolved `Path.is_relative_to()` containment plus regular-file checks. String-prefix containment is unsafe (`/allowed-evil` passes `/allowed`), and symlinks must not escape the root.
- Serve pinned local assets; assert version, byte size, and SHA-256 in tests.
- Generate a fresh CSP nonce per HTML response and inject it into every script/style element.
- Do not use `unsafe-inline`; remove `style=` attributes and inline event handlers because a nonce does not authorize them.
- Use `default-src 'none'`, same-origin scripts/styles, `connect-src 'none'`, `object-src 'none'`, `frame-ancestors 'none'`, `base-uri 'none'`, and `form-action 'self'` unless a reviewed feature requires more.
- Add `Cache-Control: no-store`, `nosniff`, `DENY` framing, `no-referrer`, and a restrictive Permissions Policy.
- POST requires exact same Origin/Host, bounded URL-encoded body, one-time CSRF token, constant-time cookie/token comparison, `HttpOnly; SameSite=Strict`, and server-side validation. Consume tokens once.

## Test isolation

- Dependency-inject the SQLite connection or preloaded rows through every analytics helper. Patching only the top-level dashboard DB is insufficient if imported helpers retain a global production DB path.
- Audit **metadata helpers as well as value helpers**: unit lookup, coverage, source/provenance labels, freshness, and fallback metric specifications can silently reopen a global DB even when the daily series itself is injected. APIs such as `unit_for(metric, _rows=...)` must consume the same injected rows.
- Include at least one non-canonical or newly introduced metric in the isolation test (for example a metric whose unit is not in the canonical-unit map). Canonical metrics can mask a hidden fallback because their unit lookup returns before touching the DB.
- In regression tests, deliberately set every subordinate/global DB path to a nonexistent directory and provide a real synthetic row for that non-canonical metric. Assert both successful full-dashboard generation and the expected unit/metadata; an empty synthetic DB does not exercise fallback paths.
- Build the complete schema in a temporary SQLite DB and generate real HTML from empty/synthetic data; avoid snapshots built from production artifacts.

## UX and medical-safety checks

- Today/actions and source freshness describe documentation/data quality only, never health risk. Parse complete supported date/datetime values fail-closed; malformed suffixes are unknown and future values are flagged for review, never clamped to age zero. Separate stable canonical data-day freshness from import/process freshness.
- Baselines require an explicit minimum number of observed values and preserve gaps; label them descriptive (for example, trailing median), not diagnostic.
- Medication/event overlays are markers, not causal explanations. Actual-dose overlays must use a strict allowlist such as `administered`/`verabreicht`; planned, cancelled, or missed doses require separately labelled planning UI and must never look administered.
- Never regenerate or display legacy food keyword/adherence scores when incomplete logging makes non-mentioned exposure implicit. Missing food/exposure remains unknown.
- Avoid green `safe` food labels even for manual entries; use neutral wording such as “bisher persönlich dokumentiert vertragen” and show the limits of timeframe/completeness.
- N-of-1 views remain prospective documentation/hypothesis tools. Put the non-causal, no-therapy/no-challenge-recommendation warning directly beside the UI—not only in docs—and recommend clinician alignment for potentially risky exposure.
- Privacy mode must be ephemeral and not persist state. Print/doctor mode includes Provenance, Missingness, and non-causal disclaimers.
- Include semantic landmarks, skip link, visible focus, ≥44 px touch targets, reduced-motion support, mobile navigation, and high-contrast behavior.

## Queue and receipt review details

For final write-path reviews, trace the complete HTTP → queue → worker path rather than accepting green happy-path tests:

- Treat same-origin as the full origin tuple (scheme, canonical host, and port), while keeping the independent Host allowlist on every supported and rejected HTTP method. If TLS terminates at a reviewed proxy, configure the expected external origin explicitly rather than silently weakening the comparison to `netloc` only.
- Enforce an exact single-value form shape before mutation. Bound `Content-Length`, require URL-encoded content, compare CSRF cookie/body values in constant time, and consume the server-side token once even under replay.
- A queue receipt confirms only durable enqueue, never successful DB application. Keep it opaque, short-lived, server-issued, and one-time. Prefer a same-site cookie or server-side flash mechanism when feasible; if an opaque receipt must briefly cross a redirect URL, ensure it contains no health payload, suppress request logging, send `Referrer-Policy: no-referrer`, consume it once, and replace the history entry immediately. Never put CSRF tokens, scores, dates, notes, patient identifiers, or processing results in URLs or Web Storage.
- Queue limits must be atomic across the actual deployment concurrency model. A process-local lock is sufficient only when systemd guarantees one server process; otherwise use filesystem/OS locking. Count pending actions under the lock, fsync the action file before rename, then fsync the directory. Review whether failed/quarantined files also need a retention/count/byte cap so repeated worker failures cannot create unbounded sensitive storage.
- Validate the queue directory itself as an owned private real directory; do not let an early `resolve()` hide a disallowed configured symlink. Open action files with `O_NOFOLLOW`, then use `fstat()` on the descriptor to check regular-file type, owner, exact `0600` mode, and size before reading. Reject duplicate JSON keys recursively and revalidate the exact payload schema in the worker.
- Explicitly pass the selected DB through mutation, analytics, and regeneration subprocesses. For a noncanonical/synthetic DB, suppress or reject every legacy generator that has an implicit production path. Test this by making production globals inaccessible and verifying the exact subprocess argv.
- Exercise worker partial-failure semantics: if DB mutation commits but regeneration fails, retries must be idempotent and the UI receipt must still say only “queued.” Do not log payloads or subprocess output that could contain notes.

## Verification gates

1. Focused server, CSRF/replay, receipt replay/forgery, queue cap/concurrency/fsync, owner/mode/symlink/size/duplicate-JSON, worker partial-failure/idempotency, Missingness, baseline, and DB-isolation tests.
2. Full test suite, Ruff on changed files, Python compile, `git diff --check`.
3. `systemd-analyze --user verify` for server/path/worker units.
4. Synthetic HTTP/browser smoke: dashboard, local asset, nonce CSP, privacy mode, form controls, no console/CSP errors.
5. Lighthouse accessibility and best-practices audit; inspect any console/DevTools issues rather than accepting the score alone.
6. Independent medical-safety and security reviews.
7. Only then: backup, production deployment, DB integrity/FK checks, runtime hash match, HTTP/CSP smoke, and explicit commit/push approval.
