From e005d1846a5dde605835179205bc64458066c27a Mon Sep 17 00:00:00 2001 From: cproudlock Date: Sun, 19 Jul 2026 12:54:53 -0400 Subject: [PATCH] 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//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. --- docs/BACKUP-RESTORE.md | 30 ++++++++++++-- docs/COLLECTOR-INTEGRATION.md | 17 ++++---- docs/CONTRACT-STABILITY.md | 11 ++++-- docs/DEPLOY-WINDOWS-IIS.md | 7 ++-- docs/DEPLOY.md | 10 +++-- docs/DEVELOPMENT-SETUP.md | 2 +- docs/GE-ENFORCE-CLIENT.md | 4 +- docs/GE-ENFORCE.md | 6 +-- docs/INSTALL-WINDOWS-IIS.md | 4 +- docs/PILOT-DEPLOY.md | 2 +- docs/PLUGIN-EXTERNAL-REPO.md | 4 +- docs/PLUGIN-GUIDE.md | 39 +++++++++++-------- docs/PLUGIN-HOOKS.md | 5 ++- docs/PLUGIN-LAB-PRINTEDPARTS.md | 21 +++++----- docs/PLUGIN-QUICKSTART.md | 7 ++-- docs/PLUGINS.md | 6 +++ docs/ROADMAP.md | 4 +- docs/UPGRADE.md | 5 ++- .../adr/ADR-001-asset-as-platform-contract.md | 2 +- docs/adr/ADR-004-deployment-topology.md | 23 ++++++----- .../ADR-005-equipment-vs-measuringtools.md | 2 +- docs/adr/ADR-009-frontend-plugin-gating.md | 4 +- docs/adr/ADR-010-frontend-plugin-hooks.md | 2 +- .../ADR-013-plugin-catalog-and-lean-builds.md | 8 ++-- docs/proposals/ge-enforce-plugin.md | 2 +- docs/proposals/printedparts-plugin.md | 2 +- 26 files changed, 145 insertions(+), 84 deletions(-) diff --git a/docs/BACKUP-RESTORE.md b/docs/BACKUP-RESTORE.md index 5106ab5..45f171f 100644 --- a/docs/BACKUP-RESTORE.md +++ b/docs/BACKUP-RESTORE.md @@ -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 diff --git a/docs/COLLECTOR-INTEGRATION.md b/docs/COLLECTOR-INTEGRATION.md index 81f406e..a11a76b 100644 --- a/docs/COLLECTOR-INTEGRATION.md +++ b/docs/COLLECTOR-INTEGRATION.md @@ -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_` 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_` 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) diff --git a/docs/CONTRACT-STABILITY.md b/docs/CONTRACT-STABILITY.md index 5a84bd0..7a1f76b 100644 --- a/docs/CONTRACT-STABILITY.md +++ b/docs/CONTRACT-STABILITY.md @@ -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. diff --git a/docs/DEPLOY-WINDOWS-IIS.md b/docs/DEPLOY-WINDOWS-IIS.md index eac85d7..5e47b9e 100644 --- a/docs/DEPLOY-WINDOWS-IIS.md +++ b/docs/DEPLOY-WINDOWS-IIS.md @@ -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) diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index 6d803c0..07decaf 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -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 diff --git a/docs/DEVELOPMENT-SETUP.md b/docs/DEVELOPMENT-SETUP.md index 5131363..ec14a19 100644 --- a/docs/DEVELOPMENT-SETUP.md +++ b/docs/DEVELOPMENT-SETUP.md @@ -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` | diff --git a/docs/GE-ENFORCE-CLIENT.md b/docs/GE-ENFORCE-CLIENT.md index ada974c..b729d39 100644 --- a/docs/GE-ENFORCE-CLIENT.md +++ b/docs/GE-ENFORCE-CLIENT.md @@ -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. diff --git a/docs/GE-ENFORCE.md b/docs/GE-ENFORCE.md index f7abe0d..a765c90 100644 --- a/docs/GE-ENFORCE.md +++ b/docs/GE-ENFORCE.md @@ -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 diff --git a/docs/INSTALL-WINDOWS-IIS.md b/docs/INSTALL-WINDOWS-IIS.md index a17de55..685a054 100644 --- a/docs/INSTALL-WINDOWS-IIS.md +++ b/docs/INSTALL-WINDOWS-IIS.md @@ -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. | diff --git a/docs/PILOT-DEPLOY.md b/docs/PILOT-DEPLOY.md index 31a85dc..e6ce4af 100644 --- a/docs/PILOT-DEPLOY.md +++ b/docs/PILOT-DEPLOY.md @@ -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`. diff --git a/docs/PLUGIN-EXTERNAL-REPO.md b/docs/PLUGIN-EXTERNAL-REPO.md index 99fd565..b1610f1 100644 --- a/docs/PLUGIN-EXTERNAL-REPO.md +++ b/docs/PLUGIN-EXTERNAL-REPO.md @@ -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": [] } ``` diff --git a/docs/PLUGIN-GUIDE.md b/docs/PLUGIN-GUIDE.md index 3a69003..f75be4c 100644 --- a/docs/PLUGIN-GUIDE.md +++ b/docs/PLUGIN-GUIDE.md @@ -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//frontend/` (ADR-010's frontend plugin hook contract). At build +time those files are staged into `frontend/src/.plugins-staged//` 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 `` and the f drops in ``, 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. diff --git a/docs/PLUGIN-HOOKS.md b/docs/PLUGIN-HOOKS.md index 23fefe5..97db3a4 100644 --- a/docs/PLUGIN-HOOKS.md +++ b/docs/PLUGIN-HOOKS.md @@ -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 diff --git a/docs/PLUGIN-LAB-PRINTEDPARTS.md b/docs/PLUGIN-LAB-PRINTEDPARTS.md index 4a01132..537cdbe 100644 --- a/docs/PLUGIN-LAB-PRINTEDPARTS.md +++ b/docs/PLUGIN-LAB-PRINTEDPARTS.md @@ -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` | diff --git a/docs/PLUGIN-QUICKSTART.md b/docs/PLUGIN-QUICKSTART.md index 3b1e800..5f16da8 100644 --- a/docs/PLUGIN-QUICKSTART.md +++ b/docs/PLUGIN-QUICKSTART.md @@ -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 diff --git a/docs/PLUGINS.md b/docs/PLUGINS.md index 5baba73..48d07a7 100644 --- a/docs/PLUGINS.md +++ b/docs/PLUGINS.md @@ -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 diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 6e83d00..77f3c36 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -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 diff --git a/docs/UPGRADE.md b/docs/UPGRADE.md index 615ad94..f27dfc0 100644 --- a/docs/UPGRADE.md +++ b/docs/UPGRADE.md @@ -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 diff --git a/docs/adr/ADR-001-asset-as-platform-contract.md b/docs/adr/ADR-001-asset-as-platform-contract.md index bb47159..7aff43c 100644 --- a/docs/adr/ADR-001-asset-as-platform-contract.md +++ b/docs/adr/ADR-001-asset-as-platform-contract.md @@ -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) diff --git a/docs/adr/ADR-004-deployment-topology.md b/docs/adr/ADR-004-deployment-topology.md index 666e630..fb62270 100644 --- a/docs/adr/ADR-004-deployment-topology.md +++ b/docs/adr/ADR-004-deployment-topology.md @@ -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_` 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 diff --git a/docs/adr/ADR-005-equipment-vs-measuringtools.md b/docs/adr/ADR-005-equipment-vs-measuringtools.md index 44aec8f..1b672c5 100644 --- a/docs/adr/ADR-005-equipment-vs-measuringtools.md +++ b/docs/adr/ADR-005-equipment-vs-measuringtools.md @@ -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) diff --git a/docs/adr/ADR-009-frontend-plugin-gating.md b/docs/adr/ADR-009-frontend-plugin-gating.md index 2c0d6c2..2c6bcd9 100644 --- a/docs/adr/ADR-009-frontend-plugin-gating.md +++ b/docs/adr/ADR-009-frontend-plugin-gating.md @@ -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/` toggle is admin-gated). 2. **Route tagging.** Every plugin-owned route carries `meta.plugin = ''`. This covers the per-plugin route modules diff --git a/docs/adr/ADR-010-frontend-plugin-hooks.md b/docs/adr/ADR-010-frontend-plugin-hooks.md index 70fd542..d5c4dde 100644 --- a/docs/adr/ADR-010-frontend-plugin-hooks.md +++ b/docs/adr/ADR-010-frontend-plugin-hooks.md @@ -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 diff --git a/docs/adr/ADR-013-plugin-catalog-and-lean-builds.md b/docs/adr/ADR-013-plugin-catalog-and-lean-builds.md index 06b007d..87d7e6f 100644 --- a/docs/adr/ADR-013-plugin-catalog-and-lean-builds.md +++ b/docs/adr/ADR-013-plugin-catalog-and-lean-builds.md @@ -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; diff --git a/docs/proposals/ge-enforce-plugin.md b/docs/proposals/ge-enforce-plugin.md index f464fd0..e3094ff 100644 --- a/docs/proposals/ge-enforce-plugin.md +++ b/docs/proposals/ge-enforce-plugin.md @@ -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 diff --git a/docs/proposals/printedparts-plugin.md b/docs/proposals/printedparts-plugin.md index bdcc798..d2b5e33 100644 --- a/docs/proposals/printedparts-plugin.md +++ b/docs/proposals/printedparts-plugin.md @@ -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)