Technical Spec
Habitable is one website that works like a phone app. The phone does the private work, a small server does the three jobs the phone can’t do safely, and a plain rules table does the deciding: AI proposes, code disposes.
How this works, in plain language
The app on your phone (the browser)
It reads your photo, computes its fingerprint (a SHA-256 hash — a code that changes if a single pixel changes), reads the time the photo was taken from inside the file (EXIF), removes location data, and keeps your whole case — address, issues, photos, follow-ups — in the browser’s own storage (IndexedDB). There is no account and nothing is uploaded except what the next part needs.
A small server (Next.js API routes on Vercel)
It does three jobs the phone can’t do safely:
- asks the vision AI to propose a problem type (the AI key stays on the server);
- stamps a receipt saying “I saw this fingerprint at this time”;
- fetches the building’s public record from NYC Open Data.
The rules table and the rules engine
(src/lib/rules.json and src/lib/rules.ts) Plain data plus plain code. Given the problem type and any facts it needs (mold size, a child under 6, heat or hot water, the date), it returns the hazard class, the correction window, the code section and the source link. The AI never touches this step. That is the kernel: AI proposes, code disposes.
The notice PDF is drawn in the browser from the saved case, so the document never passes through a server.
Why this shape: one deployable app, no database, no login, one external AI call per photo. Everything the demo needs also works offline from saved sample answers (demo mode).
Architecture and data flow
Text version of this diagram
- Phone browser (on the device): pages and components;
evidence.ts(EXIF capture time, SHA-256, strips GPS, re-encodes the photo); IndexedDB viastore.tsholding the case and issues;countdown.tsandnotice-text.tswith the PDF drawn in the browser;geosearch.tsfor address autocomplete. - Code disposes:
rules.json(cited table) andrules.ts(evaluate()). Pure, no I/O, imported by the browser; the AI never touches it. - Next.js API routes (Vercel):
/api/classify(photo and note in, proposal out, calls the AI Gateway),/api/receipt(sha256 in, HMAC receipt out),/api/building(bbl in, BuildingRecord out, calls NYC Open Data). - Outside services: Vercel AI Gateway (claude-haiku-5.5, then gemini-2.5-flash; the AI proposes only), NYC Open Data (HPD violations
wvxf-dwi5, PLUTO64uk-42ks), NYC GeoSearch (called straight from the browser).
The core journey through the system
PRD ref: prd.md > The Core Journey
Pick a step: the boxes it touches stay lit in the diagram above, and its components and files are listed. Without JavaScript, all seven steps are shown in order.
Arrive / set up
The renter types an address → the browser asks NYC GeoSearch directly (it allows cross-origin requests) → suggestions come back with borough, BBL and BIN → the renter picks one and types a unit → the browser saves the CaseFile in IndexedDB → it asks /api/building?bbl= for year built and violations (shown on the case file).
PRD
Components
Endpoints
Files
src/components/case/AddressSetupsrc/lib/geosearch.tssrc/lib/store.tssrc/app/api/building/route.tssrc/lib/building.ts
Snap
The camera input returns a file → evidence.ts reads EXIF capture time (exifr), hashes the original bytes (Web Crypto), and re-encodes a ≤1600px JPEG on a canvas (which drops every metadata tag, GPS included) → in parallel the browser posts the hash to /api/receipt and gets {receivedAt, receipt}.
PRD
Components
Endpoints
Files
src/lib/evidence.tssrc/app/api/receipt/route.tssrc/lib/receipt.ts
AI proposesAI proposes
The browser posts a ≤1024px copy + the optional note to /api/classify → in demo mode with a sample photo the server returns the saved answer; otherwise it calls the vision model with a fixed list and a Zod schema at temperature 0 → code checks confidence: below 0.7 with no answer yet → returns a clarifying question; else the proposal. Errors / no key → {status: "unavailable"} and the UI shows the manual picker.
PRD
Components
Endpoints
Files
src/app/api/classify/route.tssrc/lib/classify.tssrc/lib/demo/samples.ts
Code disposescode disposes
The browser runs evaluate(issue, facts, {now, yearBuilt}) from rules.ts → if a rule needs a fact it returns questions → the renter answers → it returns the class, window, citation and source → countdown.ts computes the requested repair-by date in America/New_York.
PRD
Components
Files
src/lib/rules.jsonsrc/lib/rules.tssrc/lib/countdown.tssrc/components/snap/RuleCardsrc/components/snap/Countdown
Building context
The case file’s BuildingRecord is filtered to the issue’s category (byIssue, recent[].issue).
PRD
Log
Components
Files
src/lib/store.tssrc/components/case/Timelinesrc/components/issue/IssueDetail
Notice
/notice builds the PDF in the browser with @react-pdf/renderer from the saved case (EN/ES text from notice-text.ts) → download or Web Share; “Copy 311 text” uses the same module.
PRD
Components
Files
src/app/notice/page.tsxsrc/components/notice/NoticePreviewsrc/components/notice/NoticeDocumentsrc/lib/notice-text.ts
Rules table: code disposes
PRD ref: prd.md > Rules card and countdown · lives in src/lib/rules.json
The table lives in src/lib/rules.json; every row carries its citation, source URL, a short official quote and a plain-English line. Verified 2026-10-24 against HPD’s “Clear Violations” page, HPD’s indoor-allergen, heat/hot-water, lead-paint and self-closing-door pages, HPD’s CY25 certification report, NYC311 articles, and the enacted texts of Local Law 55 of 2018 and Local Law 86 of 2017. Class counts quoted in notes come from live HPD Open Data rows (inspections since 2025-01-01).
Clock: every HPD window runs “from date of service” of an HPD Notice of Violation, never from a tenant photo or letter. The app’s countdown is therefore the requested repair-by date in the notice, mirroring the window.
| Issue | Fact the rule asks | Outcome | Window | Citation |
|---|---|---|---|---|
| Leak | water actively dripping? yes | Class C | 24 hours | Admin. Code §27-2005(a), §27-2026 (HPD lists “active leaks” as 24-hour C) |
| Leak | no (stain / damage) | Class B | 30 days | §27-2005(a), §27-2026 (80% of leak violations since 2025 are B) |
| Mold | area in one room < 10 sq ft | Class A | 90 days | §27-2017.3(a)(1), Local Law 55 of 2018 |
| Mold | 10–29 sq ft | Class B | 30 days | §27-2017.3(a)(2) |
| Mold | ≥ 30 sq ft | Class C | 21 days | §27-2017.3(a)(4), (c)(2) |
| Pests | mice, cockroaches or rats | Class C | 21 days | §27-2017.4(b), (c) |
| Pests | bedbugs or other | Class B | 30 days | §27-2017.4(a); §27-2018 |
| Heat / hot water | hot water (or both) | Class C | no time (notice asks 24 h) | §27-2031 (≥120°F year-round) |
| Heat / hot water | heat, Oct 1–May 31 (derived from date) | Class C | no time (notice asks 24 h) | §27-2029(a) as amended by LL 86/2017 (68°F day when < 55°F out; 62°F night) |
| Heat / hot water | heat, June 1–Sept 30 | no class — explains heat season | — | §27-2029(a) |
| Lock / door | apartment door can’t lock | Class C | 24 hours | §27-2043(a) (HPD lists “defective door locks” as 24-hour C) |
| Lock / door | apartment lock damaged but locks | Class B | 30 days | §27-2043(a), §27-2005(a) |
| Lock / door | building entrance won’t lock | Class C | 24 hours | §27-2005(a) |
| Lock / door | door doesn’t self-close | Class C | 14 days | HMC §27-2041.1 |
| Peeling paint | built before 1960 (from PLUTO, else asked) + child under 6 | Class C | 21 days | §27-2056.6 (Local Law 1 of 2004) |
| Peeling paint | plaster also broken | Class B | 30 days | §27-2005(a), §27-2013 |
| Peeling paint | paint only | Class A | 90 days | §27-2013 |
| Not covered | — | no class; manual pick or “log as note” | — | — |
18 rows from the spec’s Rules Table. Class A is non-hazardous, B hazardous, C immediately hazardous.
Where HPD’s inspector assigns the class (leaks, locks, plaster), the card says so and shows the share observed in HPD data. Unverified and therefore not claimed: the exact conditions that make HPD escalate a leak or lock to Class C; whether a tenant’s own letter starts any legal clock.
Source for the windows: HPD “Clear Violations”.
Components, tied to the PRD
Each component names the PRD heading it implements and the files that hold it. Twelve components; open one, or follow a link from the diagram.
Doodle UI kit
Hand-drawn primitives and design tokens: SketchBox, Highlight, Stamp, DoodleButton, DoodleCard, DoodleSheet, Chip, DrawnIcons, PencilLoader, previewed at /kit during the build.
PRD ref
Files
Address setup
AddressSetup component + src/lib/geosearch.ts (searchAddresses(text) calls NYC GeoSearch autocomplete straight from the browser — it allows cross-origin GETs — and returns Address[], max 6). Saves the CaseFile. “Try the demo building” writes the demo case (flag demo: true).
PRD ref
Files
Evidence capture
readEvidence(file: File): Promise<{sha256, capturedAt, captureSource, addedAt, photo, width, height, aiImage}> — EXIF via exifr, SHA-256 via crypto.subtle.digest, canvas re-encode (≤1600px stored copy, ≤1024px copy for the AI). Rejects non-images and files > 20 MB with a typed error.
PRD ref
Files
Receipt service
POST /api/receipt {sha256} → {receivedAt, receipt} where receipt = first 12 hex chars of HMAC-SHA256(RECEIPT_SECRET, sha256|receivedAt). GET /api/receipt?sha256&receivedAt&receipt → {valid}. /verify page lets anyone check a receipt from a notice.
PRD ref
Files
Classifier (AI proposes)
POST /api/classify {image (data URL ≤1024px JPEG), note?, answer?: {question, answer}, sampleId?} → ClassifyResponse.
sampleIdof a demo sample → saved answer fromsrc/lib/demo/samples.ts(labelledsource: "demo").- Otherwise: system prompt lists the seven allowed answers with one-line definitions; output schema
{issue: enum, confidence: 0..1, rationale: ≤200 chars, clarifyingQuestion: string|null, clarifyingOptions: string[]}; temperature 0; one retry; fallback model. - Code applies the threshold (0.7): below it and no
answer→ return the question (the model’s, or a default per issue); with ananswer→ return the second proposal whatever its confidence (the UI offers the manual picker when it’s still low). - Input limits: body ≤ 2 MB, image must be
data:image/jpeg|png|webp, note ≤ 280 chars.
PRD ref
Files
Rules engine (code disposes)
src/lib/rules.json (the cited table) + src/lib/rules.ts: evaluate(issue: ProposalIssue, facts: Facts, ctx: {now: Date, yearBuilt: number | null}): RuleOutcome, RULES export for the rules page. Pure, no I/O, fully unit-tested. See Rules Table.
PRD ref
Files
Countdown
src/lib/countdown.ts: repairByDate(firstDocumented: string, window): string and remaining(repairBy, now) → {days, hours, minutes, seconds, overdue}; formatNY(iso) renders dates in America/New_York. Countdown component ticks every second (paused when the tab is hidden).
PRD ref
Files
Building history
GET /api/building?bbl= → BuildingRecord. Server queries Socrata wvxf-dwi5 (open violations for the BBL, newest first) and PLUTO 64uk-42ks (yearbuilt), maps each violation’s description to an issue with the keyword table in src/lib/building.ts, counts by class and issue, caches for an hour. Demo BBLs fall back to saved fixtures in src/lib/demo/ if the live call fails. BuildingPanel component.
PRD ref
Files
Case store
src/lib/store.ts (idb-keyval): getCase/saveCase, listIssues/getIssue/saveIssue/deleteIssue, addFollowUp, markFixed, clearAll, storageAvailable(). A tiny useCase() hook re-reads after writes.
PRD ref
Files
Screens
/—Welcome(first use) orCaseFile(address card, Snap button,Timeline,BuildingPanel, notice button).SnapSheet— photo →PencilLoader→ProposalCard(violet) →ClarifyQuestion→FactQuestions→RuleCard(stamp, window, citation,Countdown, “Why?” drawer) → building snippet → “Add to my log”. “Not right?” opensIssuePicker./issue/[id]—IssueDetailwith evidence block, follow-ups, mark fixed./notice—NoticePreview+NoticeDocument(react-pdf), EN/ES toggle, names, download/share, 311 text./how-it-works— diagram, full rules table fromRULES, countdown meaning, privacy, disclaimer./verify— receipt check.
PRD ref
Files
Notice
src/lib/notice-text.ts: noticeText(case, issues, lang: "en" | "es") → structured sections; complaint311Text(case, issue). src/components/notice/NoticeDocument.tsx renders the PDF with fonts registered from /fonts/*.woff. File name habitable-notice-<street>-<yyyy-mm-dd>.pdf.
PRD ref
Files
Demo mode
src/lib/demo/: the demo building (address + BBL + fixtures), samples.ts (saved AI answers for each sample photo in public/samples/). Every demo surface shows a DEMO chip.
PRD ref
Files
PRD heading → where it lives
Stack, setup and look
Stack
- Next.js 16.4 (App Router) + React 19 + TypeScript — one app for pages and API routes; deploys to Vercel in one command. Docs: https://nextjs.org/docs (the installed version’s docs are in
node_modules/next/dist/docs/and take precedence — this version enablescacheComponents). - Tailwind CSS v4 — styling via CSS variables for the doodle tokens. https://tailwindcss.com/docs
- AI SDK 7 (
ai) — onegenerateTextcall with a structuredOutput.objectschema. Model ids route through Vercel AI Gateway (anthropic/claude-haiku-5.5primary,google/gemini-2.5-flashfallback). IfGOOGLE_GENERATIVE_AI_API_KEYis set,@ai-sdk/googleis used directly instead. https://ai-sdk.dev/docs · https://vercel.com/docs/ai-gateway - Zod 4 — validates the model’s answer and every API input. https://zod.dev
- exifr — reads EXIF capture time in the browser. https://github.com/MikeKovarik/exifr
- idb-keyval — tiny IndexedDB wrapper for the on-device case. https://github.com/jakearchibald/idb-keyval
@react-pdf/renderer4 — notice PDF in the browser with embedded fonts (no headless Chrome). https://react-pdf.org- Vitest (rules, countdown, building, notice text, API logic) and Playwright (one end-to-end journey in demo mode). https://vitest.dev · https://playwright.dev
- Fonts: Archivo, Patrick Hand, JetBrains Mono (SIL OFL) via
next/font/googlefor the web and@fontsource/*WOFF files for the PDF.
Accepted tradeoff: AI Gateway needs a card on the Vercel account to unlock its free credits; without a key the app still runs end to end (demo mode + manual picker).
Where it runs and how someone tries it
- Requirements: Node.js ≥ 20, npm.
- Local:
npm install→npm run dev→ openhttp://localhost:3000→ “Try the demo building”. - Live AI (optional):
vercel env pull .env.local(AI Gateway via OIDC) orAI_GATEWAY_API_KEY=…orGOOGLE_GENERATIVE_AI_API_KEY=…in.env.local.RECEIPT_SECRETsigns receipts (any long random string). See.env.example. - Tests:
npm test(Vitest),npm run test:e2e(Playwright, starts the app itself). - Deploy (chosen): Vercel —
vercel --prod. Public GitHub repo for the submission. The demo video records the deployed app on a phone.
Look and feel
Carried from prd.md > Look and Feel
#f3eee5
#1d1b20
#ffcf3f
#6b4eff
- Palette: paper with a faint ink grid and grain; ink; lime highlighter (primary actions, key words, “verified by code”); violet (text-safe darker shade) for everything the AI proposes — the builder’s complementary colour; stamp red for Class C / deadlines and amber for Class B, ink for Class A; pastel tints for sticky notes. Colour is always paired with a word or number.
- Type: Archivo 800 for claims, Patrick Hand for handwriting (notes, labels, the AI’s voice), JetBrains Mono for numbers, dates, hashes and countdowns.
- Motion: strokes draw themselves on appear, highlighter swipes, cards spring in, stamps thud; all disabled under
prefers-reduced-motion. - Density: one column, generous spacing, big tap targets, one primary action per screen.
- Copy tone: plain, warm, direct; never legal advice.
- Built as reusable primitives in
src/components/doodle/, previewed at/kitduring the build.
Data model and file structure
Data model
All on the device (IndexedDB via idb-keyval), shapes in src/lib/types.ts:
case→CaseFile(address, unit, yearBuilt, childUnder6, demo, createdAt, optional names). Written at setup; updated when the paint rule learns about a child under 6 or names are typed on the notice page.issues→Issue[](evidence incl. stripped photo, proposal, final issue, facts, outcome snapshot, repairBy, followUps, fixedAt). Written on “Add to my log”; updated by follow-ups / mark fixed.- Leaving and coming back: everything reloads from IndexedDB. “Delete my case” clears both keys.
- Server keeps nothing. Building records are cached by the platform for an hour.
File structure
. ├── devpost/ # Devpost learning workspace: scope, prd, spec, checklist, app map ├── .agents/skills/ # Devpost Learn skill pack (MIT) ├── public/ │ ├── samples/ # real demo photos (Wikimedia Commons, credited; EXIF capture time, no GPS) + manifest.json │ └── fonts/ # WOFF fonts embedded in the PDF ├── scripts/render-samples.mjs # rebuilds the sample photos from scripts/samples/*.jpg ├── src/ │ ├── app/ │ │ ├── layout.tsx, globals.css # fonts, doodle tokens, grid paper │ │ ├── page.tsx # Welcome or CaseFile │ │ ├── issue/[id]/page.tsx # one issue │ │ ├── notice/page.tsx # notice preview + PDF │ │ ├── how-it-works/page.tsx # rules table + diagram │ │ ├── verify/page.tsx # receipt check │ │ ├── manifest.ts, icon.svg # installable on a phone │ │ ├── kit/page.tsx # UI kit showcase (not linked) │ │ └── api/ │ │ ├── classify/route.ts # AI proposes │ │ ├── receipt/route.ts # sign / verify receipts │ │ └── building/route.ts # NYC Open Data │ ├── components/ │ │ ├── doodle/ # hand-drawn primitives │ │ ├── case/ # Welcome, AddressSetup, CaseFile, Timeline, BuildingPanel │ │ ├── snap/ # SnapSheet, ProposalCard, RuleCard, Countdown, pickers │ │ ├── issue/ # IssueDetail │ │ └── notice/ # NoticePreview, NoticeDocument │ └── lib/ │ ├── types.ts # shared domain types │ ├── rules.json, rules.ts # cited table + engine (+ rules.test.ts) │ ├── countdown.ts # NY-time deadlines (+ test) │ ├── building.ts # violation → issue mapping, summary (+ test) │ ├── geosearch.ts # NYC address autocomplete (browser) │ ├── classify.ts # prompt, schema, threshold logic (+ test) │ ├── receipt.ts # HMAC sign/verify (+ test) │ ├── evidence.ts # EXIF, hash, strip metadata │ ├── store.ts # IndexedDB case store │ ├── notice-text.ts # EN/ES notice + 311 text (+ test) │ └── demo/ # demo building fixtures, saved sample answers ├── e2e/journey.spec.ts # Playwright: demo journey photo → notice ├── README.md, LICENSE (MIT), THIRD-PARTY-NOTICES.md, .env.example
External services and dependencies
NYC GeoSearch (Planning Labs, Pelias)
GET https://geosearch.planninglabs.nyc/v2/autocomplete?text=<q>&size=6. No key. BBL at features[i].properties.addendum.pad.bbl, BIN at .pad.bin, label properties.label, borough properties.borough, zip properties.postalcode, geometry.coordinates = [lon, lat]. Don’t filter by layers (PAD records are venue); don’t gate on confidence; non-NYC text can still match NYC streets, so the renter confirms the label. ~1.6 s. Docs: https://geosearch.planninglabs.nyc/docs/
NYC Open Data (Socrata SODA)
No key needed; optional X-App-Token (SOCRATA_APP_TOKEN) raises limits. Single-BBL queries 1.5–3 s; always $select + $limit. Docs: https://dev.socrata.com/docs/queries/
- HPD violations
wvxf-dwi5:GET https://data.cityofnewyork.us/resource/wvxf-dwi5.json?$where=bbl='<bbl>' AND violationstatus='Open'&$select=violationid,class,inspectiondate,novissueddate,originalcorrectbydate,currentstatus,novdescription&$order=inspectiondate DESC&$limit=1000(bblis TEXT → quoted; “open” =violationstatus='Open', notcurrentstatus). Counts by class:$select=class,count(*) as n&$group=class. dataset - PLUTO
64uk-42ks:GET …/64uk-42ks.json?$where=bbl=<bbl>&$select=bbl,yearbuilt,unitsres(bblis a NUMBER → unquoted). dataset
Violation → issue mapping (first match wins, case-insensitive)
- mold
27-2017.3or the word MOLD (never “MOLDING”) - pests
27-2017.4, “INFESTATION CONSISTING OF”, MICE, ROACHES, RATS, BEDBUGS (drop “FILE ANNUAL BEDBUG REPORT”) - heat
27-2029,27-2031, “SUPPLY OF HEAT”, “HOT WATER” (drop boiler-room rows) - lock/door LOCK, LATCH, DEAD BOLT,
27-2043(not27-2043.1), SELF-CLOSING (drop BOILER, signage, WINDOW GUARD) - paint
27-2056, LEAD-BASED PAINT, PEELING PAINT, “PLASTERED SURFACES AND PAINT”, word PAINT (never bare “LEAD”) - leak “WATER LEAK”, LEAK, DAMPNESS, MOISTURE, SEEPAGE (never bare “WATER”)
Vercel AI Gateway
Model ids anthropic/claude-haiku-5.5 (vision, structured output; $0.10/$0.50 per M tokens) then google/gemini-2.5-flash. Auth: OIDC on Vercel / vercel env pull, or AI_GATEWAY_API_KEY. Requires a card on the Vercel account to unlock free credits. https://vercel.com/docs/ai-gateway
Google Gemini API (optional)
GOOGLE_GENERATIVE_AI_API_KEY, model gemini-2.5-flash via @ai-sdk/google. https://ai.google.dev/gemini-api/docs
Vercel
Hosting (Hobby, free). https://vercel.com/docs
NYC 311 articles (links only, no API)
KA-01074 apartment maintenance, KA-02262 mold, KA-02278 pests, KA-01036 heat/hot water, KA-02134 lead paint — https://portal.311.nyc.gov/article/?kanumber=<id>.
Demo buildings (public building records only)
618 West 136th Street, Manhattan (BBL 1020020091, built 1905, 291 open violations, 37 leak-related); fixtures in src/lib/demo/.
Failure modes, simplifications and decisions
Important failure modes
AI unavailable
No key, quota, outage, timeout 20 s → manual picker: “The AI couldn’t read this photo right now. Pick what it is — the rules still apply.”
NYC Open Data slow, down or no match
“Building records are unavailable right now” + retry; demo building uses saved fixtures; no records → “No open HPD violations on record.”
IndexedDB unavailable
Banner “This browser may not keep your log after you close it”; the session continues in memory.
What was simplified and why
On-device storage instead of accounts + database
Why: the kernel needs neither; also the privacy story. Fuller version: hosted Postgres + auth for organizer sharing.
Openly licensed photos from Wikimedia Commons for the samples instead of anyone’s own photos
Why: no private photos in a public repo, and each is credited in THIRD-PARTY-NOTICES.md; real photos still work live. (The first build used illustrated scenes; they were replaced with real photos after review.)
Saved AI answers for samples instead of a live call in demo mode
Why: the demo never depends on a quota.
Receipt = HMAC over the hash and time instead of a timestamping authority
Why: proves Habitable’s server saw the fingerprint then; a fuller version would anchor to an RFC 3161 timestamp service.
One city, six problem types
Why: keeps every rule citable.
Decisions and open issues
- Model choice (implementation detail): Claude Haiku 5.5 through AI Gateway with Gemini Flash fallback; direct Gemini key also supported. Temperature 0, one retry.
- Confidence threshold 0.7 (implementation detail), tuned on the sample photos.
- Learner uncertainty discussed: when does the legal clock start? Resolved from HPD’s own page: correction periods run from HPD’s service of a violation, so the countdown is labelled as the repair-by date the notice requests, mirroring that window.
- Resolved by research: mold area thresholds (Local Law 55), pests split B/C, self-closing doors are §27-2041.1 (not 2041/2042), ordinary peeling paint is Class A, night heat is 62°F regardless of outside temperature. Still unverified (and not claimed): what makes HPD escalate a leak or lock to Class C; whether a tenant letter starts any clock.
- Carried from prd.md > Open Questions: both resolved above.
Round 2 additions
A scope change recorded after the build, awaiting the builder's review and approval. It is listed here so the spec matches what ships.
- Screens: /heat, /landlord, /ask, /next-steps; the issue page gains the message coach and a calendar reminder; the case file gains the heat-log, next-steps and tools cards, the landlord block and backup/restore.
- Heat:
src/lib/heat.ts+GET /api/outdoor(freshest of KNYC, KLGA, KJFK). - Landlord:
registration.ts;portfolio.ts+GET /api/landlord(seven datasets, citywide baseline). - Ask the law:
src/lib/law/(verbatim corpus, BM25,verify.ts) +POST /api/ask. - Coach:
coach.ts(placeholders, validator, templates) +POST /api/coach. - Shared:
ai.ts(models, cooldowns, cache),rate-limit.ts,backup.ts,ics.ts,public/sw.js.