# ADR-054: Counter Sync Prefers my_visible_counters RPC, Falls Back to stores.json with Visible Warning

## Status

Accepted, 2026-06-22.

## Status History

```yaml
status_history:
  - date: 2026-06-22
    status: Proposed
    changed_by: hkl
    reason: Formalising progressive migration fallback for counter sync in Recce PWA
    changed_via: adr-kit (360lm)
  - date: 2026-06-22
    status: Accepted
    changed_by: hkl
    reason: RPC-prefer + stores.json fallback live; warning banner shown when fallback fires with emp-id set
    changed_via: adr-kit (360lm)
```

## Context

Counter data for Recce PWA was originally served from a static `stores.json` file (legacy approach: admin exports JSON, uploads to VPS). The modern approach uses `my_visible_counters(emp_id)` RPC which returns counters allocated to the specific employee from the DB (allocation-aware, always current). During migration to the RPC approach, the old `stores.json` path could not be removed immediately — field agents using older app versions or operating without employee ID configuration still need counter data. A silent fallback (RPC fails → use JSON without telling user) would mask misconfiguration. A visible warning banner was added so misconfiguration is immediately visible to the user.

## Decision

Recce PWA counter fetch follows this order:

1. **If `emp_id` is set:** call `my_visible_counters(emp_id)` RPC. Use result if non-empty.
2. **If RPC returns 0 rows OR network error OR `emp_id` not set:** fall through to fetch `stores.json`.
3. **If `emp_id` was set but fallback fired:** show warning banner: `"⚠ Using legacy stores.json (no counters allocated to you — ask supervisor)"`. Banner stays visible during the session.
4. **If `emp_id` was not set:** fetch `stores.json` silently (no warning — expected state for unregistered device).

The stores.json path is preserved indefinitely as the fallback — removing it would break unregistered devices and older app versions.

**Decision Maker:** hkl

## Alternatives Considered

- **RPC only (remove stores.json).** Rejected: devices without emp_id configuration have no counter data; all field agents using the app before configuration is complete are locked out; migration must be instantaneous across all devices — not operationally feasible.
- **stores.json only (no RPC).** Rejected: allocation-aware filtering (employee sees only their assigned counters) is not possible from a static file; admin must manually maintain the JSON file; stale JSON after counter additions goes unnoticed.
- **Silent fallback (no warning banner).** Rejected: a field agent configured with emp_id but receiving stores.json silently means their allocation is misconfigured (no counters assigned) — they will visit stores not in their allocation; the warning tells them to contact their supervisor before wasting a field visit.
- **Error instead of fallback (fail loudly if RPC returns 0 rows).** Rejected: a new employee with no counters yet allocated has a legitimate 0-row RPC result — an error would block them entirely; the fallback + warning is the correct middle ground.

## Consequences

**Positive:**
- Existing unregistered devices continue working without code changes.
- New employees with allocation immediately get their personal counter list.
- Misconfigured employees (emp_id set but no counters allocated) see a clear action to take.
- Warning banner is impossible to miss — it persists for the session.

**Negative / Trade-offs:**
- Two code paths to maintain (RPC + stores.json fetch).
- stores.json can become very stale if admin forgets to update it — the warning banner only fires when emp_id is set; unregistered devices may see stale counter data silently.

**Risks and mitigations:**
- emp_id set, 0-row RPC result, no warning shown (condition missed): mitigated by explicit `if (empId && usedFallback)` check; test case covers this condition.

## Related Decisions

- ADR-029 (dual-write for offline access control) — same pattern: primary authoritative source, file-based fallback, explicit fallback signal.
- ADR-020 (offline-first IndexedDB) — counter data from either source is cached in IDB for offline use.

## References

- `memory/dbt_counters.md` — "Startup auto-fetch PREFERS RPC when emp-id set; falls through to stores.json. Banner shown when fallback fires while emp-id is set"
- `counters/index.html` — my_visible_counters RPC call, stores.json fallback, warning banner
