The training pipeline looked finished. It had an export command, a RunPod handler, a review queue spec. Everything was wired together. Everything was also broken in a way that only production could reveal.

The first failure was philosophical: the pipeline exported training data that had never been written. The manual_adjustments flag was hardcoded to false. The final_polygons column was always empty. When a user edited an AI detection and saved it, the corrections vanished — the save path updated the measurement but never told the training system that a human had intervened. The pipeline was a perfect machine with no fuel.

The fix threaded correction tracking through both save paths. New refs — aiPolygonsEdited and aiOriginalResult — track when users modify AI polygons, capturing the delta between what the model drew and what the human approved. Both "Accept & Save" and "Edit → Save" now populate final_polygons and flip manual_adjustments to true. The training export command finally has data worth exporting.

The second failure was PostgreSQL being PostgreSQL. where('column', true) generates = 1 in the SQL query. MySQL shrugs and treats 1 as true. Postgres refuses — boolean columns expect = true, not = 1. The export command and the daily digest both had this bug. Both passed in SQLite tests. Both failed silently in production, returning zero rows from queries that should have returned hundreds. Changed to DB::raw('true').

With the data pipeline fixed, the review queue UI came to life. A card grid shows satellite preview thumbnails of properties with pending AI detections. Click one and an interactive map panel opens with polygon overlays — the AI's guess rendered on the actual property. Accept or reject. Stats bar at the top. Mobile-responsive bottom sheet for phone review. The BatchAiDetection artisan command runs SAM across existing properties to populate the queue — bulk detection for human review rather than one-off triggers.

AI detections got linked to properties through a foreign key that should have existed from the start. Without it, a detection was an orphan — a set of polygons floating in space, unattached to the property they described.

The RunPod worker had gone cold. Five stale jobs sat in the queue. The "Detect with AI" button returned 504 errors. The queue got purged, the worker restarted. But cold workers are a recurring problem with serverless auto-scaling — RunPod scales to zero aggressively, and the first request after scale-up takes thirty seconds to load the model. A CheckRunPodWorker command now runs every five minutes. If the worker is unhealthy or no workers are alive, it scales from zero to one automatically. A keep-warm mechanism for a system that would rather sleep.

The PostgreSQL boolean bug is worth an audit beyond these two commands. Anywhere the codebase writes where('bool_column', true) works in tests and breaks in production. It's the kind of bug that doesn't crash — it just returns empty results, and you assume there's no data.