--- name: mobile-flashcard-pwa description: Build and deploy a card-first mobile PWA for flashcard/study workflows with swipe navigation, tap-to-flip, fast update propagation, and optional backend-only LLM enrichment scripts. version: 1.0.0 author: Hermes Agent license: MIT metadata: hermes: tags: [pwa, mobile, react, vite, express, flashcards, swipe, study-app] --- # Mobile Flashcard PWA Use this when building a mobile-first study app where the card is the product. ## Recommended stack - Frontend: React + Vite + TypeScript - Gestures: `react-swipeable` - Server: single-port Express serving `dist/` - Deployment: PM2 + nginx - PWA: manifest + service worker + no-cache headers for `sw.js` and `manifest.json` ## UX rules that worked well 1. Make the card/detail content dominate the viewport. 2. Keep non-content chrome tiny — use a discreet top bar only. 3. Remove redundant bottom controls if swipe + tap already cover navigation. 4. On first use, show a small toast: "Swipe sideways to change cards. Tap the card to flip." 5. Persist a `seenGestureHint` flag in localStorage so the toast is not permanent. 6. If card metadata includes component symbols (like trigrams), show mini visual glyphs next to their names — text alone feels too abstract. 7. For alphabet/symbol-learning apps, do **not** ship a small conceptual demo with only representative signs if the user wants to “learn the letters/symbols.” Build complete deck coverage first: explicit counts per script, all glyphs visible in a grid, large study cards, search, progress states, and quiz mode. Include a coverage card so gaps are obvious. ## Mobile onboarding/account flow polish For mobile React/Vite onboarding, account setup, OAuth/device-code, or first-run flows, keep the mobile chrome and conversion steps deliberately simple: - Ensure there is exactly one visible primary menu trigger on mobile. If the brand title is needed, use a separate inert centered label rather than squeezing it into the same action row. - Hide secondary topbar actions at small widths when they crowd the brand or signup CTA; keep those actions in the drawer/sidebar instead. - Treat device codes as hero content: large centered monospace text, high-contrast block, tappable-to-copy, plus a smaller explicit copy button with temporary feedback. - Make the external authorization URL the primary CTA, and keep the final “complete setup” button disabled until the auth CTA has been opened/clicked; use precondition text like “Open authorization first.” - For iOS/mobile form inputs and autocomplete lists, set `color`, `-webkit-text-fill-color`, `caret-color`, suggestion background, suggestion text, and focus/hover colors explicitly. Reference: `references/mobile-onboarding-polish.md` captures the Astral Hermes session-derived pattern for mobile header cleanup, device-code CTA sequencing, and birth-location autocomplete contrast. ## Mobile detail modal / bottom sheet pattern For mobile React/Vite apps with review/detail modals (draft review, creative preview, record detail, etc.), treat the modal as a bottom sheet rather than a scaled-down desktop dialog: - At `max-width: 760px`, align the modal overlay to the bottom, remove side padding, and give the sheet full width with rounded top corners (`border-radius: 28px 28px 0 0`) and `max-height: ~94dvh`. - Add a small non-interactive grab handle via `:before`; position it absolute so it does not break the modal's grid rows. - Make the modal header sticky, compact, and translucent/blurred. Clamp long titles/subtitles/status pills so they do not crowd the close button. - Collapse desktop two-column preview/copy layouts to one column. Let the main media/card preview become full width, with `min-height` around 180–210px and `max-height` around 42dvh` so it feels substantial without hiding the actions. - Put destructive/approval/push actions in a sticky bottom action bar with safe-area padding, blur, and a subtle top border/shadow. Emphasize the primary action by spanning it across columns or ordering it first. - Make schedule/date inputs and action buttons at least ~42–44px tall for thumb taps. - Add a narrower breakpoint (`max-width: 390px`) to collapse two-column action/copy grids further on small phones. ## Good top bar pattern - App name - Progress count (`current/total`) - 2-4 compact chips for sequence/view mode - Avoid settings drawers unless absolutely necessary ## Gesture pattern - Tap card -> flip front/back - Swipe left/right -> next/previous card - Reset to front face when changing cards - For random mode, provide a small `Reshuffle` pill instead of a full control tray ## Sequence mode pattern Support multiple orderings with a single `createStudyDeck()` function. Recommended modes: - canonical / King Wen - user-provided custom sequence (e.g. `Human Design`) - binary / structural - random For a custom sequence supplied as an explicit 64-number array: 1. Store the array as a constant. 2. Build `const CUSTOM_INDEX: Map = new Map(order.map((n, i) => [n, i] as const))` 3. Sort with the map lookup instead of hardcoding many `if`s. 4. Add a test that verifies: - first several values match exactly - last value matches exactly - all 64 entries are unique This avoids mistakes when users hand you a specific sacred/study order they care about. ## PWA update pattern that avoids reinstall This was the important deploy requirement. ### In the client - Register `sw.js` - Immediately call `registration.update()` on load - Poll `registration.update()` every ~60s - Listen for `controllerchange` and reload once when a new SW takes control, but only if the page already had a controller before registration. Capture `const hadController = Boolean(navigator.serviceWorker.controller)` before `register()`, then in `controllerchange` do nothing on the first install. This avoids iOS standalone PWA first-launch reload loops/white screens. - Optionally show an `Update ready` pill/button when `updatefound` fires ### In `public/sw.js` - Use a versioned cache name - Call `self.skipWaiting()` in install - Call `self.clients.claim()` in activate - Delete old caches in activate - Use network-first for app shell/assets, falling back to cache - Do not cache `/api/` responses as static app shell ### In Express Serve these with no-cache headers BEFORE `express.static(distDir)`: - `/sw.js` - `/manifest.json` Example headers: - `Cache-Control: no-cache, no-store, must-revalidate` - `Service-Worker-Allowed: /` for `sw.js` ## Backend-only token pattern When the user wants an LLM/API token in the app but not exposed to the browser: 1. Load env on the server with `import 'dotenv/config'` 2. Keep the token in `.env` only 3. Never surface the raw key to the frontend 4. If useful, expose only a boolean status endpoint like `/api/config-status` -> `{ hasOpenRouter: true }` ## Reusing an existing OpenRouter key If another app on the same VPS already uses OpenRouter: - Check that app’s `.env` or PM2 ecosystem config - Copy the key into the new app’s `.env` - Keep `.env` gitignored - Add `.env.example` with placeholder values only ## LLM enrichment script pattern For datasets that need generated study text (e.g. 64 cards): - Write a standalone script under `scripts/` - Use OpenRouter chat completions with a strict JSON prompt - Request fields like: - `overview` - `judgment` - `image` - `mnemonic` - Save incrementally after each item so long runs can recover - Strip fenced code blocks before `JSON.parse()` because models may still return ```json wrappers - Merge generated JSON into the main dataset at import time rather than hardcoding everything in one file ## Recommended data merge pattern - Keep structural base data in `src/data/hexagrams.ts` - Keep generated copy in `src/data/...generated.json` - Import the generated JSON and map by ID/number onto the base dataset - Include generated JSON in git if the app should work without rerunning generation ## Deployment pattern - Build with: `tsc -b || true && vite build` - Always verify `dist/assets/index-*.js` modify timestamp changed - Run server with PM2 on a single port - nginx should proxy the subdomain directly to that one port - For a PWA, do NOT enable nginx basic auth — installed PWAs break under basic auth ## Verification checklist - `npm test` - `npm run build` - Verify dist timestamp - `curl http://127.0.0.1:PORT/api/health` - `curl -I http://127.0.0.1:PORT/sw.js` and confirm no-cache headers - Open the live app in browser tools and confirm: - minimal top bar - no redundant bottom controls - gesture toast appears for first use - flip works - tiny glyph visuals actually render on the back of the card ## Pitfalls - Tiny compact glyphs may technically exist but be too subtle to notice; increase contrast/size and give them their own small panel if needed. - OpenRouter may wrap JSON in markdown code fences; always clean before parsing. - If PM2 says `Use --update-env to update environment variables`, restarting alone won’t refresh changed env vars. - Browser text snapshots may miss subtle visual details; use screenshot/vision verification when checking tiny glyphs or minimalist UI polish.