Skip to content
- JWT login/logout with session persistence
- Role-based access (admin, member, viewer)
- Admin account creation (DevTools, gated by
ALLOW_REGISTRATION env var)
- Team member invite system — coming soon
- Service-unavailable UX when auth server is unreachable
- Time-of-day greeting with user’s first name (morning/afternoon/evening icons)
- Date display and contextual summary line (callbacks, new leads, expiring estimates)
- Stat cards with colored icon badges (leads, customers, projects, estimates)
- “Needs Attention” widget (stale leads, upcoming callbacks)
- Recent Activity feed
- “Continue where you left off” section (last 4 viewed leads/customers/projects via localStorage)
- Callback reminders with “Mark Complete” action
- Leads table with sortable columns, pagination, search
- Pipeline status column with plant emoji header, sortable
- Health status dot merged into name column (green/amber/red)
- Temperature dot indicator
- Submission count badge (multi-submit detection)
- Filter bar with typed syntax (
lead_status:pending, temperature:hot, health:stale)
- Pipeline summary bar with status counts and “Needs Attention” filter
- Column visibility toggling (show/hide optional columns)
- Lead detail page with sticky header, status dropdown, action buttons
- Lead scoring with signal breakdown pills
- Lead-to-customer conversion with loading toast and redirect to new customer profile
- Reject/archive lead with confirmation modal
- Customers table with sortable columns, pagination, search
- Customer detail page with stat cards (active projects, total value, customer since)
- Contact info and property info cards
- Converted-from-lead link back to original lead record
- Collapsible “Notes & Activity” card with NoteEditor and activity timeline
- Rich NoteEditor with markdown support (powered by
marked library)
- Side-by-side markdown preview (“Preview Markdown” button splits editor in half)
- Note templates (Site Visit Summary, Phone Call Follow-up, Estimate Discussion, Issue Report)
- Image upload via button, paste, or drag-and-drop
- Image resize before upload (client-side)
- Auto-save drafts to localStorage with 500ms debounce
- Draft persistence across SPA navigation (flush on unmount)
- Draft restored indicator
- beforeunload protection (warns on unsaved content)
- Activity timeline with type icons and relative timestamps
- Toast notifications on note save (success/error)
- Global
ToastProvider with useToast() hook
- 5 variants: success, error, warning, info, loading
- Auto-dismiss (configurable duration, default 5s) or persistent (manual dismiss)
- In-place update (e.g. loading spinner transitions to success)
- Bottom-right positioning with slide-in animation
- Dismiss button on persistent and error toasts
- “Open in Google Maps” link (uses address or lat/lng fallback)
- More coming soon
- Search dropdown from header (top 5 results per category)
- Full search page at /search with grouped results (leads, customers, projects)
- Server-side search across contact names, emails, phones, addresses
- Full quote/estimate CRUD with inline line item editing
- Catalog-based item selection (ItemSelector dropdown) or custom items
- Display IDs:
QT-{seq}-{rev} format (e.g. QT-1-A, QT-1-B for revisions)
- Revision system: revising a sent/accepted quote creates a new revision, supersedes original
- Clone: independent copy with new identity (not a revision)
- Status lifecycle: draft → sent → viewed → accepted → expired → rejected → converted → superseded → archived
- Tax rate with per-quote override (tenant default, inline “Override” button)
- Expiration dates with no-past-date validation and TTL from tenant config
- Auto-expire on list view (transitions viewed quotes past expiration to expired)
- Expiration reminder scheduling (stubbed email, ready for provider integration)
- Quote preview modal (Eye button) — renders HTML in sandboxed iframe, works for saved and unsaved quotes
- Share link generation with configurable visible contact fields (name, email, phone, property)
- Public unauthenticated quote view at
/quote/:token with optional PDF download button
- PDF generation (browser print API with HTML fallback)
- Convert-to-project flow (creates project + tasks from line items)
- Renew expired quotes with new expiration date
- Archive/unarchive with Active/Archived pill toggle (glowing blue border on active pill)
- Duplicate-to-builder from archived quotes (pre-fills title, tax settings)
- File attachments per quote via R2 storage (drag-and-drop upload, file type/size hints)
- Accepted file types: PDF, JPG, PNG, WEBP, DOCX, XLSX — Max 10MB
- Public attachment download via share token
- Undo/Redo (10-step history) with keyboard-style Undo2/Redo2 buttons
- Clear confirmation modal (danger variant)
- Save as Draft (keeps builder open) vs Save Quote (clears builder)
- Edit collision check (warns if builder has unsaved work)
- Locked banner for non-draft quotes with “Create Revision” shortcut
- Activity logging on save (estimate_draft type)
- Theme toggle (light/dark mode)
- Sidebar hover-expand toggle (default: on, persisted to localStorage)
- Company/office info editing (name, phone, email, address)
- Office address geocoding on save
- Quotes feature toggle (enables catalog management)
- Contractor token TTL configuration (1-24 hours)
- Default tax rate configuration
- Force password change toggle (per-tenant)
- Generate fake leads (10/50/100) with realistic scoring distribution
- Generate fake customers (10/50/100) via intake pipeline (creates leads, then converts)
- Soft-delete and hard-delete all leads/customers
- System info display (DB size, record counts, environment, tenant ID)
- Health checks (API, Portal, Intake Form) with lucide icons and polling
- Create user account (gated by ALLOW_REGISTRATION deployment toggle)
- Reset user password with optional force-password-change flag
- User list with lock icon for forced password change users
- BrowserStorageAdapter for dev/preview (IndexedDB, blob URLs, fully offline)
- ApiStorageAdapter for production (proxied to /api/v1/uploads for R2/S3)
- Auto-selects adapter by environment, overridable via
VITE_STORAGE_ADAPTER
- Toast notifications: uploading spinner, success, file-too-large, unsupported type, errors
- File extension and size validation (10MB limit, images only)
- Collapsible sidebar with hover-expand (smooth CSS transition)
- Sidebar stays in document flow (no overlay), main content reflows
- Breadcrumb navigation on detail pages
- Recently viewed tracking (leads, customers, projects) with cross-tab sync
- i18n system with English string table (
packages/shared/src/i18n/en.ts)
- Template interpolation (
{{name}}, {{count}})
- All user-facing strings use translation keys
- Multi-step intake form (mobile-first PWA)
- Project type selection (roofing, siding, gutters, flooring, etc.)
- Dynamic question sets per project type
- Contact info collection (name, email, phone, address)
- Urgency selection
- Material preferences
- Form validation with Zod schemas
- Email notification on submission
- CORS-enabled health endpoint
- Contractor auth via search (/contractor routes)
- Multi-step inspection flow with question sets per project type
- Multi-photo capture per question (MultiPhotoUpload component)
- Configurable minimum photo count per question
- Client-side image resize before upload
- Offline support via Service Worker (sw.js)
- IndexedDB persistence for offline data
- SyncStatus badge showing online/offline/syncing state
- Queue drain logic on reconnect (retry with exponential backoff)
- inspectionId passthrough across all steps
- Leads: list, get, create, update, delete, convert-to-customer
- Customers: list, get, create, update
- Projects: list, get, create, update
- Estimates: list, get, create, update, duplicate (revise), clone, archive/unarchive, renew, convert-to-project
- Estimate attachments: upload, list, delete, download (authenticated + public via share token)
- Estimate share: generate share token with configurable visible fields, public HTML view
- Estimate preview: render HTML from builder data without persisting
- Estimate PDF: HTML rendering with optional browser print
- Activities: list, recent, create
- Catalog: items CRUD, unit types CRUD (feature-gated)
- Register (creates admin + tenant)
- Login (returns JWT), with force-password-change flow
- Change password endpoint (purpose-scoped JWT)
- Current user info (GET /api/v1/auth/me)
- Contractor token generation (configurable TTL, purpose claim)
- Invite team member — coming soon
- Accept invite — coming soon
- Password hashing via Web Crypto API (PBKDF2-SHA256, CF Workers compatible)
- Stats aggregation (counts, pipeline breakdown)
- “Needs Attention” query (stale leads, upcoming callbacks)
- Computed health_status from last_contacted_at
- Cross-entity search (leads, customers, projects)
- POST /v1/geocode — on-demand Nominatim geocoding
- Auto-geocode on lead/customer create and update
- Distance calculation from office
- Generate fake leads/customers with realistic scoring (customers via intake pipeline)
- Soft-delete and hard-delete leads/customers
- System info (DB size via PRAGMA, record counts)
- Registration status check
- Admin account creation (deployment-gated)
- User management: list users, reset password, toggle force-password-change
- Dev error dashboard at API root (HTML, last 50 errors, 60s auto-refresh off by default)
- POST /api/v1/uploads — file upload with validation
- Estimate attachments: upload (multipart/form-data), list, delete, download
- Public attachment download via share token with expiry check
- R2 storage backend (Cloudflare R2)
- POST /api/v1/webhooks/intake — intake form submission webhook
- GET /api/v1/health — health check endpoint
- Centralized URI prefixes via
SERVICE_PREFIXES and apiPath() builder in packages/shared
- Tenant-scoped data isolation (all queries filtered by tenant_id)
- Drizzle ORM with Cloudflare D1
- PRAGMA try/catch for remote D1 compatibility
- Tenant_id indexes on all tables for efficient count queries
- Zod schemas for all API inputs (leads, customers, projects, estimates, auth, attachments)
- TypeScript types and interfaces
- Lead scoring engine (30+ weighted rules, temperature classification)
- Phone number normalization (E.164 via
normalizePhone())
- Phone display formatting
- Display value formatter (snake_case to Title Case)
- i18n string table with interpolation
- Query key constants for React Query
- Drizzle ORM schema definitions
- Constants/enums (lead statuses, temperatures, health statuses, user roles, estimate statuses)
- Centralized URI prefix constants (
SERVICE_PREFIXES) and path builders (apiPath(), portalPath(), intakePath())
- Attachment constants (accepted types, max size, MIME type map)
ci-api-deploy.yml — API preview + production deploy via Wrangler
ci-portal-deploy.yml — Portal preview + production deploy via CF Pages
ci-intake-deploy.yml — Intake form deploy
deploy-stack.yml — Manual full-stack deploy
test-suite.yml — Manual test runner
- Preview deploys on dev branch push
- Post-deploy login smoke tests with CI service account
- Auth smoke test (warning, not blocking)
- Health endpoint verification
- E2E regression suite (17 tests): login, all pages load, no broken images, no failed requests, sidebar reflow, markdown preview, toast notifications, draft persistence
- E2E DevTools tests (generate/delete data)
- E2E contractor inspection flow tests
- Unit tests (resize-image, idb-storage)
- API proxy via CF Pages functions (
/api/v1/** catch-all)
- Cloudflare D1 (SQLite)
- 11 migrations (init, geocoding, indexes, inspection drafts, force password change, quote builder, estimates status, expiration reminders, attachments, archived status)
- Dev seed script with test account (suadmin@dev.local, password via
DEFAULT_SUADMIN_PASSWORD env var) and 1200+ deterministic short IDs
- Geocode backfill script