Files

99 lines
11 KiB
Markdown
Raw Permalink 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.
# MOBILE PASS — fix spec for https://retire.krisforbes.ca
**Spec v1 — written 2026-10-09 by Hermes (GLM). Execution: DSH (local 27B), all steps, zero user interaction.**
The user said: "do another pass to ensure mobile is a good experience and have DSH fix any issues you find."
## Evidence base (before-state, already collected by Hermes)
Mobile audit screenshots (Playwright, real Chromium, mobile emulation) are in
`/home/ubuntu/.hermes/cache/scratch/mobile-audit/` — devices: iPhone-14 390×844@3x,
Pixel 360×800@2.75x, iPhone-SE 320×700@2x. Re-run anytime with:
`python3 /home/ubuntu/.hermes/cache/scratch/mobile_audit.py`
Measured geometry (390px quiz): sticky header **126px tall (wraps to ~3 rows)**;
`.ghost` buttons **32px tall** (below the 44px touch minimum); G1 SVG renders at
**336px wide → scale 0.525 → 11px axis text renders at ~5.8px (unreadable)**;
year table `scrollWidth 595` vs `clientWidth 326` (1.8× horizontal scroll);
vision card renders **gradient fallback** on direct load into results (race, see #6).
Vision-checked findings (from the screenshots):
1. **Header eats ~15% of the phone screen** — logo + privacy pill + buttons wrap to 3 rows.
2. **Chart axis/legend/annotation text unreadable on phones** (~6px at 390px, worse at 320px; the "retire" marker label is nearly illegible).
3. **Bottom sheet collapsed state looks broken** — it peeks 58px and clips chip labels mid-glyph at the screen edge ("SAVINGS AT…" cut off). Expanded sheet covers the Next button with no scrim/close affordance.
4. **Year-by-year table needs 1.8× horizontal scroll**; 11 columns; no fade/hint that it scrolls.
5. **Touch targets**: Back/Start over/Print/Share ghost buttons are 32px tall.
6. **Vision card race**: on direct load into results (shared link `#p=…` or saved state), `renderVision` runs before `fetch('vision/manifest.json')` resolves → permanent gradient fallback even though the images exist.
7. **Share button lies**: if `navigator.clipboard` is missing/fails (older mobile Safari, non-secure context), it still shows "✅ Link copied".
8. Minor: no `theme-color` meta; legend 12.5px; `.opt small` 11.5px.
## Fixes (all in `site/` — index.html, style.css, app.js)
### Fix 1 — single-row compact header on mobile
In `style.css`, add inside a `@media(max-width:860px)` block:
- `.hdr-in{flex-wrap:nowrap;padding:6px 10px;gap:8px}` and `.hdr{padding-top:env(safe-area-inset-top)}`
- `.logo{font-size:15px}`; shrink header `.ghost` to `padding:6px 9px;font-size:13px`
- Hide `.hdr .pill` at ≤700px (the privacy pill is repeated on the intro screen and the footer — no information is lost).
- Hide the header `#btn-print` at ≤860px on mobile (the results page keeps its own Print button in `.btn-row`; header print is a desktop affordance).
- Result: header ≤ ~52px tall, one row, at 390/360/320px. Desktop header unchanged.
### Fix 2 — readable charts on phones
Two parts:
- **Bump SVG base fonts in `app.js`** (affects all viewports, small improvement everywhere): `axes()` tick labels 11→13, axis-title/"age" 11→12; the `retire` annotation 10.5→12; `runs out ≈` 12→13; G3 `shortfall from ~` 12→13; G4 bar value labels 12.5→13.5 and bar name labels 10.5→12.
- **Horizontal scroll on mobile**: in `style.css` `@media(max-width:860px)`: `.chartbox{overflow-x:auto;overflow-y:hidden}` and `.chartbox svg{min-width:560px}`. Add a subtle "swipe →" hint: CSS-only via `.chartbox::after{content:"swipe →";…}` shown only ≤860px (position it top-right of the chartbox, tiny, muted). Optionally hide it after the first scroll with ≤3 lines of JS — not required.
- Keep `width:100%;height:auto` so desktop is unchanged.
### Fix 3 — bottom sheet redesign (mobile only; desktop panel untouched)
Current: fixed sheet, `transform:translateY(calc(100% - 58px))` peek, chips+insight inside, tap header toggles.
New (≤860px only):
- **Chips always visible** in the collapsed sheet (this is the user's core "building feedback" requirement — on mobile it must not hide behind a tap). `.insight{display:none}` by default on mobile; `.panel.open .insight{display:block}`.
- Remove the `transform` peek trick entirely on mobile — the sheet height is just its content (header + 2×2 chips ≈ 150px; +insight when open).
- Keep the `.panel-h` tap-to-toggle (existing document click handler) — it now toggles the insight line. Keep the ▲/▼ affordance.
- **Add a scrim**: add `<div id="scrim" hidden></div>` to `site/index.html` (inside `<main>`, after `#scr-quiz` is fine — it is `position:fixed`). CSS ≤860px: `#scrim{position:fixed;inset:0;background:#16233b33;z-index:35}` shown only when the panel has `.open` (use `#scrim.on{display:block}` — toggle a class, not `hidden`, or use `[hidden]{display:none}` carefully; simplest: JS toggles `hidden` + a class). Panel z-index stays 40 (above scrim), header 30 (below). JS: clicking `#scrim` removes `.open` from `#panel` (and hides scrim). On desktop the scrim never shows (media query display:none).
- **Safe areas + content clearance**: `.panel{bottom:calc(8px + env(safe-area-inset-bottom))}` and on mobile `.scr{padding-bottom:calc(185px + env(safe-area-inset-bottom))}` so the step content (incl. the Next button) clears the always-visible sheet.
- Verify with screenshots at 390/360/320: collapsed sheet shows header + 4 chips with values fully visible (no mid-glyph clipping); Next button reachable; tapping the sheet header reveals the insight; tapping the scrim closes it.
### Fix 4 — touch targets + tap zoom
`style.css`: `.ghost{min-height:44px}` (harmless on desktop) and `button,input{touch-action:manipulation}` (kills the 300ms double-tap zoom delay on mobile).
### Fix 5 — year-by-year table on small screens
In `style.css` `@media(max-width:560px)`: hide the two least decision-relevant columns — `th:nth-child(4),td:nth-child(4)` (Non-reg) and `th:nth-child(9),td:nth-child(9)` (GIS) — with `display:none`. **Keep** the Unfunded column (depletion rows are also highlighted red via `tr.dep`, but the explicit shortfall number is the most decision-relevant). Bump `table{font-size:13px}` on mobile. Add a right-edge fade on `.tbl-wrap` (CSS `::after` gradient overlay, `pointer-events:none`) so the horizontal scroll is discoverable. Keep the sticky first column (`td:first-child,th:first-child` sticky left) and sticky header row — verify in the after-screenshots that the Age column stays visible while scrolled right.
### Fix 6 — vision card race (app.js)
Current: `fetch('vision/manifest.json')…then(m => visionManifest = m)` at load; `renderVision` uses `visionManifest` if set, else falls back to gradient permanently for that render.
Fix: after the fetch resolves (and on fetch failure), if the results screen is currently shown and the vision card is in gradient mode, re-render the vision card with the manifest. Minimal implementation: keep a `visionReady` promise; in its `.then`/`.catch`, set `visionManifest` and call `renderVision(state.a)` again (guard with `if (state.screen === 'results')`). Keep the `img.onerror → grad()` fallback. Also add a 2.5s timeout fallback so a hung fetch can't leave the card gradient forever if images actually exist — actually simpler: on timeout just leave the gradient (images may genuinely be missing); the important fix is re-rendering once the manifest lands. Verify: load `https://retire.krisforbes.ca/#p=<shared-link>` fresh (or set localStorage screen=results and reload) → the vision card shows the real image, not the gradient. The audit script's results screenshots must show a real photo.
### Fix 7 — honest share fallback (app.js)
In `#btn-share` handler: if `navigator.clipboard.writeText` succeeds → "✅ Link copied". Otherwise (missing API or rejection) → `window.prompt('Copy your link:', url)` and set the button text to "🔗 Link shown — copy it" for 1.8s. Never claim "copied" when it wasn't.
### Fix 8 — polish
- `site/index.html`: add `<meta name="theme-color" content="#f7f9fc">` in `<head>`.
- `style.css`: `.legend{font-size:13px}`, `.opt small{font-size:12px}`.
- Optional (only if trivial): in `renderStep`, for `type:'number'` steps add a keydown listener on `#f1`: Enter → if Next is enabled, click it. Skip if it complicates the smoke shim.
## Hard constraints
- **Do not touch** `site/engine/engine.js`, `site/engine/canada.js`, any tax/benefit constants, or engine logic. This pass is UI/CSS only (plus the vision re-render and share fallback in `app.js`).
- Keep the marker `<!-- retire-planner v1 -->` as the first line inside `<head>` (deploy.sh greps for it).
- Keep the privacy statement text untouched.
- No frameworks, no build step, no CDN — vanilla files only.
- **Both test suites must stay green**: `node tests/engine.test.js` (36/36) and `node tests/ui.smoke.js` (SMOKE-OK). If a change breaks the smoke shim, extend the shim minimally — do NOT weaken assertions.
- Desktop (≥861px) layout must remain visually unchanged except the small font bumps in Fix 2/8.
## Verification steps (DSH must do all, capture evidence)
1. `node tests/engine.test.js` → 36/36 PASS; `node tests/ui.smoke.js` → SMOKE-OK.
2. Preserve before-screenshots: `mkdir -p audit/mobile-before && cp /home/ubuntu/.hermes/cache/scratch/mobile-audit/*.png audit/mobile-before/`
3. Deploy: `bash scripts/deploy.sh` → ends with `DEPLOY-OK`.
4. Origin check: `curl -s http://192.168.0.123:8103/ | grep -c 'retire-planner v1'` → 1.
5. Re-run the mobile audit: `python3 /home/ubuntu/.hermes/cache/scratch/mobile_audit.py` (needs the playwright install already on this box + `/home/ubuntu/.cache/ms-playwright/chromium-1243/chrome-linux64/chrome`). Then `mkdir -p audit/mobile-after && cp /home/ubuntu/.hermes/cache/scratch/mobile-audit/*.png audit/mobile-after/`
6. Programmatic geometry re-check on 390px (extend the audit script or a small inline Playwright snippet): header height ≤ 56px; every `.ghost`/`.opt`/`.cta` ≥ 44px tall; chart svg `min-width` 560px on mobile (scrollWidth > clientWidth inside `.chartbox`); vision card contains an `<img>` on fresh results load; collapsed sheet shows all 4 chip values fully (no clipping — compare chip bounding boxes vs `window.innerHeight`).
7. Off-LAN spot check from the Midhurst troublebox (paramiko; password candidates live in `~/projects/*.py` lines containing `b1tvvt`, host `100.96.20.61`, try `trouble` port 2222 first): `curl -s -o /dev/null -w '%{http_code}' https://retire.krisforbes.ca/` → 200, and one vision image → 200.
8. Commit + push everything to `gitea.krisforbes.ca/krisf/retire-planner` (main): site changes + `audit/mobile-before/` + `audit/mobile-after/` + this spec as `MOBILE-FIX.md` + a short `MOBILE-FIX-REPORT.md` mapping each numbered issue → fix → verification evidence.
## Definition of done
- All 8 fixes implemented; tests green; deployed; origin + off-LAN 200; after-screenshots show: single-row header, readable charts (min-width scroll), always-visible chips sheet with scrim, 44px+ touch targets, table with hidden Non-reg/GIS columns + fade, real vision image on fresh results load.
- `MOBILE-FIX-REPORT.md` committed + pushed.
- Your FINAL output line must be exactly: `MOBILE_PASS_DONE`