How the pieces actually talk today — read from the source, not the intention. Built in the same breath, reviewed for the seams. Internal document.
Wiring & dead-endsDecisions a client must trustExternal APIsWhy deep pages drag
reads: blueprint SVG · per-pin sun (/api/sunat) · canopy warm (/api/terrain-mine) · plant library · Plant.id photo IDs
writes: site_marks (one row per property, every pin + photo) + localStorage
Zero consumers in the repo. Every field capture — species IDs, health, quantities, drawn bed areas, per-pin sun — never reaches the plan, the estimate, or the report. The planting engine's own spec admits it: "existingPlants (site-marks) — designed, not yet wired."
/api/plant-library)reads: plant_master → plant_library view + overlay JSON
feeds: library.html · designer.html · site-marks enrich · planting engine — the one hub that IS wired
/api/planting-plan)reads: LaunchPad layers · grid squares (per-bed light) · plant library
writes: SVG plan + JSON schedule → brain.html schedule table, report, precompute
The JSON's why, relaxations, palette justifications render nowhere — the client sees rows/qty/$ with none of the defensibility the engine already wrote.
reads: static data.js price book + hardcoded PRICEBOOK + LaunchPad areas
writes: DOM only. commit() renders "Committed ✓" into innerHTML. Reload = gone.
buildBudget() hardcodes its own mix (Foxtail/Hibiscus/Ixora…), renderPlan() shows the engine's schedule, autoScope() builds a third service-level scope — they can disagree on the same screen and none persists.
reads: suppliers (86) + static catalog
writes: nothing — pure display
The engine prices from a 28-species hardcoded SUPPLIER map + size-class defaults (XL:180 L:45 M:14 S:10 GC:8); the estimate prices from static data.js. The 86-supplier table — the real market — reaches no dollar figure a client sees.
Saved looks/palettes go to localStorage (urthFullLooks/urthPalettes) and are read by nothing — not the engine's theme param, not the estimate.
Also orphaned: window.__plantTotal (written once, read never) · site-marks libFlag ("flag for the library") saved into marks, consumed by no process.
api/sunat.js)Canopy lookup has a 9s timeout; on failure the code keeps the flux-only band — full sun reported under the avocado, the exact bug the file exists to fix. And byte-255 canopy (county-sees-vegetation-no-height, i.e. palms) is treated as OPEN sky.
Fix: hard canopy_missing flag on the pin, never a silent fallback.
sunat.js)pct_open = kwh / brightest-pixel, banded at 70/45. One bright roof or pool deck in the radius depresses every reading on the property. Thresholds (70/45, canopy 8ft/15ft) are invented constants.
planting-plan.js)grid squares → beds → binary solar-map → nothing: two silent catch{}s mean an empty light map plants every bed as full sun, labeled "mixed light." No error reaches the SVG or the schedule.
survivesLight() barely gates (planting-plan.js)Part-sun beds accept FULL, PART, or SHADE species — the filter passes everything; null scorch fields pass the bake check; one species (firebush) is patched by name inside scoring.
grid.js)If terrain-mine fails, CAN=null → canopy 0 everywhere → every square marked ground_sun_valid — the moat feature quietly off for the whole parcel, feeding plan + report. Palms (255-cells) never block the ray-trace despite the comment saying they should.
planting-plan.js)Counts under 2 are deleted; even counts get +1 for the odd-number design rule — the priced qty is not the computed qty, and nothing says so.
buildSchedule(): palms = bed/450 (min 3), fixed species mix, $10 default price; PRICEBOOK hardcodes labor 45/32/24, markup 1.9; "Pursue / Clarify" verdict from a hardcoded 0.8 factor. None of it reads real supplier prices — the P1 price-book gate exists for exactly this.
api/plant-library.js)applyOverlay() returns data unchanged if urth-overlay.json fails to load — excluded/do-not-use species silently re-enter plans and the designer for up to 6h of cache.
A two-word species not in the library matches the first same-genus entry in object order and is presented "✓ In our library" with that other species' sun/toxicity/spacing.
SCALE = totalFt / sumTop pairs ALL "N ft" labels with the N largest polygons; any mismatch skews every mass-planting quantity and sq-ft figure.
sunat requests LOW 0.5m imagery; grid tries HIGH 0.1m first — a pin in Site Marks and the same square in the plan can band differently, and no surface says which basis it used.
| API | Weakness found | The one thing it needs |
|---|---|---|
| LaunchPad layers | Layer names matched by exact string ("Beds/Natural", "Lawn"); a renamed/missing layer silently zeroes that surface | Measurement date + layer checklist surfaced per property |
| Google Solar flux | sunat fixed at LOW 0.5m vs grid HIGH 0.1m — divergent bands; hourly nulls fall to lot-relative flux cut | One shared cached flux artifact per property, imagery quality + date shown |
| terrain-mine (canopy) | The input everything trusts; fails silent in both consumers (timeout → treetop light; catch{} → whole parcel open-sky) | Hard canopy_missing flag propagated into every derived band |
| Plant.id v3 | Best-behaved: confidence, close-call, candidates reach the UI. But genus_only is emitted and ignored on save; N-America filter can exclude planted Old-World ornamentals | Persist the ID + the human's candidate choice somewhere that gets read |
| Google geocode | Failure silently defaults to Miami center — job-feed distances then wrong with no warning | Fail loud: "geocode failed" is an error, not a hint |
| Image hotlinks (Wikimedia/iNat/UF) | onerror hides image; licensing already flagged | Self-host (already on the roadmap) |
| Supabase (public keys) | urth_cache RLS off + public write — the shipped key can poison every cached report component | FLAGGED, not fixed here — rides the URT-DATA-01 gate |
| Perenual | Referenced in old docs; not found anywhere in the repo | Strike it from the mental model |
designer.html fetches /api/grid?format=json&squares=1 directly — up to ~200k squares of multi-MB JSON parsed on a phone to compute six tallies. On cache miss the server re-runs LaunchPad + 4 GeoTIFFs + terrain-mine. sun3d routes via the cache proxy; designer and the engine don't.
Every un-versioned pin fires its own /api/sunat; each cold call pulls LaunchPad + flux GeoTIFF + full canopy JSON. sunat is not in precompute and not in the cache proxy's allow-list — it can never be pre-warmed.
save() POSTs the entire marks array including every base64 photo (~180KB each) on every field change/drag, and re-downloads it whole on open. A walked property is a multi-MB upsert per tap.
Two blueprint SVGs (sun SVG can exceed 1MB), canopy warm, full plant-library payload, and duplicate ref_data fetches (species + species_img each fetched twice by page + fix layer).
All ~454 library rows with every column go to three pages; library.html renders all cards in one innerHTML pass; brain.html loads ~800KB of Leaflet/turf regardless of tab. (P4-5 pagination card covers the render half.)
The fix that pays five times: route designer/site-marks/brain through the existing /api/c cache proxy + add sunat to precompute — the plumbing already exists, three pages just bypass it.
Cold review · read from source on main · no schema, security, or data touched. Receipts: spine rows (scintilla-live).