diff --git a/docs/API-REFERENCE.md b/docs/API-REFERENCE.md new file mode 100644 index 0000000..97cae40 --- /dev/null +++ b/docs/API-REFERENCE.md @@ -0,0 +1,3311 @@ +# ShopDB Flask - API Reference + +Auto-inventoried across core + plugins (362 endpoints). Auth legend: none = public, jwt = login required, jwt-optional = works either way, admin = admin role, permission:x = named RBAC permission, api-key = X-API-Key managed token. + +## core-identity + +### POST /api/auth/login +**Auth:** none +**Params:** body: `username`, `password` (both required) +**Purpose:** Authenticate and issue JWT access+refresh tokens; per-IP fixed-window rate limit (429) and 5-strike/15-min account lockout. + +```bash +curl -X POST http://localhost:5001/api/auth/login -H 'Content-Type: application/json' -d '{"username":"admin","password":"secret123"}' +``` + +### POST /api/auth/refresh +**Auth:** JWT (refresh token) +**Params:** none; refresh token in Authorization header +**Purpose:** Exchange a refresh token for a new access token (rejects inactive/deleted users). + +```bash +curl -X POST http://localhost:5001/api/auth/refresh -H "Authorization: Bearer $REFRESH_TOKEN" +``` + +### GET /api/auth/me +**Auth:** JWT +**Params:** none +**Purpose:** Return the authenticated user's profile, roles, permissions, `mustchangepassword` flag. + +```bash +curl http://localhost:5001/api/auth/me -H "Authorization: Bearer $TOK" +``` + +### POST /api/auth/change-password +**Auth:** JWT +**Params:** body: `new_password` (min 8, required), `current_password` (required unless `mustchangepassword` is set) +**Purpose:** Self-service password change; forced first-login change skips `current_password`; clears lockout state. + +```bash +curl -X POST http://localhost:5001/api/auth/change-password -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"current_password":"old","new_password":"newpass123"}' +``` + +### POST /api/auth/logout +**Auth:** JWT +**Params:** none +**Purpose:** Logout stub for frontend token cleanup (no server-side blacklist yet). + +```bash +curl -X POST http://localhost:5001/api/auth/logout -H "Authorization: Bearer $TOK" +``` + +### GET /api/users +**Auth:** JWT + role `admin` +**Params:** none +**Purpose:** List all users ordered by username. + +```bash +curl http://localhost:5001/api/users -H "Authorization: Bearer $TOK" +``` + +### GET /api/users/<userid> +**Auth:** JWT (admin or self, checked inline) +**Params:** path: `userid` (int) +**Purpose:** Get one user; non-admins may only fetch their own record (403 otherwise). + +```bash +curl http://localhost:5001/api/users/7 -H "Authorization: Bearer $TOK" +``` + +### POST /api/users +**Auth:** JWT + role `admin` +**Params:** body: `username`, `email`, `password` (required); `firstname`, `lastname`, `isactive`, `roles` [roleids], `mustchangepassword` (default true), `sendwelcome` (default true) +**Purpose:** Create a user, assign roles, audit-log, and best-effort send a welcome email with the temp password. + +```bash +curl -X POST http://localhost:5001/api/users -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"username":"jdoe","email":"jdoe@example.com","password":"Temp1234","roles":[2]}' +``` + +### PUT /api/users/<userid> +**Auth:** JWT (admin or self; `isactive`/`roles`/`unlock` fields admin-only) +**Params:** path: `userid`; body: `email`, `firstname`, `lastname`, `password`; admin-only: `isactive`, `roles` [roleids], `unlock` (bool) +**Purpose:** Update a user; email uniqueness enforced; admins can also change active state, roles, and unlock the account; changes audit-logged. + +```bash +curl -X PUT http://localhost:5001/api/users/7 -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"firstname":"Jane","unlock":true}' +``` + +### DELETE /api/users/<userid> +**Auth:** JWT + role `admin` +**Params:** path: `userid` (cannot be your own account) +**Purpose:** Delete a user; revokes their API tokens and detaches (nulls `userid` on) their audit-log rows first. + +```bash +curl -X DELETE http://localhost:5001/api/users/7 -H "Authorization: Bearer $TOK" +``` + +### GET /api/users/permissions +**Auth:** JWT +**Params:** none +**Purpose:** List assignable permissions (core + enabled plugins) both flat and grouped by category, for the role grid. + +```bash +curl http://localhost:5001/api/users/permissions -H "Authorization: Bearer $TOK" +``` + +### GET /api/users/roles +**Auth:** JWT +**Params:** none +**Purpose:** List all roles with description, color, user count, permission names, and `isadmin` flag. + +```bash +curl http://localhost:5001/api/users/roles -H "Authorization: Bearer $TOK" +``` + +### POST /api/users/roles +**Auth:** JWT + role `admin` +**Params:** body: `rolename` (required), `description`, `color`, `permissions` [names] +**Purpose:** Create a role and assign permissions by name; 409 if `rolename` exists. + +```bash +curl -X POST http://localhost:5001/api/users/roles -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"rolename":"viewer","permissions":["assets.view"]}' +``` + +### PUT /api/users/roles/<roleid> +**Auth:** JWT + role `admin` +**Params:** path: `roleid`; body: `description`, `color`, `permissions` [names] (permissions immutable on the admin role) +**Purpose:** Update a role's description/color/permissions; admin role's permission set cannot be modified; audit-logged. + +```bash +curl -X PUT http://localhost:5001/api/users/roles/3 -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"color":"#0057b8","permissions":["assets.view","assets.edit"]}' +``` + +### DELETE /api/users/roles/<roleid> +**Auth:** JWT + role `admin` +**Params:** path: `roleid` +**Purpose:** Delete a role; refuses for the admin role or any role still assigned to users. + +```bash +curl -X DELETE http://localhost:5001/api/users/roles/3 -H "Authorization: Bearer $TOK" +``` + +### GET /api/apitokens +**Auth:** JWT +**Params:** query: `all=true` (admin only, lists everyone's tokens with owner info) +**Purpose:** List the caller's own API tokens (never returns hashes or secrets); admins may list all. + +```bash +curl 'http://localhost:5001/api/apitokens?all=true' -H "Authorization: Bearer $TOK" +``` + +### POST /api/apitokens +**Auth:** JWT + permission `apitokens.create` +**Params:** body: `name` (required), `expiresat` (date/datetime), `scopes` [permission names, ceiling = owner's permissions], `resourcescopes` [resource names, plugin-defined] +**Purpose:** Create a personal API token for the caller; the full secret is returned once in this response and never again. + +```bash +curl -X POST http://localhost:5001/api/apitokens -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"name":"import-script","scopes":["assets.edit"],"expiresat":"2026-12-31"}' +``` + +### PUT /api/apitokens/<tokenid> +**Auth:** JWT + permission `apitokens.create` (own token, or any if admin) +**Params:** path: `tokenid`; body: `name`, `isactive` (bool), `scopes` [names, validated against the token owner's permissions], `resourcescopes` [names] +**Purpose:** Rename, rescope, or (de)activate a token; scope ceiling is always the owner, even when an admin edits. + +```bash +curl -X PUT http://localhost:5001/api/apitokens/4 -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"isactive":false}' +``` + +### DELETE /api/apitokens/<tokenid> +**Auth:** JWT + permission `apitokens.create` (own token, or any if admin) +**Params:** path: `tokenid` +**Purpose:** Revoke (deactivate, not delete) a token; audit-logged. + +```bash +curl -X DELETE http://localhost:5001/api/apitokens/4 -H "Authorization: Bearer $TOK" +``` + +### GET /api/setup/needs-admin +**Auth:** none +**Params:** none +**Purpose:** Return `{needsadmin: true}` when zero users exist, so the login screen can offer first-run setup. + +```bash +curl http://localhost:5001/api/setup/needs-admin +``` + +### POST /api/setup/create-admin +**Auth:** none (only functions while zero users exist; 403 afterwards) +**Params:** body: `username`, `email`, `password` (all required) +**Purpose:** Bootstrap the very first admin account (creates the admin role if missing). + +```bash +curl -X POST http://localhost:5001/api/setup/create-admin -H 'Content-Type: application/json' -d '{"username":"admin","email":"admin@example.com","password":"ChangeMe123"}' +``` + +### POST /api/setup/seed-reference +**Auth:** JWT + role `admin` +**Params:** none +**Purpose:** Idempotently seed core reference data, permissions, and default settings (runs the flask seed CLI routines). + +```bash +curl -X POST http://localhost:5001/api/setup/seed-reference -H "Authorization: Bearer $TOK" +``` + +### POST /api/setup/seed-starter +**Auth:** JWT + role `admin` +**Params:** none +**Purpose:** Idempotently add a starter list of common hardware vendors (Dell, HP, Lenovo, ...). + +```bash +curl -X POST http://localhost:5001/api/setup/seed-starter -H "Authorization: Bearer $TOK" +``` + +### POST /api/setup/complete +**Auth:** JWT + role `admin` +**Params:** none +**Purpose:** Set the `setup_complete` setting to true, marking the first-run wizard finished. + +```bash +curl -X POST http://localhost:5001/api/setup/complete -H "Authorization: Bearer $TOK" +``` + +### GET /api/settings +**Auth:** JWT optional +**Params:** query: `category` (filter) +**Purpose:** List settings; unauthenticated callers see only the public allowlist (branding+map categories plus named bootstrap keys); secrets always masked as `********`. + +```bash +curl 'http://localhost:5001/api/settings?category=branding' +``` + +### POST /api/settings +**Auth:** JWT + permission `settings.edit` +**Params:** body: `key` (required), `value`, `valuetype` (default string), `category` (default general), `description` +**Purpose:** Create a new setting; 409 if the key exists. + +```bash +curl -X POST http://localhost:5001/api/settings -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"key":"facility_name","value":"West Jefferson","category":"site"}' +``` + +### GET /api/settings/<key> +**Auth:** JWT optional +**Params:** path: `key` +**Purpose:** Get one setting; non-public keys return 404 (not 403) to unauthenticated callers; secret values masked. + +```bash +curl http://localhost:5001/api/settings/facility_name +``` + +### PUT /api/settings/<key> +**Auth:** JWT + permission `settings.edit` +**Params:** path: `key`; body: `value` (required; bool coerced to 'true'/'false'; secret mask `********` means leave unchanged) +**Purpose:** Update a setting (upserts a plugin-scoped string row if the key is new); audit-logged with secrets masked; invalidates the 5-min settings cache. + +```bash +curl -X PUT http://localhost:5001/api/settings/facility_name -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"value":"West Jefferson"}' +``` + +### POST /api/settings/seed +**Auth:** JWT + permission `settings.edit` +**Params:** none +**Purpose:** Idempotently create any missing default settings (identifier toggles, search toggles, map, SMTP, SAML, etc.). + +```bash +curl -X POST http://localhost:5001/api/settings/seed -H "Authorization: Bearer $TOK" +``` + +### POST /api/settings/test-email +**Auth:** JWT + permission `settings.edit` +**Params:** body: `to` (optional; falls back to `alert_recipients` setting) +**Purpose:** Send a test email to verify SMTP config; always 200 with a `sent` flag, SMTP errors returned with credentials scrubbed. + +```bash +curl -X POST http://localhost:5001/api/settings/test-email -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"to":"me@example.com"}' +``` + +### POST /api/settings/map-blueprint +**Auth:** JWT + role `admin` +**Params:** multipart/form-data: `file` (png/jpg/jpeg/gif/webp/svg), `theme=light|dark` +**Purpose:** Upload a floor-map blueprint image to the instance maps dir and point `map_blueprint_` at its served URL. + +```bash +curl -X POST http://localhost:5001/api/settings/map-blueprint -H "Authorization: Bearer $TOK" -F 'file=@floor.png' -F 'theme=light' +``` + +### GET /api/settings/map-blueprint/<path:filename> +**Auth:** none +**Params:** path: `filename` +**Purpose:** Serve an uploaded floor-map blueprint image (public so kiosk dashboards can load it). + +```bash +curl -O http://localhost:5001/api/settings/map-blueprint/blueprint-light.png +``` + +### POST /api/settings/branding-logo +**Auth:** JWT + role `admin` +**Params:** multipart/form-data: `file` (map image types plus .ico), `kind=site|qr|badge|favicon` +**Purpose:** Upload a branding logo/favicon to the instance branding dir and set the matching branding setting (`site_logo`/`qr_logo`/`badge_logo`/`site_favicon`). + +```bash +curl -X POST http://localhost:5001/api/settings/branding-logo -H "Authorization: Bearer $TOK" -F 'file=@logo.svg' -F 'kind=site' +``` + +### GET /api/settings/branding/<path:filename> +**Auth:** none +**Params:** path: `filename` +**Purpose:** Serve an uploaded branding logo (public - kiosks and print pages read it). + +```bash +curl -O http://localhost:5001/api/settings/branding/logo-site.svg +``` + +## core-catalog + +### GET /api/assets/types +**Auth:** JWT optional +**Params:** `page`, `per_page`, `active=false` to include inactive +**Purpose:** List asset types (paginated, active-only by default). + +```bash +curl http://localhost:5001/api/assets/types?active=false +``` + +### GET /api/assets/types/<type_id> +**Auth:** JWT optional +**Params:** path: `type_id` +**Purpose:** Get one asset type. + +```bash +curl http://localhost:5001/api/assets/types/1 +``` + +### POST /api/assets/types +**Auth:** permission `assets.create` +**Params:** body: `assettype` (req), `pluginname`, `tablename`, `description`, `icon`, `color` +**Purpose:** Create asset type (409 on duplicate name). + +```bash +curl -X POST http://localhost:5001/api/assets/types -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"assettype":"Robot","icon":"mdi-robot"}' +``` + +### PUT /api/assets/types/<type_id> +**Auth:** permission `assets.edit` +**Params:** body: `description`, `icon`, `color`, `isactive` +**Purpose:** Update asset type display fields only (name/plugin/table are structural, not editable). + +```bash +curl -X PUT http://localhost:5001/api/assets/types/1 -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"color":"#ff0000"}' +``` + +### GET /api/assets/statuses +**Auth:** JWT optional +**Params:** `page`, `per_page`, `active=false` +**Purpose:** List asset statuses (paginated, active-only by default). + +```bash +curl http://localhost:5001/api/assets/statuses +``` + +### GET /api/assets/statuses/<status_id> +**Auth:** JWT optional +**Params:** path: `status_id` +**Purpose:** Get one asset status. + +```bash +curl http://localhost:5001/api/assets/statuses/1 +``` + +### POST /api/assets/statuses +**Auth:** permission `assets.create` +**Params:** body: `status` (req), `description`, `color` +**Purpose:** Create asset status (409 on duplicate). + +```bash +curl -X POST http://localhost:5001/api/assets/statuses -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"status":"In Repair","color":"#f90"}' +``` + +### PUT /api/assets/statuses/<status_id> +**Auth:** permission `assets.edit` +**Params:** body: `status`, `description`, `color`, `isactive` +**Purpose:** Update asset status (rename conflict-checked). + +```bash +curl -X PUT http://localhost:5001/api/assets/statuses/2 -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"isactive":false}' +``` + +### DELETE /api/assets/statuses/<status_id> +**Auth:** permission `assets.delete` +**Params:** path: `status_id` +**Purpose:** Hard-delete asset status; 409 if any asset still uses it. + +```bash +curl -X DELETE http://localhost:5001/api/assets/statuses/9 -H "Authorization: Bearer $TOK" +``` + +### GET /api/assets/relationshiptypes +**Auth:** JWT optional +**Params:** none +**Purpose:** List relationship types incl. read-only `propagatesthrough` rails. + +```bash +curl http://localhost:5001/api/assets/relationshiptypes +``` + +### POST /api/assets/relationshiptypes +**Auth:** permission `assets.create` +**Params:** body: `relationshiptype` (req), `description`, `color`, `isdirectional` (default true) +**Purpose:** Create relationship type (409 on duplicate). + +```bash +curl -X POST http://localhost:5001/api/assets/relationshiptypes -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"relationshiptype":"controls","isdirectional":true}' +``` + +### PUT /api/assets/relationshiptypes/<type_id> +**Auth:** permission `assets.edit` +**Params:** body: `relationshiptype`, `description`, `color`, `isdirectional` +**Purpose:** Update relationship type (rename conflict-checked). + +```bash +curl -X PUT http://localhost:5001/api/assets/relationshiptypes/3 -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"color":"#00f"}' +``` + +### DELETE /api/assets/relationshiptypes/<type_id> +**Auth:** permission `assets.delete` +**Params:** path: `type_id` +**Purpose:** Hard-delete relationship type; 409 while relationships use it. + +```bash +curl -X DELETE http://localhost:5001/api/assets/relationshiptypes/3 -H "Authorization: Bearer $TOK" +``` + +### GET /api/assets +**Auth:** JWT optional +**Params:** `page`, `per_page`, `active`, `search` (assetnumber/name/serialnumber ilike), `type` (name), `typeid|type_id`, `statusid|status_id`, `locationid|location_id`, `businessunitid|businessunit_id`, `sort` (assetnumber|name|createddate|modifieddate), `dir` (asc|desc), `include_type_data` +**Purpose:** List assets with filtering, search, sorting, pagination. + +```bash +curl 'http://localhost:5001/api/assets?type=machine&search=205&include_type_data=true' +``` + +### GET /api/assets/<asset_id> +**Auth:** JWT optional +**Params:** `include_type_data` (default true) +**Purpose:** Get one asset with full details. + +```bash +curl http://localhost:5001/api/assets/42 +``` + +### POST /api/assets +**Auth:** permission `assets.create` +**Params:** body: `assetnumber` (req), `assettypeid` (req), `name`, `serialnumber`, `statusid` (default 1), `locationid`, `businessunitid`, `mapx`, `mapy`, `notes` +**Purpose:** Create asset (duplicate assetnumber 409, assettypeid validated); honors `X-Import-Mode` timestamps. + +```bash +curl -X POST http://localhost:5001/api/assets -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"assetnumber":"0205","assettypeid":1,"name":"Grinder 5"}' +``` + +### PUT /api/assets/<asset_id> +**Auth:** permission `assets.edit` +**Params:** body: `assetnumber`, `name`, `serialnumber`, `assettypeid`, `statusid`, `locationid`, `businessunitid`, `mapx`, `mapy`, `notes`, `isactive` +**Purpose:** Update asset (allowed fields incl. isactive); honors `X-Import-Mode`. + +```bash +curl -X PUT http://localhost:5001/api/assets/42 -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"locationid":3}' +``` + +### DELETE /api/assets/<asset_id> +**Auth:** permission `assets.delete` +**Params:** path: `asset_id` +**Purpose:** Soft-delete asset (`isactive=false`). + +```bash +curl -X DELETE http://localhost:5001/api/assets/42 -H "Authorization: Bearer $TOK" +``` + +### GET /api/assets/lookup/<assetnumber> +**Auth:** JWT optional +**Params:** path: `assetnumber` (string) +**Purpose:** Look up an active asset by asset number (returns full type data). + +```bash +curl http://localhost:5001/api/assets/lookup/0205 +``` + +### GET /api/assets/<asset_id>/relationships +**Auth:** JWT optional +**Params:** path: `asset_id` +**Purpose:** Get outgoing + incoming active relationships for an asset with partner asset dicts. + +```bash +curl http://localhost:5001/api/assets/42/relationships +``` + +### POST /api/assets/relationships +**Auth:** permission `assets.create` +**Params:** body: `sourceassetid` (req), `targetassetid` (req), `relationshiptypeid` (req), `notes`; `X-Import-Mode` honored +**Purpose:** Create relationship, then fan out across symmetric propagation rails (Dualpath); response carries `propagated` + `propagatedcount`. + +```bash +curl -X POST http://localhost:5001/api/assets/relationships -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"sourceassetid":10,"targetassetid":42,"relationshiptypeid":1}' +``` + +### DELETE /api/assets/relationships/<rel_id> +**Auth:** permission `assets.delete` +**Params:** path: `rel_id` +**Purpose:** Soft-delete one relationship row (no cascade to propagated partner rows). + +```bash +curl -X DELETE http://localhost:5001/api/assets/relationships/7 -H "Authorization: Bearer $TOK" +``` + +### GET /api/assets/map +**Auth:** JWT optional +**Params:** `assettype` (name), `subtype` (id, per-type), `businessunitid`, `statusid`, `locationid`, `search` +**Purpose:** Unified floor-map payload: all mapped assets (with type data, primary IP, dualpath collapse) plus filter option lists. + +```bash +curl 'http://localhost:5001/api/assets/map?assettype=machine&statusid=1' +``` + +### GET /api/assets/<asset_id>/communications +**Auth:** JWT optional +**Params:** path: `asset_id` +**Purpose:** List active communications (IPs etc.) for an asset with `comtype_name`. + +```bash +curl http://localhost:5001/api/assets/42/communications +``` + +### GET /api/locations/types +**Auth:** JWT optional +**Params:** `active=false` includes inactive +**Purpose:** List location types. + +```bash +curl http://localhost:5001/api/locations/types +``` + +### POST /api/locations/types +**Auth:** admin +**Params:** body: `locationtype` (req), `description`, `color` +**Purpose:** Create location type; reactivates a soft-deleted same-name type instead of 409. + +```bash +curl -X POST http://localhost:5001/api/locations/types -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"locationtype":"Operation"}' +``` + +### PUT /api/locations/types/<type_id> +**Auth:** admin +**Params:** body: `locationtype`, `description`, `color`, `isactive` +**Purpose:** Update location type (rename conflict-checked). + +```bash +curl -X PUT http://localhost:5001/api/locations/types/2 -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"color":"#0a0"}' +``` + +### DELETE /api/locations/types/<type_id> +**Auth:** admin +**Params:** path: `type_id` +**Purpose:** Hard-delete location type; 409 while locations use it. + +```bash +curl -X DELETE http://localhost:5001/api/locations/types/2 -H "Authorization: Bearer $TOK" +``` + +### GET /api/locations +**Auth:** JWT optional +**Params:** `page`, `per_page`, `active`, `locationname` (exact), `search` (name/building ilike) +**Purpose:** List locations (paginated); exact locationname lookup for idempotent import. + +```bash +curl 'http://localhost:5001/api/locations?locationname=Building%201' +``` + +### GET /api/locations/<location_id> +**Auth:** JWT optional +**Params:** path: `location_id` +**Purpose:** Get one location. + +```bash +curl http://localhost:5001/api/locations/3 +``` + +### POST /api/locations +**Auth:** admin +**Params:** body: `locationname` (req), `building`, `floor`, `room`, `description`, `locationtypeid`, `parentlocationid`, `mapimage`, `mapwidth`, `mapheight` +**Purpose:** Create location (409 on duplicate name); honors `X-Import-Mode`. + +```bash +curl -X POST http://localhost:5001/api/locations -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"locationname":"Cell 12","building":"B1"}' +``` + +### PUT /api/locations/<location_id> +**Auth:** admin +**Params:** body: `locationname`, `building`, `floor`, `room`, `description`, `locationtypeid`, `parentlocationid`, `mapimage`, `mapwidth`, `mapheight`, `isactive` +**Purpose:** Update location (rename conflict-checked); honors `X-Import-Mode`. + +```bash +curl -X PUT http://localhost:5001/api/locations/3 -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"room":"104"}' +``` + +### DELETE /api/locations/<location_id> +**Auth:** admin +**Params:** path: `location_id` +**Purpose:** Soft-delete location. + +```bash +curl -X DELETE http://localhost:5001/api/locations/3 -H "Authorization: Bearer $TOK" +``` + +### GET /api/vendors +**Auth:** JWT optional +**Params:** `page`, `per_page`, `active`, `vendor` (exact), `search` (ilike) +**Purpose:** List vendors (paginated); exact vendor lookup for idempotent import. + +```bash +curl 'http://localhost:5001/api/vendors?search=fanuc' +``` + +### GET /api/vendors/<vendor_id> +**Auth:** JWT optional +**Params:** path: `vendor_id` +**Purpose:** Get one vendor. + +```bash +curl http://localhost:5001/api/vendors/5 +``` + +### POST /api/vendors +**Auth:** admin +**Params:** body: `vendor` (req), `description`, `website`, `supportphone`, `supportemail`, `notes` +**Purpose:** Create vendor (409 on duplicate); honors `X-Import-Mode`. + +```bash +curl -X POST http://localhost:5001/api/vendors -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"vendor":"Fanuc"}' +``` + +### PUT /api/vendors/<vendor_id> +**Auth:** admin +**Params:** body: `vendor`, `description`, `website`, `supportphone`, `supportemail`, `notes`, `isactive` +**Purpose:** Update vendor (rename conflict-checked); honors `X-Import-Mode`. + +```bash +curl -X PUT http://localhost:5001/api/vendors/5 -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"website":"https://fanuc.com"}' +``` + +### DELETE /api/vendors/<vendor_id> +**Auth:** admin +**Params:** path: `vendor_id` +**Purpose:** Soft-delete vendor. + +```bash +curl -X DELETE http://localhost:5001/api/vendors/5 -H "Authorization: Bearer $TOK" +``` + +### GET /api/models +**Auth:** JWT optional +**Params:** `page`, `per_page`, `active`, `vendor` (id), `modeltype` (id), `modelnumber` (exact), `search` (ilike) +**Purpose:** List vendor catalog models (paginated) with flattened vendor/modeltype names; exact modelnumber+vendor lookup for import. + +```bash +curl 'http://localhost:5001/api/models?vendor=5&search=30i' +``` + +### GET /api/models/<model_id> +**Auth:** JWT optional +**Params:** path: `model_id` +**Purpose:** Get one model with nested vendor + modeltype dicts. + +```bash +curl http://localhost:5001/api/models/12 +``` + +### POST /api/models +**Auth:** admin +**Params:** body: `modelnumber` (req), `vendorid`, `modeltypeid`, `description`, `imageurl`, `documentationurl`, `notes` +**Purpose:** Create model (409 on duplicate modelnumber+vendorid); honors `X-Import-Mode`. + +```bash +curl -X POST http://localhost:5001/api/models -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"modelnumber":"R-30iB","vendorid":5}' +``` + +### PUT /api/models/<model_id> +**Auth:** admin +**Params:** body: `modelnumber`, `vendorid`, `modeltypeid`, `description`, `imageurl`, `documentationurl`, `notes`, `isactive` +**Purpose:** Update model; honors `X-Import-Mode`. + +```bash +curl -X PUT http://localhost:5001/api/models/12 -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"notes":"EOL 2027"}' +``` + +### DELETE /api/models/<model_id> +**Auth:** admin +**Params:** path: `model_id` +**Purpose:** Soft-delete model. + +```bash +curl -X DELETE http://localhost:5001/api/models/12 -H "Authorization: Bearer $TOK" +``` + +### POST /api/models/<model_id>/image +**Auth:** admin +**Params:** multipart/form-data: `file=` (.png/.jpg/.jpeg/.gif/.webp/.svg) +**Purpose:** Upload/replace model photo (saved as `instance/modelimages/model-`, one per model); sets `imageurl`. + +```bash +curl -X POST http://localhost:5001/api/models/12/image -H "Authorization: Bearer $TOK" -F file=@robot.jpg +``` + +### GET /api/models/image/<filename> +**Auth:** none +**Params:** path: `filename` +**Purpose:** Serve an uploaded model image (deliberately public - asset detail pages read it without auth). + +```bash +curl http://localhost:5001/api/models/image/model-12.jpg +``` + +### DELETE /api/models/<model_id>/image +**Auth:** admin +**Params:** path: `model_id` +**Purpose:** Clear `imageurl` and delete the uploaded file only if it lives under `/api/models/image/` (external URLs untouched). + +```bash +curl -X DELETE http://localhost:5001/api/models/12/image -H "Authorization: Bearer $TOK" +``` + +### GET /api/modeltypes +**Auth:** JWT optional +**Params:** `page`, `per_page`, `active`, `category`, `modeltype` (exact), `search` (ilike) +**Purpose:** List model types (types the vendor MODELS catalog, not machines); exact modeltype lookup for import. + +```bash +curl 'http://localhost:5001/api/modeltypes?category=Equipment' +``` + +### GET /api/modeltypes/<type_id> +**Auth:** JWT optional +**Params:** path: `type_id` +**Purpose:** Get one model type. + +```bash +curl http://localhost:5001/api/modeltypes/2 +``` + +### POST /api/modeltypes +**Auth:** admin +**Params:** body: `modeltype` (req), `category`, `description`, `icon` +**Purpose:** Create model type (409 on duplicate); category defaults to Equipment; honors `X-Import-Mode`. + +```bash +curl -X POST http://localhost:5001/api/modeltypes -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"modeltype":"Controller"}' +``` + +### PUT /api/modeltypes/<type_id> +**Auth:** admin +**Params:** body: `modeltype`, `category`, `description`, `icon`, `isactive` +**Purpose:** Update model type (rename conflict-checked); honors `X-Import-Mode`. + +```bash +curl -X PUT http://localhost:5001/api/modeltypes/2 -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"icon":"mdi-chip"}' +``` + +### DELETE /api/modeltypes/<type_id> +**Auth:** admin +**Params:** path: `type_id` +**Purpose:** Soft-delete model type; 409 while models use it. + +```bash +curl -X DELETE http://localhost:5001/api/modeltypes/2 -H "Authorization: Bearer $TOK" +``` + +### GET /api/businessunits +**Auth:** JWT optional +**Params:** `page`, `per_page`, `active`, `businessunit` (exact), `search` (name/code ilike) +**Purpose:** List business units (paginated); exact businessunit lookup for import. + +```bash +curl http://localhost:5001/api/businessunits +``` + +### GET /api/businessunits/<bu_id> +**Auth:** JWT optional +**Params:** path: `bu_id` +**Purpose:** Get one business unit with parent + children. + +```bash +curl http://localhost:5001/api/businessunits/1 +``` + +### POST /api/businessunits +**Auth:** admin +**Params:** body: `businessunit` (req), `code`, `description`, `parentid` +**Purpose:** Create business unit (409 on duplicate); honors `X-Import-Mode`. + +```bash +curl -X POST http://localhost:5001/api/businessunits -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"businessunit":"Blades","code":"BLD"}' +``` + +### PUT /api/businessunits/<bu_id> +**Auth:** admin +**Params:** body: `businessunit`, `code`, `description`, `parentid`, `isactive` +**Purpose:** Update business unit (rename conflict-checked); honors `X-Import-Mode`. + +```bash +curl -X PUT http://localhost:5001/api/businessunits/1 -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"code":"BL"}' +``` + +### DELETE /api/businessunits/<bu_id> +**Auth:** admin +**Params:** path: `bu_id` +**Purpose:** Soft-delete business unit. + +```bash +curl -X DELETE http://localhost:5001/api/businessunits/1 -H "Authorization: Bearer $TOK" +``` + +### GET /api/operatingsystems +**Auth:** JWT optional +**Params:** `page`, `per_page`, `active`, `osname` (exact), `osversion` (exact), `search` (osname ilike) +**Purpose:** List operating systems (paginated); exact osname/osversion lookup for import. + +```bash +curl 'http://localhost:5001/api/operatingsystems?osname=Windows%2011' +``` + +### GET /api/operatingsystems/<os_id> +**Auth:** JWT optional +**Params:** path: `os_id` +**Purpose:** Get one operating system. + +```bash +curl http://localhost:5001/api/operatingsystems/4 +``` + +### POST /api/operatingsystems +**Auth:** admin +**Params:** body: `osname` (req), `osversion`, `architecture`, `endoflife` +**Purpose:** Create OS (409 on duplicate osname+osversion); honors `X-Import-Mode`. + +```bash +curl -X POST http://localhost:5001/api/operatingsystems -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"osname":"Windows 11","osversion":"24H2"}' +``` + +### PUT /api/operatingsystems/<os_id> +**Auth:** admin +**Params:** body: `osname`, `osversion`, `architecture`, `endoflife`, `isactive` +**Purpose:** Update OS; honors `X-Import-Mode`. + +```bash +curl -X PUT http://localhost:5001/api/operatingsystems/4 -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"endoflife":"2031-10-14"}' +``` + +### DELETE /api/operatingsystems/<os_id> +**Auth:** admin +**Params:** path: `os_id` +**Purpose:** Soft-delete OS. + +```bash +curl -X DELETE http://localhost:5001/api/operatingsystems/4 -H "Authorization: Bearer $TOK" +``` + +### GET /api/customfields +**Auth:** JWT optional +**Params:** `assettypeid` (int), `active=false` includes inactive +**Purpose:** List custom-field definitions, ordered by sortorder. + +```bash +curl 'http://localhost:5001/api/customfields?assettypeid=1' +``` + +### POST /api/customfields +**Auth:** admin +**Params:** body: `assettypeid` (req), `label` (req), `datatype` (one of CUSTOM_FIELD_DATATYPES, default text), `fieldkey`, `options` (list or newline/comma text), `showondetail`, `showonform`, `searchable`, `sortorder` +**Purpose:** Create field definition; fieldkey auto-slugged from label; reactivates a soft-deleted same-key field instead of 409. + +```bash +curl -X POST http://localhost:5001/api/customfields -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"assettypeid":1,"label":"Coolant Type","datatype":"select","options":["Oil","Water"]}' +``` + +### PUT /api/customfields/<fieldid> +**Auth:** admin +**Params:** body: `label`, `datatype`, `options`, `showondetail`, `showonform`, `isactive`, `searchable`, `sortorder` +**Purpose:** Update field definition (label/datatype/options/flags/sortorder). + +```bash +curl -X PUT http://localhost:5001/api/customfields/7 -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"searchable":true}' +``` + +### DELETE /api/customfields/<fieldid> +**Auth:** admin +**Params:** path: `fieldid` +**Purpose:** Hard-delete field definition AND all stored values for it. + +```bash +curl -X DELETE http://localhost:5001/api/customfields/7 -H "Authorization: Bearer $TOK" +``` + +### GET /api/customfields/asset/<assetid> +**Auth:** JWT optional +**Params:** path: `assetid` +**Purpose:** Active field defs for the asset's type merged with the asset's stored values. + +```bash +curl http://localhost:5001/api/customfields/asset/42 +``` + +### PUT /api/customfields/asset/<assetid> +**Auth:** admin +**Params:** body: `{values: {fieldid: value, ...}}` +**Purpose:** Upsert per-asset values; empty string clears a value; only fields of the asset's type accepted. + +```bash +curl -X PUT http://localhost:5001/api/customfields/asset/42 -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"values":{"7":"Oil"}}' +``` + +### GET /api/supportteams +**Auth:** JWT optional +**Params:** `active`, `teamname` (exact) +**Purpose:** List support teams with contacts; exact teamname lookup for import (not paginated). + +```bash +curl http://localhost:5001/api/supportteams +``` + +### GET /api/supportteams/<team_id> +**Auth:** JWT optional +**Params:** path: `team_id` +**Purpose:** Get one support team with contacts. + +```bash +curl http://localhost:5001/api/supportteams/2 +``` + +### POST /api/supportteams +**Auth:** admin +**Params:** body: `teamname` (req), `teamurl`, `webhookurl`, `isactive` +**Purpose:** Create support team (409 on duplicate); audit-logged; honors `X-Import-Mode`. + +```bash +curl -X POST http://localhost:5001/api/supportteams -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"teamname":"CNC Support"}' +``` + +### PUT /api/supportteams/<team_id> +**Auth:** admin +**Params:** body: `teamname`, `teamurl`, `webhookurl`, `isactive` +**Purpose:** Update support team (rename conflict-checked); audit-logged; honors `X-Import-Mode`. + +```bash +curl -X PUT http://localhost:5001/api/supportteams/2 -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"webhookurl":"https://hooks/x"}' +``` + +### DELETE /api/supportteams/<team_id> +**Auth:** admin +**Params:** path: `team_id` +**Purpose:** Hard-delete team (cascade removes contacts); 409 while applications reference it; audit-logged. + +```bash +curl -X DELETE http://localhost:5001/api/supportteams/2 -H "Authorization: Bearer $TOK" +``` + +### POST /api/supportteams/<team_id>/contacts +**Auth:** admin +**Params:** body: `name` (req), `sso`, `sortorder`, `isactive` +**Purpose:** Add contact to a team; audit-logged; honors `X-Import-Mode`. + +```bash +curl -X POST http://localhost:5001/api/supportteams/2/contacts -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"name":"Jane Doe","sso":"212345678"}' +``` + +### PUT /api/supportteams/<team_id>/contacts/<contact_id> +**Auth:** admin +**Params:** body: `name`, `sso`, `sortorder`, `isactive` +**Purpose:** Update a team contact; audit-logged; honors `X-Import-Mode`. + +```bash +curl -X PUT http://localhost:5001/api/supportteams/2/contacts/9 -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"sortorder":1}' +``` + +### DELETE /api/supportteams/<team_id>/contacts/<contact_id> +**Auth:** admin +**Params:** path: `team_id`, `contact_id` +**Purpose:** Hard-delete a team contact; audit-logged. + +```bash +curl -X DELETE http://localhost:5001/api/supportteams/2/contacts/9 -H "Authorization: Bearer $TOK" +``` + +### GET /api/applications +**Auth:** JWT optional +**Params:** `page`, `per_page`, `active`, `showhidden`, `installable` (true/false), `appname` (exact), `search` (name/description ilike) +**Purpose:** List applications (paginated) with installedcount; hidden apps excluded unless `showhidden=true`; exact appname lookup for import. + +```bash +curl 'http://localhost:5001/api/applications?installable=true' +``` + +### GET /api/applications/<app_id> +**Auth:** JWT optional +**Params:** path: `app_id` +**Purpose:** Get one application with active versions, installedcount, and linked KB articles. + +```bash +curl http://localhost:5001/api/applications/15 +``` + +### POST /api/applications +**Auth:** permission `applications.create` +**Params:** body: `appname` (req), `appdescription`, `supportteamid`, `isinstallable`, `applicationnotes`, `installpath`, `applicationlink`, `documentationpath`, `ishidden`, `isprinter`, `islicenced`, `isrequired`, `image` +**Purpose:** Create application (409 on duplicate name); audit-logged; honors `X-Import-Mode`. + +```bash +curl -X POST http://localhost:5001/api/applications -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"appname":"PC-DMIS","isinstallable":true}' +``` + +### PUT /api/applications/<app_id> +**Auth:** permission `applications.edit` +**Params:** body: any of `appname`, `appdescription`, `supportteamid`, `isinstallable`, `applicationnotes`, `installpath`, `applicationlink`, `documentationpath`, `ishidden`, `isprinter`, `islicenced`, `isrequired`, `image`, `isactive` +**Purpose:** Update application (rename conflict-checked); field-level change diff audit-logged; honors `X-Import-Mode`. + +```bash +curl -X PUT http://localhost:5001/api/applications/15 -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"supportteamid":2}' +``` + +### DELETE /api/applications/<app_id> +**Auth:** permission `applications.delete` +**Params:** path: `app_id` +**Purpose:** Soft-delete application; audit-logged. + +```bash +curl -X DELETE http://localhost:5001/api/applications/15 -H "Authorization: Bearer $TOK" +``` + +### GET /api/applications/<app_id>/versions +**Auth:** JWT optional +**Params:** path: `app_id` +**Purpose:** List active versions of an application (desc). + +```bash +curl http://localhost:5001/api/applications/15/versions +``` + +### POST /api/applications/<app_id>/versions +**Auth:** permission `applications.create` +**Params:** body: `version` (req), `releasedate`, `notes` +**Purpose:** Create app version (409 on duplicate version per app); honors `X-Import-Mode` for legacy dates. + +```bash +curl -X POST http://localhost:5001/api/applications/15/versions -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"version":"2024.2"}' +``` + +### GET /api/applications/<app_id>/installed +**Auth:** JWT optional +**Params:** path: `app_id` +**Purpose:** List computers with this app installed (503 if computers plugin absent). + +```bash +curl http://localhost:5001/api/applications/15/installed +``` + +### GET /api/applications/machines/<machine_id> +**Auth:** JWT optional +**Params:** path: `machine_id` +**Purpose:** List apps installed on a computer (machine_id is a computerid; 503 without computers plugin). + +```bash +curl http://localhost:5001/api/applications/machines/8 +``` + +### POST /api/applications/machines/<machine_id> +**Auth:** permission `applications.create` +**Params:** body: `appid` (req), `appversionid` +**Purpose:** Install an app on a computer; reactivates a prior soft-deleted install; 409 if already installed. + +```bash +curl -X POST http://localhost:5001/api/applications/machines/8 -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"appid":15}' +``` + +### DELETE /api/applications/machines/<machine_id>/<app_id> +**Auth:** permission `applications.delete` +**Params:** path: `machine_id`, `app_id` +**Purpose:** Uninstall (soft-delete install row) an app from a computer. + +```bash +curl -X DELETE http://localhost:5001/api/applications/machines/8/15 -H "Authorization: Bearer $TOK" +``` + +### PUT /api/applications/machines/<machine_id>/<app_id> +**Auth:** permission `applications.edit` +**Params:** body: `appversionid` +**Purpose:** Update an installed-app row (change appversionid). + +```bash +curl -X PUT http://localhost:5001/api/applications/machines/8/15 -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"appversionid":3}' +``` + +## core-platform + +### GET /api/reports +**Auth:** jwt-optional +**Params:** none +**Purpose:** List all available reports (6 core cards plus cards contributed by enabled plugins via the get_reports hook). + +```bash +curl http://localhost:5001/api/reports +``` + +### GET /api/reports/machines-by-type +**Auth:** jwt-optional +**Params:** `businessunitid` (int filter), `format=json|csv` +**Purpose:** Machine count grouped by machine type (dualpath secondary bays collapsed when the site setting is on); 404 if machines plugin absent. + +```bash +curl 'http://localhost:5001/api/reports/machines-by-type?businessunitid=2&format=csv' +``` + +### GET /api/reports/assets-by-status +**Auth:** jwt-optional +**Params:** `assettypeid`, `businessunitid`, `format=json|csv` +**Purpose:** Asset count grouped by status with status colors. + +```bash +curl 'http://localhost:5001/api/reports/assets-by-status?assettypeid=1' +``` + +### GET /api/reports/kb-popularity +**Auth:** jwt-optional +**Params:** `limit` (default 20, max 100), `format=json|csv` +**Purpose:** Most-clicked knowledge base articles; 503 if knowledgebase plugin absent. + +```bash +curl 'http://localhost:5001/api/reports/kb-popularity?limit=10' +``` + +### GET /api/reports/software-compliance +**Auth:** jwt-optional +**Params:** `appid` (filter to one app), `format=json|csv` +**Purpose:** Required applications vs installed per PC with compliance rate and up to 100 non-compliant PCs per app; 503 if computers plugin absent. + +```bash +curl 'http://localhost:5001/api/reports/software-compliance?appid=5' +``` + +### GET /api/reports/asset-inventory +**Auth:** jwt-optional +**Params:** `businessunitid`, `locationid`, `format=json|csv` +**Purpose:** Complete asset inventory summary broken down by type, status, and location. + +```bash +curl 'http://localhost:5001/api/reports/asset-inventory?locationid=3&format=csv' +``` + +### GET /api/reports/pc-relationships +**Auth:** jwt-optional +**Params:** `format=json|csv` +**Purpose:** PC-to-shop-floor-machine relationships (machine number, vendor, model, hostname, IP) matched in both edge directions via raw SQL UNION. + +```bash +curl 'http://localhost:5001/api/reports/pc-relationships?format=csv' +``` + +### POST /api/reports/email +**Auth:** jwt + permission:reports.export +**Params:** body: `subject`, `columns` `[{key,label}]`, `rows` `[{..}]`, `intro` (optional), `to` (optional email) +**Purpose:** Email a report's rows as an HTML table on demand; recipient defaults to the site Alert Recipients setting; intended cron target via a PAT scoped to reports.export. + +```bash +curl -X POST http://localhost:5001/api/reports/email -H 'Authorization: Bearer $PAT' -H 'Content-Type: application/json' -d '{"subject":"Warranty Report","columns":[{"key":"vendor","label":"Vendor"}],"rows":[{"vendor":"Haas"}],"to":"ops@example.com"}' +``` + +### GET /api/search +**Auth:** jwt-optional +**Params:** `q` (required, 2-200 chars) +**Purpose:** Global search across assets, applications, KB, employees, notifications, custom fields, hostnames, IPs/subnets, vendor/model/type; ServiceNOW ticket prefixes and smart redirects; results capped at 50, types filterable via `search__enabled` settings. + +```bash +curl 'http://localhost:5001/api/search?q=tsgwp00525' +``` + +### GET /api/dashboard +**Auth:** jwt-optional +**Params:** none +**Purpose:** Dashboard summary: asset counts by type (machines/PCs/network/printers/measuring tools, dualpath-collapsed), counts by status, 10 most recent assets. + +```bash +curl http://localhost:5001/api/dashboard +``` + +### GET /api/dashboard/summary +**Auth:** jwt-optional +**Params:** none +**Purpose:** Alias route for the same dashboard summary handler as GET /api/dashboard. + +```bash +curl http://localhost:5001/api/dashboard/summary +``` + +### GET /api/dashboard/stats +**Auth:** jwt-optional +**Params:** none +**Purpose:** Asset counts grouped by every asset type with display category labels. + +```bash +curl http://localhost:5001/api/dashboard/stats +``` + +### GET /api/dashboard/navigation +**Auth:** none +**Params:** none +**Purpose:** Sidebar navigation items: core entries merged with get_navigation_items from every enabled plugin, sorted by position. + +```bash +curl http://localhost:5001/api/dashboard/navigation +``` + +### GET /api/dashboard/widgets +**Auth:** jwt-optional +**Params:** none +**Purpose:** Dashboard widget definitions aggregated from enabled plugins (get_dashboard_widgets hook), sorted by position. + +```bash +curl http://localhost:5001/api/dashboard/widgets +``` + +### GET /api/dashboard/health +**Auth:** none +**Params:** none +**Purpose:** Health check: runs SELECT 1 against the DB, returns ok/degraded plus app version. + +```bash +curl http://localhost:5001/api/dashboard/health +``` + +### GET /api/dashboarddefaults/visitor-location +**Auth:** none +**Params:** `fqdn` (optional), `ipaddress` (optional, defaults to client IP) +**Purpose:** Resolve the business unit for a kiosk/lobby display by FQDN first then IP (caller IP used when ipaddress omitted); null businessunitid when unmapped. + +```bash +curl 'http://localhost:5001/api/dashboarddefaults/visitor-location?fqdn=display01.wjs.geaerospace.net' +``` + +### GET /api/dashboarddefaults/display-role +**Auth:** none +**Params:** `fqdn` (optional), `ipaddress` (optional, defaults to client IP) +**Purpose:** Resolve what a display PC should show at boot: role (dashboard/lobby/partskiosk), frontend path, and business unit; null role when unmapped. + +```bash +curl 'http://localhost:5001/api/dashboarddefaults/display-role?ipaddress=10.1.2.3' +``` + +### GET /api/dashboarddefaults +**Auth:** jwt-optional +**Params:** none +**Purpose:** List all active display-to-business-unit mappings. + +```bash +curl http://localhost:5001/api/dashboarddefaults +``` + +### POST /api/dashboarddefaults +**Auth:** jwt + role:admin +**Params:** body: `fqdn`, `ipaddress`, `displayrole` (dashboard|lobby|partskiosk, default dashboard), `businessunitid`, `description` +**Purpose:** Create a display mapping; requires fqdn or ipaddress, dashboard role requires businessunitid, 409 on duplicate fqdn/IP; audit-logged. + +```bash +curl -X POST http://localhost:5001/api/dashboarddefaults -H 'Authorization: Bearer $JWT' -H 'Content-Type: application/json' -d '{"ipaddress":"10.1.2.3","displayrole":"dashboard","businessunitid":2}' +``` + +### PUT /api/dashboarddefaults/<int:default_id> +**Auth:** jwt + role:admin +**Params:** body: any of `fqdn`, `ipaddress`, `displayrole`, `businessunitid`, `description` +**Purpose:** Update a display mapping; non-dashboard roles get businessunitid nulled, dashboard role must keep one. + +```bash +curl -X PUT http://localhost:5001/api/dashboarddefaults/7 -H 'Authorization: Bearer $JWT' -H 'Content-Type: application/json' -d '{"displayrole":"lobby"}' +``` + +### DELETE /api/dashboarddefaults/<int:default_id> +**Auth:** jwt + role:admin +**Params:** none +**Purpose:** Soft-delete (deactivate) a display mapping. + +```bash +curl -X DELETE http://localhost:5001/api/dashboarddefaults/7 -H 'Authorization: Bearer $JWT' +``` + +### POST /api/collector/<pluginname> +**Auth:** api-key (X-API-Key: per-plugin `COLLECTOR_API_KEY_` or shared `COLLECTOR_API_KEY`, or a collector.ingest-scoped managed PAT via Bearer/X-API-Key) +**Params:** body: JSON payload whose schema identityfield (e.g. hostname) is required; rest is plugin-defined +**Purpose:** Generic collector ingest (ADR-006): schema-validated identity field, idempotent upsert via the plugin's apply_collector_payload; audit-logged; 404 when no collector registered for the plugin. + +```bash +curl -X POST http://localhost:5001/api/collector/computers -H 'X-API-Key: $KEY' -H 'Content-Type: application/json' -d '{"hostname":"tsgwp00525","serialnumber":"ABC123"}' +``` + +### GET /api/collector/_schemas +**Auth:** jwt +**Params:** none +**Purpose:** List collector schemas for all enabled plugins that accept collector input. + +```bash +curl http://localhost:5001/api/collector/_schemas -H 'Authorization: Bearer $JWT' +``` + +### POST /api/collector/pc +**Auth:** api-key +**Params:** body: `hostname` (required), `lastboottime` (ISO), `currentuser`, `serialnumber` +**Purpose:** Legacy computers-specific ingest: update one PC matched by hostname (or asset number) - lastreporteddate, lastboottime, loggedinuser, serialnumber. + +```bash +curl -X POST http://localhost:5001/api/collector/pc -H 'X-API-Key: $KEY' -H 'Content-Type: application/json' -d '{"hostname":"tsgwp00525","currentuser":"212345678"}' +``` + +### POST /api/collector/apps +**Auth:** api-key +**Params:** body: `hostname` (required), `apps` `[{appname, version}]` (required) +**Purpose:** Update installed applications for one PC; only apps already in the Application table are tracked, others skipped; returns created/updated/skipped counts. + +```bash +curl -X POST http://localhost:5001/api/collector/apps -H 'X-API-Key: $KEY' -H 'Content-Type: application/json' -d '{"hostname":"tsgwp00525","apps":[{"appname":"PC-DMIS","version":"2023.2"}]}' +``` + +### POST /api/collector/heartbeat +**Auth:** api-key +**Params:** body: `hostname` (string) or `hostnames` (array) +**Purpose:** Record PC online heartbeat (single hostname or batch); stamps lastreporteddate, returns updated count and notfound list. + +```bash +curl -X POST http://localhost:5001/api/collector/heartbeat -H 'X-API-Key: $KEY' -H 'Content-Type: application/json' -d '{"hostnames":["pc1","pc2"]}' +``` + +### POST /api/collector/bulk +**Auth:** api-key +**Params:** body: `pcs` `[{hostname (required), currentuser, lastboottime}]` +**Purpose:** Bulk update many PCs in one call (lastreporteddate, currentuser, lastboottime per entry); returns updated/notfound/errors. + +```bash +curl -X POST http://localhost:5001/api/collector/bulk -H 'X-API-Key: $KEY' -H 'Content-Type: application/json' -d '{"pcs":[{"hostname":"pc1","currentuser":"212345678"}]}' +``` + +### GET /api/collector/status +**Auth:** api-key +**Params:** none +**Purpose:** Collector API liveness/credential check; returns timestamp and the collector endpoint list. + +```bash +curl http://localhost:5001/api/collector/status -H 'X-API-Key: $KEY' +``` + +### GET /api/auditlogs +**Auth:** jwt + permission:audit.view +**Params:** `page` (default 1), `perpage` (default 50, max 200), `action` (created|updated|deleted), `entitytype`, `userid` (int), `search` (entityname/username ilike), `from_date`, `to_date` (ISO) +**Purpose:** List audit logs with filtering and pagination, newest first; rows enriched with best-effort SSO-to-full-name resolution. + +```bash +curl 'http://localhost:5001/api/auditlogs?action=deleted&perpage=100' -H 'Authorization: Bearer $JWT' +``` + +### GET /api/auditlogs/entity/<entitytype>/<int:entityid> +**Auth:** jwt + permission:audit.view +**Params:** path only +**Purpose:** Full audit history for one entity, newest first. + +```bash +curl http://localhost:5001/api/auditlogs/entity/Asset/42 -H 'Authorization: Bearer $JWT' +``` + +### GET /api/auditlogs/stats +**Auth:** jwt + permission:audit.view +**Params:** none +**Purpose:** Audit statistics: counts by action and entity type, last-7-days activity count, top 5 most active users. + +```bash +curl http://localhost:5001/api/auditlogs/stats -H 'Authorization: Bearer $JWT' +``` + +### GET /api/plugins +**Auth:** jwt-optional +**Params:** none +**Purpose:** List all discovered plugins (enabled or not) with metadata plus the framework contract version. + +```bash +curl http://localhost:5001/api/plugins +``` + +### GET /api/plugins/enabled +**Auth:** none (jwt-optional decorator, no claims used) +**Params:** none +**Purpose:** Flat sorted array of enabled plugin names (registry read, no DB); deliberately anonymous so kiosk routes can gate plugin-owned frontend routes. + +```bash +curl http://localhost:5001/api/plugins/enabled +``` + +### PUT /api/plugins/<name> +**Auth:** jwt + role:admin +**Params:** body: `enabled` (bool, required) +**Purpose:** Enable or disable a plugin (route changes need an app restart); 409 when unknown or a dependency conflict refuses the change. + +```bash +curl -X PUT http://localhost:5001/api/plugins/warranty -H 'Authorization: Bearer $JWT' -H 'Content-Type: application/json' -d '{"enabled":false}' +``` + +### GET /api/pluginui/settings-cards +**Auth:** jwt-optional +**Params:** none +**Purpose:** Merge enabled plugins' settings-catalog cards (get_settings_cards hook) sorted by position for the settings rail/overview. + +```bash +curl http://localhost:5001/api/pluginui/settings-cards +``` + +### GET /api/pluginui/asset-panels +**Auth:** jwt-optional +**Params:** `assetid` (int, required); 400 without it, 404 if asset missing +**Purpose:** Asset-detail panels from enabled plugins (get_asset_panels hook) matching one asset's type ('*' wildcard supported), sorted by position. + +```bash +curl 'http://localhost:5001/api/pluginui/asset-panels?assetid=42' +``` + +### GET /api/pluginui/map-overlays +**Auth:** jwt-optional +**Params:** none +**Purpose:** Merge enabled plugins' map overlay declarations (get_map_overlays hook), sorted by position. + +```bash +curl http://localhost:5001/api/pluginui/map-overlays +``` + +### GET /api/pluginui/asset-presentation +**Auth:** jwt-optional +**Params:** none +**Purpose:** Merge enabled plugins' asset-type presentation entries (icon + detail route per type) used by search rows and cross-links. + +```bash +curl http://localhost:5001/api/pluginui/asset-presentation +``` + +## plugin-computers + +### GET /api/computers/types +**Auth:** jwt-optional +**Params:** `page`, `per_page`, `active` (default true; `'false'` includes inactive), `search` (ilike on computertype) +**Purpose:** List computer types, paginated, active-only by default. +```bash +curl -H 'Authorization: Bearer $TOKEN' 'http://localhost:5001/api/computers/types?search=kiosk&per_page=50' +``` + +### GET /api/computers/types/<int:type_id> +**Auth:** jwt-optional +**Params:** `type_id` in path +**Purpose:** Get a single computer type by ID. +```bash +curl 'http://localhost:5001/api/computers/types/3' +``` + +### POST /api/computers/types +**Auth:** permission `computers.create` (jwt_required) +**Params:** body: `computertype` (required), `description`, `icon`, `color`. Matching a deactivated type revives it instead of 409. +**Purpose:** Create (or reactivate) a computer type. +```bash +curl -X POST -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' -d '{"computertype":"Shopfloor","color":"#0066cc"}' 'http://localhost:5001/api/computers/types' +``` + +### PUT /api/computers/types/<int:type_id> +**Auth:** permission `computers.edit` (jwt_required) +**Params:** body: `computertype`, `description`, `icon`, `color`, `isactive`. 409 on duplicate name. +**Purpose:** Update a computer type. +```bash +curl -X PUT -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' -d '{"isactive":false}' 'http://localhost:5001/api/computers/types/3' +``` + +### DELETE /api/computers/types/<int:type_id> +**Auth:** permission `computers.delete` (jwt_required) +**Params:** `type_id` in path. 409 if any Computer still uses the type. +**Purpose:** Hard-delete a computer type when unused. +```bash +curl -X DELETE -H 'Authorization: Bearer $TOKEN' 'http://localhost:5001/api/computers/types/3' +``` + +### GET /api/computers/protocols +**Auth:** jwt-optional +**Params:** `active` (default true; `'false'` includes disabled) +**Purpose:** List remote-access protocols (VNC/WinRM/RDP catalog), unpaginated. +```bash +curl 'http://localhost:5001/api/computers/protocols?active=false' +``` + +### POST /api/computers/protocols +**Auth:** permission `computers.edit` (jwt_required) +**Params:** body: `name`, `scheme`, `linktemplate` (all required), `defaultport`, `isactive`. 409 on duplicate name. +**Purpose:** Create an access protocol. +```bash +curl -X POST -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' -d '{"name":"VNC","scheme":"vnc","defaultport":5900,"linktemplate":"vnc://{host}:{port}"}' 'http://localhost:5001/api/computers/protocols' +``` + +### PUT|PATCH /api/computers/protocols/<int:protocol_id> +**Auth:** permission `computers.edit` (jwt_required) +**Params:** body: `name`, `scheme`, `linktemplate`, `defaultport`, `isactive` (all optional) +**Purpose:** Update an access protocol. +```bash +curl -X PATCH -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' -d '{"defaultport":5901}' 'http://localhost:5001/api/computers/protocols/2' +``` + +### DELETE /api/computers/protocols/<int:protocol_id> +**Auth:** permission `computers.edit` (jwt_required) +**Params:** `protocol_id` in path. If referenced by any ComputerAccess it deactivates instead of deleting. +**Purpose:** Delete an access protocol (soft-deactivate when in use). +```bash +curl -X DELETE -H 'Authorization: Bearer $TOKEN' 'http://localhost:5001/api/computers/protocols/2' +``` + +### GET /api/computers/display-kiosks +**Auth:** jwt-optional +**Params:** none. Uses pctype mapping for `gea-shopfloor-display` (default "Kiosk") and the `display_fqdn_domain` setting. +**Purpose:** List display-kiosk computers with derived `F.` FQDN for the Dashboard Defaults picker. +```bash +curl -H 'Authorization: Bearer $TOKEN' 'http://localhost:5001/api/computers/display-kiosks' +``` + +### GET /api/computers +**Auth:** jwt-optional +**Params:** `page`, `per_page`, `active`, `search` (assetnumber/name/serialnumber/hostname ilike), `assetnumber` (exact, for idempotent import), `typeid`|`type_id`, `osid`|`os_id`, `locationid`|`location_id`, `businessunitid`|`businessunit_id`, `shopfloor` (true/false), `sort` (hostname|assetnumber|name|lastreporteddate), `dir` (asc|desc) +**Purpose:** List computers (asset + computer + resolved access links), filtered and paginated. +```bash +curl 'http://localhost:5001/api/computers?shopfloor=true&sort=lastreporteddate&dir=desc&per_page=25' +``` + +### GET /api/computers/<int:computer_id> +**Auth:** jwt-optional +**Params:** `computer_id` in path +**Purpose:** Get one computer with full detail: asset fields, computer extension, communications, resolved access links. +```bash +curl 'http://localhost:5001/api/computers/42' +``` + +### GET /api/computers/by-asset/<int:asset_id> +**Auth:** jwt-optional +**Params:** `asset_id` in path +**Purpose:** Get computer record by its core asset ID. +```bash +curl 'http://localhost:5001/api/computers/by-asset/1234' +``` + +### GET /api/computers/by-hostname/<hostname> +**Auth:** jwt-optional +**Params:** `hostname` in path (exact match) +**Purpose:** Get computer record by hostname. +```bash +curl 'http://localhost:5001/api/computers/by-hostname/tsgwp00525' +``` + +### POST /api/computers +**Auth:** permission `computers.create` (jwt_required) +**Params:** body: `assetnumber` (required); `name`, `serialnumber`, `gaugelabreference`, `maintenancereference`, `statusid`, `locationid`, `businessunitid`, `mapx`, `mapy`, `notes`, `computertypeid`, `hostname`, `osid`, `vendorid`, `modelnumberid`, `loggedinuser`, `lastreporteddate`, `lastboottime`, `ipaddress` (creates primary IP communication), `accessmethods` `[{protocolid, portoverride?}]`. `X-Import-Mode` header preserves legacy timestamps. 409 on duplicate assetnumber or hostname. +**Purpose:** Create a computer (Asset + Computer records, optional primary IP and access methods), audit-logged. +```bash +curl -X POST -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' -d '{"assetnumber":"PC-0042","hostname":"tsgwp00042","computertypeid":1,"osid":2,"ipaddress":"10.1.2.3","accessmethods":[{"protocolid":1}]}' 'http://localhost:5001/api/computers' +``` + +### PUT /api/computers/<int:computer_id> +**Auth:** permission `computers.edit` (jwt_required) +**Params:** body: any asset field (`assetnumber`, `name`, `serialnumber`, `gaugelabreference`, `maintenancereference`, `statusid`, `locationid`, `businessunitid`, `mapx`, `mapy`, `notes`, `isactive`) or computer field (`computertypeid`, `hostname`, `osid`, `vendorid`, `modelnumberid`, `loggedinuser`, `lastreporteddate`, `lastboottime`). `ipaddress` upserts/clears the primary IP communication; `accessmethods` replaces the protocol list. 409 on assetnumber/hostname conflict. Changes audit-logged. +**Purpose:** Update a computer's asset and extension fields. +```bash +curl -X PUT -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' -d '{"locationid":7,"ipaddress":"10.1.2.4"}' 'http://localhost:5001/api/computers/42' +``` + +### DELETE /api/computers/<int:computer_id> +**Auth:** permission `computers.delete` (jwt_required) +**Params:** `computer_id` in path +**Purpose:** Soft-delete a computer (sets asset `isactive=false`), audit-logged. +```bash +curl -X DELETE -H 'Authorization: Bearer $TOKEN' 'http://localhost:5001/api/computers/42' +``` + +### GET /api/computers/<int:computer_id>/apps +**Auth:** jwt-optional +**Params:** `computer_id` in path. Returns active installs only. +**Purpose:** List installed applications on a computer. +```bash +curl 'http://localhost:5001/api/computers/42/apps' +``` + +### POST /api/computers/<int:computer_id>/apps +**Auth:** permission `computers.create` (jwt_required) +**Params:** body: `appid` (required, must exist in Applications), `appversionid`. Reactivates a soft-deleted install; 409 if already installed. +**Purpose:** Record an application install on a computer. +```bash +curl -X POST -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' -d '{"appid":17,"appversionid":3}' 'http://localhost:5001/api/computers/42/apps' +``` + +### DELETE /api/computers/<int:computer_id>/apps/<int:app_id> +**Auth:** permission `computers.delete` (jwt_required) +**Params:** `computer_id` and `app_id` in path +**Purpose:** Soft-remove an installed application (`isactive=false`). +```bash +curl -X DELETE -H 'Authorization: Bearer $TOKEN' 'http://localhost:5001/api/computers/42/apps/17' +``` + +### POST /api/computers/<int:computer_id>/report +**Auth:** permission `computers.create` (jwt_required) +**Params:** body (all optional): `loggedinuser`, `lastboottime`. Server sets `lastreporteddate` to now (UTC). +**Purpose:** Agent status check-in: refresh last-reported timestamp plus logged-in user and boot time. +```bash +curl -X POST -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' -d '{"loggedinuser":"cproudlock","lastboottime":"2026-07-30T06:00:00"}' 'http://localhost:5001/api/computers/42/report' +``` + +### GET /api/computers/dashboard/summary +**Auth:** jwt-optional +**Params:** none +**Purpose:** Dashboard counts: total active computers, breakdown by type and OS, shopfloor vs non-shopfloor. +```bash +curl 'http://localhost:5001/api/computers/dashboard/summary' +``` + +## plugin-employees + +### GET /api/employees/search + +**Auth:** none +**Params:** `q` (query string, min 2 chars, required), `limit` (max results, default 10, capped 50) +**Purpose:** Search employees by first name, last name, or SSO substring (self-hosted table or external HR DB depending on `employee_directory_mode` setting). + +```bash +curl 'http://localhost:5001/api/employees/search?q=smith&limit=5' +``` + +### GET /api/employees/lookup/<sso> + +**Auth:** none +**Params:** `sso` (path, numeric) +**Purpose:** Look up a single employee by numeric SSO; returns directory fields plus resolved `photourl`. + +```bash +curl 'http://localhost:5001/api/employees/lookup/210009518' +``` + +### GET /api/employees/lookup + +**Auth:** none +**Params:** `sso` (query, comma-separated numeric SSOs, at least one required) +**Purpose:** Bulk lookup of multiple employees by SSO list; returns `employees` array plus a joined names string. + +```bash +curl 'http://localhost:5001/api/employees/lookup?sso=210009518,210001234' +``` + +### GET /api/employees/directory + +**Auth:** jwt-optional +**Params:** none +**Purpose:** List the full self-hosted directory for the management page; 400 when directory mode is external. + +```bash +curl -H 'Authorization: Bearer $TOKEN' 'http://localhost:5001/api/employees/directory' +``` + +### POST /api/employees/directory + +**Auth:** jwt + require_role admin +**Params:** JSON body: `sso` (numeric, required), `firstname`, `lastname` (required), `team`, `role`, `picture` (also accepts external-style keys `SSO`/`First_Name`/`Last_Name`/`Team`/`Role`/`Picture`) +**Purpose:** Create a self-hosted directory employee; 409 if SSO exists, 400 in external mode. + +```bash +curl -X POST -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' -d '{"sso":210009518,"firstname":"Jane","lastname":"Doe","team":"CNC","role":"Machinist"}' 'http://localhost:5001/api/employees/directory' +``` + +### PUT /api/employees/directory/<int:sso> + +**Auth:** jwt + require_role admin +**Params:** `sso` (path); JSON body: `firstname`, `lastname`, `team`, `role`, `picture` (external-style keys also accepted; `team`/`role`/`picture` can be cleared) +**Purpose:** Update a self-hosted directory employee's name/team/role/picture; 404 if missing, 400 in external mode. + +```bash +curl -X PUT -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' -d '{"team":"Quality"}' 'http://localhost:5001/api/employees/directory/210009518' +``` + +### DELETE /api/employees/directory/<int:sso> + +**Auth:** jwt + require_role admin +**Params:** `sso` (path) +**Purpose:** Delete a self-hosted directory employee; 404 if missing, 400 in external mode. + +```bash +curl -X DELETE -H 'Authorization: Bearer $TOKEN' 'http://localhost:5001/api/employees/directory/210009518' +``` + +### POST /api/employees/directory/import + +**Auth:** jwt + require_role admin +**Params:** multipart `file=` OR JSON body `{"csv": "..."}`; rows missing numeric sso or names are skipped +**Purpose:** Bulk upsert the self-hosted directory from CSV (headers `SSO,First_Name,Last_Name,Team,Role,Picture` case-insensitive; plain `firstname`/`lastname` also accepted); returns added/updated/skipped counts. + +```bash +curl -X POST -H 'Authorization: Bearer $TOKEN' -F 'file=@employees.csv' 'http://localhost:5001/api/employees/directory/import' +``` + +### POST /api/employees/<int:sso>/photo + +**Auth:** jwt + require_role admin +**Params:** `sso` (path); multipart/form-data `file=`, extensions .png/.jpg/.jpeg/.gif/.webp only +**Purpose:** Upload or replace a self-hosted employee's photo (saved as `photo-` in instance employeephotos dir; old file wiped even on extension change); 409 in external mode, 404 if employee missing. + +```bash +curl -X POST -H 'Authorization: Bearer $TOKEN' -F 'file=@jane.jpg' 'http://localhost:5001/api/employees/210009518/photo' +``` + +### GET /api/employees/photo/<path:filename> + +**Auth:** none +**Params:** `filename` (path, e.g. `photo-210009518.jpg`) +**Purpose:** Serve an uploaded employee photo file from the instance employeephotos dir (public so kiosk recognition cards can read it). + +```bash +curl 'http://localhost:5001/api/employees/photo/photo-210009518.jpg' +``` + +### DELETE /api/employees/<int:sso>/photo + +**Auth:** jwt + require_role admin +**Params:** `sso` (path) +**Purpose:** Clear a self-hosted employee's photo record and delete the uploaded file; 409 in external mode, 404 if employee missing. + +```bash +curl -X DELETE -H 'Authorization: Bearer $TOKEN' 'http://localhost:5001/api/employees/210009518/photo' +``` + +## plugin-geenforce + +### GET /api/geenforce/manifest +**Auth:** api-key (managed service token with `geenforce.fetch` scope via `X-API-Key` or Bearer PAT) OR source IP in `geenforce_allowed_cidrs`; resource-bound tokens restricted to their listed scopes (403 otherwise) +**Params:** query: `pctype` (required, = scopename), `phase` (default `runtime`); header: `If-None-Match` for 304 +**Purpose:** Serve the current PUBLISHED manifest JSON snapshot for a scope (never the live draft) to the GE-Enforce client, with ETag/304 support and `X-Manifest-Version` header. + +```bash +curl -H 'X-API-Key: $TOKEN' 'http://localhost:5001/api/geenforce/manifest?pctype=cmm&phase=runtime' +``` + +### GET /api/geenforce/payload/<sha256> +**Auth:** api-key (`geenforce.fetch` service token) OR IP allowlist; resource-bound tokens get 404 for blobs not referenced by their scopes +**Params:** path: `sha256` (64 lowercase hex chars, 400 otherwise); header: `If-None-Match` (ETag = the hash) +**Purpose:** Download a payload blob by content hash (blob store first, then inline DB payload) so share-less PCs can pull installers over HTTPS; per-IP rate limited (120/60s default) and size-capped (512MB default, 413 above). + +```bash +curl -H 'X-API-Key: $TOKEN' -o installer.exe 'http://localhost:5001/api/geenforce/payload/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' +``` + +### POST /api/geenforce/report +**Auth:** api-key (managed service token with `geenforce.report` scope) OR IP allowlist +**Params:** JSON body: `hostname` (required); remainder parsed by `service.record_enforcement_report` (`scopename`, `phase`, `appliedversion`, `enforcerversion`, `status`, per-entry results, counts); 400 on ValueError +**Purpose:** Record one PC's enforcement cycle: applied manifest version plus per-entry self-heal outcomes (installed/skipped/failed); returns `reportid` + `status`. + +```bash +curl -X POST -H 'X-API-Key: $TOKEN' -H 'Content-Type: application/json' -d '{"hostname":"tsgwp00525","scopename":"cmm","appliedversion":4,"results":[{"entryname":"7zip","action":"installed"}]}' http://localhost:5001/api/geenforce/report +``` + +### GET /api/geenforce/scopes +**Auth:** jwt + permission `geenforce.manage` +**Params:** none +**Purpose:** List all imaging PC-type scopes with entry counts and current published version numbers, ordered by phase then scopename. + +```bash +curl -H 'Authorization: Bearer $JWT' http://localhost:5001/api/geenforce/scopes +``` + +### POST /api/geenforce/scopes +**Auth:** jwt + permission `geenforce.manage` +**Params:** JSON body: `scopename` (required), `phase` (default `runtime`, must be in PHASES), `manifestversion` (default `1.0`), `description`, `computertypeid`, `measuringtooltypeid`, `iscommon` (defaults true when scopename == `common`) +**Purpose:** Create a new manifest scope; 400 if scopename missing, phase invalid, or scope already exists for that scopename+phase; returns 201 with scope summary. + +```bash +curl -X POST -H 'Authorization: Bearer $JWT' -H 'Content-Type: application/json' -d '{"scopename":"cmm","phase":"runtime","description":"CMM bays"}' http://localhost:5001/api/geenforce/scopes +``` + +### GET /api/geenforce/scopes/<int:scopeid> +**Auth:** jwt + permission `geenforce.manage` +**Params:** path: `scopeid` +**Purpose:** Get one scope's summary plus its full draft entry list (each entry includes entryid, sortorder, curated appid/appname link, and inline-payload metadata). + +```bash +curl -H 'Authorization: Bearer $JWT' http://localhost:5001/api/geenforce/scopes/3 +``` + +### PUT /api/geenforce/scopes/<int:scopeid> +**Auth:** jwt + permission `geenforce.manage` +**Params:** JSON body (all optional): `description`, `computertypeid`, `measuringtooltypeid`, `manifestversion` (stringified), `iscommon` (bool-coerced) +**Purpose:** Update scope metadata fields (only keys present in the body are changed); scopename and phase are immutable here. + +```bash +curl -X PUT -H 'Authorization: Bearer $JWT' -H 'Content-Type: application/json' -d '{"description":"updated","manifestversion":"1.1"}' http://localhost:5001/api/geenforce/scopes/3 +``` + +### DELETE /api/geenforce/scopes/<int:scopeid> +**Auth:** jwt + permission `geenforce.manage` +**Params:** path: `scopeid` +**Purpose:** Delete a scope (and via cascade its entries); returns `{deleted: scopeid}`. + +```bash +curl -X DELETE -H 'Authorization: Bearer $JWT' http://localhost:5001/api/geenforce/scopes/3 +``` + +### GET /api/geenforce/scopes/<int:scopeid>/preview +**Auth:** jwt + permission `geenforce.manage` +**Params:** path: `scopeid` +**Purpose:** Render the DRAFT manifest JSON that a publish would freeze, for admin review before shipping. + +```bash +curl -H 'Authorization: Bearer $JWT' http://localhost:5001/api/geenforce/scopes/3/preview +``` + +### GET /api/geenforce/applications +**Auth:** jwt + permission `geenforce.manage` +**Params:** none +**Purpose:** List the core active Applications catalog (appid + appname) for the curated entry-to-app link picker in the entry editor. + +```bash +curl -H 'Authorization: Bearer $JWT' http://localhost:5001/api/geenforce/applications +``` + +### POST /api/geenforce/scopes/<int:scopeid>/entries +**Auth:** jwt + permission `geenforce.manage` +**Params:** path: `scopeid`; JSON body: `Name` (required), `Type` (required, one of ENTRY_TYPES), optional `appid` (curated app link, unknown/non-numeric ids ignored), plus manifest fields consumed by `build_entry` (PCTypes, TargetHostnames, DetectionValue, etc.) +**Purpose:** Create a manifest entry in a scope at the next sortorder; validates Name (required) and Type (must be in ENTRY_TYPES); 400 on duplicate Name in scope; returns 201 with entry payload. + +```bash +curl -X POST -H 'Authorization: Bearer $JWT' -H 'Content-Type: application/json' -d '{"Name":"7-Zip","Type":"App","appid":12}' http://localhost:5001/api/geenforce/scopes/3/entries +``` + +### PUT /api/geenforce/entries/<int:entryid> +**Auth:** jwt + permission `geenforce.manage` +**Params:** path: `entryid`; JSON body: `Name` (required), `Type` (required), optional `appid`, plus manifest fields +**Purpose:** Replace an entry's fields from the payload (re-populates via `populate_entry`, re-creating the one-to-one InUseCheck); same Name/Type validation and duplicate-Name 400 as create. + +```bash +curl -X PUT -H 'Authorization: Bearer $JWT' -H 'Content-Type: application/json' -d '{"Name":"7-Zip","Type":"App","DetectionValue":"24.08"}' http://localhost:5001/api/geenforce/entries/17 +``` + +### DELETE /api/geenforce/entries/<int:entryid> +**Auth:** jwt + permission `geenforce.manage` +**Params:** path: `entryid` +**Purpose:** Delete a manifest entry; returns `{deleted: entryid}`. + +```bash +curl -X DELETE -H 'Authorization: Bearer $JWT' http://localhost:5001/api/geenforce/entries/17 +``` + +### PUT /api/geenforce/scopes/<int:scopeid>/entries/reorder +**Auth:** jwt + permission `geenforce.manage` +**Params:** path: `scopeid`; JSON body: `order` = [entryid, ...] (must match the scope's entry ids exactly, 400 otherwise) +**Purpose:** Set entry ordering from an entryid list. + +```bash +curl -X PUT -H 'Authorization: Bearer $JWT' -H 'Content-Type: application/json' -d '{"order":[17,15,16]}' http://localhost:5001/api/geenforce/scopes/3/entries/reorder +``` + +### GET /api/geenforce/scopes/<int:scopeid>/simulate +**Auth:** jwt + permission `geenforce.manage` +**Params:** path: `scopeid`; query (all optional): `pctype` (defaults to scopename), `subtype`, `hostname`, `machinenumber`, `cmmversion`; phase comes from the scope +**Purpose:** Simulate which draft entries would apply to a given machine profile and why the rest are filtered out (PCTypes/TargetHostnames/TargetMachineNumbers/_CmmVersion), using the engine-mirror filters. + +```bash +curl -H 'Authorization: Bearer $JWT' 'http://localhost:5001/api/geenforce/scopes/3/simulate?hostname=tsgwp00525&cmmversion=2023.2' +``` + +### GET /api/geenforce/scopes/<int:scopeid>/compliance +**Auth:** jwt + permission `geenforce.manage` +**Params:** path: `scopeid` +**Purpose:** Fleet install coverage per app-linked entry (installed/version-match counts from the computers plugin's ComputerInstalledApp; null counts with `computersplugin:false` when that plugin is absent). + +```bash +curl -H 'Authorization: Bearer $JWT' http://localhost:5001/api/geenforce/scopes/3/compliance +``` + +### POST /api/geenforce/entries/<int:entryid>/payload +**Auth:** jwt + permission `geenforce.publish` +**Params:** path: `entryid`; multipart/form-data: `file` (required) +**Purpose:** Upload an inline payload file (max 1 MB, rejects empty) for an entry and point the entry at it (stores sha256, filename, mimetype); returns 201 with entry payload. + +```bash +curl -X POST -H 'Authorization: Bearer $JWT' -F 'file=@fix.ps1' http://localhost:5001/api/geenforce/entries/17/payload +``` + +### GET /api/geenforce/entries/<int:entryid>/payload +**Auth:** jwt + permission `geenforce.manage` +**Params:** path: `entryid` +**Purpose:** Download the stored inline payload bytes for an entry as an attachment (404 if the entry has no payload). + +```bash +curl -H 'Authorization: Bearer $JWT' -o fix.ps1 http://localhost:5001/api/geenforce/entries/17/payload +``` + +### POST /api/geenforce/scopes/<int:scopeid>/publish +**Auth:** jwt + permission `geenforce.publish` +**Params:** path: `scopeid`; JSON body (optional): `notes` +**Purpose:** Freeze the scope's draft into a new published version (`service.publish_scope`), recording the publishing user from the JWT identity and optional notes; returns 201 with the new versionnumber. + +```bash +curl -X POST -H 'Authorization: Bearer $JWT' -H 'Content-Type: application/json' -d '{"notes":"add 7zip 24.08"}' http://localhost:5001/api/geenforce/scopes/3/publish +``` + +### GET /api/geenforce/scopes/<int:scopeid>/versions +**Auth:** jwt + permission `geenforce.manage` +**Params:** path: `scopeid` +**Purpose:** List published versions for a scope, newest first (versionnumber, iscurrent, publishedat, publishedby, notes). + +```bash +curl -H 'Authorization: Bearer $JWT' http://localhost:5001/api/geenforce/scopes/3/versions +``` + +### GET /api/geenforce/scopes/<int:scopeid>/versions/<int:versionnumber> +**Auth:** jwt + permission `geenforce.manage` +**Params:** path: `scopeid`, `versionnumber` +**Purpose:** Fetch one published version's frozen manifest JSON (parsed and returned in the success envelope). + +```bash +curl -H 'Authorization: Bearer $JWT' http://localhost:5001/api/geenforce/scopes/3/versions/4 +``` + +### POST /api/geenforce/scopes/<int:scopeid>/rollback +**Auth:** jwt + permission `geenforce.publish` +**Params:** path: `scopeid`; JSON body: `versionnumber` (required, int) +**Purpose:** Roll the scope's current published pointer back to an earlier versionnumber (`service.rollback_scope`); 400 with the error message if the version is invalid. + +```bash +curl -X POST -H 'Authorization: Bearer $JWT' -H 'Content-Type: application/json' -d '{"versionnumber":3}' http://localhost:5001/api/geenforce/scopes/3/rollback +``` + +### POST /api/geenforce/scopes/<int:scopeid>/export-share +**Auth:** jwt + permission `geenforce.publish` +**Params:** path: `scopeid`; no body +**Purpose:** Write the scope's current published JSON to the configured share root (`geenforce_share_root` setting), backing up the old file to `_meta/history`; 400 if the share root is unconfigured or the export fails. + +```bash +curl -X POST -H 'Authorization: Bearer $JWT' http://localhost:5001/api/geenforce/scopes/3/export-share +``` + +### GET /api/geenforce/config +**Auth:** jwt + permission `geenforce.manage` +**Params:** none +**Purpose:** Read plugin config: the on-share export root (`geenforce_share_root`) and the client IP allowlist CIDRs (`geenforce_allowed_cidrs`). + +```bash +curl -H 'Authorization: Bearer $JWT' http://localhost:5001/api/geenforce/config +``` + +### PUT /api/geenforce/config +**Auth:** jwt + permission `geenforce.publish` +**Params:** JSON body (both optional): `shareroot` (string path), `allowedcidrs` (CSV/newline CIDR list); allowedcidrs is validated/normalized (comma/newline-separated CIDRs or bare IPs, 400 listing any bad entries); only keys present in the body are written +**Purpose:** Update plugin config settings. + +```bash +curl -X PUT -H 'Authorization: Bearer $JWT' -H 'Content-Type: application/json' -d '{"allowedcidrs":"10.20.30.0/24, 192.168.5.7"}' http://localhost:5001/api/geenforce/config +``` + +### GET /api/geenforce/reports +**Auth:** jwt + permission `geenforce.manage` +**Params:** query (optional): `hostname` (ILIKE match), `scopename` (exact) +**Purpose:** Latest enforcement report per PC (iscurrent rows) for the fleet compliance view: applied vs latest published version (`receivedlatest` flag), install/skip/fail/filtered counts, status, check-in times. + +```bash +curl -H 'Authorization: Bearer $JWT' 'http://localhost:5001/api/geenforce/reports?scopename=cmm' +``` + +### GET /api/geenforce/reports/<int:reportid> +**Auth:** jwt + permission `geenforce.manage` +**Params:** path: `reportid` +**Purpose:** One enforcement report in detail with per-entry outcomes (entryname, action, selfhealed, exitcode, message) plus applied-vs-latest version comparison. + +```bash +curl -H 'Authorization: Bearer $JWT' http://localhost:5001/api/geenforce/reports/42 +``` + +## plugin-knowledgebase + +### GET /api/knowledgebase + +**Auth:** jwt-optional +**Params:** query: `page`, `per_page`, `search` (ILIKE on shortdescription/keywords/application name), `appid` (int filter), `linkurl` (exact natural-key match), `shortdescription` (exact match), `sort` (`clicks|topic|description|lastupdated`, default `clicks`), `order` (`asc|desc`, default `desc`) +**Purpose:** List active KB articles with search, topic filter, natural-key lookup, sorting, and pagination; each item includes its linked application (appid/appname) or null. + +```bash +curl 'http://localhost:5001/api/knowledgebase?search=printer&sort=clicks&order=desc&page=1&per_page=20' +``` + +### GET /api/knowledgebase/stats + +**Auth:** jwt-optional +**Params:** none +**Purpose:** Return aggregate stats for active articles: `totalclicks` (sum of clicks) and `totalarticles` (count). + +```bash +curl 'http://localhost:5001/api/knowledgebase/stats' +``` + +### GET /api/knowledgebase/<int:link_id> + +**Auth:** jwt-optional +**Params:** path: `link_id` (int) +**Purpose:** Fetch a single active article by id with its application (appid/appname) or null; 404 if missing or inactive. + +```bash +curl 'http://localhost:5001/api/knowledgebase/42' +``` + +### POST /api/knowledgebase/<int:link_id>/click + +**Auth:** jwt-optional +**Params:** path: `link_id` (int); no body +**Purpose:** Increment the article's click counter and return the target `linkurl` plus new click count (used for redirect tracking); 404 if missing or inactive. + +```bash +curl -X POST 'http://localhost:5001/api/knowledgebase/42/click' +``` + +### POST /api/knowledgebase + +**Auth:** permission `kb.create` (jwt required) +**Params:** body JSON: `shortdescription` (required), `linkurl` (required), `appid` (optional int, must exist), `keywords` (optional); import-mode may pass timestamp fields +**Purpose:** Create a new KB article; validates required fields and that `appid` (if given) exists; honors `X-Import-Mode` timestamp preservation via `apply_import_timestamps`; returns 201. + +```bash +curl -X POST 'http://localhost:5001/api/knowledgebase' -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' -d '{"shortdescription":"VPN setup guide","linkurl":"https://wiki.example.com/vpn","appid":3,"keywords":"vpn,remote"}' +``` + +### PUT /api/knowledgebase/<int:link_id> + +**Auth:** permission `kb.edit` (jwt required) +**Params:** path: `link_id` (int); body JSON: any of `shortdescription`, `linkurl`, `appid`, `keywords`, `isactive` +**Purpose:** Update an article's shortdescription, linkurl, appid, keywords, and/or isactive; validates `appid` if changed; honors import timestamps; 404 if article missing. + +```bash +curl -X PUT 'http://localhost:5001/api/knowledgebase/42' -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' -d '{"keywords":"vpn,zscaler","isactive":true}' +``` + +### DELETE /api/knowledgebase/<int:link_id> + +**Auth:** permission `kb.delete` (jwt required) +**Params:** path: `link_id` (int) +**Purpose:** Soft-delete an article by setting `isactive=false` (row is retained); 404 if article missing. + +```bash +curl -X DELETE 'http://localhost:5001/api/knowledgebase/42' -H 'Authorization: Bearer $TOKEN' +``` + +## plugin-machines + +### GET /api/machines/types +**Auth:** jwt-optional +**Params:** query: `page`, `per_page`, `active` (pass `false` to include inactive), `search` (ilike on `machinetype`) +**Purpose:** List machine types (active by default) with pagination and name search. +```bash +curl 'http://localhost:5001/api/machines/types?search=cnc&page=1&per_page=25' +``` + +### GET /api/machines/types/<type_id> +**Auth:** jwt-optional +**Params:** path: `type_id` (int) +**Purpose:** Get a single machine type by ID. +```bash +curl 'http://localhost:5001/api/machines/types/3' +``` + +### POST /api/machines/types +**Auth:** jwt + permission:machines.create +**Params:** body: `machinetype` (required), `description`, `icon`, `color` +**Purpose:** Create a machine type; reactivates a soft-deleted type of the same name instead of returning 409. +```bash +curl -X POST 'http://localhost:5001/api/machines/types' -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' -d '{"machinetype":"Lathe","description":"Turning machines","icon":"mdi-rotate-3d","color":"#1976d2"}' +``` + +### PUT /api/machines/types/<type_id> +**Auth:** jwt + permission:machines.edit +**Params:** path: `type_id`; body: any of `machinetype`, `description`, `icon`, `color`, `isactive` +**Purpose:** Update a machine type; rename guarded by 409 on duplicate name. +```bash +curl -X PUT 'http://localhost:5001/api/machines/types/3' -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' -d '{"description":"5-axis mills","isactive":true}' +``` + +### DELETE /api/machines/types/<type_id> +**Auth:** jwt + permission:machines.delete +**Params:** path: `type_id` (int) +**Purpose:** Hard-delete a machine type; refused with 409 if any machine still references it. +```bash +curl -X DELETE 'http://localhost:5001/api/machines/types/3' -H 'Authorization: Bearer $TOKEN' +``` + +### GET /api/machines +**Auth:** jwt-optional +**Params:** query: `page`, `per_page`, `active` (`false` includes inactive), `assetnumber` (exact-match for idempotent import), `search` (assetnumber/name/serialnumber ilike), `typeid`|`type_id`, `vendorid`|`vendor_id`, `locationid`|`location_id`, `businessunitid`|`businessunit_id`, `sort` (`assetnumber`|`name`), `dir` (`asc`|`desc`) +**Purpose:** List machines (Asset+Machine join) with filters, sorting, pagination; collapses Dualpath dual-bay pairs to one row (annotated with `dualpathpartner`) when the site setting is enabled. +```bash +curl 'http://localhost:5001/api/machines?search=2007&typeid=2&sort=name&dir=desc&page=1&per_page=50' +``` + +### GET /api/machines/<machine_id> +**Auth:** jwt-optional +**Params:** path: `machine_id` (int) +**Purpose:** Get one machine with full asset details, nested machine dict, and `dualpathpartner` info. +```bash +curl 'http://localhost:5001/api/machines/42' +``` + +### GET /api/machines/by-asset/<asset_id> +**Auth:** jwt-optional +**Params:** path: `asset_id` (int) +**Purpose:** Get machine data looked up by core asset ID instead of machine ID. +```bash +curl 'http://localhost:5001/api/machines/by-asset/1001' +``` + +### POST /api/machines +**Auth:** jwt + permission:machines.create +**Params:** body: `assetnumber` (required, 409 on duplicate); asset fields: `name`, `gaugelabreference`, `maintenancereference`, `serialnumber`, `statusid` (default 1), `locationid`, `businessunitid`, `mapx`, `mapy`, `notes`; machine fields: `machinetypeid`, `vendorid`, `modelnumberid`, `requiresmanualconfig`, `islocationonly`, `lastmaintenancedate`, `nextmaintenancedate`, `maintenanceintervaldays`, `controllervendorid`, `controllermodelid` +**Purpose:** Create a machine (creates both core Asset row and Machine extension row); audit-logged; honors `X-Import-Mode` legacy timestamps via `apply_import_timestamps`. +```bash +curl -X POST 'http://localhost:5001/api/machines' -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' -d '{"assetnumber":"2007","name":"Makino a51","machinetypeid":2,"vendorid":5,"locationid":3,"statusid":1}' +``` + +### PUT /api/machines/<machine_id> +**Auth:** jwt + permission:machines.edit +**Params:** path: `machine_id`; body: any of `assetnumber`, `name`, `gaugelabreference`, `maintenancereference`, `serialnumber`, `statusid`, `locationid`, `businessunitid`, `mapx`, `mapy`, `notes`, `isactive`, `machinetypeid`, `vendorid`, `modelnumberid`, `requiresmanualconfig`, `islocationonly`, `lastmaintenancedate`, `nextmaintenancedate`, `maintenanceintervaldays`, `controllervendorid`, `controllermodelid` +**Purpose:** Update machine (asset + machine fields) with per-field change tracking to AuditLog; 409 on assetnumber conflict. +```bash +curl -X PUT 'http://localhost:5001/api/machines/42' -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' -d '{"locationid":7,"nextmaintenancedate":"2026-09-01"}' +``` + +### DELETE /api/machines/<machine_id> +**Auth:** jwt + permission:machines.delete +**Params:** path: `machine_id` (int) +**Purpose:** Soft-delete a machine (sets `asset.isactive=false`, keeps Machine row linked); audit-logged. +```bash +curl -X DELETE 'http://localhost:5001/api/machines/42' -H 'Authorization: Bearer $TOKEN' +``` + +### GET /api/machines/dashboard/summary +**Auth:** jwt-optional +**Params:** none +**Purpose:** Dashboard summary: total active machine count plus counts grouped by machine type and by asset status (total and by-type exclude Dualpath secondary bays when the collapse setting is enabled; by-status does not). +```bash +curl 'http://localhost:5001/api/machines/dashboard/summary' +``` + +## plugin-measuringtools + +### GET /api/measuringtools/types +**Auth:** jwt-optional +**Params:** query: `active` (default true; `'false'` includes inactive), `search` (name ilike), `page`, `perpage` +**Purpose:** List measuring-tool types (active-only by default), paginated, name-sorted. + +```bash +curl 'http://localhost:5001/api/measuringtools/types?search=caliper&page=1&perpage=25' +``` + +### GET /api/measuringtools/types/<type_id> +**Auth:** jwt-optional +**Params:** path: `type_id` (int) +**Purpose:** Get one measuring-tool type by id (404 if missing). + +```bash +curl 'http://localhost:5001/api/measuringtools/types/3' +``` + +### POST /api/measuringtools/types +**Auth:** jwt + permission:measuringtools.create +**Params:** body JSON: `name` (required), `description`, `color` +**Purpose:** Create a measuring-tool type; reactivates a soft-deleted same-named one, 409 if an active one exists. + +```bash +curl -X POST 'http://localhost:5001/api/measuringtools/types' -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' -d '{"name":"Thread Gage","description":"Go/no-go thread gages","color":"#4caf50"}' +``` + +### PUT /api/measuringtools/types/<type_id> +**Auth:** jwt + permission:measuringtools.edit +**Params:** path: `type_id`; body JSON: `name`, `description`, `color`, `isactive` (only keys present are applied) +**Purpose:** Update a measuring-tool type; 409 on rename collision with an existing name. + +```bash +curl -X PUT 'http://localhost:5001/api/measuringtools/types/3' -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' -d '{"description":"Updated","isactive":true}' +``` + +### DELETE /api/measuringtools/types/<type_id> +**Auth:** jwt + permission:measuringtools.delete +**Params:** path: `type_id` (int) +**Purpose:** Hard-delete a measuring-tool type; refused with 409 if any tool still references it. + +```bash +curl -X DELETE 'http://localhost:5001/api/measuringtools/types/3' -H 'Authorization: Bearer $TOKEN' +``` + +### GET /api/measuringtools +**Auth:** jwt-optional +**Params:** query: `active` (default true), `assetnumber` (exact match, for idempotent import), `search` (assetnumber/name/serialnumber ilike), `typeid`, `locationid`, `statusid`, `calibrationstatus` (overdue|duesoon|current|unknown), `page`, `perpage` +**Purpose:** List measuring tools (Asset core merged with extension), filtered + paginated; derived calibrationstatus filter applied post-pagination. + +```bash +curl 'http://localhost:5001/api/measuringtools?typeid=2&calibrationstatus=overdue&page=1&perpage=50' +``` + +### GET /api/measuringtools/<tool_id> +**Auth:** jwt-optional +**Params:** path: `tool_id` (int) +**Purpose:** Get one measuring tool by measuringtoolid, asset core dict with extension nested under `measuringtool`. + +```bash +curl 'http://localhost:5001/api/measuringtools/17' +``` + +### GET /api/measuringtools/by-asset/<asset_id> +**Auth:** jwt-optional +**Params:** path: `asset_id` (int) +**Purpose:** Get a measuring tool by its core assetid (404 if the asset has no extension row). + +```bash +curl 'http://localhost:5001/api/measuringtools/by-asset/1042' +``` + +### POST /api/measuringtools +**Auth:** jwt + permission:measuringtools.create +**Params:** body JSON: `assetnumber` (required, 409 on duplicate), `name`, `gaugelabreference`, `maintenancereference`, `serialnumber`, `statusid` (default 1), `locationid`, `businessunitid`, `mapx`, `mapy`, `notes`, `measuringtooltypeid`, `calibrationintervaldays`, `lastcalibrationdate` (YYYY-MM-DD), `nextcalibrationdate` (YYYY-MM-DD), `calibrationprovider`; import timestamps honored via `X-Import-Mode` +**Purpose:** Create a measuring tool: one Asset core row (assettype `measuring_tool`) plus one measuringtools extension row in a single payload; audit-logged. + +```bash +curl -X POST 'http://localhost:5001/api/measuringtools' -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' -d '{"assetnumber":"MT-0042","name":"6in Digital Caliper","measuringtooltypeid":2,"calibrationintervaldays":365,"lastcalibrationdate":"2026-01-15","nextcalibrationdate":"2027-01-15","calibrationprovider":"Gage Lab"}' +``` + +### PUT /api/measuringtools/<tool_id> +**Auth:** jwt + permission:measuringtools.edit +**Params:** path: `tool_id`; body JSON (only present keys applied): asset fields `assetnumber`, `name`, `gaugelabreference`, `maintenancereference`, `serialnumber`, `statusid`, `locationid`, `businessunitid`, `mapx`, `mapy`, `notes`, `isactive`; extension fields `measuringtooltypeid`, `calibrationintervaldays`, `calibrationprovider`, `notes`, `lastcalibrationdate`, `nextcalibrationdate` (YYYY-MM-DD) +**Purpose:** Update asset core fields and extension fields in one payload; 409 on assetnumber collision; changes audit-logged. + +```bash +curl -X PUT 'http://localhost:5001/api/measuringtools/17' -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' -d '{"locationid":5,"nextcalibrationdate":"2026-12-01"}' +``` + +### DELETE /api/measuringtools/<tool_id> +**Auth:** jwt + permission:measuringtools.delete +**Params:** path: `tool_id` (int) +**Purpose:** Soft-delete a measuring tool by setting its asset isactive=false; audit-logged. + +```bash +curl -X DELETE 'http://localhost:5001/api/measuringtools/17' -H 'Authorization: Bearer $TOKEN' +``` + +### GET /api/measuringtools/map-overlay +**Auth:** jwt-optional +**Params:** none +**Purpose:** ADR-010 map overlay: per-asset calibration badges `[{assetid, color, label}]` for active tools that are overdue or due soon only; no coordinates returned. + +```bash +curl 'http://localhost:5001/api/measuringtools/map-overlay' +``` + +### GET /api/measuringtools/report/calibration +**Auth:** jwt-optional +**Params:** none +**Purpose:** Calibration report for the Reports hub: counts and full tool lists bucketed by derived status (overdue/duesoon/current/unknown) plus statuscolors map. + +```bash +curl 'http://localhost:5001/api/measuringtools/report/calibration' +``` + +## plugin-network + +### GET /api/network/types +**Auth:** jwt-optional +**Params:** `page`, `per_page`, `active` (default true; `false` includes inactive), `search` (ilike on networkdevicetype) +**Purpose:** List network device types, paginated. + +```bash +curl 'http://localhost:5001/api/network/types?search=switch&per_page=50' +``` + +### GET /api/network/types/<int:type_id> +**Auth:** jwt-optional +**Params:** path: `type_id` +**Purpose:** Get one network device type by ID. + +```bash +curl http://localhost:5001/api/network/types/3 +``` + +### POST /api/network/types +**Auth:** jwt + permission `network.create` +**Params:** body: `networkdevicetype` (required), `description`, `icon`, `color` +**Purpose:** Create a network device type. Reactivates a soft-deleted duplicate instead of returning 409. + +```bash +curl -X POST http://localhost:5001/api/network/types -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"networkdevicetype":"Switch","icon":"mdi-switch","color":"#2196f3"}' +``` + +### PUT /api/network/types/<int:type_id> +**Auth:** jwt + permission `network.edit` +**Params:** body: `networkdevicetype`, `description`, `icon`, `color`, `isactive` +**Purpose:** Update a network device type. 409 on name collision. + +```bash +curl -X PUT http://localhost:5001/api/network/types/3 -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"description":"Access switches"}' +``` + +### DELETE /api/network/types/<int:type_id> +**Auth:** jwt + permission `network.delete` +**Params:** path: `type_id` +**Purpose:** Hard-delete a device type. 409 if any device still references it. + +```bash +curl -X DELETE http://localhost:5001/api/network/types/3 -H "Authorization: Bearer $TOKEN" +``` + +### GET /api/network +**Auth:** jwt-optional +**Params:** `page`, `per_page`, `active`, `assetnumber` (exact match for idempotent import), `search` (assetnumber/name/serialnumber/hostname), `typeid`|`type_id`, `vendorid`|`vendor_id`, `locationid`|`location_id`, `businessunitid`|`businessunit_id`, `poe` (true/false), `managed` (true/false), `sort` (hostname|assetnumber|name), `dir` (asc|desc) +**Purpose:** List network devices (Asset joined with NetworkDevice extension plus primary IP), filtered, sorted, paginated. + +```bash +curl 'http://localhost:5001/api/network?typeid=2&poe=true&sort=hostname&dir=asc' +``` + +### GET /api/network/<int:device_id> +**Auth:** jwt-optional +**Params:** path: `device_id` (networkdeviceid) +**Purpose:** Get one network device (asset dict + `networkdevice` sub-object + primary `ipaddress`). + +```bash +curl http://localhost:5001/api/network/17 +``` + +### GET /api/network/by-asset/<int:asset_id> +**Auth:** jwt-optional +**Params:** path: `asset_id` +**Purpose:** Look up a network device by its core assetid. + +```bash +curl http://localhost:5001/api/network/by-asset/1042 +``` + +### GET /api/network/by-hostname/<hostname> +**Auth:** jwt-optional +**Params:** path: `hostname` +**Purpose:** Look up a network device by exact hostname. + +```bash +curl http://localhost:5001/api/network/by-hostname/wjf-sw-idf3-01 +``` + +### POST /api/network +**Auth:** jwt + permission `network.create` +**Params:** body: `assetnumber` (required); `name`, `serialnumber`, `gaugelabreference`, `maintenancereference`, `statusid` (default 1), `locationid`, `businessunitid`, `mapx`, `mapy`, `notes`, `networkdevicetypeid`, `vendorid`, `hostname` (unique), `firmwareversion`, `portcount`, `ispoe`, `ismanaged`, `rackunit`, `ipaddress` +**Purpose:** Create a network device. Creates core Asset + NetworkDevice extension, upserts primary-IP Communication, audit-logged, honors `X-Import-Mode` legacy timestamps. + +```bash +curl -X POST http://localhost:5001/api/network -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"assetnumber":"NET-0042","hostname":"wjf-sw-idf3-01","networkdevicetypeid":1,"portcount":48,"ispoe":true,"ipaddress":"10.1.3.10"}' +``` + +### PUT /api/network/<int:device_id> +**Auth:** jwt + permission `network.edit` +**Params:** body: any of `assetnumber`, `name`, `serialnumber`, `gaugelabreference`, `maintenancereference`, `statusid`, `locationid`, `businessunitid`, `mapx`, `mapy`, `notes`, `isactive`, `networkdevicetypeid`, `vendorid`, `hostname`, `firmwareversion`, `portcount`, `ispoe`, `ismanaged`, `rackunit`, `ipaddress` +**Purpose:** Update asset + network-device fields. 409 on assetnumber/hostname conflicts, change-diff audit log, upserts primary IP when `ipaddress` present. + +```bash +curl -X PUT http://localhost:5001/api/network/17 -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"firmwareversion":"16.12.4","ipaddress":"10.1.3.11"}' +``` + +### DELETE /api/network/<int:device_id> +**Auth:** jwt + permission `network.delete` +**Params:** path: `device_id` +**Purpose:** Soft-delete a network device (sets asset.isactive=false). Audit-logged. + +```bash +curl -X DELETE http://localhost:5001/api/network/17 -H "Authorization: Bearer $TOKEN" +``` + +### GET /api/network/dashboard/summary +**Auth:** jwt-optional +**Params:** none +**Purpose:** Dashboard counts: total active devices, by type, by vendor, PoE vs non-PoE. + +```bash +curl http://localhost:5001/api/network/dashboard/summary +``` + +### GET /api/network/vlans +**Auth:** jwt-optional +**Params:** `page`, `per_page`, `active` (default true), `search` (name/description/vlannumber), `type` (exact vlantype) +**Purpose:** List VLANs, paginated, ordered by vlannumber. + +```bash +curl 'http://localhost:5001/api/network/vlans?search=voice' +``` + +### GET /api/network/vlans/<int:vlan_id> +**Auth:** jwt-optional +**Params:** path: `vlan_id` +**Purpose:** Get one VLAN including its active subnets. + +```bash +curl http://localhost:5001/api/network/vlans/5 +``` + +### POST /api/network/vlans +**Auth:** jwt + permission `network.create` +**Params:** body: `vlannumber` (required), `name` (required), `description`, `vlantype` +**Purpose:** Create a VLAN. 409 on duplicate vlannumber. Audit-logged. + +```bash +curl -X POST http://localhost:5001/api/network/vlans -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"vlannumber":120,"name":"Shopfloor","vlantype":"production"}' +``` + +### PUT /api/network/vlans/<int:vlan_id> +**Auth:** jwt + permission `network.edit` +**Params:** body: `vlannumber`, `name`, `description`, `vlantype`, `isactive` +**Purpose:** Update a VLAN. 409 on vlannumber conflict. Change-diff audit log. + +```bash +curl -X PUT http://localhost:5001/api/network/vlans/5 -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"description":"CNC cell VLAN"}' +``` + +### DELETE /api/network/vlans/<int:vlan_id> +**Auth:** jwt + permission `network.delete` +**Params:** path: `vlan_id` +**Purpose:** Soft-delete a VLAN. 400 if it still has active subnets. Audit-logged. + +```bash +curl -X DELETE http://localhost:5001/api/network/vlans/5 -H "Authorization: Bearer $TOKEN" +``` + +### GET /api/network/subnets +**Auth:** jwt-optional +**Params:** `page`, `per_page`, `active` (default true), `search` (cidr/name/description), `vlanid`, `locationid`, `type` (exact subnettype) +**Purpose:** List subnets, paginated, ordered by cidr. + +```bash +curl 'http://localhost:5001/api/network/subnets?vlanid=5' +``` + +### GET /api/network/subnets/<int:subnet_id> +**Auth:** jwt-optional +**Params:** path: `subnet_id` +**Purpose:** Get one subnet plus `devices`: every asset of any type (PC/printer/network) whose primary-IP Communication falls inside the CIDR, with cross-plugin detail URLs. + +```bash +curl http://localhost:5001/api/network/subnets/2 +``` + +### POST /api/network/subnets +**Auth:** jwt + permission `network.create` +**Params:** body: `cidr` (required, must contain `/`), `name` (required), `description`, `gatewayip`, `subnetmask`, `networkaddress`, `broadcastaddress`, `vlanid`, `subnettype`, `locationid`, `dhcpenabled` (default true), `dhcprangestart`, `dhcprangeend`, `dns1`, `dns2` +**Purpose:** Create a subnet. Validates CIDR notation and vlanid existence. 409 on duplicate cidr. Audit-logged. + +```bash +curl -X POST http://localhost:5001/api/network/subnets -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"cidr":"10.1.3.0/24","name":"IDF3 access","vlanid":5,"gatewayip":"10.1.3.1"}' +``` + +### PUT /api/network/subnets/<int:subnet_id> +**Auth:** jwt + permission `network.edit` +**Params:** body: any of `cidr`, `name`, `description`, `gatewayip`, `subnetmask`, `networkaddress`, `broadcastaddress`, `vlanid`, `subnettype`, `locationid`, `dhcpenabled`, `dhcprangestart`, `dhcprangeend`, `dns1`, `dns2`, `isactive` +**Purpose:** Update a subnet. 409 on cidr conflict. Change-diff audit log. + +```bash +curl -X PUT http://localhost:5001/api/network/subnets/2 -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"dhcpenabled":false}' +``` + +### DELETE /api/network/subnets/<int:subnet_id> +**Auth:** jwt + permission `network.delete` +**Params:** path: `subnet_id` +**Purpose:** Soft-delete a subnet (isactive=false). Audit-logged. + +```bash +curl -X DELETE http://localhost:5001/api/network/subnets/2 -H "Authorization: Bearer $TOKEN" +``` + +## plugin-notifications + +### GET /api/notifications/types + +**Auth:** none +**Params:** `page`, `per_page`; `active=false` to include inactive types +**Purpose:** List notification types, paginated, active-only by default. + +```bash +curl 'http://localhost:5001/api/notifications/types?active=false&page=1&per_page=50' +``` + +### POST /api/notifications/types + +**Auth:** JWT + permission `notifications.create` +**Params:** body: `typename` (required, unique), `typedescription`/`description`, `typecolor`/`color`, `expirymode` (`none`|`duration`|`dailytime`), `expirydays`, `expiryhour`, `expiryminute`, `splitperemployee`, `showemployeephoto`, `displaystyle` (`standard`|`carousel`|`grid`|`banner`) +**Purpose:** Create a notification type including expiry rule and shopfloor display config. + +```bash +curl -X POST http://localhost:5001/api/notifications/types -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' -d '{"typename":"Recognition","typecolor":"recognition","expirymode":"dailytime","expiryhour":8,"splitperemployee":true,"showemployeephoto":true}' +``` + +### PUT, PATCH /api/notifications/types/<int:type_id> + +**Auth:** JWT + permission `notifications.create` +**Params:** body: any of `typename` (unique-checked), `typedescription`/`description`, `typecolor`/`color`, `isactive`, `expirymode`, `expirydays`, `expiryhour`, `expiryminute`, `splitperemployee`, `showemployeephoto`, `displaystyle` +**Purpose:** Update a notification type (name/desc/color/isactive plus expiry and display fields). + +```bash +curl -X PATCH http://localhost:5001/api/notifications/types/3 -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' -d '{"expirymode":"duration","expirydays":14}' +``` + +### GET /api/notifications + +**Auth:** none +**Params:** `page`, `per_page`; `active=false` to include inactive; `typeid`/`type_id`; `ticketnumber` (exact match, for idempotent import); `current=true` (within start/end window now); `search` (ILIKE on text) +**Purpose:** List notifications with filters, newest-first, paginated. + +```bash +curl 'http://localhost:5001/api/notifications?current=true&typeid=2&search=outage&page=1' +``` + +### GET /api/notifications/<int:notification_id> + +**Auth:** none +**Params:** path: `notification_id` +**Purpose:** Get a single notification by ID. + +```bash +curl http://localhost:5001/api/notifications/42 +``` + +### POST /api/notifications + +**Auth:** JWT + permission `notifications.create` +**Params:** body: `notification`/`message` (required), `notificationtypeid`, `businessunitid`, `appid`, `starttime`/`startdate` (ISO, default now), `endtime`/`enddate` (ISO), `ticketnumber`, `link`/`linkurl`, `isshopfloor` (default false), `employeesso` (comma-list allowed), `employeename` +**Purpose:** Create a notification; `endtime` auto-derived from type expiry rule when omitted. + +```bash +curl -X POST http://localhost:5001/api/notifications -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' -d '{"notification":"Line 2 press down","notificationtypeid":1,"isshopfloor":true,"ticketnumber":"INC0012345"}' +``` + +### PUT /api/notifications/<int:notification_id> + +**Auth:** JWT + permission `notifications.edit` +**Params:** body: `notification`/`message`, `notificationtypeid`, `businessunitid`, `appid`, `ticketnumber`, `link`/`linkurl`, `isactive`, `isshopfloor`, `employeesso`, `employeename`, `starttime`/`startdate`, `endtime`/`enddate` +**Purpose:** Update any notification field; empty `starttime` resets to now, empty `endtime` clears it. + +```bash +curl -X PUT http://localhost:5001/api/notifications/42 -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' -d '{"endtime":"2026-08-01T12:00:00Z","isactive":true}' +``` + +### DELETE /api/notifications/<int:notification_id> + +**Auth:** JWT + permission `notifications.delete` +**Params:** path: `notification_id` +**Purpose:** Soft-delete a notification (sets `isactive=false`, row kept). + +```bash +curl -X DELETE http://localhost:5001/api/notifications/42 -H 'Authorization: Bearer $TOKEN' +``` + +### GET /api/notifications/active + +**Auth:** none +**Params:** none +**Purpose:** Currently active notifications for display, including ones starting within a 10-day lookahead. + +```bash +curl http://localhost:5001/api/notifications/active +``` + +### GET /api/notifications/calendar + +**Auth:** none +**Params:** `start` (ISO), `end` (ISO); invalid dates silently ignored +**Purpose:** Active notifications as FullCalendar event objects for a date range. + +```bash +curl 'http://localhost:5001/api/notifications/calendar?start=2026-07-01T00:00:00Z&end=2026-07-31T23:59:59Z' +``` + +### GET /api/notifications/dashboard/summary + +**Auth:** none +**Params:** none +**Purpose:** Dashboard counts: total currently-active notifications plus active counts grouped by type/color. + +```bash +curl http://localhost:5001/api/notifications/dashboard/summary +``` + +### GET /api/notifications/employee/<sso> + +**Auth:** none +**Params:** path: `sso` (digits only, 400 otherwise) +**Purpose:** All active recognition-type notifications mentioning an employee SSO (exact or within comma-separated `employeesso` list). + +```bash +curl http://localhost:5001/api/notifications/employee/212345678 +``` + +### GET /api/notifications/shopfloor + +**Auth:** none +**Params:** `businessunit` (numeric BU id: returns that BU's plus null-BU notifications; omitted: null-BU only) +**Purpose:** Shopfloor TV feed: current cards (active now, or ended less than 30 min ago flagged resolved) plus upcoming (starts within 5 days); splits multi-employee cards per type config, resolves employee names/photos via employees plugin, returns `configversion` hash so kiosks reload on layout changes. + +```bash +curl 'http://localhost:5001/api/notifications/shopfloor?businessunit=3' +``` + +## plugin-printedparts + +### GET /api/printedparts/items +**Auth:** jwt + permission:printedparts.view +**Params:** query: page, per_page, search (matches itemcode/gagelabtag/itemname/itemdescription/binlocation), active (default true; 'false' includes retired), lowstock=true +**Purpose:** List printed items, paginated, with search and low-stock filter. + +```bash +curl -H "Authorization: Bearer $TOK" 'http://localhost:5001/api/printedparts/items?search=bracket&lowstock=true&page=1&per_page=25' +``` + +### GET /api/printedparts/items/ +**Auth:** jwt + permission:printedparts.view +**Params:** path: item_id +**Purpose:** Get one printed item plus its 25 most recent ledger transactions. + +```bash +curl -H "Authorization: Bearer $TOK" http://localhost:5001/api/printedparts/items/42 +``` + +### POST /api/printedparts/items +**Auth:** jwt + permission:printedparts.create +**Params:** json body: itemname (required), gagelabtag, itemdescription, lowstockthreshold (default from printedparts_default_threshold setting, fallback 5), binlocation, printnotes; quantityonhand starts at 0 +**Purpose:** Create a printed item; itemcode auto-minted from printedparts_code_prefix setting + row id; optional gagelabtag unique-checked (409 on clash). + +```bash +curl -X POST -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"itemname":"Fixture clip","gagelabtag":"WJRP0042","binlocation":"B3","lowstockthreshold":10}' http://localhost:5001/api/printedparts/items +``` + +### PUT /api/printedparts/items/ +**Auth:** jwt + permission:printedparts.edit +**Params:** path: item_id; json body: any of the editable fields; gagelabtag uppercased, empty string clears it +**Purpose:** Update catalog fields (itemname, itemdescription, lowstockthreshold, binlocation, printnotes, gagelabtag); rejects quantityonhand (ledger-managed) and duplicate gagelabtag (409). + +```bash +curl -X PUT -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"binlocation":"C1","lowstockthreshold":8}' http://localhost:5001/api/printedparts/items/42 +``` + +### DELETE /api/printedparts/items/ +**Auth:** jwt + permission:printedparts.delete +**Params:** path: item_id +**Purpose:** Soft-retire an item (isactive=false); ledger history preserved. + +```bash +curl -X DELETE -H "Authorization: Bearer $TOK" http://localhost:5001/api/printedparts/items/42 +``` + +### POST /api/printedparts/items//restore +**Auth:** jwt + permission:printedparts.delete +**Params:** path: item_id +**Purpose:** Un-retire a soft-deleted item (isactive=true); code, photo, history intact. + +```bash +curl -X POST -H "Authorization: Bearer $TOK" http://localhost:5001/api/printedparts/items/42/restore +``` + +### POST /api/printedparts/items//image +**Auth:** jwt + permission:printedparts.edit +**Params:** path: item_id; multipart/form-data: file= +**Purpose:** Upload or replace the item's photo (png/jpg/jpeg/gif/webp); old image files for the item are deleted first, imageurl updated. + +```bash +curl -X POST -H "Authorization: Bearer $TOK" -F 'file=@clip.jpg' http://localhost:5001/api/printedparts/items/42/image +``` + +### GET /api/printedparts/image/ +**Auth:** none +**Params:** path: filename (e.g. printeditem-42.jpg) +**Purpose:** Serve an uploaded item image from instance/printedpartsimages (public: fetched by `` tags on kiosk and lists). + +```bash +curl http://localhost:5001/api/printedparts/image/printeditem-42.jpg -o clip.jpg +``` + +### DELETE /api/printedparts/items//image +**Auth:** jwt + permission:printedparts.delete +**Params:** path: item_id +**Purpose:** Clear the item's imageurl; deletes the file on disk only if the URL is plugin-owned (starts with /api/printedparts/image/). + +```bash +curl -X DELETE -H "Authorization: Bearer $TOK" http://localhost:5001/api/printedparts/items/42/image +``` + +### POST /api/printedparts/items//restock +**Auth:** jwt + permission:printedparts.restock +**Params:** path: item_id; json body: quantity (positive int, required), badge (required, 422 BadgeError if unresolvable) +**Purpose:** Add freshly printed stock via a 'restock' ledger write (single-commit ledger + cached quantity); badge resolved server-side to sso/name; 404 if item inactive. + +```bash +curl -X POST -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"quantity":20,"badge":"123456789"}' http://localhost:5001/api/printedparts/items/42/restock +``` + +### POST /api/printedparts/items//adjust +**Auth:** jwt + permission:printedparts.restock +**Params:** path: item_id; json body: quantitychange (non-zero int, required), reason (required), badge (required) +**Purpose:** Correct the count (damage, recount) via an 'adjust' ledger write; reason mandatory; rejects driving stock below zero; fires low-stock alert on downward threshold crossing. + +```bash +curl -X POST -H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' -d '{"quantitychange":-3,"reason":"damaged in bin","badge":"123456789"}' http://localhost:5001/api/printedparts/items/42/adjust +``` + +### GET /api/printedparts/kiosk/item/ +**Auth:** none (deliberately open per decision record; kiosk cannot carry JWT) +**Params:** path: itemcode (e.g. WJRP0042, 3DP0042, 42, or WJRP0042|3) +**Purpose:** Item summary for a scanned bin barcode; resolves full code (itemcode or gagelabtag), 'TAG|rev' QR payloads (rev stripped), or bare keypad digits when the numeric tail uniquely matches one active item. + +```bash +curl http://localhost:5001/api/printedparts/kiosk/item/WJRP0042 +``` + +### POST /api/printedparts/kiosk/take +**Auth:** none (deliberately open per decision record) +**Params:** json body: itemcode (required, same resolution as kiosk/item), badge (required, resolved server-side, 422 on BadgeError), quantity (positive int <= on hand), revision (optional int) +**Purpose:** Kiosk checkout: decrement-only 'take' ledger write, badge-attributed, bounded by quantityonhand; records optional print-file revision (explicit field or 'TAG|rev' tail); triggers low-stock alert on threshold crossing; the product's only open write. + +```bash +curl -X POST -H 'Content-Type: application/json' -d '{"itemcode":"WJRP0042|3","badge":"123456789","quantity":2}' http://localhost:5001/api/printedparts/kiosk/take +``` + +### GET /api/printedparts/reports/stock +**Auth:** jwt-optional (like all product reports) +**Params:** query: format=csv for CSV download (printedparts-stock.csv), else JSON {columns, rows} +**Purpose:** Stock-level report for active items with low-stock flags and ledgerdelta (cache-vs-ledger reconcile; nonzero means a write bypassed the single-commit rule). + +```bash +curl 'http://localhost:5001/api/printedparts/reports/stock?format=csv' -o stock.csv +``` + +### GET /api/printedparts/reports/consumption +**Auth:** jwt-optional +**Params:** query: days (default 30; 0 or negative = all time), format=csv (printedparts-consumption.csv) +**Purpose:** Take-transactions aggregated per item (takes count + quantitytaken), sorted by quantity taken descending. + +```bash +curl 'http://localhost:5001/api/printedparts/reports/consumption?days=90' +``` + +### GET /api/printedparts/reports/by-person +**Auth:** jwt-optional +**Params:** query: days (default 30; 0 or negative = all time), format=csv (printedparts-by-person.csv) +**Purpose:** Take-transactions grouped by employee SSO (takes count + quantitytaken), sorted by quantity taken descending. + +```bash +curl 'http://localhost:5001/api/printedparts/reports/by-person?days=30&format=csv' -o by-person.csv +``` + +### GET /api/printedparts/items//files +**Auth:** jwt + permission:printedparts.view +**Params:** path: item_id +**Purpose:** List the item's print-file revision history, newest revision first. + +```bash +curl -H "Authorization: Bearer $TOK" http://localhost:5001/api/printedparts/items/42/files +``` + +### POST /api/printedparts/items//files +**Auth:** jwt + permission:printedparts.edit +**Params:** path: item_id; multipart/form-data: file= (required), note= (optional) +**Purpose:** Upload the next print-file revision (append-only, auto-numbered max+1); allowed ext: stl/3mf/gcode/gco/bgcode/step/stp/obj/amf; 100 MB cap; uploader recorded from JWT identity. + +```bash +curl -X POST -H "Authorization: Bearer $TOK" -F 'file=@clip-v2.stl' -F 'note=thicker wall' http://localhost:5001/api/printedparts/items/42/files +``` + +### GET /api/printedparts/files//download +**Auth:** jwt-optional +**Params:** path: file_id +**Purpose:** Download a print-file revision as an attachment under its original filename (jwt-optional so plain anchor downloads work). + +```bash +curl -OJ http://localhost:5001/api/printedparts/files/7/download +``` + +### DELETE /api/printedparts/files/ +**Auth:** jwt + permission:printedparts.delete +**Params:** path: file_id +**Purpose:** Delete a bad print-file revision (wrong file uploaded): removes the stored file and the DB record. + +```bash +curl -X DELETE -H "Authorization: Bearer $TOK" http://localhost:5001/api/printedparts/files/7 +``` + +## plugin-printers + +### GET /api/printers/types +**Auth:** jwt-optional +**Params:** page, per_page, active (default true; 'false' includes inactive), search +**Purpose:** List printer types, paginated, with active filter and name search. + +```bash +curl -H "Authorization: Bearer $TOKEN" 'http://localhost:5001/api/printers/types?search=laser&active=false' +``` + +### GET /api/printers/types/<type_id> +**Auth:** jwt-optional +**Params:** path: type_id (int) +**Purpose:** Get a single printer type by ID. + +```bash +curl http://localhost:5001/api/printers/types/3 +``` + +### POST /api/printers/types +**Auth:** permission:printers.create +**Params:** body: printertype (required), description, icon, color; reactivates a matching inactive type instead of 409 +**Purpose:** Create a new printer type (or reactivate an inactive duplicate). + +```bash +curl -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"printertype":"Label","color":"#00f"}' http://localhost:5001/api/printers/types +``` + +### PUT /api/printers/types/<type_id> +**Auth:** permission:printers.edit +**Params:** body: printertype, description, icon, color, isactive (any subset); 409 on name clash +**Purpose:** Update a printer type. + +```bash +curl -X PUT -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"isactive":false}' http://localhost:5001/api/printers/types/3 +``` + +### DELETE /api/printers/types/<type_id> +**Auth:** permission:printers.delete +**Params:** path: type_id; 409 if any printer still references the type +**Purpose:** Hard-delete a printer type when unused. + +```bash +curl -X DELETE -H "Authorization: Bearer $TOKEN" http://localhost:5001/api/printers/types/3 +``` + +### GET /api/printers/drivers +**Auth:** jwt-optional +**Params:** active (default true; 'false' includes inactive). Unpaginated. +**Purpose:** List printer driver packages (named SMB/HTTP links). + +```bash +curl 'http://localhost:5001/api/printers/drivers?active=false' +``` + +### POST /api/printers/drivers +**Auth:** permission:printers.create +**Params:** body: name (required), location (required), description, modelnumberid, isactive +**Purpose:** Create a driver entry. + +```bash +curl -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"name":"HP UPD","location":"\\\\fileserver\\drivers\\hpupd"}' http://localhost:5001/api/printers/drivers +``` + +### PUT /api/printers/drivers/<driver_id> +**Auth:** permission:printers.edit +**Params:** body: name, location, description, isactive, modelnumberid (any subset) +**Purpose:** Update a driver entry. + +```bash +curl -X PUT -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"isactive":false}' http://localhost:5001/api/printers/drivers/5 +``` + +### DELETE /api/printers/drivers/<driver_id> +**Auth:** permission:printers.delete +**Params:** path: driver_id +**Purpose:** Hard-delete a driver entry. + +```bash +curl -X DELETE -H "Authorization: Bearer $TOKEN" http://localhost:5001/api/printers/drivers/5 +``` + +### GET /api/printers +**Auth:** jwt-optional +**Params:** page, per_page, active, assetnumber (exact-match for import idempotency), search (assetnumber/name/serial/hostname/windowsname), typeid|type_id, vendorid|vendor_id, locationid|location_id, businessunitid|businessunit_id, sort (hostname|assetnumber|name), dir (asc|desc) +**Purpose:** List printers (joined Asset+Printer) with filters, search, sorting, pagination; each row includes primary IP. + +```bash +curl 'http://localhost:5001/api/printers?search=csf&typeid=2&sort=assetnumber&dir=desc' +``` + +### GET /api/printers/install-list +**Auth:** jwt-optional +**Params:** format=text for pipe-delimited installer variant (printerid|windowsname|vendorname|modelnumber|hostname|ipaddress|mapx|mapy); default JSON +**Purpose:** Flat unpaginated list of active network printers (with map positions) for the signed printer-installer EXE; replaces classic apiprinters.asp. + +```bash +curl 'http://localhost:5001/api/printers/install-list?format=text' +``` + +### GET /api/printers/install-batch +**Auth:** jwt-optional +**Params:** printerids (required, comma-separated printer IDs) +**Purpose:** Generate and download a Windows .bat that installs selected printers (HP/Xerox via universal PrinterInstaller.exe, per-printer .exe /SILENT, or flags manual). + +```bash +curl -OJ 'http://localhost:5001/api/printers/install-batch?printerids=1,2,3' +``` + +### GET /api/printers/pc-default +**Auth:** jwt-optional +**Params:** machine (PC asset number), format=text for pipe-delimited variant; returns {} / empty body if no default set +**Purpose:** Look up a PC's default printer via the defaultprinter asset relationship (parity with classic apipcdefaultprinter.asp); used by installer to preselect map hotspot. + +```bash +curl 'http://localhost:5001/api/printers/pc-default?machine=0421&format=text' +``` + +### GET /api/printers/<printer_id> +**Auth:** jwt-optional +**Params:** path: printer_id (int) +**Purpose:** Get one printer with full asset details, communications, and active drivers matching its model. + +```bash +curl http://localhost:5001/api/printers/17 +``` + +### GET /api/printers/by-asset/<asset_id> +**Auth:** jwt-optional +**Params:** path: asset_id (int) +**Purpose:** Get printer data keyed by core asset ID. + +```bash +curl http://localhost:5001/api/printers/by-asset/204 +``` + +### POST /api/printers +**Auth:** permission:printers.create +**Params:** body: assetnumber (required); name, serialnumber, gaugelabreference, maintenancereference, statusid, locationid, businessunitid, printertypeid, vendorid, modelnumberid, hostname, windowsname, sharename, iscsf, installpath, pin, iscolor, isduplex, isnetwork, mapx, mapy, notes, ipaddress (creates primary IP comm); X-Import-Mode header preserves legacy timestamps +**Purpose:** Create a printer (Asset + Printer extension + optional primary IP communication). + +```bash +curl -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"assetnumber":"PR-0042","hostname":"wjprn042","ipaddress":"10.1.2.42","printertypeid":1}' http://localhost:5001/api/printers +``` + +### PUT /api/printers/<printer_id> +**Auth:** permission:printers.edit +**Params:** body: any subset of asset fields (assetnumber, name, serialnumber, gaugelabreference, maintenancereference, statusid, locationid, businessunitid, mapx, mapy, notes, isactive) + printer fields (printertypeid, vendorid, modelnumberid, hostname, windowsname, sharename, iscsf, installpath, pin, iscolor, isduplex, isnetwork) + ipaddress (upserts/clears primary IP comm); 409 on assetnumber clash +**Purpose:** Update printer: asset, extension, and primary IP in one call. + +```bash +curl -X PUT -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"ipaddress":"10.1.2.99","iscolor":true}' http://localhost:5001/api/printers/17 +``` + +### DELETE /api/printers/<printer_id> +**Auth:** permission:printers.delete +**Params:** path: printer_id +**Purpose:** Soft-delete a printer (sets the underlying asset isactive=false). + +```bash +curl -X DELETE -H "Authorization: Bearer $TOKEN" http://localhost:5001/api/printers/17 +``` + +### GET /api/printers/<printer_id>/supplies +**Auth:** jwt-optional +**Params:** path: printer_id; needs an IP communication on the printer +**Purpose:** Real-time supply levels from Zabbix for one printer, annotated with status/color/part numbers; fails soft (empty supplies, pingstatus -1) when Zabbix is off/unreachable. + +```bash +curl -H "Authorization: Bearer $TOKEN" http://localhost:5001/api/printers/17/supplies +``` + +### GET /api/printers/lowsupplies +**Auth:** jwt-optional +**Params:** none (data cached 5 min) +**Purpose:** Fleet-wide report of printers with low/critical supply levels from Zabbix, with summary counts. + +```bash +curl http://localhost:5001/api/printers/lowsupplies +``` + +### GET /api/printers/lookup +**Auth:** jwt-optional +**Params:** ip or fqdn (one required; value matched against communication ipaddress) +**Purpose:** Find a printer by IP/FQDN (parity with classic printerlookup.asp; used by Zabbix to deep-link to a printer record). + +```bash +curl 'http://localhost:5001/api/printers/lookup?ip=10.1.2.42' +``` + +### POST /api/printers/supplies/refresh +**Auth:** permission:printers.create +**Params:** none +**Purpose:** Clear the cached Zabbix supply data so next reads pull fresh values (toner-report Refresh button). + +```bash +curl -X POST -H "Authorization: Bearer $TOKEN" http://localhost:5001/api/printers/supplies/refresh +``` + +### GET /api/printers/dashboard/summary +**Auth:** jwt-optional +**Params:** none +**Purpose:** Dashboard summary: total active printers, counts by type and vendor, low/critical supply counts (Zabbix, if reachable). + +```bash +curl http://localhost:5001/api/printers/dashboard/summary +``` + +### GET /api/printers/supplies/meta +**Auth:** jwt-optional +**Params:** none +**Purpose:** Allowed enum values for supplytype, color, and capacitytier (for the UI forms). + +```bash +curl http://localhost:5001/api/printers/supplies/meta +``` + +### GET /api/printers/models +**Auth:** jwt-optional +**Params:** page, per_page, search (modelnumber), vendorid|vendor_id, withsupplies=true (only models that already have supplies) +**Purpose:** List printer models with supply counts for the supply-management picker (restricted to models attached to printers or already carrying supplies). + +```bash +curl 'http://localhost:5001/api/printers/models?withsupplies=true&search=M404' +``` + +### GET /api/printers/models/<modelnumberid>/supplies +**Auth:** jwt-optional +**Params:** path: modelnumberid (int) +**Purpose:** List all active supplies (toner/drum/waste part numbers) mapped to a model. + +```bash +curl http://localhost:5001/api/printers/models/12/supplies +``` + +### POST /api/printers/models/<modelnumberid>/supplies +**Auth:** permission:printers.create +**Params:** body: partnumber (required), supplytype (default toner), color (default none), capacitytier (default standard), marketingname, pageyield, notes; 409 if part already mapped to model +**Purpose:** Add a supply part-number mapping to a model. + +```bash +curl -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"partnumber":"CF259A","supplytype":"toner","color":"black"}' http://localhost:5001/api/printers/models/12/supplies +``` + +### PUT /api/printers/supplies/<modelsupplyid> +**Auth:** permission:printers.edit +**Params:** body: any subset of supplytype, color, capacitytier, partnumber, marketingname, pageyield, notes; enum-validated, 409 on partnumber clash within model +**Purpose:** Update a model supply mapping. + +```bash +curl -X PUT -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"capacitytier":"high"}' http://localhost:5001/api/printers/supplies/44 +``` + +### DELETE /api/printers/supplies/<modelsupplyid> +**Auth:** permission:printers.delete +**Params:** path: modelsupplyid +**Purpose:** Hard-delete a model supply mapping. + +```bash +curl -X DELETE -H "Authorization: Bearer $TOKEN" http://localhost:5001/api/printers/supplies/44 +``` + +## plugin-slides + +### GET /api/slides/feed +**Auth:** none +**Params:** query: `surface=lobby|shopfloor` (default `lobby`; invalid values fall back to `lobby`). Returns `{success, surface, basepath, interval, slides:[{filename, seconds}]}`; only slides whose file exists on disk are listed. +**Purpose:** Public flat playlist for a surface (lobby/shopfloor). Raw `jsonify`, not the `success_response` envelope, so the screensaver parser works unchanged. + +```bash +curl 'http://localhost:5001/api/slides/feed?surface=shopfloor' +``` + +### GET /api/slides/img/<surface>/<path:filename> +**Auth:** none +**Params:** path: `surface` (`lobby|shopfloor`), `filename` (must equal its basename). +**Purpose:** Public serving of a single slide image from `instance/slides//`, with basename path-traversal guard. 404 on unknown surface, traversal attempt, or missing file. + +```bash +curl 'http://localhost:5001/api/slides/img/lobby/Slide1.png' -o Slide1.png +``` + +### GET /api/slides/<surface> +**Auth:** jwt + permission:slides.manage +**Params:** path: `surface` (`lobby|shopfloor`; else `VALIDATION_ERROR`). +**Purpose:** Admin list of a surface's slides (TvSlide rows ordered by `sortorder, slideid`, filtered to files present on disk), each with a `url` field for the img route. + +```bash +curl -H 'Authorization: Bearer $TOKEN' 'http://localhost:5001/api/slides/lobby' +``` + +### POST /api/slides/<surface>/upload +**Auth:** jwt + permission:slides.manage +**Params:** path: `surface`. multipart/form-data body: `files` (repeatable) or single `file`. Allowed extensions: `.jpg .jpeg .png .gif .bmp .webp`. Returns `{added:[names]}`. +**Purpose:** Upload one or more slide images. Non-image extensions skipped, names `secure_filename`'d and unique-renamed (`stem_N.ext`) on collision, appended after current max sortorder in natural filename order, `seconds=0` (surface default). + +```bash +curl -X POST -H 'Authorization: Bearer $TOKEN' -F 'files=@Slide1.png' -F 'files=@Slide2.png' 'http://localhost:5001/api/slides/lobby/upload' +``` + +### POST /api/slides/<surface>/order +**Auth:** jwt + permission:slides.manage +**Params:** path: `surface`. JSON body: `{order: [filename, ...]}`. +**Purpose:** Save play order: each filename in the `order` array gets `sortorder` set to its index; unknown filenames silently ignored. + +```bash +curl -X POST -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' -d '{"order":["Slide2.png","Slide1.png"]}' 'http://localhost:5001/api/slides/lobby/order' +``` + +### POST /api/slides/<surface>/delete +**Auth:** jwt + permission:slides.manage +**Params:** path: `surface`. JSON body: `{files: [filename, ...]}`. +**Purpose:** Delete named slides: removes file from disk (OSError swallowed) and the TvSlide row. Filenames reduced to basename first; returns count of DB rows removed. + +```bash +curl -X POST -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' -d '{"files":["Slide1.png"]}' 'http://localhost:5001/api/slides/shopfloor/delete' +``` + +### PATCH /api/slides/<surface>/<int:slideid> +**Auth:** jwt + permission:slides.manage +**Params:** path: `surface`, `slideid` (int). JSON body: `{seconds: int}` (non-int -> `VALIDATION_ERROR`). +**Purpose:** Update a single slide's per-slide duration. `seconds` clamped to `>=0` (`0` means use the 10s surface default); 404 if slide missing or belongs to a different surface. + +```bash +curl -X PATCH -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' -d '{"seconds":15}' 'http://localhost:5001/api/slides/lobby/12' +``` + +## plugin-usb + +USB device inventory with check-in/check-out tracking. Dual-mode backend: self-hosted tables or external `cmmc_usb` DB, selected by the `usb_directory_mode` setting. + +### GET /api/usb + +**Auth:** JWT optional +**Params:** query: `page`, `per_page`, `status` (`available` | `checkedout` | `retired`), `search` (matches `device_id` or `device_desc`) +**Purpose:** List USB devices with checkout status (paginated). + +```bash +curl 'http://localhost:5001/api/usb?status=available&search=kingston&page=1&per_page=25' -H 'Authorization: Bearer $JWT' +``` + +### GET /api/usb/<device_id> + +**Auth:** JWT optional +**Params:** path: `device_id` +**Purpose:** Get one device plus its last 20 check-in/out log rows. 404 if unknown. + +```bash +curl 'http://localhost:5001/api/usb/USB-0042' -H 'Authorization: Bearer $JWT' +``` + +### POST /api/usb + +**Auth:** JWT + permission `usb.create` +**Params:** body JSON: `device_id` (required), `device_desc`, `device_owner` (badge), `locker_location` +**Purpose:** Create a device; starts in checked-in status. 409 on duplicate `device_id`. + +```bash +curl -X POST 'http://localhost:5001/api/usb' -H 'Authorization: Bearer $JWT' -H 'Content-Type: application/json' -d '{"device_id":"USB-0042","device_desc":"Kingston 32GB","device_owner":"212345678","locker_location":"A3"}' +``` + +### PUT /api/usb/<device_id> + +**Auth:** JWT + permission `usb.edit` +**Params:** path: `device_id`; body JSON: any of `device_desc`, `device_owner`, `locker_location`, `status` +**Purpose:** Edit device fields. 404 if unknown. + +```bash +curl -X PUT 'http://localhost:5001/api/usb/USB-0042' -H 'Authorization: Bearer $JWT' -H 'Content-Type: application/json' -d '{"device_desc":"Kingston 64GB","locker_location":"B1"}' +``` + +### POST /api/usb/<device_id>/retire + +**Auth:** JWT + permission `usb.edit` +**Params:** path: `device_id`; no body +**Purpose:** Retire a device (sets status to `retired`). 404 if unknown. + +```bash +curl -X POST 'http://localhost:5001/api/usb/USB-0042/retire' -H 'Authorization: Bearer $JWT' +``` + +### POST /api/usb/<device_id>/checkout + +**Auth:** JWT + permission `usb.create` +**Params:** path: `device_id`; body JSON: `badge` (required), `locker_location` (optional, also updates the device) +**Purpose:** Check a device out to a badge. Writes a check-out log row, sets status to checked-out, auto-creates the user from the HR directory. 409 if already checked out. + +```bash +curl -X POST 'http://localhost:5001/api/usb/USB-0042/checkout' -H 'Authorization: Bearer $JWT' -H 'Content-Type: application/json' -d '{"badge":"212345678","locker_location":"A3"}' +``` + +### POST /api/usb/<device_id>/checkin + +**Auth:** JWT + permission `usb.create` +**Params:** path: `device_id`; body JSON: `badge` (required), `locker_location`, `sanitized` (bool/1/0), `scanned_viruses` (bool/1/0) +**Purpose:** Check a device back in. Writes a check-in log row with sanitized/virus-scan flags, sets status to checked-in. 400 if not currently checked out. + +```bash +curl -X POST 'http://localhost:5001/api/usb/USB-0042/checkin' -H 'Authorization: Bearer $JWT' -H 'Content-Type: application/json' -d '{"badge":"212345678","sanitized":true,"scanned_viruses":true}' +``` + +### GET /api/usb/<device_id>/history + +**Auth:** JWT optional +**Params:** path: `device_id`; query: `page`, `per_page` +**Purpose:** Paginated check-in/out log for one device, newest first. + +```bash +curl 'http://localhost:5001/api/usb/USB-0042/history?page=1&per_page=50' -H 'Authorization: Bearer $JWT' +``` + +### GET /api/usb/checkouts + +**Auth:** JWT optional +**Params:** query: `page`, `per_page`, `active=true` (only rows whose device is still checked out), `badge` (filter by `badge_number`) +**Purpose:** List check-out log rows (paginated), each with the device's current status joined in. + +```bash +curl 'http://localhost:5001/api/usb/checkouts?active=true&badge=212345678' -H 'Authorization: Bearer $JWT' +``` + +### GET /api/usb/checkouts/active + +**Auth:** JWT optional +**Params:** query: `badge` (filter by `badge_number`) +**Purpose:** Latest check-out log row for every currently checked-out device (non-paginated list). + +```bash +curl 'http://localhost:5001/api/usb/checkouts/active?badge=212345678' -H 'Authorization: Bearer $JWT' +``` + +## plugin-warranty + +### GET /api/warranty + +**Auth:** jwt-optional +**Params:** query: `active` (default true; 'false' includes inactive), `servicetag` (exact match, for idempotent import), `vendor` (exact match), `assetid` (int, filter to warranties covering that asset), `status` (post-filter on derived status: `expired|expiring|active|unknown`) +**Purpose:** List warranties with linked-asset summaries and derived status (batch asset fetch, ordered by enddate with null last). + +```bash +curl -H "Authorization: Bearer $TOKEN" 'http://localhost:5001/api/warranty?status=expiring&assetid=42' +``` + +### GET /api/warranty/asset/<int:assetid> + +**Auth:** jwt-optional +**Params:** path: `assetid` +**Purpose:** Active warranties covering one asset, for the asset-detail panel. + +```bash +curl -H "Authorization: Bearer $TOKEN" http://localhost:5001/api/warranty/asset/42 +``` + +### GET /api/warranty/<int:warrantyid> + +**Auth:** jwt-optional +**Params:** path: `warrantyid` +**Purpose:** Fetch a single warranty by id with asset summaries; 404 if missing. + +```bash +curl -H "Authorization: Bearer $TOKEN" http://localhost:5001/api/warranty/7 +``` + +### POST /api/warranty + +**Auth:** jwt + permission: `warranty.create` +**Params:** JSON body: `vendor` (required), `servicetag`, `provider` (default 'manual', lowercased), `servicelevel`, `startdate` (YYYY-MM-DD), `enddate` (YYYY-MM-DD), `notes`, `assetids` (list of asset ids to link) +**Purpose:** Create a warranty (vendor required) and optionally link it to assets; returns 201. + +```bash +curl -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"vendor":"Dell","servicetag":"ABC1234","enddate":"2027-06-30","assetids":[42]}' http://localhost:5001/api/warranty +``` + +### PUT /api/warranty/<int:warrantyid> + +**Auth:** jwt + permission: `warranty.edit` +**Params:** path: `warrantyid`; JSON body (all optional): `vendor`, `servicetag`, `provider`, `servicelevel`, `startdate`, `enddate`, `notes`, `isactive` (bool), `assetids` (full replacement list) +**Purpose:** Partial update of any warranty field (only keys present in body change), including isactive and replacing asset links via assetids. + +```bash +curl -X PUT -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"enddate":"2028-01-15","assetids":[42,43]}' http://localhost:5001/api/warranty/7 +``` + +### DELETE /api/warranty/<int:warrantyid> + +**Auth:** jwt + permission: `warranty.delete` +**Params:** path: `warrantyid` +**Purpose:** Hard-delete a warranty (and its asset links); 404 if missing. + +```bash +curl -X DELETE -H "Authorization: Bearer $TOKEN" http://localhost:5001/api/warranty/7 +``` + +### POST /api/warranty/<int:warrantyid>/refresh + +**Auth:** jwt + permission: `warranty.edit` +**Params:** path: `warrantyid`; no body +**Purpose:** Re-query the warranty's provider (dell etc.) by service tag and update servicelevel/startdate/enddate + lastcheckeddate; 400 if provider is manual or not configured. + +```bash +curl -X POST -H "Authorization: Bearer $TOKEN" http://localhost:5001/api/warranty/7/refresh +``` + +### POST /api/warranty/sync/dell + +**Auth:** jwt + permission: `warranty.edit` +**Params:** query: `all` (default false; 'true' re-checks assets that already have a dated warranty); no body +**Purpose:** Bulk Dell sync: look up serials of active assets lacking a dated warranty via Dell bulk_lookup, creating or updating (canonicalizing to provider dell) warranties; returns candidates/tags/matched/created/updated counts. + +```bash +curl -X POST -H "Authorization: Bearer $TOKEN" 'http://localhost:5001/api/warranty/sync/dell?all=true' +``` + +### GET /api/warranty/report + +**Auth:** jwt-optional +**Params:** none +**Purpose:** Report for the Reports hub: active warranties bucketed by derived status (expired/expiring/active/unknown) with per-bucket counts and full lists. + +```bash +curl -H "Authorization: Bearer $TOKEN" http://localhost:5001/api/warranty/report +``` diff --git a/docs/PROJECT-REVIEW.md b/docs/PROJECT-REVIEW.md new file mode 100644 index 0000000..16e2073 --- /dev/null +++ b/docs/PROJECT-REVIEW.md @@ -0,0 +1,56 @@ +# ShopDB Flask - Project Health Review +As of HEAD `ecf4ef6` (2026-07-30), product `__version__ 0.7.0`, contract `__contract_version__ 0.15.0` (verified in `shopdb/__init__.py`). + +## 1. Executive Summary + +**Overall: healthy engineering, drifting focus.** The stated product vision ("plugin system is the product," CLAUDE.md) is delivered: all 7 refactor phases are done, 13 bundled plugins are contract-compliant, the per-plugin migration regime (ADR-008) is live and exercised (geenforce is at `geenforce0002blobs`, proving the post-cutover chain works in anger), and CI enforces naming, contract, and real-MySQL migration idempotency. Test count grew 1077 -> 1159 since the last CLAUDE.md snapshot. + +The concern is not quality but trajectory. CLAUDE.md and ROADMAP.md both name the last big milestone before 1.0 as "legacy-ASP data import + production pilot." The loader is built and VM-validated (16 stages, `scripts/site_imports/wjf/`), but the prod run has not happened, and ~35 of the last 60 commits went to the GE-Enforce HTTPS cutover instead. That work is legitimate and high-value, but it is feature/fleet work on one plugin, and it has accumulated two process debts that violate the project's own discipline: the cutover playbook (`docs/geenforce-api-cutover.md`) is the only dirty file in the repo and is **untracked**, and the `prod-patch-geenforce` robocopy fast-path can leave prod ahead of git. + +**Verdict: on track against standards, behind against goals.** The 1.0 gate has four items; only one is arguably done, and the roadmap doc does not know it. + +## 2. Standards Compliance + +| Rule | Status | Evidence | +|---|---|---| +| Naming (tables/columns/vars, CONTRIBUTING.md) | **MET** | `scripts/check-naming-and-style.sh` present and executable; dedicated `naming` job in `.gitea/workflows/ci.yml`. One borderline: `asset.py:175` documents a derived API key `location_name` with an underscore - not a DB-mirrored column so likely legal, but worth a glance since "response keys match column names exactly" is the spirit of the rule. | +| Plugin contract (manifest.json, BasePlugin, `shopdb.api` only) | **MET** | 13 `plugins/*/manifest.json` verified; contract test suites in `tests/`; contract bumped correctly to 0.15.0 for the geenforce resource-scope fetch tokens (75386d2), per ADR-002 discipline. | +| Migration ownership (ADR-008) | **MET** | `PLUGIN_TABLE_OWNERS` registry tested by `tests/test_plugin_migrations.py` (`EXPECTED_HEAD_REVISION` lines 47-51); `migrations-mysql` CI job does fresh utf8mb4 MySQL 8 upgrade + all plugin chains + second-upgrade-no-op assertion. The geenforce `0002blobs` revision shows the per-plugin chain is being used as designed, not just anchored. | +| Versioning/release discipline (ADR-007) | **AT RISK** | Tags through v0.7.0 exist and contract bumps are disciplined, but the documentation half of the procedure has drifted - see Gaps 3. | +| ADRs canonical, new priorities get an ADR | **AT RISK** | 14 ADRs present. But lean per-site builds are half-shipped (ADR-014 ACCEPTED and implemented; `default_enabled: false` on 5 plugins) while ADR-013, which defines the catalog/tiers/signed-artifact model those builds imply, is still PROPOSED. The GE-Enforce HTTPS cutover itself - a major architectural shift off the SMB share - lives in an untracked doc, not an ADR or ADR-012 amendment. | +| Style (plain ASCII, no emojis, comment discipline) | **MET** | Enforced by the same pre-commit hook + CI naming job. | +| Everything in git / repo as source of truth | **VIOLATED** | `docs/geenforce-api-cutover.md` untracked (only dirty file, verified `git status`); `prod-patch-geenforce` robocopy path acknowledged in-doc as leaving prod ahead of git. | + +## 3. Roadmap Status + +**Done:** Phases 0-6 (contract lock through multi-site distribution, tags v0.5.0-v0.7.0). Legacy import machinery complete: `docs/IMPORT-API.md` contract, 16-stage wjf loader VM-validated. Air-gapped deploy kit (6534590). + +**1.0 must-haves (ROADMAP.md), honestly scored:** + +1. *Asset model fully wired* - **appears DONE but unrecorded.** `Asset.mapx` (`shopdb/core/models/asset.py:121`), `inheritsposition` (`relationship.py:132`), and propagation logic (`relationship.py`, `core/api/assets.py`, `cli/__init__.py`) are all in code. ROADMAP still lists this as outstanding. Verify the ADR-001 contract tests cover it, then strike it. +2. *Equipment data migration one-shot* - **NOT DONE.** `scripts/migration/` contains only `fix_legacy_schema.sql`, `one-offs/`, and a README. No equipment script. +3. *Printers legacy-table cleanup* - **NOT DONE.** Recent printers commits (0d40780..c075658) are installer/feature work, not retirement. +4. *External plugin UI packaging* - **NOT DONE**, and gate criterion 3 (one external plugin built end-to-end) has no evidence. + +**In-flight:** GE-Enforce HTTPS cutover dominates (~35/60 recent commits). Per the cutover doc's own section 12: only displays/kiosks are on the API; cmm/collections/keyence/genspect/heattreat/partmarker/common fleet still enforce from the SFLD SMB share; loggedinuser resolution unwired; registry cleanup pending; 3DPrintRoom route is a placeholder. Secondary streams: printers install-batch, applications notes, server reclassification, TV dashboard. + +**Pace/scope health:** Velocity is high and test coverage tracks the work (17 of ~29 plugin test files are geenforce). But the project has been at 0.7.0 with "prod pilot is the last big milestone" as the stated goal since mid-July, while shipping ~185 commits of plugin-feature work. That is a real product being used - good - but the 1.0 gate is not moving, and a half-migrated fleet (API for displays, SMB for everything else) is the worst place to pause the cutover. + +## 4. Gaps and Risks + +1. **Untracked cutover playbook** (`docs/geenforce-api-cutover.md`). The single most valuable in-flight document is one `rm` away from gone, and invisible to any other machine or contributor. +2. **Prod-ahead-of-git debt.** The `prod-patch-geenforce` fast-path means production behavior may not be reproducible from any commit. This directly undermines ADR-012's "engine is source of truth" and the release discipline of ADR-007. +3. **Documentation drift, three concrete instances (all verified):** ROADMAP.md header says contract 0.13.0 (actual 0.15.0); CLAUDE.md says 1077 tests (actual 1159 collected) and claims a "lean-build" CI job that does not exist in `.gitea/workflows/ci.yml` (jobs: backend, naming, frontend, migrations-mysql - lean coverage is folded into pytest via `tests/test_lean_build_guards.py`). Also `.github/workflows/ci.yml` differs from the gitea workflow - one of them is stale. +4. **Split-brain fleet enforcement.** Displays/kiosks on the API, the rest of the fleet on the SMB share, with staged-but-unpushed manifest fixes elsewhere (MTConnect v1 stranding). Two delivery mechanisms means two failure modes and doubles the audit surface until the cutover finishes. +5. **1.0 gate criterion 4 unproven:** `docs/DEPLOY.md` has not been validated by an actual fresh-host prod deploy. The air-gapped kit exists; the pilot does not. +6. **ADR-013 limbo:** lean builds shipped under ADR-014 while the catalog/signing model that makes external distribution safe remains PROPOSED. Fine short-term, but gate criterion 3 (external plugin) will force the question. + +## 5. Prioritized Recommendations + +1. **Commit `docs/geenforce-api-cutover.md` today.** Zero-cost, eliminates the worst single-point-of-loss risk. +2. **Reconcile prod-patched geenforce files back into git** and gate or retire the robocopy fast-path. Until prod == some tag, ADR-007 is fiction for this plugin. +3. **One doc-sync pass (30 min):** ROADMAP header to 0.15.0, CLAUDE.md test count and CI job list, strike must-have (a) if contract tests confirm the Asset wiring, delete or sync the stale `.github` workflow. +4. **Finish the cutover or park it cleanly.** Either drive the remaining fleet groups onto the API per section 12, or write down the frozen state as an ADR-012 amendment so the split-brain period is a documented decision, not drift. +5. **Schedule the prodscratch import run and prod pilot.** This is the actual 1.0 milestone and everything is built for it; it validates DEPLOY.md (gate 4) for free. +6. **Pair the equipment one-shot migration with printers retirement** (must-haves b and c) - they are coordinated by design; doing them together avoids touching the legacy tables twice. +7. **Decide ADR-013** before building the external-plugin end-to-end proof (gate 3); the geenforce client work is the natural seed for that external plugin. diff --git a/docs/WIKI-UPDATE-PLAN.md b/docs/WIKI-UPDATE-PLAN.md new file mode 100644 index 0000000..b338607 --- /dev/null +++ b/docs/WIKI-UPDATE-PLAN.md @@ -0,0 +1,191 @@ +# WIKI UPDATE PLAN - shopdb-flask docs (as of HEAD ecf4ef6, 2026-07-30) + +Priority order: items 1-4 are stale-on-shipped-features (fix first), 5-6 are new pages, 7-10 are minor rows/notes, 11 is structural. + +--- + +## 1. docs/geenforce-api-cutover.md - UPDATE (and COMMIT - it is untracked) + +**Action: update + `git add`.** This is the most valuable in-flight doc and the only dirty file in the repo. Committing it is step zero of this plan. + +**Changes:** + +a) Section 5, subsection "The display-type.txt dispatcher pattern" (line ~338): it predates commit b22701a. The dispatcher is no longer file-first; it is server-first. Rewrite the subsection opening to: + +> ### The display dispatcher: server-resolved role, file fallback +> +> The inline dispatcher payload (built by `plugins/geenforce/seed_display_scope.py`) resolves what the display should show in two steps: +> +> 1. **Server (authoritative):** `GET $KioskBaseUrl/api/dashboarddefaults/display-role?fqdn=`. This is a PUBLIC endpoint (no token). The server matches the FQDN against the `dashboarddefaults` table (IP fallback) and returns `{role, path, businessunitid, businessunit}`. Roles: `dashboard`, `lobby`, `partskiosk`. Changing a display's job is now a server-side edit; no touch on the PC. +> 2. **Fallback (offline, or unmapped):** the local `C:\Enrollment\display-type.txt` value against the `DISPLAY_TYPE_TARGETS` map baked into the script. If neither resolves, the dispatcher logs and configures nothing. +> +> The FQDN is derived from the hostname plus `DisplayFqdnDomain` (registry) or the built-in default domain. `DetectionMethod = Always`, but the script is idempotent: it rewrites the all-users Startup shortcut only when the resolved target changed. + +b) Section 12 (Open items): add a line that the robocopy `prod-patch-geenforce` reconciliation debt now includes this very doc being untracked - remove that line after commit. + +c) Add a "See also" block near the top linking `GE-ENFORCE-DISPLAY.md`, `GE-ENFORCE-CLIENT.md`, `GE-ENFORCE-DEPLOY.md`, and (new) `API-REFERENCE.md`. + +--- + +## 2. docs/GE-ENFORCE-DISPLAY.md - UPDATE + +**Action: update.** One stale claim plus one missing section. + +**Changes:** + +a) The "display-type -> target map" section (lines ~30, ~61-75) presents `display-type.txt` as THE role source. Retitle the section to **"Role resolution: server first, display-type.txt fallback"** and insert before the table: + +> The dispatcher first asks the server: `GET /api/dashboarddefaults/display-role?fqdn=` (public, unauthenticated). A row in `dashboarddefaults` keyed by the display's FQDN (IP fallback) wins and returns the role and frontend path directly. Only when the server is unreachable or has no mapping does the dispatcher fall back to the local `display-type.txt` map below. To repurpose a display, edit its `dashboarddefaults` row; the change takes effect on the next enforce cycle. + +Keep the existing table, but relabel its caption "fallback map (local file)". + +b) Add a new short section **"Dashboard-defaults FQDN keying"** (this is the home for missing-doc item [D]): + +> ### Dashboard-defaults FQDN keying +> +> `dashboarddefaults` rows were historically keyed by IP. Migration `7d31_dashboarddefault_fqdn` added an `fqdn` column; resolution is now FQDN-first with IP as fallback (`_resolve_default` in `shopdb/core/api/dashboarddefaults.py`). FQDNs are stored lowercase. This survives DHCP churn on kiosk subnets. `POST /api/dashboarddefaults` accepts `fqdn`, `ipaddress`, `displayrole` (`dashboard`|`lobby`|`partskiosk`), `displaypath`, `businessunitid`. Two public read endpoints consume it: `/api/dashboarddefaults/display-role` (dispatcher) and `/api/dashboarddefaults/visitor-location` (lobby business-unit lookup). `plugins/geenforce/seed_display_scope.py` builds the client-side lookup (line ~130). + +--- + +## 3. docs/GE-ENFORCE.md - UPDATE + +**Action: update lines 288-291.** The sentence "Until that cutover, the client only REPORTS; the manifest still comes from the share via Export to Share (4.2)" is false for the displays cohort. Replace with: + +> The cutover from share-sourced to shopdb-sourced manifests is per PC type. The **displays/kiosks cohort has cut over**: share-less display PCs fetch their manifest and payloads entirely over HTTPS (see `docs/geenforce-api-cutover.md` and `docs/GE-ENFORCE-DISPLAY.md`). All other fleet PC types (cmm, collections, keyence, genspect, heattreat, partmarker, nocollections, common) still enforce from the SFLD SMB share via Export to Share (4.2) and only REPORT to shopdb. The playbook for moving the next PC type is `geenforce-api-cutover.md` section 11. + +--- + +## 4. docs/PILOT-DEPLOY.md - UPDATE + +**Action: update.** Two changes: + +a) Line 88 asset-count table predates the servers-to-network reclassify [F]. The writer must re-run the counts on the current prodscratch after the reclassify script; do not hand-edit numbers. Replace the row with re-measured values and a footnote: + +> Counts taken AFTER `scripts/reclassify_servers_to_network.py --commit`. Servers imported as computers are re-pointed to network devices, so the computer count drops and network rises by the same amount versus a raw import. + +b) Add a new numbered step to the import/verify flow, immediately after the import stages complete and before the parallel-validation counts: + +> ### Reclassify servers into network devices +> +> The classic DB stored servers as PCs, so the import lands them as `computer` assets. Re-point them in place: +> +> ``` +> DATABASE_URL=... venv/bin/python scripts/reclassify_servers_to_network.py # dry run, prints matches +> DATABASE_URL=... venv/bin/python scripts/reclassify_servers_to_network.py --commit # apply +> # match on an exact computer type instead of the SVR- name prefix: +> ... --type "Server" --commit +> ``` +> +> The assetid does not change: communications, relationships, map position, and audit history carry over. Only the extension row is swapped (computers -> networkdevices) and the asset type flipped; reclassified devices get the `Server` networkdevicetype. Run the dry run, eyeball the list, then commit. Re-running is safe (already-moved assets no longer match). + +--- + +## 5. NEW: docs/PRINTER-INSTALLER.md - CREATE + +**Action: create.** Missing-doc item [B]. This is a consumed client contract (GE-ENFORCE-DISPLAY.md:82 lists "printer map" as a fleet manifest entry) with zero coverage. Model it on COLLECTOR-INTEGRATION.md (contract doc for an external client). Draft: + +> # Printer installer map and install endpoints +> +> Replaces the classic apiprinters.asp / apipcdefaultprinter.asp / installprinter.asp contract. Shopfloor 2.0 PCs cannot run unsigned .bat maps, so a signed installer EXE (and the public web map page) drives installs from three endpoints in the printers plugin (`plugins/printers/api/asset_routes.py`). All three are `@jwt_required(optional=True)`: anonymous fleet clients work, a logged-in browser also works. +> +> ## The public map page +> +> `PrinterInstallerMap` is a public (no-login) frontend page: the floor map with printer hotspots at `mapx`/`mapy`. The user clicks printers, the page requests an install batch. The PC's default printer is preselected via `pc-default`. +> +> ## GET /api/printers/install-list +> +> Flat, unpaginated list of active NETWORK printers (must have a hostname or a non-USB IP; USB-only printers are excluded). Fields per row: `printerid`, `name`, `machinenumber`, `windowsname`, `sharename`, `hostname`, `ipaddress`, `vendorname`, `modelnumber`, `installpath`, `iscsf`, `locationname`, `mapx`, `mapy`. +> `?format=text` returns a pipe-delimited line per printer (fixed field order: printerid|windowsname|vendorname|modelnumber|hostname|ipaddress|mapx|mapy) so the Inno/Pascal installer does a split() instead of parsing JSON. +> +> ## GET /api/printers/pc-default?machine=NNNN +> +> The PC's default printer by machine (asset) number, persisted at PXE enrollment. Resolved via the `defaultprinter` asset relationship (PC asset -> printer asset). Returns `{printerid, windowsname}`, or `{}` when unknown/none. `?format=text` returns one pipe line, or an EMPTY body for none. +> +> ## GET /api/printers/install-batch?printerids=1,2,3 +> +> Returns a self-deleting Windows .bat installing the selected printers, grouped like classic installprinter.asp: +> - HP / Xerox: one universal `PrinterInstaller.exe /PRINTER="a,b,c"` call +> - printers with a `.exe` installpath: PowerShell Invoke-WebRequest download (caller's Windows creds, site base URL + IIS `/installers` folder) then run `/SILENT` +> - no installpath or `.zip`: listed as manual install +> `printerids` is required (comma-separated, non-numeric tokens ignored). +> +> ## Fleet wiring +> +> The `common` scope's `printer map` manifest entry (see GE-ENFORCE-DISPLAY.md) lays down the signed installer. Install name preference: `windowsname`, else `sharename`, else asset name/number. + +Cross-link it from PLUGINS.md printers row (item 7) and GE-ENFORCE-DISPLAY.md:82. + +--- + +## 6. docs/GE-ENFORCE-DEPLOY.md - UPDATE (minor) + +**Action: update.** Add one cross-link paragraph near the ShopdbUrl/ApiToken registry contract section: + +> For PC types that have cut over to HTTPS manifest delivery (currently displays/kiosks), the full server-side setup, auth model (IP allowlist vs ApiToken), and per-PC-type cutover playbook live in `geenforce-api-cutover.md`. This doc covers what gets laid on the PC; that doc covers where the manifest comes from. + +--- + +## 7. docs/PLUGINS.md - UPDATE (two rows) + +**Action: update.** + +Line 13, printers row, new text: + +> | `printers` | Network and shop-floor printers | Public installer map page + fleet install contract (`/api/printers/install-list`, `/pc-default`, `/install-batch`, see PRINTER-INSTALLER.md). Optional Zabbix integration for supply tracking. Legacy `PrinterData` retiring per ADR-001. | + +Line 22, slides row, new text: + +> | `slides` | Slides for the lobby display and shop-floor screensaver | Upload / reorder / delete per surface. Management gated on the `slides.manage` permission, grantable to non-admin curators; display routes are public. | + +Also add a short **"Plugin permissions"** note after the table (first place any plugin permission is documented): + +> Plugins may register their own permissions (e.g. `slides.manage`). Admins implicitly hold them; grant them to specific roles/users to delegate curation without admin. Each plugin's registered permissions appear in its `plugin.py` `get_permissions()`. + +--- + +## 8. docs/IMPORT-API.md - UPDATE (one row) + +**Action: update line 237.** Replace the dashboarddefaults row: + +> | `dashboarddefaults` | `POST /api/dashboarddefaults` | `fqdn` (preferred key, stored lowercase), `ipaddress` (fallback key), `displayrole` (`dashboard`/`lobby`/`partskiosk`), `displaypath`, `businessunitid` (remapped), `description` | `fqdn`, else `ipaddress` | + +Add a one-line note under the table: "Resolution at runtime is FQDN-first with IP fallback (migration `7d31_dashboarddefault_fqdn`); import both when the legacy source has them." + +--- + +## 9. docs/CONFIG.md - UPDATE (one note) + +**Action: update.** In the "search (dynamic)" section (line ~316), append: + +> Search terms are matched word-wise: a multi-word query returns rows containing EVERY word, each word anywhere in the searched fields, in any order ("CSF Roles" matches a row with "CSF" and "Roles" in different columns). Quoting does not force a contiguous phrase. + +No full doc for [E]; this note plus a CHANGELOG line covers it. + +--- + +## 10. docs/IMPORT-ADOPTION.md - UPDATE (optional, one line) + +**Action: update.** In the per-site loader section, add: "Post-import fixups that re-point existing assets (example: `scripts/reclassify_servers_to_network.py`, servers imported as PCs moved to network devices in place) belong in the site loader's verify stage, not in the stable API layer." + +--- + +## 11. NEW: docs/API-REFERENCE.md - CREATE (proposal) + +**Where:** `docs/API-REFERENCE.md`, wiki-bound alongside the rest. + +**Scope:** an index, not a spec. One table per audience, each row = endpoint, auth, one-line purpose, link to the owning contract doc. Sections: + +1. **Fleet/client contracts (unauthenticated or token)** - geenforce fetch/report + `/payload/` (-> GE-ENFORCE-CLIENT.md, geenforce-api-cutover.md), collector (-> COLLECTOR-INTEGRATION.md), printers install trio (-> PRINTER-INSTALLER.md), `dashboarddefaults/display-role` + `visitor-location` (-> GE-ENFORCE-DISPLAY.md). +2. **Import API** - pointer to IMPORT-API.md, do not duplicate. +3. **Core UI API** - one line: JWT-authenticated, versioned by plugin contract (0.15.0), see CONTRACT-STABILITY.md; enumerate only the public/optional-auth endpoints since those are the site-exposure surface a firewall reviewer asks about. + +**Linked from:** docs/adr/README.md sibling index if one exists, PLUGINS.md header, DEPLOY.md security section (public-endpoint inventory is exactly what a deploy reviewer needs), and each contract doc's "See also". Rule to state at top: detailed request/response shapes live in the contract docs; this page only answers "what endpoints exist, who calls them, what auth". + +--- + +## Out of scope for the wiki but flagged to the maintainer + +- CLAUDE.md drift (test count 1077 vs 1159, phantom "lean-build job", 2026-07-13 "Current state" missing the entire HTTPS-cutover arc) is repo-doc, not wiki, but should ride the same commit. +- No retirements: every existing page stays. ADR-005 must NOT be edited for [F]; ADRs are immutable and its "reclassification" is equipment/measuringtools only. The reclassify script is operational, documented in PILOT-DEPLOY.md (item 4). + +Key source files for the writer: `/home/camp/projects/shopdb-flask/plugins/printers/api/asset_routes.py` (install endpoints, lines 311/432/568), `/home/camp/projects/shopdb-flask/shopdb/core/api/dashboarddefaults.py` (display-role, line 72), `/home/camp/projects/shopdb-flask/plugins/geenforce/seed_display_scope.py` (dispatcher, line ~117-151), `/home/camp/projects/shopdb-flask/scripts/reclassify_servers_to_network.py` (usage in module docstring), `/home/camp/projects/shopdb-flask/shopdb/core/api/search.py` (`_word_match`, lines 27-42), `/home/camp/projects/shopdb-flask/plugins/slides/plugin.py` (slides.manage, line 66).