customer-presentation/CHECKLIST.md

13 KiB
Raw Permalink Blame History

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 fenjaactive (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)

H1b. After changes to engagement event tracking (events)

  • [browser, logged out] Log in fresh via the entrance form → entrance advances to the welcome step
  • sudo -u fenja node /opt/fenja/bin/events.js list --type login --limit 5 shows a new row with your email, current timestamp, populated device/os/browser, and a session-ID prefix
  • [browser, logged in] Visit /timeline → page loads
  • sudo -u fenja node /opt/fenja/bin/events.js list --type timeline_view --limit 5 shows a new row with view=desktop forced=false (or view=mobile forced=false if you tested on a phone UA)
  • [browser] Visit /timeline?view=mobile from a desktop UA → mobile page renders
  • sudo -u fenja node /opt/fenja/bin/events.js list --type timeline_view --limit 5 shows the most recent row with view=mobile forced=true
  • sudo -u fenja node /opt/fenja/bin/events.js summary includes your email with correct LOGINS and TIMELINE counts
  • sudo -u fenja node /opt/fenja/bin/events.js stats totals match what list shows; device breakdown reflects the views you generated
  • sudo -u fenja node /opt/fenja/bin/events.js for <yourtestemail> shows full per-user history
  • No 500s in journalctl -u fenja -n 100 from the test traffic

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.