> Part of the PWA DevGuide (split from pwa_dev_style.md on 2026-07-02 — see that file for the index; ADR-099).

## 15. Service Worker

Inline blob-registered (no external sw.js file):

```
Cache name: 'recce-v6'
Install: caches.add('/')
Activate: clients.claim()
Fetch: cache-first, network fallback, cache successful responses
       On network failure: return cached '/' (offline fallback)
       Skips non-GET requests
```

**Update detection:** Because the SW uses a blob URL (changes on every load), `updatefound` events are unreliable. Instead, use a version-string check against `localStorage`:
```js
const CACHE_VER = 'recce-v6';   // keep in sync with SW cache name
function checkAppUpdate() {
  const seen = localStorage.getItem('recce-ver-seen');
  localStorage.setItem('recce-ver-seen', CACHE_VER);
  if (seen && seen !== CACHE_VER) showUpdateBanner();
}
```
- First install: `seen` is null → no banner, stores version silently
- Same version reload: `seen === CACHE_VER` → no banner
- After release bump: mismatch → banner shown
- **Rule:** Always bump `CACHE_VER` and the SW cache name string together on every release

---

## 16. Utility Functions

| Function | Purpose |
|---|---|
| `compress(file, maxPx?, q?)` | Canvas-based JPEG compression, respects imgQuality global |
| `b64(blob)` | Promise → base64 data URL via FileReader |
| `fmtSz(bytes)` | Formats to B / KB / MB |
| `toast(msg, type)` | Fixed bottom toast, auto-hides after 3000ms. Types: info/success/error |
| `netStatus()` | Updates online/offline dot + text |
| `showView(n)` | Switches active view, scrolls to top |
| `calcSqft(w,h)` | `W×H sq.in = X.XX sq.ft` string |
| `getSD()` | Returns current store data from selectedStore or manual fields |
| `fitText(ctx, text, maxW)` | Truncates text with … to fit canvas width |
| `roundRectPath(ctx,x,y,w,h,r)` | Draws rounded rect path on canvas context |
| `populateBrandDropdown()` | Rebuilds f-brand select from brands array |
| `updAvailTag()` | Updates "X stores" count tags in Step 1 |

---

## 17. Known Code Notes

| Location | Note |
|---|---|
| Line 705–706 | `initDB` defined twice — v3 then v4. Second definition wins (JS hoisting not applicable; second `function` declaration executes last). |
| Line 909 | `_storeFiles.pus68(...)` — **typo bug FIXED**: corrected to `.push(...)`. Was silently dropping all store-file tracking entries after the first upload. |
| Lines 1705 | `brand-selector.scrollIntoView` on missing brand — requires `selectedStore` to have no brand column |
| `_doCapture` | `input.remove()` delayed 8000ms — if camera takes > 8s to return, input is detached from DOM (onchange still fires in most browsers) |
| Admin lock | Auto-lock after 10 min — not reset on activity, fixed timer from login |

---

## 18. Design Patterns Summary

| Pattern | Implementation |
|---|---|
| Single-page app | 7 `.view` divs, only one `.active` at a time |
| Offline-first | IndexedDB primary, server sync secondary |
| Optimistic local save | Submit saves locally first, syncs after |
| Progressive form | 5 steps with progress bar, back navigation always available |
| No router | Direct DOM manipulation, `showView()` |
| State reset | `startNewRecce()` resets all global arrays and form fields |
| Auto-draft | 500ms debounced on every field change |
| Responsive images | Canvas compression before any storage or upload |
| Two sync targets | GAS (fire-and-forget, no-cors) + PostgREST (awaited, primary) |
| Admin in-place | Admin view reuses home screen DOM, no separate route |
| Z-index stacking | 200→300→400→500→600→1000, each layer has dedicated purpose |
| Draft mode | `status TEXT DEFAULT 'submitted'` in DB; draft tile re-opens capture form pre-filled from `form_data`; separate Save Draft / Submit paths |
| GForm explicit-only | Never fire-and-forget GForm on submit; only via explicit user button; patch `gform_submitted=true` in DB on success |
| Delete gate | `canCapture && !r.gform_submitted` — delete allowed until GForm submitted; draft delete via separate button in capture form |
| GPS — non-blocking | Use `gpsLoc` not `location` (shadows `window.location`); 3s timeout; show "Getting location…" text; silent fail; capture only on final submit not draft |
| PostgREST schema reload | After any `ALTER TABLE` on a live DB, run `NOTIFY pgrst;` from psql — PostgREST reloads schema cache within seconds without restart |
| Modal DOM order | Same-z-index modals: later in DOM = on top. Place sub-pickers after parent modals |
| Bilingual static HTML | Wrap every visible string in `<span class="en-txt">...</span><span class="hi-txt">...</span>`; CSS hides one set based on `body.lang-hi` class |
| Bilingual dynamic JS | Use `t(en, hi)` helper inside template literals and `textContent` calls; avoids span injection into innerHTML (safer, no DOM parsing overhead) |
| Map var vs `t()` conflict | When using `BTYPE.map(t => ...)` alongside a `t()` translation function, rename the map parameter to `tp` or `typ` to avoid shadowing |
| App update banner | `CACHE_VER` constant compared to `localStorage('app-ver-seen')` on init; mismatch (not first install) → slides up `.upd-banner` from bottom; auto-dismiss 5s; manual OK; bilingual; z-index 550; bump both `CACHE_VER` and SW cache name together on every release |

---

