Plate Curator
Case study, Tooling
8,000+ plates, and still growing. This is what keeps them clean.
3 min read

State Spotter is only as good as its catalog, and the catalog started as a third-party CSV that went stale the day I imported it. Its 8,000+ rows were too many to fix by hand and too important to hand to a model unsupervised, so I built an admin dashboard that sits between the two.
Vite, React, TypeScript, Express 5, sharp, Ollama, Firestore
When in doubt, ask
The model drafts. A person decides
Before writing any code I set one rule: when in doubt, hand the call to a person instead of guessing. Models find images, draft descriptions, propose tags and suggest families, but nothing is accepted automatically. That makes review speed the speed of the whole system, so most of the work went into the board: one state at a time, every plate's health in three coloured icons, and the detail one hover away.
The workflow
Pipeline stages
Find image3 steps
Locate a usable source image for a plate, and record where it came from.
Read the vendor source setShortlist candidate imagesAccept a pasted URL as an escape hatchClean up the image4 steps
Crop, replace or upscale a soft plate. Runs on demand, and never overwrites the original.
Crop in the browserReplace by uploadUpscale 4× and cut out the plateKeep-new or revert, side by sideHydrate4 steps
Fill in what a player reads: what the plate is, what colour it is, and what is on it. Three passes that check each other.
Draft the description from a matched Wikipedia articleMeasure plate and text colour, no model involvedTag with a local vision model; the bulk pass runs hostedCross-check the tags against the measured colourGroup3 steps
Propose template and variant families, each with a confidence band. A person applies them, or doesn't.
Cluster offline and write proposals to a fileOptional model tiebreak, off by defaultA person applies the grouping, or does notOrder3 steps
Set the canonical order a player sees, then regenerate the manifest the game bundles.
Drag to reorder templates; variants inherit their placeMark the state doneRegenerate the game's image manifest
Stage 02, up close
Nothing is overwritten
Every image change takes the same path: write the candidate beside the live asset, move the current one to a timestamped backup, then promote. Undo restores the exact backup, so a bad crop or a bad upscale costs one click.

No app update required
Publish to players
The game ships with a catalog built in, but it also checks a small pointer in Firestore each time it opens. Publishing uploads a new, versioned snapshot of the catalog and moves that pointer, so every player picks up the new plates the next time they launch, without an app-store release. Before anything goes out, the dashboard shows exactly what players will receive: the version bump and every added, changed or removed plate, state by state. The built-in catalog stays as the offline fallback.

What I'd do differently
I would write the serial write queue correctly the first time. One rejected promise silently stopped every write after it while memory and Firestore agreed the change had landed; only the file on disk disagreed. Anything that can look applied without being applied deserves the careful version up front.