--- 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 dominate the viewport. 2. Keep non-card 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. ## 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.