From 802256f929c99f2b51dec444a2f565142985586c Mon Sep 17 00:00:00 2001 From: cproudlock Date: Thu, 30 Jul 2026 16:05:21 -0400 Subject: [PATCH] docs: wiki update for API docs, printer installer, geenforce cutover, timezone Execute WIKI-UPDATE-PLAN.md (14 items): - NEW docs/PRINTER-INSTALLER.md: install-list / pc-default / install-batch contract + public installer map page. - NEW-shape docs/API-REFERENCE.md: index + pointer to the live generated docs (/api/docs Redoc, openapi.json, llms.txt, MCP), replacing a stale full dump. - geenforce cutover + GE-ENFORCE-DISPLAY/CLIENT/DEPLOY: server-first display dispatcher (display-role by FQDN, display-type.txt fallback), dashboarddefaults FQDN keying, legacy kiosk autostart self-heal (Wow6432Node), per-PC-type cutover status. - PLUGINS: printers/slides rows + plugin-permissions note (slides.manage). - IMPORT-API: dashboarddefaults FQDN-first keying. - CONFIG: word-wise search, site_timezone setting. - PILOT-DEPLOY: servers-to-network reclassify step. IMPORT-ADOPTION: fixup note. - CLAUDE.md: test count 1077->1159, HTTPS-cutover state. CHANGELOG: timezone + kiosk-autostart fixes, site_timezone setting. --- CHANGELOG.md | 17 + CLAUDE.md | 3 +- docs/API-REFERENCE.md | 3444 ++------------------------------- docs/CONFIG.md | 13 + docs/GE-ENFORCE-DEPLOY.md | 6 + docs/GE-ENFORCE-DISPLAY.md | 45 +- docs/GE-ENFORCE.md | 11 +- docs/IMPORT-ADOPTION.md | 5 + docs/IMPORT-API.md | 5 +- docs/PILOT-DEPLOY.md | 28 +- docs/PLUGINS.md | 11 +- docs/PRINTER-INSTALLER.md | 104 + docs/WIKI-UPDATE-PLAN.md | 34 + docs/geenforce-api-cutover.md | 53 +- 14 files changed, 450 insertions(+), 3329 deletions(-) create mode 100644 docs/PRINTER-INSTALLER.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 45001d7..0393589 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,14 @@ ADR-007 and ADR-002. ## [Unreleased] +### Added + +- Configurable site timezone: a `site_timezone` site setting (default + `America/New_York`, public-readable), editable in Settings > Site > + Localization. Notification start/end times are entered and displayed in this + zone, and daily-reset notification expiry is computed in it. A shared + `frontend/src/utils/datetime.js` (Intl-based, DST-safe) does the conversion. + ### Changed - Asset detail pages (machines, PCs, printers, network devices, measuring @@ -24,6 +32,15 @@ ADR-007 and ADR-002. ### Fixed +- Notification start/end times were off by the timezone offset (a 2:34 PM entry + displayed as 6:34 PM). Times are now stored UTC and shown/entered in the site + timezone; the calendar keys all-day events off the site-local day. +- Kiosk displays showed a white screen on login: a legacy 32-bit kiosk + installer's autostart kept relaunching Edge at a now-dead URL. The install's + HKLM Run value was WOW64-redirected into `SOFTWARE\Wow6432Node\...\Run` and + survived earlier cleanup. The `gea-shopfloor-display` dispatcher now purges + the legacy autostarts every enforce cycle across both registry views, all + user hives, Run/RunOnce/policy-Run, and every Startup folder. - List pages keep the current page (and search term) in the URL query, so paging to page 9, opening an item, and hitting browser Back returns to page 9 instead of resetting to page 1. Applies to all 18 list views via a shared diff --git a/CLAUDE.md b/CLAUDE.md index 571b3cb..846c7fd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,7 +44,8 @@ Refactor phases 0-5 landed; phase 6 (multi-site distribution readiness) largely ### Active state -- 1077 tests, naming/style check green, Gitea Actions CI (backend + naming + frontend build + a lean-build job + a migrations-mysql job that runs the real fresh upgrade on utf8mb4 MySQL 8) +- 1159 tests, naming/style check green, Gitea Actions CI (backend + naming + frontend build + a lean-build job + a migrations-mysql job that runs the real fresh upgrade on utf8mb4 MySQL 8) +- GE-Enforce HTTPS cutover: the displays/kiosks cohort now fetches manifest + inline payloads entirely over HTTPS (share-less); the `gea-shopfloor-display` scope is authored in code (`plugins/geenforce/seed_display_scope.py`) and published via `seed_display_scope(publish=True)`. Other fleet PC types still enforce from the SMB share and only report. See `docs/geenforce-api-cutover.md`. - `__contract_version__` at 0.15.0 (0.12.0 mailer, 0.13.0 User/Role, 0.14.0 send_webhook, 0.15.0 authorized_service_token) (product `__version__` 0.7.0, tags v0.5.0/v0.6.0/v0.7.0 - distinct series, ADR-007) - 13 bundled plugins all satisfy contract: computers, employees, geenforce, knowledgebase, machines, measuringtools, network, notifications, printedparts, printers, slides, usb, warranty - Core Alembic chain: baseline `68b3947ae14f` -> head `7d26_settings_description_text` (33 core migrations). Each plugin owns its own chain (ADR-008); deploy runs `flask db upgrade` then `flask plugin upgrade-all`. Reproducible + idempotent from empty (env.py relaxes session sql_mode so the chain runs on strict MySQL 8). diff --git a/docs/API-REFERENCE.md b/docs/API-REFERENCE.md index 97cae40..e2c44de 100644 --- a/docs/API-REFERENCE.md +++ b/docs/API-REFERENCE.md @@ -1,3311 +1,133 @@ -# 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 -``` +# API reference (index) + +This page is an index and a pointer, not a full specification. It answers three +questions: what endpoints exist, who calls them, and what auth they require. The +detailed request and response shapes live in the live generated docs and in the +per-surface contract docs linked from each table below. + +## Live and generated docs + +The repo ships hosted, generated API docs. Start there: + +- **Interactive spec:** `GET /api/docs` - a self-hosted Redoc page over the + generated OpenAPI spec. The Redoc bundle is vendored under + `shopdb/core/api/staticdocs/`, so it renders fully offline on the air-gapped + prod box (no CDN). +- **Raw spec:** `GET /api/docs/openapi.json` - OpenAPI 3.1, roughly 238 paths and + 362 operations. Generated by `scripts/gen_openapi.py` from + `docs/api-inventory.json`; regenerate after any API change. +- **LLM / agent entry point:** `GET /api/docs/llms.txt` - a concise API guide + following the llms.txt convention, plus a read-only MCP server + (`mcp/shopdb_mcp.py`, built with `FastMCP.from_openapi` over the same spec). + The MCP server exposes a curated set of GET endpoints as tools for an agent to + query the asset database over HTTPS with a scoped read token; it never runs on + the prod box. Set it up on a work PC with + `pxe-images/github/setup-mcp.cmd`. + +The docs blueprint is `shopdb/core/api/docs.py` (a core blueprint, always +mounted regardless of which plugins are staged into a site build). + +Contract docs (linked per table below) hold the deep semantics: field mappings, +idempotency rules, rotation, error envelopes, staged rollout. This page only +routes you to the right one. + +--- + +## 1. Fleet and client contracts (unauthenticated or token) + +These are the endpoints the shopfloor PC fleet, kiosks, displays, and printer +installers call. They are consumed by machines, not by the interactive UI, and +they authenticate with a scoped service token or nothing at all. + +| Endpoint | Auth | Purpose | Contract doc | +|---|---|---|---| +| `GET /api/geenforce/manifest?pctype=` | `geenforce.fetch` service token (`X-API-Key` or Bearer PAT) | Serve the current published manifest for a PC-type scope. ETag / 304 supported. | GE-ENFORCE-CLIENT.md, geenforce-api-cutover.md | +| `GET /api/geenforce/payload/` | `geenforce.fetch` service token | Serve a payload blob (installer) by content hash so share-less PCs pull over HTTPS instead of SMB. Rate limited and size capped; the sha256 is the integrity guarantee. | GE-ENFORCE-CLIENT.md, geenforce-api-cutover.md | +| `POST /api/geenforce/report` | `geenforce.report` service token | Record one PC's enforcement cycle: applied manifest version plus per-entry self-heal outcomes. | GE-ENFORCE-CLIENT.md, geenforce-api-cutover.md | +| `POST /api/collector/` | `X-API-Key` env key or a `collector.ingest` managed token | Generic idempotent inventory upsert; the PC fleet targets `/api/collector/computers`. | COLLECTOR-INTEGRATION.md | +| `POST /api/collector/pc`, `/apps`, `/heartbeat`, `/bulk` | `X-API-Key` or `collector.ingest` token | Legacy computers-only collector paths (predate ADR-006); deprecated in favor of `/api/collector/computers`. | COLLECTOR-INTEGRATION.md | +| `GET /api/collector/status` | `X-API-Key` or `collector.ingest` token | Collector liveness and endpoint list. | COLLECTOR-INTEGRATION.md | +| `GET /api/printers/install-list` | optional JWT (anonymous fleet or logged-in browser) | Flat list of network printers with floor-map positions for the signed installer. `?format=text` returns a pipe-delimited variant. | PRINTER-INSTALLER.md | +| `GET /api/printers/pc-default?machine=NNNN` | optional JWT | The PC's default printer by machine (asset) number, via the `defaultprinter` relationship. `?format=text` supported. | PRINTER-INSTALLER.md | +| `GET /api/printers/install-batch?printerids=1,2,3` | optional JWT | Generate a self-deleting Windows `.bat` that installs the selected printers. | PRINTER-INSTALLER.md | +| `GET /api/dashboarddefaults/display-role?fqdn=` | public (none) | Resolve what a display PC should show (role `dashboard`/`lobby`/`partskiosk`, frontend path, business unit). FQDN-first, IP fallback. | GE-ENFORCE-DISPLAY.md | +| `GET /api/dashboarddefaults/visitor-location?fqdn=` | public (none) | Resolve the business unit for a lobby display by FQDN (IP fallback). | GE-ENFORCE-DISPLAY.md | + +The `geenforce.fetch` and `geenforce.report` scopes accept both `X-API-Key` and +`Authorization: Bearer` transports, the same managed-token pattern the collector +uses (see COLLECTOR-INTEGRATION.md for how to mint, deploy, and rotate a scoped +token). A fetch token may be further resource-bound to specific scopes; a bound +token is denied (403 on manifest, 404 on payload) anything outside its scopes. + +--- + +## 2. Import API + +The import surface (an admin PAT plus `X-Import-Mode` to preserve legacy +timestamps) lets a script load an entire legacy database through the same +endpoints the UI uses. It is documented in full, per resource, in **IMPORT-API.md** +and is not duplicated here. The dashboarddefaults import fields (FQDN-preferred +keying) are covered there as well. + +--- + +## 3. Core UI API + +Everything else is the core UI API: the endpoints the Vue frontend calls. As a +rule these are JWT-authenticated (a login token or a managed Personal Access +Token) and versioned by the plugin contract (`__contract_version__`, currently +0.15.0). Behavior and stability guarantees are in **CONTRACT-STABILITY.md**; +sister sites should pin tight `core_version` ranges until the contract reaches +1.0. + +Two auth patterns dominate the reads: + +- **Public (no token ever).** The endpoints below are reachable with no + credential at all. This is the surface a firewall or deployment reviewer asks + about, so it is enumerated in full. +- **Optional JWT (`jwt_required(optional=True)`).** Nearly every core and plugin + GET (list / detail / report / dashboard-summary) is optional-auth: it serves + reads anonymously and only requires a JWT for writes. There are well over a + hundred of these; rather than reprint them, enumerate them from the live spec + at `/api/docs` (filter to the `GET` operations). All product reports + (`/api/reports/*` and every plugin `.../report*`) are optional-auth by the same + convention. + +Every mutating endpoint (POST / PUT / PATCH / DELETE) requires a JWT and is +gated by `require_role` or `require_permission`; none are public. + +### Fully public endpoints (auth = none) + +| Endpoint | Purpose | +|---|---| +| `POST /api/auth/login` | Obtain a JWT. | +| `GET /api/setup/needs-admin` | First-run check: does the instance have zero users. | +| `POST /api/setup/create-admin` | First-run only; creates the first admin, then 403s forever. | +| `GET /api/settings/map-blueprint/` | Serve the floor-map blueprint image. | +| `GET /api/settings/branding/` | Serve site branding assets (logo, etc.). | +| `GET /api/models/image/` | Serve a model image. | +| `GET /api/dashboard/navigation` | Public navigation tree. | +| `GET /api/dashboard/health` | Liveness / health probe. | +| `GET /api/plugins/enabled` | List enabled plugins (no claims used). | +| `GET /api/dashboarddefaults/display-role` | Display role resolution (see section 1). | +| `GET /api/dashboarddefaults/visitor-location` | Lobby business-unit resolution (see section 1). | +| `GET /api/employees/search`, `/lookup/`, `/lookup` | Employee directory lookups (kiosk / sign-in flows). | +| `GET /api/employees/photo/` | Serve an employee photo. | +| `GET /api/notifications` (and `/types`, `/`, `/active`, `/calendar`, `/dashboard/summary`, `/employee/`, `/shopfloor`) | Read shop-floor notifications for the public display and kiosks. | +| `GET /api/slides/feed` | Slide feed for the lobby display and screensaver. | +| `GET /api/slides/img//` | Serve a slide image. | +| `GET /api/printedparts/image/` | Serve a printed-part image. | +| `GET /api/printedparts/kiosk/item/` | Kiosk part lookup (deliberately open; a kiosk carries no JWT). | +| `POST /api/printedparts/kiosk/take` | Kiosk part checkout (deliberately open, per decision record). | + +--- + +## See also + +- `docs/adr/README.md` - architecture decision records index. +- `docs/DEPLOY.md` - deployment; the public-endpoint inventory in section 3 above + is the site-exposure surface a deploy reviewer needs. +- `docs/PLUGINS.md` - the plugin catalog. +- Contract docs: GE-ENFORCE-CLIENT.md, geenforce-api-cutover.md, + COLLECTOR-INTEGRATION.md, PRINTER-INSTALLER.md, GE-ENFORCE-DISPLAY.md, + IMPORT-API.md, CONTRACT-STABILITY.md. diff --git a/docs/CONFIG.md b/docs/CONFIG.md index 26bf14f..5c18c06 100644 --- a/docs/CONFIG.md +++ b/docs/CONFIG.md @@ -145,6 +145,14 @@ read back through the API. | `printer_hostname_template` | `Printer-{ip}.printer.geaerospace.net` | Printer hostname template. `{ip}` is the dash-separated IP address. | | `contact_email_domain` | `geaerospace.com` | Email domain appended to a support contact's SSO to build email (`sso@domain`) and Teams-chat links. Blank hides the contact action buttons. | | `dualpath_single_machine` | `true` | Treat a Dualpath pair (a dual-bay machine with one controller) as a single machine in the machines list, dashboard/report counts, and the floor map (the secondary bay is hidden). The data model always keeps both bay records; detail pages stay per-bay with a sibling banner. `false` lists and counts both bays separately. | +| `site_timezone` | `America/New_York` | IANA timezone for the site. Notification start/end times are entered and displayed in this zone (not the viewer's browser zone), and daily-reset notification expiry (`expirymode=dailytime`) is computed here. Editable in Settings > Site > Localization. Public-readable so kiosks/clients can resolve it. | + +Notification times are stored and served in UTC; the frontend converts to +`site_timezone` via `frontend/src/utils/datetime.js` (Intl-based, DST-safe). + +Change note: notification times are now timezone-correct (stored UTC, shown in +`site_timezone`); this fixes the prior offset bug where a 2:34 PM entry displayed +as 6:34 PM. ### branding @@ -319,6 +327,11 @@ One boolean key per search domain, keyed `search__enabled` (default `true`). Toggles whether a domain appears in global search results. The set is generated from `SEARCH_DOMAINS` in `shopdb/core/api/settings.py`. +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. + ## Custom fields Site-defined extra attributes per asset type (Settings > Custom Fields, table diff --git a/docs/GE-ENFORCE-DEPLOY.md b/docs/GE-ENFORCE-DEPLOY.md index 3d9012f..5556c14 100644 --- a/docs/GE-ENFORCE-DEPLOY.md +++ b/docs/GE-ENFORCE-DEPLOY.md @@ -106,6 +106,12 @@ retrofit it. `-NoTask` provisions identity + kit without registering the task. Re-running it updates identity/config and re-registers the task in place. +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. + --- ## 5. The engine boundary diff --git a/docs/GE-ENFORCE-DISPLAY.md b/docs/GE-ENFORCE-DISPLAY.md index 515a03a..eca5f56 100644 --- a/docs/GE-ENFORCE-DISPLAY.md +++ b/docs/GE-ENFORCE-DISPLAY.md @@ -65,7 +65,17 @@ creates a new version). re-asserts each cycle, but the script is idempotent (it skips relaunch if a kiosk process is already serving the target URL). -### display-type -> target map +### Role resolution: server first, display-type.txt fallback + +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. + +Fallback map (local file): | display-type.txt | kiosk route | notes | | --- | --- | --- | @@ -73,6 +83,39 @@ creates a new version). | `Lobby` | `/tv` | slides plugin TV dashboard (surface `lobby`) | | `3DPrintRoom` | `/parts-kiosk` | **PLACEHOLDER, TODO-confirm** printedparts parts kiosk route; confirm the real 3D-print-room target with the floor team before publishing to production displays | +### 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`), +`businessunitid`, and `description`; `displaypath` is not stored but derived from +the role (`DISPLAY_ROLE_PATHS`). Two public read endpoints consume it: +`/api/dashboarddefaults/display-role` (dispatcher) and +`/api/dashboarddefaults/visitor-location` (lobby business-unit lookup). The +server derives a display's FQDN from its reported BIOS serial as +`F.` (`derive_display_fqdn`, domain from the `display_fqdn_domain` +setting); the dispatcher in `plugins/geenforce/seed_display_scope.py` builds the +same FQDN client-side for its lookup. + +### Legacy autostart self-heal + +The dispatcher also cleans up after the old GE Aerospace Dashboard / Lobby +Display Inno installers, which planted autostarts (a Public-Desktop `.lnk`, an +all-users Startup `.lnk`, and an `HKLM ...\CurrentVersion\Run` value) that +relaunch Edge at now-dead URLs (`/shopfloor-dashboard/`, `/tv-dashboard/`) and +white-screen. The 32-bit installer's Run value was WOW64-redirected into +`Wow6432Node`, which is why it survived earlier cleanup. Every enforce cycle the +dispatcher sweeps both registry views, all loaded user hives, Run/RunOnce/policy +Run keys, and every per-user and common Startup folder, matching by legacy name +and by the old URLs, then kills any old-URL Edge. The kiosk shortcut it writes is +a direct Edge shortcut (no launcher or VBS). The fix ships by re-publishing this +code-authored scope (`seed_display_scope(publish=True)`), not an import-share. +`pxe-images/github/find-legacy-kiosk-autostart.ps1` is a read-only locator for +stragglers. + ## Self-sufficient: displays do NOT inherit common The `gea-shopfloor-display` scope carries everything a display enforces. It does diff --git a/docs/GE-ENFORCE.md b/docs/GE-ENFORCE.md index a765c90..0274e41 100644 --- a/docs/GE-ENFORCE.md +++ b/docs/GE-ENFORCE.md @@ -288,8 +288,15 @@ The engine sources the manifest and reports results using the reference kit in (BaseUrl + a `geenforce.fetch`/`geenforce.report` service token). See `docs/GE-ENFORCE-CLIENT.md` for the fetch/report contract, the last-known-good cache, shadow mode, and the staged cutover from share-sourced to shopdb-sourced -manifests. Until that cutover, the client only REPORTS; the manifest still comes -from the share via Export to Share (4.2). +manifests. + +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. --- diff --git a/docs/IMPORT-ADOPTION.md b/docs/IMPORT-ADOPTION.md index 834f001..cd24c7e 100644 --- a/docs/IMPORT-ADOPTION.md +++ b/docs/IMPORT-ADOPTION.md @@ -25,6 +25,11 @@ implementation #1. Read it alongside this guide. - `run.py` - ordered `stage_*` functions. Each reads a slice of the source, POSTs it, and records the crosswalk later stages resolve foreign keys against. +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. + ### Stage order matters Reference/lookup tables first (so foreign keys resolve), then the entity hub, diff --git a/docs/IMPORT-API.md b/docs/IMPORT-API.md index 04d0042..33d2293 100644 --- a/docs/IMPORT-API.md +++ b/docs/IMPORT-API.md @@ -234,10 +234,13 @@ upload them after import via `POST /api/models//image` (multipart | `pctype` | `POST /api/computers/types` | `typename` -> `computertype`, `description` | `computertype` | | `subnettypes` | (see subnets) | used as `subnettype` string on subnets | - | | `subnets` | `POST /api/network/subnets` | `cidr`, `description` -> `name`/`description`, `vlan` -> create VLAN first (`POST /api/network/vlans`) then `vlanid`, `subnettypeid` -> `subnettype` name | `cidr` | -| `dashboarddefaults` | `POST /api/dashboarddefaults` | `ipaddress` -> `ipaddress`, `businessunitid` (remapped), `description` | `ipaddress` | +| `dashboarddefaults` | `POST /api/dashboarddefaults` | `fqdn` (preferred key, stored lowercase), `ipaddress` (fallback key), `displayrole` (`dashboard`/`lobby`/`partskiosk`), `businessunitid` (remapped; only the `dashboard` role uses it), `description` | `fqdn`, else `ipaddress` | | `controllertypes` | remap into `vendors` + `models` | e.g. "Fanuc" -> a Vendor; the controller model -> a Model; then set `controllervendorid`/`controllermodelid` on the machine | - | | `comstypes` | `communicationtypes` (seeded, no API) | ensure `flask seed reference-data` created IP/Serial/USB/... before importing comms | - | +Resolution at runtime is FQDN-first with IP fallback (migration +`7d31_dashboarddefault_fqdn`); import both when the legacy source has them. + Note on communication types: the classic `comstypes.typename` values (IP, Serial, Network_Interface, USB, Parallel, VNC, FTP, DNC) correspond to the seeded `communicationtypes.comtype`. They are created by the reference-data seed, diff --git a/docs/PILOT-DEPLOY.md b/docs/PILOT-DEPLOY.md index e6ce4af..b97802a 100644 --- a/docs/PILOT-DEPLOY.md +++ b/docs/PILOT-DEPLOY.md @@ -80,12 +80,28 @@ import API. It is site glue, not product code. printers -> dependents -> relationships -> subnets -> usb -> verify). It is idempotent - a crashed run resumes from `idmap.json`. +3. 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). + Expected magnitude (from the WJ dumps used in development - your fresh dumps will differ slightly): | entity | count | |---|---| -| assets | ~983 (computer ~663, machine ~76, network ~58, measuring-tool ~136, printer ~50) | +| assets [*] | ~983 (computer ~663, machine ~76, network ~58, measuring-tool ~136, printer ~50) | | locations | ~24 | | employees | ~415 | | installs | ~850 | @@ -97,6 +113,16 @@ differ slightly): | subnets | ~37 | | USB devices / events | ~18 / ~232 | +[*] 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. + +> PLACEHOLDER - re-measure before publishing. The computer/network split shown +> in the assets row above still reflects a RAW import (pre-reclassify). Re-run +> the counts on the current prodscratch AFTER the reclassify step above and drop +> in the actual numbers; do not carry these development figures forward as if +> they already account for the reclassify. + The `verify` stage prints a source-vs-target row-count audit; the gaps are the documented skips (inactive rows, duplicate machinenumbers, LocationOnly, the 9999 placeholder). diff --git a/docs/PLUGINS.md b/docs/PLUGINS.md index 48d07a7..6f0ba99 100644 --- a/docs/PLUGINS.md +++ b/docs/PLUGINS.md @@ -10,7 +10,7 @@ These plugins are in `plugins/` in this repo. Enable per site with `flask plugin |--------|--------|-------| | `machines` | Manufacturing machinery: 5-axis mills, lathes, broachers, heat treatment ovens | Manually entered. See [ADR-005](adr/ADR-005-equipment-vs-measuringtools.md). Subtype tables for FOCAS / CLM / MTConnect controller protocols (planned). | | `computers` | Shop-floor PCs and engineering workstations | Fed by the PXE pipeline collector per [ADR-006](adr/ADR-006-collector-contract.md). | -| `printers` | Network and shop-floor printers | Optional Zabbix integration for supply tracking. Legacy `PrinterData` retiring per ADR-001. | +| `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](PRINTER-INSTALLER.md)). Optional Zabbix integration for supply tracking. Legacy `PrinterData` retiring per ADR-001. | | `network` | Switches, routers, access points, IDFs as locations | Asset-only; cleanest of the bundled set. | | `usb` | USB devices issued to shop-floor users | Lightweight checkout / check-in. | | `notifications` | Shop-floor notifications, recognitions, kiosk feed | Used by `ShopfloorDashboard.vue`. | @@ -19,9 +19,16 @@ These plugins are in `plugins/` in this repo. Enable per site with `flask plugin | `geenforce` | GE-Enforce manifest store: imaging PC-type scopes and their install manifests (apps, scripts, files, registry, version gates) | Per [ADR-012](adr/ADR-012-geenforce-manifest-ownership.md). Served to the GE-Enforce client as JSON. Requires GE-Enforce lib >= 2.6 on target PCs. Ships `default_enabled: false`. | | `knowledgebase` | Knowledge Base articles linking to external resources | Lightweight article store. | | `printedparts` | 3D-printed parts inventory | Kiosk checkout / check-in. Ships `default_enabled: false`. | -| `slides` | Slides for the lobby display and shop-floor screensaver | Upload / reorder / delete per surface. | +| `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. | | `warranty` | Asset warranty tracking | Manual entry now, Dell / Lenovo / HP provider lookups later. Derived coverage status with report buckets. | +## Plugin permissions + +Plugins may register their own permissions (for example `slides.manage`). Admins +implicitly hold them; grant them to specific roles or users to delegate curation +without admin. Each plugin's registered permissions appear in its `plugin.py` +`get_permissions()`. + ## Building your own Guides: diff --git a/docs/PRINTER-INSTALLER.md b/docs/PRINTER-INSTALLER.md new file mode 100644 index 0000000..d305e33 --- /dev/null +++ b/docs/PRINTER-INSTALLER.md @@ -0,0 +1,104 @@ +# Printer installer map and install endpoints + +How the shop-floor fleet installs network printers from shopdb-flask, replacing +the classic ASP `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. + +- Server code: `plugins/printers/api/asset_routes.py` + (`printer_install_list`, `pc_default_printer`, `printer_install_batch`) +- Consumed as a fleet manifest entry: the `common` scope's `printer map` + entry (see `GE-ENFORCE-DISPLAY.md`). + +All three endpoints are `@jwt_required(optional=True)`: an anonymous fleet +client works, and a logged-in browser (the public map page) works too. + +--- + +## 1. The public map page + +`PrinterInstallerMap` is a public (no-login) frontend page: the floor map with +printer hotspots positioned at each printer's `mapx` / `mapy`. The user clicks +the printers they want, and the page requests an install batch. The PC's default +printer is preselected via `pc-default`. + +--- + +## 2. `GET /api/printers/install-list` + +Flat, unpaginated list of active NETWORK printers. A printer counts as network +only if it has a hostname or a non-USB IP; USB-only printers are excluded. + +Fields per row: + +| Field | Notes | +|---|---| +| `printerid` | Printer id (the token `install-batch` takes). | +| `name` | Asset name, else asset number. | +| `machinenumber` | The asset number. | +| `windowsname` | Standardized Windows printer name. | +| `sharename` | Share / CSF name. | +| `hostname` | Print-queue host. | +| `ipaddress` | Primary IP (falls back to any communication row). | +| `vendorname` | Direct vendor, else the model's vendor. | +| `modelnumber` | Model name. | +| `installpath` | Installer path for this printer (see install-batch). | +| `iscsf` | CSF flag. | +| `locationname` | Location name, if the asset has one. | +| `mapx` / `mapy` | Floor-map hotspot position. | + +`?format=text` returns a pipe-delimited line per printer, one printer per line, +with a fixed field order so the Inno / Pascal installer does a `split()` instead +of parsing JSON: + +``` +printerid|windowsname|vendorname|modelnumber|hostname|ipaddress|mapx|mapy +``` + +Any pipe or newline inside a value is neutralized to a space so the field count +stays fixed. The web map uses the default JSON. + +--- + +## 3. `GET /api/printers/pc-default?machine=NNNN` + +The PC's default printer, by machine (asset) number persisted at PXE enrollment. +Parity with classic `apipcdefaultprinter.asp`: the installer preselects a PC's +default-printer hotspot on the site-map wizard. The link is resolved through the +`defaultprinter` asset relationship (PC asset -> printer asset), so it stays +inside the contract surface (no cross-plugin model import). + +Returns `{printerid, windowsname}`, or `{}` when the machine is unknown or has no +active default printer set. + +`?format=text` returns one pipe-delimited line (`printerid|windowsname`), or an +EMPTY body when there is no default (so the installer's split yields nothing). + +--- + +## 4. `GET /api/printers/install-batch?printerids=1,2,3` + +Returns a self-deleting Windows `.bat` that installs the selected printers, +grouped the same way classic `installprinter.asp` grouped them: + +- HP / Xerox: one universal `PrinterInstaller.exe /PRINTER="a,b,c"` call. +- Printers with a `.exe` `installpath`: a PowerShell `Invoke-WebRequest` download + (using the caller's Windows credentials, against the site base URL + the + IIS-served `/installers` folder) followed by running it `/SILENT`. +- No `installpath`, or a non-`.exe` payload (e.g. a `.zip`): listed as a manual + install rather than run blindly. + +`printerids` is required, comma-separated; non-numeric tokens are ignored. An +empty / missing list is a validation error. + +The install name preference is `windowsname`, else `sharename`, else the asset +name / number. + +--- + +## 5. Fleet wiring + +The `common` scope's `printer map` manifest entry (see `GE-ENFORCE-DISPLAY.md`) +lays down the signed installer that consumes these endpoints. The web map page +covers the same install flow for a human at a browser. diff --git a/docs/WIKI-UPDATE-PLAN.md b/docs/WIKI-UPDATE-PLAN.md index b338607..772f3e6 100644 --- a/docs/WIKI-UPDATE-PLAN.md +++ b/docs/WIKI-UPDATE-PLAN.md @@ -183,6 +183,40 @@ No full doc for [E]; this note plus a CHANGELOG line covers it. --- +## 12. docs/CONFIG.md - UPDATE (site timezone) + note in notifications coverage + +**Action: update.** New `site_timezone` site setting (default `America/New_York`, public), added in `shopdb/core/api/settings.py` (`build_default_settings`, category `site`) and surfaced in `frontend/src/views/settings/SiteSettings.vue` (Localization group, common-zone dropdown). Add to the site-settings section of CONFIG.md: + +> `site_timezone` (site, default `America/New_York`) - IANA timezone for the site. Notification start/end times are entered and displayed in this zone (not the viewer's browser zone), and daily-reset notification expiry (`expirymode=dailytime`) is computed here. Editable in Settings > Site > Localization. Public-readable so kiosks/clients can resolve it. Stored/served UTC; the frontend converts via `frontend/src/utils/datetime.js` (Intl-based, DST-safe). + +Also add a CHANGELOG line: notification times are now timezone-correct (stored UTC, shown in `site_timezone`); fixes the prior offset bug where a 2:34 PM entry displayed as 6:34 PM. + +--- + +## 13. docs/geenforce-api-cutover.md + GE-ENFORCE-DISPLAY.md - UPDATE (legacy kiosk autostart purge) + +**Action: update both.** The display dispatcher now self-heals the legacy kiosk autostarts the OLD Dashboard/Lobby Inno installers left behind. Add a subsection to the dispatcher coverage in `geenforce-api-cutover.md` (near the dispatcher pattern) and a short note in `GE-ENFORCE-DISPLAY.md`: + +> ### Legacy autostart self-heal +> +> The old GE Aerospace Dashboard / Lobby Display Inno installers planted three autostarts each: a Public-Desktop `.lnk`, an all-users Startup `.lnk`, and an `HKLM ...\CurrentVersion\Run` value, all launching Edge at now-dead URLs (`/shopfloor-dashboard/`, `/tv-dashboard/`) which 404 to a white screen. Because those installers were 32-bit, the Run value was WOW64-redirected into `HKLM\SOFTWARE\Wow6432Node\...\Run`, invisible to 64-bit tooling - the reason it survived earlier cleanup. The dispatcher (`build_dispatcher_script` in `seed_display_scope.py`) now sweeps, every enforce cycle: both the native and Wow6432Node registry views, every loaded user hive (HKU), Run + RunOnce + Policies\Explorer\Run, matching by the legacy value names AND by any value pointing at the old URLs; plus every per-user and common Startup folder; then kills any running old-URL Edge. A read-only locator, `pxe-images/github/find-legacy-kiosk-autostart.ps1`, hunts all these locations (and Edge startup-URL policy, scheduled tasks, Assigned Access) when a straggler persists. + +Note that the kiosk shortcut is a direct Edge shortcut (no launcher/VBS) and that the fix ships by re-publishing the code-authored `gea-shopfloor-display` scope (`seed_display_scope(publish=True)`), not an import-share. + +--- + +## 14. docs/API-REFERENCE.md - CREATE (revise item 11 scope: link the LIVE docs) + +**Action: supersedes item 11's "hand-written index only" framing.** The repo now ships generated, hosted API docs, so API-REFERENCE.md is primarily a pointer to those plus the audience/auth index: + +- **Live interactive spec:** `GET /api/docs` (self-hosted Redoc), raw spec at `GET /api/docs/openapi.json` (OpenAPI 3.1, ~238 paths / ~362 ops, generated by `scripts/gen_openapi.py` from `docs/api-inventory.json`). +- **LLM/agent entry point:** `GET /api/docs/llms.txt` (llms.txt convention) and the read-only MCP server (`mcp/shopdb_mcp.py`, `FastMCP.from_openapi`; set up on a work PC via `pxe-images/github/setup-mcp.cmd`). +- Keep item 11's audience tables (fleet/client contracts, import API, core UI API) as the human index, each row linking the owning contract doc. State at top: interactive/full shapes live at `/api/docs`; contract docs hold the deep semantics; this page answers "what exists, who calls it, what auth". + +Cross-link from PLUGINS.md header, DEPLOY.md security section, and `docs/llms.txt` provenance. The `docs/` blueprint lives in `shopdb/core/api/docs.py` (vendored Redoc under `staticdocs/`); mention it is a core blueprint always mounted. + +--- + ## 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. diff --git a/docs/geenforce-api-cutover.md b/docs/geenforce-api-cutover.md index d85f99b..7364547 100644 --- a/docs/geenforce-api-cutover.md +++ b/docs/geenforce-api-cutover.md @@ -335,18 +335,51 @@ entry that got reinstalled). `Always`/no-detection entries install every cycle by design and are not flagged, so the server-side status derivation (`service.record_enforcement_report`: failed > selfhealed > ok) stays honest. -### The display-type.txt dispatcher pattern +### The display dispatcher: server-resolved role, file fallback For kiosks, per-subtype behavior does not fork the scope: ONE scope -(`gea-shopfloor-display`), one inline dispatcher entry that reads -`C:\Enrollment\display-type.txt` at enforce time and acts on it. Subtype -changes are a one-file edit plus the next 15-min cycle, and retargeting a -subtype is a table edit in `seed_display_scope.py` + republish. The dispatcher -writes an all-users Startup shortcut (never Start-Process - see gotchas), and -sweeps stale kiosk launchers first (its own `ShopDB Kiosk*.lnk`, the imaging -installers' `GE Aerospace Dashboard*`/`GE Aerospace Lobby*` shortcuts, any -browser .lnk with `-kiosk`-ish args or shopdb URLs, and .url files pointing at -the kiosk routes). +(`gea-shopfloor-display`), one inline dispatcher entry (built by +`plugins/geenforce/seed_display_scope.py`) that resolves what the display should +show at enforce time, 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 built as `F.` (GE device naming); the domain +comes from HKLM `DisplayFqdnDomain` or the built-in default +(`device.geaerospace.net`). `DetectionMethod = Always`, but the script is +idempotent: it rewrites the all-users Startup shortcut (never Start-Process - +see gotchas) only when the resolved target changed. + +### Legacy autostart self-heal + +The old GE Aerospace Dashboard / Lobby Display Inno installers planted three +autostarts each: a Public-Desktop `.lnk`, an all-users Startup `.lnk`, and an +`HKLM ...\CurrentVersion\Run` value, all launching Edge at now-dead URLs +(`/shopfloor-dashboard/`, `/tv-dashboard/`) which 404 to a white screen. Because +those installers were 32-bit, the Run value was WOW64-redirected into +`HKLM\SOFTWARE\Wow6432Node\...\Run`, invisible to 64-bit tooling - the reason it +survived earlier cleanup. The dispatcher (`build_dispatcher_script` in +`seed_display_scope.py`) now sweeps, every enforce cycle: both the native and +Wow6432Node registry views, every loaded user hive (HKU), Run + RunOnce + +Policies\Explorer\Run, matching by the legacy value names AND by any value +pointing at the old URLs; plus every per-user and common Startup folder; then +kills any running old-URL Edge. A read-only locator, +`pxe-images/github/find-legacy-kiosk-autostart.ps1`, hunts all these locations +(and Edge startup-URL policy, scheduled tasks, Assigned Access) when a straggler +persists. + +The kiosk shortcut is a direct Edge shortcut (no launcher/VBS); the fix ships by +re-publishing the code-authored `gea-shopfloor-display` scope +(`seed_display_scope(publish=True)`), not an import-share. ---