docs: wiki staleness sweep (Fable-orchestrated Opus audit)

Audited all 40 docs/ against the live codebase; fixed factual staleness in 23,
14 were clean. Highlights (all verified against code):
- equipment -> machines (ADR-011 rename) in INSTALL/DEPLOY-WINDOWS-IIS,
  PLUGIN-GUIDE, GE-ENFORCE, ROADMAP.
- Versions refreshed: contract 0.10.0 -> 0.13.0, product 0.5.0 -> 0.7.0, plus
  plugin example core_version pins.
- Bundled set corrected to the current 13 (PLUGINS.md 7 -> 13 rows; DEPLOY
  eleven -> thirteen).
- Per-plugin Alembic chain workflow (ADR-008) replacing stale core-chain steps
  in PLUGIN-QUICKSTART / BACKUP-RESTORE; deploy adds plugin upgrade-all.
- Frontend plugin staging (ADR-010) replacing 'no frontend plugin system yet'
  in PLUGIN-GUIDE; view/route paths repointed to plugins/<name>/frontend/.
- Corrected file paths (MapView.vue, manifest_schema.json), CLI (shelf-list),
  API gating (GET /api/plugins is optional-jwt), WJF 15 -> 16 stages, and
  retired Collector/PC-Types settings pages (ADR-012).
- ge-enforce proposal marked ACCEPTED/built.
This commit is contained in:
cproudlock
2026-07-19 12:54:53 -04:00
parent 49a0206b9f
commit e005d1846a
26 changed files with 145 additions and 84 deletions

View File

@@ -96,18 +96,42 @@ CREATE DATABASE shopdb_flask CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
tar xzf instance-2026-07-10.tar.gz # restores ./instance/
```
The docker-compose api container reads `instance/` from the repo working
directory; make sure it is present before starting `api`.
The default `docker-compose.yml` does NOT bind-mount `instance/` into the api
container (its only volume is `- ./plugins:/app/plugins:ro`, and the image never
copies `instance/`), so the container's Flask instance path is an empty
`/app/instance` and a restored host `./instance` is invisible to it. To make the
restored `instance/` visible, add a bind mount to the api service before starting
it:
```yaml
api:
volumes:
- ./plugins:/app/plugins:ro
- ./instance:/app/instance
```
Make sure `./instance` is present on the host before starting `api`.
### Step 4: Bring up the API and reconcile migrations
```bash
docker compose up -d api
docker compose exec api flask db upgrade
docker compose exec api flask plugin upgrade-all
```
For a non-docker deploy:
```bash
flask db upgrade
flask plugin upgrade-all
```
`flask db upgrade` is a safety net: if the dump predates the current code, this
applies any newer migrations. If the dump is at the same version it is a no-op.
applies only the core Alembic chain. `flask plugin upgrade-all` then applies any
newer per-plugin migrations (each bundled plugin owns its own chain, ADR-008);
without it, plugin-owned tables stay un-migrated. If the dump is at the same
version both are no-ops.
### Step 5: Verify

View File

@@ -78,9 +78,10 @@ suspended for it, so the token is contained to the collector API even though its
owner is an admin - it cannot act with admin authority anywhere.
1. Settings > API Tokens > New Token.
2. Click the **Collector service token** preset (pre-selects only
`collector.ingest`), name it (e.g. `wj-fleet-collector`), optionally set an
expiry, Create.
2. Check **Restrict permissions**, then in the permissions grid tick only
**Submit collector payloads (fleet reporting)** (the `collector.ingest`
permission under the Collector category). Name it (e.g. `wj-fleet-collector`),
optionally set an expiry, Create.
3. Copy the `shopdb_pat_...` secret (shown once) and deploy it to the fleet the
same way as the env key: the `collectorApiKey` field in per-site
`site-config.json` (see "Delivering the API key to clients" below). The
@@ -244,10 +245,12 @@ the PCs that use it (both render in the shared Relationships card).
### pc-type mapping (configurable per site)
`pctype` (e.g. `gea-shopfloor-cmm`) maps to a shopdb Computer Type through
`pctypemap_<pxetype>` settings (Settings > Collector PC Types).
Defaults live in `plugins/computers/pctypemap.py` and are seeded on plugin
install; edit per site in the UI. Unmapped pc-types produce a warning, not a
failure.
`pctypemap_<pxetype>` settings. The "Collector PC Types" settings page is
retired (ADR-012): pc-type-to-Computer-Type handling now lives in GE-Enforce
(each imaging PC type is a manifest scope with its own `computertypeid`). The
built-in defaults in `plugins/computers/pctypemap.py` are still seeded on plugin
install and the collector still reads them, so existing enrollment keeps
working. Unmapped pc-types produce a warning, not a failure.
### Classic api.asp field mapping (for porting the PowerShell reporter)

View File

@@ -8,11 +8,11 @@ the live code, not aspiration. The authoritative hook reference is
## Current version
The plugin contract is at **0.10.0**, declared in `shopdb/__init__.py` as
The plugin contract is at **0.13.0**, declared in `shopdb/__init__.py` as
`__contract_version__`. It is pre-1.0, which under semver means any 0.x minor
bump is allowed to break the contract, and this project has used that latitude.
The product release version (`__version__`, currently 0.5.0) is a separate
The product release version (`__version__`, currently 0.7.0) is a separate
series with its own bump rules; see [ADR-007](adr/ADR-007-product-versioning-and-releases.md).
Do not pin against it for compatibility - pin against `__contract_version__`.
@@ -28,8 +28,13 @@ Recorded in the comment block in `shopdb/__init__.py`:
| 0.7.0 | Added the four ADR-010 frontend-contribution hooks (`get_settings_cards`, `get_asset_panels`, `get_map_overlays`, `get_asset_presentation`), consumed by the `GET /api/pluginui/*` endpoints | additive optional hooks (minor) |
| 0.9.0 | Exposed the dualpath pair-resolution helpers on `shopdb.api` for the machines plugin | additive surface (minor) |
| 0.10.0 | Added the `get_permissions` hook so plugins declare their own RBAC permissions; the catalog is resolved dynamically from core + enabled plugins | additive optional hook (minor) |
| 0.11.0 | Added `service_token_authorized(scope)` to `shopdb.api` so a plugin's unattended endpoints (e.g. the GE-Enforce fetch API) can authorize a scoped managed service token without importing core token internals | additive surface (minor) |
| 0.12.0 | Added the mailer helpers (`send_email`, `send_alert`) to `shopdb.api` | additive surface (minor) |
| 0.13.0 | Added the `User` model to the `shopdb.api` surface | additive surface (minor) |
The source comment block documents 0.3.0, 0.4.0, 0.6.0, 0.7.0, 0.9.0, and 0.10.0. Earlier points
The source comment block documents 0.3.0, 0.4.0, 0.6.0, 0.7.0, 0.9.0, 0.10.0, and 0.11.0 (its
last entry); the current `__contract_version__` 0.13.0 is ahead of the last documented comment
entry. Earlier points
(0.1.x / 0.2.x) predate that recorded rationale; `PluginMeta`'s fallback
`core_version` default of `>=0.2.0,<1.0.0` is the only remaining trace of the
0.2 baseline.

View File

@@ -19,7 +19,7 @@ physical path must be `APP_ROOT` (where `wsgi.py` lives).
- **URL Rewrite** module (only for the optional real-client-IP rule).
- Network access to the MySQL 5.6 server.
- If the box is air-gapped, you cannot `pip install` live. On the dev box run
`pip download -r requirements.txt waitress -d wheels\` (on a matching
`pip download -r requirements.txt -d wheels\` (on a matching
Windows/Python target, or use `--platform` wheels), copy `wheels\` over, and
install with `pip install --no-index --find-links wheels\ ...`.
@@ -42,12 +42,11 @@ cd C:\shopdb-flask
py -3.12 -m venv venv
venv\Scripts\python -m pip install --upgrade pip
venv\Scripts\pip install -r requirements.txt
venv\Scripts\pip install waitress
```
The DB driver is `pymysql` (pure Python) so no C compiler / MySQL client libs
are needed. `waitress` is the WSGI server (installed separately, same as the
Docker image installs gunicorn separately).
are needed. `waitress` is the WSGI server and ships in `requirements.txt`
(unlike gunicorn, which the Docker image installs separately).
## 3. Prepare MySQL 5.6 (the utf8mb4 gotcha)

View File

@@ -116,13 +116,15 @@ docker compose exec api flask seed reference-data
- `seed settings` - writes the default Setting rows (branding, ServiceNow
integration, floor-map placeholders, search toggles, site identity). A site
overrides these later in Settings or the setup wizard.
- `seed reference-data` - creates default `Vendor`, `Location`, `BusinessUnit`,
`OperatingSystem`, `AssetStatus`, `RelationshipType` rows seeded with the
platform contract values (`partof`, `controls`, `connectedto`).
- `seed reference-data` - creates default `ModelType`, `AssetStatus`,
`LocationType`, `CommunicationType`, `OperatingSystem`, `RelationshipType` rows
seeded with the platform contract values (`partof`, `controls`, `connectedto`).
(`Vendor`, `Location`, and `BusinessUnit` are not seeded here; they come from
`seed demo`.)
## Step 5: Pick plugins to enable
The image bundles eleven plugins (computers, employees, knowledgebase, machines, measuringtools, network, notifications, printers, slides, usb, warranty). Only enabled plugins are loaded.
The image bundles thirteen plugins (computers, employees, geenforce, knowledgebase, machines, measuringtools, network, notifications, printedparts, printers, slides, usb, warranty). Only enabled plugins are loaded.
```bash
docker compose exec api flask plugin list

View File

@@ -23,7 +23,7 @@ sane, then use manual for day-to-day work.
| Need | Version | Check |
| --- | --- | --- |
| Python | 3.13 (64-bit) - matches prod + CI | `python --version` |
| Python | 3.13 (64-bit) - matches CI; prod images run 3.12 | `python --version` |
| Node.js | 18+ | `node --version` |
| MySQL | 8.0 (or Docker, below) | `mysql --version` |
| Git | any recent | `git --version` |

View File

@@ -88,7 +88,7 @@ POST /api/geenforce/report
present), `skipped` (detected present), `failed`, `filtered`. `selfhealed`
marks a drift correction.
- shopdb keeps the latest report per (hostname, scope, phase) plus history, and
surfaces it under Settings > Enforcement Reports.
surfaces it under GE-Enforce > Enforcement Reports.
The engine already computes these counts (`installed/skipped/failed/pcFiltered`
at the end of its main loop) and knows each entry's action; shape them into the
@@ -105,7 +105,7 @@ with `Installed/Skipped/Failed/Filtered` + a `Results` list).
3. **Read cutover**: drop `-ShadowMode`. The engine now runs against the
shopdb-sourced manifest; payloads still come from the share. Rollback is a
one-line revert to the share-sourced call. Keep exporting manifests from
shopdb to the share (Settings > Imaging PC Types > Export to Share) so the
shopdb to the share (GE-Enforce > Manifests > Export to Share) so the
share stays a break-glass copy.
4. **Payload migration** (optional, later): move small scripts/configs to
`http`/`inline` payloads, verified by `payloadsha256`. Big MSIs stay on SMB.

View File

@@ -231,9 +231,9 @@ because it is a full management surface.
### 4.1 Manifests - authoring (GE-Enforce > Manifests)
- **PC Types (scopes):** each imaging PC type is a row; add/edit/delete. (The
scope carries an optional `computertypeid` reference field, but the collector's
imaging-pc-type -> ComputerType mapping is configured separately at
Settings > Collector PC Types.)
scope's `computertypeid` field is now the imaging-pc-type -> ComputerType
mapping mechanism, per ADR-012. The old Settings > Collector PC Types page is
retired; there is nothing to configure there anymore.)
- **Entries:** an ordered list (Up/Down = the execution-order contract). Add/Edit
opens a typed form: the payload fields switch on `Type` (MSI shows Installer +
InstallArgs, PS1 shows Script + Args, File shows Source + Destination, Registry

View File

@@ -113,7 +113,7 @@ venv\Scripts\flask seed settings
# enable the plugins this site tracks (registry is empty on a fresh box).
# usb + employees install DISABLED by default - enable them later in the wizard
# if the site wants those (they create extra tables).
foreach ($p in "computers","equipment","network","notifications","printers","knowledgebase","slides","warranty") {
foreach ($p in "computers","machines","network","notifications","printers","knowledgebase","slides","warranty") {
venv\Scripts\flask plugin install $p
}
@@ -240,7 +240,7 @@ each gets its own site, app pool, port, and venv.
| **500** with an empty HttpPlatform log | app-pool identity can't read `APP_ROOT` / run the venv (step 7.3), or `.env` missing/invalid. |
| "internal error" toggling plugins, or uploads fail | app pool cannot WRITE `APP_ROOT\instance` (plugin registry, logos, photos, files live there) - step 7.3 grants it Modify. |
| "No time zone found with key America/New_York" | `tzdata` not installed (`pip install tzdata`). |
| Nav missing Equipment/PCs/... | plugins not installed (step 6 `flask plugin install`), or site not recycled. |
| Nav missing Machines/PCs/... | plugins not installed (step 6 `flask plugin install`), or site not recycled. |
| Method B: blank page / assets 404 under `/ops` | frontend `dist` built without `VITE_BASE_PATH=/ops/` (step 7b.1). |
| Method B: SPA loads but every API call 404s | `MOUNT_PATH` unset or not matching the Application alias (step 7b.3). |
| ConfigError on boot | a required `.env` var missing or left at a dev default. |

View File

@@ -76,7 +76,7 @@ import API. It is site glue, not product code.
venv/bin/python -m scripts.site_imports.wjf.run
```
The 15 stages run in order (reference -> catalog -> assets hub -> locations ->
The 16 stages run in order (reference -> catalog -> assets hub -> locations ->
printers -> dependents -> relationships -> subnets -> usb -> verify). It is
idempotent - a crashed run resumes from `idmap.json`.

View File

@@ -112,7 +112,7 @@ admits only the contract minor you tested against, not the whole 0.x line.
The current contract version is declared in `shopdb/__init__.py`:
```python
__contract_version__ = '0.6.0'
__contract_version__ = '0.13.0'
```
Recommended pin in your `manifest.json`, per ADR-002 (pip-style `>=,<` ranges):
@@ -122,7 +122,7 @@ Recommended pin in your `manifest.json`, per ADR-002 (pip-style `>=,<` ranges):
"name": "shipping",
"version": "1.0.0",
"description": "Tracks shipping-station scanners and label printers",
"core_version": ">=0.6.0,<0.7.0",
"core_version": ">=0.13.0,<0.14.0",
"dependencies": []
}
```

View File

@@ -78,7 +78,7 @@ plugin's identity (ADR-002):
Two fields deserve attention.
`core_version` is a semver range against the framework's `__contract_version__`
(declared in `shopdb/__init__.py`, currently `0.6.0`). The loader refuses to load
(declared in `shopdb/__init__.py`, currently `0.13.0`). The loader refuses to load
a plugin whose range excludes the running framework. We pin `>=0.6.0` because this
plugin uses the `get_reports` hook, which was added to the contract in 0.6.0
(see [PLUGIN-HOOKS.md](PLUGIN-HOOKS.md), "get_reports"). We cap at `<1.0.0` because
@@ -272,7 +272,7 @@ assertion to a frozen `CUTOVER_PLUGINS` list rather than to all discovered plugi
and to expect `measuringtools`'s real baseline revision in the upgrade-all test:
```python
CUTOVER_PLUGINS = ('computers', 'employees', 'equipment', 'knowledgebase',
CUTOVER_PLUGINS = ('computers', 'employees', 'knowledgebase', 'machines',
'network', 'notifications', 'printers', 'slides', 'usb', 'warranty')
@pytest.mark.parametrize('plugin', CUTOVER_PLUGINS) # not all plugins
@@ -281,7 +281,10 @@ def test_anchor_migration_is_noop(plugin):
```
Freezing the list (rather than deriving it) is intentional: a newly discovered
plugin should not silently be treated as a cutover no-op.
plugin should not silently be treated as a cutover no-op. Note that `machines`
(renamed from `equipment`, ADR-011) keeps its original cutover anchor but carries
a `machines0002rename` revision on top of it, so its expected head is that rename
revision rather than the bare anchor (`tests/test_plugin_migrations.py`).
---
@@ -499,23 +502,25 @@ the plugin small.
## 9. Frontend integration
There is no frontend plugin system yet (see
[ADR-009](adr/ADR-009-frontend-plugin-gating.md), "Future direction"). A plugin's
Vue routes and views ship in the core bundle. The plugin's job is to add them
correctly and gate them.
A plugin ships its own Vue routes and views under
`plugins/<name>/frontend/` (ADR-010's frontend plugin hook contract). At build
time those files are staged into `frontend/src/.plugins-staged/<name>/` so the
core bundle picks them up; you author them in the plugin tree, not in core
`frontend/src/`. The plugin's job is to add them correctly and gate them.
**Route module with `meta.plugin` gating (ADR-009).** A new file
`frontend/src/router/routes/measuringtools.js` is auto-discovered by the router's
**Route module with `meta.plugin` gating (ADR-009).** The file
`plugins/measuringtools/frontend/routes.js` is staged into
`frontend/src/.plugins-staged/measuringtools/` and auto-discovered by the router's
`import.meta.glob('./routes/*.js')`. Every route carries `meta.plugin =
'measuringtools'`:
```js
export default [
{ path: 'measuringtools', name: 'measuringtools',
component: () => import('../../views/measuringtools/MeasuringToolsList.vue'),
component: () => import('./views/MeasuringToolsList.vue'),
meta: { plugin: 'measuringtools' } },
{ path: 'measuringtools/new', name: 'measuringtool-new',
component: () => import('../../views/measuringtools/MeasuringToolForm.vue'),
component: () => import('./views/MeasuringToolForm.vue'),
meta: { requiresAuth: true, plugin: 'measuringtools' } },
{ path: 'measuringtools/:id', ..., meta: { plugin: 'measuringtools' } },
{ path: 'measuringtools/:id/edit', ..., meta: { requiresAuth: true, plugin: 'measuringtools' } },
@@ -539,18 +544,18 @@ reorganize the file; just add the block, mirroring `machinesApi`.
**Views mirror the master templates.** The frontend has master templates
(`PrintersList.vue` for lists, `PrinterDetail.vue` for detail pages). `measuringtools` mirrors the equivalent equipment views:
- `views/measuringtools/MeasuringToolsList.vue` - table with search, a type filter,
- `plugins/measuringtools/frontend/views/MeasuringToolsList.vue` - table with search, a type filter,
and a calibration-status filter; the status badge uses `utils/colorStyle` with
the color the API derived.
- `views/measuringtools/MeasuringToolDetail.vue` - hero + Identity card +
- `plugins/measuringtools/frontend/views/MeasuringToolDetail.vue` - hero + Identity card +
Calibration card (with the derived badge) + Location card, plus the shared
`CustomFieldsSection` and `WarrantyPanel` (section 10).
- `views/measuringtools/MeasuringToolForm.vue` - asset core fields + type +
- `plugins/measuringtools/frontend/views/MeasuringToolForm.vue` - asset core fields + type +
location + the calibration fields, plus `CustomFieldsInputs`.
- `views/reports/CalibrationReport.vue` - the four buckets (overdue / due soon /
- `plugins/measuringtools/frontend/views/CalibrationReport.vue` - the four buckets (overdue / due soon /
current / unknown), mirroring `WarrantyReport.vue`.
**Settings subtype page.** `views/settings/MeasuringToolTypesList.vue` mirrors
**Settings subtype page.** `plugins/measuringtools/frontend/views/MeasuringToolTypesList.vue` mirrors
`PCTypesList.vue`: add / edit / delete with a `ColorSwatchPicker`. It is linked from
`settingsNav.js` with a "Measuring Tools" card group, so it appears in the settings
rail and landing overview.
@@ -578,7 +583,7 @@ detail page drops in `<CustomFieldsSection :assetid="tool.assetid" />` and the f
drops in `<CustomFieldsInputs :assettypeid="assettypeid" :assetid="currentAssetId" />`,
then calls `customFieldsRef.value.save(assetId)` after the tool saves. The one
subtlety: `CustomFieldsInputs` needs the asset-type id. Rather than hardcode it (the
equipment form hardcodes `EQUIPMENT_ASSETTYPEID = 1`), the measuringtools form
machines form hardcodes `MACHINE_ASSETTYPEID = 1`), the measuringtools form
resolves it dynamically from `GET /api/assets/types`, finding the row whose
`assettype === 'measuring_tool'`. Dynamic lookup is preferred because seeded ids are
not stable across sites.

View File

@@ -487,7 +487,8 @@ What `shopdb.api` exposes:
- Model bases: `BaseModel`, `AuditMixin`
- Core models: `Asset`, `AssetType`, `AssetStatus`, `Vendor`, `Model`,
`Communication`, `CommunicationType`, `Location`, `Setting`, `AuditLog`,
`Application`, `AppVersion`, `OperatingSystem`
`Application`, `AppVersion`, `OperatingSystem`, `AssetRelationship`,
`RelationshipType`
- Responses: `success_response`, `error_response`, `paginated_response`,
`ErrorCodes`
- Pagination: `get_pagination_params`, `paginate_query`
@@ -501,6 +502,8 @@ What `shopdb.api` exposes:
- Import mode: `apply_import_timestamps`, `import_mode_active`,
`parse_import_datetime`
- Legacy employee directory: `employee_connection`
- CMMC USB check-in/out DB (read-write, used by the usb plugin):
`cmmc_usb_connection`
- `User` / `Role` (0.13.0) - the account and role models, e.g. resolving
alert recipients' emails from selected user ids or role membership
- Mailer (0.12.0): `send_email(to, subject, html, text=None)` and

View File

@@ -21,7 +21,8 @@ decision records: `docs/proposals/printedparts-plugin.md`.
Know before you start
- BUNDLED plugin: frontend files live in core `frontend/src/`, and three core
- BUNDLED plugin: frontend files live in `plugins/printedparts/frontend/`
(staged into the Vite tree by `scripts/stage-frontend.mjs`), and three core
files get small edits (api client, sidebar icon map, PLUGIN_TABLE_OWNERS).
Normal for every bundled plugin.
- Ground rules: import core ONLY via `shopdb.api` (+ `shopdb.plugins.base`);
@@ -92,7 +93,7 @@ the edit (imports/meta boilerplate unchanged from the scaffold):
"description": "3D-printed parts inventory + kiosk checkout",
"display_name": "3D Printed Parts",
"dependencies": ["employees"],
"core_version": ">=0.13.0,<1.0.0",
"core_version": ">=0.12.0,<1.0.0",
"api_prefix": "/api/printedparts",
"default_enabled": false
}
@@ -371,8 +372,8 @@ export const printedpartsApi = {
```
2. Rename the scaffold views to `PrintedItemsList/PrintedItemDetail/
PrintedItemForm.vue` and repoint `frontend/src/router/routes/printedparts.js`
(auto-discovered by the router; list/detail carry `meta.plugin`, new/edit add
PrintedItemForm.vue` and repoint `plugins/printedparts/frontend/routes.js`
(aggregated into the router via `routes.gen.js`; list/detail carry `meta.plugin`, new/edit add
`requiresAuth`).
3. The list page, core of `PrintedItemsList.vue` (master template:
@@ -885,10 +886,12 @@ defineEmits(['digit', 'clear', 'backspace'])
(Terminal-style CSS - fixed 3-column grid, big targets, press feedback - at
the tag.)
### The kiosk view - `frontend/src/views/printedparts/PartsKiosk.vue`
### The kiosk view - `plugins/printedparts/frontend/views/PartsKiosk.vue`
Full-screen, no auth, registered TOP-LEVEL beside `/shopfloor` in
`frontend/src/router/index.js` (outside AppLayout, `meta.plugin` only):
Full-screen, no auth, exported as a `toplevel` route in
`plugins/printedparts/frontend/routes.js` and aggregated into
`frontend/src/router/index.js` via `routes.gen.js` (outside AppLayout,
`meta.plugin` only):
```javascript
{
@@ -961,7 +964,7 @@ Commit + tag `lab-stage-07`.
A plugin OWNS its label page (the USB precedent) - parts are not assets, so
they do not join the shared asset-label TYPE_CONFIG. New public route beside
`/print/usb-labels`, view `frontend/src/views/print/PrintedPartsLabels.vue`.
`/print/usb-labels`, view `plugins/printedparts/frontend/views/PrintedPartsLabels.vue`.
The pieces that matter:
```javascript
@@ -1184,7 +1187,7 @@ for a contributor:
| 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` |
| Plugin-owned label print view | `plugins/usb/frontend/views/USBLabelBatch.vue` |
| Barcode/QR rendering | JsBarcode in `AssetLabel.vue`, `qrLogo.js` |
| Kiosk route posture | `/shopfloor` in `frontend/src/router/index.js` |
| List/Detail master templates | `PrintersList.vue`, `PrinterDetail.vue` |

View File

@@ -91,13 +91,14 @@ audit_log(action='created', entitytype='Camera', entityid=asset.assetid, entityn
## Step 4: Install the plugin
First add `plugins/cameras/migrations/` with a per-plugin Alembic chain that creates the plugin's tables, and register those tables in `PLUGIN_TABLE_OWNERS` (per ADR-008; the plugin chain owns plugin schema, never the core chain). Then:
```bash
flask plugin install cameras
flask db migrate -m "Add cameras plugin tables"
flask db upgrade
flask plugin upgrade-all
```
`install` runs the plugin's `on_install` hook (which seeds the AssetType row), registers it in the plugin registry, and runs migrations.
`install` runs the plugin's `on_install` hook (which seeds the AssetType row), registers it in the plugin registry, and runs the plugin's own migration chain. `flask db migrate`/`flask db upgrade` is reserved for core tables and must not be used for plugin schema.
## Step 5: Verify it works

View File

@@ -15,6 +15,12 @@ These plugins are in `plugins/` in this repo. Enable per site with `flask plugin
| `usb` | USB devices issued to shop-floor users | Lightweight checkout / check-in. |
| `notifications` | Shop-floor notifications, recognitions, kiosk feed | Used by `ShopfloorDashboard.vue`. |
| `measuringtools` | Metrology and inspection instruments: calipers, micrometers, thread/bore/height gages, indicators | Per [ADR-005](adr/ADR-005-equipment-vs-measuringtools.md). Calibration lifecycle with derived status. First plugin built on the matured scaffold; its walkthrough is [PLUGIN-GUIDE.md](PLUGIN-GUIDE.md). Ships `default_enabled: false`. |
| `employees` | Read-only employee directory lookup | Backed by a separate HR database. Ships `default_enabled: false`. |
| `geenforce` | GE-Enforce manifest store: imaging PC-type scopes and their install manifests (apps, scripts, files, registry, version gates) | Per [ADR-012](adr/ADR-012-geenforce-manifest-ownership.md). Served to the GE-Enforce client as JSON. Requires GE-Enforce lib >= 2.6 on target PCs. Ships `default_enabled: false`. |
| `knowledgebase` | Knowledge Base articles linking to external resources | Lightweight article store. |
| `printedparts` | 3D-printed parts inventory | Kiosk checkout / check-in. Ships `default_enabled: false`. |
| `slides` | Slides for the lobby display and shop-floor screensaver | Upload / reorder / delete per surface. |
| `warranty` | Asset warranty tracking | Manual entry now, Dell / Lenovo / HP provider lookups later. Derived coverage status with report buckets. |
## Building your own

View File

@@ -20,9 +20,9 @@ The last big milestone before 1.0 is the legacy-ASP data import plus a productio
### Must-have
- **Asset model fully wired**. `Asset.mapx, Asset.mapy` columns, `AssetRelationship.inheritsposition`, `AssetRelationship.propagatesthroughid` columns. Models match the locked ADR-001 surface that `resolve_asset_position` already targets.
- **Asset model fully wired**. `Asset.mapx, Asset.mapy` columns, `AssetRelationship.inheritsposition` column, and the `relationshiptypepropagations` M:N table (`RelationshipTypePropagation` model; propagation lives on `RelationshipType`, not `AssetRelationship`). Models match the locked ADR-001 surface that `resolve_asset_position` already targets.
- **Equipment data migration script** for facilities migrating from legacy ASP shopdb. One-shot script under `scripts/migration/`. Per ADR-001, only `category='Equipment' AND machinenumber IS NOT NULL` migrates.
- **Printers retirement**. Legacy `PrinterData` model, `printers_bp` legacy blueprint, and the frontend `PrinterForm.vue` references to `printer.printerdata.*` get removed in lockstep. Coordinated with the equipment migration.
- **Printers retirement**. The printers plugin already runs on the asset architecture (blueprint `printers_asset_bp`); any remaining legacy printer-table cleanup is coordinated with the equipment migration.
- **External plugin UI packaging**. The Vue-side hook contract ships (ADR-010: get_settings_cards / get_asset_panels / get_map_overlays / get_asset_presentation) and route gating is backend-driven (ADR-009), but plugin routes/views still live in core `frontend/src`. Let an external plugin ship its own Vue bundle so adopters can add UI without editing core.
### Nice-to-have

View File

@@ -105,7 +105,10 @@ default need to act.
To check what your instance points at:
```bash
docker compose exec api flask shell -c "from shopdb.core.models.setting import Setting; print(Setting.query.filter(Setting.key.like('map_blueprint%')).all())"
docker compose exec -T api flask shell <<'PY'
from shopdb.core.models.setting import Setting
print(Setting.query.filter(Setting.key.like('map_blueprint%')).all())
PY
```
## See also

View File

@@ -183,7 +183,7 @@ Skipped from migration:
## References
- `shopdb/core/models/asset.py`
- `shopdb/core/models/machine.py` (legacy, deprecated)
- (core `Machine` model retired per this decision; machine data now owned by the machines plugin at `plugins/machines/models/machine.py`)
- `shopdb/plugins/base.py`
- ADR-002 (versioning of the surface)
- ADR-003 (plugin distribution)

View File

@@ -63,16 +63,21 @@ The framework provides:
## Migration strategy (resolved)
Deploys run a single core Alembic chain: `flask db upgrade`. Bundled plugins do
NOT carry their own migration chains - their tables are folded into the core
chain (migration `7c04_fold_plugin_schema`). This was a deliberate resolution of
the Phase 7B footgun where bundled-plugin baselines and the core baseline both
created the same tables, so `flask plugin upgrade-all` would conflict. A fresh
`flask db upgrade` reproduces the live schema exactly (verified on a scratch DB).
Deploys run two commands: `flask db upgrade` then `flask plugin upgrade-all`
(lean/ADR-014 sites add an optional `flask plugin prune-schema` at initial
provisioning). The core Alembic chain applied by `flask db upgrade` creates the
full core AND bundled-plugin schema through the chain head (this includes
migration `7c04_fold_plugin_schema`). But every bundled plugin still carries its
own Alembic chain per ADR-008: `flask plugin upgrade-all` stamps each plugin's
own chain (the `alembic_version_<plugin>` tables) and applies any plugin-specific
migrations added after the ownership cutover. The earlier Phase 7B state that
folded everything into core with no per-plugin chains was superseded by ADR-008's
per-plugin ownership. A fresh `flask db upgrade` reproduces the live core schema
exactly (verified on a scratch DB).
External (out-of-tree) plugins per ADR-003 may still ship their own migrations;
the framework supports per-plugin chains for them. Only the in-tree bundled
plugins are consolidated into core.
External (out-of-tree) plugins per ADR-003 ship their own migrations too; the
framework runs the same per-plugin chain mechanism (ADR-008) for both bundled and
external plugins.
## Open questions

View File

@@ -145,5 +145,5 @@ Reclassification is one-shot, run once, archived. Like the original migration sc
- ADR-001 (Asset is platform contract)
- ADR-002 (versioning of the surface)
- `plugins/equipment/` (current placeholder)
- `plugins/machines/` (equipment plugin, renamed per ADR-011)
- `plugins/computers/` (existing example of plugin pattern)

View File

@@ -41,7 +41,9 @@ list. This is the whole of what ships now.
anonymous callers is safe because `GET /api/dashboard/navigation`
already leaks the same enabled/disabled signal, and unauthenticated
kiosk routes (`/tv`) need the answer too. It carries no metadata, so it
reveals strictly less than the admin-gated `GET /api/plugins`.
reveals strictly less than the equally-anonymous but metadata-carrying
`GET /api/plugins` (also `jwt_required(optional=True)`; only the
`PUT /api/plugins/<name>` toggle is admin-gated).
2. **Route tagging.** Every plugin-owned route carries `meta.plugin =
'<pluginname>'`. This covers the per-plugin route modules

View File

@@ -43,7 +43,7 @@ one would mean forking a core view:
printer detail page, a calibration-status card on a measuring tool. Today
`WarrantyPanel.vue` is composed in by hand-editing each detail view.
4. **Map marker / overlay contributions.** A calibration-due badge on the
shop-floor map (`frontend/src/views/map/MapView.vue`). The map draws type
shop-floor map (`frontend/src/views/MapView.vue`). The map draws type
colors but has no plugin decoration path.
5. **Search-result rendering / routing for plugin asset types.** Global search
returns assets, but core hardcodes how each type renders and where its detail

View File

@@ -134,7 +134,7 @@ before signing (section 4).
increasing `serial` plus a `revoked` list of name-version pairs. Each site
records the last-seen serial in instance state and refuses an index with a
lower serial (anti-rollback of the catalog itself). The index also carries
per-entry version/tier/core_version so `flask plugin shelf list` can display
per-entry version/tier/core_version so `flask plugin shelf-list` can display
compatibility without unpacking, but the index is a BROWSE layer only:
adopt reads dependencies, tier, and core_version from the signed manifest
inside the verified artifact, never from the index.
@@ -344,7 +344,7 @@ against the real code, not the idealized layout:
window.
- Version skew across ADR-004 sites: one shelf serves sites at different
contract versions. Adopt checks core_version from the signed manifest against
the site's own __contract_version__ (authoritative); shelf list shows an
the site's own __contract_version__ (authoritative); shelf-list shows an
advisory compatibility column from the index. Incompatible artifacts are
listable but not adoptable.
- Partial/placeholder sync files: fail closed on hash verification; the error
@@ -361,7 +361,7 @@ against the real code, not the idealized layout:
- Phase 0, groundwork (small, days): upgrade_all_plugins uses
registry.get_all(); reverse-dep checks read installed manifests from disk;
cycle detection in _sort_by_dependencies; docs/plugin-manifest.schema.json +
cycle detection in _sort_by_dependencies; shopdb/plugins/manifest_schema.json +
`flask plugin validate` (directory mode); `flask plugin apply-profile` with
install+enable closure ordering; fix Dockerfile stale comment. All additive,
zero risk to running sites.
@@ -370,7 +370,7 @@ against the real code, not the idealized layout:
ed25519 signing tooling and curator docs. No runtime behavior change yet.
- Phase 2, shelf and enforcement (medium-large, one to two weeks):
PLUGIN_SHELF_DIR, signed shelf-index with serial + revoked list,
`flask plugin shelf list` / `adopt` / `audit` with atomic verified unpack;
`flask plugin shelf-list` / `adopt` / `audit` with atomic verified unpack;
verify-at-load in load_plugin and verify-at-migrate in
run_plugin_migrations, fail-closed in prod; PLUGIN_DEV_TRUST_DIRS for
dev/test and the external-repo harness; tier:core lifecycle guard;

View File

@@ -1,6 +1,6 @@
# Proposal: GE-Enforce as a shopdb plugin
Status: DRAFT / planning only. Not accepted, not built.
Status: ACCEPTED / built - see ADR-012 and plugins/geenforce/.
Author: planning session 2026-07-12.
## 1. What this is

View File

@@ -174,7 +174,7 @@ Labels (own print view, USBLabelBatch precedent):
## 9. Manifest
name printedparts, version 0.1.0, api_prefix /api/printedparts,
core_version ">=0.11.0,<1.0.0", dependencies ["employees"],
core_version ">=0.12.0,<1.0.0", dependencies ["employees"],
default_enabled false (site opts in - USB precedent).
## 10. Explicitly out of scope (v1)