customer-presentation/CHECKLIST.md
Arlind Ukshini 72590b08bc add mobile view at protected/mobile/ (UA-dispatched)
Desktop is a GSAP/Lenis/d3 animated experience that doesn't hold up
on phones. Rather than retrofitting media queries across 1200+ lines
of scroll-trigger code, add a completely isolated static mobile tree:

- protected/mobile/index.html — one-page static flow covering the
  intro, 12 timeline events, hero, 4 capability cards, Bifrost
  reveal, 3 participation stops, and Join CTA. All copy duplicated
  from the desktop HTML on purpose — a shared data module would
  re-couple the two trees.
- protected/mobile/mobile.css — paper/ink palette, all m-prefixed,
  zero cascade overlap with the desktop CSS.
- protected/mobile/mobile.js — 60-line client: /auth/me check,
  /api/bifrost-join POST + panel swap, /auth/logout. No GSAP, no
  Lenis, no d3.

Routing (server.js):
- GET /timeline now UA-dispatches via MOBILE_UA_RE. Phone UAs get
  the mobile page; everything else gets the desktop page.
- ?view=mobile and ?view=desktop query overrides take precedence
  over the UA sniff — for bad guesses or previewing the other
  version.
- Gating is unchanged: protected/mobile/ is inside protected/ so
  the existing requireAuth + express.static gate covers it.

Docs:
- CLAUDE.md §routing now lists the UA dispatch as step 4.
- PROJECT.md gets a new "Mobile view" section explaining the
  isolation rules (no shared JS/CSS, content duplicated manually).
- CHECKLIST.md gains section H0 with dispatch curl checks, render
  verification on a phone, and an isolation audit that fails if
  mobile classes leak into the desktop HTML or vice versa.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 10:03:13 +02:00

164 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Checklist
Run the relevant section **after every change** before considering it done. Items are ordered: if an earlier one fails, stop and fix before testing the rest.
Notation: run on the VPS unless marked `[local]` or `[browser]`.
---
## A. After any code change (minimum viable smoke test)
- [ ] `sudo systemctl status fenja``active (running)`
- [ ] `sudo journalctl -u fenja -n 20` shows `[bifrost] listening on 127.0.0.1:3000`, no red errors (there is no `[mail] SMTP relay reachable` line anymore — the mail stack was removed)
- [ ] [local] `curl -I https://project-bifrost.fenja.ai/` → 200, with `X-Frame-Options: DENY` and `Content-Security-Policy` headers
- [ ] [local] `curl -I https://project-bifrost.fenja.ai/timeline.js` → 302, `Location: /`
- [ ] [browser, private window] Open `https://project-bifrost.fenja.ai/` → entrance page renders (not timeline)
If all five pass, the site is up and the gate holds.
---
## B. After changes to auth, sessions, cookies, middleware, or `src/`
Do section A, then:
- [ ] [browser, private] Enter invited email → session cookie issued immediately, welcome step appears (no 6-digit code flow anymore)
- [ ] [browser] Click "Learn more" on the welcome step → redirected to `/timeline` showing the timeline page
- [ ] [browser] Hard-refresh (Ctrl+Shift+R) → stays on the timeline (cookie persists)
- [ ] [browser, new private window] Visit `/` → entrance appears (no cookie leak between sessions)
- [ ] [browser] Click logout → lands on entrance → visiting `/timeline` redirects to `/`
- [ ] [browser] Visit `/timeline.js` or `/vendor/d3-array.min.js` directly while logged out → redirects to `/`
- [ ] DevTools → Application → Cookies: `fenja_session` shows `HttpOnly ✓`, `Secure ✓` (prod only), `SameSite=Lax`
## C. After changes to the entrance form or login endpoint
- [ ] Submit a non-invited address → inline "not invited" message, no session issued
- [ ] Submit a malformed email (`foo`, `foo@`, empty) → inline error appears, no request sent
- [ ] Submit > 30 login attempts from the same IP in an hour → rate-limit response (429)
## D. After changes to the timeline / protected pages
- [ ] [browser] Timeline loads fully: globe visible, 12 event cards, dot-nav at bottom, fonts render as true italic (not system oblique)
- [ ] [browser] Scroll works smoothly, card reveal animations fire
- [ ] [browser] Dot-nav switches between Timeline / Overview / Archive views
- [ ] DevTools console: no CSP violations, no 404s for fonts/vendor files
- [ ] DevTools network tab: all `/vendor/*` and `/fenja/fonts/*` requests return 200
## E. After changes to CSP, security headers, or Nginx
Do section A with extra attention to:
- [ ] Response headers on `/` include all six: `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, `Permissions-Policy`, `Content-Security-Policy`, `Strict-Transport-Security`
- [ ] CSP contains at minimum: `default-src 'self'`, `script-src 'self'` (no `unsafe-inline` on scripts), `frame-ancestors 'none'`
- [ ] `sudo nginx -t` → "syntax is ok" and "test is successful"
- [ ] [local] Open timeline in a browser → no red CSP violations in DevTools console
- [ ] `curl.exe -X POST https://project-bifrost.fenja.ai/auth/login -H 'Content-Type: application/json' -d '{"email":"nobody@example.com"}'` returns quickly (rate-limit zone is functioning; expect 403 `not_invited` on a real email not on the list)
## F. After dependency or Node.js upgrades
- [ ] `sudo -u fenja npm ci --omit=dev` completes without errors
- [ ] `sudo -u fenja npm audit` reports no high/critical vulnerabilities in production deps
- [ ] Run section A, then section B (full auth flow)
- [ ] Check `node --version` on the VPS is still 20+
## G. After Nginx config changes specifically
- [ ] `sudo nginx -t` before reloading (catches 95% of errors)
- [ ] `ls /etc/nginx/sites-enabled/` contains only `project-bifrost` (no shadow configs)
- [ ] After reload, `curl -I https://project-bifrost.fenja.ai/` includes `X-Powered-By: Express` (proves Nginx is still proxying to Node, not serving static files)
- [ ] Rate-limit zone still exists: `grep -r "limit_req_zone" /etc/nginx/`
## H0. After changes to the mobile view (`protected/mobile/*`)
Covers `protected/mobile/{index.html,mobile.css,mobile.js}` and the `wantsMobileView()` / `MOBILE_UA_RE` dispatch in `server.js`.
Dispatch (simulate mobile from a desktop browser by setting a phone UA, or actually load on a phone):
- [ ] `curl -I -H 'User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)' https://project-bifrost.fenja.ai/timeline -b 'fenja_session=<id>'` → 200 and HTML that contains `/mobile/mobile.css` (i.e. served the mobile page, not desktop)
- [ ] Same curl with a desktop UA (the default curl UA is fine) → 200 HTML that does NOT reference `/mobile/mobile.css` (served desktop)
- [ ] `/timeline?view=mobile` on a desktop browser → mobile HTML
- [ ] `/timeline?view=desktop` on a phone → desktop HTML (falls back gracefully — expected to look awkward but render)
- [ ] Logged-out GET `/timeline` still redirects to `/` regardless of UA (`requireAuth` runs before the UA dispatch)
Mobile page render (on a phone or DevTools device-emulation at 390×844):
- [ ] Masthead shows Fenja wordmark on the left, "Log out" button on the right. No horizontal scrollbar at any vertical position
- [ ] Intro title + body renders; the second paragraph ("As AI moves into our hospitals...") is crimson
- [ ] 12 timeline events render as a vertical list, each with kind + date + headline + body + source. Accent colours correct: Rupture = crimson, Editorial/Regulation = copper, Field Note (2024) = copper, Field Note (2025 CPH bill) = ochre, Product = terracotta
- [ ] Hero reads "Fenja AI — Secure & Sovereign, hosted where it belongs." with the italics visible
- [ ] Capabilities section renders 4 cards ("1 / 4" through "4 / 4") stacked vertically
- [ ] "Project Bifrost" reveal renders as a centered paragraph
- [ ] Three stops (Community, Advisory Council, Pilot Projects) render in order; no illustration files are required for the mobile view to be usable
- [ ] Join button is tappable and large enough (no 40px-minimum-tap-target issue)
Join CTA behaviour:
- [ ] Tap "Join Project Bifrost" → button disables, POST `/api/bifrost-join` returns 200, CTA panel hides and confirmation panel appears
- [ ] `sudo -u fenja node /opt/fenja/bin/joins.js list` on the VPS shows a new row with the user's email
- [ ] Logout button → POST `/auth/logout`, redirect to `/`, next visit to `/timeline` redirects to `/` (no session cookie)
Isolation audit (run after any change to desktop CSS/JS to confirm no mobile regression — and vice versa):
- [ ] `grep -r 'class="m-' protected/index.html` returns nothing (mobile classes only live in `protected/mobile/`)
- [ ] `grep -r 'from .*bifrost' protected/mobile/` returns nothing (mobile doesn't import desktop JS)
- [ ] Mobile page `<head>` loads exactly two stylesheets: `/fenja/colors_and_type.css` and `/mobile/mobile.css`
- [ ] Mobile page loads exactly one script: `/mobile/mobile.js` — no gsap/lenis/d3 references
## H1. After changes to the Join-CTA tracking (bifrost_joins)
- [ ] [browser, logged in] Click the final "Join Project Bifrost" CTA → confirmation panel renders (staggered checkmarks appear)
- [ ] DevTools network tab: `POST /api/bifrost-join` returns 200 with `{clicked_at: <ms>}`
- [ ] `sudo -u fenja node /opt/fenja/bin/joins.js list` shows a new row with your email and a current timestamp
- [ ] Click the CTA a second time (refresh the page first): a second row appears — the log is per-click, not per-user
- [ ] `sudo -u fenja node /opt/fenja/bin/joins.js summary` groups by email with correct `click_count`
- [ ] `sudo -u fenja node /opt/fenja/bin/joins.js stats` totals match what you see in `list`
- [ ] Logged out: `curl -X POST https://project-bifrost.fenja.ai/api/bifrost-join` → 401 (auth gate holds)
## H2. After changes to the hidden admin page (`/fenjaops`)
Covers `admin/`, the `/api/fenjaops/*` endpoints, and `requireAdmin`.
Access / gating:
- [ ] [browser, logged out] GET `/fenjaops` → 302 → `/` (not a leaky 404-that-sets-no-cookie)
- [ ] [browser, logged in as **non-admin**] GET `/fenjaops` → plain **404** page (not a redirect, not a blank 403 — the existence of the URL must not be leaked)
- [ ] [browser, logged in as admin] GET `/fenjaops` → page renders with stats, invite list (Admin badges present), join summary, raw click log, and the "Invite a new user" form at the top
Invite form — happy path:
- [ ] Fill `email` with a brand-new address, leave first name blank → click "Send invite" → green "Invited …" confirmation; form clears
- [ ] The invite list below refreshes and shows the new row with no Admin badge and `invited_by` = your admin email (not `"cli"`)
- [ ] `sudo -u fenja node /opt/fenja/bin/invite.js list` on the VPS shows the same row
Invite form — edge cases:
- [ ] Submit an email already on the list → red "already on the invite list" message; no duplicate row added
- [ ] Submit a malformed email (`foo`, `foo@`, empty) → red "not valid" message; no row added
- [ ] Submit a first name >64 chars → red "too long" message; no row added
- [ ] DevTools network: the request is `POST /api/fenjaops/invites`, `Content-Type: application/json`, 201 on success / 400 / 409 on failure
Escalation-path audit (do this after any change that touches `server.js` around `/fenjaops` or `src/middleware.js`):
- [ ] `curl -i -X POST https://project-bifrost.fenja.ai/api/fenjaops/invites -H 'Content-Type: application/json' -d '{"email":"foo@example.com","is_admin":1}'` while logged in as admin → 201, then confirm on the VPS that the new row has `is_admin = 0` (the body field must be ignored server-side)
- [ ] Same POST while logged out → 401 or redirect; no row created
- [ ] Same POST while logged in as non-admin → 404 from `requireAdmin`; no row created
- [ ] There is **no** endpoint on the server that accepts `is_admin`, `setInviteAdmin`, or `deleteInvite` from HTTP — grep `server.js` to confirm the only write is the invite-creation POST
## H. After data/DB changes (schema, migrations)
- [ ] Fresh boot creates schema cleanly: stop service, `mv data/fenja.sqlite data/fenja.sqlite.bak`, restart, verify it comes up healthy
- [ ] Restore the backup (`mv` it back) before inviting further users
- [ ] Take a manual backup before deploying: `sudo -u fenja sqlite3 /opt/fenja/data/fenja.sqlite ".backup /opt/fenja/data/pre-change-$(date +%F).sqlite"`
## I. Deploy readiness (before any push to production)
- [ ] Ran locally: `npm install && npm run dev`, walked full flow end-to-end
- [ ] No uncommitted `.env` file in the rsync source
- [ ] No `data/*.sqlite` in the rsync source
- [ ] On the VPS after deploy: section A passes
- [ ] On the VPS after deploy: section B passes
- [ ] Tailed `journalctl -u fenja -f` for 30s while refreshing the site → no errors
---
## Red flags that always mean stop
- `curl -I /` returns 200 **without** `X-Powered-By: Express` → Nginx is serving static, not proxying to Node. Site is broken.
- `curl -I /timeline.js` returns 200 → the auth gate is down. **Do not invite anyone.**
- Any `Set-Cookie: fenja_session=...` header that lacks `HttpOnly` or `Secure` (in prod) → cookie hardening regression.
- `journalctl -u fenja` showing repeated crashes → something's wrong, `systemctl restart fenja` won't fix it.
- The entrance page submits the form as a GET with `?email=` in the URL → JS isn't running (usually CSP blocking a newly-added inline script).
If any of these happen, revert to the last known-good commit and investigate offline.