# SPQ-v2 Phase 4 Implementation Reference (2026-07-07)

## Completed Features

Six features implemented in a single session using batched parallel delegation.

### Batch A — Pure frontend (no PB migrations)

**4.5 Technician time-card CSV export**
- `src/pages/advisor/TimeCards.tsx` — date-range picker, queries `technicianAssignments` by shop, aggregates clock entries per-tech per-day, CSV download via Blob + URL.createObjectURL
- Route `/time-cards` in App.tsx, nav in both Layout.tsx and MobileLayout.tsx (Clock icon)
- Preload helper in `src/lib/routePreload.ts`

**4.9 Data export / GDPR**
- `src/pages/Privacy.tsx` — export all data as JSON download (repairOrders, customers, appointments, invoices, settings), delete account with two-step confirmation
- Route `/privacy` in App.tsx under `<AdvisorOnly>`, link in Settings Account tab

### Batch B — PB hooks (no new collections)

**4.1 Quote approval notifications**
- PB M23: `app.onRecordUpdateRequest` on `quotes` — when status changes `sent → approved/declined`, emails shop owner via PB mailer

**4.2 Quote expiry enforcement**
- PB M24: `app.onRecordUpdateRequest` on `quotes` — rejects share-token updates past `created + quoteExpiryDays` with 410
- `src/pages/QuoteApprove.tsx` — distinct "Quote Has Expired" UI (Clock icon, red styling), catches 410 on load and submit

### Batch C — New PB collections + frontend

**4.3 Financial audit log**
- PB M25: `roFinancialAudit` collection (roId, field, from, to, by, at, prevHash, hash) with hash-chained hook on `repairOrders` financial blob diffs
- Viewer in RODetailModal Timeline tab — queries by roId, shows field labels, from→to JSON, hash chain

**4.6 Recurring maintenance reminders**
- PB M26: `reminders` collection (customerId, customerName, vehicleInfo, shopUserId, mileageAtService, intervalMileage, dueMileage, notes, active)
- "Set Reminder" button in RODetailModal header, inline form with computed due mileage
- "Maintenance Reminders" card in Dashboard with count badge

## Skipped / Deferred

- 4.4 Multi-vehicle history — skipped per user
- 4.7 Offline-first for technicians — sub-project scope, skipped
- 4.8 Two-factor auth — skipped per user
- 4.10 Multi-shop readiness — decision deferred (no code change needed)

## PB Migrations Applied

- M23 `1740000000023_quote_approval_notify.js`
- M24 `1740000000024_quote_expiry_enforce.js`
- M25 `1740000000025_ro_financial_audit.js`
- M26 `1740000000026_reminders.js`
- M27 `1740000000027_set_audit_and_reminder_rules.js` (no-op placeholder due to PB 0.39 rule validation quirk)

## Verification

`npm run build` ✓, `npm run lint` ✓ (0 errors, 24 pre-existing warnings), `npm run test` ✓ (86/86 tests)

## PB 0.39 Quirks Discovered

See `pb-0.39-migration-quirks.md` for full details. Key ones:
- Field-based API rules can't be set via JS migrations (even in separate files) — use empty rules + frontend filtering
- `DateTimeField` doesn't exist — use `TextField` with ISO strings
- `onRecordBeforeUpdateRequest` and `onRecordAfterUpdateRequest` don't exist — use `onRecordUpdateRequest`
- Collection constructor and `new Record()` both work in JS migrations

## GlitchTip DSN Format (FIXED 2026-07-07)

The Sentry client-side SDK v10 rejects DSNs with multi-segment paths like `/glitchtip/1`. Fix applied:

1. **`.env.production`**: DSN path changed from `/glitchtip/1` to `/1`
2. **nginx**: Added regex location `^/([0-9]+)/(api|store)/(.*)$` that proxies bare project-ID paths to GlitchTip at `127.0.0.1:8001`
3. After hard refresh, Sentry initializes with zero console errors

The nginx snippet:
```nginx
location ~ ^/([0-9]+)/(api|store)/(.*)$ {
    rewrite ^/([0-9]+)/api/(.*)$ /api/$1/$2 break;
    rewrite ^/([0-9]+)/store/(.*)$ /api/$1/$2 break;
    proxy_pass http://127.0.0.1:8001;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_buffering off;
}
```
