homelabby/.planning/phases/03-dashboard-intake-ui/03-05-SUMMARY.md

7.8 KiB


phase: 03-dashboard-intake-ui plan: "05" subsystem: ui, pwa tags: [react, typescript, pwa, service-worker, qr-scanner, zxing, camera]

Dependency graph

requires:

  • web/src/store/ui.ts (useUIStore.setScannerActive)
  • web/src/components/ui/button.tsx
  • @zxing/browser (already in package.json from Plan 01) provides:
  • web/public/manifest.json (PWA installable config)
  • web/public/sw.js (app-shell service worker)
  • web/public/icons/icon-192.png, icon-512.png (PWA icons)
  • web/src/hooks/usePWA.ts (service worker registration hook)
  • web/src/pages/ScanPage.tsx (/scan QR camera scanner)
  • web/src/lib/api.ts (fetchInventory, fetchInventoryItem typed wrappers)
  • web/src/components/layout/AppShell.tsx (page layout wrapper) affects:
  • web/index.html (theme-color meta added)
  • web/src/App.tsx (usePWA() call added)
  • web/src/router.tsx (scanRoute lazy-loads ScanPage)

Tech tracking

tech-stack: added: [] patterns: - PWA app-shell cache strategy (navigation cache-first, API network-only, static assets cache-first) - @zxing/browser decodeFromVideoDevice for live camera QR scanning - lazy() + Suspense code-splitting for heavy QR scanner bundle (444KB chunk) - extractHWID() regex — strict pattern matching before any navigation

key-files: created: - web/public/manifest.json - web/public/sw.js - web/public/icons/icon-192.png - web/public/icons/icon-512.png - web/scripts/gen-icons.cjs - web/src/hooks/usePWA.ts - web/src/pages/ScanPage.tsx - web/src/lib/api.ts - web/src/components/layout/AppShell.tsx modified: - web/index.html (theme-color meta) - web/src/App.tsx (usePWA hook) - web/src/router.tsx (lazy ScanPage + Suspense)

decisions:

  • id: PWA-D01 summary: "ScanPage code-split as separate lazy chunk (444KB) — @zxing/browser is large; lazy loading keeps initial bundle lean at 275KB"
  • id: PWA-D02 summary: "api.ts and AppShell.tsx created as minimal stubs in this plan — Plans 03-03/03-04 run in parallel and hadn't built them yet; stubs provide exactly the interface ScanPage needs without duplicating plan scope"
  • id: PWA-D03 summary: "Icon generation uses pure Node.js + zlib (no ImageMagick/canvas) — zero external tooling required; gen-icons.cjs is committed for reproducibility"

Metrics

duration: ~15min completed: 2026-04-10T06:22:00Z tasks_completed: 2 files_created: 9 files_modified: 3

Phase 3 Plan 05: PWA Manifest + Service Worker + QR Scanner Summary

PWA installable manifest with app-shell service worker, programmatically generated icons, and a live camera QR scanner at /scan that resolves HW-XXXXX labels to NetBox inventory items.

Performance

  • Duration: ~15 min
  • Started: 2026-04-10T06:07:00Z
  • Completed: 2026-04-10T06:22:00Z
  • Tasks: 2
  • Files created: 9
  • Files modified: 3

Accomplishments

  • web/public/manifest.json — PWA web app manifest: display=standalone, theme_color=#faff69, background_color=#000000, 192px + 512px icons, portrait-primary orientation
  • web/public/sw.js — App shell service worker with three-tier fetch strategy: API calls always network-only (no stale inventory), navigation requests cache-first (offline shell), static assets cache-first with network fallback and cache population
  • web/public/icons/icon-192.png + icon-512.png — Valid PNG icons generated by web/scripts/gen-icons.cjs (pure Node.js, no external deps): black canvas with volt #faff69 H monogram
  • web/src/hooks/usePWA.ts — Service worker registration hook called from App.tsx on window load
  • web/src/pages/ScanPage.tsx — Full camera QR scanner:
    • Rear camera preference (label matching: back|rear|environment)
    • Volt corner-bracket reticle overlay on live viewfinder
    • extractHWID() regex matches http://.../hw/HW-XXXXX URLs and bare HW-XXXXX IDs
    • lastScanned debounce prevents duplicate navigation
    • Resolves HW ID to NetBox numeric ID via GET /api/inventory, then navigates to /item/:id
    • Graceful camera-denied error state (no crash)
  • web/src/router.tsx — scanRoute uses lazy() + Suspense to code-split the 444KB @zxing chunk
  • npm run build exits 0, dist contains correctly split bundles

Task Commits

  1. Task 1: PWA manifest, service worker, icons, registration hook95a50f4
  2. Task 2: QR scanner page + router wiring75c91a5

Deviations from Plan

Auto-fixed Issues

1. [Rule 3 - Blocking] Created api.ts and AppShell.tsx stubs — parallel plan dependencies missing

  • Found during: Task 2 (creating ScanPage.tsx)
  • Issue: ScanPage imports fetchInventory from @/lib/api and AppShell from @/components/layout/AppShell. Plans 03-03 and 03-04 (which build these) run in wave 2 alongside this plan and had not yet executed.
  • Fix: Created web/src/lib/api.ts with InventoryItem, fetchInventory, fetchInventoryItem typed wrappers (full API contract matching Plan 03-02 handler output); created web/src/components/layout/AppShell.tsx as a minimal page wrapper. Both files are documented as stubs that Plans 03-03/03-04 will extend.
  • Files modified: web/src/lib/api.ts (new), web/src/components/layout/AppShell.tsx (new)
  • Impact: No scope creep — the stubs expose the exact interface ScanPage needs. Plans 03-03/03-04 will extend them without conflict.
  • Commits: 75c91a5

2. [Rule 1 - Bug] Fixed recursive startScanner call pattern — removed self-reference via useCallback dep

  • Found during: Task 2 implementation review
  • Issue: Plan's ScanPage template called startScanner() recursively inside its own useCallback, which would cause an undefined reference (function not yet assigned at first call). Also called stopScanner before stopScanner was defined.
  • Fix: Reordered hook definitions (stopScanner before startScanner), removed startScanner from its own dependency array, and used void startScanner() call pattern for the retry paths.
  • Files modified: web/src/pages/ScanPage.tsx

Known Stubs

Stub File Reason
AppShell (minimal layout, no TopBar/nav) web/src/components/layout/AppShell.tsx Plan 03-03 will replace with full navigation shell
fetchInventory/fetchInventoryItem (no caching) web/src/lib/api.ts Plan 03-03 will add TanStack Query hooks wrapping these; raw fetch is fully functional

Both stubs are wire-complete (they make real API calls). ScanPage functionality is not blocked by stub status.

Threat Surface Coverage

All four threats from the plan's threat register are addressed:

Threat Mitigation Where
T-03-14: QR text → navigation tampering extractHWID() strict regex HW-\d{5,} — arbitrary text cannot trigger navigation; NetBox numeric ID comes from server response, not QR content ScanPage.tsx:extractHWID
T-03-15: Camera stream info disclosure @zxing decodes frames locally; no frames sent to server ScanPage.tsx (client-only)
T-03-16: SW cache tampering API responses explicitly excluded (network-only path in sw.js) sw.js fetch handler
T-03-17: Rapid QR scan DoS lastScanned state debounce; scanner stops before resolving ScanPage.tsx:startScanner callback

Self-Check

Files created:

  • web/public/manifest.json: FOUND
  • web/public/sw.js: FOUND
  • web/public/icons/icon-192.png: FOUND (valid PNG)
  • web/public/icons/icon-512.png: FOUND (valid PNG)
  • web/src/hooks/usePWA.ts: FOUND
  • web/src/pages/ScanPage.tsx: FOUND
  • web/src/lib/api.ts: FOUND
  • web/src/components/layout/AppShell.tsx: FOUND

Commits:

  • 95a50f4: feat(03-05): PWA manifest, service worker, icons, and registration hook
  • 75c91a5: feat(03-05): QR scanner page with @zxing/browser and router wiring

Build: cd web && npm run build exits 0, 4 output files in dist/

Self-Check: PASSED