The AI-powered field-ops platform for licensed landscape and water-management crews.
Version: demo release · May 2026
VerdaKai is a mobile-first field operations system for licensed landscape, turf, and water-management businesses. It replaces the scattered stack most crews live with — paper route sheets, an insurance binder under the seat, an HOA map texted from the office, a plant-ID question Googled at a stoplight, and a spray log filled out later, maybe never — with one application that runs the whole day of work from start to finish.
It is not a general-purpose CRM. Every screen was designed around a specific field-ops workflow the authors have watched crews actually perform: a spray tech opening their phone in the truck to see today's stops, a maintenance admin publishing a newly-scanned HOA community to the mow crew, an admin running the business end while the trucks are already out.
Three design commitments set it apart:
It is AI-native, not AI-bolted-on. A tech can photograph a plant, pest, or diseased patch and get a species-level answer in under five seconds — phrased as an identification or as a diagnosis with treatment options drawn from the products already on the shelf. Admin can teach the AI anything it should know about the business; the lesson is applied to every future answer, companywide.
Compliance is a first-class citizen. FDACS licenses, CEU progress, certificates of insurance, W-9s, workers comp, business licenses — all live in the same place as the crew roster. One click prints an HOA-ready compliance PDF with an AI-generated posture summary at the top.
It integrates with Program Designer Suite (PDS). PDS is the office-mode companion — a full spray-program designer, employee manager, truck-maintenance tracker, and weekly scheduler. VerdaKai is the mobile field surface; PDS is the desktop administration surface. They share state, so a program designed at the desk is on the tech's phone by the time they start the truck.
Operator: 'i thought we had already done this but it should be universal to every scan' + 'build everything in the best order.' Three-part shipping pass:
Universal NDVI vegetation gating. Every Detect exclusions click on a single-property scan now fetches a Sentinel-Hub NDVI PNG for the satellite tile bbox, decodes it client-side, and builds a vegMask that's passed into detectHardscapePolygons. Vegetation pixels (NDVI > 0.2 — chlorophyll signal) are forbidden from being hardscape BEFORE flood-fill. This is the proper fix for canopy-edge ground passing the RGB gray clause; the existing RGB canopy guard becomes a defensive backup. Falls back to RGB- only when Sentinel-Hub credentials are absent or the call fails. Logs ndvivegmaskbuilt / ndviunavailable / ndvi_failed to the methodology trail.
Pl@ntNet phone-photo species ID. New endpoint /api/field-ops/site-survey/identify-species proxies my-api.plantnet.org for top-3 plant species candidates from a field-tech phone photo. Returns specieskey (matches our SW FL flora module canonical keys when found), commonname, scientific, score, plus inline nativestatus / treatablestatus / opsnote enrichment for known FL species. Free tier 500 req/day. Operator sets plantnetapi_key at /field-ops/settings (new field added to the keys panel). Closes the aerial-only species gap that Phoenix subspecies and oak species can't bridge.
NDVI overlay toggle on the map. After Detect exclusions fires, the in-map "Show NDVI / Hide NDVI" button appears next to Detect. Toggle ON shows the Sentinel-Hub NDVI colormap (red = non-veg, yellow = mid, green = healthy vegetation) at 0.55 opacity. Operator can SEE exactly which pixels were forbidden from being hardscape — same data the gate used. Builds confidence.
Operator: "program deep knowledge of our horticultural zone into the software... should apply to everything in site survey software and should be accessible in plant brain as well... it should also have this communication with field and the id tools in the stop cards."
New: src/lib/sw-florida-flora.ts — single source of truth for SW FL plant knowledge. Exports:
SWFLHORTZONECONTEXT — USDA Zone 9b/10a, Köppen Cfa/Aw,FL summer fertilizer blackout rules, sandy Myakka/EauGallie soils, Ch. 482 PCO licensure scope
groundcovers, ornamental grasses, succulents, turf grasses, and FLEPPC Category I + II invasives
status (native / naturalized / exotic / invasivecat1/2), treatable status (premium / standard / excluded / removerecommend / inventory_only)
findSpeciesByKey, speciesByCategory, allInvasives, premiumTreatable, buildVisionSpeciesReference
Wired into every AI endpoint that touches FL plant material:
/api/field-ops/site-survey/analyze (single-property scan)/api/field-ops/site-survey/community-scan (HOA scan)/api/field-ops/ai-garden-chat (Plant Brain)/api/field-ops/ai-diagnose (field tech Plant ID + Diagnose)site-survey-deepforest-species-id.ts (per-pin species ID)Because both route-card / stop-card plant-ID buttons and the standalone Field ID page hit /api/field-ops/ai-diagnose, they automatically inherit the same FL flora context the property-scan endpoints use. One module, every prompt.
Operator screenshot after the road-filter ship: down to 7 polygons (good), but diagonal "X" lines still crossing the buildings and Hard 2 / Hard 5 polygons still wrapping shaded tree canopy on the west side. Three fixes:
ringIsSane. SAMmask-to-contour + Douglas-Peucker simplification can produce rings where two non-adjacent edges cross (a bow-tie shape that renders as an X). Added an O(N²) edge-pair intersection check; rings that fail it now drop. ~30 vertices/ring → ~900 comparisons → instant.
isHardscape. Dense FL treecanopy in shadow has RGB clusters like (75, 80, 70) — near- equal channels with G slightly dominant (chlorophyll signature). The existing gray clause was accepting these. New rule: if G ≥ R AND G ≥ B AND G - min(R,B) ≥ 2 AND meanRgb < 130, treat as vegetation, drop.
card.** Dropped from 384×320 (sometimes rendering > 600px tall) to 96-128px square — operator browses more properties at a glance, less scroll.
Same property, second screenshot: criss-crossing "Road 5/7/8" polygons still appearing despite the morning's service / driveway skip + 30% cap. The remaining culprit is OSM tertiary / residential ways that have multi-vertex paths with sharp turns; bufferPolyline produces self-intersecting quadrilaterals at those turns. Two new filters layered on:
to parcel hull. Real roads are long-thin strips (typically 10:1+). A roughly-square "road" is a buffer geometry bug.
cross-street takes < 5% of a parcel. Tamiami Trail's strip inside the parcel is < 1%.
Toast logs osmroadsfiltered: { oversizedropped, squaredropped }. CV hardscape catches whatever real pavement these "roads" were covering, so dropping them costs nothing.
Operator screenshot of 8440 N Tamiami after the morning's color + dedup work showed three giant slate-blue "Road 5/7/8" polygons criss-crossing the parcel. Diagnosed three causes:
service highway tag was being included. Commercial parcels in Sarasota frequently have OSM tags for parking-lot internal lanes / private driveways with highway=service. The 4m buffer applied to a multi-vertex service-road polyline that snakes through a parking lot produces a chaotic strip covering most of the parcel. Added service, driveway, parkingaisle to the OSM endpoint's SKIPPEDHIGHWAY_TAGS set. CV hardscape detector still catches the same pavement.
exemption was meant to protect long thin OSM road buffers from being centroid-eaten by a building polygon. Side effect: multiple overlapping road polygons all rendered. Dropped the exemption — roads now go through dedup like any other feature class.
clipped to the parcel hull that exceeds 30% of parcel area (or 1500 sqft min) is impossible — drop it. Toast logs osmroadsoversize_dropped: N with the cap.
Plus: added road, street, highway to SAM endpoint FORBIDDEN_TYPES so vision-LLM can't ship a parking-lot polygon labeled as a road.
Operator left for the afternoon with "keep going" instruction. Four follow-on improvements:
Type-aware overlap dedup. Replaced the biggest-wins universal dedup with a class-aware version. Polygons are bucketed into feature classes (rooftop / hardscape / pool / water / driveway / road) and dedup runs WITHIN a class only. A small building polygon inside a large parking-lot polygon now keeps both — they're different physical features, both legitimately excluded.
Per-source polygon colors. Exclusion polygons used to all render as the same red. Now color-coded by feature class:
Operator can read 5 overlapping exclusions at a glance. Wired through a new exclusionMeta prop on SatelliteMap parallel to exclusionPolygons.
Pool aspect-ratio filter. Cyan stripes on driveway pavers sometimes pass the bright-blue water clause. Real pools are rectangular or oval (aspect ratio < 4:1). Pool-sized water polygons (sqft < 1500) with aspect ratio > 4:1 now drop. Toast logs dropped_aspect: N when fired.
Hardscape sub-classification by shape. Big hardscape blobs no longer all label as "Hardscape". Long-thin polygons (aspect > 6, sqft < 1500) are tagged as "Sidewalk / walkway" (type: 'sidewalk'), narrow rectangles (aspect 2.5-6, sqft < 1800) as "Driveway" (type: 'driveway'), big blobs as "Parking / pavement". The new types feed into per-source coloring and the diagnostic toast bucketing.
Operator screenshot showed 10+ criss-crossing exclusion polygons (hardscape + county GIS + SAM + driveway-topology all firing at once on a commercial parcel) plus a worrisome polygon over tree canopy on the west side. Three fixes:
Reverted dark-asphalt clause. The clause-0 added earlier ("range < 10 AND meanRgb 35-85") accepted dense canopy shadow in addition to fresh asphalt — both have near-zero saturation. Couldn't separate them with pure RGB. Net loss; reverted to the 80-floor original isHardscape. Fresh blacktop will need NIR band data (NAIP) to reliably distinguish from canopy shadow — see docs/MAPPING_RESEARCH.md.
Tree canopy out of Claude exclusion prompt. Removed preserve and wetland from the SINGLEPROPERTYPROMPT valid types list. Added explicit DO NOT INCLUDE lines for trees / canopy / shaded vegetation / native preserve. Server-side FORBIDDEN_TYPES set extended to drop any polygon Claude tags as preserve / wetland / tree / canopy / vegetation / shrub / garden / landscape / shade. SAM Claude-polygon cap dropped from 12 → 8.
SAM self-overlap dedup. When SAM returned 8-10 polygons that all covered roughly the same building + parking area, we shipped all of them. New post-process: sort biggest-first, drop any polygon whose centroid lies inside a larger already-kept polygon. Result: one polygon per distinct region.
Visual cap on the client. After source dedup, sort all exclusion polygons by sqft descending and cap to 12 for render. Drop any polygon whose ring has fewer than 4 vertices or zero area (degenerate triangle artifacts). Toast surfaces N smaller hidden for clarity when the cap fires.
When a parcel sits outside Sarasota / Manatee or county GIS is stale, countybuildings came back empty and the only rooftop signal was the CV hardscape pixel pass — which only catches the roof if it happens to fall in the gray / white / red-tile band. Now: the auto-exclude pipeline also fetches OSM building polygons on the same Overpass call as roads. County GIS stays authoritative when both populate; OSM fills the gap when county is silent. OSM rooftops are tagged method='osmbuilding_v1' with label "Building footprint (OSM)" and added to the strict render allow-list. Hardscape ↔ rooftop dedup considers OSM rooftops too, so a white-roof CV match doesn't double up with an OSM polygon.
Three quality-of-life fixes layered on top of the morning's hardscape + pool detection:
Fresh / dark asphalt now passes. The original gray clause required meanRgb 80-220, which dropped fresh-sealed commercial asphalt (typical 50-75 brightness with near-zero saturation). New clause-0 in isHardscape accepts range < 10 AND meanRgb 35-85, guarded by the existing vegetation rule so dense canopy shadow still gets dropped. Catches commercial parking lots that were silently invisible to the previous classifier.
Hardscape ↔ rooftop dedup. White roofs and red tile roofs both pass the hardscape classifier and are also returned by county GIS as building footprints. We were drawing two stacked red polygons over the same building. New centroid-in-polygon test drops any cvhardscapev1 polygon whose centroid sits inside any countygisv1 rooftop. Billing math is unaffected (overlapping AI exclusions union, not double-count); visual clutter is gone. Toast logs N duplicate-rooftop dropped when the dedup fires.
Total exclusion sqft in the toast. Operators kept asking "so how much got excluded?" — the answer was buried in the methodology panel. Toast now appends 15.4K ft² removed from billable area so you see the bottom-line number at a glance.
Operator on 8440 N Tamiami Trail (commercial 1-acre+ parcel): "exclusions seem to be following the road but are not getting the rooftops and all of the hardscape and pool." Diagnosis: the auto-exclude pipeline had no hardscape detector at all (county GIS gives only building footprints, OSM only public roads, driveway-topology only narrow strips between buildings and roads), and the CV water classifier only caught dark retention ponds — bright/cyan swimming pools fell below the thresholds.
detectHardscapePolygons wired into single-property auto-exclude. The classifier already existed in client-water-detect.ts (tuned for FL satellite imagery — gray asphalt/concrete + white roofs + red tile, with a vegetation guard). We now call it from runSinglePropertyAutoExclude after CV water detection, with maxSqftPerComponent raised to 1,000,000 so a continuous parking lot ships as one polygon. Output is tagged method='cvhardscapev1' and added to the strict render allow-list in site-survey-client.tsx. Polygons ≥ 8,000 sqft label as "Parking / pavement"; smaller ones as "Hardscape" or "Hardscape (small)". Hull-clipped to the parcel like every other source.
Swimming-pool detector. isWater in client-water-detect.ts gained a third clause for bright blue/cyan/turquoise pixels (meanRgb 90-220, B > R + 30, B ≥ G, G > R + 10, R < 130). The single-property water detector also lowered its minimum component floor from 1,500 sqft (community-pond default) to ~30 sqft so small backyard / commercial pools pass. Pools < 1,500 sqft tag as pool with label "Swimming pool"; larger water bodies stay as retention_pond.
Diagnostic toast updated. The Auto-exclude completion toast now breaks out hardscape and pools as distinct buckets: "Auto-exclude complete — 4 buildings · 2 hardscape · 1 driveway · 1 pool, all clipped to parcel hull."
Final overnight pass focused on getting the Wipe & re-detect path actually reachable from the bad-zone case.
Wipe & re-detect now surfaces in the AI Detected panel header. The original button was inside a conditional that only rendered when there were ZERO AI zones — useless when the operator HAS bad zones to wipe. The panel-side button awaits the persisted clear before kicking off the new deterministic auto-exclude pass so the two PATCHes against aimeasurementsmerge.auto_exclusions don't race. Single-property only; community scans already get a fresh exclusion pass on every "Scan everything" click.
Phase stepper shimmer compatibility. Switched from <style jsx> (styled-jsx) to plain <style> tag for the shimmer keyframes so the animation reliably emits through the Cloudflare Pages worker build pipeline — matches the existing pattern in walk-client.tsx.
Operator pushed back hard that single-property scans on a residential lot under 1 acre were returning 151,000 sqft of hardscape (3.5 acres) and rendering rough Claude-vision exclusion polygons that "don't conform to anything." Closing the loop on both:
Per-source render filter (single-property). Exclusion polygons on the single-property satellite map now only render when their type matches a deterministic source (rooftop / buildingfootprint / road / driveway / retentionpond / water / lake / pooldeck / tenniscourt / pickleballcourt / basketballcourt / parkinglot / cartpath / sidewalk). Loose Claude-vision shapes are dropped at render entirely. Per the standing memory rule "Don't ship imprecise vision-LLM polygons." Tree pins below estimate_confidence < 0.5 are likewise dropped unless they came from a precise source (DeepForest, walk-perimeter GPS, manual placement, SAM).
Hard "measure inside the parcel" prompt rule. The analyze prompt's #1 critical failure mode is now MEASURING OFF-PARCEL FEATURES — the model is told explicitly to only count pixels inside the subject parcel, the operator's exact failure case is quoted ("residential lot under 1 acre returned 151,000 sqft of hardscape because the model included an adjacent parking lot — that's a billing-relevant failure"), and a TOTAL > LOTSQFT rule tells the model to fix it before responding when zone sums exceed 1.5× the stated lotsqft.
Server-side measurement clamp with fallback anchor. The existing sum-to-lot rescaling now tries parcel.lotsqft first, then falls back to the model's own measurements.totallot_sqft estimate when county GIS didn't return parcel data. When BOTH anchors are missing, hardscape is hard-capped at max(4× turf, 50,000) so nothing ships a 3.5-acre hardscape number on a residential billing record. Note added to result.notes when the cap fires so the operator knows.
"Wipe & re-detect" button. Amber sister to "Run auto-exclude" in the AI exclusion section. Clears the persisted auto_exclusions on the survey row + drops them from in-memory state, then re-runs the deterministic single-property auto-exclude pipeline (county GIS + OSM + CV water + driveways, hull-clipped). Lets operators nuke stale Claude-vision polygons saved from earlier scans without deleting the whole survey.
Auto-exclude diagnostics. Every call to runSinglePropertyAutoExclude now toasts the source counts on success ("Auto-exclude complete — 10 buildings · 6 roads · 2 driveways"), explicitly says "no buildings, roads, or water bodies detected" when all sources came back empty, and surfaces failures via toast with the underlying error message instead of console.warn'ing them away. Operator can immediately tell whether the pipeline ran, ran-but-empty, or choked.
Status bar with motion. PhaseStepper now renders a 1.5px progress bar below the step pills with a determinate fill (steps done / total) plus an indeterminate shimmer animation that visibly moves across the current step's portion every 1.6s. After 30 seconds on the same step, an in-line note appears explaining Claude vision typically takes 30-90s and the 2-min timeout will surface a real error if the fetch genuinely stalls.
Community-scan response validation. Server now coerces every numeric field, drops oak species keys outside the allowed set (liveoak / laureloak / sandliveoak / wateroak / myrtleoak / chapmanoak / otheroak), reconciles per-species sum vs bucket count, and provides string fallbacks. The UI never sees malformed data.
Hardened saved-survey hydration. The auto_exclusions hydration loop now validates each point is {lat, lng} with finite numbers (lat ∈ [-90, 90], lng ∈ [-180, 180]), drops zones with < 3 valid points after filtering, and clamps confidence to [0, 1]. Older saved rows with partially-malformed points no longer reach Google Maps' Polygon constructor as NaN coordinates.
Polygon-clip lib unit-tested. New src/lib/polygon-clip.ts now has 14 unit tests across sutherlandHodgmanClip, ensureCcw, convexHullLatLng, and clipPolygonToParcelHull. Test count went from 299 → 313 passing.
Follow-up to "Single-property scan accuracy + reliability" below. Operator request: "ai autoscan needs to be able to have all of the same capability and accuracy as it does on Villas of Papillon." Closes the parity gaps:
- Hull boundary now passed to single-property maps. The render-time exclusion polygon clip from polygon-clip.ts couldn't fire on single-property scans because both call sites of <SatelliteMap> were missing the hullBoundary prop. Now both pass parcel.rings[0] so any AI exclusion polygon that overshoots the lot gets trimmed automatically. - Auto-exclude pipeline already wired — runSinglePropertyAutoExclude runs the same multi-source detection as VOP (CV water + OSM roads + Sarasota / Manatee county GIS buildings + driveway topology, all Sutherland-Hodgman clipped to parcel hull). It was just silently failing or quietly succeeding with zero results — both invisible to the operator. - Diagnostics surfaced. Successful runs toast a one-liner naming source counts ("Auto-exclude complete — 10 buildings · 6 roads · 2 driveways"). Empty-result runs say so explicitly. Failures show the actual error message via toast instead of only console.warn. The operator can now tell at a glance whether the pipeline ran, ran-but-found-nothing, or choked. - Scan everything always re-runs exclusions. The orchestrator used to skip the AI exclusion pass when stale zones were already in state — broke the mental model. One click = full re-scan including exclusions. - Community scan prompt now has the same Phoenix species cues + row-planting guidance the per-parcel prompt got, so HOA-scale scans pick up boulevard / entrance palms.
Customer-facing palm exclusion transparency. Operator: "explain in the customer-facing documents what palms we leave out and why." The public read-only report now shows a dedicated amber-highlighted "Palm billing — what's included & excluded" section above the footer, naming all four excluded species (Sabal palmetto, Cuban royal, Mexican fan, California fan) with the contract reasoning and the door-open close ("If you want any of the four excluded species treated, that is available as a separately-quoted add-on").
Customer-facing oak species mix. The customer report now also shows "Oak species mix: Live oak ×3 · Laurel oak ×1 (billed per inch DBH at root injection)" derived from the species tags on saved tree_positions, so the customer can audit which oak species they're being quoted on at root injection cadence.
Follow-up push after the UX overhaul, focused on the per-parcel scan path that wasn't getting the same treatment as the community scan pipeline.
Render-time exclusion polygon clipping. Vision-LLM exclusion polygons routinely overshoot the parcel boundary on single- property scans (community scans already get hull-clipped server- side). New src/lib/polygon-clip.ts adds a shared clipPolygonToParcelHull helper; the satellite-map render effect now clips every exclusion polygon against the loaded parcel hull before drawing. Polygons that don't intersect the hull at all are dropped instead of drawn outside the boundary.
Save Survey buttons surface their failure mode. When saveSurveyAll was called with no surveyId it returned silently — operator clicked Save and saw nothing happen. Now shows "No survey loaded yet — run a scan or open a saved survey before pressing Save." The three Save Survey buttons also moved to PrimaryBtn for visual consistency with the rest of the modernized action row.
Tree-counting prompt strengtheners (per-parcel).
- Phoenix species discrimination: visual cues for Canary Island date palm, Medjool / true date, silver date, pygmy date, and Senegal date so the model picks a specific specieskey per pin instead of collapsing them all into one bucket. - Mexican fan / California fan palm (Washingtonia) excluded from billable count alongside Sabal and Cuban royal — too tall and structural for ground-based L&O injection. - Row plantings: when palms line an entrance drive or parking-lot frontage, the prompt now requires walking the row palm-by-palm and emitting a count + pin for every distinct crown. - SW Florida oak species ID: liveoak / laureloak / sandliveoak / wateroak / myrtleoak / chapmanoak / otheroak. Rolls up in the existing inventory species tally as "Live oak x3 · Laurel oak x1". - Ornamental beds without visible plant material (bare mulch, fresh-mulch installs awaiting plants) are now classified as hardscape, not billable ornamental sqft. - Mixed turf (St. Augustine + invading common Bermuda) now flagged with turftype_guess: "mixed" and a plain-language crops description so the L&O tech can plan resistant- population chemistry.
Customer-facing public report.
- The free-form notes field now leads with agronomic data (turf health, stress patterns, ornamental composition, palm/tree fertility, irrigation hints), not a storm-damage narrative. Storm observations get an optional one-sentence mention at the end only when actually visible. - Aerial caption shows imagery currency context: typical 6-18 month refresh on Esri / Google FL coast tiles, link to Google Earth's historical-imagery slider for verification, and the site-assessment completion date.
Reliability.
- Community scan now has a 130 s client-side timeout. Both /api/field-ops/site-survey/community-scan call sites abort after 130 s and surface "Community scan timed out after 2 min — the AI provider or imagery fetch is stalled. Try again, or run per-parcel batch scans instead." instead of leaving the "Starting… CLAUDE MEASURING ZONES 0/1 0%" progress card on screen forever.
Late-day push to make the Site Survey workflow less crowded and the AI more accurate. Highlights:
One-click "Scan everything" button. New amber primary action in the AI Keys & Circuits row. Selects every parcel in the loaded community, runs the AI exclusion (community footprint) scan if not already done, then fans out per-parcel batch scans. The granular Detect AI Exclusions and Scan N Parcels buttons remain for stage-by-stage control. Pre-scan confirmation banner now appears inline directly under the Scan button instead of hundreds of pixels above it.
Modern button styling. PrimaryBtn / SecondaryBtn switched to soft top-to-bottom gradients with shadow, ring, and a small lift on hover. Rounded-xl pill radius reads as modern. Same API and tones — purely cosmetic.
Pricing changes (commercial).
- Commercial turf: $500/acre → $350/acre ($8.03/K). - Commercial palms: $12/palm → $15/palm with $125 minimum. - Pricing engine auto-promotes to commercial when turf ≥ 1 acre regardless of operator-selected mode. Quote Lab and Site Survey pricing toggles show "Auto-applied: Commercial rates (X.X acres ≥ 1 acre threshold)" hint, and the Commercial button visually activates so the label matches what the engine is actually using. - Pricing context dropdown collapsed to two options: Residential and Commercial (estate / farm option removed; AI scan still auto-detects estate context internally based on parcel features).
Tree counting accuracy.
- Per-parcel and community AI scan prompts decouple treecount (RECALL — every visible canopy) from treepositions (PRECISION — pixel-precise pin subset). The AI no longer drops counts to match its pin-confidence; under-counting palms is now treated as the worse failure than approximate pins. - Mexican fan / California fan palm (Washingtonia) added to the excluded-from-billing list alongside Sabal palmetto and Cuban royal — too tall and structural for ground-based L&O injection. Visual cues for the model emphasize the trunk petticoat thatch and formal-row plantings. - Tree Inventory section now shows neighborhood-detected counts above the pinned list: "Tree inventory (38 detected · 4 pinned)" with bold count tiles for palms / oaks / shade / ornamental. - Community scan prompt strengthened so an HOA with mature canopies no longer returns 0 oaks / 0 shade trees. - Ornamental beds without visible plant material (bare mulch, fresh-mulch installs) are now classified as hardscape, not billable ornamental sqft.
AI exclusion zone editing.
- Removing an AI-detected zone now actually saves to the right table (communitysurveys for HOA, pasite_surveys for single property), with toast feedback ("Exclusion removed and saved") and an honest error message when the server rejects. - Undo button appears next to Clear All when removals are on the stack — restores the last removed zone at its original index. Stack supports multi-step undo (Undo (3) means three zones can be popped back). - AI zone groups default-collapsed (operator clicks a group header to inspect individual zones — useful at the 77-zone HOA scale).
Information density improvements.
- Past Week Rainfall card replaces the old Current Weather and Spray Window cards in the Site Survey intel panel. Shows 7-day total + a per-day bar chart so the operator can read rainfall distribution (single big event vs steady drizzle) for fertigation timing and granular vs liquid product choice. - Topography & Drainage card combines drainage class (moved from Soil card) with slope / elevation / relief. Verbose dynamic guidance based on slope × drainage combination. Honest flood-history note: FEMA NFHL overlay is a roadmap item not yet wired. - Tree Inventory and Tree Library & Map Filters sections default OPEN — they're pertinent data, not a buried detail. - Scan Reports by Street, Parcel List, and the AI zone groups default CLOSED — useful but space-eating; click the header to expand. - "Bulk HOA QA ops" panel removed (selection-count summary + QA-CSV export got no field uptake).
Map.
- Duplicate zoom controls fixed — Google's default suppressed, only the custom column with live zoom-level readout remains. - Fullscreen toggle button restored at the bottom of the zoom column. - Maps API loader race fixed (loading=async + importLibrary awaited) — root cause of the recurring "Map could not load" banner. Auto-retry no longer destroys the script tag.
Operator: "I did not want HOA inventory view to be a CSV. It should be digital." The Site Inventory section's primary green button used to read "Download inventory CSV" — making CSV feel like the deliverable. The whole page below the header IS already a digital view (Field Valuation Preview + By phase / stratum data table), so the framing was wrong.
Changes:
- Primary CTA is now Map & pins (jumps to the in-app community map). That's the most operator-centric next step from the inventory header. - Email board summary… stays as a secondary outline button. Body text in the email no longer says "Attach the CSV from Verdakai" — now says "Full digital inventory + per-phase rollup is on the Site Survey page; highlights below." - Export CSV moved to a small text link at the end of the button row, no longer styled like a primary action. CSV still available for operators who specifically need spreadsheet output, but no longer fronts the digital view.
The actual digital inventory below the header (Field valuation preview, by-phase table) is unchanged — it was already the canonical view; the header just stops pretending CSV is the answer.
The "Map could not load" first-load false positive has had three rounds of fixes today:
- Round 1 (582cc82): apiKey-arrival race — failure used to fire while the runtime-key fetch was still in flight. Fixed. - Round 2 (295a0f7): tile-fail timer was 6 s and used Google's tilesloaded event which doesn't always fire on Esri basemap. Bumped to 15 s + added idle event listener. - Round 3 (this commit): even after the script-load Promise rejects (network blip / browser extension race / transient failure), operators reported the second attempt almost always works. So the component now auto-retries once silently before showing the banner — clear the script tag, wait 400 ms, bump the internal retry nonce. Operators only see the banner if BOTH attempts fail. - Also added loading=async to the Maps JS URL per Google's current recommended loading mode. Without it, Google logs a console warning about deprecated loading; the warning was being caught by the page's error interceptor in some cases.
After three rounds, a fresh page load should show a working map nine times out of ten without any manual Retry click. If the banner still appears: it now means BOTH attempts genuinely failed — likely a real key / quota / billing issue, and the browser console will show the specific Google error code.
The per-parcel analyze prompt already excluded Sabal palmetto (cabbage palm) and Roystonea regia (Cuban royal) from tree_count.palm — Genesis doesn't treat them under L&O contracts, so counting them inflates the billable scope. The community-scope scan was missing the same rule, so HOA-level scans were over- counting palms whenever those species were present.
Added explicit identification rules for Claude:
- Sabal palmetto: single trunk with persistent leaf-base "boots," costapalmate (fan-shaped) fronds, very common roadside / natural-area volunteer. Florida state tree. - Roystonea regia (Cuban royal palm): 40–70 ft, smooth concrete-like gray-white trunk, prominent green crownshaft, large pinnate fronds arching downward. Formal plantings only. Not native to Florida.
When uncertain, the prompt instructs Claude to default to NOT counting — under-detection beats inflating the bill.
Three operator-requested improvements bundled:
(1) AI-detected zones panel — grouped + collapsible. A 53-zone HOA scan was rendering one multi-line row per zone (~2900 px scroll). Now zones are grouped by type ("Building footprint × 30 (88,742 sqft)", "Detected water body × 4 (76,124 sqft)", etc.) with the largest type expanded by default. Click any group header to expand and see / remove individual zones. Each zone preserves its original index for accurate removal.
(2) Recent property surveys — address search. New search input at the top of the Recent Property Surveys section (works in both Tiles and Compact view modes). Address-only contains-match, case-insensitive. Inline counter shows "12/49" when filtered, "49" when empty.
(3) Compact list right-side status pills. The compact list previously rendered nothing on the right side for non-complete surveys — operator asked why some scans had a summary and others didn't. Now every row carries something:
- analyzing → blue "AI scanning…" pill - geocoded → amber "Tap to scan" pill - walk_only → emerald "Walk in progress" pill - failed / error → rose "Scan failed" pill - complete → existing turf sqft + measured/scan source label - other → status text in gray
Operator-requested. The "Recent property surveys" gallery on the Site Survey idle screen used to be image-rich tiles only — fine on desktop but heavy when an operator needs to scan a long list of saved scans quickly. Added a Tiles / Compact toggle pill at the top of the gallery:
- Tiles (default): the existing horizontal-scroll image gallery. Big satellite thumbnail, address, date, turf sqft, status. Best for visually picking a property. - Compact: single-line per scan list. Address + date / status on the left, turf sqft on the right, action buttons (Continue walk / + Scan / ✕ Remove) inline. Best for browsing many saved scans without the visual weight.
Mode persists in localStorage (verdakai-recent-surveys-view) so once an operator picks compact, every visit honors it until they flip back. The same measurement-precedence logic (effective > operator_meta > raw AI) applies in both modes — the "measured" / "scan" suffix on the turf number tells the operator whether they're seeing operator-corrected or raw AI numbers.
Same rationale as the operator's "i keep losing scans when updating" report — making the saved-scan list more visible / scannable addresses the same UX gap.
The first round (582cc82) fixed the apiKey-not-arrived-yet race — the failure banner no longer fires while the runtime-key fetch is in flight. Operator was still seeing the banner on fresh page loads, clearable only by hitting Retry. Second root cause identified and fixed:
The 6-second tileFailTimer flips failed=true if Google Maps' tilesloaded event hasn't fired. Two reasons that fired spuriously:
1. The operator's default basemap is Esri (sharper than Google downsampled satellite for SW Florida residential). Esri tiles load via a custom ImageMapType registered with Google Maps — Google's tilesloaded event is tied to Google's own tile fetcher and doesn't always fire when Esri tiles arrive, even though the map is rendering correctly. 2. On spotty cell signal (truck cab / outdoor field work), 6 s is too tight for the initial Maps JS bundle + first-tile fetch.
Two fixes:
- Bumped tileFailTimer from 6 s to 15 s so initial loads on slow networks have headroom. - Added a parallel listener on Google Maps' idle event, which fires when the map has fully drawn AND is sitting still regardless of which mapType actually painted. If idle fires before the timer, we mark the map as ready and don't trip the failure path.
Combined, failed=true should no longer fire spuriously on fresh loads, even with Esri basemap on a slow network. The auth-failure paths (gm_authFailure callback + console.error interceptor) are unchanged — those still fire failed=true for genuine API key issues.
Follow-up to the GPS-walk species tagging commit (f863c64). The Property Intel Flora panel on /field-ops/properties/[id] now merges operator-tagged GPS-walked trees from aimeasurements.gpswalkedtrees into the same per-species rollup as the AI-detected treepositions. So a property where the operator walked the perimeter and tagged 3 Queen Palms + 2 Live Oaks reads:
Palms (3): Queen palm × 3 Oaks (2): Live oak × 2
…alongside any AI-detected species. Walked trees are reported at 100% confidence (operator ground truth) so the panel doesn't display a misleading "0% conf" badge.
Two operator-flagged issues bundled into one commit:
Pricing context dropdown. The "Property context" select on the Site Survey scan-quality cluster used to read Auto / Standard L&O / Estate-farm — terms that confused operators trying to pick the right pricing model. Relabeled to "Pricing context" with options:
- "Residential — suburban home / yard" (was "Auto") - "Commercial / HOA / large pad" (was "Standard L&O") - "Estate / farm / large rural" (unchanged)
Inline copy now states which Quote Lab rate card applies — Residential shows tiered $22–25/K turf, $27–33/K ornamental, $4/palm; Commercial shows $350/acre/visit turf, $20/K ornamental, $15/palm ($125 min). Internal SiteSurveyPropertyArchetypeMode values (auto / standard / estate_farm) are unchanged so the AI scan endpoints keep working without any back-end touch.
Community-scan oak counting. The community-footprint AI scan prompt was missing oak from its treecount schema — Claude could only return palm / shadetree / ornamental_tree, so oaks always displayed as 0 even on heavily-oaked Sarasota HOAs. Genesis bills oak root injection separately ($16–24/in DBH), so undercounting oaks here directly costs revenue. Added oak to the schema and wrote explicit prompt rules:
- Oaks read as wide spreading canopies (40–80 ft crown) with rough textured / mottled signature from above - Common FL oaks: live oak (dominant in Sarasota), laurel oak, sand live oak - Wide-canopy spreading hardwoods in Sarasota default to OAK, not shadetree - No double-counting (oak goes in treecount.oak only) - shadetree narrowed to non-oak / non-palm shade canopies - ornamentaltree narrowed to small decorative trees under ~25 ft
Per-parcel batch scan (analyze route) already counted oaks correctly — only the community-scope scan had the gap.
The Site Survey soil card was reporting pH 5.5–7.5 for SW Florida coastal soils (Sarasota / Manatee / Charlotte / Lee / Collier). That range is the native flatwoods baseline — irrigated / maintained residential and commercial properties read pH 7.0–8.2 in practice because of limestone parent material, decomposing shell fill, and high-bicarbonate well water. The native range was misleading operators into recommending sulfur amendments on properties where pH 7.5–8.0 is the natural maintained baseline.
The card now reports:
pH 7.0 – 8.2 (typical maintained surface range — limestone + shell fill + alkaline well water push native ~5.5–7.5 up; lab test confirms specific lot)
Notes block updated to reflect the practical consequence at this pH: limiting factors are Fe / Mn / Zn (which precipitate as oxides above 7.5); answer is foliar / chelated micros, NOT sulfur acidification unless pH > 8.2 AND acid-loving plants are present.
This matches the soil-test interpreter's pH baseline updated earlier today (commit 97dd5e8). Lab tests still override the static card.
Operator: "Ask Bubby window should not appear on printouts and PDFs." Added print:hidden Tailwind class to both the floating button trigger and the open-panel container in src/components/BubbyFloatingWidget.tsx. When the operator hits Ctrl+P or any print-style media query fires, both surfaces collapse so the service-map / property PDF renders clean for the customer.
Operator-requested. The GPS walk-perimeter screen already let operators "Drop tree at my GPS" with a broad type (palm / oak / shade / ornamental). Now there's a per-type species selector right above the drop button so each tagged tree carries its actual species (Queen palm, Live oak, Foxtail palm, etc.) into the per-property tree library that the customer sees on their property page.
What's in v1:
- Curated per-type SW Florida species list (palms, oaks, shade, ornamentals) — keys match what the AI vision pipeline emits, so species-brain.resolveSpeciesKey() resolves to the right botanical (queenpalm → Syagrus romanzoffiana, liveoak → Quercus virginiana, etc.) - "Unspecified" is the default — drop fast in the field, tag species later before saving - Toast on drop shows the species label when set ("Queen palm dropped"), or the type fallback ("Palm tree dropped") when unspecified - Persisted to pasitesurveys.aimeasurements.gpswalkedtrees[] with new species (snakecase key, normalized server-side) and notes (free-text, capped 500 chars) fields alongside lat/lng
API change: POST /api/field-ops/site-survey/walked-trees accepts optional species and notes per tree in the trees array. Both are sanitized on the server (lowercase + underscore + 60-char cap for species; 500-char cap for notes). Backward-compatible — old clients that don't send these fields still work.
Two operator-requested improvements:
(1) Scan progress in the sticky save bar. The sticky save bar at the top of the community-survey view now flips to a scan-progress view when either a community-footprint AI scan or a per-parcel batch scan is running. Shows:
- Spinner + scan label ("AI community scan" or "Per-parcel batch scan (12/83)") - Step counter: "step 2 of 3 · AI community scan" - A horizontal progress bar that caps at 95% while scanning so it never reads 100% before the scan actually finishes - The current detail string from the scan phase (community name, selected lot count, etc.)
When the scan completes, the bar reverts to its normal saved / unsaved state. No more wondering whether the AI is still working or stuck.
(2) Newest scan always auto-saves. The community-footprint scan function (scanCommunityFootprint) now explicitly calls saveCommunityRef.current?.() at the end of its success path. The existing useEffect-based auto-save (line ~3744) watches exclusionZones for changes — but if a scan returns 0 AI exclusions, the count doesn't change and no save fires. The explicit call closes that gap so EVERY successful scan persists without waiting on the operator to hit Save.
Companion to the Property Intel Flora panel that lives on /field-ops/properties/[id]. The Site Survey single-property detail view now has a "Detected flora" collapsible panel (closed by default so it doesn't crowd the satellite map). When opened it shows the per-species rollup of the trees / palms detected on this scan, grouped by type (Palms / Oaks / Shade trees / Ornamental trees), and the operator can click any species row to expand it inline with the catalog entry — botanical name, family, alternate common names, origin (native / naturalized / ornamental / invasive), Florida hardiness zones, mature height + spread, native range, care, pests, deficiencies, seasonal cues, catalog notes.
Library data is fetched lazily from a new endpoint /api/field-ops/species/resolve-keys (POST {keys: [...]}) — the operator only pays the round trip when they actually expand a row. The endpoint runs each key through species-brain's resolveSpeciesKey so AI snakecase (queenpalm, liveoak, pygmydate_palm, etc.) resolves to the correct botanical via the existing alias map.
Species without a catalog entry show a clear "no library entry" message pointing the operator to /field-ops/plant-library to add it. Once added there, future expansions show the data inline.
Why client-side fetch (vs server-rendered like the Property Intel panel): the Site Survey scan evolves live as the operator drags pins / adds manual trees / re-scans, so pre-resolving every species at request time would either miss late additions or force a full page reload on every change. Lazy fetch is the right primitive here.
Operator-requested. When a scan has produced numbers but no enabled exclusion zones exist (either none were drawn / detected, or all were toggled off), the community view now surfaces a conspicuous amber callout right under the green stats hero:
"⚠ Apply exclusions for actual treatable turf Billable turf is currently <X> sqft — that's the gross lot area with nothing deducted. Real treatable turf is lower once you exclude buildings, water, hardscape, common areas, and roads."
The body text adapts to the current pipeline state:
- No scan run yet: "Run the AI exclusion scan or draw zones manually below." - Scan ran with pipeline failures: "The last AI scan had pipeline failures (see Measurement methodology) — re-run or draw zones manually." - Scan ran cleanly but found nothing: "The last AI scan returned no exclusions. Re-run if conditions changed, or draw zones manually."
Includes a one-click CTA — "Run AI exclusion scan" or "Re-run AI scan" depending on whether one's been done — that fires the same scanCommunityFootprint action as the manual button.
Suppressed when: - Manual override is set (operator already locked the number) - Batch scan in progress - At least one enabled exclusion zone exists
The single-property view already had a similar "No auto-detected exclusions on this scan" reminder (the emerald "Run auto-exclude" banner) — that one stays unchanged for the case it covers.
The community-footprint AI exclusion pipeline (CV water → OSM features → County GIS buildings → driveway topology → CV hardscape fallback) used to swallow each step's failures with console.warn. On a community where one or more steps crashed silently, the operator would see "0 exclusions detected" and have no way to tell whether the property genuinely had nothing to exclude or the OSM endpoint timed out / the CV decoder errored / the driveway detector threw.
Per memory rule feedbackdiagnosticfirsterrorpaths.md, every silent failure now records a structured entry in the scan_provenance trail. The four wrap points covered:
- cvwaterdetectionfailed - osmfeaturesfetchfailed - cvhardscapedetectionfailed - drivewaydetection_failed
Each entry carries the error message (truncated to 300 chars) under detail.error.
UI surface:
- Top banner after a scan now appends "⚠ N pipeline steps failed silently — see Measurement methodology below for details." when any failures occurred. - Methodology panel rows for failure entries render in amber instead of the default gray/emerald, with a "⚠ FAILED" badge in the timestamp line and the error message highlighted. Both the standalone methodology panel and the scan-summary nested panel use the same rule.
The console.warn calls are preserved alongside the provenance push so existing browser-devtools workflows keep working unchanged. Customer- facing PDF methodology stays clean — failure entries are visible to the operator only.
The white "Selected / Total Lot Area / Avg Lot / Turf" summary card on the COMMUNITY PARCELS view used to show the 35% lot-area heuristic in the Turf tile whenever per-parcel batch scans hadn't rolled in yet. The green "Community footprint analyzed" hero card right below it would meanwhile show the actual scan-derived billable turf. On a community where the AI-exclusion pipeline returned zero zones, the two cards diverged sharply (white showed 185k, green showed 530k for the same property). Both numbers were internally correct for what they represented, but the inconsistency was confusing and bad to show a customer.
Fix: the white card now considers a community-footprint scan as sufficient evidence to switch off the heuristic, even if no per-lot batch scan has run. New prop hasCommunityScan on CommunitySummaryCard, sourced from the parent as scanProvenance.length > 0 || !!communityResults.
Behavior:
- No scan at all → 35% heuristic, label "Turf (est.)", sub-text "sqft (~35%)" (unchanged from before) - Community-footprint scan ran, no batch yet → scan-derived billable, label "Turf", sub-text "sqft · community footprint (−X sqft excl)" or "sqft · community footprint · 0 excl" - Per-lot batch scan ran → scan-derived billable, label "Turf", sub-text "sqft · N of M scanned (−X sqft excl)" - Manual override → "Turf (override)" (unchanged) - Scan ran but produced an implausibly low number (less than half the 35% heuristic) → falls back to the heuristic with "scan undercut" warning. Protects against the pipeline silently eating too much area.
Also surfaces "0 excl" on the sub-line when scan ran but found no exclusions, so the operator can see at a glance that the gross is the billable for that property and decide whether to draw manual zones.
After OAuth credentials rotated successfully through Planet Insights Platform, NDVI requests started failing with HTTP 400 from the Sentinel Hub Process API:
`` Invalid script! Band 'SCL' of collection 'S2L2A' requested in unsupported units 'REFLECTANCE'! Supported units for this band: DN. ``
Sentinel Hub tightened their units validation. The previous evalscript declared all three bands (B04, B08, SCL) in a single input group with units: "REFLECTANCE". SCL is a per-pixel integer class id (clear / cloud / shadow / water / snow / etc.) — it has no meaningful reflectance interpretation, so the API now rejects that combination outright.
Fix: split the input declaration into two groups so each band family gets the right units:
``js input: [ { bands: ["B04", "B08"], units: "REFLECTANCE" }, { bands: ["SCL"], units: "DN" } ] ``
B04 and B08 stay in REFLECTANCE because the NDVI math (B08 - B04) / (B08 + B04) expects normalized [0..1] values. SCL moves to DN so the integer class-id comparison (scl === 3 || scl === 8 || ...) for cloud/shadow masking still works on the same value space the API now provides.
See https://forum.sentinel-hub.com/t/units-improvements/4442 for the upstream change.
The Sentinel Hub OAuth credentials (SENTINELHUBCLIENTID / SENTINELHUBCLIENTSECRET) used to live ONLY in Cloudflare / Vercel env vars — rotating them required a dashboard redeploy on each platform. Now they follow the same DB-first pattern as Anthropic, Resend, and Google Maps keys: stored in organizations.settings (sentinelhubclientid / sentinelhubclientsecret), with env vars as fallback.
How rotation works now:
→ OAuth clients → Create new client / regenerate.
https://supabase.com/dashboard/project/dpofsrqirmojujhmeqfh/sql/new and run: ``sql UPDATE organizations SET settings = jsonbset( jsonbset( coalesce(settings, '{}'::jsonb), '{sentinelhubclientid}', '"<NEWCLIENTID>"'::jsonb ), '{sentinelhubclientsecret}', '"<NEWCLIENTSECRET>"'::jsonb ) WHERE slug = 'genesis'; ``
https://verdakai-test.pages.dev/api/field-ops/site-survey/ndvi-preview/health should return {"configured": true, "token_ok": true}.
without further env-var work.
What changed in code:
resolveSentinelHubCredentials() added to org-settings.ts — returns {clientId, clientSecret} from DB or env, undefined when neither has both halves
probeSentinelHubOAuth() in ndvi-sentinel-hub.ts now uses theresolver (was reading env directly)
isSentinelHubConfiguredAsync() added — DB-first check the routes use; the sync isSentinelHubConfigured() env-only check stays for any caller that needs a no-DB check
/api/field-ops/site-survey/ndvi-preview GET + POST and /api/field-ops/site-survey/ndvi-preview/health all use the new async path
Diagnostic-friendly behavior preserved: an invalid credential pair still surfaces tokenerror: "Sentinel Hub OAuth 401: invalidclient" in the health endpoint instead of silently returning 503.
Operators saw the "Map could not load" banner on a second satellite map (grid-compare imagery, freshly-mounted single-property map after a state change) and had to hit Retry for it to load. Cause: the component was initializing failed = !apiKey synchronously on first mount, which evaluated to true while the runtime-key fetch was still in flight. By the time the key arrived, the banner had already rendered.
Fix: defer the failure declaration until the runtime-key fetch has actually settled (succeeded OR failed). New logic:
- Initial state: failed = false regardless of whether apiKey is populated yet - A new effect watches apiKey + runtimeFetchSettled and only flips failed = true once we've tried and got nothing - The existing useLayoutEffect skips its setFailed(true) branch when there's no key — it just waits for the deferred effect
Net effect: maps that load slowly now show an empty container during the fetch (the same "loading" state they had before tiles arrive), then transition cleanly to loaded state. No more spurious banner / no more required Retry click on the second map.
When the operator looks up a new community via "Find boundaries" (loadBoundaries), the AI exclusion pipeline (CV water + county GIS buildings + OSM roads + driveways + Claude common-area inventory) now fires automatically once boundaries land. No more "Detect AI Exclusions" button click needed for the first scan. Already-saved surveys still auto-run via the existing reload path.
Gating — auto-trigger only fires when:
- The survey has no prior scan_provenance (not scanned before) - No AI exclusion zones already exist (won't clobber operator edits) - Not currently scanning (communityScanning / batchScanning false) - Boundaries actually returned parcels (zero-result lookups don't fire the AI)
Failures still surface in the same red error banner — the auto-run just removes the manual click; it doesn't suppress diagnostics. Cost parity: same scan, same Anthropic budget — just one less click.
Each exclusion zone in the Site Survey list now has a ✓/✗ toggle on its chip. Clicking ✗ skips that zone — it stays in the list (and on the map for visual reference) but does NOT subtract from billable turf. Useful for what-if pricing: "what's the deduction if we skip the side preserve?" without having to delete and redraw.
- Toggling marks the zone with enabled: false on save. Disabled zones round-trip through Supabase persistence so a reload shows the same state. - Disabled chips render at 60% opacity with line-through, the sqft display goes gray, and the type dropdown disables. - The "Total excluded" footer shows: kept-on total + a "· N skipped (X sqft, not deducting)" note when any are off, so the operator can see at a glance how much could be added/removed. - Both batch-scan mode and community-footprint mode honor the toggle. Batch mode still only counts manual exclusions, so skipped manual zones are removed from the deduction; AI zones in batch mode were never deducted to begin with (per the no-double-deduct rule). - Backward-compat: zones saved before this change have no enabled field; they're treated as enabled (counted toward billable).
A new "Property Intel — Flora" panel sits on /field-ops/properties/[id] between the AI Survey Measurements card and Active Programs. It rolls up the latest Site Survey AI auto-scan into a per-species breakdown grouped by plant type (turf, palms, oaks, shade trees, ornamental trees), with click-to-expand library entries inline.
What it shows:
- Turf row populated from fieldopsproperties.turftype. The library entry is pulled from the species catalog by common-name search (e.g. "St. Augustine grass" → Stenotaphrum secundatum). - Palms / Oaks / Shade trees / Ornamental trees rolled up from pasitesurveys.treepositions. Counts are per-species inside each type bucket. AI confidence shown per species when reported. - Each row is a clickable header that toggles open. The expanded panel shows: botanical name, family, alternate common names, origin tag (native / naturalized / ornamental / invasive), Florida hardiness zones, mature height + spread, native range, care notes, pest/pressure notes, deficiency triggers, seasonal cues, and any free-form notes from the catalog. - Species without a catalog entry show a "No library entry" badge and a hint pointing the operator to /field-ops/plant-library to add it. Future scans pick up the new entry automatically.
How it stays out of the way:
- Only one inline panel per property — no separate page navigation, no map of its own (the operator was clear: "should not interfere with the map view"). - Hidden when there's no scan data and no turf type set. - Library lookups are deduplicated server-side (one round trip per unique species key, even if the property has 30 of the same palm). - The species-brain catalog import is dynamic so it only enters this route's Cloudflare Worker bundle, not the shared chunk.
What's NOT in this version yet:
- Per-plant identification of ornamentals (the bushes / hedges in the ornamental sqft). Today only TREES are auto-identified by species; the ornamental_sqft is just a measured area. - Tags / notes per species per property. The library entry is catalog-wide. Operator-attached notes are still in the property notes field for now.
The soil-test interpretation prompt was applying generic textbook "pH 6.0–7.0 is ideal" reasoning, which is wrong for Sarasota. Native + irrigated SW Florida coastal soils naturally read pH 7.0–8.2 due to limestone parent material, decomposing shell deposits, and high-bicarbonate well water. The AI was consistently flagging normal local readings as "high" or interpreting acceptable readings as too-low relative to a baseline that doesn't apply here.
The prompt now leads with explicit Sarasota / SW Florida coastal pH thresholds:
- Low for Sarasota: pH < 6.5 (rare, almost always amendment-related, not the natural baseline) - Normal range: pH 7.0–8.2 — agronomically acceptable for St. Augustine, Bahia, Zoysia, queen palms, oaks, and most landscape ornamentals. Do not flag as "high" or recommend sulfur acidification. - High: pH > 8.2 — sulfur amendment justified only when acid-loving plants (azaleas, gardenias, blueberries, ixora) are actually on the property. - Practical consequence of pH 7.5–8.0: Fe / Mn / Zn precipitate as oxides and are less plant-available. Chlorotic turf or palms at this pH should get foliar / chelated micronutrient delivery, NOT pH lowering. - Lab software's generic "high" flag in the interpretation column is now explicitly told to be ignored — it doesn't know Florida coastal context.
This affects the AI-generated interpretation and recommendation_summary only; the extracted numeric pH value is unchanged (that's parsed straight from the lab PDF).
When Claude fails to extract a soil-test PDF (scanned-image PDFs with no extractable text, rate limiting, transient network errors), the failure used to be buried inside the green "Interpretation" panel as the body string (parse failed: ...). Visually it read the same as a successful interpretation; an operator skimming an old test wouldn't notice the parse never produced lab values.
Now the failure is surfaced explicitly in two places:
- List view: rows whose parse failed get a small rose Parse failed pill next to the customer / sample name. No need to open the row to know which records still need attention. - Detail sheet: when a failed row is opened, the green Interpretation panel is replaced with a rose error banner that states the actual error message and suggests re-uploading. Lab values, interpretation, and recommendations are correctly shown as unavailable rather than implied to exist.
The on-disk format hasn't changed — interpretation = "(parse failed: <error>)" is still what the upload action writes. The new helper at src/lib/soil-test-parse-status.ts parses that marker once so the format is auditable from one place.
A long single-day Site Survey push focused on the operator's "Detect AI Exclusions" pipeline plus the way customer-facing PDFs and Quote Lab consume the resulting numbers. Driven by an operator-reported series of mismatches, hallucinated polygons, and PDF copy that read as operational promises the field couldn't honor.
Driveway detection via building↔road topology. Driveways aren't in any free authoritative dataset (county GIS skips them; OSM has them only sporadically). The new detectDrivewayPolygons module classifies each pixel against the existing hardscape signature, removes anything inside a building footprint or road buffer, then keeps only connected components that touch BOTH a building polygon AND a road buffer within 6 m. That topology constraint is what makes CV reliable here — random patio / sidewalk patches don't bridge a building to a road, so they get rejected. 150-2500 sqft band per component (residential driveway range), Chebyshev O(N) two-pass dilation for the proximity check. Slots into scanCommunityFootprint after county buildings + OSM roads complete. Each result is clipped to the parcel hull via Sutherland-Hodgman, same as every other AI exclusion source.
Turf sqft parity between summary card and detail view. Operator opened a saved community and saw a different turf number on the list-view card vs the detail page. Cause: the detail view recomputed gross from parcel rings (the deterministic shoelace fix from 2026-04-30), but the summary card kept reading the persisted aggturfsqft — which could be a stale Claude estimate, or simply unwritten because the auto-PATCH after Detect AI Exclusions only persisted exclusion changes. Fix: extracted parcelPolygonAreaSqft to site-survey-batch-rollup.ts as the single source of truth, used in both resolveCommunitySurveyMetrics and the live detail view. Auto- PATCH now also persists aggturfsqft and netturfsqft alongside exclusions. The cap-to-totallotsqft still fires when ring data is unavailable (fallback only) — ring area is geometric truth and isn't capped.
Quote Lab now reads parcel rings. loadQuickQuotePickerData was selecting communityresults, aggturfsqft, netturfsqft from communitysurveys — but NOT parcelboundaries, so the shared resolveCommunitySurveyMetrics fell back to the persisted (potentially stale) gross. Quote Lab now selects parcelboundaries and exclusion_zones; same shoelace gross the survey detail uses, no more divergence when handing a community over to Quote Lab.
AI exclusion zone list collapsible. Auto-detected scans on dense subdivisions can yield 30-50 chips (water + roads + buildings + driveways). The list dominated the UI between the map and the action buttons. Now the chip list collapses by default; the header shows count + total sqft and toggles open on click. Manual zone draws still surface immediately because the header reflects them in the count.
Verbose measurement provenance — in-app + customer PDF. Operator: "Site survey should have extremely verbose data on how the measurements were obtained and what tools and technologies were used." Implemented end to end:
- scanCommunityFootprint records a structured scanprovenance array on every Detect AI Exclusions run. Each entry has at, action, and a detail object listing the tool, upstream source, classifier, parameters, and result counts. Steps captured: scan started, AI inventory returned, CV water detected, county GIS buildings fetched (with the actual ArcGIS endpoint URL), OSM roads + buildings fetched (with the highway-tag width buffers), driveways detected (with the topology constraint), gross turf computed (shoelace source), net turf math. - New "Measurement methodology" collapsible panel under the exclusion zone list shows every entry — timestamp, action, detail dict — with a small footer that says "the same data ships in the customer-facing PDF's Methodology section." - Persisted under communityresults.scanprovenance. Hydrated on saved-community load. Carried forward through the explicit Save flow (does NOT overwrite a prior trail with empty data). - The community PDF route now reads scanprovenance from the communitysurveys row when communitysurvey_id is in the body and renders a "Measurement Methodology — Detail" section immediately after the existing Methodology table. Two-column layout: action on the left, parameter dict on the right. Foot copy: "Pipeline order: AI scan (identification) → CV water → County GIS buildings → OSM roads → CV driveway connectors. Every polygon clipped to parcel hull via Sutherland-Hodgman."
Service Calendar block removed from customer PDFs. The "Proposed Service Calendar" was a per-month script ("April: pre-emergent #2…"). Genesis is year-round and treatments are diagnosis-driven, not date-driven — the calendar read as operational promises the field couldn't always honor. The "year-round diagnosis-driven" framing already lives in the included-services block, so no replacement needed.
Per-Property Breakdown hidden from customer PDFs. On a 200-home HOA the per-lot table generated 200 rows of address + turf + ornamental + palms + oaks + trees inline in the customer proposal. Now gated on crewMode === true — the route table still ships in the crew packet PDF (techs need it for routing), but customer-facing PDFs stop at the street rollup.
Help & Reference re-architected as plain static HTML. The operator hit a 404 on /field-ops/help because Cloudflare's build was stripping the route entirely (per a workflow comment about readFileSync being unreliable in next-on-pages). After trying multiple Next-route variants (prebuild script generating a TS module, Cloudflare 3 MiB Worker bundle limit kept biting), the help routes are now plain HTML files generated at build time into public/field-ops/help/. Cloudflare's CDN serves them directly with zero Worker bundle cost. The prebuild script (scripts/sync-help-docs.mjs) ports the minimal markdown→HTML renderer from src/lib/markdown.ts inline so there's no new npm dependency. routes.json is patched post-build to exclude /field-ops/help/* from the Worker so the static assets win. Both Vercel and Cloudflare run the prebuild via the npm prebuild hook — change docs/VERDAKAIMANUAL.md and the next deploy refreshes both surfaces automatically.
A long second-half push covering education-content rewrites, the new Spray Crew compliance section, and a UI tone change across Plant Brain.
PCO question bank rebuilt + expanded. Operator scored 9% on the previous bank (24-year industry veteran), exposing systemic issues: answer-length bias (correct answer always 3-5x longer than wrong answers), academic-style stems, and at least one factual error around § 482.071 ("full-time" was not in the statute). Scrapped and rebuilt to FDACS exam style — single-clause stems, plain 4-option answers of similar length, elaboration in the explanation field. Grew from 77 → 138 questions. Math now 9.2% (matches the ~10% real-exam balance). Coverage organized to mirror the SP-251 Core manual chapters plus L&O / Cat 3 Florida content. Each fact verified against Florida Statutes Ch. 482 / 487, FAC 5E-14, FIFRA / 40 CFR, EPA, FDACS, UF/IFAS EDIS, IRAC / FRAC / HRAC MOA classifications.
Photo weed-ID questions for PCO. Question schema extended with optional image (URL) and imageCredit (license). Quiz UI (coach-modal, exam-mode-modal, education page) renders the image above the stem. Seven photo-ID questions added covering common Florida L&O weeds — goosegrass, dollarweed, yellow nutsedge, Florida pusley, crabgrass, doveweed, spotted spurge — hot-linked from Wikimedia Commons via Special:FilePath URLs (which remain valid even if the source file is renamed) under CC licenses.
ISA Certified Arborist bank rebuilt. Same exam-style rebuild PCO got. 110 → 62 questions. Coverage tracks the ISA Certified Arborist exam outline: tree biology, soil and roots, water + nutrition, pruning (ANSI A300), installation/establishment, diagnosis, construction protection, risk assessment, ANSI Z133 work safety, support systems, lightning, urban forestry. Florida content integrated for oak wilt, laurel wilt, lethal bronzing, palm Mn / Mg deficiencies, Hypoxylon canker.
CCA Certified Crop Adviser bank rebuilt. 100 → 62 questions covering the ASA/CCA International + Florida Local Board content outline — Nutrient Management (~35%), Soil & Water Management (~30%), IPM (~15%), Crop Management (~15%), with Florida-specific sandy-soil + BMP / FDACS programs woven in. Verified against ASA/CCA Performance Objectives, Brady & Weil 16th ed., IPNI 4R Stewardship, USDA-NRCS, USDA Salinity Handbook 60.
GI-BMP question bank (new). The gi_bmp track was previously listed in the UI but had NO bank — it ran zero questions. Built a 39-question bank covering UF/IFAS Green Industries Best Management Practices curriculum: program scope, fertilizer rates + rainy-season blackout + 10-ft setback, FL sandy soil + reclaimed water, irrigation BMPs (rain sensors, depth, timing), Florida- Friendly Landscaping nine principles, mowing BMPs, pesticide BMPs, stormwater + TMDL + BMAP, pollinator protection, fertilizer math, records + renewal. Verified against § 403.9337, § 482.1562, § 373.62, F.S. and FDEP TMDL/BMAP program docs.
Sentinel Hub / NDVI activated. OAuth credentials added to both .env.local and the Cloudflare Pages secrets store (SENTINELHUBCLIENTID + SENTINELHUBCLIENTSECRET). The NDVI overlay is no longer "planned" — it's live in Site Survey. See section 8.x (NDVI subsection) for the operator workflow, practical L&O uses (pre-visit triage, quote justification, before/after documentation, irrigation diagnostics), and caveats (10m resolution, ~5-day revisit, FL summer cloud cover).
Spray Crew FDACS § 482.091 compliance section. New mowertrainingrecords table (migration 054) + API + crew-detail UI panel exposed only when crew_type='spray'. Tracks per-employee training events with date, hours, topics covered, trainer/course, optional CEU provider number. The compliance banner at the top of each spray-crew member's detail page tells the operator at a glance whether the trailing 12 months meet the >= 2 hr / pesticide-safety + IPM + laws-and- rules requirement. Records are the audit artifact FDACS may demand under FAC 5E-14.1421. Spray Crew form labels also flipped from "Company" to "Name" (operator runs Genesis only — not a multi-vendor portfolio).
Edge cookie session fallback patched at the source. getServerSession now falls back to parsing the raw Cookie header via headers() when next/headers cookies() returns null on the Cloudflare edge bundle. This single fix patches every server action that uses requireActionRole — was breaking "delete quote" and "delete crew member" with confusing "dashboard error" / forbidden responses. Mower DELETE route also explicitly uses the dual-read auth pattern with NextRequest cookies as the primary source.
Quote Lab PDF map: server-side embed. The customer-facing aerial map was sometimes "all black" because the hot-linked Esri PNG didn't always finish loading before the browser print engine snapshotted to PDF, leaving the dark fallback background showing. Fix: server-fetches the Esri image, encodes to a base64 data URL, and embeds it directly in the HTML. The print engine has nothing to wait on. 12s timeout on the server fetch so the operator never hangs — falls back to a hot-link only if the server-side fetch itself fails.
Help & Guide pages 404. All three help routes (/field-ops/help, /manual, /pamphlet) were 404ing on Cloudflare because they used readFileSync('fs') at request time and force-dynamic pushed them into the request-time function bundle, where fs is unavailable on the edge runtime. Removed force-dynamic so the markdown is read at next-build time and the result is served as static HTML — same behavior on both Vercel and Cloudflare.
Properties: Danger zone with Delete property. Operator request: "anything created should have a way to be deleted safely." Properties detail page now has a Danger zone with a red Delete property button. Soft-delete (is_active=false) so all linked records — activity log, scouts, surveys — stay in the database and can be restored via Supabase admin. Redirects to the list after delete.
Property Audit (Scouting) hidden from nav. Operator: "remove property audit, it's no good." Removed from both navigation catalogs (desktop drawer + mobile bottom tabs). Route still resolves for saved bookmarks — full deletion deferred until operator confirms.
Plant Brain visual tone — reference-tool, not playful. Operator: "looks like it's made for little kids, turn this into a section for professionals." Source badges replaced — emoji plus casual labels (🌿 Catalog, 🐛 Pest, 💊 Treat) replaced with monospace 3-letter codes (CAT, PST, DIS, TRT, LEG, MAP, FID, PAR) on outlined low-saturation chips. Filter buttons squared, smaller, uppercase tracking. "Genesis ✓" badge replaced with "Implementation". Detail-page tone chips use outlined borders instead of bright fill backgrounds.
Bubby UX polish. Launcher is now icon-only on desktop (expands to "🤖 Ask Bubby" on hover). Chat panel no longer dims the page when consulted on desktop — the panel docks in the bottom-right and the rest of the page stays interactive. Launcher is draggable on desktop with localStorage-persisted position; double-click resets to corner. Auto-hides while any input is focused or while the panel is open.
After the receipt-PDF debugging marathon (next section) settled the Site Survey export pipeline, an audit + harden sweep ran through the rest of Field Ops to make sure the same kinds of silent failures couldn't bite anywhere else.
Edge-cookie auth fallback expanded to 11 more routes. Every field-ops API handler that calls getServerSession() now also calls getSessionFromRequest(req) first — next/headers cookies() returns null on the Cloudflare Pages edge bundle even when a session cookie is set. Routes patched: photos, mowers, programs, species, quote-pdf, directory, presence, pa-programs, maintenance-company, edenpro-health, ai-knowledge. Combined with the morning's site-survey sweep, every authenticated field-ops route now uses the dual-read pattern.
Outer try/catch + diagnostic field on more routes. Same shape as receipt-pdf (uncaught exception → friendly error JSON with the actual exception message + first 3 stack lines as diagnostic). Applied to photos GET/POST/DELETE and directory GET. Quote-pdf, estimate-pdf, community-pdf got the treatment earlier in the spree.
Dispatch reassign-visit no longer silently swallows errors. The empty catch {} was eating fetch failures so the operator clicked a tech and got no feedback when the reassign didn't go through. Now extracts the diagnostic from the response body and surfaces it via an alert (proper inline UI is a follow-up).
Hardcoded Vercel URLs replaced with runtime base-URL resolution. The security-alerts and security-digest cron paths had https://verdakai-sigma.vercel.app/... baked into outgoing email links — those would 404 since Vercel was paused. Both now resolve NEXTPUBLICAPPURL → SITEPUBLICBASEURL → verdakai-test.pages.dev fallback at send time.
Verify CI workflow. Has been failing all day on a package-lock.json sync issue (multiple esbuild versions transitively required, lockfile only carried one). Lockfile regenerated with npm install --package-lock-only. tsc + vitest still green. The actual Cloudflare Pages deploy was never blocked by Verify — they're parallel workflows — but the red badge was masking real signal on every push.
A morning of operator-driven fixes plus two real features. Ship list:
Site Survey API edge-cookie auth. PDF download and email buttons were returning 401 "you're signed out" on the Cloudflare Pages deploy because next/headers cookies() doesn't reliably wire up on edge route handlers. New getSessionFromRequest(req) helper reads cookies straight off NextRequest.cookies and is now the primary session source (with getServerSession() as fallback) across 16 Site Survey routes — including survey-receipt-pdf, walked-zones, walked-trees, confirm-species, parcel-diff, work-order-export, ai-status, ai-circuit-reset, cancel-analyze, scan-fleet-snapshot, public-link, public-ortho-overlay, gold-set-run, ops-health, and reference-map POST/DELETE.
Modern email compose for the Site Survey PDF. The "send to customer" dialog is now a real compose window: pre-filled editable subject (Site survey measurement summary — <address>), optional Cc with a "Send a copy to me" toggle that auto-adds the operator's email, optional 2,000-char message rendered as a "Note from sender" card in the email HTML, and a green Sent confirmation state with the Cc list shown. POST handler accepts subject/cc/ message, sanitizes (CRLF strip on subject as a header-injection guard, ≤5 valid Cc, dedup against To), and passes through to Resend. Telemetry log adds cccount, hascustomsubject, messagechars.
Scan-not-found diagnostic split. When survey-receipt-pdf can't load a row, distinguish a real DB error (RLS / service-role- key gaps on edge — anon key hits RLS, query errors, user sees "not in database" even though the row is there) from a truly missing survey. Switched to .maybeSingle(), log the pg_code on errors, and serve a 503 "couldn't reach the database" page instead of the misleading 404 when the row IS there but the query fell over.
Quote PDF + Estimate PDF: full-property map + verbose data. Quote Lab's PDF was framing only a small spot in the middle of the parcel because the handoff stripped parcelrings and quote-pdf fell back to the fixed-zoom-19 preview around mapcenter. Round-trip the rings (Site Survey → handoff context → quote-pdf POST body) so the bbox-fit Esri image path engages and the whole property fits within the parcel boundary like every other customer-facing surface.
Both customer-facing PDFs now lead with a Property Snapshot table — treated turf with acres in parentheses, gross footprint with excluded sqft, ornamental beds with density, palm/oak/shade/ ornamental tree counts, GPS-walked perimeter totals when present, and the source label (operator-corrected / GPS-walked / AI- measured) on each measurement so the customer can audit where the number came from. Quote PDF adds a Cadence Comparison table (same per-visit price across 4/8/12 visit cadences with the 4-visit row flagged Inadequate, the 12-visit row Recommended, and a coastal-FL liability footnote) plus a Walked-zone provenance notes box. Estimate PDF gets its own aerial map block from surveytrace.parcelrings. Quote Lab's customer-signature canvas is removed; the proposal isn't where customers sign.
Standalone GPS walk + bidirectional scan merge. The walk feature no longer requires a prior satellite scan. New entry page at /field-ops/site-survey/walk-perimeter (no [surveyId] segment) holds a watchPosition GPS fix until accuracy is tight, optionally captures address/customer name, and POSTs to /api/field-ops/site-survey/walk-only-start which creates a pasitesurveys stub with status='walk_only'. The mobile GPS Walk FAB no longer gates on a loaded survey — with a survey loaded, the walk attaches to that scan; without one, it routes to the standalone entry. Walked zones / GPS-walked trees persist exactly as they do for scanned rows.
The reverse direction ships too: walk-only stubs in the recent- surveys gallery render a "Walk" badge plus two action links — "Continue walk →" and "+ Add satellite scan." The latter seeds the search field with the address + lat/lng + surveyid of the walk- only row; the existing geocode endpoint already accepts surveyid and reuses it, upgrading the row in place. Status flows walkonly → geocoded → analyzing → complete without ever changing the row id, so walked zones (FK surveyid) and GPS-walked trees (in ai_measurements) stay attached. Five operator paths now converge on a single row: walk first, walk only, scan first, scan + walk, walk → add scan later.
PCO practice exam aligned with Florida Chapter 482. The Education Center's PCO track was internally contradictory ("FL Commercial Pesticide Applicator (PCO)") and pointed at the wrong FDACS page. Per UF/IFAS PI292 + FL Statute § 482.021 + FAC Rule 5E-14, an L&O lawn-spray business holds a Pest Control Operator's certificate (PCO L&O) under Chapter 482 — NOT a Commercial Pesticide Applicator license under Chapter 487 (those are separate FDACS programs; Ch. 487 isn't valid for residential or within 10 ft of buildings). Track relabeled FL Pest Control Operator — L&O (Ch. 482) and the source URL flipped to the FDACS Pest Control Licensing page.
Eleven existing PCO questions corrected: the records-retention rule cited as 5E-14.117 F.A.C. (a rule that doesn't exist) is fixed to 5E-14.142 in two places; questions citing Ch. 487 for L&O label-compliance / fumigation / record-keeping reframed to Ch. 482 / FAC 5E-14; one question whose stated correct answer claimed Ch. 487 governs PCO licensing — factually wrong — fully replaced with a 482-vs-487 explainer that quotes UF/IFAS PI292.
Plus 15 new questions covering both required exams: § 482.071 (certified operator required in each category at each business location), § 482.091 (ID Cards: 30-day issuance, 2 hr/yr cardholder training, joint licensee/operator responsibility), § 482.021 verbatim "lawn"/"ornamental" definitions, Ch. 482 vs Ch. 487 boundary at sod farms / commercial nurseries, two pesticide-math problems (rate × area, a.i. lb/gal → fl-oz product), Southern chinch bug as primary St. Augustine pest (UF/IFAS EENY-226), tropical sod webworm seasonality, large patch (Rhizoctonia solani) trigger, take-all root rot pH BMP, lethal bronzing (16SrIV-D phytoplasma — formerly Texas Phoenix Palm Decline), frizzletop (Mn deficiency in palms), doveweed control program, and the Florida summer fertilizer blackout under § 403.9337 + GI-BMP. Total PCO bank: 115 questions.
A targeted operator-driven fix list. Three threads, all in production.
Program Architect — April Turf Builder Brandt duplication fixed. public/pa/index.html had LEGACYSKUALIASES.SOP = "23014BRN025" — that pointed the SOP alias at BRANDT Agra Sol Micro Mix's SKU. The April Turf default has both {sku:"SOP", rate:10} and {sku:"23014BRN025", rate:2.5} lines, so the bad alias rendered BOTH as "BRANDT Agra Sol Micro Mix" — once at 10 lb (SOP rate, wrong product) and once at 2.5 lb (correct Brandt rate). The same alias table in app.html, src/lib/field-ops-product-label.ts, and src/lib/program-intelligence.ts already had MP50POT (Diamond K UltraFines 0-0-50 100% SOP). Only index.html was the fork-artifact bad copy. After the fix, April Turf shows one Diamond K SOP line at 10 lb and one Brandt Agra Sol Micro Mix line at 2.5 lb — distinct products, distinct rates.
Quote Lab crew field packet — verbose + load-out math. Quote Lab now passes mulch yardage / sod pallets / turf and ornamental sqft / soil temp into the community-pdf route. Three new sections:
mulch yd³ at 3" depth (ornSqft / 108), sod pallets at 500 sqft / Floratam pallet, with the coverage math spelled out so customers can audit it. Crew mode drops the cost column and reframes as load-out spec.
soil temp into the chemistry the truck pre-stages: N / K / Iron / Brandt micros at temp-band rates from CLAUDE.md, with BANDIT + Crosscheck unlocked above 75°F and the K rate bumped above 80°F. Tank counts at 60K turf / 10K ornamental coverage per 150-gal tank.
zones, oak injection coordination (no broadcast within 6 ft of trunks flagged for injection), palm species injection eligibility (Bismarckia, Phoenix canariensis, Adonidia, Sylvester, Pygmy Date ARE injectable; copper is never OK on any palm).
Field notes uncapped in crew mode. Client PDFs still cap at 10 to keep proposals scannable.
Quote Lab — PA-style 2-column composition area. Continuing the Program Designer port. Inputs (customer, pricing mode, property size, services, visits, specials) sit in the left column; live calculation outputs (price summary, profitability, AI estimate language, annual comparison) sit in the right column. Bulky long-form cards (one-time materials, agronomic notes, signature, save banner) span full width below. Mobile collapses to a single column. Pairs with the sticky header that landed earlier this push.
Plant Brain — images + Genesis-anchored treatment text. Two catalog complaints addressed in one shipping wave.
/api/field-ops/plant-brain/augment (admin only) fetches Wikipedia REST page-summary images (scientific name first, common name fallback) and saves to imageurl + imagesource. For treatments, also passes the org's actual fieldopsproducts catalog to Sonnet 4.6 and asks for a 2-3 paragraph implementation block anchored to specific SKUs, rates per 1,000 sqft / per palm / per DBH inch, and Sarasota-Manatee soil-temp triggers. Genesis voice rules baked in: no equipment names, no fixed-script promises.
/field-ops/plant-brain/admin shows per-kindtallies of rows missing image / Genesis text and runs the augmenter in batches (default 10, max 25) without clicking each row. 250 ms gap between Sonnet calls so long runs don't trip Anthropic tier-1 rate limits. Session-local run log surfaces applied fields per row plus any errors.
detail page (treatment / pest / disease) for ad-hoc re-augmentation when the catalog row gets edited.
treatments.genesis_implementation + treatments.genesisimplementationupdatedat and an imagesource column on all three tables. Two-pass write tolerance so the augmenter runs against pre-052 schemas (image fetch still works without the new columns).
and the two-pass write fallback; total suite is 244 passing across 46 files.
A long extension of the April 29 work above. Same throughline: ground-truth measurements over AI raster output, every customer document on a single brand template, and Plant Brain elevated to peer-reviewed agronomic depth.
Walk the Perimeter (new feature, end-to-end)
/field-ops/site-survey/walk-perimeter/<surveyId>. Tech taps Start, walks the boundary of a turf / ornamental / palm-bed area with the phone, taps Finish, and the polygon's area + raw GPS track persists to the new sitesurveywalkedzones table (migration 053). State machine: idle → warmingup (5 s GPS gate) → recording → paused → finished → saved. Pause is first-class so techs can step around shrubs without bogus vertices.
Shoelace formula for area, Haversine for perimeter. No turf.js dependency. 20 vitest cases lock the math in, including the spec 100 ft × 50 ft rectangle round-tripping to 5,000 ft² ±2%.
every state transition, 56 px minimum touch targets, sun-readable persistent header (live area / perimeter / GPS accuracy / elapsed / point count), aria-live area announcements every 5 s, auto-close pill when the operator returns within 3 m of start moving slowly, drift-snap when end is within 5 m of start, sanity gates before save (<4 points blocks; <100 sqft / >5 ac / self-intersecting confirm), tap-to-delete vertex editor.
Esri World Imagery overlay toggle, parcel ring rendered in dashed gray for context, walked path live as polyline / closed polygon on finish, current position dot with translucent emerald accuracy halo.
Save-to-Properties button plus an inventory list of zones already saved for the survey (per-zone pills with type / sqft / perimeter / walked-at, aggregate sqft per zone-type in the section header). DELETE endpoint with admin-or-owner gate so the tech who walked a zone can undo it.
a survey has walked turf or ornamental zones, those totals override the AI raster measurement in the live pricing inputs. Emerald banner above the existing scan-import banner shows the override clearly. Closes the value loop — the GPS-walked boundaries actually flow into pricing, not just into a measurement log.
comments): vertex DRAG-to-adjust + LONG-PRESS-midpoint-to-add need a touch-aware Leaflet edit layer; offline tile pre-cache needs service-worker plumbing.
GPS tree mapping (v2 of the walk-perimeter screen)
panel, and a colored pin lands at the current GPS lat/lng. Pins persist to pasitesurveys.aimeasurements.gpswalked_trees via POST /api/field-ops/site-survey/walked-trees when the walk saves.
Pin colors and emoji match the rate-card buckets (palm 🌴 emerald, oak 🌳 amber-800, shade 🌲 green-600, ornamental 🌸 purple, other 🌿 slate). Tap a pin on the map to remove it before save.
- Walk perimeter (default) — same flow as before, plus the Trees panel above the Pause/Finish buttons. Operator can drop trees at any moment during the perimeter walk. - Map trees only — toggle button under the Start Walking button. Skips the polygon-record path entirely; HUD shows tree count instead of square footage; primary action becomes Save trees. Useful when the operator just wants to inventory a property's palms / oaks without rewalking the boundary.
section under Tree Inventory. Operator-walked counts supersede AI detection counts for billing decisions.
tree_positions (AI vision pixel coords) and gpswalkedtrees (operator real-world lat/lng) so the system can compare them later for AI accuracy tuning.
Site Survey scan reliability
When the operator picks DeepForest and the route returns 503 sitesurveydeepforest_unconfigured, the workspace silently retries against /llm-detect with the equivalent body. Surfaces an amber notice on the run summary so the operator knows Sonnet vision actually ran.
instead of DeepForest framing. The previous "Zero detections at threshold 0.20 / DEEPFORESTSERVICEURL" message rendered after every LLM-detect zero result, which was wrong end-to-end. New copy explains the three real causes (open lot / parcel-clip too narrow / off-nadir-or-shadow imagery) and points to the manual Trees palette.
expand the full dropdown + currency warning + attribution. Earlier full-panel position covered ~30 % of the map view on phones.
PDF map fitting + region awareness
with a 6 % breathing margin, instead of expanding the bbox to a fixed 1.5 : 1 image aspect that left ~50 % dead space around square / tall parcels. New esriBboxImageryFit returns the URL plus the picked image dimensions so the rendered img tag matches what ArcGIS sends back without browser stretching.
src/lib/survey-region.tsclassifies the survey into floridacoastalsw / floridaother / temperateus / unknown by lat-lng (with a county short-circuit). community-pdf gates the Sarasota-specific service calendar, risk profile, methodology copy, sod material wording, and liability thresholds on the result. Out-of-state scans (Illinois, Ohio, etc.) get a temperate-US calendar (cool/warm-season transitions, no palms) and risk list (crabgrass, Japanese-beetle grub, fall broadleaf, snow mold) instead of palm + chinch-bug claims that don't apply.
Quote Lab Email PDF actually sends
mailto: link with body text instructing the operator to manually attach a PDF — left customers with a half-finished email and no attachment. Now prompts for a recipient (defaulting to the survey contact email when present), POSTs to community-pdf with a new optional to body field, and the route generates the same fully-styled HTML and emails it inline via Resend (using the existing RESENDAPIKEY + SITESURVEYPDFFROMEMAIL env vars). Customer receives a real email with the rendered proposal in the body.
Crew Field Packet load-out
one-time-material load-out (mulch yd³ at 3" depth, sod pallets at 500 sqft / Floratam pallet) since Quote Lab passes mulchyards / mulchcost / sodpallets / turfsqft / ornamentalsqft / soiltemp into the route. Crew Loadout & Tank Setup section translates today's soil temp into the chemistry the truck pre-stages: N / K / Iron / Brandt micros at temp-band rates, BANDIT + Crosscheck unlocked above 75 °F, K rate bumped above 80 °F. Site-Specific Hazards & Crew Notes section: exclusion zones, oak injection coordination, palm species injection eligibility (Bismarckia, Phoenix canariensis, Adonidia, Sylvester, Pygmy Date — copper never OK on palms). Field notes uncapped in crew mode.
Plant Brain — peer-reviewed depth + connect-everywhere
content. 4-6 dense paragraphs per treatment, citing IFAS EDIS / ISA palms / FRAC-IRAC-HRAC groups / FDACS / SWFWMD / NRCS soil survey inline. Required structure: chemistry / mode-of-action → Genesis SKU implementation → SW Florida timing → label / regulatory → resistance management → optional field experience addendum. max_tokens raised 700 → 1800.
page-summary REST endpoint now falls back to MediaWiki search when the title doesn't match (lethal bronzing, take-all root rot, etc. resolve through the search hop). Filters Wikipedia's image-missing placeholder URLs so the augmenter doesn't save red-X icons. 12 vitest cases (was 11).
implementation snippets inline — operator no longer has to click through to see how Genesis treats it. Bubby's plantBrainEntityContext follows the join out and pumps up to 3 linked-treatment Genesis recipes into the prompt so Bubby answers cite Genesis's actual SKUs and rates.
Plant Brain via bidirectional substring match (catalog "Southern chinch bug" matches scout "chinch bug" both ways).
augmented entries plus a "Genesis ✓" badge on treatments that have implementation text — admins can scan augmenter coverage at a glance.
CustomEvent to the floating widget that opens with a pre-filled question; operator taps Send to get a Genesis-anchored answer.
/field-ops/plant-brain/admin —per-kind tallies of rows missing image / Genesis text and a batch runner (default 10, max 25 rows per click). 250 ms gap between Sonnet calls so long runs don't trip Anthropic tier-1 rate limits.
Turf Builder AI Program Intelligence — three-layer timeout fix
AbortController ceiling on the Open-Meteo fetch in fetchWeatherConditions (was awaiting indefinitely).
cache on /api/field-ops/program-recommendations so a second panel open after a slow first call is instant.
AbortController + Retry button + "Loadingrecommendations…" placeholder on the PA panel itself. Soil/RH/rain badge appends " · seasonal" when conditions came from the fallback table.
Program updates propagate everywhere
revalidateAllProgramSurfaces() now invalidates /programs, /turf-builder, /orn-builder, /special-treatment, /route, /properties, /customer-360 after every program save. Per- request consumers (Bubby ai-garden-chat, PDFs, scouting AI) read fresh Supabase on every call so they pick up changes automatically.
GET /api/field-ops/pa-programs returns the currentprograms in PA's internal shape (key / name / mode / tankCoverageSqft / lines) plus latestupdatedat, so PA can refresh its localStorage state from VerdaKai's database after a mobile-builder edit.
Document style unification
src/lib/verdakai-pdf-style.ts lib that mirrors the PA Customer Estimate. Primitives: pdfDocOpen / pdfHeader / pdfMetaLine / pdfServiceTable / pdfNotesBox / pdfFooter / pdfDocClose. Inline-style variants exposed for Gmail-safe email rendering. Black 2 px header divider, Genesis subtitle, gray-header service table with right-aligned tabular price column, emerald totals row, light-gray bordered notes boxes for Program Overview / What Each Treatment Does / Terms.
packet), and the Site Survey PDF all migrated to the shared chrome. Section content kept where domain-specific (phase breakdown, methodology, risk profile, etc.).
Site Survey PDF — comprehensive scientific document
buildSiteSurveyReceiptPdfBytes. Old versionwas a thin 2-page text dump; new version is a multi-page customer-grade scientific report.
Aerial Imagery (Esri World Imagery PNG fetched at request time via esriBboxImageryFit, embedded directly into the PDF, bbox- fit to parcel with 6 % breathing margin) → 4-tile Scan Summary band → Measured Areas table with % of lot → Tree Inventory by type with treatment-relevance column → Per-Specimen Position List (up to 100 specimens with #, type, lat, lng, species) → Walked Perimeter Zones (when the survey has any) → AI Zone Breakdown (per-polygon segmentation with confidence %) → Plant Material Identified by category → Field Notes (L&O + mow as bulleted lists) → Capture & Methodology block detailing the AI pipeline, imagery source, capture date, prompt registry version, and operator-review policy → Disclaimer in a bordered box → per-page footer with page number and final-page generated-at ISO stamp.
Empty sections skipped — a single-lot scan without trees won't render an empty Tree Inventory page.
Migrations applied during the push
treatments.genesis_implementation, genesisimplementationupdatedat, and imagesource columns on pests / diseases / treatments.
sitesurveywalked_zones table with FK to pasitesurveys.id (text, not uuid — the FK type was caught on first apply and corrected).
Late-late wave — Resend key fallback, Save Survey visibility, full document unification, walked zones in customer PDFs
After the receipt-PDF rebuild, several follow-ups landed in the same session:
/api/estimate-pdf (the saved-quote download from /field-ops/quotes) now matches the same chrome as quote-pdf, community-pdf, and the receipt PDF. File trimmed 472 → 303 lines.
report routes (/api/field-ops/daily-report, /api/field-ops/treatment-report) now flow through pdfDocOpen / pdfHeader / pdfFooter with section-specific CSS layered via the new extraCss option on pdfDocOpen. Every server-rendered HTML document in Verdakai (six routes total) now reads as the same brand.
key pattern — resendapikey and resendfromemail are now fields on organizations.settings. New resolveResendKey() and resolveResendFromEmail() helpers in src/lib/org-settings.ts. survey-receipt-pdf POST and community-pdf email path both read through the resolver, so a Vercel env-var teardown can't knock email out — admin saves the key once via Settings → Service Keys and it persists in Supabase. Settings UI gets a new "Resend API Key" KeyField with a description naming every surface that depends on it.
renamed to "Save Survey", sized up (48 px / extrabold / text-base / shadow-lg), with an amber "Unsaved changes" pill and pulsing amber ring around the emerald button when there are pending zone edits. Sticky at the top of the Measure Controls toolbar.
"GPS-Walked Treatment Zones (Operator Ground Truth)" section when the survey has any walked zones — per-zone-type aggregate with count / area / perimeter, color-coded labels, emerald totals row, sub-headline explaining the precedence. Closes the walked-zones loop end-to-end across receipt-PDF, community-PDF, Quote Lab pricing override, and the saved-survey card inventory list.
A two-day push of operator-driven fixes and customer-facing polish. The throughline: every quote / scan / PDF should reflect what's actually on the property, in the operator's voice, with no overcommitments to specific equipment or staff.
Site Survey
downsampled hybrid in residential SW Florida. Zoom cap raised to z22 so operators can draw close. Auto-route honors Esri instead of silently swapping back to Google. Earlier Esri tile-row math bug (gray "Map data not yet available" placeholder) fixed; tiles now serve correctly. Operators can still switch to Google or Nearmap via the basemap dropdown when Esri's capture is older for a specific lot.
consumers — single survey, service map, refine pins, community panel). One tap snaps the map back to the property's lat/lng at the original prop zoom.
survey scan now persists into fieldopsproperties with one tap — creates a new property row when one doesn't exist, updates the linked one when it does, and writes the property id back to the survey so the property page's Apply Survey Measurements widget finds it. Closes the gap where scans never landed in the property list.
number inputs let the operator record field-tape readings directly into each row; edits save with the survey via existing autosave. Source label flips to "Field" for operator-recorded values.
Filters palms only, shows height + clear-trunk + auto-derived injection tier (<15 ft / 15-30 ft / 30-45 ft / 45+ ft). Built for height-tiered palm injection pricing.
radius search before rendering, so the panel no longer renders a black tile when no panorama exists at the address.
Property page
pasitesurveys aimeasurements diverge from the manual property record (>1% drift or >2 units), an amber panel appears between the header and Active Programs showing Manual vs. Survey side-by-side for Turf sqft, Ornamental sqft, Palms. Per-field "Apply" buttons or a single "Apply all survey measurements" button copy the survey numbers into fieldops_properties so quotes, schedules, and pricing stop drifting from the actual scan.
pasoiltests rows fuzzy-matched on customer/address and renders them under "Legacy (Program Architect)" with pH, OM, CEC, and Na flagged red over 100 ppm.
"live alert" reading) — watchlist of pests that target the turf and palms on the lot. Recently-observed entries from scouting reports surface at the top with a date tag.
Quote Lab and Quotes
cost guesses replaced with per-service cost configs ported from public/pa/index.html PACalc into src/lib/pa-cost-engine.ts. Per-category margin tags use real cost-of-goods, painted in severity colors (ok / warn / bad) relative to PA's target margins and tolerance bands. ZEROCOST / NEGATIVE_MARGIN flags surface inline.
per-stroke beginPath() so drag-off-canvas no longer leaks the drawing state and single taps are visible. Empty-canvas guard before save. Signatures now persist into surveytrace.customeracceptance so admin can verify a quote was actually signed.
/field-ops/quotes. Surfaces the signed-by name, signed-at date, accepted total + annual, and a signature thumbnail when the quote carries an acceptance block.
underlying site survey or property record was updated more than an hour after the quote was generated.
public/pa/customer-estimate.html(emerald palette, white card, 3px green underline, emerald-on- white pricing accent). VerdaKai's estimate and PA's customer estimate read as the same document.
Quote form, and the PDF route all draw from src/lib/agronomic-narrative.ts — one Rick-approved voice across surfaces.
Customer-facing PDFs
single-property print snapshots (was Google Static).
selected lot in the HOA with its individual turf / orn / palm / oak / tree counts, phase tags inline, sorted by phase → street → address number for route order. Unscanned lots show "—" so the membership stays complete.
Composition (turf vs. ornamental vs. exclusion percentages), Methodology & Data Sources (names the AI tools — DeepForest, Claude Sonnet 4.6 vision, SAM-2, county GIS, with operator-review callout), Service Calendar (12-month / 8-visit / 4-visit variants), What's Included Each Visit, and Regional Risk Profile (Sarasota-area pressures conditional on what's on the lot).
("Z-Spray application", "Isuzu NPR tank fill"), specific staff / cert claims ("FNGLA Certified Horticulture Professional", "certified expert inspects monthly"), specific resolution claims ("sub-meter resolution"), and active-ingredient names (Prodiamine, bifenthrin, azoxystrobin, oxytetracycline) all swapped for outcome / strategy language. Customer copy now describes what the program achieves, not the specific tools used to get there.
Bubby
quote / THIS survey when the operator opens the floating widget from a contextual page. Five context streams fire in parallel per turn: plant brain, property snapshot, page context (quotes / surveys / scouting / route / soil tests / programs / products / plant-brain entities / help / field-id), paaicorrections, and curated knowledge notes.
residential pricing rules, Rick's agronomic preferences, and communication rules (no em dashes, no per-home/month, "proposed timeline" not "guaranteed", disciplined tone) sourced from CLAUDE.md.
(something not loading, error, sign-in, etc.), Bubby shifts into troubleshooting: gathers page / role / browser / error, walks through quick fixes, then emits a structured "TECH SUPPORT NOTE — VerdaKai" the tech can text to Rick verbatim.
(geocode → AI scan → DeepForest → SAM-2 → manual zone overrides), the basemap dropdown trade-offs, NDVI's resolution caveats, and the five most common operator questions with the right answer.
composer. Snap → 1024px-max client-side downscale → POST to /api/field-ops/ai-diagnose. A violet "photo in scope" chip surfaces above the composer; follow-up text questions stay on the diagnose route so Claude keeps reading the image.
page, Bubby's API call carries the property uuid; the route injects a property snapshot (turf / palms / soil / recent scouts / recent applications) into the system prompt so answers reference this specific lot.
propagated to the site survey AI prompt, Bubby's house-style block, and ai-diagnose so new surveys count Bismarcks and Bubby answers correctly when asked about palm injection scope.
tab refresh, keyed per page-context (one thread per property / one shared thread for site-survey workspace / one for quotes / one global). 60-message cap, 7-day expiry, in-header Clear button. Photo data URLs intentionally not persisted.
maintenance now have the same Bubby on the floating widget, sidebar, and mobile tabs.
everywhere; floating widget swap, admin Feedback Inbox removed, feedback API removed. The feedback_submissions table and migration 036 are preserved for historical data.
AI source disclosure
AI-derived value — DeepForest (palm count), Claude Sonnet 4.6 vision (zone polygons, plant ID), Claude Sonnet 4.6 PDF (soil test extraction), Claude Sonnet 4.6 text (interpretation / recommendations). Tap to expand the technical detail and the "Not 100% accurate. Verify before treating, billing, or signing" caveat. Currently placed on the Apply Survey Measurements widget and Soil Test detail panel; expanding to other surfaces in follow-ups.
Infra / fixes
updated_at retry onlegacy rows + freshness compute wrapped in try/catch so the Quotes page always renders even when an underlying row trips on schema drift.
page (instead of raw JSON) when the survey row hasn't been saved to pasitesurveys yet, naming the actual cause and pointing the operator at "tap Save Survey first."
A run of fixes landed this morning around the audit finding that the same property could have three different turf-sqft numbers across the app. Operators now see the divergence and can resolve it in one click:
(/field-ops/properties/[id]). When the latest linked pasitesurveys row has measurements that diverge from the manual fieldopsproperties record (>1% drift or >2 units), an amber panel appears between the header and Active Programs showing Manual vs. Survey side-by-side for Turf sqft, Ornamental sqft, and Palms. Per-field "Apply" buttons (or a single "Apply all survey measurements" button) copy the survey numbers into fieldopsproperties via an admin-gated server action so quotes, schedules, and pricing stop drifting from the actual scan. Read-only on pasitesurveys — no schema change, no PA-side mutations.
The panel now also pulls pasoiltests rows fuzzy-matched on customer/address (PA stores property_name as free text, no FK) and renders them under a "Legacy (Program Architect)" subsection with pH, OM, CEC, and Na flagged red over 100 ppm. The panel header gains a +N legacy chip so the operator sees full agronomic history without manually checking PA. Read-only.
/field-ops/quotes. The page batch- fetches pasitesurveys.aianalysisat/updatedat and fieldopsproperties.updatedat for every quote that links one and flags any quote whose underlying source was updated more than an hour after the quote. Each stale card shows an amber banner — "Underlying site survey/property record updated Xh after this quote — measurements or scope may be stale" — with an "Open property →" jump.
feedback bubble has been retired. In its place, an emerald Ask Bubby pill on every authenticated screen opens a docked chat panel — bottom-sheet on mobile, 420×600 dialog on desktop — wired to /api/field-ops/ai-garden-chat (same backend as the dedicated /field-ops/bubby page). Bubby also takes the freed admin sidebar slot under Tools as "Bubby AI Assistant". The /field-ops/admin/feedback admin inbox and feedback-submission API have been removed; the underlying feedback_submissions table and migration 036 are preserved for historical data.
Plant Brain — the federated knowledge layer that unifies species, pests, diseases, and treatments — is now first-class in the UI and addressable from outside the VerdaKai session.
desktop Tools section (next to Field ID) and in the mobile bottom tabs. The icon is the same Heroicons "sparkles" used elsewhere for AI surfaces. Available to ALL_ROLES — every field user can search, not just admins.
/api/field-ops/plant-brain/.* Same-origin requests from the in-app UI are allowed unconditionally. Cross-origin callers (e.g. Program Architect) must present Authorization: Bearer <FIELDOPSAPIKEY> or x-api-key: <key>. When FIELDOPSAPIKEY is unset the check is a no-op (dev mode). This decouples Plant Brain from the verdakai-session cookie so other KAIR tools can hit it without inheriting VerdaKai's auth surface.
brainPlantBlock helper used by ai-diagnose and ai-garden-chat now resolves snake_case aliases first, then federates searchPlantBrain across species + pests + diseases (top match per entity type), then falls back to fuzzy species search if nothing federates. Both routes inherit a defensive try/catch — pest/disease lookup failures log and return empty rather than 500-ing the route.
Behind this sit Phase 5a-5g: migration 047 added plantbrainpests / plantbraindiseases / plantbraintreatments, the federation adapter layer (src/lib/plant-brain.ts, src/lib/plant-brain-auth.ts, src/types/plant-brain.ts), the vision pipeline, the /field-ops transition banner, and a UF/IFAS pest / disease / treatment seed.
The Soil Test route (/field-ops/soil-test) is no longer a desktop- only stub. The full upload-to-AI-recommendations flow now lives on the phone:
cap), an optional Property attach, and an optional sample label ("Front yard - east side", etc.).
document content block. The prompt asks for pH, OM%, CEC, EC, texture, all macros and secondaries (P/K/Ca/Mg/S), all micros (Fe/Mn/Zn/Cu/B), salt indicators (Na/Cl), plus a Sarasota-context 3-5 sentence interpretation and three short recommendations. Treatment classes only — never brand names.
name, sample date, and the most-load-bearing chemistry (pH, OM%, CEC, plus a "Na high" red flag when sodium > 100 ppm).
full numerical grid (17 measurements), and an Open Original PDF button. Sodium > 100 ppm and chloride > 200 ppm get colored red in the grid.
Backed by migration 049 (fieldopssoiltestresults). PDFs are uploaded to the existing field-photos bucket under soil-tests/{orgid}/.... The structured numeric columns coexist with a rawextract JSON column so future lab format changes don't require a schema bump.
If the AI parse fails (e.g. scan-only PDF the model can't read), the PDF is still saved and an amber banner explains what went wrong; the record can be deleted and re-uploaded with a better scan.
The Orders route (/field-ops/orders) is no longer a desktop-only stub. The mobile page now lets you draft, send, and receive POs end to end:
notes, and editable line items. Each line picks from your Materials catalog (auto-fills name + unit + price) or accepts a free-text ad-hoc description with manual qty / unit / unit-cost. Line totals and PO total update live.
- Draft → Save Draft or Send PO (sets sentat) - Sent → Mark Received (sets receivedat) - Any non-final → Cancel PO (sets status='canceled')
immediately. PO numbers auto-increment as PO-0001, PO-0002, … scoped per org.
Backed by migration 048 (fieldopspurchaseorders + fieldopspurchaseorder_lines). RLS allows org members to read / write their own POs; service role bypasses for admin scripts. PDS retains the heavier desktop-only PO workflow if needed.
The Label Center route (/field-ops/labels) is no longer a desktop-only stub. Field techs can now pull up any product's regulatory data on the phone:
or EPA reg #. Category chips (Fungicide / Insecticide / Herbicide / Fertilizer / Other) plus a "RUP only" toggle for restricted-use products.
amber / CAUTION yellow), RUP badge, label-PDF and SDS pills if present.
days, active ingredient, EPA reg #, category, unit, carrier rate, NPK, notes — plus prominent buttons to open the Label PDF and SDS in a new tab.
count, missing-SDS count — so the admin sees compliance gaps at a glance.
No new schema. Reuses the existing fieldopsproducts table fields (labelurl, sdsurl, reihours, phidays, signalword, eparegnumber, restricteduse, active_ingredient).
The Operations route (/field-ops/operations) is no longer a desktop-only stub. It now renders a one-screen cash pulse for the business:
invoices), $ overdue (with oldest-overdue days callout), pipeline value (open quotes).
showing where the open A/R is concentrated.
flag if past due. Deep-links to the full Invoices list.
days. Tap to open in Quote Lab. Footer shows Won YTD as the "converted pipeline" reference.
Same EdenPro outage handling as the rest of the cash-side pages: if EDENPROSERVICEROLEKEY is missing or a relation isn't found, the banner shows and the page falls back to local data only (the pipeline chart still works from fieldops_quotes).
PDS retains the per-job line-item P&L breakdown (labor, materials, margin) on desktop for the cases that genuinely need a wider grid.
The Trends route (/field-ops/trends) is no longer a desktop-only stub. It now renders five live sections on the phone:
YoY % delta (green/red).
EdenPro visits × rate, bucketed by year-quarter.
share of revenue, percentage + dollar.
fieldopsquotes (status='won' vs 'lost') so the win-rate trend shows up alongside the revenue trend.
EdenPro outage handling matches the Revenue Tracker pattern: if EDENPROSERVICEROLE_KEY is missing or the visits table is unavailable, the page renders an amber banner and falls back to local data only (the close-rate chart still works).
PDS still owns the deeper analyses (cohort, technician attribution, margin) on desktop.
The Estimate History route (/field-ops/history) is no longer a desktop-only stub. The mobile page now renders the full quote list with:
to-date, and a close-rate percentage with W/L breakdown.
Active so the list starts with what needs follow-up.
Lab, Mark Won, Mark Lost** (with optional reason), and a soft-delete option. Status writes use the existing updateQuoteStatusAction so the close-date and lost-reason columns stay consistent with the desktop quotes page.
Replaces the prior <PdsMobileStub> placeholder. Admin-only, same as before.
The DeepForest + SAM-2 controls used to live in three separated zones on the survey results page (a synth-warning callout with a duplicate Re-detect button, a separately-rendered phase indicator, and the tools panel). Operators reported the spread was hard to use. Everything now lives in one unified workspace card at the bottom of the tree-counts section.
The card has three persistent regions, top to bottom:
1. Header strip with the title, a setting summary (merge, ≥ 0.30, SAM on/off chips) so the active configuration is visible at a glance, and a Hide/Show toggle. 2. Status strip (always visible, even when the body is collapsed): shows live phase progress while a re-detect is running, the last-run summary after one finishes, or a "Recommended: try a re-detect" hint when AI pin placement was approximate. Only one of those is shown at a time. 3. Body (collapsible, open by default): primary controls in a responsive 2-column grid (Pin merge mode and Detection sensitivity side-by-side on wider screens), then the SAM-2 refinement toggle as a full-width emphasis card, then the Run action with a duration estimate, then the admin Training-gold toggle, then nested Advanced GeoJSON import.
The synth-warning callout in the tree-counts area is now a slim single-line notice with an "Open tools ↓" link that scrolls to the workspace and expands it. No more duplicate Re-detect button.
The single-property scan results panel now opens the Tree pin precision tools (DeepForest + SAM-2) section by default instead of hiding it behind a click. Each control is grouped with a clear label, a one-line explanation of what it does, and copy describing when to use it. The synth-warning callout (when AI pin placement is approximate) now shows the current settings in plain English ("merge mode, threshold 0.30, SAM-2 off") and links to the tools panel for adjustments before re-running.
While a re-detect is running, the operator sees a phased progress indicator: which step is active (fetching imagery, running DeepForest, refining with SAM-2, saving), an elapsed-seconds counter, and a one-line description of what the system is doing. The last-run summary stays visible above the tools panel after a successful run, so the operator knows whether to nudge the threshold without re-opening anything.
The training-gold admin flag also gets a verbose description block explaining when to use it and where the flag lives in the data model, so admins can flag confidently without consulting the manual.
The DeepForest panel now exposes a "Refine pin placement with SAM-2" checkbox. When enabled, the route runs Meta's Segment Anything 2 over the same satellite overview after DeepForest, then snaps each box center to the canopy mask's centroid for sub-meter pin precision. Adds ~10-30s and one Replicate call per scan, so it's off by default. The same daily SAM cap that protects the analyze flow applies here. The result panel shows how many pins SAM moved (e.g. "SAM snapped 6 of 14 pins").
The route also stops collapsing every DeepForest detection into shadetree. Worker labels palm, oak, shadetree, and ornamental_tree now flow through to the imported tree's type (with oak carrying species oak), so a class-aware checkpoint produces operator-visible per-class pins instead of a wall of shade trees.
A new admin-only "Training gold" toggle on the DeepForest panel sets aimeasurements.operatormeta.traininggold = true on the survey. That's the flag the new exportgenesis_corrections.py --training-gold-only filter reads, so admins can mark the cleanest operator corrections as the fine-tune set without an out-of-band SQL update.
After a Re-detect, the panel shows a one-line summary: how many pins came in, the threshold used, how many were dropped below it, and SAM stats. Lets the operator decide whether to nudge the slider on the next run.
The DeepForest re-detect path now exposes a per-scan score threshold slider in the Site Survey "Import GeoJSON / DeepForest worker" panel. Higher values produce fewer, stricter pins (good for sparse residential lots where the worker tends to double-pin tight crowns). Lower values produce more pins (good for canopy-heavy commercial sites where some real trees fall below the default 0.30 confidence). The slider sends the threshold per request, so the operator can dial it on a single property without touching the worker's environment, and the value used is recorded back in the survey's scan metadata.
The fine-tune training pipeline grew two flags. --class-aware on exportgenesiscorrections.py emits per-class labels (palm, oak, shadetree, ornamentaltree) instead of a single Tree class. --training-gold-only filters the export to surveys an admin has flagged as operatormeta.traininggold = true, so the training set stays clean. The downstream finetunegenesis.py auto-detects multi-class CSVs and configures the model with the right numclasses and label_dict before training. See 7.12 Site Survey Imagery & AI Stack for the full pipeline.
A focused decluttering and accuracy pass on the single-property scan view.
Source-certainty model is now end-to-end. Operator-corrected measurements (override > measured > AI > missing) are mirrored to a top-level aimeasurements.effectivemeasurements field by the survey PATCH route, so any consumer — Last Property Survey tile, Quote Lab handoff, the Measure Controls header, the Scan Measurements card — reads the same canonical value. The header's Turf / Beds / Hardscape labels carry the source label inline (override, measured, scan) and surface the drawn-zones subtotal in muted gray when it differs from the effective number, so the operator never sees two contradictory turf readings on the same page. The shared resolver lives at src/lib/site-survey-effective-sqft.ts (8 vitest cases).
Overlapping AI zones no longer double-count. Multiple turf or hardscape polygons that share area used to inflate the per-type total by their intersection. unionSqftByZoneType (manual-zone-tools.ts) runs a geodesic union per zone-type bucket via @turf/turf. The Measure Controls summary surfaces an Overlap removed: turf X · beds Y · hard Z ft² chip whenever dedup actually subtracted area.
All handoff gates are now advisory, not blocking. The Send to Quote Lab button stays enabled regardless of confidence-ack, pre-flight checklist, source-certainty, stale-packet, or org-policy lock state. handoffBlockReason always returns null; handoffWarnReason returns priority-ordered advisory copy that the UI surfaces as a banner. The heavy-canopy VERIFY BEFORE QUOTING checkbox panel was removed entirely. Operators ship on their own judgment and the system records the decision via handoffopenedquotelab audit events (with reason freshafterstalelock when the operator bypasses a stale lock).
Scan-draft and recent-address persistence. query, surveyAddress, scanCustomerName, scanPhone, scanEmail, and scanNotes write to localStorage on every change (400 ms debounce) and hydrate once on mount when no surveyId is loaded. Successful geocodes promote the address into a deduped 12-entry recent-addresses ring, surfaced as one-tap pills under the search bar. src/lib/site-survey-scan-draft.ts (9 vitest cases).
Saved-survey gallery. The previous text-only <select> dropdown of "Other recent surveys" was replaced with a horizontally scrolling gallery of clickable tiles. Each tile shows the satellite thumbnail, address, scan date, AI confidence, and the operator's effective turf with its source label.
Save-state indicator. The Measure Controls summary line now renders the metaSaveState machine — Saving…, Saved · Xs ago, Retrying save…, Offline · queued, or Save failed — with relative time refreshed every 15 s. The retry loop is capped at 3 attempts with exponential backoff (1.2 s, 3 s, 8 s); 400 / 422 responses are treated as terminal so a malformed payload no longer loops. The pending retry timer is cancelled on unmount and on every fresh save call.
Tree-count overrides per type. Final Measurements gained explicit oakcount, shadetreecount, ornamentaltreecount inputs alongside the existing palmcount. A hardscape_sqft override input joined the row. An inline note explains that the AI is FL residential / HOA-tuned and may miscount windrows, shelterbelts, or non-FL species. The Last Property Survey tile renders an advisory when total trees = 0 AND (confidence < 70% OR lot > ~1 acre) pointing the operator at the Trees palette on the map.
Default basemap is satellite. The earlier roadmap default forced operators to swap before they could see the property they were measuring. The earlier auto-router force-switched to roadmap on zone-draw entry; that override was removed so the operator's choice persists. The genuine tile-failure fallback to roadmap remains.
Real fullscreen. The Full screen button now calls requestFullscreen({ navigationUI: 'hide' }) on the map panel ref so the operating system fills the monitor with imagery — not just in-page resizing under the sidebar. Zone-draw (Turf / Beds / Hardscape) and tree-pin palettes both render inside the fullscreenable container so measurement entry never requires exiting fullscreen. Falls back silently to in-page maximized on browsers without the API.
Imagery freshness pill. A yellow "Verify imagery currency" pill rendered under the Aerial dropdown when any imagery basemap is active. On Google sources it links to earth.google.com/web/@LAT,LNG,500m so the operator can use the historical imagery slider to verify capture date. Esri and Nearmap show their own currency context.
Google Places autocomplete on the address search. The site-survey search input progressively enhances into a Places Autocomplete field on the same Maps API key the satellite map uses. Falls back silently to plain text when the script fails to load.
Bahia / rough-pasture default flipped to exclude. The Bahia turf policy now defaults to exclude so rough roadside / easement bahia and tall pasture stop inflating turf totals on cold loads. Operators with a per-property preference in localStorage keep their setting.
Operator panel collapsed by default. The pre-flight checklist, verify mission, source recommendation, eco engine, ops telemetry, operator metrics, outcome loop, meta persistence, delta card, recovery path, and audit-events stack now live behind a Show details toggle next to Save modifications. The summary line (Turf / Beds / Hardscape / Map health / Save state) stays visible.
Decluttered single-property layout. The earlier two-column layout (map left, secondary panels right) was replaced with a vertical space-y-4 stack: map, then Zone Breakdown, then address card, then Scan Measurements card (turf/beds/hardscape effective tiles with source labels, lot envelope, turf type, density, optional impervious + tree canopy + tree counts, plant material, mow / L&O notes, confidence footer). Plant material now sits directly under the map in the readable single-column flow.
Share & export panel. A dedicated row below the Send to Quote Lab actions exposes Download PDF, Email PDF (existing receipt dialog), and Save to Drive (downloads the PDF and opens Google Drive in a new tab; direct OAuth Drive upload is a follow-up).
Eco-intelligence library + qa-dashboard surfaces. The eco engine now exposes explainEcoRecommendation (per-zone "why" prose), describeTimelineDiff (labeled before/after entries), computeOutcomeMetrics with composite-key dedup so replayed audit events can't inflate counts, and summarizePortfolioRollup. Three new sections render on /field-ops/site-survey/qa-dashboard: recommendation explainability, timeline history with two-snapshot diff picker, and portfolio analytics with bar visualizations.
File split foundation. Five reusable components carved out of site-survey-client.tsx so future cuts converge on the same shapes: exclusion-list-panel.tsx, aggregate-results-grid.tsx, scan-measurements-card.tsx, recent-surveys-gallery.tsx, rescan-comparison-card.tsx. The 14.7K-line client is still oversized; the split is ongoing.
Playwright coverage for source-certainty handoff. Two new tests on tests/site-survey-handoff.spec.ts cover the timelinesnapshots[0].effectivemeasurements PATCH contract: happy-path acceptance and 400 rejection of a malformed timeline_snapshots: 'not-an-array' payload.
Lint and type-check are clean. Zero ESLint warnings, zero tsc --noEmit errors, 208 vitest cases pass. The session also cleared five long-standing react-hooks/exhaustive-deps warnings, three dead isIdempotencyTableMissingError destructures, one unused now in the idempotency lib, and a runtime bug in applyZoneTemplate (it called three undefined functions and silently failed to apply zone templates).
Quote Lab now supports Residential and Commercial pricing modes via a toggle button. Commercial rates are flat (no tiering):
Same per-visit rate regardless of visit frequency (4/8/12). More visits = more value, not higher price. Available in both PDS Quote Lab and VerdaKai Quote Lab.
All AI features now use Anthropic Claude as the primary provider. Single-property scans use Claude exclusively for accuracy. Batch scans use Claude primary with free-tier fallbacks for cost savings. The multi-provider fallback chain in ai-proposal and ai-diagnose has been removed — Anthropic only.
force flag, no clearing neededFixed a race condition in the field-ops shell where mobile users with valid session cookies were redirected to /login → /field-ops (dashboard) instead of staying on the requested page. The shell now waits for the async session fetch before redirecting.
Added Vitest test framework with 15 tests covering the pricing engine: rate tiers, density multipliers, combo discounts, minimum service charges, commercial mode, and cost/margin calculations.
All Open-Meteo weather and soil temperature API calls now route through a server-side proxy at /api/field-ops/weather-proxy. Fixes fetch failures caused by ad blockers, browser extensions, DNS filtering, or VPN.
Disease and pest advisories on the PDS dashboard are now month-aware for Southwest Florida:
Login page is now light by default (white background, dark text). Switches to dark when the device or app has dark mode enabled.
Smart Mailbox is now restricted to super-admin accounts only. Other admin users are redirected away — the mailbox is connected to the owner's personal Gmail, not a shared resource.
Signup requires Name · Email · Company · Role. New teammates can either sign up directly (verify via email link) or accept an admin-sent invite — the invite flow lands on a 4-step welcome (password → profile confirm → tour → dashboard) and stamps the company on their profile so it shows up in the Team Directory immediately.
Elevated roles are invite-only. Public signup is clamped to Spray Tech and Maintenance Crew — the Admin and Maintenance Admin cards on the login page are marked invite only and route to an explainer instead of creating an account. An existing admin must send an invite from Access Control → Team → + Add → Email invite for those two roles. This stops anyone with the URL from self-selecting admin and getting full access to the org.
Forgot-password sends a reset link via Supabase Auth. Admin can also reset passwords on demand from Access Control → Team.
A full auth audit log lives at /field-ops/admin/security. Every signin, signup, rate-limit hit, role change, and admin action is recorded with IP, user agent, and metadata. Four tabs:
kind, or severity. CSV export for auditors.
country (with a non-US fade), and a list of auto-blocked IPs with a manual Unblock button.
triggered them.
window, duration), account lockout (fails, window), retention days, digest recipients. All changes persist to organizations.settings.security_config and take effect on the next request.
IP auto-block fires when an IP hits the configured signin-fail threshold (default 40 in 5 minutes) → blocked for 60 minutes. Blocked IPs can't even reach the signin endpoint. Persisted in the blocked_ips table so it survives restarts.
Impossible-travel detection compares every successful signin's geolocation to the prior one for the same email. If the implied speed exceeds 900 km/h (above commercial jet cruise), a critical alert fires with from/to locations, distance, and computed speed.
AI in the suite: a 0–100 risk score with posture (green / yellow / red) auto-loads on every dashboard view, powered by Haiku. Click the 💬 Ask AI button to run a natural-language query against the log ("Any unfamiliar IPs trying admin signins this week?"). The weekly digest email still runs Mondays at 9 UTC.
90-day retention cron purges old events nightly at 3 UTC. Rate limits are persisted in Postgres and survive Vercel cold starts.
Team tab now shows pending-invite pills on users who haven't signed in yet. Temporary role elevation lets admin grant an elevated role for a bounded window (auto-reverts). Per-user access overrides grant or deny specific modules regardless of the role matrix.
✨ AI tab adds:
30-day activity.
haven't used in the last 30 days — with an AI-generated summary.
with its grant source (role default, override, temp role, locked). CSV export for compliance handoff.
A team chat lives at /field-ops/chat. Every org has a default #general room; anyone can be added to any channel. Channel owners can invite users from other VerdaKai organizations for cross-company work coordination when allow_external is on.
Markdown-light: ` inline code , ``fenced blocks` , and @mentions render natively. Type @ to open autocomplete from room members. Supabase Realtime streams messages; a 15s poll handles dropped sockets. Unread badges on both the desktop sidebar and mobile bottom nav.
/field-ops/directory shows every member of your organization with name, role, company, phone, certifications, and avatar. Email and other sensitive fields are deliberately omitted. Search by name, role, or cert.
A floating emerald Ask Bubby pill on the bottom-right of every authenticated screen opens a docked chat panel — bottom-sheet on mobile, 420×600 dialog on desktop. Same backend as the dedicated /field-ops/bubby page (/api/field-ops/ai-garden-chat) so plant, pest, rate, and timing questions can be asked without leaving the current screen. Hidden on /login, /accept-invite, /forgot-password, /reset-password. The previous Make Us Better feedback widget and /field-ops/admin/feedback admin inbox have been retired in favor of Bubby.
The admin can run the whole business from a phone. PDS itself is blocked on mobile (a client-side gate in /pa/index.html redirects every phone visit back to /field-ops), but each PDS program has a native VerdaKai mobile surface:
Fully editable on phone
/field-ops/turf-builder) — list, create,edit, delete turf programs. Per-line product picker pulled from Materials, rate + rate_unit + carrier inputs.
/field-ops/orn-builder) — same editor,scoped to ornamental programs.
/field-ops/special-treatment) — same editor, scoped to special service_type.
/field-ops/bubby) — phone-first chat UI wrapping the ai-garden-chat endpoint. Suggested prompts, typing indicator, Enter-to-send.
/field-ops/photos) — native phone camera capture (via capture="environment"), property picker, tag chooser (general / scouting / before-after / ai-analysis), notes, square-tile gallery filterable by property.
Native read-only on phone, full editor on desktop
programs, recent applications, recent scouting reports per property. Tap a row in the list for the full record.
stats. Graceful fallback when EDENPROSERVICEROLE_KEY isn't configured.
EdenPro visits × rate, with MTD vs prior-MTD delta.
estimator), Quote History, and the desktop-only full Quote Lab.
Mobile-native stubs (upgrade to full editing on request)
Label Center, Trends & Insights — each lands on a native VerdaKai page that loads read-only data where safe and shows "Open in PDS (desktop)" for the full editor.
Plus regular VerdaKai mobile tabs (Today, Field ID, Site Survey, Mailbox, Dispatch, Compliance, Fleet, Scouting, Maint Crew, Spray Crew, Materials, Quote, Education, Prices, Team Directory, Work Chat, Access Control, Cybersecurity Suite). Admin scrolls through the bottom nav horizontally; role filtering keeps non-admins on a shorter list.
?handoff= in the URL. You can copy that link, open it on another device, or return after a new tab — not only one browser tab’s session storage. The handoff is consumed on first successful load; after that, use Site Survey again or rely on the prefill the page stashed in your browser for refresh.surveyqahints, and a link back to Site Survey.survey_trace on saved quotes: When you save a quote that came from Site Survey, the app stores a snapshot (IDs, measurements context, QA hints) on the quote row for support and audits.sitesurveyid in the URL) and Open service map when a communitysurveyid is present.Database: Operators should apply migration 043-site-survey-handoff-funnel.sql so handoffs and funnel events persist. Rows may contain operational metadata — purge expired handoffs and old funnel rows per your retention policy. Run node scripts/apply-migration-043-handoff-funnel.mjs with DATABASE_URL in .env.local, or paste the SQL in the Supabase SQL Editor.
API key vs browser: When FIELDOPSAPI_KEY is set, Quick Quote, quote-handoff, and funnel-event still accept a signed-in verdakai-session cookie from the same origin (no secret in client JS). Other field-ops routes that use key-only requireAuth() are unchanged.
Retention: Vercel cron /api/cron/field-ops-data-retention (04:00 UTC daily) deletes expired handoffs and funnel rows older than FIELDOPSFUNNELRETENTIONDAYS (default 180, clamped 30–730).
A ↻ icon in the mobile top bar clears local cache and reloads. Useful after a new deploy if the phone browser is holding onto stale HTML.
The desktop sidebar footer has an Open EdenPro button for admin. It generates a short-lived HMAC-signed handoff token containing email + role so EdenPro can establish a session without a second signin. Requires NEXTPUBLICEDENPROURL + EDENPROSSO_SECRET env in Vercel.
VerdaKai ships a manifest.json and apple-touch-icon, so iOS and Android both offer "Add to Home Screen" and install the app with the Genesis icon, no App Store required.
Navigate to your deployment URL. The reference instance lives at verdakai-sigma.vercel.app; a self-hosted install uses whatever hostname you configured.
The first screen is the role picker. Tap the card that matches who you are: Admin, Spray Technician, Maintenance Crew, or Maintenance Admin. There is no password yet — the selection is stored in a long-lived browser cookie (one year) and can be swapped at any time from the profile avatar in the top-right.
On a fresh install, the admin should work through four setup steps before inviting crews in:
Keys entered here are stored in the organization's settings and override any environment-variable fallback.
business name, logo, phone, email, and license number that appear on every printed compliance report.
Inventory drives the stop-card mix picker, the program designer, and the AI's product-context, so an empty catalog will produce generic answers instead of ones grounded in what the company actually applies.
combines county GIS parcel data with AI measurements of turf, ornamental beds, palms, and exclusion zones; the resulting service map is the foundation for everything downstream.
VerdaKai was designed phone-first. Every interactive element is at least a 44-pixel tap target, forms use native date and time controls, and the bottom tab bar gets you anywhere in two taps. Desktop works equally well for admins running the office, with wider sidebars and multi-column grids, but the primary user is a tech with a phone in the cab of a truck.
VerdaKai has four roles. Each page enforces its role gate on the server, so typing an admin URL as a technician redirects to /field-ops rather than leaking content.
| Feature | Admin | Tech | Maint. | Maint. Admin |
|---|---|---|---|---|
| Dashboard / Today | ✓ | ✓ | ✓ | ✓ |
| Route / Stop Cards | ✓ | ✓ | ✓ | |
| Field ID (plant lookup) | ✓ | ✓ | ✓ | ✓ |
| Education Center | ✓ | ✓ | ✓ | ✓ |
| Team Directory | ✓ | ✓ | ✓ | ✓ |
| Work Chat | ✓ | ✓ | ✓ | ✓ |
| Ask Bubby (floating chat) | ✓ | ✓ | ✓ | ✓ |
| Master Price List (read) | ✓ | ✓ | ||
| Quick Quote | ✓ | ✓ | ||
| Help & Guide | ✓ | ✓ | ✓ | ✓ |
| Materials & Inventory | ✓ | |||
| Quote Lab (full) | ✓ | |||
| Properties | ✓ | |||
| Site Survey | ✓ | ✓ | ||
| Maintenance Crew roster | ✓ | ✓ | ||
| Spray Crew roster | ✓ | |||
| Compliance Center | ✓ | |||
| Mailbox · Dispatch · Fleet | ✓ | |||
| AI Memory (Knowledge Notes) | ✓ | |||
| Program Designer Suite | ✓ | |||
| Access Control | ✓ | |||
| Cybersecurity Suite | ✓ | |||
| Bubby AI Assistant (sidebar) | ✓ | |||
| Site Survey QA (funnel / flags) | ✓ | |||
| Open EdenPro (cross-launch) | ✓ |
Maintenance Admin is a superset of Maintenance. It is scoped to the mow-crew side of the business: a maintenance admin can manage their own crew's roster, compliance, and site access, but is never exposed to the spray roster, pricing, or the AI memory.
You clock in from your phone. VerdaKai opens on Today.
The route is laid out in chronological order, color-coded by service type, with drive-time estimates threaded between stops. Each card surfaces the customer, address (tap to navigate), the scheduled window, the rate, any notes the office left for you ("call ahead", "fire ants east bed"), and the mix pre-built from the property's program. Tapping a card opens the Stop Card.
The Stop Card is your one screen for one visit. You arrive, you start, and the mix builder is already populated. Edit the product or rate per bed if the field conditions disagree with the plan. The inline Field ID button opens the camera for any identification the job needs — a plant you can't name, a pest on a leaf, a patch of discolored turf — and returns an answer in a few seconds, grounded in the products your company actually stocks.
When you tap Complete, four things happen in the same tap. The visit is timestamped closed. Each product you applied is logged as an application_log entry with your applicator name, license number, and treatment price. The inventory is decremented accordingly. The corresponding EdenPro visit is marked completed so the office dashboard updates in real time. If you clocked out by mistake, the Completed view has a Reopen button on today's stops.
Alongside the stop workflow, the Education Center is the study tool baked into the app. The tracks — pesticide safety, L&O, pest ID, turf chemistry, ornamental — run practice questions and track accuracy per topic. Anything under 70% is flagged; the AI Coach is a one-on-one tutor that drills weak topics with Florida-specific regulatory context, which is where most state-exam preparation material falls short.
Quick Quote is the curbside estimator. Type an address, pick a program, and a proposal appears in seconds. It uses the same pricing engine as the Admin-side Quote Lab, so nothing gets under-quoted.
Admin is the operator's seat. Everything is available to you.
The Dashboard shows today's route summary (completed, in-progress, remaining), per-tech revenue and on-site time, license alerts for anything expiring in the next 30 days, fleet alerts for overdue service items, and a small sync-health strip confirming the most recent application log and EdenPro activity. Every card is clickable and drops you one tap deeper. Nothing on this screen is decorative; every number traces back to a live query.
Dispatch is the map view: every clocked-in tech, their GPS position, the stop they are on, and how long they have been there. Stale pings (more than two minutes old) dim out so it is obvious at a glance who is moving and who is idle.
Site Survey is the marquee office feature. Type a community name. VerdaKai looks the parcels up in the county GIS, draws their boundaries on a satellite overlay, and runs AI measurement against turf, ornamental beds, palms, shade trees, and exclusion zones (ponds, pools, common areas, hardscape). For large HOAs, batch-scan 15 to 20 parcels and extrapolate the totals — a 332-lot community can be measured in three minutes with sampling-confidence figures shown live. When the scan is ready, tapping Publish to Maintenance pushes the service map to the mow crew; you pick exactly which fields to share (map, tree counts, measurements, exclusions) so nothing the crew shouldn't see ever leaks.
Send to Quote Lab hands off HOA or single-lot measurements to Quick Quote (all roles that have it) or Quote Lab (admin) with optional server handoff links, survey trace on save, and a read-only QA panel on the quote screen. Admins can monitor funnel health and sanity checks on Site Survey QA.
Quote Lab is the full-fidelity proposal generator. It knows your product costs, your per-square-foot turf and ornamental rates, the special-treatment pricing tiers, and the labor and drive-time multipliers your HOAs incur. Pick a program, adjust the knobs, press Generate — the output is a PDF with the company's letterhead already applied.
Crew management lives in two parallel rosters: Maintenance Crew for the mow side, Spray Crew for the licensed applicators. Each row holds the photo, role, company, contact info, status, and the compliance documents on file (COI, W-9, business license, workers comp) plus the certifications and licenses — including CEU progress for regulated ones. If a crew member's certifications look like a different crew type than the one they are filed under (a spray tech sitting in the maintenance list, say), the row carries an amber miscategorization warning with a one-tap Move action.
Three compliance reports print straight to PDF: the Maintenance Crew portfolio, the Spray Crew portfolio with FDACS license numbers, and the Combined Compliance Report with an AI-generated compliance posture paragraph at the top. Any of the three can be emailed directly to an HOA board from the same screen; the printed document is letter-sized, numbered, and signed.
AI Memory — formally the Knowledge Notes page — is where the admin teaches the AI what the company knows. A note might read "Don't recommend imidacloprid for ornamental pest control in residential — prefer horticultural oil or insecticidal soap." Active notes are prepended to every AI system prompt, companywide. Every Field ID, every diagnosis, every tech chat inherits it. This is how VerdaKai goes from a smart assistant to a smart assistant that knows your company's playbook.
Mailbox brings the Gmail inbox in-app with AI-drafted replies for every message in three tones — polite, direct, apologetic — so a busy admin can triage a week of correspondence in twenty minutes instead of two hours.
Maintenance Admin is the vendor-facing role for a mow crew working under Genesis. The view is scoped to what's relevant to that team: the daily route, the communities Genesis has published, the crew's own roster (with full edit access to docs, certs, and photos), the branded company profile that appears on the printed portfolio, and the maintenance-side compliance PDF. The spray roster, pricing, quotes, mailbox, dispatch, fleet, and AI Memory are not part of this role.
The Maintenance Admin screen does one thing very well — it makes it simple for a subcontracted mow operator to keep their own compliance binder current without ever seeing the parent company's private operational data.
Each property has a name, an address, service-area measurements, program assignments, application history, scouting reports, photos, and any attached mailbox threads. Opening a property shows the full service history in one pane so the office can answer "when did we last touch this lawn" without cross-checking a spreadsheet.
A program is a recurring treatment schedule a property is enrolled in. Programs are built per-month, per-service-type, and get assigned to properties so the route generator knows what to do on each visit.
Genesis runs programs across three service types:
today) covering fertilization, pre- and post-emergent herbicide, and insecticide rotation. Default tank coverage: 60,000 sqft.
covering ornamental bed fertilizer, fungicide rotation, and systemic insecticide for bed pest pressure. Default tank coverage: 10,000 sqft.
treatment, and systemic insecticide / fungicide application for palm pest and disease pressure.
Alongside the recurring programs, Genesis offers six special treatments priced per stop: New Sod Establishment, Fire Ant Eradication, Deep Root Feeding (tree and palm injection), Salinity Correction, Hydretain surfactant application, and Plant Growth Regulator for commercial HOA maintenance. A Weed Control Backpack Mix recipe covers the ad-hoc tank a tech uses when a property manager flags a broadleaf breakout between cycles.
Every product has a name, SKU, unit, category, active ingredient, current on-hand quantity, reorder threshold, price, margin, and an adjustment log tracking every plus or minus to the stock level. Products feed the Stop Card mixes, the program line items, the quote builder, and the AI product-context that grounds plant diagnoses in what the company actually stocks.
The read-only pricing reference that spray techs see from the field. It reflects the current snapshot of the Program Designer Suite's editable master pricing — admins update prices there; techs see the result here without needing an admin seat.
The one-screen price estimator. Built for curbside quoting: roll up to a prospect, type the address, hand them a price in thirty seconds. Available to both admin and spray tech.
Site Survey and Property Audit can pre-fill Quick Quote via a handoff URL (?handoff=) or a one-shot browser prefill. HOA imports also load the same communities picker as Quote Lab where configured. Saving stores survey_trace when the quote came from Site Survey.
The full-fidelity proposal generator. Program selection, custom line items, discount overrides, and a printable PDF on company letterhead. Admin-only.
The daily report view. Every completed stop with its time in and out, revenue, treatments logged, and notes. From this screen admin can also print the Daily Report PDF — a one-page end-of-day summary suitable for forwarding to an office manager or a tax accountant.
The vehicle registry and maintenance schedule. Each truck carries a mileage tracker, service items, and next-service-due alerts that surface on the admin dashboard before they become roadside problems.
An in-app copy of this manual and the product pamphlet, available to every role. The markdown source of both documents lives in the repo at docs/, so editing the canonical text doesn't require touching the React layer.
Site Survey QA at /field-ops/site-survey/qa-dashboard shows recent funnel events (community created, quote handoff created, explicit community save, quote saved with survey context) and highlights communities whose saved numbers look inconsistent. Use it for internal ops and support — not customer-facing.
Settings holds the Anthropic and Google Maps API keys (stored in the organization's settings so they can differ per tenant) plus optional Gmail and Twilio credentials for mailbox and SMS integrations.
The Diagnostics page is admin-only. It reports which environment variables are set (without exposing values), the row counts of every table the app depends on, schema-drift detection against the EdenPro visits table, the health of today's route, any license or fleet alerts, and runtime metrics. A one-click copy exports a JSON snapshot suitable for pasting into a support ticket.
A site survey passes through several layers between "operator types an address" and "Quote Lab gets measurements." This section is the field tech's reference for what each layer does and which controls are operator-tunable.
Satellite imagery (Google Static Maps). Every scan downloads a 1280x1280 zoom-20 overview tile centered on the parcel centroid (or on the geocoded address if the parcel boundary is missing). This is the same tile the AI vision model sees, the same tile DeepForest runs on, and the same tile that gets re-projected for pin placement. Imagery is fetched fresh on each scan, never cached.
Geocode + parcel boundary. The address is geocoded once, then cross-referenced against the county parcel layer to get the polygon rings used for clipping pins, computing parcel area, and centering the imagery tile.
AI vision (Anthropic). A multimodal model runs on the satellite overview to produce species identification, tree-type counts, turf and ornamental zones, and a confidence score. The model is good at counting and identifying but is not pixel-precise on individual pin placement, especially when canopies overlap.
DeepForest re-detect (optional). When pin precision matters, the operator can run DeepForest, a dedicated bounding-box tree detector, on the same imagery. It replaces the AI-placed pins with pixel-precise detections. The "Import GeoJSON / DeepForest worker" panel exposes:
Mode (merge or replace) controls whether DeepForest pins join the existing set or wipe and replace. Score threshold (0.00 to 1.00, default 0.30) controls how strict the detector is. Raise it on sparse residential lots where DeepForest tends to double-pin tight crowns. Lower it on canopy-heavy commercial properties where real trees are getting filtered out. The threshold used is recorded in the scan metadata so reviewers can see what produced a given result. Refine pin placement with SAM-2 runs Meta's Segment Anything 2 (via Replicate) on the same overview after DeepForest, then snaps each detection's box center to the canopy mask centroid for sub-meter precision. Off by default since it costs ~10-30s and one Replicate call per scan. Subject to the org's maxSamTreeAssistPerUtcDay cap. Training gold (admin only). Marks the survey's operator- corrected pins as gold-quality training data. Picked up by exportgenesiscorrections.py --training-gold-only when building the next fine-tune set. Use this on surveys where the operator carefully placed every pin — not on partial fixes.
The class-aware fine-tune output is wired through end-to-end. A checkpoint trained with --class-aware returns labels palm, oak, shadetree, or ornamentaltree instead of a generic Tree, and the route maps those to the imported pin's type (with oak carrying species oak so the oak-injection rate card picks it up).
After each re-detect, the panel shows a one-line summary so the operator can see whether to adjust the threshold on the next run: how many detections came back, how many were dropped below the threshold, and how many pins SAM-2 moved (when refinement is on).
NDVI (active as of 2026-04-30). Red (B4) and near-infrared (B8) bands from Sentinel-2 L2A are pulled through the Sentinel Hub Process API and rendered as a vegetation-health heat map clipped to the property or community bounds. The overlay paints over the Google satellite basemap as a Google Maps GroundOverlay: green = healthy canopy, yellow/red = stressed or thin turf, dark = cloud or shadow masked out. Server-only auth using SENTINELHUBCLIENTID + SENTINELHUBCLIENTSECRET (added to both .env.local and the Cloudflare Pages secrets on 2026-04-30).
Operator workflow.
click "Load NDVI". The endpoint pulls the most recent cloud-free scene from a sliding ~90-day window and aligns the overlay to the parcel rings.
community → in the Community Intelligence panel, click "Load NDVI". The overlay covers the whole community boundary; lots showing red/yellow are the ones to scout first.
Practical uses for L&O.
service to identify the lots to walk first.
documents widespread stress for new prospects.
~5 days; pre- and post-treatment images show recovery.
that follow the irrigation grid.
Caveats. Resolution is 10 m/pixel — fine for lot-level patches, not individual plants. Cloud cover is the main blocker in coastal FL summer; the API picks the most recent cloud-free scene. Free- tier Sentinel Hub accounts are limited to ~30k Processing Units per month — single property renders are single-digit PUs; community-scale renders climb fast. Watch usage at https://www.sentinel-hub.com/oh-dashboard if requests start throttling.
Operator pin corrections become training data. When an admin flags a corrected survey with operatormeta.traininggold = true, the exportgenesiscorrections.py script can pull only those surveys, project the operator's pins back onto the imagery, and emit a DeepForest-format CSV. Run it with --class-aware to emit per-class labels (palm, oak, shadetree, ornamentaltree); without that flag everything collapses to a single Tree class for use with the pretrained model. The matching finetunegenesis.py auto-detects multi-class CSVs and configures the trainer with the right numclasses and labeldict. The resulting checkpoint can be loaded by setting DEEPFORESTCHECKPOINT_PATH on the worker.
Program Designer Suite (PDS) is the office-mode companion to VerdaKai's field-mode. Admin-only. Opened from the bottom-tab PDS button or the sidebar entry.
PDS is a single-page application with its own sidebar. Its modules cover the side of the business that doesn't happen in a truck:
bulk operations
month's service type and product rate is editable here.
reference that the AI reads when answering grounded questions
flows into every quote
recommended product, rate, and price
to techs by color; recurring jobs auto-generate up to 60 days out. Each calendar cell carries a color dot per assigned tech; the weekly summary breaks down workload per tech so admin can spot imbalance before it becomes overtime.
tracking against specific jobs
costs. Writes to the fieldopstechnician_licenses table that VerdaKai's compliance PDF reads from, closing the data loop between the office and the field.
desktop real estate for large multi-page proposals
archive
at /field-ops, which opens in a new tab so admin can verify what techs see without losing their place
Under the hood, PDS is built as static HTML with modular JavaScript organized around pa-*.js files. State is held in localStorage for instant response on the admin's desktop, and a sync module writes selected slices back to Supabase so field mode can read them.
The camera button opens the native camera. The photo is compressed client-side, sent to the server, and fed to Claude via a prompt tuned for either identification (species, botanical name, confidence, care notes) or diagnosis (pest or disease identification, severity, and a treatment recommendation grounded in the company's product catalog). Admin-mode includes an extra Maintenance Notes addendum covering pruning, mowing, and irrigation considerations alongside the chemistry.
On any AI response, admin sees a Save to Knowledge button. Tapping it auto-generates a topic name, classifies the answer by keyword, and files it into one of four libraries: Plant, Pest, Disease, or Diagnosis. There's no modal — the save is a single tap, and a toast confirms the destination library. The classification rules favor disease over pest when both match, because a diseased plant with pest pressure is almost always diagnosed on the disease first.
Knowledge Notes are the admin's prompt injection for institutional memory. Active notes are prepended to every AI call — Field ID, diagnosis, Coach chat, Mailbox reply. A note is active or inactive; inactive notes stay saved but stop influencing answers, which lets admin retire outdated guidance without deleting its history.
The Coach opens as a chat sidebar preloaded with the tech's weakest topic. It behaves as a private tutor, offering practice questions, explanations, and mnemonic tricks. It knows Florida's FDACS regulatory context, the restricted-use pesticide rules, the Worker Protection Standard trainer requirements — material most crews study for licensing exams but rarely practice in context.
The AI-generated paragraph at the top of the Combined Compliance Report runs at print time. It summarizes which crews are current, which are expiring, and any gaps an HOA compliance officer would want to know about. A deterministic fallback paragraph renders if the AI call fails, so the PDF always prints.
Three candidate drafts per email — polite, direct, apologetic. One tap copies to clipboard; two taps replaces a generic "Hi," opener with the recipient's first name. The admin still chooses which voice to use; the AI does the typing.
Florida's Department of Agriculture issues the applicator licenses a licensed spray operation can't work without. VerdaKai tracks them in two places, which feed a single compliance record: the Program Designer Suite's Employees module (where HR enters them), and the VerdaKai spray-crew detail page (where admin or the tech themselves maintain them).
Every license record carries the license type (free-text; e.g. "FDACS Limited Urban Commercial Fertilizer Applicator"), the license number, the issued and expiration dates, the renewal cost (the dollar amount actually due), the CEU requirement and completed hours, and any free-text notes. The detail view turns CEU status into a progress bar so a tech can see at a glance how many hours they still owe before renewal.
Licenses expiring within 30 days surface an amber banner on the crew list and the dashboard. Expired licenses surface a red banner until resolved.
Beyond licenses, each crew member can hold the documents an HOA or commercial client will ask to see: Certificate of Insurance, W-9, business license, workers comp, and anything else filed as "Other" with optional expiration plus a free-text note.
Uploading a document stores it in Supabase Storage and, for a COI, extracts the policy number and carrier via AI so admin doesn't have to retype them. Documents are always attached to a specific crew member, so the roster view can show expiration countdowns per person.
The printed compliance PDF is print-optimized for letter size and carries the Genesis letterhead (or the Maintenance Admin profile's letterhead, depending on who runs the report). The header includes a unique document number, the print date, a prepared-by signature block, and a received / acknowledged line the HOA can sign. The body is structured per crew member — photo, role, certifications with expirations, compliance documents with expirations — so a compliance officer can audit it top to bottom without flipping pages.
(e.g. "LESCO 46-0-0 at 0.5 lb N/1000 sqft, spreader").
property.
property: pest pressure, disease signs, or abiotic stress.
pond, pool, common area, hardscape, playground.
treated square footage.
companion application.
to apply restricted-use pesticides in Florida.
must accumulate a defined number per renewal cycle.
their own limited admin seat.
site survey with the mow crew so both sides see the same map.
For the marketing-style overview of the product, see the digital pamphlet. For an end-of-day snapshot of your own install, see /field-ops/diagnostics.