Add installable PWA (Option A — iPhone-first, no service worker)

Make the app installable to the iOS home screen and launch standalone
(full-screen, no browser chrome, dark status bar). Add manifest.webmanifest,
square app icons (ten31-app-icon.svg -> 192/512/apple-touch-icon), the
apple-mobile-web-app + manifest <head> tags, viewport-fit=cover, and a
pre-auth /manifest.webmanifest route. No service worker by design.
This commit is contained in:
Keysat
2026-06-20 08:42:29 -05:00
parent 81ed6cbbab
commit 0490910687
10 changed files with 78 additions and 5 deletions
+4 -3
View File
@@ -107,11 +107,12 @@ Subsystem rules live in `docs/guides/` and lazy-load in Claude Code via `.claude
## Current state
_**Box live at v0.1.0:94**; `main` ahead by mobile Phases 07 + P3b + drag-reorder + **8a8i — Phase 8 complete** — **all deploy-pending** (no s9pk built). **The fundraising grid + email capture is the canonical system of record.** Active thread: **mobile-first redesign — feature-complete; next is deploy + real-phone device-test** (nothing verified on a real phone yet). Build reference: `design/phase8-conformance.md` (the 8a8i spec; now fully landed — archive-eligible). **Plan (Grant, 2026-06-19): finish features first → then Grant device-tests + deploys.** History: git log + `start9/0.4/startos/versions/`._
_**Box live at v0.1.0:94**; `main` ahead by mobile Phases 07 + P3b + drag-reorder + **8a8i — Phase 8 complete** + **installable PWA (Option A)** — **all deploy-pending** (no s9pk built). **The fundraising grid + email capture is the canonical system of record.** Active thread: **mobile-first redesign — feature-complete; next is deploy + real-phone device-test** (nothing verified on a real phone yet). Build reference: `design/phase8-conformance.md` (the 8a8i spec; now fully landed — archive-eligible). **Plan (Grant, 2026-06-19): finish features first → then Grant device-tests + deploys.** History: git log + `start9/0.4/startos/versions/`._
- **Mobile redesign — 4 core surfaces built (Grid · Contacts · Pipeline · Reminders), each a rules-of-hooks-safe `useIsMobile()``Mobile*`/`Desktop*` pair (desktop untouched).** Foundation: bottom-tab bar + `:root` mobile vars; 4-stage enum; derived grid signals injected-on-GET/stripped-on-write at both points; mobile writes use **one-row endpoints only** (log-communication, pipeline link/stage, reminders, `update-row`) — never whole-grid PUT.
- **Phase 8 done (8a8i — complete)** — cards/details/sheets/shell re-authored to the dc anatomy. The durable per-primitive record is the **Design convention's primitives list**; per-phase "what changed" is in git log. Recent: **8h** Grid-detail loose ends (G4 tappable stage card, G5 dedicated Reminder card, Open-in-Grid deep-link on all three detail surfaces); **8i** shell — `BottomTabIcon` SVG line-icons replace the bottom-tab emoji glyphs + the `·Ten31·` `.mobile-wordmark` in the top bar (page title now `desktop-only`); the dc top bar's quick-log/theme/account cluster already existed. jsdom-verified (throwaway 375px harness).
- **Live (deployed):** W2 NL query (v94); W1 reminders (v93); grid Pipeline (v88); Matrix intake + Gmail capture (DWD) + daily digest; Thesis/Architect (dual-approval); outreach — all draft-only.
- **Tests:** **40/40 backend green** (`python3 backend/run_tests.py`), `py_compile` clean. Mobile surfaces interaction-verified via throwaway 375px jsdom harnesses (deleted after); harness recipe + the `Simulate.change` input gotcha live in the `mobile-surface-verification` memory.
- **Next — feature-complete; deploy + device-test:** Phase 8 is fully landed (8i closed S1/S2; **Pipeline accordion skipped** per Grant). Remaining is **deploy P0P8 + P3b in one s9pk** (**authorize + version-bump first**, per `docs/guides/packaging.md`) and **device-test light/dark on a real phone** — nothing mobile has run on a real device yet (smoke/jsdom only).
- **Open / risks:** all mobile work + light theme **built but never deployed or device-tested** (smoke/jsdom only); `MobileDetailRow` unused-but-retained (legacy-usage sweep); Pipeline detail "Committed" tile shows grid-committed not deal-expected (forecast in a footnote); `handle_get_opportunity` (single-opp GET) deliberately does NOT inject `existing_investor`/`last_contact_date` — no surface needs it (the card uses the list injection; the detail derives `existing` from the contact fetch); W2 happy-path only; **Claude/Architect path unverified live on the box**; v2.0 reserve-asset spine **not canonical**; doc drift — `crm-overview.md`/`EVALUATION.md` still call `lp_profiles` live.
- **Installable PWA — BUILT 2026-06-20 (Option A, iPhone-first, no service worker):** `frontend/manifest.webmanifest` (standalone, `theme_color` `#0b1118`) + square icons (`ten31-app-icon.svg``icon-192`/`icon-512`/`apple-touch-icon`) + `<head>` manifest/`apple-mobile-web-app-*`/`viewport-fit=cover` metas + one pre-auth `/manifest.webmanifest` route (`application/manifest+json`). Adds to home screen → full-screen standalone, dark status bar. No SW by design (iOS A2HS needs none; avoids the stale-shell class). render-smoke + live-curl verified. Detail in `ROADMAP.md` "Mobile PWA". **Deploy:** rides the same s9pk.
- **Next — feature-complete; deploy + device-test:** Phase 8 is fully landed (8i closed S1/S2; **Pipeline accordion skipped** per Grant) + the installable PWA. Remaining is **deploy P0P8 + P3b + PWA in one s9pk** (**authorize + version-bump first**, per `docs/guides/packaging.md`) and **device-test light/dark on a real phone** (incl. installing to the iOS home screen + standalone launch) — nothing mobile has run on a real device yet (smoke/jsdom only).
- **Open / risks:** all mobile work + light theme **built but never deployed or device-tested** (smoke/jsdom only); `MobileDetailRow` unused-but-retained (legacy-usage sweep); Pipeline detail "Committed" tile shows grid-committed not deal-expected (forecast in a footnote); `handle_get_opportunity` (single-opp GET) deliberately does NOT inject `existing_investor`/`last_contact_date` — no surface needs it (the card uses the list injection; the detail derives `existing` from the contact fetch); W2 happy-path only; **Claude/Architect path unverified live on the box**; v2.0 reserve-asset spine **not canonical**; doc drift — `crm-overview.md`/`EVALUATION.md` still call `lp_profiles` live; **PWA iOS status bar is fixed `black` at launch** (`apple-mobile-web-app-status-bar-style`) — in *light* theme it's a black strip above the light header (proper fix = `black-translucent` + header top-safe-area padding + theme-synced `theme-color`; deferred, validate on-device; dark is default so low-priority); PWA manifest/icons sent with no `Cache-Control` (consistent with all static routes — a manifest change post-install may be served stale by iOS until it re-fetches).
+27
View File
@@ -280,6 +280,33 @@ backlog. **Scoped 2026-06-19 (plan below); not yet started.**
The comps are signed-off prototypes, **not drop-in** (Claude Design runtime, seed data) — each
surface is re-authored in the app's React idiom and wired to the **real API**.*
#### Mobile PWA — installable home-screen app — BUILT 2026-06-20 (deploy pending)
**Option A (iPhone-first, no service worker).** Makes the app installable to the iOS home
screen and launch **standalone** (full-screen, no Safari chrome, dark themed status bar,
splash). Shipped: `frontend/manifest.webmanifest` (`display:standalone`, `start_url:/`,
`theme_color`/`background_color` = the brand base `#0b1118` already reserved for this in
`design/tokens.tokens.json`); square icons generated from `ten31-app-icon.svg` (full-bleed
`#0b1118` + white "T31", maskable-safe) → `icon-192.png`/`icon-512.png`/`apple-touch-icon.png`
(180); `<head>` gains `rel=manifest`, `theme-color`, the `apple-mobile-web-app-*` metas
(status bar `black` — opaque, so content never slides under the notch), `apple-touch-icon`,
and `viewport-fit=cover` (so the tab bar's existing `env(safe-area-inset-bottom)` clears the
home indicator). One pre-auth backend route serves `/manifest.webmanifest` as
`application/manifest+json` (`backend/server.py`); icons serve via the existing `/assets/`
handler. **No service worker** — on iOS the install prompt doesn't exist regardless (A2HS is
always manual via Share), standalone display needs none, and a cache-first SW would reintroduce
the stale-shell class the render-smoke gate guards against. Verified: render-smoke green +
live-curl (manifest + icons 200 pre-auth, correct content-types). **Deploy:** ships in the
next s9pk with the mobile phases.
- **Known minor:** the iOS status bar is fixed `black` at launch (can't follow the in-app
light/dark toggle); a barely-perceptible seam vs the `#0b1118` app. Acceptable; dark is default.
- **Deferred (not needed for iPhone):** a network-first service worker → Android's "Install"
prompt + faster relaunches; the JSX-precompile build-step (ROADMAP below) is the better lever
if relaunch speed is ever a felt problem.
- **Adjacent issue (not PWA, noted while here):** a phone in **landscape** can exceed the 768px
breakpoint and render the *desktop* layout; `orientation:portrait` in the manifest hints at
this but iOS ignores it for home-screen apps. Revisit if it bites during device-testing.
#### Phase 8 — conform to the FINAL Claude Design mockups (mobile) — **NEXT SESSION (scoped 2026-06-19)**
*Phases 07 built the mobile surfaces + light theme. Phase 8 closes the gap to the **final** design + functional parity. Two independent agent passes ran 2026-06-19 (functional-parity + visual-conformance); their findings + the source-of-truth correction below drive this plan.*
+5
View File
@@ -2226,6 +2226,11 @@ class CRMHandler(BaseHTTPRequestHandler):
# Serve frontend
if path == '/' or path == '/index.html':
return self.send_file(os.path.join(FRONTEND_DIR, 'index.html'))
# PWA manifest — served at root (manifest scope = its own path's directory) and
# pre-auth, since the browser fetches it (and the icons under /assets/) before login
# to offer "Add to Home Screen". No service worker (iOS-first; see ROADMAP).
if path == '/manifest.webmanifest':
return self.send_file(os.path.join(FRONTEND_DIR, 'manifest.webmanifest'), 'application/manifest+json')
if path.startswith('/assets/'):
filepath = os.path.join(FRONTEND_DIR, path.lstrip('/'))
# Containment check: get_path()/urlparse does NOT normalize '..', so without
+1 -1
View File
@@ -2,7 +2,7 @@
"$description": "Ten31 CRM design tokens (W3C DTCG). Extracted as-built from frontend/index.html :root + an inline-style census, 2026-06-18. The app currently inlines these values (CSS :root vars + ~1300 inline style objects); this file is the canonical source going forward. Some real values (composite shadows, the radial-gradient page background) do not map to DTCG primitives and are documented as strings.",
"color": {
"bg": {
"base": { "$type": "color", "$value": "#0b1118", "$description": "Page background (darkest layer). Also the de-facto theme color; use for a future PWA manifest theme_color." },
"base": { "$type": "color", "$value": "#0b1118", "$description": "Page background (darkest layer). Also the de-facto theme color; used as the PWA manifest theme_color/background_color and the app-icon ground (manifest.webmanifest, ten31-app-icon.svg)." },
"panel": { "$type": "color", "$value": "#111a27", "$description": "Cards, sections, modals, sidebar, slide-over." },
"elevated": { "$type": "color", "$value": "#152233", "$description": "Elevated/hover panel state." },
"hover": { "$type": "color", "$value": "#1b2a3a", "$description": "Generic hover background." },
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- PWA / home-screen app icon. Full-bleed opaque #0b1118 square (the brand base color,
reserved as the PWA theme_color in design/tokens.tokens.json) with the white "T31"
mark centered inside the maskable safe zone (inner ~80%), so the SAME asset works
un-cropped (purpose "any"), masked by Android (purpose "maskable"), and rounded by
iOS (apple-touch-icon). No inner border/rounding — the OS applies its own. -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Ten31">
<rect x="0" y="0" width="64" height="64" fill="#0b1118"/>
<text x="32" y="41" text-anchor="middle" fill="#ffffff" font-size="26" font-weight="700" font-family="Georgia, 'Times New Roman', serif">T31</text>
</svg>

After

Width:  |  Height:  |  Size: 785 B

+14 -1
View File
@@ -2,10 +2,23 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- viewport-fit=cover lets env(safe-area-inset-*) return real values on notched
iPhones, so the bottom tab bar's existing safe-area padding actually clears the
home indicator when launched as a standalone PWA. -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>Ten31 database</title>
<link rel="icon" type="image/png" href="/assets/ten31-inverted-square.png">
<link rel="shortcut icon" href="/assets/ten31-inverted-square.png">
<!-- PWA: home-screen install + standalone (full-screen, no browser chrome). iOS-first,
no service worker (see ROADMAP "Mobile PWA"). theme_color = the brand base #0b1118
reserved in design/tokens.tokens.json. iOS status bar is solid black (not
translucent) so content never slides under the notch. -->
<link rel="manifest" href="/manifest.webmanifest">
<meta name="theme-color" content="#0b1118">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Ten31">
<link rel="apple-touch-icon" href="/assets/apple-touch-icon.png">
<!-- Vendored + SRI-pinned (v0.1.0:82). These ship inside the s9pk and are served
same-origin from /assets/vendor/, so a CDN can never swap our prod deps (the
v78/v79 blank-screen class) and the box needs no outbound internet to render.
+17
View File
@@ -0,0 +1,17 @@
{
"name": "Ten31 CRM",
"short_name": "Ten31",
"description": "Ten31 Venture fundraising CRM — investors, pipeline, reminders, contacts.",
"id": "/",
"start_url": "/",
"scope": "/",
"display": "standalone",
"orientation": "portrait",
"background_color": "#0b1118",
"theme_color": "#0b1118",
"icons": [
{ "src": "/assets/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
{ "src": "/assets/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" },
{ "src": "/assets/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
]
}