Two operator-visible defects and three doc corrections, closing the tail of the 2026-09-03 run. THE BADGE. The two kiosks read the same physical badges and disagreed. The parts kiosk understood both shapes a scanner emits - the bare SSO and the PayNo wrapper 0<digits>BZ - while the USB kiosk compared the raw string to an SSO, so the wrapper resolved to nobody. An operator whose keyboard-wedge scanner emits it could take a 3D-printed part and could not check out a drive, with the same badge, at two cabinets a few metres apart. The shape now lives in shopdb/utils/badges.py on the contract surface, because a plugin may not import another plugin and because a badge is a property of the site rather than of a cabinet. Both kiosks use it, and both store the DIGITS rather than the string that arrived: one row per spelling makes a custody history that cannot be joined to itself. That closed a subtler hole in the same place. str.isdigit() is true for Arabic-Indic and other decimal forms, and int() parses them, so a badge written in those characters was accepted, matched a REAL employee, and was then stored as an SSO string nothing else matches. ASCII digits only. An empty scan and an unreadable one now get different answers, because they are different problems for the person at the cabinet. THE IMAGES. /api/applications/image/<filename> and /api/models/image/<filename> required a permission, and a browser fetching <img src="..."> sends no Authorization header - so the gate did not protect the file, it guaranteed the picture never appeared. Both are ungated now, like the four sibling image routes (branding logo, map blueprint, employee photo, part picture), and registered in PUBLIC_ENDPOINTS with that reasoning. /api/printers/lookup?fqdn= never matched anything: both arguments were compared against Communication.ipaddress, so a hostname lookup 404d on printers whose hostname was on the record. An FQDN now matches the hostname, with a fallback to the short name. The slide manager gained the per-slide hold time. The PATCH route and the feed have always carried `seconds`; nothing called it, so the one documented per-slide control was curl-only. Docs: API-REFERENCE listed the employee lookups and the whole notifications surface as unauthenticated when they answer 401, and called both kiosks "deliberately open" when they are off by default and refuse anything without the site's device token. The installer download says applications.download, which is what the code requires. api-inventory.json said printers/lookup was jwt-optional. Fourteen tests. The badge rule verified by planting the old per-kiosk behaviour: three fail, one in each kiosk. 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.