Wave 1 of the remediation plan for the 2026-09-03 feature test run. Two kiosks, one shape of defect, one critical consequence. Both kiosks read the state, decided in Python, and then wrote. Nothing stops two requests interleaving between the read and the write, so both see the same pre-state and both proceed. Reproduced on MySQL 5.6 over HTTP, five concurrent takes of 5 from a bin holding 5: all five were accepted and quantityonhand landed on -20, because each request wrote its own arithmetic. The ledger had recorded every one of them, so the cached quantity and the ledger disagreed permanently, which is the number the shop floor actually reads. The USB kiosk had the same shape: concurrent scans of one drive all succeeded and inserted several open custody rows, so one drive was recorded out to two people and the documented 409 never fired. The comparison now lives in the WHERE clause, where the database settles it at write time. printedparts._move_quantity issues UPDATE ... WHERE quantityonhand >= :n and treats rowcount 0 as the refusal, raising InsufficientStock, which take and adjust turn back into the message they already gave - carrying the quantity the database holds now, not the stale one the request read. The low-stock alert derives its before-value from what the write produced for the same reason. Both USB paths claim the device with ischeckedout (self-hosted) or status (external cmmc_usb) in the WHERE clause; the external one claims BEFORE writing the log line, so a refused claim leaves nothing to undo. Check-in is guarded the same way. After the fix, the same five concurrent takes: one accepted, four refused, quantityonhand 0 and the ledger sum 0. Check-in also closes EVERY open row for the device rather than the newest. One device can only be out once, so a second open row is corruption, and closing only the newest left the older ones open forever - the device then read as returned while its custody history said otherwise. Separately, usbcheckouts now records who handed a drive back (checkinsso/checkinname, migration usb0004checkinidentity). Returning for a colleague is a supported flow that the kiosk asks to confirm, but the row it wrote attributed the check-in to the HOLDER, so the custody history and the audit report both named somebody who was not there. Nullable and deliberately not backfilled: copying the holder's badge into old rows would manufacture evidence for the exact claim this stops making. Four tests. Both guards verified by planting: strip the condition out of either UPDATE and its test fails. Scope: product
Printedparts plugin
3D-printed parts inventory + kiosk checkout
This plugin was generated by flask plugin new printedparts. It satisfies the framework contract out of the box. Replace the example model and routes with your domain.
What's here
plugin.py- thePrintedpartsPluginclass extendingBasePlugin. Editinit_appfor custom setup,on_installto seed reference data.models/printedparts.py- example Asset extension table. Replaceexamplefieldwith your domain fields.api/routes.py- example list and detail endpoints. Add CRUD as needed.schemas/__init__.py- marshmallow schema stub for request/response validation.tests/test_plugin.py- smoke tests asserting contract compliance.manifest.json- plugin metadata. Bumpversionon changes; keepcore_versionrange broad.
Common edits
| You want to... | Do this |
|---|---|
| Add a hook (search, navigation, dashboard widget) | Override the method in PrintedpartsPlugin. See the PLUGIN-HOOKS wiki page. |
| Accept external collector data | Override get_collector_schema() to return a JSON Schema. See ADR-006. |
| Add another model | Create models/<other>.py, export it in models/__init__.py, return it in get_models(). |
| Add a CLI command | Override get_cli_commands() returning a list of Click commands. |
Frontend
Vue components for this plugin live under frontend/src/views/printedparts/ (per project convention). Backend scaffolding does not generate frontend yet; copy from an existing plugin's view files (e.g., frontend/src/views/network/) as a starting point.
Install and run
flask plugin install printedparts
flask db migrate -m "Add printedparts plugin tables"
flask db upgrade
pytest plugins/printedparts/tests/
References
- the PLUGIN-HOOKS wiki page - canonical hook reference
- the PLUGIN-QUICKSTART wiki page - 30-minute walkthrough
migrations/adr/ADR-001-asset-as-platform-contract.md- the platform contractmigrations/adr/ADR-002-plugin-versioning.md- versioning rules
Why the kiosk take endpoint is unauthenticated
POST /api/printedparts/kiosk/take is the product's first open WRITE (every
other kiosk endpoint is a read). Accepted deliberately, against the criteria
in docs/proposals/printedparts-plugin.md:
- Decrement-only: it can reduce stock of an active item, nothing else.
- Fully attributed: it refuses to act without a badge that resolves under the site policy; every action lands in the ledger with SSO + name + time.
- Bounded blast radius: worst case is stock counts driven low - visible in the ledger and reversible with an adjust.
- Physically rate-limited: it serves a touch screen on the shop floor; nothing enumerable, nothing worth scraping.
Any future open-write endpoint must clear the same bar.