Searchable, version-controlled inventory of lab reagents, kits, consumables, and equipment — replacing the spreadsheet in legacy/.
Development plan and architecture: PLAN.md · Photo rules (public repo): PHOTO_POLICY.md
Phases 1–3 complete — the spreadsheet is migrated to validated CSVs, there's a searchable site with printable QR stickers, and you can add or update items from a form without touching git. Photo intake with LLM descriptions (Phase 4) is still to come; see PLAN.md §5.
| Items | 499 |
| Locations | 173 |
| Verified since 2021 | 0% — everything carries status: unverified |
| Rows needing a human pass | 16, in data/review_queue.csv |
| File | What it is |
|---|---|
data/items.csv |
One row per item. The inventory. |
data/locations.csv |
Controlled vocabulary of physical places, nested via parent_id. Carries its own last_verified, so confirming a drawer is still empty counts as a real check. |
data/rooms.csv |
The declared scope: rooms 209, 213, 228, 230, and the -80˚C room. |
data/review_queue.csv |
Legacy rows too ambiguous to migrate automatically. Each has a stated reason. |
Coverage today is uneven — 209=70, 213=99, 228=0, 230=2, M80=2 locations. Room 228 is in scope but appears nowhere in the legacy spreadsheet, so it has nothing to migrate and needs an inventory pass; validate.py warns about it on every run until it does.
location_id is the stable key — 209-CAB-01, 213-F20-S02-D08, 209-FRIDGE-DOOR-SHELF-1. It's built to be short because it becomes a URL fragment and a QR-sticker payload.
Two fields carry more weight than they look like they do:
last_verified— every migrated row says2021-08-29, because that's what the spreadsheet's own sheet names claim. This makes five years of staleness a queryable fact instead of a caveat.source—legacy-xlsx,manual, orphoto-llm. Permanent provenance, so an audit can always ask which rows a model wrote.
You don't need git, and you don't need to know the file layout. File an issue:
| Add an item | Something new in the lab |
| Mark consumed / low / missing | You used the last of it, or it isn't where we say |
| I verified a location | You opened a drawer and checked. The most valuable thing you can file. |
A bot turns the form into a pull request for a maintainer to merge. If something's wrong with the form — unknown location, a name matching five items — it comments on your issue and changes nothing.
The fastest path is from the shelf itself: scan the QR sticker → tap "Add an item here", and the location arrives already filled in.
Empty drawers are worth filing too. 46 locations have nothing recorded; confirming one is still empty takes ten seconds and retires it from the unknown pile. Switch the site's last dropdown to Locations to find them — each row has its own verify link.
Only the first three fields of the add form are required. A name and a location is already a useful record — don't stall on catalog numbers.
Search UI and QR stickers deploy from main to GitHub Pages. Build and preview locally:
python3 scripts/build_site.py && python3 scripts/make_qr_labels.pyThat writes _site/index.html (self-contained — just open it, no server needed) and _site/labels.html, a printable sheet of QR stickers. Each sticker encodes <site>/#<location_id>, so scanning the one on a drawer shows that drawer's contents, including anything nested inside it.
Stickers for just part of the lab:
python3 scripts/make_qr_labels.py --rooms 213 --kinds drawer cabinetpip install -r requirements.txtValidate before opening a PR (CI runs the same thing):
python3 scripts/validate.pyRegenerate the CSVs from the frozen spreadsheet:
python3 scripts/migrate_legacy.pyNote that re-running the migration overwrites data/, discarding hand edits. It's a one-time tool kept for reproducibility and for re-tuning the item categorizer; once real edits start landing, items.csv is the source of truth and the spreadsheet is history.
Two bot branches cut from the same main will both merge without a conflict and still leave the data invalid: git text-merges items.csv, so an edited row's old and new copy both survive, and next_item_id() hands the same id to two different items. Neither branch is wrong on its own.
validate.py alone doesn't catch this, because a PR opened with GITHUB_TOKEN doesn't trigger validate.yml — the only check a bot branch gets is the one issue-to-pr runs before the PR exists. Main then moves and nothing looks again.
So merge-guard.yml merges every open PR into current main and re-runs validate.py on the result, every time main moves, and reports the verdict as a merge-guard commit status on the PR. A PR that has gone stale turns red without anyone touching it.
This only blocks a merge if merge-guard is a required status check — Settings → Branches → branch protection for main → Require status checks to pass → add merge-guard. Without that it's advisory.
A red merge-guard on a bot PR means regenerate, not rebase: the branch is fully derived from its issue, so close the PR and re-trigger the issue rather than resolving anything by hand.
gh workflow run merge-guard.yml -f pr=123 # re-check one PR
gh workflow run merge-guard.yml # re-check all open PRsTo rebuild the PRs for issues that don't have a good one — either they never got one, or merge-guard has marked theirs red:
python3 scripts/replay_issues.py --dry-run # see the plan firstIt re-triggers each issue one at a time and waits for the PR to appear before starting the next. Pace it. Filing form issues in a rapid burst is what broke this repo once already: Actions quietly stopped creating workflow runs partway through, and no error appears anywhere — not on the issue, not in the Actions tab. Run --only red after a batch of merges to regenerate whatever went stale.
- Dates are ISO 8601, and partial dates are allowed where that's the honest precision:
2019,2019-11,2019-11-27. category,status,kind, andsourceare closed sets, enforced in CI. Adding a value means editingscripts/validate.pyin the same PR.quantityis a number or empty. Ranges, guesses, and "about half a box" go innotes.- Don't block an entry on details you don't have. A name and a location is a useful row; the rest can be filled in later.