# 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 ``, 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; } ```