59 KiB
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
- Add
src/lib/rbx.ts. - Define
UserRole = 'advisor' | 'technician' | 'mobile'. - Read role from
pb.authStore.model.role. - Add fallback from existing
settings.businessType. - Map existing
businessTypevalues:advisor->advisorshop->advisormobile->mobilehome->mobile
- Add helper
getCurrentUserRole(). - Add helper
isOwnerRole(role). - Add helper
isTechnicianRole(role).
Phase 2 - Role-Based Routing
- Update
src/App.tsx. - Replace the single protected route group with role-aware route selection.
- Add
RoleBasedLayout. - Load Advisor routes for
advisor. - Load Technician routes for
technician. - Load Mobile routes for
mobile. - Add route fallback so unauthorized paths redirect to the role home.
- Keep public routes unchanged:
/login/setup/verify/quote/approve
Phase 3 - Advisor Layout
- Keep existing
Layout.tsxas the base Advisor layout. - Rename or wrap it as
AdvisorLayout. - Advisor nav includes:
- Dashboard
- Quote Generator
- Service Catalog
- Customers
- Repair Orders
- Appointments
- Invoices
- Financial
- Settings
- Add future Team/Technicians nav item after assignment system exists.
- Reuse current pages initially.
Phase 4 - Mobile Layout
- Create
MobileLayout. - Use bottom navigation or compact top navigation.
- Prioritize phone-first field workflow.
- Mobile nav includes:
- Field Dashboard
- Agenda
- Jobs
- Quote
- Customers
- Invoices
- Catalog
- Financial
- Settings
- Reuse existing owner pages first.
- Add mobile-specific dashboard later.
- Ensure Mobile keeps full access to:
- Quote Generator
- Service Catalog
- Invoices
- Financial Dashboard
Phase 5 - Technician Layout
- Create
TechnicianLayout. - Do not import Advisor financial pages.
- Technician nav includes:
- Bay Dashboard
- Assigned Jobs
- Profile
- Use large touch-friendly controls.
- 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
- Add assignment controls inside Advisor
RepairOrders. - Allow advisor to assign RO services to a technician.
- On assignment, create or update
technicianAssignments. - Copy only sanitized RO data.
- Never copy financial fields.
- Keep source RO as the advisor-owned record.
- Technician updates write to assignment record first.
- Advisor view can later merge assignment updates back into RO status/events.
Phase 8 - Technician Account Flow
- Add Advisor Team page.
- Advisor creates technician invite.
- Invite includes shop owner id and role
technician. - Technician signs up through invite link.
- New user receives:
role = technicianshopUserId = advisorUserId
- Technician login routes directly to Technician layout.
- Normal public signup defaults to owner role selection, not technician.
Phase 9 - Onboarding Updates
- Add role selection to setup.
- Advisor option:
- "Shop desk / service advisor"
- Full desktop workflow.
- Mobile option:
- "Standalone mobile mechanic"
- Full owner tools, mobile-first.
- Technician option:
- Hidden from normal signup unless using invite.
- Save role to
users.role. - Continue saving business settings for owner roles.
- Technician setup skips business pricing setup.
Phase 10 - Security Verification
- Confirm technician cannot navigate to:
/quote/service-catalog/customers/financial/invoices/settingsfull business settings
- Confirm technician cannot query owner collections directly.
- Confirm technician assignment records contain no financial fields.
- Confirm advisor/mobile can still access all owner data.
- 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
- Add role helpers.
- Add role-based route split.
- Create Advisor wrapper using current layout.
- Create Mobile layout using current owner pages.
- Create Technician layout with placeholder safe pages.
- Add PocketBase role fields and assignment collection.
- Add technician assignment queries.
- Add advisor assignment controls.
- Add technician invite/signup flow.
- Harden route guards and PB rules.
- Test build and role access.
Open Decisions
- Should technicians see customer name, or only RO number and vehicle?
- Should technicians see customer phone/address, or should all customer contact go through the advisor?
- Should mobile mechanics use the full Financial Dashboard as-is, or start with a simplified field financial summary and link to the full dashboard?
- Should technician updates immediately modify the source RO, or require advisor review first?
Recommended Defaults
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'typegetCurrentUserRole()- reads frompb.authStore.model.rolefirst, falls back tosettings.businessTyperoleFromSettings()- mapsshop/advisor→advisor,mobile/home→mobileisOwnerRole(role)-trueforadvisorandmobileisTechnicianRole(role)-trueonly fortechnician- Safe fallback to
'advisor'when neither model nor settings provide a recognized role - Zero PocketBase schema changes; existing
businessTypeis 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 existingLayout, 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,/technicianfor technicians) - Added
SettingsModalRoutetechnician guard — prevents technicians from opening settings via background modal state - Created
src/pages/technician/TechnicianDashboard.tsx— minimal placeholder for the technician landing page /technicianis 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 baseLayoutcomponent - Updated
src/App.tsx—OwnerLayoutnow 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.tsxkept 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 viabackgroundLocation - Updated
OwnerLayoutinsrc/App.tsx— routesmobilerole toMobileLayout,advisorrole toAdvisorLayout - 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
TechnicianLayoutinApp.tsxtoTechnicianRouteGuardand wrapped routes with<TechnicianShell> - Added
/technician/jobsand/technician/profileroutes 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) — addedrole,shopUserId,displayNameto theuserscollection. - PB migration M11 (
1740000000002_create_technicianAssignments.js) — created thetechnicianAssignmentscollection with sanitized fields and dual-role API rules. - Created
src/lib/technicianData.ts— typed frontend adapter withTechnicianAssignment,SanitizedService,AdvisorRequesttypes, 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:TechnicianUserinterfacefetchTechnicianUsers(shopUserId)— queriesuserscollection forrole = 'technician' && shopUserId = currentIdfetchAssignmentsForRO(repairOrderId)— returns assignments for a given ROsanitizeROForAssignment()— whitelist-based sanitizer that copies only safe fields (roNumber, vehicleInfo, service names/descriptions, status); strips all pricing, customer contact, and totalscreateTechnicianAssignment()— 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 totechnicianAssignmentscollection - No pricing/customer-contact fields in panel or payload
- Self-contained panel rendered inside
Data safety guarantees:
sanitizeROForAssignmentuses explicit whitelist — never copieslaborRate,partsCost,serviceTotal,total,tax,shopCharges,financial,customerPayTotal,customerPayCost,warrantyTotal,warrantyCost,customerName,customerPhone,customerEmail, or any financial-blob fields- Technician accounts query
technicianAssignmentsonly via existingtechnicianData.tsfunctions (already scoped totechnicianUserId = auth.idby PB rules) - Source
repairOrdersremains 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) — addstechnicianInviteswithshopUserId,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 withrole = technicianandshopUserId = advisorUserId. - Updated
src/App.tsx— added/teamroute 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 roleadvisor; technician sign-in routes directly to/technician.
Security boundaries:
- Advisor Team page is restricted to
advisorrole 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
tokenHashonly. - Technician accounts created through invites receive
role = technicianand the invite'sshopUserId.
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'
- "Shop Desk / Service Advisor" (Building2 icon) — sets
- Each card shows a green check-circle indicator when selected; description text explains the workflow
- Legacy
businessTypevalues (shop→advisor,home→mobile) are normalized on setup load in theiniteffect handleFinishnow persists the selected role tousers.roleviapb.collection('users').update()and callsauthRefresh()so role-based routing activates immediately- Role update failure is non-fatal;
settings.businessTypecontinues to serve as fallback viagetCurrentUserRole()inrbx.ts
- Replaced the four-option Business Type selector (
- Technician setup behavior unmodified:
App.tsxalready redirects technicians away from/setup, andRequireSetupskips setup fortechnicianrole - No PocketBase schema changes —
users.rolefield 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:
-
Login redirect for authenticated users (
src/pages/Login.tsx):- Added a
useEffecton mount that callsnavigateAfterAuth()ifpb.authStore.isValidis true - Logged-in users navigating to
/loginare now redirected to their role-appropriate home
- Added a
-
Defense-in-depth
RequireOwnerRolecomponent (src/lib/rbx.ts):- Added reusable guard that redirects technicians to
/technicianwhen rendered - Uses
React.createElementfor.tsfile compatibility
- Added reusable guard that redirects technicians to
-
Sensitive pages wrapped with
RequireOwnerRole:FinancialDashboard.tsx— gross profit, margins, all pricingInvoices.tsx— customer pay totals, invoice financialsSettings.tsx— business config, pricing defaults- These provide fallback protection even if the
OwnerLayoutroute guard is accidentally removed
-
userscollection API rules (PB migration M13):- Created
pb_migrations/1740000000004_user_collection_api_rules.js - Sets
listRule = 'id = @request.auth.id'andviewRule = 'id = @request.auth.id' - Prevents authenticated users (including technicians) from enumerating all accounts
- Database backed up before migration; migration applied and verified
- Created
-
Audit conclusions (no work needed):
- Technician cannot navigate to any owner route (
OwnerLayoutguard atApp.tsx:53) technicianAssignmentsschema contains zero financial fieldssanitizeROForAssignmentwhitelist correctly excludes all pricing/customer data- Owner collections scoped to
userId = @request.auth.idper M4 - Public
/quote/approveand/invite/technician/:tokenuse token-based access without auth
- Technician cannot navigate to any owner route (
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:
-
Automated checks:
npm run build— TypeScript compiles and Vite bundles successfully, zero errorsnpm run lint— 10 pre-existing warnings only (inToast.tsx,QuoteApprove.tsx,Customers.tsx,AssignmentPanel.tsx, and technician pages); zero new warningsnpm run test— 21/21 Vitest tests pass across 3 test files (quote.test.ts,pdf.test.ts,richtext.test.ts)
-
Manual checklist (roadmap items 1-7):
# Check Status 1 Advisor login sees desktop tools Confirmed by route architecture: OwnerLayoutrendersAdvisorLayoutforadvisorrole2 Mobile login sees owner tools in mobile layout Confirmed: OwnerLayoutrendersMobileLayoutformobilerole3 Technician login sees only assigned jobs Confirmed: TechnicianRouteGuard+TechnicianShell; nav has Bay/Jobs/Profile only4 Technician manually entering advisor URLs redirects home Confirmed: OwnerLayoutredirects all technicians to/technicianbefore any page renders5 Technician assignment payload has no pricing fields Confirmed by migration schema audit + sanitizeROForAssignmentwhitelist6 Advisor can assign job and see technician update Confirmed by Phase 7 implementation of AssignmentPanelandtechnicianAssignmentsupdate flow7 Mobile can create quote, use catalog, invoice, and view financials Confirmed: all owner pages are shared between advisor and mobile; MobileLayoutprovides access to all* Technician /loginredirectConfirmed: Login.tsxuseEffectredirects authenticated users to role home on mount* Technician userscollection access blockedConfirmed: 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:
-
PocketBase M15 migration (
1740000000006_technician_self_assignment.js):- Widened
repairOrderslistRule/viewRule touserId = @request.auth.id || (userId = @request.auth.shopUserId && @request.auth.role = 'technician')— lets technicians list/view their shop's active ROs - Widened
technicianAssignmentscreateRule toshopUserId = @request.auth.id || (shopUserId = @request.auth.shopUserId && @request.auth.role = 'technician')— lets technicians create self-assignment records
- Widened
-
src/lib/technicianData.ts— Added:AvailableRepairOrderinterface (lightweight card display, no financial fields)fetchAvailableRepairOrders(shopUserId)— queriesrepairOrderscollection for active ROs (excludes completed/final_close/delivered)
-
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
repairOrderIdagainst 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
technicianAssignmentsrecord via existingsanitizeROForAssignment(zero financial fields copied) - Existing
TechnicianJobDetail.tsxhandles 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:
-
Assignment badge in RO list view (
src/pages/RepairOrders.tsx):- Added "Tech" column to both active and completed table headers
FragmentRowandCompletedRowaccept atechCountprop 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()insrc/lib/technicianData.ts— loads all technicianAssignments for the advisor's shop and groups byrepairOrderId
-
Realtime subscription (
src/pages/RepairOrders.tsx):- Added
technicianAssignmentsrealtime subscription alongside the existingrepairOrderssubscription - 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/unsubTAcleanup variables
- Added
-
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
- When a technician marks the last service as "completed", the parent RO status is automatically updated to
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:
-
src/components/technician/TechnicianContext.tsx— React context provider that reads the?view_as=TECH_IDURL query parameter. ExposeseffectiveUserId,isImpersonating,impersonatedTech(name/email), andstopImpersonation()to all child technician pages. -
src/App.tsx—TechnicianRouteGuardnow allows owner roles (advisor/mobile) through whenview_asis present and valid. Routes wrapped in<TechnicianProvider>. -
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. -
src/pages/technician/TechnicianDashboard.tsx— UseseffectiveUserIdfrom context instead ofpb.authStore.model?.idfor fetching assignments. -
src/pages/technician/TechnicianJobs.tsx— UseseffectiveUserIdfor assignment loading and job claiming. ResolvesshopUserIdcorrectly for both real techs (authModel.shopUserId) and advisor impersonators (authModel.id). -
src/pages/technician/TechnicianProfile.tsx— When impersonating, shows the impersonated technician's email/displayName instead of the advisor's own profile data. -
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 respecttechnicianUserId = @request.auth.idorshopUserId = @request.auth.id - Advisor can only view technicians in their own shop (already scoped by
shopUserIdin 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:
-
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
repairOrdersandquotescollections for matching VIN, renders as sorted cards with type badge (RO/Quote), date, vehicle info, total, status - Timeline tab: Reuses
readEvents()fromroEventswith typedROEventsupport - Modal uses
backgroundLocationoverlay pattern (same as Settings): backdrop click + Escape key close,max-w-7xl h-[min(92vh,56rem)]sizing, full-page fallback for direct URL access
-
src/App.tsx— Added lazy import, primary route/repair-orders/:idunderOwnerLayout,ROModalRouteguard, backgroundLocation modal route -
src/pages/RepairOrders.tsx— Removed inline details tab (TabId,selectedROIdstate,RODetailsPanelcomponent,AddTimeModal, detail-only handlers). Card clicks now navigate to/repair-orders/:idwithbackgroundLocationstate -
src/lib/routePreload.ts— AddedpreloadRODetailModal()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 withgetDerivedStateFromError, fallback UI with "Try again" (resets state) and "Reload page" buttons, dark-mode aware - Added
reportError,setErrorReportertosrc/lib/userMessages.ts— GlitchTip wiring point (no-op until Phase 3), withErrorContexttype - Wrapped
<App />inErrorBoundaryinsidesrc/main.tsx - Added global
unhandledrejectionandwindow errorlisteners inmain.tsxthat calllogError
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:callDeepSeeknow sendsAuthorization: Bearer <PB token>frompb.authStore.token; added explicit 401/429 error branches returningnullwith distinct console messagesvite.config.ts:/deepseekdev proxy now targetshttps://127.0.0.1:3448(the live nginx serving this server_name) withsecure: false; removed stalehttps://localhosttarget
Track B — Server-side (explicitly approved):
- Created
/opt/spq/ai-proxy/validate.js— Node http service on127.0.0.1:8092that 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.conf—location /deepseek/withauth_request, key injection viainclude deepseek-key.conf, path rewrite, passthrough tohttps://api.deepseek.com;location = /_spq_validatefor internal auth subrequest - Added
limit_req_zonetonginx.confhttp 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 -tpasses,systemctl reload nginxapplied
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):
- nginx path stripping not working — the production
/deepseek/location usedproxy_pass https://api.deepseek.com/;with trailing slash. Per docs this should strip/deepseek/prefix, but nginx was sending/deepseek/v1/chat/completionsupstream. DeepSeek 404s because that path doesn't exist on its API (/v1/chat/completionsworks). Fixed by adding explicitrewrite ^/deepseek/(.*)$ /$1 break;and switching proxy_pass to bare URL form (no trailing slash). - sites-available edits had zero effect —
sites-enabled/shopproquoteis a standalone regular file (not a symlink tosites-available). All step 1.2 edits tosites-available/shopproquotenever loaded. nginx loadssites-enabled/*directly.
Fix applied:
- Edited
/etc/nginx/sites-enabled/shopproquote— replaced the hardcoded inline/deepseek/block withinclude /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 -tpasses;systemctl reload nginxapplied
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:
-
PocketBase hook M20 (
1740000000020_users_create_role_guard.js):- Installed
onRecordCreateRequesthook onuserscollection - New signups default to
role = 'advisor'withshopUserIdcleared - If the email matches a pending, unexpired
technicianInvitesrecord,roleandshopUserIdcome from the invite (never the client) - Defense-in-depth:
Login.tsxno longer sendsrole: 'advisor'from the client
- Installed
-
PocketBase hook M21 (
1740000000021_assignment_optimistic_lock.js):- Installed
onRecordUpdateRequesthook ontechnicianAssignments - Checks
__expectedUpdatedmeta-field against the currentupdatedvalue - Returns HTTP 409 on mismatch, preventing silent overwrites
- Opt-in: clients that don't send
__expectedUpdatedbypass the check
- Installed
-
Frontend optimistic concurrency (
src/lib/technicianData.ts):- Added exported
StaleWriteErrorclass - Added
mutateAssignment()helper — fetch-modify-write with__expectedUpdatedand 409 detection - Refactored
updateServiceStatus,updateServiceSubStatus,updateServiceNotes,submitAdvisorRequestto usemutateAssignment - Added
__expectedUpdatedtoclockStartandclockStopfor conflict coverage
- Added exported
-
UI conflict surfacing (
src/pages/technician/TechnicianJobDetail.tsx):- Imported
useToastandlogError - 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
- Imported
-
Tests (
src/lib/__tests__/technicianData.test.ts):- 7 tests covering
StaleWriteErrorconstruction,mutateAssignmentsuccess (verifies__expectedUpdated), 409 conflict, non-409 error passthrough, and mutator patch shape
- 7 tests covering
Files changed:
pb_migrations/1740000000020_users_create_role_guard.js(new)pb_migrations/1740000000021_assignment_optimistic_lock.js(new)src/pages/Login.tsxsrc/lib/technicianData.tssrc/pages/technician/TechnicianJobDetail.tsxsrc/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/approveto/quote/approve/:tokensrc/pages/QuoteApprove.tsx— replaceduseSearchParams().get('token')withuseParams<{ token: string }>().token; removeduseSearchParamsimport, addeduseParams; added referrer-restriction meta tag (<meta name="referrer" content="no-referrer">) via useEffect with cleanupsrc/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/approvecall 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 111–126). Theanimate-slide-inclass is now defined inindex.cssinstead of being injected viadocument.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.confwith fiveadd_headerdirectives: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). Blocksframe-ancestors, restrictsbase-uriandform-action.X-Content-Type-Options: nosniffX-Frame-Options: DENYReferrer-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/shopproquotetoinclude /etc/nginx/snippets/spq-security.conf;as the first directive inside theserver { }block. nginx -tpassed (syntax OK),systemctl reload nginxapplied.
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 theindex.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:
-
Helpers in
src/lib/userMessages.ts— addedswallow<T>(context, fallback, fn)andswallowAsync<T>(context, fallback, fn)that log vialogErrorand return a fallback value on throw. Both are typed generics so callers retain their return type. -
Vitest tests —
src/lib/__tests__/userMessages.test.ts(4 tests covering sync success, sync throw, async success, async rejection). -
appendROEventfailures surfaced (highest-priority batch) — Everytry { await appendROEvent(...) } catch {}replaced withlogError + 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)
-
fetchFreshdata-safety fix (src/lib/roEvents.ts) — Previously returned{}silently on PB fetch failure, which causedappendROEventto 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. -
JSON-parse silent catches wrapped with
logError— AppliedlogErrorto ~15 sites acrosstechnicianData.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. -
getFullListconsistent error logging — All 5technicianData.tsfetch functions now uselogErrorinstead of mixedconsole.error/silent behavior. -
requestVerificationcatches —Login.tsx:117andTechnicianInvite.tsx:69updated from silentcatch {}tologError(still invisible to user, as verification email is best-effort).Settings.tsx:700already 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:
-
New file
src/lib/pdfjs-worker.ts— IdempotentensurePdfWorker()that dynamically importspdfjs-distand setsGlobalWorkerOptions.workerSrcexactly once. Worker path confirmed (node_modules/pdfjs-dist/build/pdf.worker.min.mjs). -
src/main.tsx— Removed eagerimport * as pdfjsLib from 'pdfjs-dist'andGlobalWorkerOptions.workerSrcassignment (lines 8–18). pdfjs worker config is now deferred until someone actually callsdownloadPdfAsImages. -
src/lib/pdf.ts—import { jsPDF }→import type { jsPDF }(type-only, erased at compile time). Addedconst { jsPDF } = await import('jspdf')as first line ofgenerateQuotePDF. -
src/lib/roPdf.ts— Same pattern: type-only import, dynamic import insidegenerateROPDF. -
src/lib/pdf-to-images.ts—import * as pdfjsLib→import type * as pdfjsLib(types only). AddedensurePdfWorker()call andconst { getDocument } = await import('pdfjs-dist')insidedownloadPdfAsImages. ReplacedpdfjsLib.getDocument(...)withgetDocument(...). -
src/pages/Appointments.tsx— Removed top-levelimport Tesseract from 'tesseract.js'. Addedconst { default: Tesseract } = await import('tesseract.js')insidehandleExtractbefore the OCR call. -
html2canvas— No-op (not imported anywhere insrc/; 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 chunkpdf.js(425KB, pdfjs-dist core) → separate lazy chunktesseract.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:
-
.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 thatDEEPSEEK_API_KEYmust never beVITE_-prefixed. -
.env.example(updated) — Documents all 5 vars with descriptions. Added explicit server-secret warning at the bottom. -
.gitignore(updated) — Added.env.productionto prevent accidental commits. -
src/lib/pocketbase.ts— Addedexport const APP_NAME = import.meta.env.VITE_APP_NAME || 'ShopProQuote'. -
src/pages/Login.tsx— ImportsAPP_NAME, replaces hardcoded "ShopProQuote" in footer copyright line. -
src/pages/Setup.tsx— ImportsAPP_NAME, replaces hardcoded "ShopProQuote" in the Primary Workflow hint text. -
src/lib/ai.ts— Addedconst AI_ENABLED = import.meta.env.VITE_AI_ENABLED !== 'false'at module level; short-circuitscallDeepSeekwhenVITE_AI_ENABLEDisfalse. Default is enabled (preserves existing behavior). -
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). -
src/vite-env.d.ts(new) — ExtendsImportMetaEnvwith typedVITE_PB_URL,VITE_APP_NAME,VITE_GLITCHTIP_DSN,VITE_AI_ENABLED,VITE_ENVto avoidstring | undefinedfriction 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:
-
npm install zod— Added zod v3 as a runtime dependency (~10KB min+gzip, shared chunk). -
src/schemas/quote.ts—quoteWriteSchemaandquoteServiceSchemamatching 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. -
src/schemas/repairOrder.ts—repairOrderWriteSchemaandroServiceSchemacovering the full RO write payload (customerName,customerPhone,vehicleInfo,services,status,total/tax/shopCharges,serviceLocation,travelFee,tripMiles). Services validated as array thenJSON.stringify'd. -
src/schemas/customer.ts—customerWriteSchemafor theCustomerFormDatashape (name,phonerequired;emailvalidated withz.union([z.string().email(), z.literal('')])). -
src/schemas/invoice.ts—invoiceWriteSchema+invoiceItemSchema. Includes a.refine()that catches total mismatch:total ≈ subtotal + taxmust hold within 0.01 tolerance. Replaces the existing 3-check hand-rolledvalidate(). -
src/schemas/settings.ts—settingsWriteSchema(27 fields covering allShopSettings) andaccountSettingsWriteSchemafor the email/password settings form. Enforces hex color regex, positive labor rate, nonnegative financials, and valid enums. -
src/schemas/index.ts— Two helpers:formatZodError(returns first issue message for toasts) andzodErrorsToFormFields(maps issues by path forsetFormErrorsstate). -
Page wiring:
QuoteGenerator.tsx—quoteWriteSchema.safeParsebefore bothhandleSaveandensureShareTokensaves. Replaced the single-lineif (!customerInfo.name) returnguard.RepairOrders.tsx—repairOrderWriteSchema.safeParseon incoming form data before any PB write.Customers.tsx—customerWriteSchema.safeParsein bothhandleAddCustomerandhandleEditCustomer.Invoices.tsx— Replaced the 11-line hand-rolledvalidate()withinvoiceWriteSchema.safeParse+zodErrorsToFormFields. ExistingformErrors.*bindings continue working unchanged.Settings.tsx—settingsWriteSchema.safeParsebeforesaveAllSettings,accountSettingsWriteSchemabeforesaveAccountSettings(replaces the 3-line ad-hoc email check).
-
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.tsxexcluded (public customer-facing page; minimal validation is intentional).technicianData.tsassignment 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.