Last updated: 2026-03-31 Current phase: Phase 2 — Code Audit
- v0.0.12 Rust Engine Core
- turn multi-quote parity checks into repeatable fixture coverage or CI gates
- verify Node fallback and shadow-mode diagnostics against
/api/availability - decide whether the
SKIP_RUST_RELEASE_GUARD=1escape hatch should remain available for local emergency packaging
- Rust pricing engine
- add quote totals parity endpoint for legacy vs Rust pricing
- validate item discounts, logistics subtotal split, quote adjustments, tax, and total output
- route live quote totals through Rust behind
USE_RUST_PRICINGwith shadow-mode diagnostics - keep Rust scope explicitly limited to pricing + inventory until both are stable
- Full codebase audit (assigned to Codex — see CODE_AUDIT_PLAN.md)
- Quote assistant follow-through
- add assistant-specific settings UI (
ai_agent_provider,ai_agent_model,ai_agent_enabled) - extend tool registry with quote availability, fulfillment readiness, and quote message digest
- evaluate whether assistant transcript retention needs export/delete controls
- add assistant-specific settings UI (
- Pricing engine
- taxes, discounts, adjustments, delivery split, quote totals parity
- Fulfillment reconciliation engine
- checkout/checkin math, missing items, mismatch detection
- Logistics planning engine
- truck loads, stop sequencing, crew assignment constraints
- Forecasting / utilization engine
- shortages, utilization trends, restock pressure
- Recommendation scoring engine
- package completeness, related products, substitute scoring
- Rust engine core foundation
- Added
rust-core/Cargo workspace with Axum API, inventory-engine, shared-types, db, config, telemetry, and events crates - Added
POST /engine/inventory/checkplus health, readiness, and metrics endpoints - Added Node feature-flag integration via
USE_RUST_INVENTORYandRUST_INVENTORY_SHADOW_MODE - Added AI coordination files for
v0.0.12 Rust Engine Core
- Added
- Rust conflicts path
- Added Rust-side
conflictsaction and wiredGET /api/availability/conflictsbehind the same feature flags - Added server-side Rust engine diagnostics endpoint at
/api/admin/diagnostics/rust-engine - Improved Rust reservation handling for unsigned quote changes to better match legacy behavior
- Added Rust-side
- Rust parity tooling
- Added
server/services/rustInventoryParityService.jsfor legacy-vs-Rust quote comparison - Added
GET /api/admin/diagnostics/rust-engine/compare/:quoteId - Added CLI parity command:
node server/cli.js rust-compare-quote --quote-id <id>
- Added
- Rust batch parity tooling
- Added
compareQuotes()support for checking multiple live quotes in one run - Added
GET /api/admin/diagnostics/rust-engine/compare?limit=... - Added CLI batch command:
node server/cli.js rust-compare-batch --limit 10
- Added
- Rust item-level parity controls
- Added auto item-id selection per quote for parity runs via
includeItems - Added
itemLimitPerQuoteto keep batch compare runs bounded - Added compact mismatch summaries so compare output highlights changed item ids instead of only dumping full payloads
- Added auto item-id selection per quote for parity runs via
- Rust parity report artifact
- Added
rust-parity-reportCLI flow to writeAI/reports/rust-parity-latest.md - Added root script
npm run rust:parity-reportwith a standard batch preset - Added JSON companion artifact
AI/reports/rust-parity-latest.json
- Added
- Rust operator/admin visibility
- Added Rust engine status and parity snapshot cards to the Admin System tab
- Added combined local startup scripts for Node + Rust + client via
npm run dev:stack
- Rust release guard
- Added
npm run check:rust:releaseto start Rust if needed and run the standard parity report - Wired the guard into the root
releasescript so release cuts fail when parity fails
- Added
- Rust package guard
- Moved the parity guard onto the root
packagescript so packaging and release share the same gate - Added
SKIP_RUST_RELEASE_GUARD=1escape hatch for intentional local override
- Moved the parity guard onto the root
- Rust release traceability
- Parity report now includes app version and execution context
postpackagenow copies the latest Rust parity markdown and JSON artifacts intodist/release-checks/postpackagenow writesdist/release-checks/manifest.jsonsummarizing version, parity state, and packaged artifacts
- Quote assistant foundation
- Added persistent quote-scoped assistant transcripts via
quote_agent_messages - Added read-only quote assistant API under
/api/ai/quotes/:id/assistant - Added provider abstraction with OpenAI + deterministic fallback
- Added Quote Detail
Assistanttab in the client - Added product-native tools for quote overview, financials, inventory pressure, activity digest, item recommendations, and client follow-up drafting
- See
AI/AGENT_ASSISTANT_PLAN.md
- Added persistent quote-scoped assistant transcripts via
- Upload compression controls
- Added settings toggles for image compression and auto-WebP conversion
- Skip compression for files under 200 KB so small uploads are not bloated
- Reduced encoder effort to speed up upload-time processing
-
Audit
server/lib/authMiddleware.js- Findings / fixes applied (additive):
- Enforced JWT algorithm allowlist (
HS256) during verification. - Added bearer token parsing + length guardrail (reject huge tokens early).
- Ensured verified payload is an object before accepting.
- Added compatibility: set
req.user.id = req.user.subwhen missing (several routes usedreq.user.id).
- Enforced JWT algorithm allowlist (
- Follow-ups:
- Consider adding
aud/issvalidation once deployment audience is known. - Consider server-side user revocation checks (DB lookup) for sensitive routes (trade-off: perf).
- Consider adding
- Files:
server/lib/authMiddleware.js,server/routes/files.js - Priority: High
- Findings / fixes applied (additive):
-
Audit public routes in
server/index.js(lines 130–270)- Verify no-auth routes are safe, CORS config is correct
- Files:
server/index.js - Priority: High
- Notes / fixes applied:
- Added lightweight in-memory rate limiting for sensitive public-token flows:
GET /api/quotes/public/:tokenPOST /api/quotes/approve-by-tokenGET/POST /api/quotes/public/:token/messagesPOST /api/quotes/contract/signGET /api/files/:id/serve(abuse guard)
- Update (2026-03-30):
GET /api/files/:id/serveno longer accepts JWT via?token=; useAuthorization: Beareror signedsig/expquery (public quote images). Authenticated UI usesPOST /api/files/serve-links+GET /api/files/:id/serve-linkto obtain signed paths for<img src>/ links.
- Added lightweight in-memory rate limiting for sensitive public-token flows:
-
Audit input validation in
server/routes/quotes.js- Files:
server/routes/quotes.js - Priority: High
- Findings / fixes applied (additive):
- List
GET:statusrestricted to known filters;ORDER BYuses column whitelist (sortColumnMap); queries parameterized. POSTcreate:namemax length,guest_countrange; inserts use validated values.PUTquote:namelength,guest_countrange,lead_idvalidated as positive int when provided;lead_idbind passes parsed int ornull(unchanged). Note:COALESCE(?, lead_id)cannot clearlead_idto NULL with SQL NULL alone; clearing remains a product follow-up if needed.- Line items / reorder / discount_type: stricter validation (positive IDs, quantity bounds,
discount_typeenum).
- List
- Follow-ups: object-level authorization (BOLA) across
:idroutes remains a separate checklist item.
- Files:
-
Audit file upload handling in
server/routes/items.js- Files:
server/routes/items.js,server/lib/safeFilename.js,server/routes/files.js(actual uploads) - Priority: High
- Findings:
server/routes/items.jshas no multipart/file upload. Inventoryphoto_urlis a string (typically a path to/api/files/...after upload elsewhere). POST/PUT use parameterized SQL throughout.server/routes/files.js:multerdisk storage, 50MB cap, 20 files per request; stored names are random hex + original extension; magic-bytedetectMime+ allowlist / settings-based types; unsupported files unlinked before response.safeFilename: used inserver/index.js/server/api/v1.jsfor downloadContent-Disposition, not initems.js(strips control chars, quotes, backslash; length cap).
- Files:
-
Audit
server/routes/auth.js- Files:
server/routes/auth.js - Priority: High
- Findings / fixes applied:
- Passwords:
bcryptjswith cost factor 10 on setup/reset/dev-login; min length 8 on setup/reset. - Login: math challenge + optional reCAPTCHA; IP-based brute-force window (5 failures / 15 min) via
login_attempts; generic 401 on bad password. - Forgot/reset: 32-byte hex tokens, 1h expiry, single-use (
used); forgot response does not enumerate accounts. - JWT: tokens now explicitly HS256 on sign;
GET /meverifies withalgorithms: ['HS256']to matchauthMiddleware. - test-mail: behind auth + operator; per-IP rate cap (5/min).
- Passwords:
- Follow-ups:
SECRET()still mirrors legacyJWT_SECRET || 'change-me'(startup should enforce in prod); considermaxLengthon email/password bodies.
- Files:
-
Audit
server/routes/messages.js- Files:
server/routes/messages.js - Priority: High
- Findings / fixes applied:
- GET list: now validates
quote_idas positive int; if omitted, listing all messages is restricted to operator/admin (prevents broad message exfiltration for non-privileged accounts). Addedlimit/offsetpaging +directionallowlist. - POST create: validates
quote_id,reply_to_id, capsbody_text/subjectlengths, validates/sanitizeslinksandattachmentsshape (positivefile_id, name length), capsrich_payloadsize; SQL remains parameterized. - PUT read / DELETE: validates
:idas positive int and returns 404 when not found.
- GET list: now validates
- Follow-ups: consider object-level authorization checks tying quotes/messages to user accounts if multi-tenant is introduced.
- Files:
-
Audit DOMPurify usage in
client/src/pages/PublicQuotePage.jsx- Files:
client/src/pages/PublicQuotePage.jsx,client/src/components/public-quote/PublicQuoteContractView.jsx,client/src/lib/sanitizeHtml.js,client/src/components/messages/MessageBody.jsx - Priority: High
- Findings / fixes applied:
- Bug fix:
PublicQuotePage.jsxusedDOMPurify.sanitizewithout importing DOMPurify (would throw at runtime when contract HTML rendered). Contract HTML sanitization is now centralized insanitizeContractHtml()(client/src/lib/sanitizeHtml.js) and used by both standard and contract views. - Message threads:
MessageBodypreviously injected rawbody_htmlinto an iframesrcDoc; it now runssanitizeMessageBodyHtml()(DOMPurify with a slightly broader allowlist for email-like content, still no script/style by default). - Rich messages / plain text: unchanged;
RichMessageRendereruses structured React rendering; plain text usesautoLink(no raw HTML).
- Bug fix:
- Follow-ups: consider tightening
RichMessageRendererexternal URLs (imageUrl/ctaUrl) if untrusted payloads become possible.
- Files:
Source references:
- Express “Production Best Practices: Security” (
https://expressjs.com/en/advanced/best-practice-security.html) - OWASP API Security Top 10 (2023) (
https://owasp.org/API-Security/editions/2023/en/0x11-t10/) - Existing internal audit:
SECURITY_AUDIT.md,AI/reports/code-audit.md
Observed dependency findings via npm audit (root + client + server):
- Server:
- Patched by
npm audit fix(confirmed resolved):multer,nodemailer,path-to-regexp,brace-expansion,mailparser,imapflow(lockfile updates). - Remaining (no fix available):
xlsx(high) — used byserver/routes/sheets.jsandserver/routes/leads.jsfor spreadsheet parsing; advisories include prototype pollution and ReDoS.
- Patched by
- Client:
vite/esbuild(moderate) — dev-server related advisory; fix requires breaking major upgrade (npm audit fix --force→ Vite 8+).
- Root:
pkg(moderate) — build-time tooling advisory; no npm fix available.
Implications:
- Prioritize
xlsxremoval/replacement or add compensating controls (strict auth + size/rate limits) around spreadsheet upload/parse endpoints. - Treat Vite/esbuild as dev-only exposure; mitigate operationally (don’t browse untrusted sites while dev server runs; don’t expose dev server to LAN) until a planned upgrade.
- Treat
pkgas build-time risk; ensure CI/build runners are locked down.
-
Add API-wide rate limiting + tighter per-route limits
- Why: Protect against OWASP API4 (Unrestricted Resource Consumption) + API6 (Sensitive business flows).
- Where:
server/index.js(global), and stricter limits on token-mutation + auth endpoints. - Notes: Use a production-safe store (Redis) if multi-instance; avoid in-memory only.
- Done (baseline):
- Global: all
/api/*requests (exceptOPTIONSandGET /api/health,GET /api/v1/health) limited per client IP — default 600 requests / 60s (API_RATE_LIMIT_WINDOW_MS,API_RATE_LIMIT_MAX). - Auth POST:
login,forgot,reset,setup,dev-loginon/api/auth/*and/api/v1/auth/*— default 60 / 15 min per IP per path (API_AUTH_RATE_LIMIT_WINDOW_MS,API_AUTH_RATE_LIMIT_MAX). - Existing per-route limits remain for public-token quote/file flows (see public routes audit above).
- Global: all
- Follow-up: shared store when horizontally scaling; optional tighter caps on upload/parse routes (
/api/sheets,/api/admin/db/import).
-
Remove/forbid bearer tokens in query strings everywhere
- Why: Prevent credential leakage via logs/referrers/history; aligns with best practices.
- Where:
server/index.js,server/api/v1.jsfile serve;client/src/api.js+ call sites. - Done: Removed
?token=JWT from file serve; clientapi.fileServeUrlnow returns cached signed paths afterapi.prefetchFileServeUrls. New routes:POST /api/files/serve-links,GET /api/files/:id/serve-link(also under/api/v1/files/...via shared router). - Note: Password-reset links still use
?token=on the SPA route/reset?token=…(opaque DB token, not JWT) — different risk profile.
-
Enforce object-level authorization checks on all ID-based endpoints
- Why: OWASP API1 (BOLA) / API5 (Function-level authorization).
- Where: All routes accepting
:id(quotes/files/items/leads/messages/vendors/templates/etc.). - Output: Document per-route access rules and add a consistent “canAccessX(user, xId)” pattern.
- Progress (2026-03-31):
- Tenant foundation added (single-tenant default, enables real BOLA later):
- New migration
server/db/migrations/orgs.js: createsorgs(default org id=1) and addsorg_idcolumns (default 1) tousers,quotes,items,vendors,leads,files,messages.
- New migration
- Scoped high-risk routes to org_id (prevents cross-tenant leakage once multi-tenant exists):
server/routes/files.js: list/upload/delete + signed serve-link endpoints now requirefiles.org_id = 1.server/routes/messages.js: list/create/unread-count/read/delete now requiremessages.org_id = 1; create also requiresquotes.org_id = 1.
- Tightened function-level access where UI expects operator/admin:
server/index.js:/api/vendorsand/api/updatesnow require operator/admin.
- Tenant foundation added (single-tenant default, enables real BOLA later):
- More progress (2026-03-31):
- Quotes service layer now enforces
org_id = 1at read/write boundaries:server/db/queries/quotes.js,server/services/quoteCoreService.js,server/services/quoteListService.js,server/services/quoteService.js, plus quote item/section/custom-item services.
- Leads/items/vendors now scope to
org_id = 1:server/db/queries/leads.js,server/services/leadService.jsserver/db/queries/items.js,server/services/itemService.jsserver/routes/vendors.js
- Quotes service layer now enforces
- Remaining: propagate scoping to remaining routers/services that query by bare
id(notablyavailability,templatesgroups likepayment_policies/rental_terms/contract_templates, and any future multi-tenant admin/user management).
-
Harden public-token endpoints against automation
- Why: OWASP API6 + API4. Public approve/sign/message are inherently sensitive flows.
- Where: token-based endpoints in
server/index.jsand any v1 equivalents. - Notes: Add rate limits per token + per IP, and strong state-transition guards (409 on invalid states).
-
Remove/replace
xlsxto eliminate high-severity advisories- Why: Current
xlsxhas known prototype pollution + ReDoS advisories with no fix available via npm. - Where:
server/routes/sheets.js,server/routes/leads.js - Approach:
- Prefer a maintained library (e.g.
exceljs) for.xlsxreading, or constrain supported formats to.csvonly. - Add strict size limits + row/column caps + timeouts around parsing regardless of library choice.
- Prefer a maintained library (e.g.
- Why: Current
-
Add security headers with Helmet
- Why: Express recommends Helmet for baseline header hardening.
- Where:
server/index.js(install + configure). - Notes: Include at least
frameguard,noSniff,referrerPolicy; add CSP if feasible for your SPA.
-
Add/verify
app.disable('x-powered-by')- Why: Reduce fingerprinting (Express best practices).
- Where:
server/index.js
-
Set
trust proxyexplicitly when deployed behind a reverse proxy- Why: Rate limiting and audit IP attribution rely on
req.ip/x-forwarded-for. - Where:
server/index.js - Notes: If enabled, document the proxy topology so spoofing doesn’t bypass controls.
- Why: Rate limiting and audit IP attribution rely on
-
Input validation at API boundaries
- Why: OWASP API2/API8; prevent type/bounds issues and “mass assignment” style bugs.
- Where: High-value mutation endpoints:
server/routes/quotes.js,server/routes/files.js,server/routes/messages.js,server/routes/settings.js,server/routes/auth.js. - Notes: Define allowlists for writable fields; reject unknown fields; validate numeric bounds/enums.
-
Centralize and standardize error handling (no stack traces to clients)
- Why: OWASP API8 (Security Misconfiguration) + safer operational posture.
- Where: add Express 404 + error middleware in
server/index.js, ensure consistent JSON envelope.
-
Dependency security process
- Why: Express best practice: “Ensure your dependencies are secure.”
- Where: root,
server/,client/ - Tasks:
- add scheduled
npm audit/bun auditchecks in CI - track/resolve known advisories (server upload stack, parsers, etc.)
- document exceptions with rationale (e.g.
pkgbuild-time only; Vite dev-only;xlsxuntil replaced)
- add scheduled
-
Plan Vite upgrade to clear
esbuilddev-server advisory- Why:
vitecurrently pins vulnerableesbuildin audit output; fix requires major upgrade. - Where:
client/package.json,client/vite.config.js,client/src/* - Notes: Do this as a dedicated change with smoke tests; avoid
--forceupgrade without migration.
- Why:
-
File upload constraints review
- Why: Reduce risk of storing/serving dangerous content; OWASP API8.
- Where:
server/routes/files.jsand any other upload paths - Notes: Enforce allowlisted types + magic-byte checks; cap counts/sizes; quarantine unknown types; consider AV scanning if threat model requires it.
-
SSRF hardening for proxy routes
- Why: OWASP API7 (SSRF).
- Where:
server/lib/imageProxy.js, any future fetch/proxy endpoints - Notes: Strict hostname allowlist rules, block private IP ranges, and validate URL parsing.
-
Security logging / audit trail improvements
- Why: Detect abuse and aid incident response.
- Where: auth endpoints, public-token mutations, file serve, permission failures.
- Notes: Log request ids, actor type (JWT vs extension), and scoped object ids (avoid logging secrets/tokens).
- Auth token storage strategy review
- Why: SPA localStorage tokens are vulnerable to XSS; consider httpOnly cookies + CSRF if needed.
- Where:
client/src/api.js,server/lib/authMiddleware.js - Notes: Only if your deployment threat model requires it; document trade-offs clearly.
-
N+1 query audit in
server/routes/quotes.js- Look for queries inside loops, multiple queries where one JOIN would do
- Files:
server/routes/quotes.js - Priority: High
-
Check for missing DB indexes in
server/db.js- Look for CREATE INDEX statements. Are quote_id, item_id, status, event_date indexed?
- Files:
server/db.js - Priority: High
-
Audit pagination in all list endpoints
- Verify limit/offset is applied before returning results
- Files:
server/routes/items.js,server/routes/quotes.js,server/routes/leads.js - Priority: Medium
-
Audit
server/routes/availability.js- Conflict detection query — check for full table scans
- Files:
server/routes/availability.js - Priority: Medium
-
Audit
server/routes/stats.js- Check for aggregate queries that scan entire tables without date/limit constraints
- Files:
server/routes/stats.js - Priority: Medium
-
Check service layer separation in
server/routes/quotes.js- Flag business logic that lives in route handlers and should be in a service module
- Files:
server/routes/quotes.js - Priority: Medium
-
Audit
client/src/pages/QuoteDetailPage.jsx(~1550 lines)- Flag distinct concerns that should be extracted to sub-components
- Identify state that could be lifted or simplified
- Files:
client/src/pages/QuoteDetailPage.jsx - Priority: Medium
-
Audit
client/src/components/QuoteBuilder.jsx(~1060 lines)- Flag extraction opportunities
- Check for missing useCallback/useMemo on handlers passed as props
- Files:
client/src/components/QuoteBuilder.jsx - Priority: Medium
-
Audit
client/src/api.js- Error handling consistency — are all errors propagated correctly?
- Are there any fetch calls without error handling?
- Files:
client/src/api.js - Priority: Medium
-
Audit memory leak risks
- Check useEffect cleanups for timers, intervals, polling (PublicQuotePage polls every 8s)
- Files:
client/src/pages/PublicQuotePage.jsx,client/src/pages/QuoteDetailPage.jsx - Priority: Medium
-
Check for XSS via dangerouslySetInnerHTML
- Any use of innerHTML or dangerouslySetInnerHTML without sanitization?
- Files: all JSX files
- Priority: High
-
Audit
client/src/theme.css- Are all 4 themes complete? Are there missing variable definitions in some themes?
- Files:
client/src/theme.css - Priority: Low
-
Scan all
*.module.cssfiles for hardcoded color/font/spacing values- Flag any hex colors, px font sizes, or margin values not using CSS variables
- Files:
client/src/components/*.module.css,client/src/pages/*.module.css - Priority: Low
-
Check z-index consistency across all module files
- Files: all
*.module.css - Priority: Low
- Files: all
-
Find duplicated price calculation logic
computeTotalsexists in PublicQuotePage — is it duplicated from QuoteDetailPage?- Files:
client/src/pages/QuoteDetailPage.jsx,client/src/pages/PublicQuotePage.jsx - Priority: Medium
-
Find and flag all
console.login server code- These should be removed or replaced with structured logging
- Files: all
server/files - Priority: Low
-
Find commented-out code blocks
- Flag for removal or documentation
- Files: all files
- Priority: Low
-
Audit
package.jsonscripts across root, client, server- Are lint and test scripts present?
- Files:
package.json,client/package.json,server/package.json - Priority: Low
-
Check global error handling in
server/index.js- Is there an Express error middleware (4-arg function)?
- Are
unhandledRejectionanduncaughtExceptionhandled? - Files:
server/index.js - Priority: Medium
-
Audit
server/services/emailPoller.js- Is the background service resilient to network errors and IMAP failures?
- Files:
server/services/emailPoller.js - Priority: Medium
-
Check
.env/ environment variable documentation- Is there a
.env.examplefile? - Files: root directory
- Priority: Low
- Is there a
-
Architecture recommendation: service layer pattern for server routes
- After Codex audit, Claude to write refactor plan for extracting service modules
- Blocked by: Codex completing the audit
-
Refactor plan for QuoteDetailPage.jsx
- After Codex identifies the sub-components, Claude to write extraction plan
- Blocked by: Codex completing the audit
-
Apply UI fixes identified in Codex audit
- Blocked by: Codex completing
AI/reports/code-audit.md
- Blocked by: Codex completing
-
Polish and validate any CSS/layout findings
- Blocked by: Codex completing
AI/reports/code-audit.md
- Blocked by: Codex completing
All items completed 2026-03-28.
Full plan:
AI/reports/redesign-plan.md
- Noir (dark) theme added as 5th theme —
data-theme="noir"(2026-03-28) - 7 critical
flex-wrapresponsive fixes across action bars (2026-03-28) -
Layout.module.css—max-width: 1400pxon.mainInner;padding: 28px 36pxon ≥1280px (2026-03-28) -
theme.css—.btnrefined:justify-content: center,white-space: nowrap,box-shadowtransition,8px 16pxpadding (2026-03-28) -
theme.css— typography scale utilities:.page-title,.section-title,.card-label,.page-sub(2026-03-28) -
StatsBar.module.css—width: 180px→flex: 0 1 180px; min-width: 100px; value →min-width: 36px(2026-03-28) -
DashboardPage.module.css— barLabel/barValue fixed widths → min/max-width + ellipsis (2026-03-28) -
BillingPage.module.css— searchwidth: min(220px, 100%); table scroll-fade overlay (2026-03-28)
- Typography pass —
clamp(18px, 2.2vw, 24px)on all 13 page title rules;letter-spacing: -0.02em; line-height: 1.2(2026-03-28) - ImportPage stepper — 28→32px circles, glow ring on active, CSS variable colors, thicker connecting line (2026-03-28)
- BillingPage — right-edge scroll-fade indicator on
.tableWrapper(2026-03-28) - FilesPage — 480px breakpoint: count moves to top, viewSelect right-aligned (2026-03-28)
- QuoteBuilder mobile — already handled (2-col grid, toolbar wrap, pagination stack) — verified (2026-03-28)
- QuoteDetailPage component extraction —
QuoteSummaryPanel,QuoteContractPanel,QuoteFilesPanel,QuoteMessagesPanel(deferred to Wave 5) - Mobile bottom tab bar for core nav (Home, Projects, Inventory, Messages, More) (2026-03-28)
- Skeleton loaders on QuotePage — 6-card shimmer grid replaces spinner (2026-03-28)
- Empty state upgrade on QuotePage — CTA button + filter-aware messaging (2026-03-28)
- Skeleton loaders on StatsPage — 8 animated bar rows in card structure (2026-03-28)
- Skeleton loaders on MessagesPage — 7 shimmer thread rows replace spinner (2026-03-28)
- QuotePage header responsiveness —
.headerflex-wrap,.headerActionsflex-wrap + gap fix (2026-03-28)
- Removed
max-width: 1400pxfrom.mainInner— content fills full available width (2026-03-28) - Hidden top bar on desktop (≥769px) — all nav accessible via sidebar, recovers 44px vertical height (2026-03-28)
- Added Extension/Help as top-level sidebar nav item for all users — previously hidden from non-operator users (2026-03-28)
- Reduced 1280px+ main padding:
28px 36px→24px 32px(2026-03-28)
- QuoteDetailPage full-page loading skeleton — mirrors tab bar + header + two-column layout (2026-03-28)
- BillingPage skeleton — overpaid table (5 rows) + history table (8 rows) replace both spinners (2026-03-28)
- FilesPage skeleton — 12 shimmer tile cards matching grid layout (2026-03-28)
- VendorsPage skeleton — 5 shimmer rows matching card list layout (2026-03-28)
- SettingsPage skeleton — 3 cards each with label + 3 field rows (2026-03-28)
- AdminPage skeleton — 2 cards with system info row structure (2026-03-28)
- VendorsPage title — clamp typography to match system-wide standard (2026-03-28)
- DashboardPage stat grid —
repeat(4, 1fr)→repeat(auto-fill, minmax(160px, 1fr))— all 5 cards now show in one row on desktop (2026-03-28) - DashboardPage title — clamp typography to match system-wide standard (2026-03-28)
- TemplatesPage — removed
max-width: 720pxpage constraint (2026-03-28) - MessagesPage thread pane —
280px→300pxbase,360pxat ≥1280px (2026-03-28) - QuoteDetailPage right sidebar —
320pxbase,380pxat ≥1280px,420pxat ≥1600px (2026-03-28)
- LeadsPage two-pane desktop layout — table left, sticky activity timeline right (360px→420px) with empty state prompt (2026-03-28)
- LeadsPage eventsLoading — spinner → 4 skeleton timeline rows (2026-03-28)
- ItemDetailPage skeleton — mirrors 300px image col + info col grid (2026-03-28)
- ItemDetailPage title — clamp typography (2026-03-28)
- TemplatesPage skeleton — 2 skeleton cards with header + 3 list rows each (2026-03-28)
Added 2026-03-31. Based on full API audit for customer-facing React site (see AI/Api/).
These are gaps between what the current API provides and what a WooCommerce-style customer site needs. Grouped by impact.
-
Add a public
POST /api/public/leadsendpoint (no auth)- Why: The current
POST /api/leadsrequires a Bearer JWT (operator role). A customer inquiry form running in a browser cannot safely hold a service token. Either add a no-auth version or add reCAPTCHA protection to the public endpoint. - Where:
server/routes/leads.js,server/routes/publicCatalog.js - Notes: Rate-limit per IP (e.g. 10 leads / 10 min). Add honeypot field or reCAPTCHA check (settings already support
recaptcha_site_key). Mirror the existing lead fields:name,email,phone,event_date,event_type,notes,source_url. - Risk: Without this, every customer site either exposes an operator JWT to the browser or requires a separate middleware proxy.
- Why: The current
-
Outgoing webhooks system
- Why: No way for the e-commerce site to know when a quote is sent, approved, or confirmed without polling. This breaks any real-time order tracking or notification flow.
- Where: New
server/routes/webhooks.js, new DB tables (webhook_endpoints,webhook_deliveries) - Suggested events:
lead.created,quote.created,quote.status_changed,quote.approved_by_client,quote.contract_signed,quote.payment_added,message.received - Notes: Full implementation plan in
AI/Api/webhooks-and-events.mdincluding schema, payload envelope, HMAC signing, and retry logic. - Priority: Required for any non-polling integration architecture.
-
Add
accessoriestoGET /api/public/items/:idresponse- Why: Product detail pages need "you might also need" / related items. Accessories are already modeled in
item_accessoriesbut not exposed in the public API. - Where:
server/routes/publicCatalog.js—GET /api/public/items/:id - Notes: Return
accessories: [{ id, title, unit_price, photo_url, category }]. Limit to 6. Only include non-hidden accessories.
- Why: Product detail pages need "you might also need" / related items. Accessories are already modeled in
-
Add
slugfield to items for SEO-friendly URLs- Why: Customer site URLs like
/shop/item/42are not SEO-friendly./shop/item/gold-chiavari-chairis. Slugs need to be stable (generated from title, stored in DB). - Where:
server/db/schema/items.js,server/routes/items.js,server/routes/publicCatalog.js - Notes: Add
slug TEXT UNIQUEcolumn. Generate on create/update from title (lowercase, hyphens, strip special chars).GET /api/public/items/:idOrSlugshould resolve both. Add slug to sitemap URLs.
- Why: Customer site URLs like
-
Expose WebP/AVIF variant URLs in public catalog responses
- Why: The public API returns a single
photo_urlsigned URL but doesn't expose the WebP or AVIF variants the server already generates. Customer sites waste bandwidth using original formats. - Where:
server/routes/publicCatalog.js - Notes: Add
photo_variants: { webp: "...", avif: "..." }alongsidephoto_url(original). Only populate if the variant exists infile_variants.
- Why: The public API returns a single
-
Add public availability check endpoint
- Why: Customers want to know if items are available before requesting a quote. "Show items available on my date" is a standard e-commerce feature.
- Where: New
GET /api/public/availability?item_id=7&start=2025-09-20&end=2025-09-20 - Notes: Returns
{ item_id, quantity_in_stock, reserved, available }. Only countconfirmedquotes in reserved (not draft/sent). Do not expose quote details. Rate-limit per IP.
-
Standardize API versioning across all routes
- Why: Some routes live under
/api/v1/and some under/api/. A customer integration that pins to/api/quotescould break silently if routes migrate. The v1 prefix needs to be consistent. - Where:
server/index.js,server/api/v1.js - Notes: Audit which routes are on
/api/*vs/api/v1/*. Either commit all public-facing routes to/api/v1/*or document the/api/*routes as stable. Public catalog should be explicitly versioned.
- Why: Some routes live under
-
Add pagination metadata to
GET /api/public/itemsresponse- Why: The response includes
totaland the items array, but nopage/limit/offsetecho-back orhas_moreflag. Clients must track this themselves. - Where:
server/routes/publicCatalog.js - Notes: Add
{ total, offset, limit, has_more: offset + items.length < total }to response envelope.
- Why: The response includes
-
Add
sortparam toGET /api/public/items- Why: Currently always sorted by
category ASC, title ASC. Customer sites often want "newest first", "price low→high", or "most popular". - Where:
server/routes/publicCatalog.js - Notes: Allow
sort=price_asc,sort=price_desc,sort=name_asc,sort=newest. Whitelist these values server-side; never interpolate raw query param into SQL.
- Why: Currently always sorted by
-
Add
GET /api/public/categoriesstandalone endpoint- Why:
/api/public/catalog-metareturns categories + counts + company info all at once. A site that only needs the category list for nav rendering must over-fetch. - Where:
server/routes/publicCatalog.js - Notes:
GET /api/public/categories→{ categories: [{ name, count }] }. Simple and cacheable.
- Why:
-
Support multiple images per item
- Why: Items currently have a single
photo_url. A proper product detail page needs a gallery (main image + thumbnails). - Where: New
item_imagestable (item_id,file_id,sort_order,is_primary). New endpoints:GET /api/items/:id/images,POST /api/items/:id/images,DELETE /api/items/:id/images/:imageId. - Notes:
photo_urlon the item becomes the primary image shortcut. Public API returnsimages: [{ url, webp_url, sort_order }].
- Why: Items currently have a single
-
Add
featuredflag to items- Why: E-commerce landing pages need a "featured items" or "popular rentals" section. Currently no way to curate this without using categories.
- Where: Add
featured INTEGER DEFAULT 0column toitems. AddGET /api/public/items?featured=1. - Notes: Low effort, high-value for marketing pages.
-
Return
quantity_available(not justquantity_in_stock) in public API- Why:
quantity_in_stockis the total physical count. Customers need to see how many are actually free to rent, which requires subtracting confirmed reservations. The current public API exposes raw stock, which could mislead. - Where:
server/routes/publicCatalog.js - Notes: Availability check requires date range to be meaningful. Without a date param, return both fields:
quantity_in_stock(total) and omit or nullquantity_reservedto avoid confusion. Document clearly.
- Why:
-
Add
GET /api/public/items?ids=1,2,3batch fetch- Why: After cart restoration (localStorage), the customer site needs to validate that all saved items still exist and fetch current prices. Calling
/api/public/items/:idin a loop creates N requests. - Where:
server/routes/publicCatalog.js - Notes:
?ids=1,2,3→ return array of items for those IDs (skip non-existent). Cap at 50 IDs per request.
- Why: After cart restoration (localStorage), the customer site needs to validate that all saved items still exist and fetch current prices. Calling
-
Add
GET /api/public/searchwith relevance ranking- Why: Current search is basic
LIKEon title/description. For a large catalog, customers expect ranked results (title match > description match) and possibly typo tolerance. - Where:
server/routes/publicCatalog.js - Notes: SQLite FTS5 full-text search (
CREATE VIRTUAL TABLE items_fts USING fts5(title, description)) provides ranking. Not worth the added complexity until catalog exceeds ~500 items.
- Why: Current search is basic
-
Expose rental term duration as structured data
- Why: Items are priced "per event" but some are priced per day. There's no machine-readable duration field — this is inferred from
descriptiontext. E-commerce sites can't programmatically show "3-day rental" vs "single event". - Where: Add
pricing_unit TEXT DEFAULT 'event'toitems(event|day|week|hour). Expose in public API.
- Why: Items are priced "per event" but some are priced per day. There's no machine-readable duration field — this is inferred from
-
Add lead source tracking fields
- Why:
source_urltracks the page, but not the UTM campaign, referrer, or how the customer found the business. Marketing attribution requires this. - Where: Add
utm_source,utm_medium,utm_campaigncolumns toleads. Accept from the public lead form. - Notes: Pass from
URLSearchParamson the e-commerce site:?utm_source=google&utm_medium=cpc.
- Why:
-
Add a public quote status polling endpoint
- Why: The customer portal calls
GET /api/quotes/public/:tokenwhich returns the entire quote payload on every poll. For a lightweight status indicator, this is heavy. - Where: New
GET /api/quotes/public/:token/status→{ status, updated_at, contract_signed, amount_paid, total }only. - Notes: Reduces payload from ~50KB to <1KB for polling use cases. Rate limit: 30 req/min per IP.
- Why: The customer portal calls
-
Add
X-Request-IDheader to all API responses- Why: Debugging integration issues across distributed logs is painful without a correlation ID.
- Where:
server/index.jsmiddleware — generate UUID per request, attach toreq, echo in response header and error bodies.
-
Standardize error response envelope
- Why: Current errors return
{ error: "message" }inconsistently (some routes return different shapes). External integrations need a stable shape to parse. - Where:
server/index.jsglobal error handler + all route error responses. - Notes: Adopt:
{ error: { code: "NOT_FOUND", message: "Quote not found", request_id: "..." } }. See existing P1 security TODO for centralized error handling.
- Why: Current errors return
-
Document all
event_typevalues inquote_activity_log- Why:
AI/Api/docs list common values but the server never defines an enum. External integrations polling the activity log need a complete, stable list. - Where:
server/lib/quoteActivity.js— define and export aQUOTE_EVENT_TYPESconst. Reference in docs.
- Why:
Cross-reference: full API docs at AI/Api/. Full webhook architecture at AI/Api/webhooks-and-events.md.
- Claude scanned repository (2026-03-25)
-
AI/CODE_AUDIT_PLAN.mdcreated (2026-03-25) -
AI/TODO.mdpopulated (2026-03-25) -
AI/BADSHUFFLE_AGENTIC_CODE_AUDIT.mdwritten (2026-03-25) -
AI/AI-System-Setup.mdwritten (2026-03-25) - WCAG accessibility overhaul — ~80 fixes across 30+ files (2026-03-28)
- CSS theme system — replaced all hardcoded hex colors in module files with CSS vars (2026-03-28)
- WCAG remaining items — arrows, labels, type="button", alt text (2026-03-28)