Files
shopdb-flask/docs/PLUGIN-LAB-PRINTEDPARTS.md
cproudlock 6362cef699
Some checks failed
CI / backend (push) Successful in 1m38s
CI / naming (push) Successful in 1s
CI / frontend (push) Successful in 8s
CI / migrations-mysql (push) Failing after 7s
printedparts docs: record the open-write kiosk decision; defer the dashboard widget
The kiosk take endpoint is the product's first unauthenticated
mutation; spell out the acceptance criteria (decrement-only, badge
attributed, bounded, physically rate-limited) so future open-write
endpoints meet the same bar. The dashboard-widget milestone is marked
optional: get_dashboard_widgets predates the ADR-010 data-only
renderers and needs a core component to render.
2026-07-16 16:35:17 -04:00

11 KiB

Plugin lab: build the printedparts plugin yourself

A guided, milestone-based exercise: build the 3D-printed-parts storefront + kiosk plugin specified in docs/proposals/printedparts-plugin.md. Each milestone lists what to build, which existing code to imitate, and a checkpoint that proves you are done. Read the spec first, keep it open.

Prerequisites: a working dev environment (README quick start), the three plugin docs skimmed once - PLUGIN-QUICKSTART.md (mechanics), PLUGIN-GUIDE.md (the measuringtools walkthrough - your narrative reference), PLUGIN-HOOKS.md (hook reference). Naming rules: CONTRIBUTING.md - the pre-commit hook enforces them, read it before naming anything.

Ground rules

  • Import core ONLY via shopdb.api (+ shopdb.plugins.base). The contract test fails your build otherwise.
  • DB columns: lowercase concatenated (quantityonhand, not quantity_on_hand).
  • Run bash scripts/check-naming-and-style.sh and the test suite at every checkpoint.

Milestone 1 - skeleton, models, migration (backend exists)

Build

  1. flask plugin new printedparts - scaffolds plugins/printedparts/.
  2. The scaffold assumes an Asset-extension plugin; ours is standalone. In plugin.py strip the AssetType seeding from on_install (imitate plugins/knowledgebase/plugin.py instead of the template).
  3. Replace the scaffold model with the two spec tables: PrintedItem, PrintedItemTransaction (models/printeditem.py). Use BaseModel + AuditMixin from shopdb.api. Itemcode: leave generation to the API layer (M2), column just unique=True, index=True.
  4. Register both tables in PLUGIN_TABLE_OWNERS (shopdb/plugins/alembic_template.py).
  5. Create plugins/printedparts/migrations/ with the 3-line env.py + script.py.mako (copy from measuringtools) and a REAL baseline versions/0001_printedparts_baseline.py - hand-written op.create_table(...) for both tables (see plugins/measuringtools/migrations/versions/0001_measuringtools_baseline.py).
  6. Manifest: api_prefix /api/printedparts, dependencies: ["employees"], default_enabled: false.

Checkpoint

flask plugin install printedparts && flask plugin enable printedparts
flask plugin upgrade-all      # applies your 0001
mysql> SHOW TABLES LIKE 'printed%';          -- both tables
mysql> SELECT * FROM alembic_version_printedparts;   -- your revision id
pytest tests/ -q               # nothing broken, contract tests green

Milestone 2 - CRUD API + permissions + itemcode

Build

  1. api/routes.py: list/detail/create/update/soft-delete per the spec table. Imitate a clean plugin blueprint (plugins/measuringtools/api/routes.py) for pagination (perpage, dir), search, and the shared success_response/error_response helpers from shopdb.api.
  2. Itemcode on create: <prefix>-<id zero-padded to 4>; prefix from Setting printedparts_code_prefix (read via self.get_setting or Setting model through shopdb.api). Two-step: insert, flush to get the id, set code.
  3. get_permissions() on the plugin class: view/create/edit/delete/restock (tuples, category printedparts - copy shape from plugins/usb/plugin.py). Gate mutations with @jwt_required() + @require_permission(...); reads are @jwt_required(optional=True).
  4. Restock + adjust endpoints: both write a LEDGER row and move quantityonhand in the same commit. Adjust requires reason, rejects a result below zero. Both record the operator: accept badge in the body and resolve it (M4 extracts the resolver - for now digits=SSO is enough).
  5. Seed the 3 settings in on_install (Setting.set pattern - see how other plugins seed in on_install; category printedparts).

Checkpoint

pytest plugins/printedparts/tests/ -q      # write tests as you go:
# - create mints 3DP-0001 style codes
# - restock/adjust move both ledger and cache atomically
# - adjust below zero -> 400/422
# - permission gates: anonymous create -> 401, wrong-perm user -> 403
curl -s localhost:5001/api/printedparts/items | jq   # anonymous list OK

Milestone 3 - management frontend + images

Build

  1. The scaffold already dropped PrintedpartsList/Detail/Form.vue starters and a router file; rename/build them into PrintedItemsList/Detail/Form per the spec. Master templates: PrintersList.vue (list), PrinterDetail.vue (detail). Global CSS only; CSS variables for colors (frontend/CLAUDE.md rules).
  2. Register the API client in frontend/src/api/index.js (paste the generated frontend-api-snippet.js, extend with restock/adjust/image calls).
  3. Low-stock highlighting on the list (quantityonhand <= lowstockthreshold -> danger badge). Filters: search + low-stock-only checkbox.
  4. Image upload: replicate the models-image trio - upload/serve/delete - from shopdb/core/api/models.py INTO the plugin blueprint (instance/printedpartsimages/, public GET serve, imageurl column, prefix-guarded delete). Wire the Form upload + Detail hero image.
  5. Nav: get_navigation_items() -> "3D Parts" (usb plugin shape). Router meta: list/detail plugin-gated, new/edit requiresAuth (see frontend/src/router/routes/usb.js).

Checkpoint: create an item with a photo in the UI; thumbnail on list, hero on detail; restock from detail updates qty + shows in history; frontend build + npx vitest run green; naming hook green.

Milestone 4 - badge resolution + kiosk

Build

  1. Badge resolver in the plugin (services/badges.py): copy the USB contract
    • all-digits -> SSO; ^0(\d+)BZ$ case-insensitive -> PayNo; resolve display name via the employees plugin directory the way plugins/usb/api/selfhosted.py::_resolve_name does (lazy import inside the function, degrade gracefully when the plugin is absent). Policy setting printedparts_unknown_badge (deny -> 422).
  2. Kiosk endpoints (UNauthenticated - the notifications/employees open-read precedent): GET /kiosk/item/<itemcode> and POST /kiosk/take {itemcode, badge, quantity}. Take: validate active item, 1 <= qty <= onhand, resolve badge, single-commit ledger row + decrement. Clear error strings - the kiosk displays them verbatim.
  3. Kiosk view /parts-kiosk: top-level route, NO requiresAuth, outside AppLayout (register beside /shopfloor in frontend/src/router/index.js). Three-step flow per the spec. The scanner is a keyboard wedge: hidden always-focused input, submit on Enter, route the scan to whichever step is active. Build TouchKeypad.vue (3x4 grid of big buttons, emits digits/ clear/backspace) - net-new, nothing to copy, keep it dumb.
  4. Manual fallback path (typed item search + badge entry) behind a small "no scanner?" link.

Checkpoint: full kiosk walkthrough on a touchscreen (or browser): scan/type an itemcode -> item card; badge 0123456BZ and plain SSO both resolve; take 3 -> success screen, qty down 3, ledger row has your name; taking more than onhand -> friendly error; unknown badge -> denied message. Backend tests for the resolver shapes + take validation.

Milestone 5 - labels (1in x 0.5in)

Build

  1. Public print route /print/printedparts-labels + view (imitate USBLabelBatch.vue - USB is the precedent for a plugin OWNING its label page instead of joining TYPE_CONFIG).
  2. New stock size: @page { size: 1in 0.5in; margin: 0 }, one label per page (roll-fed label printers treat each page as one label). Layout: CODE128 via JsBarcode (~0.9in wide, displayValue false), itemcode text ~7pt under it, optional truncated name. Offer QR as a variant but default barcode.
  3. Batch: multi-select items -> sequence of labels; plus a ULINE mini-grid sheet fallback (mini72 pattern in AssetLabelBatch.vue).
  4. Print buttons on Detail (single) and List (batch selected).

Checkpoint: print preview shows one 1x0.5 label per page; a printed (or PDF-zoomed) barcode scans back into the kiosk and pulls up the right item. That round trip - label printed from the catalog, scanned at the kiosk, stock decremented with your name on it - is the demo moment; make it work end to end before polishing.

Milestone 6 - metrics, reports, widget

Build

  1. get_reports() -> stock, consumption (date range), by-person; endpoints in the plugin blueprint, @jwt_required(optional=True), ?format=csv via the generate_csv helper pattern (shopdb/core/api/reports.py shows the shape; a plugin report lives in the plugin and is merged into GET /api/reports automatically when enabled).
  2. Stock report includes the reconcile check: flag rows where cached quantityonhand != SUM(ledger). Should always be empty; if not, you have a non-atomic write path - find it.
  3. OPTIONAL/deferred: get_dashboard_widgets() -> low-stock count. Caveat: this hook predates the ADR-010 data-only renderers - the widget names a frontend component that must already exist in core, so a plugin widget only renders if you also add that component. Reports are the primary monitoring surface; skip the widget unless you want the extra credit.
  4. Nice-to-have if time: burn rate (avg weekly takes over trailing 4 weeks + weeks-to-empty). Plain SQL over the ledger.

Checkpoint: reports appear on /reports grouped under the plugin, CSV downloads; widget renders on the dashboard; reconcile column all-clear after a kiosk session.

Milestone 7 - lifecycle + closeout

Build/verify

  1. Disable/enable cycle: flask plugin disable printedparts - nav entry, routes, reports, and grantable permissions all disappear; enable restores.
  2. Fresh-database proof: point DATABASE_URL at a scratch DB, flask db upgrade + flask plugin install/enable/upgrade-all - everything works with zero manual SQL.
  3. Full suite: backend pytest, vitest, frontend build, naming hook.
  4. End checklist from PLUGIN-GUIDE.md section 12.

Done means: a colleague can clone the repo, enable the plugin, print a bin label, and take a part at the kiosk with their badge - without asking you anything.


Where each pattern lives (cheat sheet)

Need Copy from
Standalone (non-asset) plugin shape plugins/knowledgebase/
Checkout/ledger + badge contract plugins/usb/ (api/routes.py badge regex, api/selfhosted.py name resolve)
Real-baseline plugin migration plugins/measuringtools/migrations/
Blueprint style, pagination, authz plugins/measuringtools/api/routes.py
Image upload/serve/delete shopdb/core/api/models.py
Open kiosk endpoints precedent plugins/employees/api/routes.py, plugins/notifications/api/routes.py
Plugin-owned label print view frontend/src/views/print/USBLabelBatch.vue
Barcode/QR rendering JsBarcode usage in AssetLabel.vue, qrLogo.js
Kiosk route posture /shopfloor in frontend/src/router/index.js
List/Detail master templates PrintersList.vue, PrinterDetail.vue
Reports hook + CSV plugins/warranty/ report + shopdb/core/api/reports.py
Permissions declaration plugins/usb/plugin.py::get_permissions