The single source of truth for what the platform does, how it's built, and where it's going — structured to feed a technical design guide, a user instruction guide, and an investor package.
One master plan, three downstream documents. The same facts are told at three depths, so keeping them in one place keeps them from drifting apart.
| Downstream document | Audience | Pulls from |
|---|---|---|
| Technical design guide | Developers | Per-tool step logic, Data & architecture, Security & credentials |
| User instruction guide | Techs & salespeople | Per-tool step-by-step (the friendly retelling), Tool map |
| Investor package | Investors / partners | Product overview, Tool map, Roadmap & status |
Field Engineer is a mobile-first platform for a residential HVAC service-and-replacement business. It standardizes how the company evaluates jobs, prices them, repairs equipment, and manages parts — encoding the judgment of an experienced operator into tools any team member can use in the field.
Field outcomes today depend heavily on the individual: a strong salesperson sizes and quotes well, a senior tech diagnoses fast, and the right parts are (sometimes) on the truck. That variability caps growth and quality. Field Engineer raises the floor — it makes a newer salesperson or technician perform closer to a seasoned one, consistently, while protecting margin and creating a record of every job.
Encode senior-level process once; deliver it to everyone; protect the business's economics while doing it. The same equipment-identification and AI backbone serves every tool, so the pieces compound rather than stand alone.
Five internal tools plus a planned customer-facing app, all sharing one backend and AI layer. Pricing feeds Sales; Inventory is read by Tech and the future customer app; everything that needs the model or an external lookup goes through one backend.
Every section is tagged so readers always know what exists today versus what's designed.
Built working and deployed First pass built but intentionally shallow Planned designed, not built Future later stage
Single-page internal app (one HTML file, multiple tool "modes") + one stateless serverless backend (/api/lookup) that holds API keys and relays to Anthropic, RentCast, and (future) vendors. Customer app to be a separate front end on the same backend.
Per-device browser storage (platform store or localStorage fallback). Keys: surveys, tech profile, tech calls, price book + settings, inventory + locations, backend URL/token. [expand] — full data models per tool; the move to a central database for cross-device + multi-user.
Claude (current model) for nameplate vision, repair diagnosis, rebate search, and price extraction. RentCast for property data. Vendor pricing via files/API. [expand] — per-action request/response contracts (several already in the Technical Reference).
APP_TOKEN gate; spend limits as backstop.| Stage | Item | Status |
|---|---|---|
| 1 | Sales / Survey + recommendation | First pass |
| 1 | Property & rebate lookups (backend) | Built |
| 1 | Pricing / Price Book + markup | First pass |
| 1 | AI nameplate read + published-spec lookup (per equipment type) | Built |
| 1 | AI bill ingestion — electric & gas, two utilities, real rates | Built |
| 1 | Tonnage-aware incentive engine + live multi-jurisdiction lookup | Built |
| 1 | AI sizing sanity check (recommendation → proposal) | Built |
| 1 | White-label brand config + suite naming (Empower Toolbox) | Built |
| 1/2 | All-electric / baseboard → ductless multi-zone path (zone pricing, gas-service gating) | Built |
| 1 | Capture existing cooling (swamp/none) → sell added AC | Built |
| 1 | AI multi-unit equipment identification (batch photos → auto-assembled systems) | Built |
| 3 | Bill consolidation & energy-story chart (customer offering) | Built first pass |
| 2 | Tech / Repair Copilot (Sidekick) — separate app, credential gating, safety stops, diagnostic loop | Built first pass |
| 2 | Inventory (truck & shop) | First pass |
| 3 | Sales↔Pricing wired; Inventory↔Pricing link | Built first pass |
| 3 | Proposal / customer-facing pricing presentation | Built first pass |
| 3 | Customer Service Request app (extends Concierge) | Built first pass |
| 4 | Accounts, roles, admin console, central DB | Built first pass |
| 4 | Curated diagnostic trees; vendor API integration | Built first pass |
| 5 | Install / Commissioning tool (Foreman) | Built first pass |
A running log so decisions are captured once and don't get re-litigated. [expand as we go]
utility action (Claude + web search) names the electric & gas distribution utilities; a blocking confirmation window requires the salesperson to confirm (edit = pick-from-list) before Step 1 advances. Never auto-passes.parsebill vision action upgrades the savings tier; opt-in, never required, usage/cost extracted only, bill image never persisted (Safety/privacy condition).rates action. Pull effective residential rates from EIA (utility-level for electric when known, state for gas, region for propane), cached/refreshed ~monthly, with a Claude+web-search fallback — rather than scraping provider websites. Each rate carries an asOf vintage and recent trend; the recommendation screen shows the as-of date with an asterisk and an escalation note ("based on 2024 rates; prices up ~X% since, so real savings likely higher"). Headline computed on the conservative as-of rate; bills override to actuals.estimate action (Claude + web search, labeled rough, ~$11–15k for a typical Front Range furnace+AC), baking in CO altitude sizing + 96%+ AFUE. Adjustments = preset priced add-ons + an "Other" freeform priced from company labor + markup + AI materials. Production swaps the estimate for the Pricing tool import via the same contract.parsebill vision action reads electric and gas bills from two separate utilities — extracting usage, the customer's actual $/kWh and $/therm, peak demand, and monthly temperatures (incl. 12-month history graphs). Bills are fuel-tagged; the customer's real rates drive the savings estimate. Bill images are still never persisted.ductless equipment type (Ductless Mini-Split, multi-zone) wired through tiers/pricing/rebates/savings; recommendedType routes baseboard — and electric/propane/oil with no ducts — to ductless; zone/head-based pricing (baseline + per-head × zones, editable on the rec screen) since multi-zone isn't a single tonnage. Why: resistance→heat-pump is the biggest-savings conversion we quote, and ductless fits where there are no ducts (line sets, no drywall). Gas service field-confirmed (Confirmed / None / Unknown on the utility step) — never inferred from the map, because gas-on-the-map ≠ gas-at-the-home; it suppresses dual-fuel and gates the gas-service-dependent Xcel cold-climate rebate (no gas → a conservative electric-utility line instead, so we never quote a rebate the home can't claim). A gas sanity check reads the fuel-tagged bills: electric-only bills (no gas bill) flag "likely no natural gas" and offer a one-tap "Mark: no gas service," distinguishing all-electric from delivered fuel. Panel/circuit prompt extended to ductless on ≤125A service. Baseboard kept as emergency backup until design-temp capacity is confirmed (Safety condition).identify action takes a batch of label photos (furnace, AC condenser, heat pump, swamp cooler — any mix), reads every distinct unit, looks up published specs per model, and returns a structured units[] list; the app auto-assembles them into systems (a furnace + an AC condenser collapse into one Furnace+AC system; lone units stand alone) and picks each dropdown type. Why: removes manual type selection and slot assignment — the tech just photographs the labels. Reads never overwrite a system already filled in (append-only), and every assembled system is marked "AI-detected — change if wrong." The dropdown stays for overrides and manual entry.BRAND config; rebranding for another contractor is a one-value change. The suite is named as a "crew of roles": platform Empower Toolbox with apps Evaluator (Sales), Sidekick (Repair), Bookkeeper (Pricing), Quartermaster (Inventory), Concierge (Customer), Foreman (Install). Role names stay across brands; only the prefix changes. Enables SaaS resale to other companies.sidekick.html) on the shared core (Store, Backend client, BRAND/SUITE with THIS_APP="repair", design system, photo-AI) — so a rebrand still flows from one BRAND config. Implements: admin-set tech credentials (level, EPA type, skill gates, conditional-override — a tech can't grant their own); job-type templates; an AI diagnostic loop (backend diagnose action proposes the next check / a diagnosis / a hard stop, with an offline demo path); app-authoritative safety — a persistent ⛔ stop, universal hard stops (gas odor / CO / water-on-energized) with no override, conditional stops (cracked HX, irreconcilable readings) clearable only with the matching gate + override + a stamped acknowledgment; credential gating that escalates rather than dead-ends; full job stamping (tech ID, EPA cert on refrigerant work, credentials used); and the repair-vs-replace crossover that hands off to Evaluator. Why a separate app: the Architect's module-boundary + the Safety veto — repair safety logic and the diagnostic flow are isolated from Sales, while sharing the core. Reasons captured per the council: Investor (repair→replace is the revenue hinge), Operator (admin-set creds + unmissable red-tag + stamping), Field Tech (one step at a time, escalate-don't-dead-end, always-visible stop), Safety (universal stops no-override, AI flags but never clears). Next pass: wire batch identify into component capture, real part-spec lookups in repair guidance, and the priced repair-vs-replace handoff into a live Evaluator visit.identify action: snap every label, AI reads them, types each component, and fills model/serial/specs (appended, marked “verify”). The backend identify action gained an additive subtype (heat_pump_outdoor/indoor, minisplit_outdoor/head, ac_condenser, evaporator_coil, rtu, zoning, etc.) so it tells indoor from outdoor and a coil from a condenser — kept backward-compatible (the Evaluator still reads the coarse unit, unaffected). Why (council): Field Tech wanted fast typed capture + batch photo identify; Architect wanted a real taxonomy + reuse of identify with an additive subtype; Operator wanted the coil/zoning/RTU nameplates techs forget for parts & warranty; Safety required “verify” prompts so an AI tag never silently mislabels a combustion unit; Investor noted near-zero marginal cost and better records feeding repair-vs-replace and future inventory.diagnose action returns safety.hazard and a lookFor {component, label, location, note}; the offline demo carries them too (incl. an AC contactor/condenser-panel sequence). Why: a green tech needs to recognize the part and be unmistakably warned about energized/charged components — the illustration is deliberately diagrammatic so it can never be mistaken for the exact unit, and the disclaimer + editable nature keep it safe.part_lookup, web-search) populates the correct part from the unit’s make/model — spec, OEM number, cost range, labor. The tech can request acceptable substitutes when the exact part isn’t on the truck (e.g. a 35/5 µF run cap can run on a 40/7.5 µF); each substitute carries the engineering rationale and constraints and an acceptable rating (yes / in-a-pinch / not-acceptable is blocked; a substitute may never lower the voltage rating). The repair estimate (part cost + labor×rate) feeds the existing repair-vs-replace crossover against the unit’s age/expected life. At close-out the tech photographs the installed part and “Verify installed part” (backend verify_part, vision) reads the label and returns match / workable / mismatch / unreadable. Completion is gated: a job with a required part can’t be stamped complete without a verification of match or workable — mismatch/unreadable block it; a real mismatch must be corrected & re-verified or flagged & escalated with the read-vs-required discrepancy stamped to the job. Why: the owner’s requirement — stop techs from leaving jobs incomplete or claiming one part while installing another, by accident or intent. Safety framing: AI part guidance is advisory and constraint-bounded (never an unsafe substitute); the install verification is the authoritative integrity check and is non-bypassable for parted jobs. Backend grew to 11 actions (added part_lookup, verify_part); offline demos included.BUILD version (v0.9.0 / date). Sidekick shows “Empower Sidekick · v… · date” on the Home and Admin footers; the Evaluator shows it in Settings; the backend returns version from the ping action (browse /api/lookup health / Test connection). This ends the “is my deployment current?” guesswork — if the footer shows an older version, the live file is stale. Also: the diagnostic-step part illustration now fuzzy-matches the look-for component (e.g. a live backend returning “compressor contactor” or “dual-run capacitor” still draws the contactor/capacitor schematic), so the picture shows on the live path, not just the offline demo. Note: the part picture + large hazard banner require both the latest sidekick.html (render) and api/lookup.js (so diagnose returns safety.hazard + lookFor) to be deployed.sidekick.html only.TREES JSON node-graph) for high-frequency tickets (furnace no-heat and AC no-cool to start), with the existing AI diagnose loop as the fallback for unresolved leaves, an explicit “Something unusual” exit, and uncommon gear. On the diagnose step the tech picks Guided checklist or AI copilot; the checklist walks branch nodes by tapping the observed state. Safety guardrails (per veto): measurement nodes require a real reading (logged to the trail) before the outcome tap — no convenience-tapping past a critical check; hard stops and credential gating are enforced by the app on both paths; leaf diagnoses carry requiresGate (EPA-U on low-charge, G1 on gas valve, G2 on venting/HX). The walked path becomes observations that seed the AI on handoff (backend diagnose now reads an observations field), so the model starts warm and cheaper. Trees reuse the hazard banners + look-for illustrations. Next: author trees for the remaining tickets and measure which leaves get used. Redeploy sidekick.html + api/lookup.js.capture="environment", which had forced the mobile camera and hid the photo roll. They now show the OS chooser (Take Photo / Photo Library), keeping accept="image/*" + multi-select. Sidekick v0.10.1, Evaluator v0.9.1 — app-only, redeploy index.html + sidekick.html.APP_TOKEN required (a public AI endpoint on real phones is the clearest risk); (4) mandatory CO/combustion capture on gas appliances before completion (Safety veto: the liability record must persist off-device). Strongly-wanted, non-blocking: (5) per-job feedback (diagnosis right/wrong) for beta learning; (6) trees for the remaining tickets (boiler, heat pump, mini-split, water heaters); (7) customer-facing summary + sign-off. Main tension: offline-first vs. real job-sync — resolved as export/import + queued retry for beta, real sync post-beta. Scope: 3–5 trusted techs, two trees + AI, export + token-lock.fe:handoff:<id> record (same-origin localStorage) and stamps the job. The Evaluator shows pending hand-offs on its Home and Start sales visit prefills a new survey (customer, address, the existing unit as a system, and a hand-off note with the diagnosis/estimate), then consumes the record. Sidekick v0.11.0, Evaluator v0.9.2 — app-only, redeploy index.html + sidekick.html.sidekick.html. Beta checklist status: #2 export ✓, #4 combustion ✓; remaining must-haves: #1 offline resilience, #3 backend token-lock (deferred by request).sidekick.html. Beta must-haves: #1 offline ✓ (beta-level), #2 export ✓, #4 combustion ✓, #6 trees ✓; #3 token-lock deferred.sidekick.html. Beta: #1 offline ✓, #2 export ✓, #4 combustion ✓, #5 feedback ✓, #6 trees ✓, #7 sign-off ✓; only #3 token-lock outstanding (deferred).accept() requires a drawn signature, sets the proposal signature (dataURL) + decision, marks the visit ready_to_install, and the drawn signature now prints on the proposal (replacing the old typed-name line). Decline captures an optional reason and marks the visit declined (reopenable). Share / export: Print / Save PDF (existing), Copy summary (proposalText → clipboard), Download JSON (full visit), and Share (Web Share where available). App-only — redeploy index.html. Remaining bundle: structured customer (first/last/email), offline Net pill, JSON-backup polish; financing + quick-quote deferred.{firstName, lastName, phone, email} (was a single name), matching Sidekick. The customer step captures First + Last; a custName() helper composes the display name everywhere (surveys list, proposal, print, commission, exports); legacy single-name records auto-split on open via migrateCustomer(); and the Sidekick→Evaluator hand-off now maps first/last directly instead of concatenating. App-only — redeploy index.html. Remaining bundle: offline Net pill + fail-fast, fuller JSON backup/restore; financing + quick-quote deferred.Net module + header ● Online / ○ Offline pill (auto-detected, updates on reconnect); every backend call (property/RentCast, utility, nameplate read, batch identify, AI sizing check, bill ingestion, live rebate search, estimate, ping) now fails fast when offline with a "saved — retry when back online" message instead of a 45s hang, and the built-in estimates/sizing still work offline. Backup/restore: Settings → Data now has Export all (backup) — one JSON of every visit + settings — and Import / restore, which accepts a full backup (optionally restoring settings) or a single-visit export and migrates legacy records on the way in. App-only — redeploy index.html. Evaluator close-the-loop + parity bundle COMPLETE (#1 accept/e-sign/share, #2 structured customer, #3 offline, #4 backup/restore). Deferred: financing/lender presentation, quick-quote path. Shared: backend token-lock.APP_TOKEN in Vercel now locks the backend: every request (including ping) must carry a matching token via Authorization: Bearer or it gets a 401 — distinct token_required (none sent) vs token_invalid (wrong) messages. Both apps already send the token and have an App token field in Settings/Admin (relabeled + noted for beta); a 401 now surfaces a clear "add it in Settings" prompt instead of a raw HTTP error. Unset APP_TOKEN still leaves the backend open (backwards compatible). Verified: locked rejects missing/bad tokens, accepts the right one, unlocked stays open. Deploy: set APP_TOKEN in Vercel + redeploy api/lookup.js, index.html, sidekick.html; paste the same token into each app. BACKEND_SETUP.md updated to make locking the required beta step. This was the last outstanding pre-beta security item — both apps are now beta-ready.manual_lookup action (one targeted web search on brand+model) that returns the manufacturer doc link plus extracted fault/error-code table, sequence of operation, wiring notes, specs/charging/combustion targets, safety notes, with a confidence rating. In Sidekick, each captured component gets a 📄 Find & pull manual button; results are cached on the component (offline-readable) and shown in a manual sheet reachable per-component and from a persistent 📄 header icon on every job screen. Each manual shows the matched model + confidence badge + "verify this matches your unit" note + a link to the full document. On-demand (not auto-fired per component), offline-aware (fetch needs signal; cached sections work offline), and demo manual offline. Deploy: redeploy api/lookup.js + sidekick.html. Safety: extracted summaries are explicitly labeled as not a substitute for the manufacturer document on gas/electrical/combustion.identify backend, types each unit and fills model/serial/specs). The gap vs. the Evaluator was placement: it appended every detected unit as a new component, leaving the job-type’s suggested boxes empty alongside. New placeIdentified() drops each AI-read unit into an existing empty box of the matching type first, then any empty box, and only appends if none are free — so loading 1/2/3+ photos populates the appropriate component boxes cleanly (verified: furnace + condenser photos fill the furnace and condenser boxes, no duplicates). Per-batch photo cap raised 8 → 12; card copy clarified. App-only — redeploy sidekick.html.sidekick.html.sidekick.html. Future: pull real new-system cost from a hand-off / the Evaluator; per-shop life & cost defaults in Admin.replacementCostRef() resolves the "cost of a like system" in priority order: (1) a shared fe:pricebook from the future inventory/cost tool, (2) the Evaluator’s actual recent quotes — Sidekick reads fe:survey:* (shared device storage), maps each proposal line item’s system type to a key, and averages the real sell prices, (3) a normalized default. The advisor prefills the new-system cost from this and shows the source ("Evaluator quotes (avg of N)", "Inventory/cost tool", or "default"), with a ↻ pull latest button; the tech can still override per job. Also fixed a pre-existing bug: the job-type equipment labels ("Boiler", "Water heater (tank)", etc.) didn’t match the old life/cost tables, so the advisor was defaulting most types to 15-yr/$12k — new eqLife()/eqDefaultReplace() normalizers give each type its correct useful life and default (boiler 25yr/$9k, water-heater 12yr/$2.6k, tankless 18yr/$3.8k, etc.), which matters because depreciation divides by life. App-only — redeploy sidekick.html. Eventual: blend Evaluator + inventory pricing; a same-customer hand-off quote would be the most specific source.prepImage: photos that decoded with unreported dimensions (common with iPhone HEIC / very large images) produced a 0×0 canvas, so toDataURL returned an empty image that the AI couldn’t read — and the old try/catch only caught thrown errors, not the empty-but-valid case, so it failed silently. The photo whose dimensions decoded normally (the condenser) was the only one that worked. Fix: prepImage now falls back to the original image bytes whenever the canvas can’t produce a usable image (0×0, empty output, or decode error), so every readable photo reaches the AI. This repairs both the batch identify (multiple photos → one read) and the per-section reads. Also hardened capture: per-file try/catch so one bad photo can’t break a batch, empties are skipped with a count, and reads now give honest feedback — "Couldn’t read that label — retake or type it in" instead of implying success. App-only — redeploy sidekick.html.completeJob routes to the sign-off screen if no acknowledgment is recorded, after all other gates pass. The acknowledgment was strengthened to explicitly confirm the work was completed, the replaced part was shown or the fix explained, and the price was disclosed (the billed total now appears on the sign-off and is snapshotted to job.signoff.billedTotal); the completed-work photos are shown on the screen so the tech can show the customer. Per the council, it’s required-with-a-documented-exception, not an absolute wall: a one-tap Customer not available to sign branch records a reason (not on site / tenant present / authorized by phone / property manager / declined) and still closes the job with the summary saved to the report/JSON. Safety lines held: the wording is an honest acknowledgment (not a rights waiver), and a missing signature never blocks a safety action or red-tag (those use their own paths, not completeJob). Caught in test: an unqualified openSignoff() call that would have crashed on close. App-only — redeploy sidekick.html.customerDocHTML() + printCustomerDoc() for a clean customer-facing document: branded header (company, phone, license, owner line, tagline), customer/site, equipment, a plain-language "What we found and did" summary, the repair-vs-replace explanation when relevant, an itemized Charges block (service charge + repair parts/labor + total), and the acknowledgment statement with the captured signature (or "No signature on file — ‹reason›", or blank signature lines if printed before signing). It reuses the same summary text, prices, and signature as the on-screen sign-off (one source of truth) and deliberately omits internal tech data. Buttons: prominent "🧾 Print customer copy" on the summary (office report relabeled), plus one on the sign-off screen to hand over a copy on the spot. App-only — redeploy sidekick.html.support.html, intake v0.1.0; backend v0.12.0). A no-login web page where a homeowner describes a problem; the AI triages, asks for photos only if useful, and returns a high-level "here’s what it might be" list that repeatedly defers to the on-site technician as the final judge — built to earn trust and let the tech stock the right parts, not to self-diagnose. Council endorsed it as the front of Concierge but set hard conditions, all built in: emergency detection first (client-side keyword check + server backstop — gas/CO/smoke/sparks/electrical-water route straight to a STOP screen with 911 + gas-line buttons, no causes shown); high-level only, no DIY steps; a tech-only parts hint (not shown to the customer); explicit consent before sending; and a privacy-minded footer. Delivery (owner’s pick): email/SMS the shop, no new infra. New PUBLIC backend actions triage + support_request bypass the APP_TOKEN (tech actions stay locked) with payload caps; support_request emails the shop the summary + photos via a transactional email API when configured, else the page falls back to the customer’s own mail/SMS/call apps so it works day one. Open risk (logged): the public endpoint is unauthenticated and cost-bearing — caps are in place but real rate-limiting/abuse protection needs infra (Vercel WAF or a KV) before promoting it widely. Deploy: redeploy api/lookup.js (new actions) and add support.html to the repo root → /support.html; for auto-email set Vercel env vars RESEND_API_KEY, SHOP_EMAIL, SHOP_FROM_EMAIL (optional — fallback works without). Leverages the Evaluator/Sidekick brains (reuses photo identify + Sidekick’s diagnostic knowledge via the bounded triage prompt) and seeds the future Concierge tool.panels[] model (main + add sub-panels), each with service size, open spaces, red-flag list, and a 📷 AI panel read (new token-locked backend action panel_read → reads the main-breaker amps, estimates open spaces, flags hazards like Federal Pacific/Zinsco/double-taps) with an explicit "open spaces ≠ spare capacity — not a load calc" disclaimer and a heat-pump-readiness warning at ≤125A. Ductwork: condition + type + return adequacy + visible-issues checklist + optional static-pressure reading + a pre-1980 asbestos flag (auto-suggested from year built) with a do-not-disturb/test warning. Access: the multi-select now prices each condition (finished basement, crawlspace, attic, low attic, tight access, stairs, 2nd-floor, roof/crane, long line-set) into an "Access / site conditions" proposal line that stays in sync as selections change. Legacy surveys migrate (migrateSite) and pricing/flags now read the new model. Council guardrails held: AI read is an aid (human is final judge), never a load calc, never instructs panel work; asbestos is do-not-disturb. Redeploy api/lookup.js (new action) and index.html. Tunable per-condition adders and per-shop life/cost defaults remain a future Admin item.· v<version> in the header subtitle next to the app name (Evaluator v0.12.1, Sidekick v0.18.2), so the running version is visible at a glance after a deploy — the faint footer stamp stays too. App-only — redeploy index.html and sidekick.html.fld, the ⓘ auto-attaches by label — ~53 total entries now, still one editable dictionary. App-only — redeploy index.html.recommendedType now picks dual-fuel instead of an electric-backup heat pump whenever gas is available and the home is ducted; the rec carries a transparent “Why dual-fuel” note (gas backup removes the big electric-strip load, often avoids a 200A upgrade, holds heat in deep cold), and the load calc shows a dual-fuel nudge when electric strips are entered on a gas-available home. Honest lean (Moralist): only where gas genuinely exists; no-gas homes still correctly use electric backup with that disclosed. App-only — redeploy index.html.recommendedType now reads the existing equipment for a like-for-like “direct replacement” (furnace+AC → furnace+AC, electric baseboard → ductless, existing heat pump → heat pump / dual-fuel on gas), and the rec shows a transparent “Why this recommendation” note stating the basis (existing equipment + fuel + goal). (4) ⓘ help for SEER2 / HSPF2 / AFUE added on the recommendation tab right above the Good/Better/Best specs. App-only — redeploy index.html.index.html.html field (vs the plain d text) and the help sheet scrolls (max-height 82vh) for longer references. App-only — redeploy index.html.Empower-Sales-Training-Manual.html (in outputs + bundle docs). NOTE: Care Plan pricing is a launch default — office to confirm before quoting; plan terms are not yet wired into the Evaluator proposal.Empower-Residential-Service-Plans.docx (plan summaries + comparison matrix + full service-plan agreement: term/auto-renewal, payment, scope, transfer, assignment, cancellation, mediation→arbitration, liability). Training manual Lesson 6 + quizzes + final test updated to these tiers. FLAGS: pricing is ~3× the earlier draft (premium positioning — office to confirm for the Front-Range market); agreement contains arbitration + assignment + auto-renewal clauses that need legal counsel review before use (not legal advice); plans still not wired into the Evaluator proposal screen.computeProposal (default performance), prints on the branded customer copy with the auto-renewal/loyalty disclosures, and is referenced in the acceptance acknowledgment. The plan is kept separate from the financed equipment total — it’s a membership, not part of the loan. App-only — redeploy index.html. NEXT: redesign the signed agreement/contract format (financing terms, plan terms, cancellation, deposit/balance) — in discussion.AGREEMENT, easy to edit): (1) Scope — what’s included + explicit exclusions (asbestos, electrical beyond listed, drywall, hidden conditions → change order) + preliminary-sizing note; (2) Price & payment — deposit-at-signing / balance-at-completion, plan billed separately; (3) Financing disclosure — generic third-party-lender language (no lender yet), monthly/APR/term + total of payments, approval-separate, not-the-lender; (4) Service plan terms (Year-1-free, renewal, auto-renew, loyalty); (5) Legal — warranty (mfr + 1-yr workmanship), limitation of liability, mediation→arbitration, governing law (CO), entire agreement. Plus a Colorado-style Notice of Right to Cancel (3 business days). The doc relabels to “Installation Agreement,” the signature line acknowledges the terms + right-to-cancel receipt, and the in-app acceptance checkbox + a reviewer note were updated. App-only — redeploy index.html. FLAG: contract + arbitration + right-to-cancel language is a template for attorney review, not legal advice; office should set the mailing address used in the cancel notice.index.html. FLAGS: app brand (Empower Service / 720-676-6412) differs from the contract entity (Empower Service / 720-676-6412) — reconcile which is correct; full contract remains a template for attorney review, not legal advice.index.html as the hub (two tiles — Evaluator, Sidekick — plus reference links to the training manual, service plans, master plan & tech ref, and a clearly-separated “send customers here” link to support.html). Evaluator moved from / to /evaluator.html; Sidekick’s openEvaluator() repointed to evaluator.html (Sidekick v0.18.6). New deployed layout: /=hub, /evaluator.html, /sidekick.html, /support.html, /api/lookup, plus /docs/ for the hub’s reference links. BACKEND_SETUP + README updated. Redeploy index.html (hub) + evaluator.html + sidekick.html (+ docs/). OPEN: hub is not login-gated — add a light access step before sharing widely (Investor flag).BRAND.ownerLine is now “locally owned and operated” (Evaluator v0.12.18, Sidekick v0.18.7) and the training manual prose updated to match. App-only — redeploy evaluator.html + sidekick.html (+ docs training manual).Empower-Sidekick-Tech-Levels-and-Manual.html (outputs + bundle docs) and linked from the hub. FLAG: certifications are typical/recommended — licensing varies by AHJ; the office sets the actual standard.recommendedType() recommendation decision tree, the Sidekick repair workflow + the safety hard-stop decision tree + the credential gate check, the Concierge intake/emergency-triage flow, and the backend request-routing flow. Each diagram carries reference callouts to the relevant system docs (Architecture & Design, Technical Reference, Stage-2 Spec, User Guide, Master Plan) and the Investor Brief, and the doc ends with a full documentation map. Generated via build_flows.py (programmatic SVG layout). Delivered as Empower-System-Flowcharts-and-Decision-Trees.html (outputs + bundle docs), linked from the hub (ref 6) and cross-linked from inside both the Sidekick manual (§08) and the Evaluator Sales Training Manual so the charts live inside the training material.guardPublic() — honeypot field, min-time-to-submit, per-IP rate limit (12 triage / 5 support per 10 min), a best-effort daily AI-call ceiling (800), and payload caps (image count/size, description length). Frontend sends a hidden honeypot + load timestamp. Email upgraded to structured address + access. NOTE: rate-limit/ceiling are per warm serverless instance (best-effort) — for production-grade global limits add Vercel KV/Upstash. Redeploy support.html AND api/lookup.js (backend change → Vercel redeploy required). This closes the council’s Concierge backlog and the public-endpoint cost/abuse risk — the gate before sharing the link widely is now cleared.nameplate rewritten to read-first, specs best-effort — it never throws on a hard read, returns whatever brand/model/serial is legible with a read status + legibilityNote, and web-search is capped lower (3 uses) to cut latency/timeouts; identify likewise no longer hard-fails; (c) client read timeout 45s→60s (matches Vercel maxDuration). Layout reworked to an Evaluator-style two-step flow: a numbered “Identify everything from photos” card, then clean per-component cards with a prominent full-width “Read this nameplate with AI” CTA, capture tips, an “AI-filled — verify” badge, and tidy fields. Per-component read now surfaces what was read or the legibility hint. Redeploy sidekick.html AND api/lookup.js (backend → Vercel redeploy required).core.css + core.js (Store, Backend client, BRAND/SUITE registry, UI helpers, app shell/nav, real auth + role gate; no build step) as the single source of truth, ending the copy-pasted-core tax. Field tools stay local-first / offline; office tools (inventory, accounting) require a thin, role-based, multi-user office datastore (persistent shared state localStorage can’t provide) — the gating dependency for Quartermaster & Bookkeeper. Build Quartermaster first (truck stock tied to Sidekick parts usage); integrate QuickBooks rather than rebuild a ledger. Two tracks: (1) core-extraction pilot (hub + Evaluator first); (2) office-datastore spec / build-vs-buy before any data-layer spend. Safety: shared core keeps Sidekick gates/hard-stops in app logic. NEXT (in progress): writing the office-datastore data spec, eliciting owner wants per area.Empower-Platform-Data-Spec.html; hub reference #7 added. Open follow-ons: resale go-to-market/economics council; QBO Online-vs-Desktop & per-site class mapping; datastore vendor/region; scheduling depth; fe: data migration to site/corp.customerType (residential / property_manager / commercial), defaultTerms (due-on-completion · deposit+balance · net 10/15/30/45/60), taxStatus, poRequired, notToExceed, depositRule — terms map 1:1 to QuickBooks Terms; deposits map to a QBO deposit/retainer item the final invoice nets against; property managers = parent/child customers (sub-customer per property/unit, mirroring QBO sub-customers) with optional consolidated statements. Tech finalizes the invoice at the call as the gate to mark a job Done: parts auto-filled from Quartermaster inventory usage + flat-rate price book, terms preset by customer type. Residential = deposit-to-reserve at booking → balance on completion, collected via QBO payment link / card-on-file (no card in-app → no PCI). PM/commercial = finalize + capture PO & authorization, push to QBO with correct net term; collection downstream. QBO stays AR system of record (suite finalizes + syncs invoice, reads back payment); suite does not mirror QBO’s ledger. Compliance (Safety veto): deposits & authorizations must be documented not verbal; deposit terms disclosed + refundable on cancellation; card-on-file auto-charge needs logged consent. Moralist: consistent price book — vary terms, not the number for the same work. Use-case backlog: estimate→PO approval before work, change orders + NTE re-approval, tax-exempt, progress/draw billing for installs, warranty/$0 invoices that still decrement inventory, service-plan pricing on invoice, card-on-file auto-charge w/ consent, remote approval when no one on-site, cancellation/deposit-refund terms, AR status surfaced back on the job. Sequencing: build after Quartermaster (invoice depends on inventory part-pricing). TODO: fold into Data Spec §05/§06; resale council to decide if term-customer support/statements are a higher SaaS tier.cardSurchargePolicy): absorb the processing cost into the price book (no line), or surcharge a percentage added as an explicit, disclosed invoice line on credit-card payments. Surcharging is gated state-by-state: the platform maintains a StateCardRule reference (allowed?, max %, disclosure required) keyed on the job/customer state; the suite applies the lower of the corp’s chosen %, the state cap, and the processor’s cost-of-acceptance cap. Hard rules: credit only — never debit/prepaid; disclosed up front to the customer before they choose card and itemized on the receipt; auto-suppressed where a state disallows it. NOT legal advice — the StateCardRule table is a platform-maintained compliance reference that must be kept current and verified against current law, card-network rules, and the processor’s surcharge program. Folded into Data Spec v0.2 (§05 Corporation/Customer/Invoice/Deposit/StateCardRule entities; §06 customer types→QBO terms, deposits, parent/child PMs, invoice-at-call gate, surcharge subsection, billing backlog). All prior invoicing/terms decisions also folded in. Principle reaffirmed: all decisions documented before work begins.fe:→tenant data migration plan; notifications + TCPA/SMS consent; sales-tax calculation across jurisdictions (integrate a tax service); compliance records (refrigerant/EPA 608 logs, license & insurance expiry). Tier 2 (elevate into the phase plan): customer portal (pay / approve quote / service history) + customer communications (reminders / on-the-way / review asks) — both directly serve the prompt-payment goal; time tracking + tech mobile day (route/next-stop/on-my-way) in the scheduling phase; recurring-agreement scheduling & renewals; warranty + callback/comeback tracking; analytics/reporting (DSO, close rate, revenue/tech, AR aging). Tier 3 (backlog): lead pipeline/CRM, financing, referral/marketing attribution. Build-vs-buy reminders: SMS (e.g. Twilio), tax (e.g. Avalara/TaxJar), financing, portal auth = integrate. Safety veto-flags: no SMS without consent capture; no tax/surcharge correctness claims without a real rules source; AI triage stays “a licensed human decides.” Field Tech: new office features must degrade gracefully offline. Moralist: tenant data export on offboarding + end-customer right-to-delete baked in; commission-vs-repair/replace guardrails. TODO: fold Tier 1 into Data Spec as new sections (File & media storage; Auth, privacy & retention; Notifications & consent; Tax; Compliance records; Reporting; Testing & environments); place Tier 2 in phasing; then start Phase 0.fe:→tenant adoption; (5) SMS consent (TCPA, transactional-vs-marketing split, STOP, email fallback, provider); (6) integrate a tax service; (7) refrigerant 608 log (Sidekick-fed) + license/insurance expiry alerts; (8) customer portal → P4; (9) comms reminders/on-the-way → P3, review asks → P4; (10) GPS clock+geofence → P3, continuous breadcrumb = company-device-only, manual notice, on-shift, short retention; (11) recurring agreements auto-renew by default + advance notice/cancel; (12) warranty + callback tracking; (13) reporting Site→Corp→platform. Hierarchy corrected: Super-Admin = software provider (not a customer) › Corp Admin (customer ceiling, corp-wide) › Site Admin (single site) › Office/Bookkeeper/Tech; recovery split Super-Admin cross-tenant vs Corp Admin own-corp. Device fleet = corp-managed assets (P1), continuous tracking gated on corporationOwned. Lead pipeline (merges CRM + #16): multi-source intake (website, email-forward, social/AI, referral), source+referrer attribution, referral-payout ledger; email-forward+web first; own phase (P5). Financing backlog. Spec grew to 19 sections; phasing updated (0 interface → 1 tenancy+devices+auth+storage → 2 Quartermaster+compliance → 3 scheduling+time+comms+agreements → 4 money+QBO+tax+portal+warranty → 5 pipeline+reporting). Sequencing: spec-first, then Phase 0.Empower-Toolbox-Baseline-v1.0_2026-06-18.zip (28 files). Captures Evaluator v0.12.19, Sidekick v0.19.0, Concierge v0.2.0, Backend v0.15.0, Hub v1.0, Data Spec v0.3.1, Master Plan (108 decisions), plus all manuals/guides/training/flowcharts/service-plans and the build scripts. Contains a deployable site/ (drop-in restore target), a RESTORE-README (versions, redeploy steps, env vars, working↔deployed filename map) and a MANIFEST with SHA-256 integrity hashes. This is the last single-tenant/local-first version; all forward (multi-tenant) work happens on copies, never on this package.core.css (design tokens — the :root palette + base .btn/.note/.sec) and core.js (window.Core: BRAND, SUITE registry, Store [localStorage+memory], Net status, a configurable Backend client via makeBackend(cfgProvider), pure helpers esc/num/money/fmtPhone, a shell header, and an auth/role-gate SCAFFOLD — the 6-role model + Auth.can(minRole), defaulting to a local owner session, flagged isPlaceholder until Phase 1 wires Supabase Auth). core.js also exposes bare globals (Store/BRAND/SUITE/Net) for incremental app adoption. Pilot: Hub converted (v1.0→v1.1) to load core.css/core.js and drive all brand text from Core (rebrand once in core.js → hub updates), with the original static text kept as fallback if core fails to load. Headless-tested: exports, helpers, Store roundtrip, Auth ranking, backend availability all pass; hub no longer defines its own tokens. Deploy note: core.css and core.js are NEW root files — add them to the repo root alongside index.html. Next: convert the Evaluator to core (its own regression-tested pass), then Sidekick + Concierge.Store, Backend, Net, BRAND, and SUITE definitions deleted (−53 lines) and re-pointed at window.Core (const Store = window.Core.Store, Backend = window.Core.makeBackend(…, 45000) preserving its 45s timeout, etc.). Loads <script src="core.js"> before the app script. CSS deliberately left untouched — the Evaluator’s palette differs from core (--cool is bluer, plus app-only --warn/--inv), so unifying tokens is a separate design decision, not this mechanical refactor. Syntax-checked + headless-booted: all five objects bind to the shared Core (Store===Core.Store etc.), backend client functional, boot title + home render produce output — no regression. Now suite-coupled: the Evaluator requires core.js at the site root (already in the bundle). Next: Sidekick, then Concierge.Store/Backend/Net/BRAND/SUITE deleted (−35 lines) and re-pointed at window.Core (Backend = window.Core.makeBackend(…, 60000) preserving its 60s timeout; core also fixes a stale “over 45s” message by deriving it from the timeout). CSS left untouched (Sidekick palette differs from core). Safety-critical regression done: headless boot verified the safety logic survives the refactor — GATE_DEFS (12), UNIVERSAL_STOPS (3), CONDITIONAL_STOPS (2), SAFETY_FLAGS (2) all load, gateLabel() resolves, and App.triggerUniversal/triggerConditional are intact; title “Empower Sidekick” + home render produce output. Now suite-coupled (needs core.js at root; already in bundle). Only Concierge remains.callBackend (honeypot _hp, submit-timing _t, structured address, 45s) is deliberately left untouched; only BRAND was re-pointed at window.Core.BRAND, which matters because this is the customer-facing page — a reseller rebrand in core.js now flows to the public intake’s company name + phone. Verified: BRAND===Core.BRAND, PHONE_DIGITS derived correctly, callBackend still intact, emergency-first safety screen renders. Phase 0 outcome: the copy-paste-core tax is gone — Hub, Evaluator, Sidekick, Concierge all read brand/Store/Backend/Net from one core.js (+ core.css on the Hub). All four are now suite-coupled (require core.js at the site root; it ships in the bundle). CSS-palette unification deferred (apps have intentionally different palettes — a design decision, not a refactor). Next: Phase 1 — tenancy spine + Supabase Auth (replaces the Core.Auth isPlaceholder scaffold) + device fleet + object storage; or a CSS-token unification pass; or start Quartermaster.corporation_id (one Supabase project) after a cost check: startup and year-1 cost is identical (~$0 diff, ~$300/yr) across all isolation models for tenant #1; only physical project-per-tenant isolation adds cost later (~$10–$60/mo per tenant), and shared-RLS stays flat — so it’s both cheapest-at-scale and the locked choice. Schema-per-tenant rejected (same $ as shared-RLS but more ops overhead, no payoff); project-per-tenant reserved for a future customer whose compliance contract demands it (and funds it). Supabase pricing confirmed Jun 2026: Free $0 (pauses after 1wk idle), Pro $25/mo, Team $599/mo. Tenant isolation is treated as a safety/integrity property (Safety + Moralist): cross-tenant bleed = liability; impersonation guardrails (consent/cause + audit + banner) designed in from the start; field safety path still runs offline/logged-out (veto intact). Device-fleet/GPS + resale storefront deferred. Now building: the tenancy-spine schema + RLS + audit, validated by a real isolation test.db/). Four ordered migrations: 001_foundation (tenancy spine — corporation › site › profile, app_role enum, updated_at triggers, a check that only superadmin has a null corporation), 002_rls (recursion-safe SECURITY DEFINER helpers — current_corporation_id()/is_superadmin()/is_corp_admin() — + full CRUD policies on the spine; provider-only writes to corporation), 003_audit (append-only audit_log [no update/delete policy] + impersonation_session that requires a reason and is visible to the impersonated tenant), 004_operational (survey/repair_job/intake_request — typed columns the owner dashboard queries + a jsonb payload so the field-app sync shape can evolve; repair_job carries had_hard_stop/hard_stop_summary so safety surfaces to the owner). Plus a local test shim, a two-rival-tenant seed, and a one-command isolation test (run_isolation_test.sh) asserting A can’t see B, cross-tenant insert is blocked, and the provider sees both. Validation: structural lint passed (parens/dollar-quote balance, RLS on all 8 tables, append-only audit, SECURITY DEFINER helpers, impersonation guardrails) + design review; the behavioral RLS test must run once on a real Postgres/Supabase (this sandbox had no PG — apt + pgserver/pglast were proxy-blocked). R2 photos are tenant-prefixed (/<corporation_id>/…) so isolation extends to files. Next: wire Supabase Auth into the Core.Auth scaffold (replace isPlaceholder), add a field→cloud sync path, and build the owner dashboard read-view.Auth.isPlaceholder now false). Core.Auth is now real and additive: when configured + online it signs into Supabase (signIn / signInWithLink magic-link / signOut / restore), loads the caller’s profile row, and session() returns {userId, email, corporationId, role, name, siteId}; can(minRole) uses the role rank. When unconfigured OR offline it falls back to a local session so the field apps and the safety path never hit a login wall (Safety veto honored). supabase-js is lazy-loaded from CDN only when real auth is invoked, so field-only pages never pay for the SDK. Config via window.CORE_CONFIG (supabaseUrl + public anon key; RLS protects data, not the key) auto-read on load; shipped core-config.example.js. Validated headless with a mock Supabase client: local fallback → configure → signIn maps profile to corporation_id+role → can() correct → signOut/restore fall back to local; all four apps still boot on the new core (no regression). Next: a hub login screen (role-aware), the field→cloud sync path, and the owner dashboard read-view.Core.Auth: Local mode (Supabase not configured — cloud off, hub works exactly as before), Not signed in (configured — offers Sign in), and Signed in as {name} · {role} (+ Sign out). Sign-in panel does email+password (signIn) or magic-link (signInWithLink); restore() runs on boot to pick up an existing session. Role-aware gating: the internal planning docs (Master Plan, Technical Reference, Data Spec) are marked admin-only and shown only when Auth.can(site_admin) — a signed-in tech doesn’t see them; admins and local mode do. Loads an optional core-config.js before core.js (404 is harmless → stays local until the deployment drops the file in). Headless-tested all four states + gating render correctly; local mode unchanged. Next: field→cloud sync, then the owner dashboard.Core.Sync: push(table, rows, {onConflict}) upserts and stamps corporation_id from the SESSION (a spoofed corp in a record is overridden — a compromised client can’t write into another tenant), plus pull() for dashboards. Both field apps push on boot + on the online event + debounced after each save: Evaluator → survey (customer/address/status/proposal_total), Sidekick → repair_job (equipment/status/invoice_total + had_hard_stop/hard_stop_summary so safety surfaces to the owner). Additive + safe: runs only when signed in + online, never blocks, swallows errors, and is off the safety path beyond a debounced timer. Photos/signatures are stripped before upload (base64 belongs in R2, not a JSON column) — verified no base64 leaks into the payload. Local ids aren’t uuids, so migration 005 adds local_id + a unique (corporation_id, local_id) index and sync dedupes on that (the cloud keeps uuid PKs). Headless-tested both mappings + tenancy stamping + offline/signed-out skips; Sidekick safety structures still intact (12 gates / 3+2 stops). Next: the owner dashboard read-view; and wiring the Concierge backend to write intake_request (so intakes show too).survey + repair_job + intake_request for the current corp (RLS-scoped) and shows a Today / 7-day summary — surveys, open quotes, quoted value, accepted, repairs, safety hard-stops, revenue (done), and new requests (emergencies flagged) — plus lists with status tags, hard-stop badges, and an emergency badge. Uses Core.Sync.pull with a since-date filter; Refresh + range toggle. Added an admin-only “Owner Dashboard” link on the hub. Headless-tested: summary math (quoted $23,200, revenue $300), hard-stop + emergency surfacing, all three gate states, and the empty state. Remaining Phase 1 piece: wire the Concierge backend to write intake_request to Supabase so customer requests populate the dashboard (currently only field-app surveys/repairs sync).support_request now also writes the request into Supabase intake_request (in addition to emailing the shop), so customer requests appear on the Owner Dashboard alongside field surveys/repairs. Additive + safe: it runs only when SUPABASE_URL + SUPABASE_SERVICE_KEY + DEFAULT_CORPORATION_ID are set, is wrapped in try/catch so a sync failure never blocks the request, runs before the email early-return (so it records even when email is off), and does not upload photos (base64 stays out of the JSON column; R2 later). The service-role key bypasses RLS and corporation_id is stamped from env (single-tenant now; the multi-tenant Concierge link will carry the corp later). New env vars documented in BACKEND_SETUP. Phase 1 build-complete: schema+RLS, Auth, field→cloud sync, dashboard, and intake→cloud are all in. What remains to go LIVE is user-side (create the Supabase project, apply migrations, run the isolation test, drop in core-config.js, set Vercel env vars, create the corporation row + first admin) — captured in the GO-LIVE runbook.ISOLATION VERIFIED, insert the Empower corporation row + first corp_admin profile (with exact SQL), drop core-config.js (anon key) at the site root, set the three Vercel env vars (incl. service key + DEFAULT_CORPORATION_ID), then a 4-point end-to-end verify. Includes a key/secret cheat-sheet (anon = browser, service = server-only) and a clean rollback (remove config → back to local). Also fixed db/README to list all five migrations. Phase 1 is build-complete and documented for activation.part, stock_location (shop/warehouse/truck), stock_level (cached on-hand), append-only stock_movement ledger (+ suppliers/POs in 2.1). Sidekick’s part picker becomes the consumption capture point (additive/offline-safe, off the safety path), which also lets true parts cost flow into repair profitability. Six decisions flagged ⟐ PROPOSED — needs ratification before any build: (1) ledger-derived on-hand, (2) moving-average costing, (3) trucks-as-locations with optional lightweight consumption, (4) loose catalog↔job coupling with later reconciliation, (5) QBO owns financial valuation / Quartermaster owns operational stock, (6) manual + CSV catalog seeding. Pivotal two = #2 costing and #4 coupling (they shape the schema + the Sidekick change). Hard stop: awaiting ratification of §4 before writing 006_inventory.sql + the app shell.part, stock_location (shop/warehouse/truck), stock_level (cached on-hand), append-only stock_movement ledger; RLS on all four (corp-scoped reads; office+ manage catalog/locations; movements append-only with a tech-can-write-from-own-truck clause); linted (parens balanced, 4 tables, append-only enforced). 007_inventory_ledger.sql — triggers that resolve client local_ids→uuids and project the ledger into stock_level + moving-average avg_cost (the ratified ledger-derived design), SECURITY DEFINER, re-runnable. quartermaster.html — local-first single-file app on core.css/core.js: catalog + CSV import, locations, receive (moving-average), transfer, adjust, on-hand view with low-stock banner, activity ledger; additive cloud sync of part/stock_location/stock_movement (carries local-id refs for 007 to resolve). Added a Quartermaster hub tile (visible to all; office-side gated in-app later). Tested: headless — moving-average (9.50→10.50), on-hand across locations, transfer/adjust, low-stock, sync mappings; the isolation test now also functionally proves the ledger triggers (seeds a receipt → asserts on-hand=6, avg_cost=9.50, rival sees 0). Behavioral SQL still needs the user’s real-Postgres run (no PG in sandbox). Deferred follow-ons: dashboard low-stock card (cross-surface owner view), Sidekick part-picker → consumption capture (the §6 coupling), suppliers/POs (2.1), QBO COGS export (2.2).Core.Inventory so Quartermaster AND Sidekick consume through one path (no local/cloud on-hand drift). Quartermaster now delegates to it (regression-tested identical). Sidekick: when a job closes, recordPartConsumption records the installed part (j.part.selected.spec) as a consumption drawing from the tech’s truck — loose-coupled (matches a catalog SKU by text, else records unlinked with part_text for office reconciliation), lightweight (qty defaults 1), and it never gates completion (runs after status=done + save, fully try/caught, once-per-job guard). True parts cost can now flow to the cloud ledger. Also fixed a latent Phase 1 gap: the field apps never loaded core-config.js or restored the session, so their cloud sync was dormant — both Evaluator (v0.12.22) and Sidekick now load config + call Auth.restore() on boot (picks up the session a tech created on the hub, same-origin), which activates survey/repair/inventory sync. Local/offline + safety paths unchanged. Tested: consumption draws truck 5→4, matched + unlinked paths, double-record guard; full field regression green (Evaluator+Sidekick sync, 12 safety gates intact). Deferred: dashboard low-stock card; suppliers/POs (2.1).part + stock_level (RLS-scoped), sums on-hand per part, and surfaces a Low stock card (alert-styled when >0), a Stock value card (Σ avg_cost×on-hand), and a “Low stock · reorder” worklist listing each low part with on-hand / reorder-point and the suggested order qty — the actionable list that turns reordering from memory into a worklist. Defensive: the inventory pull is a separate try/catch, so a pre-inventory or pre-migration project shows the original dashboard untouched (no card, no section). Headless-tested: low-stock math (1 low of 2; stock value $103), worklist contents (2/4, order 10), non-low part excluded, and the no-inventory dashboard unchanged. Quartermaster loop now visible end-to-end: receive → truck → consume-on-a-job → dashboard reorder alert. Remaining Phase 2: suppliers/POs (2.1), QBO COGS export (2.2).Core.Inventory.receive() so moving-average + on-hand stay one code-path. (1) Receipt capture (the field path): a tech photographs a supplier receipt → new token-gated backend action receipt_read (Anthropic vision) extracts supplier/date/line-items → the tech reviews/edits the parsed lines, picks the destination (defaults to their truck), and confirms → stock added. Unmatched lines auto-create a catalog part; a read supplier name auto-creates a supplier. (2) Purchase Order (office path): create a PO (supplier, lines, destination), status draft/ordered → Receive writes the receipts. Core.Inventory gained suppliers/POs/po-lines model + commitReceipt/receivePO/findOrCreatePart + cloud push of supplier/purchase_order/po_line (local refs in payload). 008_purchasing.sql adds supplier, purchase_order, po_line (corp-scoped RLS, office+ manage, techs may create a field receipt-run); linted, added to the isolation-test migration loop (now 8 migrations). New Quartermaster Buy tab (Scan receipt / New PO / Add supplier + PO list with Receive). Decisions documented in spec first (PO-optional capture, human-confirms-AI, unmatched→create, per-receive location, photos not stored in the ledger). Tested: commitReceipt (matched SKU + new part + subtotal), receivePO (draft→received), full app flow (scan→review→commit, PO→receive), sync mappers; full regression green incl. 12 safety gates. Remaining: QBO COGS export (2.2); receipt photo → R2.vehicle, expense + RLS: employees create/see own, approvers see all), backend expense_read + odometer_read vision actions (never extract full card numbers), paymaster.html app, HUB tile, dashboard pending-approvals card; QBO export + R2 photos in v2. Hard stop: awaiting ratification of the ⟐ decisions (pivotal: #1 new “Paymaster” app, #2 dashboard-placard vehicle ID, #4 single-step approval) before building 009 + the actions + the app.expense_read) reads merchant/date/total/category (+fuel gallons), the employee confirms, and an approver runs single-step approve / reject / needs-info; personal-card approvals become reimbursements. Fuel auto-assigns to the truck: a second photo of the dashboard → odometer_read reads the odometer + the fleet placard number → matched to a vehicle (manual fallback), giving per-truck fuel cost/mileage. 009_expenses.sql: vehicle + expense, corp-scoped RLS (employees create/see their own; approvers see all) + a self-approval guard trigger (a non-approver can’t move their own expense to approved/reimbursed at the DB level); linted, in the isolation-test loop (now 9 migrations). Backend never extracts full card numbers (last-4 only). New paymaster.html (Capture / Mine / Review / Vehicles, role-gated) + HUB tile + dashboard Pending approvals & Reimbursements due cards and an awaiting-approval list. Tested: fuel capture (mocked AI → placard→vehicle match → submit), approve/reimburse transitions, sync mappers, dashboard surfacing, non-approver tab gating. v2: QBO export, card-statement reconciliation, MPG reporting, receipt photos → R2.tool_event ledger, current holder/location derived; (3) calibration-due + out-of-service are first-class — a damaged or uncalibrated tool can’t be checked out (Safety veto), and any employee can take a tool out of service but only office+ can return/repair-complete/calibrate/retire it; (4) straight-line depreciation capture → computed book value for the insurance/asset view, QBO export later; (5) asset-tag/valuable flag for a “where are the expensive tools” view (QR scan v2). Build v1: migration 010 (tool + tool_event + projection/guard trigger), Core.Inventory tool model + ops + bookValue + canCheckout + sync, Quartermaster Tools tab, optional tool_read setup action, dashboard “tools needing attention” card.tool (name, asset tag, serial, make/model, category, purchase cost/date, useful-life + salvage for straight-line depreciation, warranty provider/expiry/terms, valuable + safety flags, calibration_due, status) + append-only tool_event ledger; a SECURITY DEFINER trigger projects the latest event into the tool’s cached holder/location/status, and a guard blocks non-office from repair-complete/calibrated/retire (a tech may take a tool OUT of service but only office+ puts it back). Linted; in the isolation-test loop (now 10 migrations). Core.Inventory gained the tool model + ops (checkout/return/move/use/damage/repair/calibrate/retire), bookValue (straight-line), canCheckout (blocks if out-of-service, already out, or a safety tool is calibration-overdue), and tool/tool_event sync. Quartermaster Tools tab: list (status, holder/location, valuable/safety/calib flags, book value), rich add-tool form, point-of-use checkout/return/flag-damaged/repair/calibrate/retire (office-gated where required), and per-tool history. Optional backend tool_read (nameplate/serial at setup). Dashboard adds Tools + Tools needing attention cards and a list. Tested: 21 model checks (book value, custody derivation, damage→out-of-service blocking, safety calibration-overdue block, office guard, mappers), 11 Tools-tab UI checks, dashboard surfacing, and full regression (inventory, purchasing, Sidekick consumption + 12 safety gates, Paymaster) green. v2: QR/asset-tag camera scan, QBO asset export, maintenance scheduling, photos→R2.count_items) tallies the like-items; the tech confirms or edits, and on Post each line writes an adjustment movement tagged cycle_count (variance = counted − system) on the existing ledger, with a session variance summary. Ratified guardrails: AI counts, it does not identify — part identity comes from the catalog (AI returns a generic label + confidence, never a precise HVAC rating); nothing auto-posts — every count is a draft a human confirms (Safety); one part per photo in v1 (multi-SKU shelf detection = v2). Core.Inventory.countTo() sets on-hand to the counted value and posts the tagged adjustment; reuses stock_level + the movement ledger so on-hand reprojects with a full audit trail — zero new tables. Backend count_items (count + generic label + confidence; same API key). Tested: countTo variance math (over/under/zero), cycle_count audit movements, and the full flow (mocked AI tally → confirm → post → variance, including “stock unchanged until Post”); regression green. v2: multi-SKU shelf detection, a count-session report table, placard-in-frame location read, QR part scan.review_request (auto, off the safety path, never blocks; manual “Send review request” button too). A Vercel Cron (api/review-cron.js, every 15 min) emails it ~1 hour later via Resend, linking to a Concierge-style public review.html (“How did we do?”). No review gating (Safety veto-backed + FTC 2024 / Google policy): every customer is shown the one-tap Google review link AND a private “tell the owner directly” path — the public link is never withheld based on the rating; sentiment/rating/feedback are recorded for the dashboard. Per-tenant Google URL lives in corp_settings.google_review_url (set on the Dashboard; corporation table stays superadmin-locked). 011 adds review_request + corp_settings with corp-scoped RLS (one-request-per-job unique); public token ops run through the backend service key (review_get/review_respond are PUBLIC actions). Core gains Core.Reviews (one-per-job, tokenized, sync). Dashboard adds Review responses, Avg rating, Needs follow-up cards + a recent-feedback list. Email v1; SMS v2 (needs a provider + a TCPA consent checkbox at intake). New env: PUBLIC_BASE_URL, CRON_SECRET. Tested: Core.Reviews one-per-job + mapper, the public page flow incl. the no-gating compliance check (Google shown even on a 2★), dashboard surfacing, and full regression (incl. Sidekick’s 12 safety gates) green. Now 11 migrations.qbo_exported_at stamps on expense/purchase_order/repair_job/tool (a second export can’t double-post), an immutable append-only export_log (type/period/rows/total), and per-corp account mapping in corp_settings.qbo_accounts edited on the Dashboard. Never auto-posts — generate → the bookkeeper reviews → import. New Core.QBO pure builders (CSV + depreciation math) keep it testable; depreciation is period-based (logged, not row-stamped, since it recurs). Tested: 14 builder checks (filtering, account mapping, CSV escaping, balanced depreciation JE, idempotency, unmapped-account fallback) + dashboard render/handler wiring (download, mark-exported, log) + full regression green. Now 12 migrations.Sync.registerPusher/markDirty/flushAll registry; Inventory, Reviews, and the field apps (Evaluator/Sidekick/Paymaster) register so survey/repair/expense/inventory/tool/review data all self-heal after offline. Added a service worker (sw.js) + web manifest (registered by core.js) so the apps load offline (cache-first shell; /api + Supabase + AI always network). Cloud pushes stay idempotent (no double-post); AI features degrade to manual entry offline. Tested: reconnect flush, offline no-op keeps data queued, dirty gating, registry de-dupe; full regression incl. Sidekick’s 12 safety gates green.qbo_connection = per-corp encrypted OAuth tokens, service-role only via RLS deny-all so tokens never reach a browser; qbo_entity_id on expense/PO/repair/tool for idempotent updates; corp_settings.qbo_account_ids); api/qbo.js with tested pure helpers — authorize-URL builder, AES-256-GCM token encrypt/decrypt round-trip, and four entity mappers (expense→Purchase, PO→Bill, depreciation→balanced JournalEntry, repair→SalesReceipt); a token-refresh cron (api/qbo-refresh.js, 6h, keeps the 101-day refresh token alive); and a dashboard "QuickBooks live sync (beta)" panel (Connect / Check / Push). Network boundary marked "validate live" (OAuth handshake + QBO API POSTs are written against Intuit’s documented API but never executed). Deploy via QBO-DEPLOY-RUNBOOK.md — Intuit app + questionnaire, env vars (QBO_CLIENT_ID/SECRET/REDIRECT_URI/ENVIRONMENT/TOKEN_KEY + CRON_SECRET), sandbox validation, first-contact checklist. Single-company now; resale (per-realm tokens) is v2b. File export remains the proven path until validated. Now 13 migrations.Core.Sms pure helpers (E.164 normalize, STOP/HELP detection, quiet-hours window, the send GATE) — 17 checks incl. every gate outcome. Backend: api/sms-cron.js sends due SMS review_requests behind server-authoritative gates (suppression → consent → quiet-hours, all fail-safe) — 8 cron-gating checks confirm only a consented, unsuppressed, in-window number sends; api/sms-inbound.js honors STOP (global suppression + auto-reply) / HELP — 4 checks; api/sms-consent.js records Concierge consent via service role. Consent capture: dedicated SMS checkbox + disclosure in Concierge (separate from generic consent), and a "customer agreed to texts" path in Sidekick’s review block (off the safety path; 12 gates verified intact). Dashboard SMS panel: enable/from/quiet-hours/consent-text + consent/opt-out/log visibility. Validate-live boundary: the Twilio send + inbound signature verification are unexercised (no provider network here); owner does Twilio + A2P 10DLC brand+campaign registration (~1wk, carrier-blocked if unregistered) + consent-language sign-off per SMS-DEPLOY-RUNBOOK.md before flipping sms_enabled. TCPA exposure $500–$1,500/text — system refuses to send without a stored consent + open opt-out. Now 14 migrations.settings.office.commissionPlan. The rep’s "Where I stand" screen computes from their own sold deals (status accepted): month-to-date booked GP → current tier → retroactive commission, the next-sale retroactive bump, quarterly spiff progress, the tier table, and this month’s jobs. Factors are adjustable by manager/office (canSeeCost) inline; salespeople view only. Consumer-safety (the requirement): removed the commission “$” button from the persistent header (it had shown on customer-facing survey/proposal screens); entry now only from Home; and a tap-to-reveal guard ("Not for customer view" → "Show my numbers") that re-hides whenever the rep navigates away, so numbers never display unattended. Tested headless (11 checks): header has no $ button, guard hidden by default, $36K→GP→28%→$10,080 retroactive math, re-hide on exit, admin config updates. Standalone commission-modeler.html stays as the owner’s what-if sandbox; the Evaluator screen is the rep’s live standing + the live plan config. Per-rep cloud roll-up across the team is still the future Dashboard commission tracker.settings.techBountyPlan + per-tech callbackRate. Eligibility is derived from the existing repair-vs-replace advisor (must show crossover) and hard-excludes any safety condemnation: red-tag, hard stop, or advisor safetyReplace → never a bounty (Safety veto enforced in bountyEval). The fee attaches to the existing Sidekick→Evaluator handoff (originatedByTech, bountyEligible in payload + j.bounty), pays on installed & collected with clawback on cancel (reconciled downstream). New private "My referrals" tech screen: qualifying handoffs, projected bounty, integrity-gate status, safety-excluded jobs shown as such — entry from Home only, with the same tap-to-reveal / "not for customer view" guard that re-hides on navigation. Integrity gate reads an office-recorded callback rate (provisional until automated callback tracking exists — a noted future module). Tested: 14 checks incl. all safety exclusions + 12 hard-stop gates intact. Counterweight (reward the right repair-first call via a tracked quality metric) still recommended; pairs with the gate. Folds into the roll-up as an originated_by_tech payout line.techBountyPlan.callbackWindowDays) and the ceiling % (default 8). This computed rate replaces the manual callbackRate field (manual kept only as a no-data fallback); part failures, old-system second failures, no-fault, and safety-handled jobs never count against the tech. Migration 015_callbacks: callback table links original job + original_tech_id, six-category check, attribution_confirmed (office-only update via RLS), corp-scoped; lint-clean, in the isolation loop (now FIFTEEN migrations; README + GO-LIVE updated). Sidekick syncs callbacks to the cloud (callback table) alongside repair_job. New "Callbacks (quality)" screen from Home: rate card, one-tap log form, and the logged list with per-callback category + office confirm. Tested: 13 checks (log→unconfirmed→office-confirm flips it; only confirmed workmanship+misdiagnosis count; part/second-unrelated excluded; window in/out; computed rate drives the gate; window+ceiling adjustable) + 12 hard-stop gates intact. Cross-device office-confirm workflow + the Dashboard roll-up remain the future module.Empower-HVAC-Inventory-Top250.xlsx (master — Parts sheet with universal flag + "covers/replaces" coverage notes + failure tier + truck/shop + est. cost + suggested par + a cost×par stocking formula; "Carry less, cover more" summary with the hero consolidators and category counts; Read-me) and Empower-Inventory-Import.csv in Quartermaster’s exact name, sku, category, cost, reorder_point format. Tested: all 250 load through Quartermaster’s real CSV importer across 29 categories; xlsx recalcs with 0 formula errors; names sanitized comma-free for the naive importer. Est. cost & par are explicit placeholders — owner replaces cost with distributor pricing and tunes par after real consumption. A2L (R-454B/R-32) stocked only once techs are A2L-trained; compressors special-order. In bundle under data/.aliases, findPartByText matches them (so Sidekick consumption can deduct a free-typed nickname), and a new Core.Inventory.search() powers a catalog search box in Quartermaster that matches name/SKU/category/nickname. Migration 016 adds part.aliases (idempotent; now SIXTEEN migrations) and aliases sync to the cloud. Quartermaster’s CSV importer gained an optional 6th aliases column (pipe-separated), the Add-part form has a Nicknames field, and the catalog shows "also:" nicknames per row. Generated an 84-item shop-stock list (fasteners incl. 1/4in/5/16in/zip/self-tapping screws, tapes & sealants incl. foil tape, mastic/foil-mastic tape, duct tape, and mastic = "pookie", plus chemicals, insulation, electrical, duct hardware, shop misc) with 264 nicknames — e.g. pookie→mastic, zips→self-drilling screws, foil→foil tape, marrettes→wire nuts. Deliverables: Empower-Shop-Stock.xlsx (Nicknames column + Read-me) and Empower-ShopStock-Import.csv (name, sku, category, cost, reorder_point, aliases). Tested: 11 checks (pookie/zips/foil/marrettes search hits, nickname-driven findPartByText for consume, catalog filter + empty state, 84-item aliased import) + 12 hard-stop gates intact + QM/core suites green. Redeploy core.js + quartermaster.html. Crews can add their own slang in the Nicknames field anytime.reading action in lookup.js, validate-live; snap → read → tech-confirm), best-effort voice (Web Speech, parses “liquid 300 vapor 150 cap 44.8”, photo/tap fallback). Safety routing: CO readings write into j.combustion so the existing combustionDanger() hard-stop fires — never just logged. Migration 017 reading table (job-linked, pass/source checks, corp RLS; now SEVENTEEN migrations). Syncs readings to cloud. Glasses shelved (phone-only) per decision; input layer kept source-agnostic for later. Tested: 17 checks (unit vs generic resolution + banner, grading incl. cap ±6%/voltage ±10%/static/CO, CO→combustionDanger routing, voice parse, render) + 12 hard-stop gates intact. Increment 2 (next): wire the same engine inline into the measurement-anchored fault-tree steps so targets/capture appear at the moment the flow asks for each reading. Redeploy Sidekick + lookup.js.TREE_SLOT) so the TREES decision/gate logic is untouched; treeChoose still reads the same input and its branching is unchanged, but now also logs the captured value into j.readings (with source) so it flows to the standalone Readings screen + cloud. Value/source reset on advance/back. Tested: 10 inline checks (unit-specific target at run-cap, Snap/Speak present, PASS/FAIL badge, reading logged with slot+source on choose, treePath behavior intact, flame-sensor generic target, non-measure nodes unmapped) + increment-1 engine (17) still green + 12 hard-stop gates intact. Redeploy Sidekick.commissionReportHTML + printCommissionReport, modeled on the existing customer doc): brand header, customer/equipment, an overall verdict banner (“Commissioned to spec” / “outside target” / safety), and Before/After tables with target, measured, and pass/warn/fail — printable/shareable as proof-of-work. Snap-captured gauge photos are retained on the reading and shown in the report. Review tie-in reuses the existing gated flow: a “Request a review” CTA appears on the report only once the job is signed off (status done), never on red-tag; SMS stays on the completion summary where the consent checkbox lives. Tested: 12 report checks (before/after split, verdict math, report HTML sections + values + customer, all-in-spec vs out-of-target banner, photo in report, footer + phase toggle, review CTA gating) + increments 1 (17) & 2 (10) green + 12 hard-stop gates intact. Redeploy Sidekick. This completes the readings feature: standalone capture, inline-at-the-step, and the commissioning report. Future (optional): cross-device office roll-up of readings + photo persistence to R2.reading table and adds a “Commissioning & readings” panel — in-spec %, out-of-target and safety-flag cards, per-job verdict rows (joined to repair_job for customer/equipment/tech), and an out-of-target list — giving cross-device visibility into how systems are being commissioned. Part B — photo persistence: gauge photos captured on the Readings screen now upload to Cloudflare R2 (S3-compatible) via a new server-side r2_upload action in lookup.js; the SigV4 signing chain is unit-verified against AWS’s documented test vector, the PUT itself is validate-live (real bucket). The reading stores photo_url (migration 018, eighteen total), readingToRow syncs it, and the commissioning report prefers the persisted R2 URL over the local thumb so photos survive beyond the session and show on any device; graceful degrade to a local thumbnail if R2 isn’t configured. Credentials stay server-side (env: R2_ACCOUNT_ID/R2_BUCKET/R2_ACCESS_KEY_ID/R2_SECRET_ACCESS_KEY/R2_PUBLIC_BASE). R2-DEPLOY-RUNBOOK.md added. Tested: 10 roll-up checks (cards, per-job verdict + tech join, out-of-target list, empty state) + 2 photo_url plumbing checks + SigV4 vector match + 12 hard-stop gates + all prior readings increments green. Redeploy dashboard.html + Sidekick + lookup.js; apply migration 018.Core.Readings; Sidekick delegates to it (local names kept, all call sites + 12 hard-stop gates intact, behaviour identical — 17+10+12+2 readings checks green), and combustionDanger/combustionElevated now read the thresholds from MEAS_DEFS so there is one definition of “dangerous CO” across Repair + Install. (2) Foreman rebuilt on Core.Readings — its lightweight mirror is gone; commissioning grades + the do-not-leave-operating block now run the same engine (coDanger). (3) install_job cloud table (migration 020, corp RLS, twenty total) + sync: saveJob pushes, load pulls + merges for cross-device/office visibility. (4) scope snapshotted at hand-off into the install_job (immune to later survey edits). (5) warranty = serials captured, office files. (6) closeout → QBO: completion sets balance_due + flips status to installed and syncs; the office invoices the 40% (validate-live). A CO danger reading sets red_tagged and blocks sign-off. Tested: 10 Foreman checks through the real shared engine + full Sidekick regression + dashboard/admin/Evaluator clean. Follow-ons logged: migrate Foreman readings into the shared reading table + R2 photos; extract the commissioning report to core for reuse at closeout; server-side QBO trigger on installed+balance_due. Redeploy core.js (all apps) + Sidekick + Foreman; apply migration 020.commissionReportHTML(rj) now live in core; Sidekick delegates all of them (12 gates + 17+10+12+2 readings checks identical). (5) Foreman readings rebuilt as rich records via the shared makeRecord (was a flat {slot:value} map): each capture now carries label/target/pass/phase, the install’s commissioning readings sync to the shared reading table (job_local_id = install id) so they appear in the Dashboard readings roll-up and can carry R2 gauge photos (Snap → OCR + r2_upload, key installs/<id>/<slot>), and Foreman prints the same commissioning report at closeout via the shared core fn. A CO danger reading still red-tags + blocks sign-off. No new migration (reuses reading + install_job). Tested: 13 Foreman checks (rich records, report render, reading-table sync, safety) + full Sidekick regression + dashboard clean. Redeploy core.js (all apps) + Sidekick + Foreman. (R2 OCR/upload remain validate-live.)evap_frozen (iced coil / weak cooling — thaw-first, airflow before charge, charge gated to EPA-608, never “just add gas”) and condensate_leak (water leak — power-off-first, never bypass the float switch), with matching JOB_TYPES; 19 integrity + safety checks pass, 12 gates intact. (11) Scaffolded — pluggable vendor adapter (backend vendor_lookup): normalized shape with a mock (no fabricated price) so Pricing/Inventory can develop today; the live Ferguson call is env-gated and partnership-blocked / validate-live (VENDOR-API-RUNBOOK.md). Redeploy Sidekick + backend (lookup.js). 8/9 await ratify.?track=<token> → backend request_status → received/contacted/scheduled/completed timeline). Public boundary (Safety): server-side rate-limiting on the submit action (best-effort in-memory; durable KV is the production step), status read is token-scoped via the service role exposing only status (no internal data), the emergency pre-check is retained. Migration 021 adds public_token + intent to intake_request (21 total). Confirmations (SMS + email) are wired as validate-live. Tested: 6 client checks (intent fork, token capture, tracking link, status timeline, not-found) + backend status-mapping + rate-limit logic. Build-on 9 (Energy Story) is the next increment. Redeploy Concierge + backend; apply migration 021.buildEnergyStory: consolidated current annual spend by fuel, projected spend with the new system, a 15-year cumulative-savings series escalated by the rate trend, the low–high range, as-of rate, assumptions) and a “Publish & copy the energy story” button (publish_energy_story, token-gated) that returns an opaque link and copies it. energy-story.html is the public page: reads ?t=<token> → public energy_story action → renders the savings headline, current-vs-projected spend, per-fuel breakdown, an inline-SVG cumulative-savings chart, the assumptions, and a clear estimate disclaimer with the as-of rate; a “about the same yearly cost” branch keeps it honest when savings are negligible. Public boundary (Safety): the snapshot holds only customer-facing estimate figures (no cost, margin, or pricing internals); the table is read through the service role so it stays private; the link is opaque and read-only. Migration 022 adds the energy_story table (22 total). Tested: 8 page render/chart checks + 7 snapshot-builder checks (range, consolidation, projected = current − savings, 15-yr series, no-bills fallback). Redeploy the Evaluator + Concierge + backend + energy-story.html; apply migration 022. This completes the customer-facing products pair (8 + 9).survey table, and RLS gates rows, not fields inside a blob — so payload.pricing.cost / costBasis was readable by anyone who could read the row, making the UI gating cosmetic. Fix (two parts together): (1) the Evaluator now strips cost / margin / commission from the synced payload (targeted — never the customer’s own bill costs) and writes those figures to a new office-only survey_financials table; (2) migration 023 adds has_cost_access() (= sales_manager/office/site_admin/corp_admin/superadmin), RLS on that table that lets any corp member write but only cost-access roles read (a plain sales/tech JWT gets zero rows), enables RLS on all 30 base tables (defense in depth), and adds restrictive cost-access gates on purchase_order/po_line. The Dashboard reads GP/commission from survey_financials and degrades to sell-based estimates when a role has no access. RLS-ROLE-MATRIX.md documents every table’s scope, sensitive columns, and enforced-vs-follow-up status. Ratified deferral (flagged, not closed): cost still lives in the rep’s local storage because pricing runs on-device — that closes when pricing moves server-side (price-book work); the cloud hole is closed now, the device is not. Tested: 11 strip/financials-builder + 6 Dashboard-rewire checks; the isolation test gains a role-gating proof (a seeded sales session sees 0 financials rows, owner/provider see them) — run against real Postgres at go-live (no Postgres in the build sandbox). Twenty-three migrations. Redeploy the Evaluator + Dashboard + apply migration 023; validate-live: run the isolation test on Supabase and confirm a sales JWT is denied.price_book table (cost bases + margins, cost-access RLS — sales devices never read it); the backend gains a price engine — price_sheet returns a sell-only sheet (sellBase + floor per type×tier, add-on/electrical sells; no cost, no margins leave the server), and record_financials computes cost server-side from the book, enforces the floor (rejects below-floor sells — Safety’s ask, with only the sell-side floor returned), and writes survey_financials via the service role. Day-one defaults live in the backend DEFAULT_BOOK (exact parity with the prior on-device math — sellBase = round(cost/(1−margin)/50)×50) so switching the Evaluator over won’t move prices. Tested: 8 engine checks — parity across every tier/type, sheet carries no cost/margins/raw cost numbers, floor rejection, and record_financials leaks no gp/cost to the device. Twenty-four migrations. Next increments (staged): (a) wire the Evaluator to fetch/cache the sell sheet and price from it (closing the on-device cost hole for real) with server record_financials + fallback to the current model when no book; (b) the office price-book editor (cost-access only) to enter real prices + the Quartermaster cost link. Redeploy the backend; apply migration 024.fe:pricesheet) from the backend on connect, and prices from it instead of the hardcoded cost bases: each tier carries the server’s sellBase + floor with no cost on the device, so pricing.cost / margin are null on-device when a book is live — this is what closes the on-device cost exposure the RLS work flagged. Cost/GP for the office is computed server-side via record_financials (called on sync for proposed surveys), which also enforces the margin floor. Offline-first preserved: the cached sell sheet lets reps price with no signal; cost still never lands on the device. Graceful fallback: with no book/sheet the Evaluator uses the prior on-device model unchanged (and in that degraded path only, cost is on-device as before). Parity proven: the server sheet reproduces today’s prices exactly, so switching over does not move a single quote. Tested: 9 checks — fallback keeps cost + computes sell from it; with a sheet, tiers are sell-only, p.cost is null, sell still computes, and sheet sell === fallback sell; surveyToFinPayload emits the right config for server cost. Last increment (staged): the office price-book editor (cost-access only) to enter real prices + the Quartermaster cost link. Redeploy the Evaluator; backend + migration 024 already shipped.Core.Access.canEditPricing; sales/tech are turned away) to maintain the book: edit margins (target + floor), equipment tier costs with a live customer-facing sell preview under each, add-ons, electrical options, and save/reset — writing the price_book row (cost-access RLS permits). The Quartermaster cost link (build-on 7): a tier cost can be built from inventory parts (pick parts × qty → cost auto-sums from each part’s moving-average cost), and on save the resolved number is baked into the book so the backend engine still sees a plain number; an inventory cost-reference panel grounds manual entries in real costs. Added a Price Book tile to the HUB (admin-only). Tested: 7 checks — sell-preview parity, manual vs inventory-derived cost resolution, save bakes component cost to a number + pushes an engine-compatible book, margin edits move the preview. Build-ons 6–7 are now complete: server engine (migration 024) → Evaluator wired to the sell sheet (cost off device) → office editor + inventory link. The office enters their real numbers here; until then it runs on the seeded defaults at parity. Ship pricebook.html + the updated HUB.legalName already matches — no change needed. Mailing address deferred by owner; agreement/cancel-notice continue to show 8989 E 148th Circle, Brighton (placeholder) until the owner updates it in Settings. No code change.sidekick.html. Same power-basics front can be cloned to the AC / heat-pump / boiler trees next.sidekick.html.deriveFromSurvey runs after equipment identify/nameplate reads, the panel read, and the property lookup, and pre-fills empty energy/site fields from what the photos already revealed: heating fuel (from the furnace/boiler fuel, or heat-pump/baseboard type), cooling (central when an AC/heat-pump/condenser is present), ductwork "none" for baseboard-only homes, and the pre-1980 asbestos flag from year built. Per the council it is fill-empty-only — it never overwrites a manually entered value, asbestos only raises (never auto-clears), and inference is limited to directly-implied facts. Everything filled is listed in a confirm banner at the top of the survey ("Auto-filled from your photos — please verify") with a Got it dismiss, so nothing is committed silently. App-only — redeploy index.html.index.html. Access adders remain the next candidate for the same Settings-tunable treatment.vercel.json, so the serverless function hit Vercel’s default ~10–15s limit and was killed mid-search; (2) the rebates action allowed up to 9 web searches, routinely 30–60s. Fixes: added vercel.json with maxDuration:60 for api/lookup.js (gives web-search actions room); cut rebates to 5 targeted searches with an instruction to lean on the known Colorado figures and only confirm/catch local programs (leaner tokens too); and softened the app’s failure message — a timeout now says it’s keeping the built-in incentive estimates (which the proposal already shows by default), so it’s never a dead end. Note: maxDuration:60 requires Vercel Pro; on Hobby the function is hard-capped at 10s and the live search may still time out (built-in estimates always work). Redeploy api/lookup.js and add vercel.json at the repo root; redeploy index.html.comparisonScenarios): the like-for-like furnace + AC upgrade, the climate-appropriate heat pump (cold-climate for our market), and dual-fuel when the home has gas. Each column: equipment + tier + tonnage, gross price, incentives (itemized, labeled estimates-subject-to-verification, per-type via rebatesFromTons), net out-of-pocket (the headline), est. monthly (financeMonthly at shop-tunable office.financeApr/termMonths), and est. annual heating energy (annualOperatingForType from modeled fuel prices). Same margin and same site work on every column for a fair, honest compare — it shows the heat pump’s operating cost even when cheap gas makes it higher. iPad-optimized 2–3 column layout (wide breakpoint, stacked on phone) plus a one-tap 8.5×11 print sheet (buildComparePrint). App-only — redeploy index.html.setDiscount called render() on every keystroke, rebuilding the input and dropping focus after each character. Now it follows the app’s text-input rule: update the model on oninput without re-rendering (focus preserved), and refresh the totals once on blur via onchange. App-only — redeploy index.html.HELP dictionary (28 entries, written at a 5th-grade level) and a reusable tap-ⓘ button that opens a tap-to-dismiss bottom-sheet. fld/fldS auto-attach the ⓘ to any labeled field whose normalized label matches a dictionary key (so every jargon field — service size, open breaker spaces, static pressure, returns, asbestos, target/floor margin, the electrical options, the per-ton rebates, deposit, commission, age, year built, sq ft, etc. — gets help from one edit), while plain fields (name, phone) correctly get none. Explicit ⓘ added for the chip-based terms: system types, Good/Better/Best tiers, the comparison sheet, and incentives. Per the council, simple stays truthful — Safety reviewed the safety-critical entries: asbestos says “don’t touch it, get it tested,” open spaces says “an electrician has to check,” and smart panel keeps “only if a load calc and the utility allow — not guaranteed.” App-only — redeploy index.html. Help text is easy to extend (one dictionary).index.html.backendToken in both apps so the backend lock is pre-filled (no manual paste). Token value: dfsaefAFS343#%#BBW (chosen exactly as given, including # and % — note these were flagged as the characters behind the earlier token_invalid bug). The intake page is intentionally left without a token (it uses only public actions). Requirements: the Vercel APP_TOKEN env var must be set to this exact value and redeployed, and verify it saved without corruption in the Vercel UI (the # paste risk). App-only — redeploy index.html and sidekick.html.Field Engineer — Master Platform Plan (skeleton draft). Sections marked [expand] are placeholders to fill collaboratively. Update status tags as the build advances.