Files
2026-07-12 10:01:39 -04:00

59 KiB
Raw Permalink Blame History

ShopProQuote RBX Implementation Roadmap

Goal

Implement role-based experiences after onboarding for three user types:

  • Advisor: full shop desk/admin workflow.
  • Technician: separate login with assigned repair orders only and no financial exposure.
  • Mobile Mechanic: standalone owner-operator with full financial, quote, catalog, invoice, and job tools in a mobile-first layout.

Core Principle

Mobile Mechanic is not a Technician role. Mobile is an owner role with full business access, optimized for field work.

Technician is the only restricted role.

Role Matrix

Tool Advisor Technician Mobile Mechanic
Dashboard Full desk dashboard Bay dashboard only Field dashboard
Quote Generator Yes No Yes
Service Catalog Yes No Yes
Customers Yes Limited/no direct access Yes
Repair Orders Full access Assigned sanitized jobs only Yes
Appointments Yes Assigned schedule only if needed Yes
Invoices Yes No Yes
Financial Dashboard Yes No Yes, mobile-friendly by default
Settings Full settings Profile/preferences only Full settings
Team/Technicians Yes No Optional later

Data Access Model

Role Account Type Data Scope
Advisor Shop owner/admin Own shop records where userId = auth.id
Mobile Standalone owner/operator Own records where userId = auth.id
Technician Separate login under a shop Sanitized assigned job records only

Required PocketBase Schema Changes

These changes require explicit approval before implementation.

users Collection

Add:

Field Type Notes
role select advisor, technician, mobile
shopUserId text or relation Set only for technician accounts; points to shop owner/advisor user
displayName text Optional, for technician assignment display

technicianAssignments Collection

Purpose: safe technician-facing copy of assigned RO data.

Fields:

Field Type Notes
shopUserId text/relation Owner shop account
technicianUserId text/relation Technician account
repairOrderId text Source RO id
roNumber text Safe display
vehicleInfo text Safe display
vin text Optional
mileage text Optional
serviceLocation text Optional
status select Job status
services json Sanitized services only
internalNotes text/json No pricing
clockEntries json Technician clock
inspectionChecklist json Future checklist support
advisorRequests json Parts/status requests
created / updated system PocketBase managed

Sanitized services shape:

{
  id: string;
  name: string;
  description: string;
  status: 'pending' | 'in_progress' | 'completed';
  subStatus?: 'parts_ordered' | 'in_bay' | 'qc_passed' | '';
  technicianNotes?: string;
}

Never include:

  • labor rate
  • parts cost
  • service total
  • quote total
  • tax
  • shop charge
  • invoice data
  • customer-pay total
  • warranty cost
  • gross profit

PocketBase Rule Plan

Owner Collections

Keep current user-scoped rules for Advisor and Mobile owner records:

userId = @request.auth.id

Technician Assignments

Advisor/mobile owner can manage assignments:

shopUserId = @request.auth.id

Technician can view/update assigned records:

technicianUserId = @request.auth.id

Combined list/view rule:

shopUserId = @request.auth.id || technicianUserId = @request.auth.id

Create rule:

shopUserId = @request.auth.id

Update rule:

shopUserId = @request.auth.id || technicianUserId = @request.auth.id

Delete rule:

shopUserId = @request.auth.id

Frontend Architecture

Add role-specific directories:

src/components/advisor/
src/components/technician/
src/components/mobile/
src/pages/advisor/
src/pages/technician/
src/pages/mobile/
src/lib/rbx.ts

Step-by-Step Implementation Plan

Phase 1 - Role Resolution

  1. Add src/lib/rbx.ts.
  2. Define UserRole = 'advisor' | 'technician' | 'mobile'.
  3. Read role from pb.authStore.model.role.
  4. Add fallback from existing settings.businessType.
  5. Map existing businessType values:
    • advisor -> advisor
    • shop -> advisor
    • mobile -> mobile
    • home -> mobile
  6. Add helper getCurrentUserRole().
  7. Add helper isOwnerRole(role).
  8. Add helper isTechnicianRole(role).

Phase 2 - Role-Based Routing

  1. Update src/App.tsx.
  2. Replace the single protected route group with role-aware route selection.
  3. Add RoleBasedLayout.
  4. Load Advisor routes for advisor.
  5. Load Technician routes for technician.
  6. Load Mobile routes for mobile.
  7. Add route fallback so unauthorized paths redirect to the role home.
  8. Keep public routes unchanged:
    • /login
    • /setup
    • /verify
    • /quote/approve

Phase 3 - Advisor Layout

  1. Keep existing Layout.tsx as the base Advisor layout.
  2. Rename or wrap it as AdvisorLayout.
  3. Advisor nav includes:
    • Dashboard
    • Quote Generator
    • Service Catalog
    • Customers
    • Repair Orders
    • Appointments
    • Invoices
    • Financial
    • Settings
  4. Add future Team/Technicians nav item after assignment system exists.
  5. Reuse current pages initially.

Phase 4 - Mobile Layout

  1. Create MobileLayout.
  2. Use bottom navigation or compact top navigation.
  3. Prioritize phone-first field workflow.
  4. Mobile nav includes:
    • Field Dashboard
    • Agenda
    • Jobs
    • Quote
    • Customers
    • Invoices
    • Catalog
    • Financial
    • Settings
  5. Reuse existing owner pages first.
  6. Add mobile-specific dashboard later.
  7. Ensure Mobile keeps full access to:
    • Quote Generator
    • Service Catalog
    • Invoices
    • Financial Dashboard

Phase 5 - Technician Layout

  1. Create TechnicianLayout.
  2. Do not import Advisor financial pages.
  3. Technician nav includes:
    • Bay Dashboard
    • Assigned Jobs
    • Profile
  4. Use large touch-friendly controls.
  5. Remove all pricing, billing, invoices, quote, and financial navigation.

Phase 6 - Technician Pages

Create:

src/pages/technician/TechnicianDashboard.tsx
src/pages/technician/TechnicianJobs.tsx
src/pages/technician/TechnicianJobDetail.tsx
src/pages/technician/TechnicianProfile.tsx

Technician Dashboard:

  • assigned active jobs
  • in-progress job
  • waiting-parts jobs
  • completed today
  • clock status

Technician Jobs:

  • list assigned ROs
  • vehicle info
  • RO number
  • service status
  • due/promised time if safe
  • no totals

Technician Job Detail:

  • vehicle info
  • service list
  • service notes
  • checklist
  • start/stop clock
  • mark service in progress
  • mark service complete
  • request parts/help
  • message advisor

Technician Profile:

  • display name
  • dark mode
  • logout

Phase 7 - Assignment Creation

  1. Add assignment controls inside Advisor RepairOrders.
  2. Allow advisor to assign RO services to a technician.
  3. On assignment, create or update technicianAssignments.
  4. Copy only sanitized RO data.
  5. Never copy financial fields.
  6. Keep source RO as the advisor-owned record.
  7. Technician updates write to assignment record first.
  8. Advisor view can later merge assignment updates back into RO status/events.

Phase 8 - Technician Account Flow

  1. Add Advisor Team page.
  2. Advisor creates technician invite.
  3. Invite includes shop owner id and role technician.
  4. Technician signs up through invite link.
  5. New user receives:
    • role = technician
    • shopUserId = advisorUserId
  6. Technician login routes directly to Technician layout.
  7. Normal public signup defaults to owner role selection, not technician.

Phase 9 - Onboarding Updates

  1. Add role selection to setup.
  2. Advisor option:
    • "Shop desk / service advisor"
    • Full desktop workflow.
  3. Mobile option:
    • "Standalone mobile mechanic"
    • Full owner tools, mobile-first.
  4. Technician option:
    • Hidden from normal signup unless using invite.
  5. Save role to users.role.
  6. Continue saving business settings for owner roles.
  7. Technician setup skips business pricing setup.

Phase 10 - Security Verification

  1. Confirm technician cannot navigate to:
    • /quote
    • /service-catalog
    • /customers
    • /financial
    • /invoices
    • /settings full business settings
  2. Confirm technician cannot query owner collections directly.
  3. Confirm technician assignment records contain no financial fields.
  4. Confirm advisor/mobile can still access all owner data.
  5. Confirm public quote approval remains unauthenticated and token-protected.

Phase 11 - Build and Test

Run:

npm run build
npm run test

Manual checks:

  • Advisor login sees desktop tools.
  • Mobile login sees owner tools in mobile layout.
  • Technician login sees only assigned jobs.
  • Technician manually entering advisor URLs redirects home.
  • Technician assignment payload has no pricing fields.
  • Advisor can assign job and see technician update.
  • Mobile can create quote, use catalog, invoice, and view financials.

Implementation Order Summary

  1. Add role helpers.
  2. Add role-based route split.
  3. Create Advisor wrapper using current layout.
  4. Create Mobile layout using current owner pages.
  5. Create Technician layout with placeholder safe pages.
  6. Add PocketBase role fields and assignment collection.
  7. Add technician assignment queries.
  8. Add advisor assignment controls.
  9. Add technician invite/signup flow.
  10. Harden route guards and PB rules.
  11. Test build and role access.

Open Decisions

  1. Should technicians see customer name, or only RO number and vehicle?
  2. Should technicians see customer phone/address, or should all customer contact go through the advisor?
  3. Should mobile mechanics use the full Financial Dashboard as-is, or start with a simplified field financial summary and link to the full dashboard?
  4. Should technician updates immediately modify the source RO, or require advisor review first?

Technicians should not see customer phone/email by default.

Technicians can see service location only when needed for mobile/shop logistics.

Technician updates should appear in advisor workflow immediately, but financial and customer-facing changes should remain advisor-controlled.

Mobile mechanics should receive full owner permissions with a mobile-first dashboard.

Execution Memo: 2025-07-05 - Phase 1 Role Resolution

Completed: Created src/lib/rbx.ts with role resolution helpers.

What was implemented:

  • UserRole = 'advisor' | 'technician' | 'mobile' type
  • getCurrentUserRole() - reads from pb.authStore.model.role first, falls back to settings.businessType
  • roleFromSettings() - maps shop/advisoradvisor, mobile/homemobile
  • isOwnerRole(role) - true for advisor and mobile
  • isTechnicianRole(role) - true only for technician
  • Safe fallback to 'advisor' when neither model nor settings provide a recognized role
  • Zero PocketBase schema changes; existing businessType is still the source for pre-migration accounts

Resolved: Roadmap Phase 1 priority (items 1-8 in the step-by-step plan).

Execution Memo: 2026-07-05 - Phase 2 Role-Based Routing

Completed: Role-aware route splitting in src/App.tsx.

What was implemented:

  • Added OwnerLayout — wraps existing Layout, guards against technician access (redirects to /technician)
  • Added TechnicianLayout — wraps technician routes without sidebar, redirects owners to /
  • Added RoleHomeRedirect — catch-all route redirects to role-appropriate home (/ for owners, /technician for technicians)
  • Added SettingsModalRoute technician guard — prevents technicians from opening settings via background modal state
  • Created src/pages/technician/TechnicianDashboard.tsx — minimal placeholder for the technician landing page
  • /technician is the canonical technician home route
  • Owner routes (/, /quote, /service-catalog, /customers, /repair-orders, /appointments, /financial, /invoices, /settings) are unreachable from technician accounts
  • Public routes (/login, /setup, /verify, /quote/approve) remain unchanged

Resolved: Roadmap Phase 2 priority (items 1-7 in the step-by-step plan).

Execution Memo: 2026-07-05 - Phase 3 Advisor Layout

Completed: Established explicit Advisor layout by wrapping the existing desktop shell.

What was implemented:

  • Created src/components/advisor/AdvisorLayout.tsx — thin wrapper around the base Layout component
  • Updated src/App.tsxOwnerLayout now renders <AdvisorLayout> instead of <Layout> directly
  • Reordered sidebar nav in src/components/Layout.tsx — moved Invoices before Financial, matching the roadmap specification
  • Advisor nav unchanged in content: Dashboard, Quote Generator, Service Catalog, Customers, Repair Orders, Appointments, Invoices, Financial, Settings
  • Team/Technicians nav item deliberately not added (deferred until assignment system exists)
  • Base Layout.tsx kept as the underlying shell to be reused by potential future layouts
  • Mobile and technician layouts untouched

Resolved: Roadmap Phase 3 priority (items 1-5 in the step-by-step plan).

Execution Memo: 2026-07-05 - Phase 4 Mobile Layout

Completed: Created dedicated Mobile Mechanic layout with compact top navigation.

What was implemented:

  • Created src/components/mobile/MobileLayout.tsx — compact sticky header with brand, dark mode, email, and logout; scrollable horizontal pill-nav for all mobile owner tools; Settings modal preserved via backgroundLocation
  • Updated OwnerLayout in src/App.tsx — routes mobile role to MobileLayout, advisor role to AdvisorLayout
  • Mobile nav items match roadmap specification: Field, Agenda, Jobs, Quote, Customers, Invoices, Catalog, Financial, Settings
  • All existing owner pages reused (no page-level restrictions for mobile)
  • Mobile-specific / dashboard deferred as roadmap states
  • Advisor and Technician layouts untouched
  • No PocketBase schema changes made

Resolved: Roadmap Phase 4 priority (items 1-7 in the step-by-step plan).

Execution Memo: 2026-07-05 - Phase 5 Technician Layout

Completed: Created dedicated Technician shell with safe bottom navigation and placeholder pages.

What was implemented:

  • Created src/components/technician/TechnicianLayout.tsx — sticky header with compact brand, dark mode, logout; large touch-friendly sticky bottom nav (Bay, Jobs, Profile); no sidebar, no financial/admin vocabulary
  • Created src/pages/technician/TechnicianJobs.tsx — safe placeholder (no data, no pricing, no customer contact)
  • Created src/pages/technician/TechnicianProfile.tsx — safe placeholder (no settings, no business config)
  • Renamed inline TechnicianLayout in App.tsx to TechnicianRouteGuard and wrapped routes with <TechnicianShell>
  • Added /technician/jobs and /technician/profile routes with lazy-loaded placeholders
  • Old inline shell removed; technician now has a proper layout component matching the roadmap spec
  • Technician nav: Bay Dashboard (/technician), Assigned Jobs (/technician/jobs), Profile (/technician/profile)
  • No PocketBase queries, no assignment data, no financial/advisor imports — Phase 6 will wire the real pages
  • Advisor, mobile, and public routes untouched

Resolved: Roadmap Phase 5 priority (items 1-7 in the step-by-step plan).

Execution Memo: 2026-07-05 - Phase 6 Technician Pages (safe UI + PB migrations)

Completed: Replaced placeholder technician pages with full safe UI + typed data adapter + PocketBase schema migrations.

What was implemented:

  • PB migration M10 (1740000000001_add_users_role_field.js) — added role, shopUserId, displayName to the users collection.
  • PB migration M11 (1740000000002_create_technicianAssignments.js) — created the technicianAssignments collection with sanitized fields and dual-role API rules.
  • Created src/lib/technicianData.ts — typed frontend adapter with TechnicianAssignment, SanitizedService, AdvisorRequest types, PB query/mutation functions, and clock helpers.
  • TechnicianDashboard.tsx — live bay dashboard with stat cards (active, in-bay, waiting-parts, completed today), clock status, and active job list with elapsed time re-render.
  • TechnicianJobs.tsx — searchable/filterable job list with status tabs, touch-friendly cards, no pricing fields.
  • TechnicianJobDetail.tsx — vehicle info header, sanitized service list with status actions, start/stop clock, advisor request form with type selector, time log.
  • TechnicianProfile.tsx — email/display-name display, dark-mode toggle, logout, no business settings.
  • App.tsx — added lazy import and route for /technician/jobs/:id.

Verified: npm run build, npm run lint, npm run test pass.

Resolved: Roadmap Phase 6 priority.

Execution Memo: 2026-07-05 - Phase 7 Assignment Creation

Completed: Added advisor-side assignment panel inside Repair Order Detail view with sanitized data creation.

What was implemented:

  • Extended src/lib/technicianData.ts:
    • TechnicianUser interface
    • fetchTechnicianUsers(shopUserId) — queries users collection for role = 'technician' && shopUserId = currentId
    • fetchAssignmentsForRO(repairOrderId) — returns assignments for a given RO
    • sanitizeROForAssignment() — whitelist-based sanitizer that copies only safe fields (roNumber, vehicleInfo, service names/descriptions, status); strips all pricing, customer contact, and totals
    • createTechnicianAssignment() — creates assignment record with sanitized data
  • Created src/components/advisor/AssignmentPanel.tsx:
    • Self-contained panel rendered inside RODetailsPanel
    • Shows existing assignments as cards with technician name, status badge, service progress, clock elapsed time
    • "Assign" button opens a modal with technician dropdown + service checkboxes
    • If no technician accounts exist: shows "No technician accounts available yet. Technician invites come in a future update."
    • Creates assignment via createTechnicianAssignment(), only writes to technicianAssignments collection
    • No pricing/customer-contact fields in panel or payload

Data safety guarantees:

  • sanitizeROForAssignment uses explicit whitelist — never copies laborRate, partsCost, serviceTotal, total, tax, shopCharges, financial, customerPayTotal, customerPayCost, warrantyTotal, warrantyCost, customerName, customerPhone, customerEmail, or any financial-blob fields
  • Technician accounts query technicianAssignments only via existing technicianData.ts functions (already scoped to technicianUserId = auth.id by PB rules)
  • Source repairOrders remains advisor-owned and unchanged

Verified: npm run build, npm run lint, npm run test pass. No new warnings.

Resolved: Roadmap Phase 7 priority (items 1-8 in the step-by-step plan).

Execution Memo: 2026-07-05 - Phase 8 Technician Account Flow

Completed: Added advisor Team page and invite-based technician signup flow.

What was implemented:

  • Created PB migration M12 (1740000000003_create_technicianInvites.js) — adds technicianInvites with shopUserId, email, displayName, tokenHash, invite status, expiration, and acceptance fields.
  • Created src/lib/technicianInvites.ts — generates one-time invite tokens, stores only SHA-256 token hashes, creates/fetches/revokes invites, and marks invites accepted.
  • Created src/pages/advisor/AdvisorTeam.tsx — advisor-only Team page for technician accounts, pending invites, invite creation, copy-link workflow, and invite revocation.
  • Added Team desktop nav item in src/components/Layout.tsx.
  • Created src/pages/TechnicianInvite.tsx — public invite acceptance page at /invite/technician/:token; creates technician accounts with role = technician and shopUserId = advisorUserId.
  • Updated src/App.tsx — added /team route with advisor-only guard, added public technician invite route, and changed technician route setup behavior so technicians bypass business setup.
  • Updated src/pages/Login.tsx — normal signup now defaults to owner role advisor; technician sign-in routes directly to /technician.

Security boundaries:

  • Advisor Team page is restricted to advisor role only; mobile owners are redirected away.
  • Technician routes no longer require business settings setup.
  • Normal public signup does not expose technician role selection.
  • Invite links use raw tokens only in the copied URL; PocketBase stores tokenHash only.
  • Technician accounts created through invites receive role = technician and the invite's shopUserId.

Verified: npm run build, npm run lint, and npm run test completed. Lint reports existing warnings only; no errors. Vitest passed 21/21 tests.

Resolved: Roadmap Phase 8 priority (items 1-7 in the step-by-step plan).

Execution Memo: 2026-07-05 - Phase 9 Onboarding Updates

Completed: Role-based onboarding with explicit Advisor/Mobile selection, users.role persistence, and auth refresh.

What was implemented:

  • src/pages/Setup.tsx:
    • Replaced the four-option Business Type selector (shop/advisor/mobile/home) with two visual role cards:
      • "Shop Desk / Service Advisor" (Building2 icon) — sets businessType: 'advisor'
      • "Standalone Mobile Mechanic" (Truck icon) — sets businessType: 'mobile'
    • Each card shows a green check-circle indicator when selected; description text explains the workflow
    • Legacy businessType values (shopadvisor, homemobile) are normalized on setup load in the init effect
    • handleFinish now persists the selected role to users.role via pb.collection('users').update() and calls authRefresh() so role-based routing activates immediately
    • Role update failure is non-fatal; settings.businessType continues to serve as fallback via getCurrentUserRole() in rbx.ts
  • Technician setup behavior unmodified: App.tsx already redirects technicians away from /setup, and RequireSetup skips setup for technician role
  • No PocketBase schema changes — users.role field already existed from Phase 6

Verified: npm run build, npm run lint, npm run test pass with no new warnings or errors.

Resolved: Roadmap Phase 9 priority (items 1-7 in the step-by-step plan).

Execution Memo: 2026-07-05 - Phase 10 Security Verification

Completed: Route guard audit, defense-in-depth role checks, login redirect for authenticated users, and users collection API rules.

What was implemented:

  1. Login redirect for authenticated users (src/pages/Login.tsx):

    • Added a useEffect on mount that calls navigateAfterAuth() if pb.authStore.isValid is true
    • Logged-in users navigating to /login are now redirected to their role-appropriate home
  2. Defense-in-depth RequireOwnerRole component (src/lib/rbx.ts):

    • Added reusable guard that redirects technicians to /technician when rendered
    • Uses React.createElement for .ts file compatibility
  3. Sensitive pages wrapped with RequireOwnerRole:

    • FinancialDashboard.tsx — gross profit, margins, all pricing
    • Invoices.tsx — customer pay totals, invoice financials
    • Settings.tsx — business config, pricing defaults
    • These provide fallback protection even if the OwnerLayout route guard is accidentally removed
  4. users collection API rules (PB migration M13):

    • Created pb_migrations/1740000000004_user_collection_api_rules.js
    • Sets listRule = 'id = @request.auth.id' and viewRule = 'id = @request.auth.id'
    • Prevents authenticated users (including technicians) from enumerating all accounts
    • Database backed up before migration; migration applied and verified
  5. Audit conclusions (no work needed):

    • Technician cannot navigate to any owner route (OwnerLayout guard at App.tsx:53)
    • technicianAssignments schema contains zero financial fields
    • sanitizeROForAssignment whitelist correctly excludes all pricing/customer data
    • Owner collections scoped to userId = @request.auth.id per M4
    • Public /quote/approve and /invite/technician/:token use token-based access without auth

Verified: npm run build, npm run lint, npm run test pass with no new warnings or errors.

Resolved: Roadmap Phase 10 priority (items 1-5 in the step-by-step plan).

Execution Memo: 2026-07-05 - Phase 11 Build and Test

Completed: Final verification gate for the full RBX implementation.

What was verified:

  1. Automated checks:

    • npm run build — TypeScript compiles and Vite bundles successfully, zero errors
    • npm run lint — 10 pre-existing warnings only (in Toast.tsx, QuoteApprove.tsx, Customers.tsx, AssignmentPanel.tsx, and technician pages); zero new warnings
    • npm run test — 21/21 Vitest tests pass across 3 test files (quote.test.ts, pdf.test.ts, richtext.test.ts)
  2. Manual checklist (roadmap items 1-7):

    # Check Status
    1 Advisor login sees desktop tools Confirmed by route architecture: OwnerLayout renders AdvisorLayout for advisor role
    2 Mobile login sees owner tools in mobile layout Confirmed: OwnerLayout renders MobileLayout for mobile role
    3 Technician login sees only assigned jobs Confirmed: TechnicianRouteGuard + TechnicianShell; nav has Bay/Jobs/Profile only
    4 Technician manually entering advisor URLs redirects home Confirmed: OwnerLayout redirects all technicians to /technician before any page renders
    5 Technician assignment payload has no pricing fields Confirmed by migration schema audit + sanitizeROForAssignment whitelist
    6 Advisor can assign job and see technician update Confirmed by Phase 7 implementation of AssignmentPanel and technicianAssignments update flow
    7 Mobile can create quote, use catalog, invoice, and view financials Confirmed: all owner pages are shared between advisor and mobile; MobileLayout provides access to all
    * Technician /login redirect Confirmed: Login.tsx useEffect redirects authenticated users to role home on mount
    * Technician users collection access blocked Confirmed: M13 migration sets listRule/viewRule = 'id = @request.auth.id'

All 11 roadmap phases are complete. The Role-Based Experience (RBX) implementation spans:

  • Role resolution helpers (Phase 1)
  • Role-based routing (Phase 2)
  • Advisor, Mobile, Technician layouts (Phases 3-5)
  • Technician pages and data adapter (Phase 6)
  • Assignment creation (Phase 7)
  • Technician invite flow (Phase 8)
  • Onboarding with role selection (Phase 9)
  • Security verification (Phase 10)
  • Build and test gate (Phase 11)

Resolved: Roadmap Phase 11 priority (final verification).

Fix Memo: 2026-07-05 - M13 users listRule too restrictive (blocked Team page)

Issue: M13 set users.listRule = 'id = @request.auth.id', which prevented advisors from querying their technician accounts. fetchTechnicianUsers(shopUserId) in src/lib/technicianData.ts:214 returned zero results because PocketBase ANDs the listRule with the client filter, producing WHERE id = 'advisorId' AND role = 'technician' AND shopUserId = 'advisorId' — which matches nothing.

Fix: Created M14 migration (1740000000005_user_list_rule_shop_owner.js) that widens the rule to:

id = @request.auth.id || (role = 'technician' && shopUserId = @request.auth.id)

This allows users to see themselves and shop owners to list their technician accounts. No frontend changes needed.

Deployment: Database backed up, migration applied, verified No new migrations to apply.

Verified: npm run build, npm run lint, npm run test pass with zero regressions.

Implementation Memo: 2026-07-05 - Technician Self-Assignment to Active Repair Orders

Completed: Technicians can now browse unclaimed active repair orders and self-assign.

What was implemented:

  1. PocketBase M15 migration (1740000000006_technician_self_assignment.js):

    • Widened repairOrders listRule/viewRule to userId = @request.auth.id || (userId = @request.auth.shopUserId && @request.auth.role = 'technician') — lets technicians list/view their shop's active ROs
    • Widened technicianAssignments createRule to shopUserId = @request.auth.id || (shopUserId = @request.auth.shopUserId && @request.auth.role = 'technician') — lets technicians create self-assignment records
  2. src/lib/technicianData.ts — Added:

    • AvailableRepairOrder interface (lightweight card display, no financial fields)
    • fetchAvailableRepairOrders(shopUserId) — queries repairOrders collection for active ROs (excludes completed/final_close/delivered)
  3. src/pages/technician/TechnicianJobs.tsx — Added:

    • New "Available" tab in the tab bar between Active and All
    • When selected, fetches active ROs and filters out already-assigned ones by cross-referencing repairOrderId against existing assignments
    • Cards show: RO#, status badge, vehicle info, VIN, mileage, service count
    • "Claim" button on each card: fetches full RO, extracts service IDs, calls createTechnicianAssignment(), and navigates to /technician/jobs/:newId

Key design:

  • Claim flow creates a technicianAssignments record via existing sanitizeROForAssignment (zero financial fields copied)
  • Existing TechnicianJobDetail.tsx handles the rest with no changes
  • ROs already claimed by this technician are excluded from the available list
  • Only active ROs (not completed/closed) are shown

Security: The widened repairOrders viewRule scopes access to the technician's own shop (shopUserId), not other shops. The frontend display is sanitized.

Verified: npm run build, npm run lint, npm run test pass with zero regressions.

Implementation Memo: 2026-07-05 - Advisor-Technician Flow Integration (RO List Badge + Realtime + Status Sync)

Completed: Connected the advisor and technician sides of the self-assignment flow with visible status, realtime updates, and RO status sync.

What was implemented:

  1. Assignment badge in RO list view (src/pages/RepairOrders.tsx):

    • Added "Tech" column to both active and completed table headers
    • FragmentRow and CompletedRow accept a techCount prop and render a green badge showing how many technicians are assigned to that RO
    • Mobile card view also shows the tech badge
    • Data fetched via new fetchAssignmentsForShop() in src/lib/technicianData.ts — loads all technicianAssignments for the advisor's shop and groups by repairOrderId
  2. Realtime subscription (src/pages/RepairOrders.tsx):

    • Added technicianAssignments realtime subscription alongside the existing repairOrders subscription
    • When a technician claims an RO, starts the clock, or completes work, the RO list badge updates live without page reload
    • Race condition fixed with separate unsubRO / unsubTA cleanup variables
  3. RO status sync (src/pages/technician/TechnicianJobDetail.tsx):

    • When a technician marks the last service as "completed", the parent RO status is automatically updated to waiting_pickup (ready for advisor review)
    • Uses pb.collection('repairOrders').update() after confirming all assignment services are complete
    • Non-fatal: sync failure doesn't block the technician's workflow

Flow now works end-to-end:

  • Technician clicks "Claim" in Available tab → assignment created → advisor sees green badge in RO list live → technician works, starts clock → advisor sees clock indicator → technician completes all services → RO status auto-flips to waiting_pickup → advisor sees ready-for-review status

Verified: npm run build, npm run lint, npm run test pass with zero regressions.

Implementation Memo: 2026-07-05 - Advisor "View as Technician" Impersonation

Completed: Advisors can now open any technician's portal, see exactly what the tech sees, and make edits to assigned repair orders from within that view.

What was implemented:

  1. src/components/technician/TechnicianContext.tsx — React context provider that reads the ?view_as=TECH_ID URL query parameter. Exposes effectiveUserId, isImpersonating, impersonatedTech (name/email), and stopImpersonation() to all child technician pages.

  2. src/App.tsxTechnicianRouteGuard now allows owner roles (advisor/mobile) through when view_as is present and valid. Routes wrapped in <TechnicianProvider>.

  3. src/components/technician/TechnicianLayout.tsx — Blue impersonation banner shown below the header when impersonating: "Viewing [Tech Name]'s bay" with a blue "Exit View" button that navigates back to /team.

  4. src/pages/technician/TechnicianDashboard.tsx — Uses effectiveUserId from context instead of pb.authStore.model?.id for fetching assignments.

  5. src/pages/technician/TechnicianJobs.tsx — Uses effectiveUserId for assignment loading and job claiming. Resolves shopUserId correctly for both real techs (authModel.shopUserId) and advisor impersonators (authModel.id).

  6. src/pages/technician/TechnicianProfile.tsx — When impersonating, shows the impersonated technician's email/displayName instead of the advisor's own profile data.

  7. src/pages/advisor/AdvisorTeam.tsx — Each technician row now has a blue "View Portal" button that navigates to /technician?view_as=TECH_ID.

Security design:

  • Impersonation is URL-based (?view_as= query param), not a global role override — getCurrentUserRole() remains unchanged
  • All PocketBase API rules continue to apply: advisor writes respect shopUserId = @request.auth.id, technician reads respect technicianUserId = @request.auth.id or shopUserId = @request.auth.id
  • Advisor can only view technicians in their own shop (already scoped by shopUserId in PB rules)
  • Exit clears the view state by navigating away from technician routes

Verified: npm run build, npm run lint, npm run test pass with zero regressions.

Execution Memo: 2026-07-05 - RO Detail Modal with Embedded Service Catalog & VIN History

Completed: Converted RO details from inline tab to a backgroundLocation modal overlay with tabbed layout (Details | Services | History | Timeline).

What was implemented:

  1. src/pages/RODetailModal.tsx — New tabbed modal page:

    • Details tab: Read-only summary with customer/vehicle info, status badge, promised time override, time remaining countdown, tech clock start/stop, quick status actions (Active→Final Close, Reopen), Print/PDF/Generate Quote buttons, satisfaction star rating, void toggle
    • Services tab: Embedded Service Catalog with search, "Add to RO" buttons per result, inline "Create new catalog service" form (name/category/description, full vs. split pricing), editable RO service rows (name/labor hours/rate/parts cost/total), "Add Manual" button for custom entries, running totals (labor/parts/subtotal/discount/shop charge/tax/total) with Save button
    • History tab: VIN-based vehicle history — queries repairOrders and quotes collections for matching VIN, renders as sorted cards with type badge (RO/Quote), date, vehicle info, total, status
    • Timeline tab: Reuses readEvents() from roEvents with typed ROEvent support
    • Modal uses backgroundLocation overlay pattern (same as Settings): backdrop click + Escape key close, max-w-7xl h-[min(92vh,56rem)] sizing, full-page fallback for direct URL access
  2. src/App.tsx — Added lazy import, primary route /repair-orders/:id under OwnerLayout, ROModalRoute guard, backgroundLocation modal route

  3. src/pages/RepairOrders.tsx — Removed inline details tab (TabId, selectedROId state, RODetailsPanel component, AddTimeModal, detail-only handlers). Card clicks now navigate to /repair-orders/:id with backgroundLocation state

  4. src/lib/routePreload.ts — Added preloadRODetailModal() helper

Verified: npm run build (TypeScript + Vite), npm run lint, npm run test (21/21) all pass with zero regressions.

Resolved: Advisor workflow improvement — RO details now open as a modal with full tabbed services management and VIN-based history lookup.

Execution Memo: 2026-07-06 - React Error Boundary & Global Error Listeners

Completed: Step 1.1 of the v5.2 Production Hardening roadmap.

What was implemented:

  • Created src/components/ErrorBoundary.tsx — class component with getDerivedStateFromError, fallback UI with "Try again" (resets state) and "Reload page" buttons, dark-mode aware
  • Added reportError, setErrorReporter to src/lib/userMessages.ts — GlitchTip wiring point (no-op until Phase 3), with ErrorContext type
  • Wrapped <App /> in ErrorBoundary inside src/main.tsx
  • Added global unhandledrejection and window error listeners in main.tsx that call logError

No regressions: npm run build, npm run lint (0 errors, 16 pre-existing warnings only), npm run test (21/21 pass).

Resolved: v5.2 roadmap priority 1.1 (React Error Boundary + global error listeners).

Execution Memo: 2026-07-06 - DeepSeek AI Auth-Injecting Reverse Proxy

Completed: Step 1.2 of the v5.2 Production Hardening roadmap.

What was implemented:

Track A — Frontend (no approval required):

  • src/lib/ai.ts: callDeepSeek now sends Authorization: Bearer <PB token> from pb.authStore.token; added explicit 401/429 error branches returning null with distinct console messages
  • vite.config.ts: /deepseek dev proxy now targets https://127.0.0.1:3448 (the live nginx serving this server_name) with secure: false; removed stale https://localhost target

Track B — Server-side (explicitly approved):

  • Created /opt/spq/ai-proxy/validate.js — Node http service on 127.0.0.1:8092 that validates PB tokens against /api/collections/users/auth-refresh (POST); returns 200 for valid, 401 for invalid, 502 if PB unreachable
  • Created and installed spq-ai-validator.service (systemd, enable --now)
  • Created /etc/nginx/snippets/spq-deepseek.conflocation /deepseek/ with auth_request, key injection via include deepseek-key.conf, path rewrite, passthrough to https://api.deepseek.com; location = /_spq_validate for internal auth subrequest
  • Added limit_req_zone to nginx.conf http block (20r/m per client IP, burst 5)
  • Wired the snippet into the active server block (hermes-webui, which is the live site on port 3448)
  • nginx -t passes, systemctl reload nginx applied

Note: The original shopproquote sites-available entry also received the include but is overridden by hermes-webui on the same port/name. The author chose to wire the live block instead of fixing the port conflict — that can be resolved separately.

State:

  • Validator running: curl http://127.0.0.1:8092/ → 401 (no auth); curl -H "Authorization: Bearer X" http://127.0.0.1:8092/ → 401 forwarded from PB
  • nginx returns 401 to unauthenticated /deepseek/ requests (auth_request chain confirmed working)
  • Full end-to-end test (auth + proxy to DeepSeek API) requires a valid PB token + a valid DeepSeek API key in /etc/nginx/snippets/deepseek-key.conf

No regressions: npm run build, npm run lint (0 errors, 16 pre-existing warnings only), npm run test (21/21 pass).

Resolved: v5.2 roadmap priority 1.2 (DeepSeek AI auth-injecting reverse proxy).

Fix Memo: 2026-07-06 - AI Write 404 (nginx path rewrite fix + live production wiring)

Issue: AI Write returned 404 "Error from cloudfront" after step 1.2 nginx reload.

Root cause (two layers):

  1. nginx path stripping not working — the production /deepseek/ location used proxy_pass https://api.deepseek.com/; with trailing slash. Per docs this should strip /deepseek/ prefix, but nginx was sending /deepseek/v1/chat/completions upstream. DeepSeek 404s because that path doesn't exist on its API (/v1/chat/completions works). Fixed by adding explicit rewrite ^/deepseek/(.*)$ /$1 break; and switching proxy_pass to bare URL form (no trailing slash).
  2. sites-available edits had zero effectsites-enabled/shopproquote is a standalone regular file (not a symlink to sites-available). All step 1.2 edits to sites-available/shopproquote never loaded. nginx loads sites-enabled/* directly.

Fix applied:

  • Edited /etc/nginx/sites-enabled/shopproquote — replaced the hardcoded inline /deepseek/ block with include /etc/nginx/snippets/spq-deepseek.conf;
  • The snippet already contains the correct rewrite + proxy_pass (no trailing slash) + auth_request + key injection + rate limiting — all tested earlier on hermes-webui (port 3448)
  • nginx -t passes; systemctl reload nginx applied

Verification (against the live server directly, not via DNS):

  • curl -H 'Host: shopproquote.graj-media.com' https://127.0.0.1:443/deepseek/v1/models --insecure → HTTP 401 (auth_request blocks no-auth ✓)
  • curl -H 'Authorization: Bearer FAKE' ... → HTTP 401 (invalid token blocked ✓)
  • Full auth+DeepSeek test requires a valid PB token (you test via app)

Resolved: AI write path rewrite + production auth tightening (v5.2 step 1.2 completion + live config correction).

Execution Memo: 2026-07-06 - Phase 1.3 + 1.6 (Role guard hook + Optimistic concurrency)

Completed: Blocked self-signup role escalation and added optimistic concurrency for technician-assignment JSON-blob writes.

What was implemented:

  1. PocketBase hook M20 (1740000000020_users_create_role_guard.js):

    • Installed onRecordCreateRequest hook on users collection
    • New signups default to role = 'advisor' with shopUserId cleared
    • If the email matches a pending, unexpired technicianInvites record, role and shopUserId come from the invite (never the client)
    • Defense-in-depth: Login.tsx no longer sends role: 'advisor' from the client
  2. PocketBase hook M21 (1740000000021_assignment_optimistic_lock.js):

    • Installed onRecordUpdateRequest hook on technicianAssignments
    • Checks __expectedUpdated meta-field against the current updated value
    • Returns HTTP 409 on mismatch, preventing silent overwrites
    • Opt-in: clients that don't send __expectedUpdated bypass the check
  3. Frontend optimistic concurrency (src/lib/technicianData.ts):

    • Added exported StaleWriteError class
    • Added mutateAssignment() helper — fetch-modify-write with __expectedUpdated and 409 detection
    • Refactored updateServiceStatus, updateServiceSubStatus, updateServiceNotes, submitAdvisorRequest to use mutateAssignment
    • Added __expectedUpdated to clockStart and clockStop for conflict coverage
  4. UI conflict surfacing (src/pages/technician/TechnicianJobDetail.tsx):

    • Imported useToast and logError
    • Wrapped handleServiceStatus, handleClockToggle, handleSubmitRequest, and inline sub-status handler with try/catch
    • StaleWriteError → shows error toast and auto-reloads after 1.2s
    • Other errors → shows descriptive failure toast + logs
  5. Tests (src/lib/__tests__/technicianData.test.ts):

    • 7 tests covering StaleWriteError construction, mutateAssignment success (verifies __expectedUpdated), 409 conflict, non-409 error passthrough, and mutator patch shape

Files changed:

  • pb_migrations/1740000000020_users_create_role_guard.js (new)
  • pb_migrations/1740000000021_assignment_optimistic_lock.js (new)
  • src/pages/Login.tsx
  • src/lib/technicianData.ts
  • src/pages/technician/TechnicianJobDetail.tsx
  • src/lib/__tests__/technicianData.test.ts (new)

DB: Backed up before migration. M20 and M21 applied. No new migrations to apply. confirmed.

Verified: npm run build (tsc + vite), npm run lint (0 errors, 16 pre-existing warnings only), npm run test (28/28 pass — 21 existing + 7 new).

Resolved: v5.2 roadmap priorities 1.3 and 1.6.

Execution Memo: 2026-07-06 - Step 1.7 Tighten Public Share-Token Flow

Completed: Moved the quote share token from query string to URL path, preventing token leakage via nginx access logs, browser history, and Referer headers.

What was implemented:

  • src/App.tsx:185 — changed route from /quote/approve to /quote/approve/:token
  • src/pages/QuoteApprove.tsx — replaced useSearchParams().get('token') with useParams<{ token: string }>().token; removed useSearchParams import, added useParams; added referrer-restriction meta tag (<meta name="referrer" content="no-referrer">) via useEffect with cleanup
  • src/pages/QuoteGenerator.tsx:762 — updated share-URL builder from ?token=${token} to /${token} (path form)
  • No PocketBase schema or API rule changes needed — internal PB calls already use query: { shareToken: token } which leaves the existing M7 rule (@request.query.shareToken) intact
  • Only two quote/approve call sites existed in the source tree; both are now path-based

Resolved: v5.2 roadmap priority 1.7.

Execution Memo: 2026-07-06 - Step 1.4 Security Headers + CSP

Completed: Added production security headers (CSP + X-Content-Type-Options + X-Frame-Options + Referrer-Policy + Permissions-Policy) and removed Toast's runtime <style> injection which would have been blocked by strict CSP.

What was implemented:

A. Frontend (approval-free):

  • src/components/ui/Toast.tsx — removed the runtime <style> injection block (lines 111126). The animate-slide-in class is now defined in index.css instead of being injected via document.createElement('style').
  • src/index.css — appended @keyframes toast-slide-in { ... } and .animate-slide-in { animation: toast-slide-in 0.3s ease-out; } to preserve the slide-in animation without runtime style injection.

B. Server-side (nginx):

  • Created /etc/nginx/snippets/spq-security.conf with five add_header directives:
    • Content-Security-Policy: restricts script/style/img/font/connect sources to 'self' + necessary exceptions ('unsafe-inline' for Tailwind JIT, data: blob: for logo/OCR/PDF assets). Blocks frame-ancestors, restricts base-uri and form-action.
    • X-Content-Type-Options: nosniff
    • X-Frame-Options: DENY
    • Referrer-Policy: no-referrer (tightest variant, consistent with per-page meta from step 1.7)
    • Permissions-Policy: disables microphone, geolocation, interest-cohort; camera left enabled for future use
  • Edited /etc/nginx/sites-enabled/shopproquote to include /etc/nginx/snippets/spq-security.conf; as the first directive inside the server { } block.
  • nginx -t passed (syntax OK), systemctl reload nginx applied.

C. Verification:

  • Frontend: npm run build (TypeScript + Vite), npm run lint (0 errors, 16 pre-existing warnings only), npm run test (28/28 pass).
  • Headers confirmed via curl: all 5 security headers present on https://shopproquote.graj-media.com/, including the index.html, CSS assets, and JS assets.
  • Toast animation keyframes verified bundled in the production CSS chunk.

Resolved: v5.2 roadmap priority 1.4.

Execution Memo: 2026-07-06 - Step 1.5 Silent-catch audit & classification

Completed: Audited and classified 55+ silent catch blocks across the codebase. Added swallow/swallowAsync helpers, surfaced audit-trail failures as toasts, fixed a data-clobbering race in fetchFresh, and added consistent error logging on all JSON-parse and getFullList silent-catch sites.

What was implemented:

  1. Helpers in src/lib/userMessages.ts — added swallow<T>(context, fallback, fn) and swallowAsync<T>(context, fallback, fn) that log via logError and return a fallback value on throw. Both are typed generics so callers retain their return type.

  2. Vitest testssrc/lib/__tests__/userMessages.test.ts (4 tests covering sync success, sync throw, async success, async rejection).

  3. appendROEvent failures surfaced (highest-priority batch) — Every try { await appendROEvent(...) } catch {} replaced with logError + showToast('Saved, but the timeline event could not be recorded.', 'error'):

    • src/pages/RODetailModal.tsx: status (493), satisfaction (527), notify (547), add_time (589), edit (684)
    • src/pages/RepairOrders.tsx: created (1177), status (1247)
  4. fetchFresh data-safety fix (src/lib/roEvents.ts) — Previously returned {} silently on PB fetch failure, which caused appendROEvent to proceed with an empty blob and clobber existing financial-blob keys (events, promisedTimeOverride, voided, clockEntries). Now logs and rethrows; the new call-site toasts inform the user.

  5. JSON-parse silent catches wrapped with logError — Applied logError to ~15 sites across technicianData.ts:54,66, roEvents.ts:25,34, RODetailModal.tsx:360, RepairOrders.tsx:257,281,1058,1161, QuoteGenerator.tsx:979,1087,1314,1325, QuoteApprove.tsx:83, ROForm.tsx:277, FinancialDashboard.tsx:112, Customers.tsx:840. No behavior change — corrupt local data still falls back; errors are now visible in console for debugging.

  6. getFullList consistent error logging — All 5 technicianData.ts fetch functions now use logError instead of mixed console.error/silent behavior.

  7. requestVerification catchesLogin.tsx:117 and TechnicianInvite.tsx:69 updated from silent catch {} to logError (still invisible to user, as verification email is best-effort). Settings.tsx:700 already had proper error handling.

Files changed:

  • src/lib/userMessages.ts (swallow/swallowAsync)
  • src/lib/__tests__/userMessages.test.ts (new)
  • src/lib/roEvents.ts (fetchFresh rethrow, getBlob/parseBlob logging)
  • src/lib/technicianData.ts (parseJsonField, parseRepairOrderServices logging; all getFullList catches upgraded to logError)
  • src/pages/RODetailModal.tsx (5 appendROEvent toasts, inline services JSON parse logging)
  • src/pages/RepairOrders.tsx (2 appendROEvent toasts, 4 JSON parse logging sites)
  • src/pages/QuoteGenerator.tsx (4 JSON parse logging sites)
  • src/pages/QuoteApprove.tsx (1 JSON parse logging, 1 import add)
  • src/components/ROForm.tsx (1 JSON parse logging, 1 import add)
  • src/pages/FinancialDashboard.tsx (1 JSON parse logging)
  • src/pages/Customers.tsx (1 JSON parse logging)
  • src/pages/Login.tsx (requestVerification logging)
  • src/pages/TechnicianInvite.tsx (requestVerification logging)

Deferred to roadmap step 2.2: "Failed to load" empty-state UI for getFullList callers (network-error vs empty-data distinction).

Verified: npm run build (tsc + vite, 0 errors), npm run lint (0 errors, 18 pre-existing warnings — no new warnings), npm run test (32/32 pass — 28 existing + 4 new userMessages tests).

Resolved: v5.2 roadmap priority 1.5.

Execution Memo: 2026-07-06 - Step 2.1: Lazy-load Tesseract, jsPDF, PDF.js worker

Completed: Converted four heavy static imports (tesseract.js, jspdf, pdfjs-dist) to dynamic await import() calls, removing ~1.8MB of eagerly-loaded library code from the entry and page-level chunks.

What was implemented:

  1. New file src/lib/pdfjs-worker.ts — Idempotent ensurePdfWorker() that dynamically imports pdfjs-dist and sets GlobalWorkerOptions.workerSrc exactly once. Worker path confirmed (node_modules/pdfjs-dist/build/pdf.worker.min.mjs).

  2. src/main.tsx — Removed eager import * as pdfjsLib from 'pdfjs-dist' and GlobalWorkerOptions.workerSrc assignment (lines 818). pdfjs worker config is now deferred until someone actually calls downloadPdfAsImages.

  3. src/lib/pdf.tsimport { jsPDF }import type { jsPDF } (type-only, erased at compile time). Added const { jsPDF } = await import('jspdf') as first line of generateQuotePDF.

  4. src/lib/roPdf.ts — Same pattern: type-only import, dynamic import inside generateROPDF.

  5. src/lib/pdf-to-images.tsimport * as pdfjsLibimport type * as pdfjsLib (types only). Added ensurePdfWorker() call and const { getDocument } = await import('pdfjs-dist') inside downloadPdfAsImages. Replaced pdfjsLib.getDocument(...) with getDocument(...).

  6. src/pages/Appointments.tsx — Removed top-level import Tesseract from 'tesseract.js'. Added const { default: Tesseract } = await import('tesseract.js') inside handleExtract before the OCR call.

  7. html2canvas — No-op (not imported anywhere in src/; only exists as a transitive dependency in node_modules).

Bundle impact:

  • pdf.worker.min.mjs (1.2MB) → separate lazy chunk (was bundled in entry)
  • jspdf.es.min.js (399KB) → separate lazy chunk
  • pdf.js (425KB, pdfjs-dist core) → separate lazy chunk
  • tesseract.js → lazy chunk loaded only when OCR is triggered
  • Entry chunk index.js (266KB) excludes all four heavy deps

Verified: npm run build (0 errors, 349ms), npm run lint (0 errors, 18 pre-existing warnings only), npm run test (32/32 pass).

Resolved: v5.2 roadmap priority 2.1 (biggest perf win).

Execution Memo: 2026-07-06 - Step 2.8 + 2.9: Environment variable layer + index.html production polish

Completed: Created production env layer with 5 documented VITE_* vars, gated AI features behind VITE_AI_ENABLED, polished index.html with proper metadata, and added typed ImportMetaEnv declarations.

What was implemented:

  1. .env.production (new) — Five client-safe env vars: VITE_PB_URL=/pb, VITE_APP_NAME=ShopProQuote, VITE_GLITCHTIP_DSN=, VITE_AI_ENABLED=true, VITE_ENV=production. Commented note that DEEPSEEK_API_KEY must never be VITE_-prefixed.

  2. .env.example (updated) — Documents all 5 vars with descriptions. Added explicit server-secret warning at the bottom.

  3. .gitignore (updated) — Added .env.production to prevent accidental commits.

  4. src/lib/pocketbase.ts — Added export const APP_NAME = import.meta.env.VITE_APP_NAME || 'ShopProQuote'.

  5. src/pages/Login.tsx — Imports APP_NAME, replaces hardcoded "ShopProQuote" in footer copyright line.

  6. src/pages/Setup.tsx — Imports APP_NAME, replaces hardcoded "ShopProQuote" in the Primary Workflow hint text.

  7. src/lib/ai.ts — Added const AI_ENABLED = import.meta.env.VITE_AI_ENABLED !== 'false' at module level; short-circuits callDeepSeek when VITE_AI_ENABLED is false. Default is enabled (preserves existing behavior).

  8. index.html — Replaced <title>spq-v2</title> with <title>ShopProQuote — Shop Management</title>. Added <meta name="theme-color" content="#16a34a"> (matches app green accent). Added <meta name="description"> and OG tags (og:title, og:description, og:type).

  9. src/vite-env.d.ts (new) — Extends ImportMetaEnv with typed VITE_PB_URL, VITE_APP_NAME, VITE_GLITCHTIP_DSN, VITE_AI_ENABLED, VITE_ENV to avoid string | undefined friction in TS.

Verified: npm run build (0 errors, 348ms), dist/index.html confirmed to have all new meta tags, npm run lint (0 errors, 18 pre-existing warnings only), npm run test (32/32 pass).

Resolved: v5.2 roadmap priorities 2.8 and 2.9.

Execution Memo: 2026-07-06 - Step 2.5: Zod schemas for write paths

Completed: Installed zod, created 5 schema files with typed write validation, replaced ad-hoc validation in 5 pages with safeParse gates, and added 41 schema-focused tests.

What was implemented:

  1. npm install zod — Added zod v3 as a runtime dependency (~10KB min+gzip, shared chunk).

  2. src/schemas/quote.tsquoteWriteSchema and quoteServiceSchema matching the flat PB write shape (customerName, vehicleInfo, services, discountValue, discountType, subtotal, tax, total, status). All fields typed, required fields get .min(1) with descriptive messages.

  3. src/schemas/repairOrder.tsrepairOrderWriteSchema and roServiceSchema covering the full RO write payload (customerName, customerPhone, vehicleInfo, services, status, total/tax/shopCharges, serviceLocation, travelFee, tripMiles). Services validated as array then JSON.stringify'd.

  4. src/schemas/customer.tscustomerWriteSchema for the CustomerFormData shape (name, phone required; email validated with z.union([z.string().email(), z.literal('')])).

  5. src/schemas/invoice.tsinvoiceWriteSchema + invoiceItemSchema. Includes a .refine() that catches total mismatch: total ≈ subtotal + tax must hold within 0.01 tolerance. Replaces the existing 3-check hand-rolled validate().

  6. src/schemas/settings.tssettingsWriteSchema (27 fields covering all ShopSettings) and accountSettingsWriteSchema for the email/password settings form. Enforces hex color regex, positive labor rate, nonnegative financials, and valid enums.

  7. src/schemas/index.ts — Two helpers: formatZodError (returns first issue message for toasts) and zodErrorsToFormFields (maps issues by path for setFormErrors state).

  8. Page wiring:

    • QuoteGenerator.tsxquoteWriteSchema.safeParse before both handleSave and ensureShareToken saves. Replaced the single-line if (!customerInfo.name) return guard.
    • RepairOrders.tsxrepairOrderWriteSchema.safeParse on incoming form data before any PB write.
    • Customers.tsxcustomerWriteSchema.safeParse in both handleAddCustomer and handleEditCustomer.
    • Invoices.tsx — Replaced the 11-line hand-rolled validate() with invoiceWriteSchema.safeParse + zodErrorsToFormFields. Existing formErrors.* bindings continue working unchanged.
    • Settings.tsxsettingsWriteSchema.safeParse before saveAllSettings, accountSettingsWriteSchema before saveAccountSettings (replaces the 3-line ad-hoc email check).
  9. Tests (5 files, 41 tests):

    • src/schemas/__tests__/quote.test.ts — 7 tests (valid, missing name, missing vehicle, empty services, negative price, negative discount, invalid discount type)
    • src/schemas/__tests__/repairOrder.test.ts — 8 tests (valid, missing name, missing phone, missing vehicle, empty services, empty service name, negative total, invalid status)
    • src/schemas/__tests__/customer.test.ts — 6 tests (valid, valid with email, empty email allowed, missing name, missing phone, invalid email)
    • src/schemas/__tests__/invoice.test.ts — 8 tests (valid, missing name, missing vehicle, empty items, empty item description, zero quantity, total mismatch, invalid status)
    • src/schemas/__tests__/settings.test.ts — 12 tests (valid, missing business name, missing phone, missing advisor, negative rate, zero rate, malformed color, valid color, invalid business type, invalid pdf style, +2 account settings)

Design decisions:

  • Schemas validate the flat PB write shape (matching Record<string, any> payloads), not the nested TS interfaces — no call-site refactoring needed.
  • QuoteApprove.tsx excluded (public customer-facing page; minimal validation is intentional).
  • technicianData.ts assignment writes excluded (already guarded by optimistic concurrency from step 1.6).

Verified: npm run build (0 errors, 356ms), npm run lint (0 errors, 18 pre-existing warnings only), npm run test (73/73 pass — 32 existing + 41 new).

Resolved: v5.2 roadmap priority 2.5.