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>
164 lines
12 KiB
Markdown
164 lines
12 KiB
Markdown
# 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.
|