diff --git a/docs/api-inventory.json b/docs/api-inventory.json index ad5af10..00bbbfb 100644 --- a/docs/api-inventory.json +++ b/docs/api-inventory.json @@ -1,3058 +1,3236 @@ [ - { - "surface": "core-identity", - "endpoints": [ - { - "method": "POST", - "path": "/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", - "example": "curl -X POST http://localhost:5001/api/auth/login -H 'Content-Type: application/json' -d '{\"username\":\"admin\",\"password\":\"secret123\"}'" - }, - { - "method": "POST", - "path": "/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)", - "example": "curl -X POST http://localhost:5001/api/auth/refresh -H \"Authorization: Bearer $REFRESH_TOKEN\"" - }, - { - "method": "GET", - "path": "/api/auth/me", - "auth": "jwt", - "params": "none", - "purpose": "Return the authenticated user's profile, roles, permissions, mustchangepassword flag", - "example": "curl http://localhost:5001/api/auth/me -H \"Authorization: Bearer $TOK\"" - }, - { - "method": "POST", - "path": "/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", - "example": "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\"}'" - }, - { - "method": "POST", - "path": "/api/auth/logout", - "auth": "jwt", - "params": "none", - "purpose": "Logout stub for frontend token cleanup (no server-side blacklist yet)", - "example": "curl -X POST http://localhost:5001/api/auth/logout -H \"Authorization: Bearer $TOK\"" - }, - { - "method": "GET", - "path": "/api/users", - "auth": "jwt + require_role admin", - "params": "none", - "purpose": "List all users ordered by username", - "example": "curl http://localhost:5001/api/users -H \"Authorization: Bearer $TOK\"" - }, - { - "method": "GET", - "path": "/api/users/", - "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)", - "example": "curl http://localhost:5001/api/users/7 -H \"Authorization: Bearer $TOK\"" - }, - { - "method": "POST", - "path": "/api/users", - "auth": "jwt + require_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", - "example": "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]}'" - }, - { - "method": "PUT", - "path": "/api/users/", - "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", - "example": "curl -X PUT http://localhost:5001/api/users/7 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"firstname\":\"Jane\",\"unlock\":true}'" - }, - { - "method": "DELETE", - "path": "/api/users/", - "auth": "jwt + require_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", - "example": "curl -X DELETE http://localhost:5001/api/users/7 -H \"Authorization: Bearer $TOK\"" - }, - { - "method": "GET", - "path": "/api/users/permissions", - "auth": "jwt", - "params": "none", - "purpose": "List assignable permissions (core + enabled plugins) both flat and grouped by category, for the role grid", - "example": "curl http://localhost:5001/api/users/permissions -H \"Authorization: Bearer $TOK\"" - }, - { - "method": "GET", - "path": "/api/users/roles", - "auth": "jwt", - "params": "none", - "purpose": "List all roles with description, color, user count, permission names, and isadmin flag", - "example": "curl http://localhost:5001/api/users/roles -H \"Authorization: Bearer $TOK\"" - }, - { - "method": "POST", - "path": "/api/users/roles", - "auth": "jwt + require_role admin", - "params": "body: rolename (required), description, color, permissions[names]", - "purpose": "Create a role and assign permissions by name; 409 if rolename exists", - "example": "curl -X POST http://localhost:5001/api/users/roles -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"rolename\":\"viewer\",\"permissions\":[\"assets.view\"]}'" - }, - { - "method": "PUT", - "path": "/api/users/roles/", - "auth": "jwt + require_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", - "example": "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\"]}'" - }, - { - "method": "DELETE", - "path": "/api/users/roles/", - "auth": "jwt + require_role admin", - "params": "path: roleid", - "purpose": "Delete a role; refuses for the admin role or any role still assigned to users", - "example": "curl -X DELETE http://localhost:5001/api/users/roles/3 -H \"Authorization: Bearer $TOK\"" - }, - { - "method": "GET", - "path": "/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", - "example": "curl 'http://localhost:5001/api/apitokens?all=true' -H \"Authorization: Bearer $TOK\"" - }, - { - "method": "POST", - "path": "/api/apitokens", - "auth": "jwt + require_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", - "example": "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\"}'" - }, - { - "method": "PUT", - "path": "/api/apitokens/", - "auth": "jwt + require_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", - "example": "curl -X PUT http://localhost:5001/api/apitokens/4 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"isactive\":false}'" - }, - { - "method": "DELETE", - "path": "/api/apitokens/", - "auth": "jwt + require_permission apitokens.create (own token, or any if admin)", - "params": "path: tokenid", - "purpose": "Revoke (deactivate, not delete) a token; audit-logged", - "example": "curl -X DELETE http://localhost:5001/api/apitokens/4 -H \"Authorization: Bearer $TOK\"" - }, - { - "method": "GET", - "path": "/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", - "example": "curl http://localhost:5001/api/setup/needs-admin" - }, - { - "method": "POST", - "path": "/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)", - "example": "curl -X POST http://localhost:5001/api/setup/create-admin -H 'Content-Type: application/json' -d '{\"username\":\"admin\",\"email\":\"admin@example.com\",\"password\":\"ChangeMe123\"}'" - }, - { - "method": "POST", - "path": "/api/setup/seed-reference", - "auth": "jwt + require_role admin", - "params": "none", - "purpose": "Idempotently seed core reference data, permissions, and default settings (runs the flask seed CLI routines)", - "example": "curl -X POST http://localhost:5001/api/setup/seed-reference -H \"Authorization: Bearer $TOK\"" - }, - { - "method": "POST", - "path": "/api/setup/seed-starter", - "auth": "jwt + require_role admin", - "params": "none", - "purpose": "Idempotently add a starter list of common hardware vendors (Dell, HP, Lenovo, ...)", - "example": "curl -X POST http://localhost:5001/api/setup/seed-starter -H \"Authorization: Bearer $TOK\"" - }, - { - "method": "POST", - "path": "/api/setup/complete", - "auth": "jwt + require_role admin", - "params": "none", - "purpose": "Set the setup_complete setting to true, marking the first-run wizard finished", - "example": "curl -X POST http://localhost:5001/api/setup/complete -H \"Authorization: Bearer $TOK\"" - }, - { - "method": "GET", - "path": "/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 ********", - "example": "curl 'http://localhost:5001/api/settings?category=branding'" - }, - { - "method": "POST", - "path": "/api/settings", - "auth": "jwt + require_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", - "example": "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\"}'" - }, - { - "method": "GET", - "path": "/api/settings/", - "auth": "jwt-optional", - "params": "path: key", - "purpose": "Get one setting; non-public keys return 404 (not 403) to unauthenticated callers; secret values masked", - "example": "curl http://localhost:5001/api/settings/facility_name" - }, - { - "method": "PUT", - "path": "/api/settings/", - "auth": "jwt + require_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", - "example": "curl -X PUT http://localhost:5001/api/settings/facility_name -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"value\":\"West Jefferson\"}'" - }, - { - "method": "POST", - "path": "/api/settings/seed", - "auth": "jwt + require_permission settings.edit", - "params": "none", - "purpose": "Idempotently create any missing default settings (identifier toggles, search toggles, map, SMTP, SAML, etc.)", - "example": "curl -X POST http://localhost:5001/api/settings/seed -H \"Authorization: Bearer $TOK\"" - }, - { - "method": "POST", - "path": "/api/settings/test-email", - "auth": "jwt + require_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", - "example": "curl -X POST http://localhost:5001/api/settings/test-email -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"to\":\"me@example.com\"}'" - }, - { - "method": "POST", - "path": "/api/settings/map-blueprint", - "auth": "jwt + require_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", - "example": "curl -X POST http://localhost:5001/api/settings/map-blueprint -H \"Authorization: Bearer $TOK\" -F 'file=@floor.png' -F 'theme=light'" - }, - { - "method": "GET", - "path": "/api/settings/map-blueprint/", - "auth": "none", - "params": "path: filename", - "purpose": "Serve an uploaded floor-map blueprint image (public so kiosk dashboards can load it)", - "example": "curl -O http://localhost:5001/api/settings/map-blueprint/blueprint-light.png" - }, - { - "method": "POST", - "path": "/api/settings/branding-logo", - "auth": "jwt + require_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)", - "example": "curl -X POST http://localhost:5001/api/settings/branding-logo -H \"Authorization: Bearer $TOK\" -F 'file=@logo.svg' -F 'kind=site'" - }, - { - "method": "GET", - "path": "/api/settings/branding/", - "auth": "none", - "params": "path: filename", - "purpose": "Serve an uploaded branding logo (public - kiosks and print pages read it)", - "example": "curl -O http://localhost:5001/api/settings/branding/logo-site.svg" - } - ] - }, - { - "surface": "core-platform", - "endpoints": [ - { - "method": "GET", - "path": "/api/reports", - "purpose": "List all available reports (6 core cards plus cards contributed by enabled plugins via the get_reports hook)", - "auth": "jwt-optional", - "params": "none", - "example": "curl http://localhost:5001/api/reports" - }, - { - "method": "GET", - "path": "/api/reports/machines-by-type", - "purpose": "Machine count grouped by machine type (dualpath secondary bays collapsed when the site setting is on); 404 if machines plugin absent", - "auth": "jwt-optional", - "params": "businessunitid (int filter), format=json|csv", - "example": "curl 'http://localhost:5001/api/reports/machines-by-type?businessunitid=2&format=csv'" - }, - { - "method": "GET", - "path": "/api/reports/assets-by-status", - "purpose": "Asset count grouped by status with status colors", - "auth": "jwt-optional", - "params": "assettypeid, businessunitid, format=json|csv", - "example": "curl 'http://localhost:5001/api/reports/assets-by-status?assettypeid=1'" - }, - { - "method": "GET", - "path": "/api/reports/kb-popularity", - "purpose": "Most-clicked knowledge base articles; 503 if knowledgebase plugin absent", - "auth": "jwt-optional", - "params": "limit (default 20, max 100), format=json|csv", - "example": "curl 'http://localhost:5001/api/reports/kb-popularity?limit=10'" - }, - { - "method": "GET", - "path": "/api/reports/software-compliance", - "purpose": "Required applications vs installed per PC with compliance rate and up to 100 non-compliant PCs per app; 503 if computers plugin absent", - "auth": "jwt-optional", - "params": "appid (filter to one app), format=json|csv", - "example": "curl 'http://localhost:5001/api/reports/software-compliance?appid=5'" - }, - { - "method": "GET", - "path": "/api/reports/asset-inventory", - "purpose": "Complete asset inventory summary broken down by type, status, and location", - "auth": "jwt-optional", - "params": "businessunitid, locationid, format=json|csv", - "example": "curl 'http://localhost:5001/api/reports/asset-inventory?locationid=3&format=csv'" - }, - { - "method": "GET", - "path": "/api/reports/pc-relationships", - "purpose": "PC-to-shop-floor-machine relationships (machine number, vendor, model, hostname, IP) matched in both edge directions via raw SQL UNION", - "auth": "jwt-optional", - "params": "format=json|csv", - "example": "curl 'http://localhost:5001/api/reports/pc-relationships?format=csv'" - }, - { - "method": "POST", - "path": "/api/reports/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", - "auth": "jwt + permission:reports.export", - "params": "body: subject, columns [{key,label}], rows [{..}], intro (optional), to (optional email)", - "example": "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\"}'" - }, - { - "method": "GET", - "path": "/api/search", - "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", - "auth": "jwt-optional", - "params": "q (required, 2-200 chars)", - "example": "curl 'http://localhost:5001/api/search?q=WKSTN0042'" - }, - { - "method": "GET", - "path": "/api/dashboard", - "purpose": "Dashboard summary: asset counts by type (machines/PCs/network/printers/measuring tools, dualpath-collapsed), counts by status, 10 most recent assets", - "auth": "jwt-optional", - "params": "none", - "example": "curl http://localhost:5001/api/dashboard" - }, - { - "method": "GET", - "path": "/api/dashboard/summary", - "purpose": "Alias route for the same dashboard summary handler as GET /api/dashboard", - "auth": "jwt-optional", - "params": "none", - "example": "curl http://localhost:5001/api/dashboard/summary" - }, - { - "method": "GET", - "path": "/api/dashboard/stats", - "purpose": "Asset counts grouped by every asset type with display category labels", - "auth": "jwt-optional", - "params": "none", - "example": "curl http://localhost:5001/api/dashboard/stats" - }, - { - "method": "GET", - "path": "/api/dashboard/navigation", - "purpose": "Sidebar navigation items: core entries merged with get_navigation_items from every enabled plugin, sorted by position", - "auth": "none", - "params": "none", - "example": "curl http://localhost:5001/api/dashboard/navigation" - }, - { - "method": "GET", - "path": "/api/dashboard/widgets", - "purpose": "Dashboard widget definitions aggregated from enabled plugins (get_dashboard_widgets hook), sorted by position", - "auth": "jwt-optional", - "params": "none", - "example": "curl http://localhost:5001/api/dashboard/widgets" - }, - { - "method": "GET", - "path": "/api/dashboard/health", - "purpose": "Health check: runs SELECT 1 against the DB, returns ok/degraded plus app version", - "auth": "none", - "params": "none", - "example": "curl http://localhost:5001/api/dashboard/health" - }, - { - "method": "GET", - "path": "/api/dashboarddefaults/visitor-location", - "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", - "auth": "none", - "params": "fqdn (optional), ipaddress (optional, defaults to client IP)", - "example": "curl 'http://localhost:5001/api/dashboarddefaults/visitor-location?fqdn=display01.example.net'" - }, - { - "method": "GET", - "path": "/api/dashboarddefaults/display-role", - "purpose": "Resolve what a display PC should show at boot: role (dashboard/lobby/partskiosk), frontend path, and business unit; null role when unmapped", - "auth": "none", - "params": "fqdn (optional), ipaddress (optional, defaults to client IP)", - "example": "curl 'http://localhost:5001/api/dashboarddefaults/display-role?ipaddress=10.1.2.3'" - }, - { - "method": "GET", - "path": "/api/dashboarddefaults", - "purpose": "List all active display-to-business-unit mappings", - "auth": "jwt-optional", - "params": "none", - "example": "curl http://localhost:5001/api/dashboarddefaults" - }, - { - "method": "POST", - "path": "/api/dashboarddefaults", - "purpose": "Create a display mapping; requires fqdn or ipaddress, dashboard role requires businessunitid, 409 on duplicate fqdn/IP; audit-logged", - "auth": "jwt + role:admin", - "params": "body: fqdn, ipaddress, displayrole (dashboard|lobby|partskiosk, default dashboard), businessunitid, description", - "example": "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}'" - }, - { - "method": "PUT", - "path": "/api/dashboarddefaults/", - "purpose": "Update a display mapping; non-dashboard roles get businessunitid nulled, dashboard role must keep one", - "auth": "jwt + role:admin", - "params": "body: any of fqdn, ipaddress, displayrole, businessunitid, description", - "example": "curl -X PUT http://localhost:5001/api/dashboarddefaults/7 -H \"Authorization: Bearer $JWT\" -H 'Content-Type: application/json' -d '{\"displayrole\":\"lobby\"}'" - }, - { - "method": "DELETE", - "path": "/api/dashboarddefaults/", - "purpose": "Soft-delete (deactivate) a display mapping", - "auth": "jwt + role:admin", - "params": "none", - "example": "curl -X DELETE http://localhost:5001/api/dashboarddefaults/7 -H \"Authorization: Bearer $JWT\"" - }, - { - "method": "POST", - "path": "/api/collector/", - "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", - "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", - "example": "curl -X POST http://localhost:5001/api/collector/computers -H \"X-API-Key: $KEY\" -H 'Content-Type: application/json' -d '{\"hostname\":\"WKSTN0042\",\"serialnumber\":\"ABC123\"}'" - }, - { - "method": "GET", - "path": "/api/collector/_schemas", - "purpose": "List collector schemas for all enabled plugins that accept collector input", - "auth": "jwt", - "params": "none", - "example": "curl http://localhost:5001/api/collector/_schemas -H \"Authorization: Bearer $JWT\"" - }, - { - "method": "POST", - "path": "/api/collector/pc", - "purpose": "Legacy computers-specific ingest: update one PC matched by hostname (or asset number) - lastreporteddate, lastboottime, loggedinuser, serialnumber", - "auth": "api-key", - "params": "body: hostname (required), lastboottime (ISO), currentuser, serialnumber", - "example": "curl -X POST http://localhost:5001/api/collector/pc -H \"X-API-Key: $KEY\" -H 'Content-Type: application/json' -d '{\"hostname\":\"WKSTN0042\",\"currentuser\":\"212345678\"}'" - }, - { - "method": "POST", - "path": "/api/collector/apps", - "purpose": "Update installed applications for one PC; only apps already in the Application table are tracked, others skipped; returns created/updated/skipped counts", - "auth": "api-key", - "params": "body: hostname (required), apps [{appname, version}] (required)", - "example": "curl -X POST http://localhost:5001/api/collector/apps -H \"X-API-Key: $KEY\" -H 'Content-Type: application/json' -d '{\"hostname\":\"WKSTN0042\",\"apps\":[{\"appname\":\"PC-DMIS\",\"version\":\"2023.2\"}]}'" - }, - { - "method": "POST", - "path": "/api/collector/heartbeat", - "purpose": "Record PC online heartbeat (single hostname or batch); stamps lastreporteddate, returns updated count and notfound list", - "auth": "api-key", - "params": "body: hostname (string) or hostnames (array)", - "example": "curl -X POST http://localhost:5001/api/collector/heartbeat -H \"X-API-Key: $KEY\" -H 'Content-Type: application/json' -d '{\"hostnames\":[\"pc1\",\"pc2\"]}'" - }, - { - "method": "POST", - "path": "/api/collector/bulk", - "purpose": "Bulk update many PCs in one call (lastreporteddate, currentuser, lastboottime per entry); returns updated/notfound/errors", - "auth": "api-key", - "params": "body: pcs [{hostname (required), currentuser, lastboottime}]", - "example": "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\"}]}'" - }, - { - "method": "GET", - "path": "/api/collector/status", - "purpose": "Collector API liveness/credential check; returns timestamp and the collector endpoint list", - "auth": "api-key", - "params": "none", - "example": "curl http://localhost:5001/api/collector/status -H \"X-API-Key: $KEY\"" - }, - { - "method": "GET", - "path": "/api/auditlogs", - "purpose": "List audit logs with filtering and pagination, newest first; rows enriched with best-effort SSO-to-full-name resolution", - "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)", - "example": "curl 'http://localhost:5001/api/auditlogs?action=deleted&perpage=100' -H \"Authorization: Bearer $JWT\"" - }, - { - "method": "GET", - "path": "/api/auditlogs/entity//", - "purpose": "Full audit history for one entity, newest first", - "auth": "jwt + permission:audit.view", - "params": "path only", - "example": "curl http://localhost:5001/api/auditlogs/entity/Asset/42 -H \"Authorization: Bearer $JWT\"" - }, - { - "method": "GET", - "path": "/api/auditlogs/stats", - "purpose": "Audit statistics: counts by action and entity type, last-7-days activity count, top 5 most active users", - "auth": "jwt + permission:audit.view", - "params": "none", - "example": "curl http://localhost:5001/api/auditlogs/stats -H \"Authorization: Bearer $JWT\"" - }, - { - "method": "GET", - "path": "/api/plugins", - "purpose": "List all discovered plugins (enabled or not) with metadata plus the framework contract version", - "auth": "jwt-optional", - "params": "none", - "example": "curl http://localhost:5001/api/plugins" - }, - { - "method": "GET", - "path": "/api/plugins/enabled", - "purpose": "Flat sorted array of enabled plugin names (registry read, no DB); deliberately anonymous so kiosk routes can gate plugin-owned frontend routes", - "auth": "none (jwt-optional decorator, no claims used)", - "params": "none", - "example": "curl http://localhost:5001/api/plugins/enabled" - }, - { - "method": "PUT", - "path": "/api/plugins/", - "purpose": "Enable or disable a plugin (route changes need an app restart); 409 when unknown or a dependency conflict refuses the change", - "auth": "jwt + role:admin", - "params": "body: enabled (bool, required)", - "example": "curl -X PUT http://localhost:5001/api/plugins/warranty -H \"Authorization: Bearer $JWT\" -H 'Content-Type: application/json' -d '{\"enabled\":false}'" - }, - { - "method": "GET", - "path": "/api/pluginui/settings-cards", - "purpose": "Merge enabled plugins' settings-catalog cards (get_settings_cards hook) sorted by position for the settings rail/overview", - "auth": "jwt-optional", - "params": "none", - "example": "curl http://localhost:5001/api/pluginui/settings-cards" - }, - { - "method": "GET", - "path": "/api/pluginui/asset-panels", - "purpose": "Asset-detail panels from enabled plugins (get_asset_panels hook) matching one asset's type ('*' wildcard supported), sorted by position", - "auth": "jwt-optional", - "params": "assetid (int, required); 400 without it, 404 if asset missing", - "example": "curl 'http://localhost:5001/api/pluginui/asset-panels?assetid=42'" - }, - { - "method": "GET", - "path": "/api/pluginui/map-overlays", - "purpose": "Merge enabled plugins' map overlay declarations (get_map_overlays hook), sorted by position", - "auth": "jwt-optional", - "params": "none", - "example": "curl http://localhost:5001/api/pluginui/map-overlays" - }, - { - "method": "GET", - "path": "/api/pluginui/asset-presentation", - "purpose": "Merge enabled plugins' asset-type presentation entries (icon + detail route per type) used by search rows and cross-links", - "auth": "jwt-optional", - "params": "none", - "example": "curl http://localhost:5001/api/pluginui/asset-presentation" - } - ] - }, - { - "surface": "plugin-employees", - "endpoints": [ - { - "method": "GET", - "path": "/api/employees/search", - "purpose": "Search employees by first name, last name, or SSO substring (self-hosted table or external HR DB depending on employee_directory_mode setting).", - "auth": "none", - "params": "q (query string, min 2 chars, required), limit (max results, default 10, capped 50)", - "example": "curl 'http://localhost:5001/api/employees/search?q=smith&limit=5'" - }, - { - "method": "GET", - "path": "/api/employees/lookup/", - "purpose": "Look up a single employee by numeric SSO; returns directory fields plus resolved photourl.", - "auth": "none", - "params": "sso (path, numeric)", - "example": "curl 'http://localhost:5001/api/employees/lookup/210009518'" - }, - { - "method": "GET", - "path": "/api/employees/lookup", - "purpose": "Bulk lookup of multiple employees by SSO list; returns employees array plus a joined names string.", - "auth": "none", - "params": "sso (query, comma-separated numeric SSOs, at least one required)", - "example": "curl 'http://localhost:5001/api/employees/lookup?sso=210009518,210001234'" - }, - { - "method": "GET", - "path": "/api/employees/directory", - "purpose": "List the full self-hosted directory for the management page; 400 when directory mode is external.", - "auth": "jwt-optional", - "params": "none", - "example": "curl -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/employees/directory'" - }, - { - "method": "POST", - "path": "/api/employees/directory", - "purpose": "Create a self-hosted directory employee; 409 if SSO exists, 400 in external mode.", - "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)", - "example": "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'" - }, - { - "method": "PUT", - "path": "/api/employees/directory/", - "purpose": "Update a self-hosted directory employee's name/team/role/picture; 404 if missing, 400 in external mode.", - "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)", - "example": "curl -X PUT -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"team\":\"Quality\"}' 'http://localhost:5001/api/employees/directory/210009518'" - }, - { - "method": "DELETE", - "path": "/api/employees/directory/", - "purpose": "Delete a self-hosted directory employee; 404 if missing, 400 in external mode.", - "auth": "jwt + require_role admin", - "params": "sso (path)", - "example": "curl -X DELETE -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/employees/directory/210009518'" - }, - { - "method": "POST", - "path": "/api/employees/directory/import", - "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.", - "auth": "jwt + require_role admin", - "params": "multipart file= OR JSON body {\"csv\": \"...\"}; rows missing numeric sso or names are skipped", - "example": "curl -X POST -H \"Authorization: Bearer $TOKEN\" -F 'file=@employees.csv' 'http://localhost:5001/api/employees/directory/import'" - }, - { - "method": "POST", - "path": "/api/employees//photo", - "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.", - "auth": "jwt + require_role admin", - "params": "sso (path); multipart/form-data file=, extensions .png/.jpg/.jpeg/.gif/.webp only", - "example": "curl -X POST -H \"Authorization: Bearer $TOKEN\" -F 'file=@jane.jpg' 'http://localhost:5001/api/employees/210009518/photo'" - }, - { - "method": "GET", - "path": "/api/employees/photo/", - "purpose": "Serve an uploaded employee photo file from the instance employeephotos dir (public so kiosk recognition cards can read it).", - "auth": "none", - "params": "filename (path, e.g. photo-210009518.jpg)", - "example": "curl 'http://localhost:5001/api/employees/photo/photo-210009518.jpg'" - }, - { - "method": "DELETE", - "path": "/api/employees//photo", - "purpose": "Clear a self-hosted employee's photo record and delete the uploaded file; 409 in external mode, 404 if employee missing.", - "auth": "jwt + require_role admin", - "params": "sso (path)", - "example": "curl -X DELETE -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/employees/210009518/photo'" - } - ] - }, - { - "endpoints": [ - { - "method": "GET", - "path": "/api/knowledgebase", - "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.", - "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)", - "example": "curl 'http://localhost:5001/api/knowledgebase?search=printer&sort=clicks&order=desc&page=1&per_page=20'" - }, - { - "method": "GET", - "path": "/api/knowledgebase/stats", - "purpose": "Return aggregate stats for active articles: totalclicks (sum of clicks) and totalarticles (count).", - "auth": "jwt-optional", - "params": "none", - "example": "curl 'http://localhost:5001/api/knowledgebase/stats'" - }, - { - "method": "GET", - "path": "/api/knowledgebase/", - "purpose": "Fetch a single active article by id with its application (appid/appname) or null; 404 if missing or inactive.", - "auth": "jwt-optional", - "params": "path: link_id (int)", - "example": "curl 'http://localhost:5001/api/knowledgebase/42'" - }, - { - "method": "POST", - "path": "/api/knowledgebase//click", - "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.", - "auth": "jwt-optional", - "params": "path: link_id (int); no body", - "example": "curl -X POST 'http://localhost:5001/api/knowledgebase/42/click'" - }, - { - "method": "POST", - "path": "/api/knowledgebase", - "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.", - "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", - "example": "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\"}'" - }, - { - "method": "PUT", - "path": "/api/knowledgebase/", - "purpose": "Update an article's shortdescription, linkurl, appid, keywords, and/or isactive; validates appid if changed; honors import timestamps; 404 if article missing.", - "auth": "permission:kb.edit (jwt required)", - "params": "path: link_id (int); body JSON: any of shortdescription, linkurl, appid, keywords, isactive", - "example": "curl -X PUT 'http://localhost:5001/api/knowledgebase/42' -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"keywords\":\"vpn,zscaler\",\"isactive\":true}'" - }, - { - "method": "DELETE", - "path": "/api/knowledgebase/", - "purpose": "Soft-delete an article by setting isactive=false (row is retained); 404 if article missing.", - "auth": "permission:kb.delete (jwt required)", - "params": "path: link_id (int)", - "example": "curl -X DELETE 'http://localhost:5001/api/knowledgebase/42' -H \"Authorization: Bearer $TOKEN\"" - } - ], - "surface": "plugin-knowledgebase" - }, - { - "surface": "core-catalog", - "endpoints": [ - { - "method": "GET", - "path": "/api/assets/types", - "purpose": "List asset types (paginated, active-only by default)", - "auth": "jwt-optional", - "params": "page, per_page, active=false to include inactive", - "example": "curl http://localhost:5001/api/assets/types?active=false" - }, - { - "method": "GET", - "path": "/api/assets/types/", - "purpose": "Get one asset type", - "auth": "jwt-optional", - "params": "path: type_id", - "example": "curl http://localhost:5001/api/assets/types/1" - }, - { - "method": "POST", - "path": "/api/assets/types", - "purpose": "Create asset type (409 on duplicate name)", - "auth": "permission:assets.create", - "params": "body: assettype (req), pluginname, tablename, description, icon, color", - "example": "curl -X POST http://localhost:5001/api/assets/types -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"assettype\":\"Robot\",\"icon\":\"mdi-robot\"}'" - }, - { - "method": "PUT", - "path": "/api/assets/types/", - "purpose": "Update asset type display fields only (name/plugin/table are structural, not editable)", - "auth": "permission:assets.edit", - "params": "body: description, icon, color, isactive", - "example": "curl -X PUT http://localhost:5001/api/assets/types/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"color\":\"#ff0000\"}'" - }, - { - "method": "GET", - "path": "/api/assets/statuses", - "purpose": "List asset statuses (paginated, active-only by default)", - "auth": "jwt-optional", - "params": "page, per_page, active=false", - "example": "curl http://localhost:5001/api/assets/statuses" - }, - { - "method": "GET", - "path": "/api/assets/statuses/", - "purpose": "Get one asset status", - "auth": "jwt-optional", - "params": "path: status_id", - "example": "curl http://localhost:5001/api/assets/statuses/1" - }, - { - "method": "POST", - "path": "/api/assets/statuses", - "purpose": "Create asset status (409 on duplicate)", - "auth": "permission:assets.create", - "params": "body: status (req), description, color", - "example": "curl -X POST http://localhost:5001/api/assets/statuses -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"status\":\"In Repair\",\"color\":\"#f90\"}'" - }, - { - "method": "PUT", - "path": "/api/assets/statuses/", - "purpose": "Update asset status (rename conflict-checked)", - "auth": "permission:assets.edit", - "params": "body: status, description, color, isactive", - "example": "curl -X PUT http://localhost:5001/api/assets/statuses/2 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"isactive\":false}'" - }, - { - "method": "DELETE", - "path": "/api/assets/statuses/", - "purpose": "Hard-delete asset status; 409 if any asset still uses it", - "auth": "permission:assets.delete", - "params": "path: status_id", - "example": "curl -X DELETE http://localhost:5001/api/assets/statuses/9 -H \"Authorization: Bearer $TOK\"" - }, - { - "method": "GET", - "path": "/api/assets/relationshiptypes", - "purpose": "List relationship types incl. read-only propagatesthrough rails", - "auth": "jwt-optional", - "params": "none", - "example": "curl http://localhost:5001/api/assets/relationshiptypes" - }, - { - "method": "POST", - "path": "/api/assets/relationshiptypes", - "purpose": "Create relationship type (409 on duplicate)", - "auth": "permission:assets.create", - "params": "body: relationshiptype (req), description, color, isdirectional (default true)", - "example": "curl -X POST http://localhost:5001/api/assets/relationshiptypes -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"relationshiptype\":\"controls\",\"isdirectional\":true}'" - }, - { - "method": "PUT", - "path": "/api/assets/relationshiptypes/", - "purpose": "Update relationship type (rename conflict-checked)", - "auth": "permission:assets.edit", - "params": "body: relationshiptype, description, color, isdirectional", - "example": "curl -X PUT http://localhost:5001/api/assets/relationshiptypes/3 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"color\":\"#00f\"}'" - }, - { - "method": "DELETE", - "path": "/api/assets/relationshiptypes/", - "purpose": "Hard-delete relationship type; 409 while relationships use it", - "auth": "permission:assets.delete", - "params": "path: type_id", - "example": "curl -X DELETE http://localhost:5001/api/assets/relationshiptypes/3 -H \"Authorization: Bearer $TOK\"" - }, - { - "method": "GET", - "path": "/api/assets", - "purpose": "List assets with filtering, search, sorting, pagination", - "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", - "example": "curl 'http://localhost:5001/api/assets?type=machine&search=205&include_type_data=true'" - }, - { - "method": "GET", - "path": "/api/assets/", - "purpose": "Get one asset with full details", - "auth": "jwt-optional", - "params": "include_type_data (default true)", - "example": "curl http://localhost:5001/api/assets/42" - }, - { - "method": "POST", - "path": "/api/assets", - "purpose": "Create asset (duplicate assetnumber 409, assettypeid validated); honors X-Import-Mode timestamps", - "auth": "permission:assets.create", - "params": "body: assetnumber (req), assettypeid (req), name, serialnumber, statusid (default 1), locationid, businessunitid, mapx, mapy, notes", - "example": "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\"}'" - }, - { - "method": "PUT", - "path": "/api/assets/", - "purpose": "Update asset (allowed fields incl. isactive); honors X-Import-Mode", - "auth": "permission:assets.edit", - "params": "body: assetnumber, name, serialnumber, assettypeid, statusid, locationid, businessunitid, mapx, mapy, notes, isactive", - "example": "curl -X PUT http://localhost:5001/api/assets/42 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"locationid\":3}'" - }, - { - "method": "DELETE", - "path": "/api/assets/", - "purpose": "Soft-delete asset (isactive=false)", - "auth": "permission:assets.delete", - "params": "path: asset_id", - "example": "curl -X DELETE http://localhost:5001/api/assets/42 -H \"Authorization: Bearer $TOK\"" - }, - { - "method": "GET", - "path": "/api/assets/lookup/", - "purpose": "Look up an active asset by asset number (returns full type data)", - "auth": "jwt-optional", - "params": "path: assetnumber (string)", - "example": "curl http://localhost:5001/api/assets/lookup/0205" - }, - { - "method": "GET", - "path": "/api/assets//relationships", - "purpose": "Get outgoing + incoming active relationships for an asset with partner asset dicts", - "auth": "jwt-optional", - "params": "path: asset_id", - "example": "curl http://localhost:5001/api/assets/42/relationships" - }, - { - "method": "POST", - "path": "/api/assets/relationships", - "purpose": "Create relationship, then fan out across symmetric propagation rails (Dualpath); response carries propagated + propagatedcount", - "auth": "permission:assets.create", - "params": "body: sourceassetid (req), targetassetid (req), relationshiptypeid (req), notes; X-Import-Mode honored", - "example": "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}'" - }, - { - "method": "DELETE", - "path": "/api/assets/relationships/", - "purpose": "Soft-delete one relationship row (no cascade to propagated partner rows)", - "auth": "permission:assets.delete", - "params": "path: rel_id", - "example": "curl -X DELETE http://localhost:5001/api/assets/relationships/7 -H \"Authorization: Bearer $TOK\"" - }, - { - "method": "GET", - "path": "/api/assets/map", - "purpose": "Unified floor-map payload: all mapped assets (with type data, primary IP, dualpath collapse) plus filter option lists", - "auth": "jwt-optional", - "params": "assettype (name), subtype (id, per-type), businessunitid, statusid, locationid, search", - "example": "curl 'http://localhost:5001/api/assets/map?assettype=machine&statusid=1'" - }, - { - "method": "GET", - "path": "/api/assets//communications", - "purpose": "List active communications (IPs etc.) for an asset with comtype_name", - "auth": "jwt-optional", - "params": "path: asset_id", - "example": "curl http://localhost:5001/api/assets/42/communications" - }, - { - "method": "GET", - "path": "/api/locations/types", - "purpose": "List location types", - "auth": "jwt-optional", - "params": "active=false includes inactive", - "example": "curl http://localhost:5001/api/locations/types" - }, - { - "method": "POST", - "path": "/api/locations/types", - "purpose": "Create location type; reactivates a soft-deleted same-name type instead of 409", - "auth": "admin", - "params": "body: locationtype (req), description, color", - "example": "curl -X POST http://localhost:5001/api/locations/types -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"locationtype\":\"Operation\"}'" - }, - { - "method": "PUT", - "path": "/api/locations/types/", - "purpose": "Update location type (rename conflict-checked)", - "auth": "admin", - "params": "body: locationtype, description, color, isactive", - "example": "curl -X PUT http://localhost:5001/api/locations/types/2 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"color\":\"#0a0\"}'" - }, - { - "method": "DELETE", - "path": "/api/locations/types/", - "purpose": "Hard-delete location type; 409 while locations use it", - "auth": "admin", - "params": "path: type_id", - "example": "curl -X DELETE http://localhost:5001/api/locations/types/2 -H \"Authorization: Bearer $TOK\"" - }, - { - "method": "GET", - "path": "/api/locations", - "purpose": "List locations (paginated); exact locationname lookup for idempotent import", - "auth": "jwt-optional", - "params": "page, per_page, active, locationname (exact), search (name/building ilike)", - "example": "curl 'http://localhost:5001/api/locations?locationname=Building%201'" - }, - { - "method": "GET", - "path": "/api/locations/", - "purpose": "Get one location", - "auth": "jwt-optional", - "params": "path: location_id", - "example": "curl http://localhost:5001/api/locations/3" - }, - { - "method": "POST", - "path": "/api/locations", - "purpose": "Create location (409 on duplicate name); honors X-Import-Mode", - "auth": "admin", - "params": "body: locationname (req), building, floor, room, description, locationtypeid, parentlocationid, mapimage, mapwidth, mapheight", - "example": "curl -X POST http://localhost:5001/api/locations -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"locationname\":\"Cell 12\",\"building\":\"B1\"}'" - }, - { - "method": "PUT", - "path": "/api/locations/", - "purpose": "Update location (rename conflict-checked); honors X-Import-Mode", - "auth": "admin", - "params": "body: locationname, building, floor, room, description, locationtypeid, parentlocationid, mapimage, mapwidth, mapheight, isactive", - "example": "curl -X PUT http://localhost:5001/api/locations/3 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"room\":\"104\"}'" - }, - { - "method": "DELETE", - "path": "/api/locations/", - "purpose": "Soft-delete location", - "auth": "admin", - "params": "path: location_id", - "example": "curl -X DELETE http://localhost:5001/api/locations/3 -H \"Authorization: Bearer $TOK\"" - }, - { - "method": "GET", - "path": "/api/vendors", - "purpose": "List vendors (paginated); exact vendor lookup for idempotent import", - "auth": "jwt-optional", - "params": "page, per_page, active, vendor (exact), search (ilike)", - "example": "curl 'http://localhost:5001/api/vendors?search=fanuc'" - }, - { - "method": "GET", - "path": "/api/vendors/", - "purpose": "Get one vendor", - "auth": "jwt-optional", - "params": "path: vendor_id", - "example": "curl http://localhost:5001/api/vendors/5" - }, - { - "method": "POST", - "path": "/api/vendors", - "purpose": "Create vendor (409 on duplicate); honors X-Import-Mode", - "auth": "admin", - "params": "body: vendor (req), description, website, supportphone, supportemail, notes", - "example": "curl -X POST http://localhost:5001/api/vendors -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"vendor\":\"Fanuc\"}'" - }, - { - "method": "PUT", - "path": "/api/vendors/", - "purpose": "Update vendor (rename conflict-checked); honors X-Import-Mode", - "auth": "admin", - "params": "body: vendor, description, website, supportphone, supportemail, notes, isactive", - "example": "curl -X PUT http://localhost:5001/api/vendors/5 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"website\":\"https://fanuc.com\"}'" - }, - { - "method": "DELETE", - "path": "/api/vendors/", - "purpose": "Soft-delete vendor", - "auth": "admin", - "params": "path: vendor_id", - "example": "curl -X DELETE http://localhost:5001/api/vendors/5 -H \"Authorization: Bearer $TOK\"" - }, - { - "method": "GET", - "path": "/api/models", - "purpose": "List vendor catalog models (paginated) with flattened vendor/modeltype names; exact modelnumber+vendor lookup for import", - "auth": "jwt-optional", - "params": "page, per_page, active, vendor (id), modeltype (id), modelnumber (exact), search (ilike)", - "example": "curl 'http://localhost:5001/api/models?vendor=5&search=30i'" - }, - { - "method": "GET", - "path": "/api/models/", - "purpose": "Get one model with nested vendor + modeltype dicts", - "auth": "jwt-optional", - "params": "path: model_id", - "example": "curl http://localhost:5001/api/models/12" - }, - { - "method": "POST", - "path": "/api/models", - "purpose": "Create model (409 on duplicate modelnumber+vendorid); honors X-Import-Mode", - "auth": "admin", - "params": "body: modelnumber (req), vendorid, modeltypeid, description, imageurl, documentationurl, notes", - "example": "curl -X POST http://localhost:5001/api/models -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"modelnumber\":\"R-30iB\",\"vendorid\":5}'" - }, - { - "method": "PUT", - "path": "/api/models/", - "purpose": "Update model; honors X-Import-Mode", - "auth": "admin", - "params": "body: modelnumber, vendorid, modeltypeid, description, imageurl, documentationurl, notes, isactive", - "example": "curl -X PUT http://localhost:5001/api/models/12 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"notes\":\"EOL 2027\"}'" - }, - { - "method": "DELETE", - "path": "/api/models/", - "purpose": "Soft-delete model", - "auth": "admin", - "params": "path: model_id", - "example": "curl -X DELETE http://localhost:5001/api/models/12 -H \"Authorization: Bearer $TOK\"" - }, - { - "method": "POST", - "path": "/api/models//image", - "purpose": "Upload/replace model photo (saved as instance/modelimages/model-, one per model); sets imageurl", - "auth": "admin", - "params": "multipart/form-data: file= (.png/.jpg/.jpeg/.gif/.webp/.svg)", - "example": "curl -X POST http://localhost:5001/api/models/12/image -H \"Authorization: Bearer $TOK\" -F file=@robot.jpg" - }, - { - "method": "GET", - "path": "/api/models/image/", - "purpose": "Serve an uploaded model image (deliberately public - asset detail pages read it without auth)", - "auth": "none", - "params": "path: filename", - "example": "curl http://localhost:5001/api/models/image/model-12.jpg" - }, - { - "method": "DELETE", - "path": "/api/models//image", - "purpose": "Clear imageurl and delete the uploaded file only if it lives under /api/models/image/ (external URLs untouched)", - "auth": "admin", - "params": "path: model_id", - "example": "curl -X DELETE http://localhost:5001/api/models/12/image -H \"Authorization: Bearer $TOK\"" - }, - { - "method": "GET", - "path": "/api/modeltypes", - "purpose": "List model types (types the vendor MODELS catalog, not machines); exact modeltype lookup for import", - "auth": "jwt-optional", - "params": "page, per_page, active, category, modeltype (exact), search (ilike)", - "example": "curl 'http://localhost:5001/api/modeltypes?category=Equipment'" - }, - { - "method": "GET", - "path": "/api/modeltypes/", - "purpose": "Get one model type", - "auth": "jwt-optional", - "params": "path: type_id", - "example": "curl http://localhost:5001/api/modeltypes/2" - }, - { - "method": "POST", - "path": "/api/modeltypes", - "purpose": "Create model type (409 on duplicate); category defaults to Equipment; honors X-Import-Mode", - "auth": "admin", - "params": "body: modeltype (req), category, description, icon", - "example": "curl -X POST http://localhost:5001/api/modeltypes -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"modeltype\":\"Controller\"}'" - }, - { - "method": "PUT", - "path": "/api/modeltypes/", - "purpose": "Update model type (rename conflict-checked); honors X-Import-Mode", - "auth": "admin", - "params": "body: modeltype, category, description, icon, isactive", - "example": "curl -X PUT http://localhost:5001/api/modeltypes/2 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"icon\":\"mdi-chip\"}'" - }, - { - "method": "DELETE", - "path": "/api/modeltypes/", - "purpose": "Soft-delete model type; 409 while models use it", - "auth": "admin", - "params": "path: type_id", - "example": "curl -X DELETE http://localhost:5001/api/modeltypes/2 -H \"Authorization: Bearer $TOK\"" - }, - { - "method": "GET", - "path": "/api/businessunits", - "purpose": "List business units (paginated); exact businessunit lookup for import", - "auth": "jwt-optional", - "params": "page, per_page, active, businessunit (exact), search (name/code ilike)", - "example": "curl http://localhost:5001/api/businessunits" - }, - { - "method": "GET", - "path": "/api/businessunits/", - "purpose": "Get one business unit with parent + children", - "auth": "jwt-optional", - "params": "path: bu_id", - "example": "curl http://localhost:5001/api/businessunits/1" - }, - { - "method": "POST", - "path": "/api/businessunits", - "purpose": "Create business unit (409 on duplicate); honors X-Import-Mode", - "auth": "admin", - "params": "body: businessunit (req), code, description, parentid", - "example": "curl -X POST http://localhost:5001/api/businessunits -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"businessunit\":\"Blades\",\"code\":\"BLD\"}'" - }, - { - "method": "PUT", - "path": "/api/businessunits/", - "purpose": "Update business unit (rename conflict-checked); honors X-Import-Mode", - "auth": "admin", - "params": "body: businessunit, code, description, parentid, isactive", - "example": "curl -X PUT http://localhost:5001/api/businessunits/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"code\":\"BL\"}'" - }, - { - "method": "DELETE", - "path": "/api/businessunits/", - "purpose": "Soft-delete business unit", - "auth": "admin", - "params": "path: bu_id", - "example": "curl -X DELETE http://localhost:5001/api/businessunits/1 -H \"Authorization: Bearer $TOK\"" - }, - { - "method": "GET", - "path": "/api/operatingsystems", - "purpose": "List operating systems (paginated); exact osname/osversion lookup for import", - "auth": "jwt-optional", - "params": "page, per_page, active, osname (exact), osversion (exact), search (osname ilike)", - "example": "curl 'http://localhost:5001/api/operatingsystems?osname=Windows%2011'" - }, - { - "method": "GET", - "path": "/api/operatingsystems/", - "purpose": "Get one operating system", - "auth": "jwt-optional", - "params": "path: os_id", - "example": "curl http://localhost:5001/api/operatingsystems/4" - }, - { - "method": "POST", - "path": "/api/operatingsystems", - "purpose": "Create OS (409 on duplicate osname+osversion); honors X-Import-Mode", - "auth": "admin", - "params": "body: osname (req), osversion, architecture, endoflife", - "example": "curl -X POST http://localhost:5001/api/operatingsystems -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"osname\":\"Windows 11\",\"osversion\":\"24H2\"}'" - }, - { - "method": "PUT", - "path": "/api/operatingsystems/", - "purpose": "Update OS; honors X-Import-Mode", - "auth": "admin", - "params": "body: osname, osversion, architecture, endoflife, isactive", - "example": "curl -X PUT http://localhost:5001/api/operatingsystems/4 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"endoflife\":\"2031-10-14\"}'" - }, - { - "method": "DELETE", - "path": "/api/operatingsystems/", - "purpose": "Soft-delete OS", - "auth": "admin", - "params": "path: os_id", - "example": "curl -X DELETE http://localhost:5001/api/operatingsystems/4 -H \"Authorization: Bearer $TOK\"" - }, - { - "method": "GET", - "path": "/api/customfields", - "purpose": "List custom-field definitions, ordered by sortorder", - "auth": "jwt-optional", - "params": "assettypeid (int), active=false includes inactive", - "example": "curl 'http://localhost:5001/api/customfields?assettypeid=1'" - }, - { - "method": "POST", - "path": "/api/customfields", - "purpose": "Create field definition; fieldkey auto-slugged from label; reactivates a soft-deleted same-key field instead of 409", - "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", - "example": "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\"]}'" - }, - { - "method": "PUT", - "path": "/api/customfields/", - "purpose": "Update field definition (label/datatype/options/flags/sortorder)", - "auth": "admin", - "params": "body: label, datatype, options, showondetail, showonform, isactive, searchable, sortorder", - "example": "curl -X PUT http://localhost:5001/api/customfields/7 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"searchable\":true}'" - }, - { - "method": "DELETE", - "path": "/api/customfields/", - "purpose": "Hard-delete field definition AND all stored values for it", - "auth": "admin", - "params": "path: fieldid", - "example": "curl -X DELETE http://localhost:5001/api/customfields/7 -H \"Authorization: Bearer $TOK\"" - }, - { - "method": "GET", - "path": "/api/customfields/asset/", - "purpose": "Active field defs for the asset's type merged with the asset's stored values", - "auth": "jwt-optional", - "params": "path: assetid", - "example": "curl http://localhost:5001/api/customfields/asset/42" - }, - { - "method": "PUT", - "path": "/api/customfields/asset/", - "purpose": "Upsert per-asset values; empty string clears a value; only fields of the asset's type accepted", - "auth": "admin", - "params": "body: {values: {fieldid: value, ...}}", - "example": "curl -X PUT http://localhost:5001/api/customfields/asset/42 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"values\":{\"7\":\"Oil\"}}'" - }, - { - "method": "GET", - "path": "/api/supportteams", - "purpose": "List support teams with contacts; exact teamname lookup for import (not paginated)", - "auth": "jwt-optional", - "params": "active, teamname (exact)", - "example": "curl http://localhost:5001/api/supportteams" - }, - { - "method": "GET", - "path": "/api/supportteams/", - "purpose": "Get one support team with contacts", - "auth": "jwt-optional", - "params": "path: team_id", - "example": "curl http://localhost:5001/api/supportteams/2" - }, - { - "method": "POST", - "path": "/api/supportteams", - "purpose": "Create support team (409 on duplicate); audit-logged; honors X-Import-Mode", - "auth": "admin", - "params": "body: teamname (req), teamurl, webhookurl, isactive", - "example": "curl -X POST http://localhost:5001/api/supportteams -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"teamname\":\"CNC Support\"}'" - }, - { - "method": "PUT", - "path": "/api/supportteams/", - "purpose": "Update support team (rename conflict-checked); audit-logged; honors X-Import-Mode", - "auth": "admin", - "params": "body: teamname, teamurl, webhookurl, isactive", - "example": "curl -X PUT http://localhost:5001/api/supportteams/2 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"webhookurl\":\"https://hooks/x\"}'" - }, - { - "method": "DELETE", - "path": "/api/supportteams/", - "purpose": "Hard-delete team (cascade removes contacts); 409 while applications reference it; audit-logged", - "auth": "admin", - "params": "path: team_id", - "example": "curl -X DELETE http://localhost:5001/api/supportteams/2 -H \"Authorization: Bearer $TOK\"" - }, - { - "method": "POST", - "path": "/api/supportteams//contacts", - "purpose": "Add contact to a team; audit-logged; honors X-Import-Mode", - "auth": "admin", - "params": "body: name (req), sso, sortorder, isactive", - "example": "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\"}'" - }, - { - "method": "PUT", - "path": "/api/supportteams//contacts/", - "purpose": "Update a team contact; audit-logged; honors X-Import-Mode", - "auth": "admin", - "params": "body: name, sso, sortorder, isactive", - "example": "curl -X PUT http://localhost:5001/api/supportteams/2/contacts/9 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"sortorder\":1}'" - }, - { - "method": "DELETE", - "path": "/api/supportteams//contacts/", - "purpose": "Hard-delete a team contact; audit-logged", - "auth": "admin", - "params": "path: team_id, contact_id", - "example": "curl -X DELETE http://localhost:5001/api/supportteams/2/contacts/9 -H \"Authorization: Bearer $TOK\"" - }, - { - "method": "GET", - "path": "/api/applications", - "purpose": "List applications (paginated) with installedcount; hidden apps excluded unless showhidden=true; exact appname lookup for import", - "auth": "jwt-optional", - "params": "page, per_page, active, showhidden, installable (true/false), appname (exact), search (name/description ilike)", - "example": "curl 'http://localhost:5001/api/applications?installable=true'" - }, - { - "method": "GET", - "path": "/api/applications/", - "purpose": "Get one application with active versions, installedcount, and linked KB articles", - "auth": "jwt-optional", - "params": "path: app_id", - "example": "curl http://localhost:5001/api/applications/15" - }, - { - "method": "POST", - "path": "/api/applications", - "purpose": "Create application (409 on duplicate name); audit-logged; honors X-Import-Mode", - "auth": "permission:applications.create", - "params": "body: appname (req), appdescription, supportteamid, isinstallable, applicationnotes, installpath, applicationlink, documentationpath, ishidden, isprinter, islicenced, isrequired, image", - "example": "curl -X POST http://localhost:5001/api/applications -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"appname\":\"PC-DMIS\",\"isinstallable\":true}'" - }, - { - "method": "PUT", - "path": "/api/applications/", - "purpose": "Update application (rename conflict-checked); field-level change diff audit-logged; honors X-Import-Mode", - "auth": "permission:applications.edit", - "params": "body: any of appname, appdescription, supportteamid, isinstallable, applicationnotes, installpath, applicationlink, documentationpath, ishidden, isprinter, islicenced, isrequired, image, isactive", - "example": "curl -X PUT http://localhost:5001/api/applications/15 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"supportteamid\":2}'" - }, - { - "method": "DELETE", - "path": "/api/applications/", - "purpose": "Soft-delete application; audit-logged", - "auth": "permission:applications.delete", - "params": "path: app_id", - "example": "curl -X DELETE http://localhost:5001/api/applications/15 -H \"Authorization: Bearer $TOK\"" - }, - { - "method": "GET", - "path": "/api/applications//versions", - "purpose": "List active versions of an application (desc)", - "auth": "jwt-optional", - "params": "path: app_id", - "example": "curl http://localhost:5001/api/applications/15/versions" - }, - { - "method": "POST", - "path": "/api/applications//versions", - "purpose": "Create app version (409 on duplicate version per app); honors X-Import-Mode for legacy dates", - "auth": "permission:applications.create", - "params": "body: version (req), releasedate, notes", - "example": "curl -X POST http://localhost:5001/api/applications/15/versions -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"version\":\"2024.2\"}'" - }, - { - "method": "GET", - "path": "/api/applications//installed", - "purpose": "List computers with this app installed (503 if computers plugin absent)", - "auth": "jwt-optional", - "params": "path: app_id", - "example": "curl http://localhost:5001/api/applications/15/installed" - }, - { - "method": "GET", - "path": "/api/applications/machines/", - "purpose": "List apps installed on a computer (machine_id is a computerid; 503 without computers plugin)", - "auth": "jwt-optional", - "params": "path: machine_id", - "example": "curl http://localhost:5001/api/applications/machines/8" - }, - { - "method": "POST", - "path": "/api/applications/machines/", - "purpose": "Install an app on a computer; reactivates a prior soft-deleted install; 409 if already installed", - "auth": "permission:applications.create", - "params": "body: appid (req), appversionid", - "example": "curl -X POST http://localhost:5001/api/applications/machines/8 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"appid\":15}'" - }, - { - "method": "DELETE", - "path": "/api/applications/machines//", - "purpose": "Uninstall (soft-delete install row) an app from a computer", - "auth": "permission:applications.delete", - "params": "path: machine_id, app_id", - "example": "curl -X DELETE http://localhost:5001/api/applications/machines/8/15 -H \"Authorization: Bearer $TOK\"" - }, - { - "method": "PUT", - "path": "/api/applications/machines//", - "purpose": "Update an installed-app row (change appversionid)", - "auth": "permission:applications.edit", - "params": "body: appversionid", - "example": "curl -X PUT http://localhost:5001/api/applications/machines/8/15 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"appversionid\":3}'" - }, - { - "method": "POST", - "path": "/api/applications//image", - "auth": "jwt + applications.edit", - "params": "multipart/form-data: file= (png/jpg/gif/webp/svg/ico)", - "purpose": "Upload or replace an application's image; sets application.image to the served URL", - "example": "curl -X POST -H 'Authorization: Bearer ' -F file=@logo.png http://localhost:5001/api/applications/5/image" - }, - { - "method": "GET", - "path": "/api/applications/image/", - "auth": "none", - "params": "none", - "purpose": "Serve an uploaded application image. Public: application tiles render before login", - "example": "curl http://localhost:5001/api/applications/image/application-5.png" - }, - { - "method": "DELETE", - "path": "/api/applications//image", - "auth": "jwt + applications.edit", - "params": "none", - "purpose": "Remove an application's image and clear the field", - "example": "curl -X DELETE -H 'Authorization: Bearer ' http://localhost:5001/api/applications/5/image" - }, - { - "method": "POST", - "path": "/api/applications//package", - "auth": "jwt + applications.edit", - "params": "multipart/form-data: file= (exe/msi/msp/zip/7z/cab/iso/appx/msix/ps1/bat/txt/pdf), max 500MB", - "purpose": "Upload or replace the installer; sets installpath to the download URL", - "example": "curl -X POST -H 'Authorization: Bearer ' -F file=@setup.msi http://localhost:5001/api/applications/5/package" - }, - { - "method": "GET", - "path": "/api/applications/package/", - "auth": "jwt + applications.view", - "params": "none", - "purpose": "Download an uploaded installer as an attachment. Authenticated: licensed vendor software", - "example": "curl -OJ -H 'Authorization: Bearer ' http://localhost:5001/api/applications/package/application-5.msi" - }, - { - "method": "DELETE", - "path": "/api/applications//package", - "auth": "jwt + applications.edit", - "params": "none", - "purpose": "Remove an uploaded installer; clears installpath only when it pointed at the upload", - "example": "curl -X DELETE -H 'Authorization: Bearer ' http://localhost:5001/api/applications/5/package" - } - ] - }, - { - "endpoints": [ - { - "method": "GET", - "path": "/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", - "example": "curl -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/computers/types?search=kiosk&per_page=50'" - }, - { - "method": "GET", - "path": "/api/computers/types/", - "auth": "jwt-optional", - "params": "type_id in path", - "purpose": "Get a single computer type by ID", - "example": "curl 'http://localhost:5001/api/computers/types/3'" - }, - { - "method": "POST", - "path": "/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", - "example": "curl -X POST -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"computertype\":\"Shopfloor\",\"color\":\"#0066cc\"}' 'http://localhost:5001/api/computers/types'" - }, - { - "method": "PUT", - "path": "/api/computers/types/", - "auth": "permission:computers.edit (jwt_required)", - "params": "body: computertype, description, icon, color, isactive; 409 on duplicate name", - "purpose": "Update a computer type", - "example": "curl -X PUT -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"isactive\":false}' 'http://localhost:5001/api/computers/types/3'" - }, - { - "method": "DELETE", - "path": "/api/computers/types/", - "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", - "example": "curl -X DELETE -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/computers/types/3'" - }, - { - "method": "GET", - "path": "/api/computers/protocols", - "auth": "jwt-optional", - "params": "active (default true; 'false' includes disabled)", - "purpose": "List remote-access protocols (VNC/WinRM/RDP catalog), unpaginated", - "example": "curl 'http://localhost:5001/api/computers/protocols?active=false'" - }, - { - "method": "POST", - "path": "/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", - "example": "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'" - }, - { - "method": "PUT|PATCH", - "path": "/api/computers/protocols/", - "auth": "permission:computers.edit (jwt_required)", - "params": "body: name, scheme, linktemplate, defaultport, isactive (all optional)", - "purpose": "Update an access protocol", - "example": "curl -X PATCH -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"defaultport\":5901}' 'http://localhost:5001/api/computers/protocols/2'" - }, - { - "method": "DELETE", - "path": "/api/computers/protocols/", - "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)", - "example": "curl -X DELETE -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/computers/protocols/2'" - }, - { - "method": "GET", - "path": "/api/computers/display-kiosks", - "auth": "jwt-optional", - "params": "none; uses pctype mapping for gea-shopfloor-display (default 'Kiosk') and display_fqdn_domain setting", - "purpose": "List display-kiosk computers with derived F. FQDN for the Dashboard Defaults picker", - "example": "curl -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/computers/display-kiosks'" - }, - { - "method": "GET", - "path": "/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", - "example": "curl 'http://localhost:5001/api/computers?shopfloor=true&sort=lastreporteddate&dir=desc&per_page=25'" - }, - { - "method": "GET", - "path": "/api/computers/", - "auth": "jwt-optional", - "params": "computer_id in path", - "purpose": "Get one computer with full detail: asset fields, computer extension, communications, resolved access links", - "example": "curl 'http://localhost:5001/api/computers/42'" - }, - { - "method": "GET", - "path": "/api/computers/by-asset/", - "auth": "jwt-optional", - "params": "asset_id in path", - "purpose": "Get computer record by its core asset ID", - "example": "curl 'http://localhost:5001/api/computers/by-asset/1234'" - }, - { - "method": "GET", - "path": "/api/computers/by-hostname/", - "auth": "jwt-optional", - "params": "hostname in path (exact match)", - "purpose": "Get computer record by hostname", - "example": "curl 'http://localhost:5001/api/computers/by-hostname/WKSTN0042'" - }, - { - "method": "POST", - "path": "/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", - "example": "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'" - }, - { - "method": "PUT", - "path": "/api/computers/", - "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 protocol list; 409 on assetnumber/hostname conflict; changes audit-logged", - "purpose": "Update a computer's asset and extension fields", - "example": "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'" - }, - { - "method": "DELETE", - "path": "/api/computers/", - "auth": "permission:computers.delete (jwt_required)", - "params": "computer_id in path", - "purpose": "Soft-delete a computer (sets asset isactive=false), audit-logged", - "example": "curl -X DELETE -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/computers/42'" - }, - { - "method": "GET", - "path": "/api/computers//apps", - "auth": "jwt-optional", - "params": "computer_id in path; returns active installs only", - "purpose": "List installed applications on a computer", - "example": "curl 'http://localhost:5001/api/computers/42/apps'" - }, - { - "method": "POST", - "path": "/api/computers//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", - "example": "curl -X POST -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"appid\":17,\"appversionid\":3}' 'http://localhost:5001/api/computers/42/apps'" - }, - { - "method": "DELETE", - "path": "/api/computers//apps/", - "auth": "permission:computers.delete (jwt_required)", - "params": "computer_id and app_id in path", - "purpose": "Soft-remove an installed application (isactive=false)", - "example": "curl -X DELETE -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/computers/42/apps/17'" - }, - { - "method": "POST", - "path": "/api/computers//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", - "example": "curl -X POST -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"loggedinuser\":\"jsmith\",\"lastboottime\":\"2026-07-30T06:00:00\"}' 'http://localhost:5001/api/computers/42/report'" - }, - { - "method": "GET", - "path": "/api/computers/dashboard/summary", - "auth": "jwt-optional", - "params": "none", - "purpose": "Dashboard counts: total active computers, breakdown by type and OS, shopfloor vs non-shopfloor", - "example": "curl 'http://localhost:5001/api/computers/dashboard/summary'" - } - ], - "surface": "plugin-computers" - }, - { - "surface": "plugin-measuringtools", - "endpoints": [ - { - "method": "GET", - "path": "/api/measuringtools/types", - "purpose": "List measuring-tool types (active-only by default), paginated, name-sorted.", - "auth": "jwt-optional", - "params": "query: active (default true; 'false' includes inactive), search (name ilike), page, perpage", - "example": "curl 'http://localhost:5001/api/measuringtools/types?search=caliper&page=1&perpage=25'" - }, - { - "method": "GET", - "path": "/api/measuringtools/types/", - "purpose": "Get one measuring-tool type by id (404 if missing).", - "auth": "jwt-optional", - "params": "path: type_id (int)", - "example": "curl 'http://localhost:5001/api/measuringtools/types/3'" - }, - { - "method": "POST", - "path": "/api/measuringtools/types", - "purpose": "Create a measuring-tool type; reactivates a soft-deleted same-named one, 409 if an active one exists.", - "auth": "jwt + permission:measuringtools.create", - "params": "body JSON: name (required), description, color", - "example": "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\"}'" - }, - { - "method": "PUT", - "path": "/api/measuringtools/types/", - "purpose": "Update a measuring-tool type; 409 on rename collision with an existing name.", - "auth": "jwt + permission:measuringtools.edit", - "params": "path: type_id; body JSON: name, description, color, isactive (only keys present are applied)", - "example": "curl -X PUT 'http://localhost:5001/api/measuringtools/types/3' -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"description\":\"Updated\",\"isactive\":true}'" - }, - { - "method": "DELETE", - "path": "/api/measuringtools/types/", - "purpose": "Hard-delete a measuring-tool type; refused with 409 if any tool still references it.", - "auth": "jwt + permission:measuringtools.delete", - "params": "path: type_id (int)", - "example": "curl -X DELETE 'http://localhost:5001/api/measuringtools/types/3' -H \"Authorization: Bearer $TOKEN\"" - }, - { - "method": "GET", - "path": "/api/measuringtools", - "purpose": "List measuring tools (Asset core merged with extension), filtered + paginated; derived calibrationstatus filter applied post-pagination.", - "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", - "example": "curl 'http://localhost:5001/api/measuringtools?typeid=2&calibrationstatus=overdue&page=1&perpage=50'" - }, - { - "method": "GET", - "path": "/api/measuringtools/", - "purpose": "Get one measuring tool by measuringtoolid, asset core dict with extension nested under 'measuringtool'.", - "auth": "jwt-optional", - "params": "path: tool_id (int)", - "example": "curl 'http://localhost:5001/api/measuringtools/17'" - }, - { - "method": "GET", - "path": "/api/measuringtools/by-asset/", - "purpose": "Get a measuring tool by its core assetid (404 if the asset has no extension row).", - "auth": "jwt-optional", - "params": "path: asset_id (int)", - "example": "curl 'http://localhost:5001/api/measuringtools/by-asset/1042'" - }, - { - "method": "POST", - "path": "/api/measuringtools", - "purpose": "Create a measuring tool: one Asset core row (assettype 'measuring_tool') plus one measuringtools extension row in a single payload; audit-logged.", - "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", - "example": "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\"}'" - }, - { - "method": "PUT", - "path": "/api/measuringtools/", - "purpose": "Update asset core fields and extension fields in one payload; 409 on assetnumber collision; changes audit-logged.", - "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)", - "example": "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\"}'" - }, - { - "method": "DELETE", - "path": "/api/measuringtools/", - "purpose": "Soft-delete a measuring tool by setting its asset isactive=false; audit-logged.", - "auth": "jwt + permission:measuringtools.delete", - "params": "path: tool_id (int)", - "example": "curl -X DELETE 'http://localhost:5001/api/measuringtools/17' -H \"Authorization: Bearer $TOKEN\"" - }, - { - "method": "GET", - "path": "/api/measuringtools/map-overlay", - "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.", - "auth": "jwt-optional", - "params": "none", - "example": "curl 'http://localhost:5001/api/measuringtools/map-overlay'" - }, - { - "method": "GET", - "path": "/api/measuringtools/report/calibration", - "purpose": "Calibration report for the Reports hub: counts and full tool lists bucketed by derived status (overdue/duesoon/current/unknown) plus statuscolors map.", - "auth": "jwt-optional", - "params": "none", - "example": "curl 'http://localhost:5001/api/measuringtools/report/calibration'" - } - ] - }, - { - "surface": "plugin-machines", - "endpoints": [ - { - "method": "GET", - "path": "/api/machines/types", - "purpose": "List machine types (active by default) with pagination and name search", - "auth": "jwt-optional", - "params": "query: page, per_page, active (pass 'false' to include inactive), search (ilike on machinetype)", - "example": "curl 'http://localhost:5001/api/machines/types?search=cnc&page=1&per_page=25'" - }, - { - "method": "GET", - "path": "/api/machines/types/", - "purpose": "Get a single machine type by ID", - "auth": "jwt-optional", - "params": "path: type_id (int)", - "example": "curl 'http://localhost:5001/api/machines/types/3'" - }, - { - "method": "POST", - "path": "/api/machines/types", - "purpose": "Create a machine type; reactivates a soft-deleted type of the same name instead of 409ing", - "auth": "jwt + permission:machines.create", - "params": "body: machinetype (required), description, icon, color", - "example": "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\"}'" - }, - { - "method": "PUT", - "path": "/api/machines/types/", - "purpose": "Update a machine type (rename guarded by 409 on duplicate name)", - "auth": "jwt + permission:machines.edit", - "params": "path: type_id; body: any of machinetype, description, icon, color, isactive", - "example": "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}'" - }, - { - "method": "DELETE", - "path": "/api/machines/types/", - "purpose": "Hard-delete a machine type; refused with 409 if any machine still references it", - "auth": "jwt + permission:machines.delete", - "params": "path: type_id (int)", - "example": "curl -X DELETE 'http://localhost:5001/api/machines/types/3' -H \"Authorization: Bearer $TOKEN\"" - }, - { - "method": "GET", - "path": "/api/machines", - "purpose": "List machines (Asset+Machine join) with filters, sorting, pagination; collapses Dualpath dual-bay pairs to one row (annotated with dualpathpartner) when site setting enabled", - "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)", - "example": "curl 'http://localhost:5001/api/machines?search=2007&typeid=2&sort=name&dir=desc&page=1&per_page=50'" - }, - { - "method": "GET", - "path": "/api/machines/", - "purpose": "Get one machine with full asset details, nested machine dict, and dualpathpartner info", - "auth": "jwt-optional", - "params": "path: machine_id (int)", - "example": "curl 'http://localhost:5001/api/machines/42'" - }, - { - "method": "GET", - "path": "/api/machines/by-asset/", - "purpose": "Get machine data looked up by core asset ID instead of machine ID", - "auth": "jwt-optional", - "params": "path: asset_id (int)", - "example": "curl 'http://localhost:5001/api/machines/by-asset/1001'" - }, - { - "method": "POST", - "path": "/api/machines", - "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", - "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", - "example": "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}'" - }, - { - "method": "PUT", - "path": "/api/machines/", - "purpose": "Update machine (asset + machine fields), with per-field change tracking to AuditLog and 409 on assetnumber conflict", - "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", - "example": "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\"}'" - }, - { - "method": "DELETE", - "path": "/api/machines/", - "purpose": "Soft-delete a machine (sets asset.isactive=False, keeps Machine row linked); audit-logged", - "auth": "jwt + permission:machines.delete", - "params": "path: machine_id (int)", - "example": "curl -X DELETE 'http://localhost:5001/api/machines/42' -H \"Authorization: Bearer $TOKEN\"" - }, - { - "method": "GET", - "path": "/api/machines/dashboard/summary", - "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 collapse setting enabled; by-status does not)", - "auth": "jwt-optional", - "params": "none", - "example": "curl 'http://localhost:5001/api/machines/dashboard/summary'" - } - ] - }, - { - "surface": "plugin-geenforce", - "endpoints": [ - { - "method": "GET", - "path": "/api/geenforce/manifest", - "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.", - "auth": "api-key (managed service token with geenforce.fetch scope via X-API-Key or Bearer PAT) OR source IP in geenforce_allowed_cidrs setting; resource-bound tokens restricted to their listed scopes (403 otherwise)", - "params": "query: pctype (required, =scopename), phase (default 'runtime'); header: If-None-Match for 304", - "example": "curl -H \"X-API-Key: $TOKEN\" 'http://localhost:5001/api/geenforce/manifest?pctype=cmm&phase=runtime'" - }, - { - "method": "GET", - "path": "/api/geenforce/payload/", - "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).", - "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)", - "example": "curl -H \"X-API-Key: $TOKEN\" -o installer.exe 'http://localhost:5001/api/geenforce/payload/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'" - }, - { - "method": "POST", - "path": "/api/geenforce/report", - "purpose": "Record one PC's enforcement cycle: applied manifest version plus per-entry self-heal outcomes (installed/skipped/failed); returns reportid + status.", - "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", - "example": "curl -X POST -H \"X-API-Key: $TOKEN\" -H 'Content-Type: application/json' -d '{\"hostname\":\"WKSTN0042\",\"scopename\":\"cmm\",\"appliedversion\":4,\"results\":[{\"entryname\":\"7zip\",\"action\":\"installed\"}]}' http://localhost:5001/api/geenforce/report" - }, - { - "method": "GET", - "path": "/api/geenforce/scopes", - "purpose": "List all imaging PC-type scopes with entry counts and current published version numbers, ordered by phase then scopename.", - "auth": "jwt + permission:geenforce.manage", - "params": "none", - "example": "curl -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/scopes" - }, - { - "method": "POST", - "path": "/api/geenforce/scopes", - "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.", - "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')", - "example": "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" - }, - { - "method": "GET", - "path": "/api/geenforce/scopes/", - "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).", - "auth": "jwt + permission:geenforce.manage", - "params": "path: scopeid", - "example": "curl -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/scopes/3" - }, - { - "method": "PUT", - "path": "/api/geenforce/scopes/", - "purpose": "Update scope metadata fields (only keys present in the body are changed); scopename and phase are immutable here.", - "auth": "jwt + permission:geenforce.manage", - "params": "JSON body (all optional): description, computertypeid, measuringtooltypeid, manifestversion (stringified), iscommon (bool-coerced)", - "example": "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" - }, - { - "method": "DELETE", - "path": "/api/geenforce/scopes/", - "purpose": "Delete a scope (and via cascade its entries); returns {deleted: scopeid}.", - "auth": "jwt + permission:geenforce.manage", - "params": "path: scopeid", - "example": "curl -X DELETE -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/scopes/3" - }, - { - "method": "GET", - "path": "/api/geenforce/scopes//preview", - "purpose": "Render the DRAFT manifest JSON that a publish would freeze, for admin review before shipping.", - "auth": "jwt + permission:geenforce.manage", - "params": "path: scopeid", - "example": "curl -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/scopes/3/preview" - }, - { - "method": "GET", - "path": "/api/geenforce/applications", - "purpose": "List the core active Applications catalog (appid + appname) for the curated entry-to-app link picker in the entry editor.", - "auth": "jwt + permission:geenforce.manage", - "params": "none", - "example": "curl -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/applications" - }, - { - "method": "POST", - "path": "/api/geenforce/scopes//entries", - "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.", - "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.)", - "example": "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" - }, - { - "method": "PUT", - "path": "/api/geenforce/entries/", - "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.", - "auth": "jwt + permission:geenforce.manage", - "params": "path: entryid; JSON body: Name (required), Type (required), optional appid, plus manifest fields", - "example": "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" - }, - { - "method": "DELETE", - "path": "/api/geenforce/entries/", - "purpose": "Delete a manifest entry; returns {deleted: entryid}.", - "auth": "jwt + permission:geenforce.manage", - "params": "path: entryid", - "example": "curl -X DELETE -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/entries/17" - }, - { - "method": "PUT", - "path": "/api/geenforce/scopes//entries/reorder", - "purpose": "Set entry ordering from an entryid list; 400 unless the list is exactly the set of this scope's entry ids.", - "auth": "jwt + permission:geenforce.manage", - "params": "path: scopeid; JSON body: order = [entryid, ...] (must match the scope's entry ids exactly)", - "example": "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" - }, - { - "method": "GET", - "path": "/api/geenforce/scopes//simulate", - "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.", - "auth": "jwt + permission:geenforce.manage", - "params": "path: scopeid; query (all optional): pctype (defaults to scopename), subtype, hostname, machinenumber, cmmversion; phase comes from the scope", - "example": "curl -H \"Authorization: Bearer $JWT\" 'http://localhost:5001/api/geenforce/scopes/3/simulate?hostname=WKSTN0042&cmmversion=2023.2'" - }, - { - "method": "GET", - "path": "/api/geenforce/scopes//compliance", - "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).", - "auth": "jwt + permission:geenforce.manage", - "params": "path: scopeid", - "example": "curl -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/scopes/3/compliance" - }, - { - "method": "POST", - "path": "/api/geenforce/entries//payload", - "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.", - "auth": "jwt + permission:geenforce.publish", - "params": "path: entryid; multipart/form-data: file (required)", - "example": "curl -X POST -H \"Authorization: Bearer $JWT\" -F 'file=@fix.ps1' http://localhost:5001/api/geenforce/entries/17/payload" - }, - { - "method": "GET", - "path": "/api/geenforce/entries//payload", - "purpose": "Download the stored inline payload bytes for an entry as an attachment (404 if the entry has no payload).", - "auth": "jwt + permission:geenforce.manage", - "params": "path: entryid", - "example": "curl -H \"Authorization: Bearer $JWT\" -o fix.ps1 http://localhost:5001/api/geenforce/entries/17/payload" - }, - { - "method": "POST", - "path": "/api/geenforce/scopes//publish", - "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.", - "auth": "jwt + permission:geenforce.publish", - "params": "path: scopeid; JSON body (optional): notes", - "example": "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" - }, - { - "method": "GET", - "path": "/api/geenforce/scopes//versions", - "purpose": "List published versions for a scope, newest first (versionnumber, iscurrent, publishedat, publishedby, notes).", - "auth": "jwt + permission:geenforce.manage", - "params": "path: scopeid", - "example": "curl -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/scopes/3/versions" - }, - { - "method": "GET", - "path": "/api/geenforce/scopes//versions/", - "purpose": "Fetch one published version's frozen manifest JSON (parsed and returned in the success envelope).", - "auth": "jwt + permission:geenforce.manage", - "params": "path: scopeid, versionnumber", - "example": "curl -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/scopes/3/versions/4" - }, - { - "method": "POST", - "path": "/api/geenforce/scopes//rollback", - "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.", - "auth": "jwt + permission:geenforce.publish", - "params": "path: scopeid; JSON body: versionnumber (required, int)", - "example": "curl -X POST -H \"Authorization: Bearer $JWT\" -H 'Content-Type: application/json' -d '{\"versionnumber\":3}' http://localhost:5001/api/geenforce/scopes/3/rollback" - }, - { - "method": "POST", - "path": "/api/geenforce/scopes//export-share", - "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.", - "auth": "jwt + permission:geenforce.publish", - "params": "path: scopeid; no body", - "example": "curl -X POST -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/scopes/3/export-share" - }, - { - "method": "GET", - "path": "/api/geenforce/config", - "purpose": "Read plugin config: the on-share export root (geenforce_share_root) and the client IP allowlist CIDRs (geenforce_allowed_cidrs).", - "auth": "jwt + permission:geenforce.manage", - "params": "none", - "example": "curl -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/config" - }, - { - "method": "PUT", - "path": "/api/geenforce/config", - "purpose": "Update plugin config settings; allowedcidrs is validated/normalized (comma/newline-separated CIDRs or bare IPs, 400 listing any bad entries) and only keys present in the body are written.", - "auth": "jwt + permission:geenforce.publish", - "params": "JSON body (both optional): shareroot (string path), allowedcidrs (CSV/newline CIDR list)", - "example": "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" - }, - { - "method": "GET", - "path": "/api/geenforce/reports", - "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.", - "auth": "jwt + permission:geenforce.manage", - "params": "query (optional): hostname (ILIKE match), scopename (exact)", - "example": "curl -H \"Authorization: Bearer $JWT\" 'http://localhost:5001/api/geenforce/reports?scopename=cmm'" - }, - { - "method": "GET", - "path": "/api/geenforce/reports/", - "purpose": "One enforcement report in detail with per-entry outcomes (entryname, action, selfhealed, exitcode, message) plus applied-vs-latest version comparison.", - "auth": "jwt + permission:geenforce.manage", - "params": "path: reportid", - "example": "curl -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/reports/42" - } - ] - }, - { - "surface": "plugin-notifications", - "endpoints": [ - { - "method": "GET", - "path": "/api/notifications/types", - "purpose": "List notification types, paginated, active-only by default", - "auth": "none", - "params": "page, per_page; active=false to include inactive types", - "example": "curl 'http://localhost:5001/api/notifications/types?active=false&page=1&per_page=50'" - }, - { - "method": "POST", - "path": "/api/notifications/types", - "purpose": "Create a notification type incl. expiry rule and shopfloor display config", - "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)", - "example": "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}'" - }, - { - "method": "PUT, PATCH", - "path": "/api/notifications/types/", - "purpose": "Update a notification type (name/desc/color/isactive plus expiry and display fields)", - "auth": "jwt + permission:notifications.create", - "params": "body: any of typename (unique-checked), typedescription/description, typecolor/color, isactive, expirymode, expirydays, expiryhour, expiryminute, splitperemployee, showemployeephoto, displaystyle", - "example": "curl -X PATCH http://localhost:5001/api/notifications/types/3 -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"expirymode\":\"duration\",\"expirydays\":14}'" - }, - { - "method": "GET", - "path": "/api/notifications", - "purpose": "List notifications with filters, newest-first, paginated", - "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)", - "example": "curl 'http://localhost:5001/api/notifications?current=true&typeid=2&search=outage&page=1'" - }, - { - "method": "GET", - "path": "/api/notifications/", - "purpose": "Get a single notification by ID", - "auth": "none", - "params": "path: notification_id", - "example": "curl http://localhost:5001/api/notifications/42" - }, - { - "method": "POST", - "path": "/api/notifications", - "purpose": "Create a notification; endtime auto-derived from type expiry rule when omitted", - "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", - "example": "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\"}'" - }, - { - "method": "PUT", - "path": "/api/notifications/", - "purpose": "Update any notification field; empty starttime resets to now, empty endtime clears it", - "auth": "jwt + permission:notifications.edit", - "params": "body: notification/message, notificationtypeid, businessunitid, appid, ticketnumber, link/linkurl, isactive, isshopfloor, employeesso, employeename, starttime/startdate, endtime/enddate", - "example": "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}'" - }, - { - "method": "DELETE", - "path": "/api/notifications/", - "purpose": "Soft-delete a notification (sets isactive=false, row kept)", - "auth": "jwt + permission:notifications.delete", - "params": "path: notification_id", - "example": "curl -X DELETE http://localhost:5001/api/notifications/42 -H \"Authorization: Bearer $TOKEN\"" - }, - { - "method": "GET", - "path": "/api/notifications/active", - "purpose": "Currently active notifications for display, incl. ones starting within a 10-day lookahead", - "auth": "none", - "params": "none", - "example": "curl http://localhost:5001/api/notifications/active" - }, - { - "method": "GET", - "path": "/api/notifications/calendar", - "purpose": "Active notifications as FullCalendar event objects for a date range", - "auth": "none", - "params": "start (ISO), end (ISO); invalid dates silently ignored", - "example": "curl 'http://localhost:5001/api/notifications/calendar?start=2026-07-01T00:00:00Z&end=2026-07-31T23:59:59Z'" - }, - { - "method": "GET", - "path": "/api/notifications/dashboard/summary", - "purpose": "Dashboard counts: total currently-active notifications plus active counts grouped by type/color", - "auth": "none", - "params": "none", - "example": "curl http://localhost:5001/api/notifications/dashboard/summary" - }, - { - "method": "GET", - "path": "/api/notifications/employee/", - "purpose": "All active recognition-type notifications mentioning an employee SSO (exact or within comma-separated employeesso list)", - "auth": "none", - "params": "path: sso (digits only, 400 otherwise)", - "example": "curl http://localhost:5001/api/notifications/employee/212345678" - }, - { - "method": "GET", - "path": "/api/notifications/shopfloor", - "purpose": "Shopfloor TV feed: current cards (active now, or ended <30 min ago flagged resolved) + 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", - "auth": "none", - "params": "businessunit (numeric BU id: returns that BU's plus null-BU notifications; omitted: null-BU only)", - "example": "curl 'http://localhost:5001/api/notifications/shopfloor?businessunit=3'" - } - ] - }, - { - "surface": "plugin-slides", - "endpoints": [ - { - "method": "GET", - "path": "/api/slides/feed", - "purpose": "Public flat playlist for a surface (lobby/shopfloor); raw jsonify, not the success_response envelope, so the screensaver parser works unchanged", - "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", - "example": "curl 'http://localhost:5001/api/slides/feed?surface=shopfloor'" - }, - { - "method": "GET", - "path": "/api/slides/img//", - "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", - "auth": "none", - "params": "path: surface (lobby|shopfloor), filename (must equal its basename)", - "example": "curl 'http://localhost:5001/api/slides/img/lobby/Slide1.png' -o Slide1.png" - }, - { - "method": "GET", - "path": "/api/slides/", - "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", - "auth": "jwt + permission:slides.manage", - "params": "path: surface (lobby|shopfloor; else VALIDATION_ERROR)", - "example": "curl -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/slides/lobby'" - }, - { - "method": "POST", - "path": "/api/slides//upload", - "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)", - "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]}", - "example": "curl -X POST -H \"Authorization: Bearer $TOKEN\" -F 'files=@Slide1.png' -F 'files=@Slide2.png' 'http://localhost:5001/api/slides/lobby/upload'" - }, - { - "method": "POST", - "path": "/api/slides//order", - "purpose": "Save play order: each filename in the order array gets sortorder set to its index; unknown filenames silently ignored", - "auth": "jwt + permission:slides.manage", - "params": "path: surface. JSON body: {order: [filename, ...]}", - "example": "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'" - }, - { - "method": "POST", - "path": "/api/slides//delete", - "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", - "auth": "jwt + permission:slides.manage", - "params": "path: surface. JSON body: {files: [filename, ...]}", - "example": "curl -X POST -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"files\":[\"Slide1.png\"]}' 'http://localhost:5001/api/slides/shopfloor/delete'" - }, - { - "method": "PATCH", - "path": "/api/slides//", - "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", - "auth": "jwt + permission:slides.manage", - "params": "path: surface, slideid (int). JSON body: {seconds: int} (non-int -> VALIDATION_ERROR)", - "example": "curl -X PATCH -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"seconds\":15}' 'http://localhost:5001/api/slides/lobby/12'" - } - ] - }, - { - "surface": "plugin-printedparts", - "endpoints": [ - { - "method": "GET", - "path": "/api/printedparts/items", - "purpose": "List printed items, paginated, with search and low-stock filter", - "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", - "example": "curl -H \"Authorization: Bearer $TOK\" 'http://localhost:5001/api/printedparts/items?search=bracket&lowstock=true&page=1&per_page=25'" - }, - { - "method": "GET", - "path": "/api/printedparts/items/", - "purpose": "Get one printed item plus its 25 most recent ledger transactions", - "auth": "jwt + permission:printedparts.view", - "params": "path: item_id", - "example": "curl -H \"Authorization: Bearer $TOK\" http://localhost:5001/api/printedparts/items/42" - }, - { - "method": "POST", - "path": "/api/printedparts/items", - "purpose": "Create a printed item; itemcode auto-minted from printedparts_code_prefix setting + row id; optional gagelabtag unique-checked (409 on clash)", - "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", - "example": "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" - }, - { - "method": "PUT", - "path": "/api/printedparts/items/", - "purpose": "Update catalog fields (itemname, itemdescription, lowstockthreshold, binlocation, printnotes, gagelabtag); rejects quantityonhand (ledger-managed) and duplicate gagelabtag (409)", - "auth": "jwt + permission:printedparts.edit", - "params": "path: item_id; json body: any of the editable fields; gagelabtag uppercased, empty string clears it", - "example": "curl -X PUT -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"binlocation\":\"C1\",\"lowstockthreshold\":8}' http://localhost:5001/api/printedparts/items/42" - }, - { - "method": "DELETE", - "path": "/api/printedparts/items/", - "purpose": "Soft-retire an item (isactive=false); ledger history preserved", - "auth": "jwt + permission:printedparts.delete", - "params": "path: item_id", - "example": "curl -X DELETE -H \"Authorization: Bearer $TOK\" http://localhost:5001/api/printedparts/items/42" - }, - { - "method": "POST", - "path": "/api/printedparts/items//restore", - "purpose": "Un-retire a soft-deleted item (isactive=true); code, photo, history intact", - "auth": "jwt + permission:printedparts.delete", - "params": "path: item_id", - "example": "curl -X POST -H \"Authorization: Bearer $TOK\" http://localhost:5001/api/printedparts/items/42/restore" - }, - { - "method": "POST", - "path": "/api/printedparts/items//image", - "purpose": "Upload or replace the item's photo (png/jpg/jpeg/gif/webp); old image files for the item are deleted first, imageurl updated", - "auth": "jwt + permission:printedparts.edit", - "params": "path: item_id; multipart/form-data: file=", - "example": "curl -X POST -H \"Authorization: Bearer $TOK\" -F 'file=@clip.jpg' http://localhost:5001/api/printedparts/items/42/image" - }, - { - "method": "GET", - "path": "/api/printedparts/image/", - "purpose": "Serve an uploaded item image from instance/printedpartsimages (public: fetched by tags on kiosk and lists)", - "auth": "none", - "params": "path: filename (e.g. printeditem-42.jpg)", - "example": "curl http://localhost:5001/api/printedparts/image/printeditem-42.jpg -o clip.jpg" - }, - { - "method": "DELETE", - "path": "/api/printedparts/items//image", - "purpose": "Clear the item's imageurl; deletes the file on disk only if the URL is plugin-owned (starts with /api/printedparts/image/)", - "auth": "jwt + permission:printedparts.delete", - "params": "path: item_id", - "example": "curl -X DELETE -H \"Authorization: Bearer $TOK\" http://localhost:5001/api/printedparts/items/42/image" - }, - { - "method": "POST", - "path": "/api/printedparts/items//restock", - "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", - "auth": "jwt + permission:printedparts.restock", - "params": "path: item_id; json body: quantity (positive int, required), badge (required, 422 BadgeError if unresolvable)", - "example": "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" - }, - { - "method": "POST", - "path": "/api/printedparts/items//adjust", - "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", - "auth": "jwt + permission:printedparts.restock", - "params": "path: item_id; json body: quantitychange (non-zero int, required), reason (required), badge (required)", - "example": "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" - }, - { - "method": "GET", - "path": "/api/printedparts/kiosk/item/", - "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", - "auth": "none (deliberately open per decision record; kiosk cannot carry JWT)", - "params": "path: itemcode (e.g. WJRP0042, 3DP0042, 42, or WJRP0042|3)", - "example": "curl http://localhost:5001/api/printedparts/kiosk/item/WJRP0042" - }, - { - "method": "POST", - "path": "/api/printedparts/kiosk/take", - "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", - "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)", - "example": "curl -X POST -H 'Content-Type: application/json' -d '{\"itemcode\":\"WJRP0042|3\",\"badge\":\"123456789\",\"quantity\":2}' http://localhost:5001/api/printedparts/kiosk/take" - }, - { - "method": "GET", - "path": "/api/printedparts/reports/stock", - "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)", - "auth": "jwt-optional (like all product reports)", - "params": "query: format=csv for CSV download (printedparts-stock.csv), else JSON {columns, rows}", - "example": "curl 'http://localhost:5001/api/printedparts/reports/stock?format=csv' -o stock.csv" - }, - { - "method": "GET", - "path": "/api/printedparts/reports/consumption", - "purpose": "Take-transactions aggregated per item (takes count + quantitytaken), sorted by quantity taken descending", - "auth": "jwt-optional", - "params": "query: days (default 30; 0 or negative = all time), format=csv (printedparts-consumption.csv)", - "example": "curl 'http://localhost:5001/api/printedparts/reports/consumption?days=90'" - }, - { - "method": "GET", - "path": "/api/printedparts/reports/by-person", - "purpose": "Take-transactions grouped by employee SSO (takes count + quantitytaken), sorted by quantity taken descending", - "auth": "jwt-optional", - "params": "query: days (default 30; 0 or negative = all time), format=csv (printedparts-by-person.csv)", - "example": "curl 'http://localhost:5001/api/printedparts/reports/by-person?days=30&format=csv' -o by-person.csv" - }, - { - "method": "GET", - "path": "/api/printedparts/items//files", - "purpose": "List the item's print-file revision history, newest revision first", - "auth": "jwt + permission:printedparts.view", - "params": "path: item_id", - "example": "curl -H \"Authorization: Bearer $TOK\" http://localhost:5001/api/printedparts/items/42/files" - }, - { - "method": "POST", - "path": "/api/printedparts/items//files", - "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", - "auth": "jwt + permission:printedparts.edit", - "params": "path: item_id; multipart/form-data: file= (required), note= (optional)", - "example": "curl -X POST -H \"Authorization: Bearer $TOK\" -F 'file=@clip-v2.stl' -F 'note=thicker wall' http://localhost:5001/api/printedparts/items/42/files" - }, - { - "method": "GET", - "path": "/api/printedparts/files//download", - "purpose": "Download a print-file revision as an attachment under its original filename (jwt-optional so plain anchor downloads work)", - "auth": "jwt-optional", - "params": "path: file_id", - "example": "curl -OJ http://localhost:5001/api/printedparts/files/7/download" - }, - { - "method": "DELETE", - "path": "/api/printedparts/files/", - "purpose": "Delete a bad print-file revision (wrong file uploaded): removes the stored file and the DB record", - "auth": "jwt + permission:printedparts.delete", - "params": "path: file_id", - "example": "curl -X DELETE -H \"Authorization: Bearer $TOK\" http://localhost:5001/api/printedparts/files/7" - } - ] - }, - { - "endpoints": [ - { - "method": "GET", - "path": "/api/network/types", - "purpose": "List network device types, paginated", - "auth": "jwt-optional", - "params": "page, per_page, active (default true; 'false' includes inactive), search (ilike on networkdevicetype)", - "example": "curl 'http://localhost:5001/api/network/types?search=switch&per_page=50'" - }, - { - "method": "GET", - "path": "/api/network/types/", - "purpose": "Get one network device type by ID", - "auth": "jwt-optional", - "params": "path: type_id", - "example": "curl http://localhost:5001/api/network/types/3" - }, - { - "method": "POST", - "path": "/api/network/types", - "purpose": "Create a network device type (reactivates a soft-deleted duplicate instead of 409ing)", - "auth": "jwt + permission:network.create", - "params": "body: networkdevicetype (required), description, icon, color", - "example": "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\"}'" - }, - { - "method": "PUT", - "path": "/api/network/types/", - "purpose": "Update a network device type; 409 on name collision", - "auth": "jwt + permission:network.edit", - "params": "body: networkdevicetype, description, icon, color, isactive", - "example": "curl -X PUT http://localhost:5001/api/network/types/3 -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"description\":\"Access switches\"}'" - }, - { - "method": "DELETE", - "path": "/api/network/types/", - "purpose": "Hard-delete a device type; 409 if any device still references it", - "auth": "jwt + permission:network.delete", - "params": "path: type_id", - "example": "curl -X DELETE http://localhost:5001/api/network/types/3 -H \"Authorization: Bearer $TOKEN\"" - }, - { - "method": "GET", - "path": "/api/network", - "purpose": "List network devices (Asset joined with NetworkDevice extension + primary IP), filtered/sorted/paginated", - "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)", - "example": "curl 'http://localhost:5001/api/network?typeid=2&poe=true&sort=hostname&dir=asc'" - }, - { - "method": "GET", - "path": "/api/network/", - "purpose": "Get one network device (asset dict + networkdevice sub-object + primary ipaddress)", - "auth": "jwt-optional", - "params": "path: device_id (networkdeviceid)", - "example": "curl http://localhost:5001/api/network/17" - }, - { - "method": "GET", - "path": "/api/network/by-asset/", - "purpose": "Look up a network device by its core assetid", - "auth": "jwt-optional", - "params": "path: asset_id", - "example": "curl http://localhost:5001/api/network/by-asset/1042" - }, - { - "method": "GET", - "path": "/api/network/by-hostname/", - "purpose": "Look up a network device by exact hostname", - "auth": "jwt-optional", - "params": "path: hostname", - "example": "curl http://localhost:5001/api/network/by-hostname/wjf-sw-idf3-01" - }, - { - "method": "POST", - "path": "/api/network", - "purpose": "Create a network device (creates core Asset + NetworkDevice extension; upserts primary-IP Communication; audit-logged; honors X-Import-Mode legacy timestamps)", - "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", - "example": "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\"}'" - }, - { - "method": "PUT", - "path": "/api/network/", - "purpose": "Update asset + network-device fields; 409 on assetnumber/hostname conflicts; change-diff audit log; upserts primary IP when ipaddress present", - "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", - "example": "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\"}'" - }, - { - "method": "DELETE", - "path": "/api/network/", - "purpose": "Soft-delete a network device (sets asset.isactive=false; audit-logged)", - "auth": "jwt + permission:network.delete", - "params": "path: device_id", - "example": "curl -X DELETE http://localhost:5001/api/network/17 -H \"Authorization: Bearer $TOKEN\"" - }, - { - "method": "GET", - "path": "/api/network/dashboard/summary", - "purpose": "Dashboard counts: total active devices, by type, by vendor, PoE vs non-PoE", - "auth": "jwt-optional", - "params": "none", - "example": "curl http://localhost:5001/api/network/dashboard/summary" - }, - { - "method": "GET", - "path": "/api/network/vlans", - "purpose": "List VLANs, paginated, ordered by vlannumber", - "auth": "jwt-optional", - "params": "page, per_page, active (default true), search (name/description/vlannumber), type (exact vlantype)", - "example": "curl 'http://localhost:5001/api/network/vlans?search=voice'" - }, - { - "method": "GET", - "path": "/api/network/vlans/", - "purpose": "Get one VLAN including its active subnets", - "auth": "jwt-optional", - "params": "path: vlan_id", - "example": "curl http://localhost:5001/api/network/vlans/5" - }, - { - "method": "POST", - "path": "/api/network/vlans", - "purpose": "Create a VLAN; 409 on duplicate vlannumber; audit-logged", - "auth": "jwt + permission:network.create", - "params": "body: vlannumber (required), name (required), description, vlantype", - "example": "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\"}'" - }, - { - "method": "PUT", - "path": "/api/network/vlans/", - "purpose": "Update a VLAN; 409 on vlannumber conflict; change-diff audit log", - "auth": "jwt + permission:network.edit", - "params": "body: vlannumber, name, description, vlantype, isactive", - "example": "curl -X PUT http://localhost:5001/api/network/vlans/5 -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"description\":\"CNC cell VLAN\"}'" - }, - { - "method": "DELETE", - "path": "/api/network/vlans/", - "purpose": "Soft-delete a VLAN; 400 if it still has active subnets; audit-logged", - "auth": "jwt + permission:network.delete", - "params": "path: vlan_id", - "example": "curl -X DELETE http://localhost:5001/api/network/vlans/5 -H \"Authorization: Bearer $TOKEN\"" - }, - { - "method": "GET", - "path": "/api/network/subnets", - "purpose": "List subnets, paginated, ordered by cidr", - "auth": "jwt-optional", - "params": "page, per_page, active (default true), search (cidr/name/description), vlanid, locationid, type (exact subnettype)", - "example": "curl 'http://localhost:5001/api/network/subnets?vlanid=5'" - }, - { - "method": "GET", - "path": "/api/network/subnets/", - "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", - "auth": "jwt-optional", - "params": "path: subnet_id", - "example": "curl http://localhost:5001/api/network/subnets/2" - }, - { - "method": "POST", - "path": "/api/network/subnets", - "purpose": "Create a subnet; validates CIDR notation and vlanid existence; 409 on duplicate cidr; audit-logged", - "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", - "example": "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\"}'" - }, - { - "method": "PUT", - "path": "/api/network/subnets/", - "purpose": "Update a subnet; 409 on cidr conflict; change-diff audit log", - "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", - "example": "curl -X PUT http://localhost:5001/api/network/subnets/2 -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"dhcpenabled\":false}'" - }, - { - "method": "DELETE", - "path": "/api/network/subnets/", - "purpose": "Soft-delete a subnet (isactive=false); audit-logged", - "auth": "jwt + permission:network.delete", - "params": "path: subnet_id", - "example": "curl -X DELETE http://localhost:5001/api/network/subnets/2 -H \"Authorization: Bearer $TOKEN\"" - } - ], - "surface": "plugin-network" - }, - { - "surface": "plugin-usb", - "endpoints": [ - { - "method": "GET", - "path": "/api/usb", - "purpose": "List USB devices with checkout status (paginated); dual-mode: selfhosted tables or external cmmc_usb DB per usb_directory_mode setting", - "auth": "jwt-optional", - "params": "query: page, per_page, status=available|checkedout|retired, search (matches device_id or device_desc)", - "example": "curl 'http://localhost:5001/api/usb?status=available&search=kingston&page=1&per_page=25' -H \"Authorization: Bearer $JWT\"" - }, - { - "method": "GET", - "path": "/api/usb/", - "purpose": "Get one device plus its last 20 check-in/out log rows; 404 if unknown", - "auth": "jwt-optional", - "params": "path: device_id", - "example": "curl 'http://localhost:5001/api/usb/USB-0042' -H \"Authorization: Bearer $JWT\"" - }, - { - "method": "POST", - "path": "/api/usb", - "purpose": "Create a device (starts in checked-in status); 409 on duplicate device_id", - "auth": "jwt + permission:usb.create", - "params": "body JSON: device_id (required), device_desc, device_owner (badge), locker_location", - "example": "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\"}'" - }, - { - "method": "PUT", - "path": "/api/usb/", - "purpose": "Edit device fields (device_desc / device_owner / locker_location / status); 404 if unknown", - "auth": "jwt + permission:usb.edit", - "params": "path: device_id; body JSON: any of device_desc, device_owner, locker_location, status", - "example": "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\"}'" - }, - { - "method": "POST", - "path": "/api/usb//retire", - "purpose": "Retire a device (sets status to retired); 404 if unknown", - "auth": "jwt + permission:usb.edit", - "params": "path: device_id; no body", - "example": "curl -X POST 'http://localhost:5001/api/usb/USB-0042/retire' -H \"Authorization: Bearer $JWT\"" - }, - { - "method": "POST", - "path": "/api/usb//checkout", - "purpose": "Check a device out to a badge (writes check-out log row, sets status checked-out, auto-creates user from HR directory); 409 if already checked out", - "auth": "jwt + permission:usb.create", - "params": "path: device_id; body JSON: badge (required), locker_location (optional, also updates device)", - "example": "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\"}'" - }, - { - "method": "POST", - "path": "/api/usb//checkin", - "purpose": "Check a device back in (writes check-in log row with sanitized/virus-scan flags, sets status checked-in); 400 if not currently checked out", - "auth": "jwt + permission:usb.create", - "params": "path: device_id; body JSON: badge (required), locker_location, sanitized (bool/1/0), scanned_viruses (bool/1/0)", - "example": "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}'" - }, - { - "method": "GET", - "path": "/api/usb//history", - "purpose": "Paginated check-in/out log for one device, newest first", - "auth": "jwt-optional", - "params": "path: device_id; query: page, per_page", - "example": "curl 'http://localhost:5001/api/usb/USB-0042/history?page=1&per_page=50' -H \"Authorization: Bearer $JWT\"" - }, - { - "method": "GET", - "path": "/api/usb/checkouts", - "purpose": "List check-out log rows (paginated), each with the device's current status joined in", - "auth": "jwt-optional", - "params": "query: page, per_page, active=true (only rows whose device is still checked out), badge (filter by badge_number)", - "example": "curl 'http://localhost:5001/api/usb/checkouts?active=true&badge=212345678' -H \"Authorization: Bearer $JWT\"" - }, - { - "method": "GET", - "path": "/api/usb/checkouts/active", - "purpose": "Latest check-out log row for every currently checked-out device (non-paginated list)", - "auth": "jwt-optional", - "params": "query: badge (filter by badge_number)", - "example": "curl 'http://localhost:5001/api/usb/checkouts/active?badge=212345678' -H \"Authorization: Bearer $JWT\"" - } - ] - }, - { - "surface": "plugin-printers", - "endpoints": [ - { - "method": "GET", - "path": "/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.", - "example": "curl -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/printers/types?search=laser&active=false'" - }, - { - "method": "GET", - "path": "/api/printers/types/", - "auth": "jwt-optional", - "params": "path: type_id (int)", - "purpose": "Get a single printer type by ID.", - "example": "curl http://localhost:5001/api/printers/types/3" - }, - { - "method": "POST", - "path": "/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).", - "example": "curl -X POST -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"printertype\":\"Label\",\"color\":\"#00f\"}' http://localhost:5001/api/printers/types" - }, - { - "method": "PUT", - "path": "/api/printers/types/", - "auth": "permission:printers.edit", - "params": "body: printertype, description, icon, color, isactive (any subset); 409 on name clash", - "purpose": "Update a printer type.", - "example": "curl -X PUT -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"isactive\":false}' http://localhost:5001/api/printers/types/3" - }, - { - "method": "DELETE", - "path": "/api/printers/types/", - "auth": "permission:printers.delete", - "params": "path: type_id; 409 if any printer still references the type", - "purpose": "Hard-delete a printer type when unused.", - "example": "curl -X DELETE -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/printers/types/3" - }, - { - "method": "GET", - "path": "/api/printers/drivers", - "auth": "jwt-optional", - "params": "active (default true; 'false' includes inactive). Unpaginated.", - "purpose": "List printer driver packages (named SMB/HTTP links).", - "example": "curl 'http://localhost:5001/api/printers/drivers?active=false'" - }, - { - "method": "POST", - "path": "/api/printers/drivers", - "auth": "permission:printers.create", - "params": "body: name (required), location (required), description, modelnumberid, isactive", - "purpose": "Create a driver entry.", - "example": "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" - }, - { - "method": "PUT", - "path": "/api/printers/drivers/", - "auth": "permission:printers.edit", - "params": "body: name, location, description, isactive, modelnumberid (any subset)", - "purpose": "Update a driver entry.", - "example": "curl -X PUT -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"isactive\":false}' http://localhost:5001/api/printers/drivers/5" - }, - { - "method": "DELETE", - "path": "/api/printers/drivers/", - "auth": "permission:printers.delete", - "params": "path: driver_id", - "purpose": "Hard-delete a driver entry.", - "example": "curl -X DELETE -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/printers/drivers/5" - }, - { - "method": "GET", - "path": "/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.", - "example": "curl 'http://localhost:5001/api/printers?search=csf&typeid=2&sort=assetnumber&dir=desc'" - }, - { - "method": "GET", - "path": "/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.", - "example": "curl 'http://localhost:5001/api/printers/install-list?format=text'" - }, - { - "method": "GET", - "path": "/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).", - "example": "curl -OJ 'http://localhost:5001/api/printers/install-batch?printerids=1,2,3'" - }, - { - "method": "GET", - "path": "/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.", - "example": "curl 'http://localhost:5001/api/printers/pc-default?machine=0421&format=text'" - }, - { - "method": "GET", - "path": "/api/printers/", - "auth": "jwt-optional", - "params": "path: printer_id (int)", - "purpose": "Get one printer with full asset details, communications, and active drivers matching its model.", - "example": "curl http://localhost:5001/api/printers/17" - }, - { - "method": "GET", - "path": "/api/printers/by-asset/", - "auth": "jwt-optional", - "params": "path: asset_id (int)", - "purpose": "Get printer data keyed by core asset ID.", - "example": "curl http://localhost:5001/api/printers/by-asset/204" - }, - { - "method": "POST", - "path": "/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).", - "example": "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" - }, - { - "method": "PUT", - "path": "/api/printers/", - "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.", - "example": "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" - }, - { - "method": "DELETE", - "path": "/api/printers/", - "auth": "permission:printers.delete", - "params": "path: printer_id", - "purpose": "Soft-delete a printer (sets the underlying asset isactive=false).", - "example": "curl -X DELETE -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/printers/17" - }, - { - "method": "GET", - "path": "/api/printers//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.", - "example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/printers/17/supplies" - }, - { - "method": "GET", - "path": "/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.", - "example": "curl http://localhost:5001/api/printers/lowsupplies" - }, - { - "method": "GET", - "path": "/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).", - "example": "curl 'http://localhost:5001/api/printers/lookup?ip=10.1.2.42'" - }, - { - "method": "POST", - "path": "/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).", - "example": "curl -X POST -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/printers/supplies/refresh" - }, - { - "method": "GET", - "path": "/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).", - "example": "curl http://localhost:5001/api/printers/dashboard/summary" - }, - { - "method": "GET", - "path": "/api/printers/supplies/meta", - "auth": "jwt-optional", - "params": "none", - "purpose": "Allowed enum values for supplytype, color, and capacitytier (for the UI forms).", - "example": "curl http://localhost:5001/api/printers/supplies/meta" - }, - { - "method": "GET", - "path": "/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).", - "example": "curl 'http://localhost:5001/api/printers/models?withsupplies=true&search=M404'" - }, - { - "method": "GET", - "path": "/api/printers/models//supplies", - "auth": "jwt-optional", - "params": "path: modelnumberid (int)", - "purpose": "List all active supplies (toner/drum/waste part numbers) mapped to a model.", - "example": "curl http://localhost:5001/api/printers/models/12/supplies" - }, - { - "method": "POST", - "path": "/api/printers/models//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.", - "example": "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" - }, - { - "method": "PUT", - "path": "/api/printers/supplies/", - "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.", - "example": "curl -X PUT -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"capacitytier\":\"high\"}' http://localhost:5001/api/printers/supplies/44" - }, - { - "method": "DELETE", - "path": "/api/printers/supplies/", - "auth": "permission:printers.delete", - "params": "path: modelsupplyid", - "purpose": "Hard-delete a model supply mapping.", - "example": "curl -X DELETE -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/printers/supplies/44" - }, - { - "method": "GET", - "path": "/api/printers/supplies/forecast", - "auth": "jwt-optional", - "params": "days (1-365, default 90)", - "purpose": "One row per CARTRIDGE: days-to-empty from Zabbix history (hourly trends over a long window), soonest first, banded empty/soon/month/later, each with its part numbers and replacement count. Also returns orderlist - what to buy within horizondays, grouped by part number with a quantity. Cartridges with no honest estimate come back separately with the reason. available=false when Zabbix is unreachable", - "example": "curl 'http://localhost:5001/api/printers/supplies/forecast?days=90'" - } - ] - }, - { - "surface": "plugin-warranty", - "endpoints": [ - { - "method": "GET", - "path": "/api/warranty", - "purpose": "List warranties with linked-asset summaries and derived status (batch asset fetch, ordered by enddate with null last).", - "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)", - "example": "curl -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/warranty?status=expiring&assetid=42'" - }, - { - "method": "GET", - "path": "/api/warranty/asset/", - "purpose": "Active warranties covering one asset, for the asset-detail panel.", - "auth": "jwt-optional", - "params": "path: assetid", - "example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/warranty/asset/42" - }, - { - "method": "GET", - "path": "/api/warranty/", - "purpose": "Fetch a single warranty by id with asset summaries; 404 if missing.", - "auth": "jwt-optional", - "params": "path: warrantyid", - "example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/warranty/7" - }, - { - "method": "POST", - "path": "/api/warranty", - "purpose": "Create a warranty (vendor required) and optionally link it to assets; returns 201.", - "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)", - "example": "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" - }, - { - "method": "PUT", - "path": "/api/warranty/", - "purpose": "Partial update of any warranty field (only keys present in body change), including isactive and replacing asset links via assetids.", - "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)", - "example": "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" - }, - { - "method": "DELETE", - "path": "/api/warranty/", - "purpose": "Hard-delete a warranty (and its asset links); 404 if missing.", - "auth": "jwt + permission:warranty.delete", - "params": "path: warrantyid", - "example": "curl -X DELETE -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/warranty/7" - }, - { - "method": "POST", - "path": "/api/warranty//refresh", - "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.", - "auth": "jwt + permission:warranty.edit", - "params": "path: warrantyid; no body", - "example": "curl -X POST -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/warranty/7/refresh" - }, - { - "method": "POST", - "path": "/api/warranty/sync/dell", - "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.", - "auth": "jwt + permission:warranty.edit", - "params": "query: all (default false; 'true' re-checks assets that already have a dated warranty); no body", - "example": "curl -X POST -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/warranty/sync/dell?all=true'" - }, - { - "method": "GET", - "path": "/api/warranty/report", - "purpose": "Report for the Reports hub: active warranties bucketed by derived status (expired/expiring/active/unknown) with per-bucket counts and full lists.", - "auth": "jwt-optional", - "params": "none", - "example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/warranty/report" - }, - { - "method": "POST", - "path": "/api/warranty//proof", - "auth": "jwt + warranty.edit", - "params": "multipart/form-data: file= (pdf/png/jpg/gif/webp/tif/msg/eml/doc/docx/xls/xlsx), max 25MB", - "purpose": "Upload or replace the proof-of-cover document (invoice, certificate)", - "example": "curl -X POST -H 'Authorization: Bearer ' -F file=@invoice.pdf http://localhost:5001/api/warranty/12/proof" - }, - { - "method": "GET", - "path": "/api/warranty/proof/", - "auth": "jwt + warranty.view", - "params": "none", - "purpose": "Download a proof document under the vendor's original filename. Authenticated: carries pricing and a service tag", - "example": "curl -OJ -H 'Authorization: Bearer ' http://localhost:5001/api/warranty/proof/warranty-12.pdf" - }, - { - "method": "DELETE", - "path": "/api/warranty//proof", - "auth": "jwt + warranty.edit", - "params": "none", - "purpose": "Remove a proof document and clear both columns", - "example": "curl -X DELETE -H 'Authorization: Bearer ' http://localhost:5001/api/warranty/12/proof" - } - ] - } -] \ No newline at end of file + { + "surface": "core-identity", + "endpoints": [ + { + "method": "POST", + "path": "/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", + "example": "curl -X POST http://localhost:5001/api/auth/login -H 'Content-Type: application/json' -d '{\"username\":\"admin\",\"password\":\"secret123\"}'" + }, + { + "method": "POST", + "path": "/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)", + "example": "curl -X POST http://localhost:5001/api/auth/refresh -H \"Authorization: Bearer $REFRESH_TOKEN\"" + }, + { + "method": "GET", + "path": "/api/auth/me", + "auth": "jwt", + "params": "none", + "purpose": "Return the authenticated user's profile, roles, permissions, mustchangepassword flag", + "example": "curl http://localhost:5001/api/auth/me -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/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", + "example": "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\"}'" + }, + { + "method": "POST", + "path": "/api/auth/logout", + "auth": "jwt", + "params": "none", + "purpose": "Logout stub for frontend token cleanup (no server-side blacklist yet)", + "example": "curl -X POST http://localhost:5001/api/auth/logout -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/users", + "auth": "jwt + require_role admin", + "params": "none", + "purpose": "List all users ordered by username", + "example": "curl http://localhost:5001/api/users -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/users/", + "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)", + "example": "curl http://localhost:5001/api/users/7 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/users", + "auth": "jwt + require_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", + "example": "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]}'" + }, + { + "method": "PUT", + "path": "/api/users/", + "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", + "example": "curl -X PUT http://localhost:5001/api/users/7 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"firstname\":\"Jane\",\"unlock\":true}'" + }, + { + "method": "DELETE", + "path": "/api/users/", + "auth": "jwt + require_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", + "example": "curl -X DELETE http://localhost:5001/api/users/7 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/users/permissions", + "auth": "jwt", + "params": "none", + "purpose": "List assignable permissions (core + enabled plugins) both flat and grouped by category, for the role grid", + "example": "curl http://localhost:5001/api/users/permissions -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/users/roles", + "auth": "jwt", + "params": "none", + "purpose": "List all roles with description, color, user count, permission names, and isadmin flag", + "example": "curl http://localhost:5001/api/users/roles -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/users/roles", + "auth": "jwt + require_role admin", + "params": "body: rolename (required), description, color, permissions[names]", + "purpose": "Create a role and assign permissions by name; 409 if rolename exists", + "example": "curl -X POST http://localhost:5001/api/users/roles -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"rolename\":\"viewer\",\"permissions\":[\"assets.view\"]}'" + }, + { + "method": "PUT", + "path": "/api/users/roles/", + "auth": "jwt + require_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", + "example": "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\"]}'" + }, + { + "method": "DELETE", + "path": "/api/users/roles/", + "auth": "jwt + require_role admin", + "params": "path: roleid", + "purpose": "Delete a role; refuses for the admin role or any role still assigned to users", + "example": "curl -X DELETE http://localhost:5001/api/users/roles/3 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/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", + "example": "curl 'http://localhost:5001/api/apitokens?all=true' -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/apitokens", + "auth": "jwt + require_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", + "example": "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\"}'" + }, + { + "method": "PUT", + "path": "/api/apitokens/", + "auth": "jwt + require_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", + "example": "curl -X PUT http://localhost:5001/api/apitokens/4 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"isactive\":false}'" + }, + { + "method": "DELETE", + "path": "/api/apitokens/", + "auth": "jwt + require_permission apitokens.create (own token, or any if admin)", + "params": "path: tokenid", + "purpose": "Revoke (deactivate, not delete) a token; audit-logged", + "example": "curl -X DELETE http://localhost:5001/api/apitokens/4 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/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", + "example": "curl http://localhost:5001/api/setup/needs-admin" + }, + { + "method": "POST", + "path": "/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)", + "example": "curl -X POST http://localhost:5001/api/setup/create-admin -H 'Content-Type: application/json' -d '{\"username\":\"admin\",\"email\":\"admin@example.com\",\"password\":\"ChangeMe123\"}'" + }, + { + "method": "POST", + "path": "/api/setup/seed-reference", + "auth": "jwt + require_role admin", + "params": "none", + "purpose": "Idempotently seed core reference data, permissions, and default settings (runs the flask seed CLI routines)", + "example": "curl -X POST http://localhost:5001/api/setup/seed-reference -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/setup/seed-starter", + "auth": "jwt + require_role admin", + "params": "none", + "purpose": "Idempotently add a starter list of common hardware vendors (Dell, HP, Lenovo, ...)", + "example": "curl -X POST http://localhost:5001/api/setup/seed-starter -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/setup/complete", + "auth": "jwt + require_role admin", + "params": "none", + "purpose": "Set the setup_complete setting to true, marking the first-run wizard finished", + "example": "curl -X POST http://localhost:5001/api/setup/complete -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/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 ********", + "example": "curl 'http://localhost:5001/api/settings?category=branding'" + }, + { + "method": "POST", + "path": "/api/settings", + "auth": "jwt + require_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", + "example": "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\"}'" + }, + { + "method": "GET", + "path": "/api/settings/", + "auth": "jwt-optional", + "params": "path: key", + "purpose": "Get one setting; non-public keys return 404 (not 403) to unauthenticated callers; secret values masked", + "example": "curl http://localhost:5001/api/settings/facility_name" + }, + { + "method": "PUT", + "path": "/api/settings/", + "auth": "jwt + require_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", + "example": "curl -X PUT http://localhost:5001/api/settings/facility_name -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"value\":\"West Jefferson\"}'" + }, + { + "method": "POST", + "path": "/api/settings/seed", + "auth": "jwt + require_permission settings.edit", + "params": "none", + "purpose": "Idempotently create any missing default settings (identifier toggles, search toggles, map, SMTP, SAML, etc.)", + "example": "curl -X POST http://localhost:5001/api/settings/seed -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/settings/test-email", + "auth": "jwt + require_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", + "example": "curl -X POST http://localhost:5001/api/settings/test-email -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"to\":\"me@example.com\"}'" + }, + { + "method": "POST", + "path": "/api/settings/map-blueprint", + "auth": "jwt + require_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", + "example": "curl -X POST http://localhost:5001/api/settings/map-blueprint -H \"Authorization: Bearer $TOK\" -F 'file=@floor.png' -F 'theme=light'" + }, + { + "method": "GET", + "path": "/api/settings/map-blueprint/", + "auth": "none", + "params": "path: filename", + "purpose": "Serve an uploaded floor-map blueprint image (public so kiosk dashboards can load it)", + "example": "curl -O http://localhost:5001/api/settings/map-blueprint/blueprint-light.png" + }, + { + "method": "POST", + "path": "/api/settings/branding-logo", + "auth": "jwt + require_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)", + "example": "curl -X POST http://localhost:5001/api/settings/branding-logo -H \"Authorization: Bearer $TOK\" -F 'file=@logo.svg' -F 'kind=site'" + }, + { + "method": "GET", + "path": "/api/settings/branding/", + "auth": "none", + "params": "path: filename", + "purpose": "Serve an uploaded branding logo (public - kiosks and print pages read it)", + "example": "curl -O http://localhost:5001/api/settings/branding/logo-site.svg" + } + ] + }, + { + "surface": "core-platform", + "endpoints": [ + { + "method": "GET", + "path": "/api/reports", + "purpose": "List all available reports (6 core cards plus cards contributed by enabled plugins via the get_reports hook)", + "auth": "jwt-optional", + "params": "none", + "example": "curl http://localhost:5001/api/reports" + }, + { + "method": "GET", + "path": "/api/reports/machines-by-type", + "purpose": "Machine count grouped by machine type (dualpath secondary bays collapsed when the site setting is on); 404 if machines plugin absent", + "auth": "jwt-optional", + "params": "businessunitid (int filter), format=json|csv", + "example": "curl 'http://localhost:5001/api/reports/machines-by-type?businessunitid=2&format=csv'" + }, + { + "method": "GET", + "path": "/api/reports/assets-by-status", + "purpose": "Asset count grouped by status with status colors", + "auth": "jwt-optional", + "params": "assettypeid, businessunitid, format=json|csv", + "example": "curl 'http://localhost:5001/api/reports/assets-by-status?assettypeid=1'" + }, + { + "method": "GET", + "path": "/api/reports/kb-popularity", + "purpose": "Most-clicked knowledge base articles; 503 if knowledgebase plugin absent", + "auth": "jwt-optional", + "params": "limit (default 20, max 100), format=json|csv", + "example": "curl 'http://localhost:5001/api/reports/kb-popularity?limit=10'" + }, + { + "method": "GET", + "path": "/api/reports/software-compliance", + "purpose": "Required applications vs installed per PC with compliance rate and up to 100 non-compliant PCs per app; 503 if computers plugin absent", + "auth": "jwt-optional", + "params": "appid (filter to one app), format=json|csv", + "example": "curl 'http://localhost:5001/api/reports/software-compliance?appid=5'" + }, + { + "method": "GET", + "path": "/api/reports/asset-inventory", + "purpose": "Complete asset inventory summary broken down by type, status, and location", + "auth": "jwt-optional", + "params": "businessunitid, locationid, format=json|csv", + "example": "curl 'http://localhost:5001/api/reports/asset-inventory?locationid=3&format=csv'" + }, + { + "method": "GET", + "path": "/api/reports/pc-relationships", + "purpose": "PC-to-shop-floor-machine relationships (machine number, vendor, model, hostname, IP) matched in both edge directions via raw SQL UNION", + "auth": "jwt-optional", + "params": "format=json|csv", + "example": "curl 'http://localhost:5001/api/reports/pc-relationships?format=csv'" + }, + { + "method": "POST", + "path": "/api/reports/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", + "auth": "jwt + permission:reports.export", + "params": "body: subject, columns [{key,label}], rows [{..}], intro (optional), to (optional email)", + "example": "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\"}'" + }, + { + "method": "GET", + "path": "/api/search", + "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", + "auth": "jwt-optional", + "params": "q (required, 2-200 chars)", + "example": "curl 'http://localhost:5001/api/search?q=WKSTN0042'" + }, + { + "method": "GET", + "path": "/api/dashboard", + "purpose": "Dashboard summary: asset counts by type (machines/PCs/network/printers/measuring tools, dualpath-collapsed), counts by status, 10 most recent assets", + "auth": "jwt-optional", + "params": "none", + "example": "curl http://localhost:5001/api/dashboard" + }, + { + "method": "GET", + "path": "/api/dashboard/summary", + "purpose": "Alias route for the same dashboard summary handler as GET /api/dashboard", + "auth": "jwt-optional", + "params": "none", + "example": "curl http://localhost:5001/api/dashboard/summary" + }, + { + "method": "GET", + "path": "/api/dashboard/stats", + "purpose": "Asset counts grouped by every asset type with display category labels", + "auth": "jwt-optional", + "params": "none", + "example": "curl http://localhost:5001/api/dashboard/stats" + }, + { + "method": "GET", + "path": "/api/dashboard/navigation", + "purpose": "Sidebar navigation items: core entries merged with get_navigation_items from every enabled plugin, sorted by position", + "auth": "none", + "params": "none", + "example": "curl http://localhost:5001/api/dashboard/navigation" + }, + { + "method": "GET", + "path": "/api/dashboard/widgets", + "purpose": "Dashboard widget definitions aggregated from enabled plugins (get_dashboard_widgets hook), sorted by position", + "auth": "jwt-optional", + "params": "none", + "example": "curl http://localhost:5001/api/dashboard/widgets" + }, + { + "method": "GET", + "path": "/api/dashboard/health", + "purpose": "Health check: runs SELECT 1 against the DB, returns ok/degraded plus app version", + "auth": "none", + "params": "none", + "example": "curl http://localhost:5001/api/dashboard/health" + }, + { + "method": "GET", + "path": "/api/dashboarddefaults/visitor-location", + "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", + "auth": "none", + "params": "fqdn (optional), ipaddress (optional, defaults to client IP)", + "example": "curl 'http://localhost:5001/api/dashboarddefaults/visitor-location?fqdn=display01.example.net'" + }, + { + "method": "GET", + "path": "/api/dashboarddefaults/display-role", + "purpose": "Resolve what a display PC should show at boot: role (dashboard/lobby/partskiosk), frontend path, and business unit; null role when unmapped", + "auth": "none", + "params": "fqdn (optional), ipaddress (optional, defaults to client IP)", + "example": "curl 'http://localhost:5001/api/dashboarddefaults/display-role?ipaddress=10.1.2.3'" + }, + { + "method": "GET", + "path": "/api/dashboarddefaults", + "purpose": "List all active display-to-business-unit mappings", + "auth": "jwt-optional", + "params": "none", + "example": "curl http://localhost:5001/api/dashboarddefaults" + }, + { + "method": "POST", + "path": "/api/dashboarddefaults", + "purpose": "Create a display mapping; requires fqdn or ipaddress, dashboard role requires businessunitid, 409 on duplicate fqdn/IP; audit-logged", + "auth": "jwt + role:admin", + "params": "body: fqdn, ipaddress, displayrole (dashboard|lobby|partskiosk, default dashboard), businessunitid, description", + "example": "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}'" + }, + { + "method": "PUT", + "path": "/api/dashboarddefaults/", + "purpose": "Update a display mapping; non-dashboard roles get businessunitid nulled, dashboard role must keep one", + "auth": "jwt + role:admin", + "params": "body: any of fqdn, ipaddress, displayrole, businessunitid, description", + "example": "curl -X PUT http://localhost:5001/api/dashboarddefaults/7 -H \"Authorization: Bearer $JWT\" -H 'Content-Type: application/json' -d '{\"displayrole\":\"lobby\"}'" + }, + { + "method": "DELETE", + "path": "/api/dashboarddefaults/", + "purpose": "Soft-delete (deactivate) a display mapping", + "auth": "jwt + role:admin", + "params": "none", + "example": "curl -X DELETE http://localhost:5001/api/dashboarddefaults/7 -H \"Authorization: Bearer $JWT\"" + }, + { + "method": "POST", + "path": "/api/collector/", + "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", + "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", + "example": "curl -X POST http://localhost:5001/api/collector/computers -H \"X-API-Key: $KEY\" -H 'Content-Type: application/json' -d '{\"hostname\":\"WKSTN0042\",\"serialnumber\":\"ABC123\"}'" + }, + { + "method": "GET", + "path": "/api/collector/_schemas", + "purpose": "List collector schemas for all enabled plugins that accept collector input", + "auth": "jwt", + "params": "none", + "example": "curl http://localhost:5001/api/collector/_schemas -H \"Authorization: Bearer $JWT\"" + }, + { + "method": "POST", + "path": "/api/collector/pc", + "purpose": "Legacy computers-specific ingest: update one PC matched by hostname (or asset number) - lastreporteddate, lastboottime, loggedinuser, serialnumber", + "auth": "api-key", + "params": "body: hostname (required), lastboottime (ISO), currentuser, serialnumber", + "example": "curl -X POST http://localhost:5001/api/collector/pc -H \"X-API-Key: $KEY\" -H 'Content-Type: application/json' -d '{\"hostname\":\"WKSTN0042\",\"currentuser\":\"212345678\"}'" + }, + { + "method": "POST", + "path": "/api/collector/apps", + "purpose": "Update installed applications for one PC; only apps already in the Application table are tracked, others skipped; returns created/updated/skipped counts", + "auth": "api-key", + "params": "body: hostname (required), apps [{appname, version}] (required)", + "example": "curl -X POST http://localhost:5001/api/collector/apps -H \"X-API-Key: $KEY\" -H 'Content-Type: application/json' -d '{\"hostname\":\"WKSTN0042\",\"apps\":[{\"appname\":\"PC-DMIS\",\"version\":\"2023.2\"}]}'" + }, + { + "method": "POST", + "path": "/api/collector/heartbeat", + "purpose": "Record PC online heartbeat (single hostname or batch); stamps lastreporteddate, returns updated count and notfound list", + "auth": "api-key", + "params": "body: hostname (string) or hostnames (array)", + "example": "curl -X POST http://localhost:5001/api/collector/heartbeat -H \"X-API-Key: $KEY\" -H 'Content-Type: application/json' -d '{\"hostnames\":[\"pc1\",\"pc2\"]}'" + }, + { + "method": "POST", + "path": "/api/collector/bulk", + "purpose": "Bulk update many PCs in one call (lastreporteddate, currentuser, lastboottime per entry); returns updated/notfound/errors", + "auth": "api-key", + "params": "body: pcs [{hostname (required), currentuser, lastboottime}]", + "example": "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\"}]}'" + }, + { + "method": "GET", + "path": "/api/collector/status", + "purpose": "Collector API liveness/credential check; returns timestamp and the collector endpoint list", + "auth": "api-key", + "params": "none", + "example": "curl http://localhost:5001/api/collector/status -H \"X-API-Key: $KEY\"" + }, + { + "method": "GET", + "path": "/api/auditlogs", + "purpose": "List audit logs with filtering and pagination, newest first; rows enriched with best-effort SSO-to-full-name resolution", + "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)", + "example": "curl 'http://localhost:5001/api/auditlogs?action=deleted&perpage=100' -H \"Authorization: Bearer $JWT\"" + }, + { + "method": "GET", + "path": "/api/auditlogs/entity//", + "purpose": "Full audit history for one entity, newest first", + "auth": "jwt + permission:audit.view", + "params": "path only", + "example": "curl http://localhost:5001/api/auditlogs/entity/Asset/42 -H \"Authorization: Bearer $JWT\"" + }, + { + "method": "GET", + "path": "/api/auditlogs/stats", + "purpose": "Audit statistics: counts by action and entity type, last-7-days activity count, top 5 most active users", + "auth": "jwt + permission:audit.view", + "params": "none", + "example": "curl http://localhost:5001/api/auditlogs/stats -H \"Authorization: Bearer $JWT\"" + }, + { + "method": "GET", + "path": "/api/plugins", + "purpose": "List all discovered plugins (enabled or not) with metadata plus the framework contract version", + "auth": "jwt-optional", + "params": "none", + "example": "curl http://localhost:5001/api/plugins" + }, + { + "method": "GET", + "path": "/api/plugins/enabled", + "purpose": "Flat sorted array of enabled plugin names (registry read, no DB); deliberately anonymous so kiosk routes can gate plugin-owned frontend routes", + "auth": "none (jwt-optional decorator, no claims used)", + "params": "none", + "example": "curl http://localhost:5001/api/plugins/enabled" + }, + { + "method": "PUT", + "path": "/api/plugins/", + "purpose": "Enable or disable a plugin (route changes need an app restart); 409 when unknown or a dependency conflict refuses the change", + "auth": "jwt + role:admin", + "params": "body: enabled (bool, required)", + "example": "curl -X PUT http://localhost:5001/api/plugins/warranty -H \"Authorization: Bearer $JWT\" -H 'Content-Type: application/json' -d '{\"enabled\":false}'" + }, + { + "method": "GET", + "path": "/api/pluginui/settings-cards", + "purpose": "Merge enabled plugins' settings-catalog cards (get_settings_cards hook) sorted by position for the settings rail/overview", + "auth": "jwt-optional", + "params": "none", + "example": "curl http://localhost:5001/api/pluginui/settings-cards" + }, + { + "method": "GET", + "path": "/api/pluginui/asset-panels", + "purpose": "Asset-detail panels from enabled plugins (get_asset_panels hook) matching one asset's type ('*' wildcard supported), sorted by position", + "auth": "jwt-optional", + "params": "assetid (int, required); 400 without it, 404 if asset missing", + "example": "curl 'http://localhost:5001/api/pluginui/asset-panels?assetid=42'" + }, + { + "method": "GET", + "path": "/api/pluginui/map-overlays", + "purpose": "Merge enabled plugins' map overlay declarations (get_map_overlays hook), sorted by position", + "auth": "jwt-optional", + "params": "none", + "example": "curl http://localhost:5001/api/pluginui/map-overlays" + }, + { + "method": "GET", + "path": "/api/pluginui/asset-presentation", + "purpose": "Merge enabled plugins' asset-type presentation entries (icon + detail route per type) used by search rows and cross-links", + "auth": "jwt-optional", + "params": "none", + "example": "curl http://localhost:5001/api/pluginui/asset-presentation" + } + ] + }, + { + "surface": "plugin-employees", + "endpoints": [ + { + "method": "GET", + "path": "/api/employees/search", + "purpose": "Search employees by first name, last name, or SSO substring (self-hosted table or external HR DB depending on employee_directory_mode setting).", + "auth": "none", + "params": "q (query string, min 2 chars, required), limit (max results, default 10, capped 50)", + "example": "curl 'http://localhost:5001/api/employees/search?q=smith&limit=5'" + }, + { + "method": "GET", + "path": "/api/employees/lookup/", + "purpose": "Look up a single employee by numeric SSO; returns directory fields plus resolved photourl.", + "auth": "none", + "params": "sso (path, numeric)", + "example": "curl 'http://localhost:5001/api/employees/lookup/210009518'" + }, + { + "method": "GET", + "path": "/api/employees/lookup", + "purpose": "Bulk lookup of multiple employees by SSO list; returns employees array plus a joined names string.", + "auth": "none", + "params": "sso (query, comma-separated numeric SSOs, at least one required)", + "example": "curl 'http://localhost:5001/api/employees/lookup?sso=210009518,210001234'" + }, + { + "method": "GET", + "path": "/api/employees/directory", + "purpose": "List the full self-hosted directory for the management page; 400 when directory mode is external.", + "auth": "jwt-optional", + "params": "none", + "example": "curl -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/employees/directory'" + }, + { + "method": "POST", + "path": "/api/employees/directory", + "purpose": "Create a self-hosted directory employee; 409 if SSO exists, 400 in external mode.", + "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)", + "example": "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'" + }, + { + "method": "PUT", + "path": "/api/employees/directory/", + "purpose": "Update a self-hosted directory employee's name/team/role/picture; 404 if missing, 400 in external mode.", + "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)", + "example": "curl -X PUT -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"team\":\"Quality\"}' 'http://localhost:5001/api/employees/directory/210009518'" + }, + { + "method": "DELETE", + "path": "/api/employees/directory/", + "purpose": "Delete a self-hosted directory employee; 404 if missing, 400 in external mode.", + "auth": "jwt + require_role admin", + "params": "sso (path)", + "example": "curl -X DELETE -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/employees/directory/210009518'" + }, + { + "method": "POST", + "path": "/api/employees/directory/import", + "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.", + "auth": "jwt + require_role admin", + "params": "multipart file= OR JSON body {\"csv\": \"...\"}; rows missing numeric sso or names are skipped", + "example": "curl -X POST -H \"Authorization: Bearer $TOKEN\" -F 'file=@employees.csv' 'http://localhost:5001/api/employees/directory/import'" + }, + { + "method": "POST", + "path": "/api/employees//photo", + "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.", + "auth": "jwt + require_role admin", + "params": "sso (path); multipart/form-data file=, extensions .png/.jpg/.jpeg/.gif/.webp only", + "example": "curl -X POST -H \"Authorization: Bearer $TOKEN\" -F 'file=@jane.jpg' 'http://localhost:5001/api/employees/210009518/photo'" + }, + { + "method": "GET", + "path": "/api/employees/photo/", + "purpose": "Serve an uploaded employee photo file from the instance employeephotos dir (public so kiosk recognition cards can read it).", + "auth": "none", + "params": "filename (path, e.g. photo-210009518.jpg)", + "example": "curl 'http://localhost:5001/api/employees/photo/photo-210009518.jpg'" + }, + { + "method": "DELETE", + "path": "/api/employees//photo", + "purpose": "Clear a self-hosted employee's photo record and delete the uploaded file; 409 in external mode, 404 if employee missing.", + "auth": "jwt + require_role admin", + "params": "sso (path)", + "example": "curl -X DELETE -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/employees/210009518/photo'" + }, + { + "method": "GET", + "path": "/api/employees/resolve/", + "auth": "jwt", + "params": "sso in path", + "purpose": "Resolve one SSO to a name and photo, and say HOW it resolved or why it did not. Built because the failure is otherwise invisible: the shop-floor board resolves names through functions that returned None on any miss, so a directory problem and a typo looked identical", + "example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/employees/resolve/123456789" + } + ] + }, + { + "endpoints": [ + { + "method": "GET", + "path": "/api/knowledgebase", + "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.", + "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)", + "example": "curl 'http://localhost:5001/api/knowledgebase?search=printer&sort=clicks&order=desc&page=1&per_page=20'" + }, + { + "method": "GET", + "path": "/api/knowledgebase/stats", + "purpose": "Return aggregate stats for active articles: totalclicks (sum of clicks) and totalarticles (count).", + "auth": "jwt-optional", + "params": "none", + "example": "curl 'http://localhost:5001/api/knowledgebase/stats'" + }, + { + "method": "GET", + "path": "/api/knowledgebase/", + "purpose": "Fetch a single active article by id with its application (appid/appname) or null; 404 if missing or inactive.", + "auth": "jwt-optional", + "params": "path: link_id (int)", + "example": "curl 'http://localhost:5001/api/knowledgebase/42'" + }, + { + "method": "POST", + "path": "/api/knowledgebase//click", + "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.", + "auth": "jwt-optional", + "params": "path: link_id (int); no body", + "example": "curl -X POST 'http://localhost:5001/api/knowledgebase/42/click'" + }, + { + "method": "POST", + "path": "/api/knowledgebase", + "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.", + "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", + "example": "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\"}'" + }, + { + "method": "PUT", + "path": "/api/knowledgebase/", + "purpose": "Update an article's shortdescription, linkurl, appid, keywords, and/or isactive; validates appid if changed; honors import timestamps; 404 if article missing.", + "auth": "permission:kb.edit (jwt required)", + "params": "path: link_id (int); body JSON: any of shortdescription, linkurl, appid, keywords, isactive", + "example": "curl -X PUT 'http://localhost:5001/api/knowledgebase/42' -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"keywords\":\"vpn,zscaler\",\"isactive\":true}'" + }, + { + "method": "DELETE", + "path": "/api/knowledgebase/", + "purpose": "Soft-delete an article by setting isactive=false (row is retained); 404 if article missing.", + "auth": "permission:kb.delete (jwt required)", + "params": "path: link_id (int)", + "example": "curl -X DELETE 'http://localhost:5001/api/knowledgebase/42' -H \"Authorization: Bearer $TOKEN\"" + } + ], + "surface": "plugin-knowledgebase" + }, + { + "surface": "core-catalog", + "endpoints": [ + { + "method": "GET", + "path": "/api/assets/types", + "purpose": "List asset types (paginated, active-only by default)", + "auth": "jwt-optional", + "params": "page, per_page, active=false to include inactive", + "example": "curl http://localhost:5001/api/assets/types?active=false" + }, + { + "method": "GET", + "path": "/api/assets/types/", + "purpose": "Get one asset type", + "auth": "jwt-optional", + "params": "path: type_id", + "example": "curl http://localhost:5001/api/assets/types/1" + }, + { + "method": "POST", + "path": "/api/assets/types", + "purpose": "Create asset type (409 on duplicate name)", + "auth": "permission:assets.create", + "params": "body: assettype (req), pluginname, tablename, description, icon, color", + "example": "curl -X POST http://localhost:5001/api/assets/types -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"assettype\":\"Robot\",\"icon\":\"mdi-robot\"}'" + }, + { + "method": "PUT", + "path": "/api/assets/types/", + "purpose": "Update asset type display fields only (name/plugin/table are structural, not editable)", + "auth": "permission:assets.edit", + "params": "body: description, icon, color, isactive", + "example": "curl -X PUT http://localhost:5001/api/assets/types/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"color\":\"#ff0000\"}'" + }, + { + "method": "GET", + "path": "/api/assets/statuses", + "purpose": "List asset statuses (paginated, active-only by default)", + "auth": "jwt-optional", + "params": "page, per_page, active=false", + "example": "curl http://localhost:5001/api/assets/statuses" + }, + { + "method": "GET", + "path": "/api/assets/statuses/", + "purpose": "Get one asset status", + "auth": "jwt-optional", + "params": "path: status_id", + "example": "curl http://localhost:5001/api/assets/statuses/1" + }, + { + "method": "POST", + "path": "/api/assets/statuses", + "purpose": "Create asset status (409 on duplicate)", + "auth": "permission:assets.create", + "params": "body: status (req), description, color", + "example": "curl -X POST http://localhost:5001/api/assets/statuses -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"status\":\"In Repair\",\"color\":\"#f90\"}'" + }, + { + "method": "PUT", + "path": "/api/assets/statuses/", + "purpose": "Update asset status (rename conflict-checked)", + "auth": "permission:assets.edit", + "params": "body: status, description, color, isactive", + "example": "curl -X PUT http://localhost:5001/api/assets/statuses/2 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"isactive\":false}'" + }, + { + "method": "DELETE", + "path": "/api/assets/statuses/", + "purpose": "Hard-delete asset status; 409 if any asset still uses it", + "auth": "permission:assets.delete", + "params": "path: status_id", + "example": "curl -X DELETE http://localhost:5001/api/assets/statuses/9 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/assets/relationshiptypes", + "purpose": "List relationship types incl. read-only propagatesthrough rails", + "auth": "jwt-optional", + "params": "none", + "example": "curl http://localhost:5001/api/assets/relationshiptypes" + }, + { + "method": "POST", + "path": "/api/assets/relationshiptypes", + "purpose": "Create relationship type (409 on duplicate)", + "auth": "permission:assets.create", + "params": "body: relationshiptype (req), description, color, isdirectional (default true)", + "example": "curl -X POST http://localhost:5001/api/assets/relationshiptypes -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"relationshiptype\":\"controls\",\"isdirectional\":true}'" + }, + { + "method": "PUT", + "path": "/api/assets/relationshiptypes/", + "purpose": "Update relationship type (rename conflict-checked)", + "auth": "permission:assets.edit", + "params": "body: relationshiptype, description, color, isdirectional", + "example": "curl -X PUT http://localhost:5001/api/assets/relationshiptypes/3 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"color\":\"#00f\"}'" + }, + { + "method": "DELETE", + "path": "/api/assets/relationshiptypes/", + "purpose": "Hard-delete relationship type; 409 while relationships use it", + "auth": "permission:assets.delete", + "params": "path: type_id", + "example": "curl -X DELETE http://localhost:5001/api/assets/relationshiptypes/3 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/assets", + "purpose": "List assets with filtering, search, sorting, pagination", + "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", + "example": "curl 'http://localhost:5001/api/assets?type=machine&search=205&include_type_data=true'" + }, + { + "method": "GET", + "path": "/api/assets/", + "purpose": "Get one asset with full details", + "auth": "jwt-optional", + "params": "include_type_data (default true)", + "example": "curl http://localhost:5001/api/assets/42" + }, + { + "method": "POST", + "path": "/api/assets", + "purpose": "Create asset (duplicate assetnumber 409, assettypeid validated); honors X-Import-Mode timestamps", + "auth": "permission:assets.create", + "params": "body: assetnumber (req), assettypeid (req), name, serialnumber, statusid (default 1), locationid, businessunitid, mapx, mapy, notes", + "example": "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\"}'" + }, + { + "method": "PUT", + "path": "/api/assets/", + "purpose": "Update asset (allowed fields incl. isactive); honors X-Import-Mode", + "auth": "permission:assets.edit", + "params": "body: assetnumber, name, serialnumber, assettypeid, statusid, locationid, businessunitid, mapx, mapy, notes, isactive", + "example": "curl -X PUT http://localhost:5001/api/assets/42 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"locationid\":3}'" + }, + { + "method": "DELETE", + "path": "/api/assets/", + "purpose": "Soft-delete asset (isactive=false)", + "auth": "permission:assets.delete", + "params": "path: asset_id", + "example": "curl -X DELETE http://localhost:5001/api/assets/42 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/assets/lookup/", + "purpose": "Look up an active asset by asset number (returns full type data)", + "auth": "jwt-optional", + "params": "path: assetnumber (string)", + "example": "curl http://localhost:5001/api/assets/lookup/0205" + }, + { + "method": "GET", + "path": "/api/assets//relationships", + "purpose": "Get outgoing + incoming active relationships for an asset with partner asset dicts", + "auth": "jwt-optional", + "params": "path: asset_id", + "example": "curl http://localhost:5001/api/assets/42/relationships" + }, + { + "method": "POST", + "path": "/api/assets/relationships", + "purpose": "Create relationship, then fan out across symmetric propagation rails (Dualpath); response carries propagated + propagatedcount", + "auth": "permission:assets.create", + "params": "body: sourceassetid (req), targetassetid (req), relationshiptypeid (req), notes; X-Import-Mode honored", + "example": "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}'" + }, + { + "method": "DELETE", + "path": "/api/assets/relationships/", + "purpose": "Soft-delete one relationship row (no cascade to propagated partner rows)", + "auth": "permission:assets.delete", + "params": "path: rel_id", + "example": "curl -X DELETE http://localhost:5001/api/assets/relationships/7 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/assets/map", + "purpose": "Unified floor-map payload: all mapped assets (with type data, primary IP, dualpath collapse) plus filter option lists", + "auth": "jwt-optional", + "params": "assettype (name), subtype (id, per-type), businessunitid, statusid, locationid, search", + "example": "curl 'http://localhost:5001/api/assets/map?assettype=machine&statusid=1'" + }, + { + "method": "GET", + "path": "/api/assets//communications", + "purpose": "List active communications (IPs etc.) for an asset with comtype_name", + "auth": "jwt-optional", + "params": "path: asset_id", + "example": "curl http://localhost:5001/api/assets/42/communications" + }, + { + "method": "GET", + "path": "/api/locations/types", + "purpose": "List location types", + "auth": "jwt-optional", + "params": "active=false includes inactive", + "example": "curl http://localhost:5001/api/locations/types" + }, + { + "method": "POST", + "path": "/api/locations/types", + "purpose": "Create location type; reactivates a soft-deleted same-name type instead of 409", + "auth": "admin", + "params": "body: locationtype (req), description, color", + "example": "curl -X POST http://localhost:5001/api/locations/types -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"locationtype\":\"Operation\"}'" + }, + { + "method": "PUT", + "path": "/api/locations/types/", + "purpose": "Update location type (rename conflict-checked)", + "auth": "admin", + "params": "body: locationtype, description, color, isactive", + "example": "curl -X PUT http://localhost:5001/api/locations/types/2 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"color\":\"#0a0\"}'" + }, + { + "method": "DELETE", + "path": "/api/locations/types/", + "purpose": "Hard-delete location type; 409 while locations use it", + "auth": "admin", + "params": "path: type_id", + "example": "curl -X DELETE http://localhost:5001/api/locations/types/2 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/locations", + "purpose": "List locations (paginated); exact locationname lookup for idempotent import", + "auth": "jwt-optional", + "params": "page, per_page, active, locationname (exact), search (name/building ilike)", + "example": "curl 'http://localhost:5001/api/locations?locationname=Building%201'" + }, + { + "method": "GET", + "path": "/api/locations/", + "purpose": "Get one location", + "auth": "jwt-optional", + "params": "path: location_id", + "example": "curl http://localhost:5001/api/locations/3" + }, + { + "method": "POST", + "path": "/api/locations", + "purpose": "Create location (409 on duplicate name); honors X-Import-Mode", + "auth": "admin", + "params": "body: locationname (req), building, floor, room, description, locationtypeid, parentlocationid, mapimage, mapwidth, mapheight", + "example": "curl -X POST http://localhost:5001/api/locations -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"locationname\":\"Cell 12\",\"building\":\"B1\"}'" + }, + { + "method": "PUT", + "path": "/api/locations/", + "purpose": "Update location (rename conflict-checked); honors X-Import-Mode", + "auth": "admin", + "params": "body: locationname, building, floor, room, description, locationtypeid, parentlocationid, mapimage, mapwidth, mapheight, isactive", + "example": "curl -X PUT http://localhost:5001/api/locations/3 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"room\":\"104\"}'" + }, + { + "method": "DELETE", + "path": "/api/locations/", + "purpose": "Soft-delete location", + "auth": "admin", + "params": "path: location_id", + "example": "curl -X DELETE http://localhost:5001/api/locations/3 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/vendors", + "purpose": "List vendors (paginated); exact vendor lookup for idempotent import", + "auth": "jwt-optional", + "params": "page, per_page, active, vendor (exact), search (ilike)", + "example": "curl 'http://localhost:5001/api/vendors?search=fanuc'" + }, + { + "method": "GET", + "path": "/api/vendors/", + "purpose": "Get one vendor", + "auth": "jwt-optional", + "params": "path: vendor_id", + "example": "curl http://localhost:5001/api/vendors/5" + }, + { + "method": "POST", + "path": "/api/vendors", + "purpose": "Create vendor (409 on duplicate); honors X-Import-Mode", + "auth": "admin", + "params": "body: vendor (req), description, website, supportphone, supportemail, notes", + "example": "curl -X POST http://localhost:5001/api/vendors -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"vendor\":\"Fanuc\"}'" + }, + { + "method": "PUT", + "path": "/api/vendors/", + "purpose": "Update vendor (rename conflict-checked); honors X-Import-Mode", + "auth": "admin", + "params": "body: vendor, description, website, supportphone, supportemail, notes, isactive", + "example": "curl -X PUT http://localhost:5001/api/vendors/5 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"website\":\"https://fanuc.com\"}'" + }, + { + "method": "DELETE", + "path": "/api/vendors/", + "purpose": "Soft-delete vendor", + "auth": "admin", + "params": "path: vendor_id", + "example": "curl -X DELETE http://localhost:5001/api/vendors/5 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/models", + "purpose": "List vendor catalog models (paginated) with flattened vendor/modeltype names; exact modelnumber+vendor lookup for import", + "auth": "jwt-optional", + "params": "page, per_page, active, vendor (id), modeltype (id), modelnumber (exact), search (ilike)", + "example": "curl 'http://localhost:5001/api/models?vendor=5&search=30i'" + }, + { + "method": "GET", + "path": "/api/models/", + "purpose": "Get one model with nested vendor + modeltype dicts", + "auth": "jwt-optional", + "params": "path: model_id", + "example": "curl http://localhost:5001/api/models/12" + }, + { + "method": "POST", + "path": "/api/models", + "purpose": "Create model (409 on duplicate modelnumber+vendorid); honors X-Import-Mode", + "auth": "admin", + "params": "body: modelnumber (req), vendorid, modeltypeid, description, imageurl, documentationurl, notes", + "example": "curl -X POST http://localhost:5001/api/models -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"modelnumber\":\"R-30iB\",\"vendorid\":5}'" + }, + { + "method": "PUT", + "path": "/api/models/", + "purpose": "Update model; honors X-Import-Mode", + "auth": "admin", + "params": "body: modelnumber, vendorid, modeltypeid, description, imageurl, documentationurl, notes, isactive", + "example": "curl -X PUT http://localhost:5001/api/models/12 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"notes\":\"EOL 2027\"}'" + }, + { + "method": "DELETE", + "path": "/api/models/", + "purpose": "Soft-delete model", + "auth": "admin", + "params": "path: model_id", + "example": "curl -X DELETE http://localhost:5001/api/models/12 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/models//image", + "purpose": "Upload/replace model photo (saved as instance/modelimages/model-, one per model); sets imageurl", + "auth": "admin", + "params": "multipart/form-data: file= (.png/.jpg/.jpeg/.gif/.webp/.svg)", + "example": "curl -X POST http://localhost:5001/api/models/12/image -H \"Authorization: Bearer $TOK\" -F file=@robot.jpg" + }, + { + "method": "GET", + "path": "/api/models/image/", + "purpose": "Serve an uploaded model image (deliberately public - asset detail pages read it without auth)", + "auth": "none", + "params": "path: filename", + "example": "curl http://localhost:5001/api/models/image/model-12.jpg" + }, + { + "method": "DELETE", + "path": "/api/models//image", + "purpose": "Clear imageurl and delete the uploaded file only if it lives under /api/models/image/ (external URLs untouched)", + "auth": "admin", + "params": "path: model_id", + "example": "curl -X DELETE http://localhost:5001/api/models/12/image -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/modeltypes", + "purpose": "List model types (types the vendor MODELS catalog, not machines); exact modeltype lookup for import", + "auth": "jwt-optional", + "params": "page, per_page, active, category, modeltype (exact), search (ilike)", + "example": "curl 'http://localhost:5001/api/modeltypes?category=Equipment'" + }, + { + "method": "GET", + "path": "/api/modeltypes/", + "purpose": "Get one model type", + "auth": "jwt-optional", + "params": "path: type_id", + "example": "curl http://localhost:5001/api/modeltypes/2" + }, + { + "method": "POST", + "path": "/api/modeltypes", + "purpose": "Create model type (409 on duplicate); category defaults to Equipment; honors X-Import-Mode", + "auth": "admin", + "params": "body: modeltype (req), category, description, icon", + "example": "curl -X POST http://localhost:5001/api/modeltypes -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"modeltype\":\"Controller\"}'" + }, + { + "method": "PUT", + "path": "/api/modeltypes/", + "purpose": "Update model type (rename conflict-checked); honors X-Import-Mode", + "auth": "admin", + "params": "body: modeltype, category, description, icon, isactive", + "example": "curl -X PUT http://localhost:5001/api/modeltypes/2 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"icon\":\"mdi-chip\"}'" + }, + { + "method": "DELETE", + "path": "/api/modeltypes/", + "purpose": "Soft-delete model type; 409 while models use it", + "auth": "admin", + "params": "path: type_id", + "example": "curl -X DELETE http://localhost:5001/api/modeltypes/2 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/businessunits", + "purpose": "List business units (paginated); exact businessunit lookup for import", + "auth": "jwt-optional", + "params": "page, per_page, active, businessunit (exact), search (name/code ilike)", + "example": "curl http://localhost:5001/api/businessunits" + }, + { + "method": "GET", + "path": "/api/businessunits/", + "purpose": "Get one business unit with parent + children", + "auth": "jwt-optional", + "params": "path: bu_id", + "example": "curl http://localhost:5001/api/businessunits/1" + }, + { + "method": "POST", + "path": "/api/businessunits", + "purpose": "Create business unit (409 on duplicate); honors X-Import-Mode", + "auth": "admin", + "params": "body: businessunit (req), code, description, parentid", + "example": "curl -X POST http://localhost:5001/api/businessunits -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"businessunit\":\"Blades\",\"code\":\"BLD\"}'" + }, + { + "method": "PUT", + "path": "/api/businessunits/", + "purpose": "Update business unit (rename conflict-checked); honors X-Import-Mode", + "auth": "admin", + "params": "body: businessunit, code, description, parentid, isactive", + "example": "curl -X PUT http://localhost:5001/api/businessunits/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"code\":\"BL\"}'" + }, + { + "method": "DELETE", + "path": "/api/businessunits/", + "purpose": "Soft-delete business unit", + "auth": "admin", + "params": "path: bu_id", + "example": "curl -X DELETE http://localhost:5001/api/businessunits/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/operatingsystems", + "purpose": "List operating systems (paginated); exact osname/osversion lookup for import", + "auth": "jwt-optional", + "params": "page, per_page, active, osname (exact), osversion (exact), search (osname ilike)", + "example": "curl 'http://localhost:5001/api/operatingsystems?osname=Windows%2011'" + }, + { + "method": "GET", + "path": "/api/operatingsystems/", + "purpose": "Get one operating system", + "auth": "jwt-optional", + "params": "path: os_id", + "example": "curl http://localhost:5001/api/operatingsystems/4" + }, + { + "method": "POST", + "path": "/api/operatingsystems", + "purpose": "Create OS (409 on duplicate osname+osversion); honors X-Import-Mode", + "auth": "admin", + "params": "body: osname (req), osversion, architecture, endoflife", + "example": "curl -X POST http://localhost:5001/api/operatingsystems -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"osname\":\"Windows 11\",\"osversion\":\"24H2\"}'" + }, + { + "method": "PUT", + "path": "/api/operatingsystems/", + "purpose": "Update OS; honors X-Import-Mode", + "auth": "admin", + "params": "body: osname, osversion, architecture, endoflife, isactive", + "example": "curl -X PUT http://localhost:5001/api/operatingsystems/4 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"endoflife\":\"2031-10-14\"}'" + }, + { + "method": "DELETE", + "path": "/api/operatingsystems/", + "purpose": "Soft-delete OS", + "auth": "admin", + "params": "path: os_id", + "example": "curl -X DELETE http://localhost:5001/api/operatingsystems/4 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/customfields", + "purpose": "List custom-field definitions, ordered by sortorder", + "auth": "jwt-optional", + "params": "assettypeid (int), active=false includes inactive", + "example": "curl 'http://localhost:5001/api/customfields?assettypeid=1'" + }, + { + "method": "POST", + "path": "/api/customfields", + "purpose": "Create field definition; fieldkey auto-slugged from label; reactivates a soft-deleted same-key field instead of 409", + "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", + "example": "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\"]}'" + }, + { + "method": "PUT", + "path": "/api/customfields/", + "purpose": "Update field definition (label/datatype/options/flags/sortorder)", + "auth": "admin", + "params": "body: label, datatype, options, showondetail, showonform, isactive, searchable, sortorder", + "example": "curl -X PUT http://localhost:5001/api/customfields/7 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"searchable\":true}'" + }, + { + "method": "DELETE", + "path": "/api/customfields/", + "purpose": "Hard-delete field definition AND all stored values for it", + "auth": "admin", + "params": "path: fieldid", + "example": "curl -X DELETE http://localhost:5001/api/customfields/7 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/customfields/asset/", + "purpose": "Active field defs for the asset's type merged with the asset's stored values", + "auth": "jwt-optional", + "params": "path: assetid", + "example": "curl http://localhost:5001/api/customfields/asset/42" + }, + { + "method": "PUT", + "path": "/api/customfields/asset/", + "purpose": "Upsert per-asset values; empty string clears a value; only fields of the asset's type accepted", + "auth": "admin", + "params": "body: {values: {fieldid: value, ...}}", + "example": "curl -X PUT http://localhost:5001/api/customfields/asset/42 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"values\":{\"7\":\"Oil\"}}'" + }, + { + "method": "GET", + "path": "/api/supportteams", + "purpose": "List support teams with contacts; exact teamname lookup for import (not paginated)", + "auth": "jwt-optional", + "params": "active, teamname (exact)", + "example": "curl http://localhost:5001/api/supportteams" + }, + { + "method": "GET", + "path": "/api/supportteams/", + "purpose": "Get one support team with contacts", + "auth": "jwt-optional", + "params": "path: team_id", + "example": "curl http://localhost:5001/api/supportteams/2" + }, + { + "method": "POST", + "path": "/api/supportteams", + "purpose": "Create support team (409 on duplicate); audit-logged; honors X-Import-Mode", + "auth": "admin", + "params": "body: teamname (req), teamurl, webhookurl, isactive", + "example": "curl -X POST http://localhost:5001/api/supportteams -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"teamname\":\"CNC Support\"}'" + }, + { + "method": "PUT", + "path": "/api/supportteams/", + "purpose": "Update support team (rename conflict-checked); audit-logged; honors X-Import-Mode", + "auth": "admin", + "params": "body: teamname, teamurl, webhookurl, isactive", + "example": "curl -X PUT http://localhost:5001/api/supportteams/2 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"webhookurl\":\"https://hooks/x\"}'" + }, + { + "method": "DELETE", + "path": "/api/supportteams/", + "purpose": "Hard-delete team (cascade removes contacts); 409 while applications reference it; audit-logged", + "auth": "admin", + "params": "path: team_id", + "example": "curl -X DELETE http://localhost:5001/api/supportteams/2 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/supportteams//contacts", + "purpose": "Add contact to a team; audit-logged; honors X-Import-Mode", + "auth": "admin", + "params": "body: name (req), sso, sortorder, isactive", + "example": "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\"}'" + }, + { + "method": "PUT", + "path": "/api/supportteams//contacts/", + "purpose": "Update a team contact; audit-logged; honors X-Import-Mode", + "auth": "admin", + "params": "body: name, sso, sortorder, isactive", + "example": "curl -X PUT http://localhost:5001/api/supportteams/2/contacts/9 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"sortorder\":1}'" + }, + { + "method": "DELETE", + "path": "/api/supportteams//contacts/", + "purpose": "Hard-delete a team contact; audit-logged", + "auth": "admin", + "params": "path: team_id, contact_id", + "example": "curl -X DELETE http://localhost:5001/api/supportteams/2/contacts/9 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/applications", + "purpose": "List applications (paginated) with installedcount; hidden apps excluded unless showhidden=true; exact appname lookup for import", + "auth": "jwt-optional", + "params": "page, per_page, active, showhidden, installable (true/false), appname (exact), search (name/description ilike)", + "example": "curl 'http://localhost:5001/api/applications?installable=true'" + }, + { + "method": "GET", + "path": "/api/applications/", + "purpose": "Get one application with active versions, installedcount, and linked KB articles", + "auth": "jwt-optional", + "params": "path: app_id", + "example": "curl http://localhost:5001/api/applications/15" + }, + { + "method": "POST", + "path": "/api/applications", + "purpose": "Create application (409 on duplicate name); audit-logged; honors X-Import-Mode", + "auth": "permission:applications.create", + "params": "body: appname (req), appdescription, supportteamid, isinstallable, applicationnotes, installpath, applicationlink, documentationpath, ishidden, isprinter, islicenced, isrequired, image", + "example": "curl -X POST http://localhost:5001/api/applications -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"appname\":\"PC-DMIS\",\"isinstallable\":true}'" + }, + { + "method": "PUT", + "path": "/api/applications/", + "purpose": "Update application (rename conflict-checked); field-level change diff audit-logged; honors X-Import-Mode", + "auth": "permission:applications.edit", + "params": "body: any of appname, appdescription, supportteamid, isinstallable, applicationnotes, installpath, applicationlink, documentationpath, ishidden, isprinter, islicenced, isrequired, image, isactive", + "example": "curl -X PUT http://localhost:5001/api/applications/15 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"supportteamid\":2}'" + }, + { + "method": "DELETE", + "path": "/api/applications/", + "purpose": "Soft-delete application; audit-logged", + "auth": "permission:applications.delete", + "params": "path: app_id", + "example": "curl -X DELETE http://localhost:5001/api/applications/15 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/applications//versions", + "purpose": "List active versions of an application (desc)", + "auth": "jwt-optional", + "params": "path: app_id", + "example": "curl http://localhost:5001/api/applications/15/versions" + }, + { + "method": "POST", + "path": "/api/applications//versions", + "purpose": "Create app version (409 on duplicate version per app); honors X-Import-Mode for legacy dates", + "auth": "permission:applications.create", + "params": "body: version (req), releasedate, notes", + "example": "curl -X POST http://localhost:5001/api/applications/15/versions -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"version\":\"2024.2\"}'" + }, + { + "method": "GET", + "path": "/api/applications//installed", + "purpose": "List computers with this app installed (503 if computers plugin absent)", + "auth": "jwt-optional", + "params": "path: app_id", + "example": "curl http://localhost:5001/api/applications/15/installed" + }, + { + "method": "GET", + "path": "/api/applications/machines/", + "purpose": "List apps installed on a computer (machine_id is a computerid; 503 without computers plugin)", + "auth": "jwt-optional", + "params": "path: machine_id", + "example": "curl http://localhost:5001/api/applications/machines/8" + }, + { + "method": "POST", + "path": "/api/applications/machines/", + "purpose": "Install an app on a computer; reactivates a prior soft-deleted install; 409 if already installed", + "auth": "permission:applications.create", + "params": "body: appid (req), appversionid", + "example": "curl -X POST http://localhost:5001/api/applications/machines/8 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"appid\":15}'" + }, + { + "method": "DELETE", + "path": "/api/applications/machines//", + "purpose": "Uninstall (soft-delete install row) an app from a computer", + "auth": "permission:applications.delete", + "params": "path: machine_id, app_id", + "example": "curl -X DELETE http://localhost:5001/api/applications/machines/8/15 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/applications/machines//", + "purpose": "Update an installed-app row (change appversionid)", + "auth": "permission:applications.edit", + "params": "body: appversionid", + "example": "curl -X PUT http://localhost:5001/api/applications/machines/8/15 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"appversionid\":3}'" + }, + { + "method": "POST", + "path": "/api/applications//image", + "auth": "jwt + applications.edit", + "params": "multipart/form-data: file= (png/jpg/gif/webp/svg/ico)", + "purpose": "Upload or replace an application's image; sets application.image to the served URL", + "example": "curl -X POST -H 'Authorization: Bearer ' -F file=@logo.png http://localhost:5001/api/applications/5/image" + }, + { + "method": "GET", + "path": "/api/applications/image/", + "auth": "none", + "params": "none", + "purpose": "Serve an uploaded application image. Public: application tiles render before login", + "example": "curl http://localhost:5001/api/applications/image/application-5.png" + }, + { + "method": "DELETE", + "path": "/api/applications//image", + "auth": "jwt + applications.edit", + "params": "none", + "purpose": "Remove an application's image and clear the field", + "example": "curl -X DELETE -H 'Authorization: Bearer ' http://localhost:5001/api/applications/5/image" + }, + { + "method": "POST", + "path": "/api/applications//package", + "auth": "jwt + applications.edit", + "params": "multipart/form-data: file= (exe/msi/msp/zip/7z/cab/iso/appx/msix/ps1/bat/txt/pdf), max 500MB", + "purpose": "Upload or replace the installer; sets installpath to the download URL", + "example": "curl -X POST -H 'Authorization: Bearer ' -F file=@setup.msi http://localhost:5001/api/applications/5/package" + }, + { + "method": "GET", + "path": "/api/applications/package/", + "auth": "jwt + applications.view", + "params": "none", + "purpose": "Download an uploaded installer as an attachment. Authenticated: licensed vendor software", + "example": "curl -OJ -H 'Authorization: Bearer ' http://localhost:5001/api/applications/package/application-5.msi" + }, + { + "method": "DELETE", + "path": "/api/applications//package", + "auth": "jwt + applications.edit", + "params": "none", + "purpose": "Remove an uploaded installer; clears installpath only when it pointed at the upload", + "example": "curl -X DELETE -H 'Authorization: Bearer ' http://localhost:5001/api/applications/5/package" + } + ] + }, + { + "endpoints": [ + { + "method": "GET", + "path": "/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", + "example": "curl -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/computers/types?search=kiosk&per_page=50'" + }, + { + "method": "GET", + "path": "/api/computers/types/", + "auth": "jwt-optional", + "params": "type_id in path", + "purpose": "Get a single computer type by ID", + "example": "curl 'http://localhost:5001/api/computers/types/3'" + }, + { + "method": "POST", + "path": "/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", + "example": "curl -X POST -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"computertype\":\"Shopfloor\",\"color\":\"#0066cc\"}' 'http://localhost:5001/api/computers/types'" + }, + { + "method": "PUT", + "path": "/api/computers/types/", + "auth": "permission:computers.edit (jwt_required)", + "params": "body: computertype, description, icon, color, isactive; 409 on duplicate name", + "purpose": "Update a computer type", + "example": "curl -X PUT -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"isactive\":false}' 'http://localhost:5001/api/computers/types/3'" + }, + { + "method": "DELETE", + "path": "/api/computers/types/", + "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", + "example": "curl -X DELETE -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/computers/types/3'" + }, + { + "method": "GET", + "path": "/api/computers/protocols", + "auth": "jwt-optional", + "params": "active (default true; 'false' includes disabled)", + "purpose": "List remote-access protocols (VNC/WinRM/RDP catalog), unpaginated", + "example": "curl 'http://localhost:5001/api/computers/protocols?active=false'" + }, + { + "method": "POST", + "path": "/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", + "example": "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'" + }, + { + "method": "PUT|PATCH", + "path": "/api/computers/protocols/", + "auth": "permission:computers.edit (jwt_required)", + "params": "body: name, scheme, linktemplate, defaultport, isactive (all optional)", + "purpose": "Update an access protocol", + "example": "curl -X PATCH -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"defaultport\":5901}' 'http://localhost:5001/api/computers/protocols/2'" + }, + { + "method": "DELETE", + "path": "/api/computers/protocols/", + "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)", + "example": "curl -X DELETE -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/computers/protocols/2'" + }, + { + "method": "GET", + "path": "/api/computers/display-kiosks", + "auth": "jwt-optional", + "params": "none; uses pctype mapping for gea-shopfloor-display (default 'Kiosk') and display_fqdn_domain setting", + "purpose": "List display-kiosk computers with derived F. FQDN for the Dashboard Defaults picker", + "example": "curl -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/computers/display-kiosks'" + }, + { + "method": "GET", + "path": "/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", + "example": "curl 'http://localhost:5001/api/computers?shopfloor=true&sort=lastreporteddate&dir=desc&per_page=25'" + }, + { + "method": "GET", + "path": "/api/computers/", + "auth": "jwt-optional", + "params": "computer_id in path", + "purpose": "Get one computer with full detail: asset fields, computer extension, communications, resolved access links", + "example": "curl 'http://localhost:5001/api/computers/42'" + }, + { + "method": "GET", + "path": "/api/computers/by-asset/", + "auth": "jwt-optional", + "params": "asset_id in path", + "purpose": "Get computer record by its core asset ID", + "example": "curl 'http://localhost:5001/api/computers/by-asset/1234'" + }, + { + "method": "GET", + "path": "/api/computers/by-hostname/", + "auth": "jwt-optional", + "params": "hostname in path (exact match)", + "purpose": "Get computer record by hostname", + "example": "curl 'http://localhost:5001/api/computers/by-hostname/WKSTN0042'" + }, + { + "method": "POST", + "path": "/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", + "example": "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'" + }, + { + "method": "PUT", + "path": "/api/computers/", + "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 protocol list; 409 on assetnumber/hostname conflict; changes audit-logged", + "purpose": "Update a computer's asset and extension fields", + "example": "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'" + }, + { + "method": "DELETE", + "path": "/api/computers/", + "auth": "permission:computers.delete (jwt_required)", + "params": "computer_id in path", + "purpose": "Soft-delete a computer (sets asset isactive=false), audit-logged", + "example": "curl -X DELETE -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/computers/42'" + }, + { + "method": "GET", + "path": "/api/computers//apps", + "auth": "jwt-optional", + "params": "computer_id in path; returns active installs only", + "purpose": "List installed applications on a computer", + "example": "curl 'http://localhost:5001/api/computers/42/apps'" + }, + { + "method": "POST", + "path": "/api/computers//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", + "example": "curl -X POST -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"appid\":17,\"appversionid\":3}' 'http://localhost:5001/api/computers/42/apps'" + }, + { + "method": "DELETE", + "path": "/api/computers//apps/", + "auth": "permission:computers.delete (jwt_required)", + "params": "computer_id and app_id in path", + "purpose": "Soft-remove an installed application (isactive=false)", + "example": "curl -X DELETE -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/computers/42/apps/17'" + }, + { + "method": "POST", + "path": "/api/computers//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", + "example": "curl -X POST -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"loggedinuser\":\"jsmith\",\"lastboottime\":\"2026-07-30T06:00:00\"}' 'http://localhost:5001/api/computers/42/report'" + }, + { + "method": "GET", + "path": "/api/computers/dashboard/summary", + "auth": "jwt-optional", + "params": "none", + "purpose": "Dashboard counts: total active computers, breakdown by type and OS, shopfloor vs non-shopfloor", + "example": "curl 'http://localhost:5001/api/computers/dashboard/summary'" + }, + { + "method": "GET", + "path": "/api/computers/dashboard/quiet", + "auth": "permission:computers.view", + "params": "none", + "purpose": "Dashboard card: PCs that WERE reporting, have stopped, and are still meant to be in use - a PC that never reported is not evidence of a fault", + "example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/dashboard/quiet" + }, + { + "method": "GET", + "path": "/api/computers/dashboard/sharedmachines", + "auth": "permission:computers.view", + "params": "none", + "purpose": "Dashboard card: machine numbers claimed by more than one PC, which is either a swap in progress or a mis-typed number", + "example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/dashboard/sharedmachines" + }, + { + "method": "PUT|PATCH", + "path": "/api/computers/protocols/", + "auth": "permission:computers.edit (jwt_required)", + "params": "protocol_id in path; body: any of protocolname, port, isactive", + "purpose": "Update one remote-access protocol in the catalog (VNC, WinRM, RDP) that PCs report against", + "example": "curl -X PATCH -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"port\":5900}' http://localhost:5001/api/computers/protocols/3" + } + ], + "surface": "plugin-computers" + }, + { + "surface": "plugin-measuringtools", + "endpoints": [ + { + "method": "GET", + "path": "/api/measuringtools/types", + "purpose": "List measuring-tool types (active-only by default), paginated, name-sorted.", + "auth": "jwt-optional", + "params": "query: active (default true; 'false' includes inactive), search (name ilike), page, perpage", + "example": "curl 'http://localhost:5001/api/measuringtools/types?search=caliper&page=1&perpage=25'" + }, + { + "method": "GET", + "path": "/api/measuringtools/types/", + "purpose": "Get one measuring-tool type by id (404 if missing).", + "auth": "jwt-optional", + "params": "path: type_id (int)", + "example": "curl 'http://localhost:5001/api/measuringtools/types/3'" + }, + { + "method": "POST", + "path": "/api/measuringtools/types", + "purpose": "Create a measuring-tool type; reactivates a soft-deleted same-named one, 409 if an active one exists.", + "auth": "jwt + permission:measuringtools.create", + "params": "body JSON: name (required), description, color", + "example": "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\"}'" + }, + { + "method": "PUT", + "path": "/api/measuringtools/types/", + "purpose": "Update a measuring-tool type; 409 on rename collision with an existing name.", + "auth": "jwt + permission:measuringtools.edit", + "params": "path: type_id; body JSON: name, description, color, isactive (only keys present are applied)", + "example": "curl -X PUT 'http://localhost:5001/api/measuringtools/types/3' -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"description\":\"Updated\",\"isactive\":true}'" + }, + { + "method": "DELETE", + "path": "/api/measuringtools/types/", + "purpose": "Hard-delete a measuring-tool type; refused with 409 if any tool still references it.", + "auth": "jwt + permission:measuringtools.delete", + "params": "path: type_id (int)", + "example": "curl -X DELETE 'http://localhost:5001/api/measuringtools/types/3' -H \"Authorization: Bearer $TOKEN\"" + }, + { + "method": "GET", + "path": "/api/measuringtools", + "purpose": "List measuring tools (Asset core merged with extension), filtered + paginated; derived calibrationstatus filter applied post-pagination.", + "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", + "example": "curl 'http://localhost:5001/api/measuringtools?typeid=2&calibrationstatus=overdue&page=1&perpage=50'" + }, + { + "method": "GET", + "path": "/api/measuringtools/", + "purpose": "Get one measuring tool by measuringtoolid, asset core dict with extension nested under 'measuringtool'.", + "auth": "jwt-optional", + "params": "path: tool_id (int)", + "example": "curl 'http://localhost:5001/api/measuringtools/17'" + }, + { + "method": "GET", + "path": "/api/measuringtools/by-asset/", + "purpose": "Get a measuring tool by its core assetid (404 if the asset has no extension row).", + "auth": "jwt-optional", + "params": "path: asset_id (int)", + "example": "curl 'http://localhost:5001/api/measuringtools/by-asset/1042'" + }, + { + "method": "POST", + "path": "/api/measuringtools", + "purpose": "Create a measuring tool: one Asset core row (assettype 'measuring_tool') plus one measuringtools extension row in a single payload; audit-logged.", + "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", + "example": "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\"}'" + }, + { + "method": "PUT", + "path": "/api/measuringtools/", + "purpose": "Update asset core fields and extension fields in one payload; 409 on assetnumber collision; changes audit-logged.", + "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)", + "example": "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\"}'" + }, + { + "method": "DELETE", + "path": "/api/measuringtools/", + "purpose": "Soft-delete a measuring tool by setting its asset isactive=false; audit-logged.", + "auth": "jwt + permission:measuringtools.delete", + "params": "path: tool_id (int)", + "example": "curl -X DELETE 'http://localhost:5001/api/measuringtools/17' -H \"Authorization: Bearer $TOKEN\"" + }, + { + "method": "GET", + "path": "/api/measuringtools/map-overlay", + "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.", + "auth": "jwt-optional", + "params": "none", + "example": "curl 'http://localhost:5001/api/measuringtools/map-overlay'" + }, + { + "method": "GET", + "path": "/api/measuringtools/report/calibration", + "purpose": "Calibration report for the Reports hub: counts and full tool lists bucketed by derived status (overdue/duesoon/current/unknown) plus statuscolors map.", + "auth": "jwt-optional", + "params": "none", + "example": "curl 'http://localhost:5001/api/measuringtools/report/calibration'" + } + ] + }, + { + "surface": "plugin-machines", + "endpoints": [ + { + "method": "GET", + "path": "/api/machines/types", + "purpose": "List machine types (active by default) with pagination and name search", + "auth": "jwt-optional", + "params": "query: page, per_page, active (pass 'false' to include inactive), search (ilike on machinetype)", + "example": "curl 'http://localhost:5001/api/machines/types?search=cnc&page=1&per_page=25'" + }, + { + "method": "GET", + "path": "/api/machines/types/", + "purpose": "Get a single machine type by ID", + "auth": "jwt-optional", + "params": "path: type_id (int)", + "example": "curl 'http://localhost:5001/api/machines/types/3'" + }, + { + "method": "POST", + "path": "/api/machines/types", + "purpose": "Create a machine type; reactivates a soft-deleted type of the same name instead of 409ing", + "auth": "jwt + permission:machines.create", + "params": "body: machinetype (required), description, icon, color", + "example": "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\"}'" + }, + { + "method": "PUT", + "path": "/api/machines/types/", + "purpose": "Update a machine type (rename guarded by 409 on duplicate name)", + "auth": "jwt + permission:machines.edit", + "params": "path: type_id; body: any of machinetype, description, icon, color, isactive", + "example": "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}'" + }, + { + "method": "DELETE", + "path": "/api/machines/types/", + "purpose": "Hard-delete a machine type; refused with 409 if any machine still references it", + "auth": "jwt + permission:machines.delete", + "params": "path: type_id (int)", + "example": "curl -X DELETE 'http://localhost:5001/api/machines/types/3' -H \"Authorization: Bearer $TOKEN\"" + }, + { + "method": "GET", + "path": "/api/machines", + "purpose": "List machines (Asset+Machine join) with filters, sorting, pagination; collapses Dualpath dual-bay pairs to one row (annotated with dualpathpartner) when site setting enabled", + "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)", + "example": "curl 'http://localhost:5001/api/machines?search=2007&typeid=2&sort=name&dir=desc&page=1&per_page=50'" + }, + { + "method": "GET", + "path": "/api/machines/", + "purpose": "Get one machine with full asset details, nested machine dict, and dualpathpartner info", + "auth": "jwt-optional", + "params": "path: machine_id (int)", + "example": "curl 'http://localhost:5001/api/machines/42'" + }, + { + "method": "GET", + "path": "/api/machines/by-asset/", + "purpose": "Get machine data looked up by core asset ID instead of machine ID", + "auth": "jwt-optional", + "params": "path: asset_id (int)", + "example": "curl 'http://localhost:5001/api/machines/by-asset/1001'" + }, + { + "method": "POST", + "path": "/api/machines", + "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", + "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", + "example": "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}'" + }, + { + "method": "PUT", + "path": "/api/machines/", + "purpose": "Update machine (asset + machine fields), with per-field change tracking to AuditLog and 409 on assetnumber conflict", + "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", + "example": "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\"}'" + }, + { + "method": "DELETE", + "path": "/api/machines/", + "purpose": "Soft-delete a machine (sets asset.isactive=False, keeps Machine row linked); audit-logged", + "auth": "jwt + permission:machines.delete", + "params": "path: machine_id (int)", + "example": "curl -X DELETE 'http://localhost:5001/api/machines/42' -H \"Authorization: Bearer $TOKEN\"" + }, + { + "method": "GET", + "path": "/api/machines/dashboard/summary", + "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 collapse setting enabled; by-status does not)", + "auth": "jwt-optional", + "params": "none", + "example": "curl 'http://localhost:5001/api/machines/dashboard/summary'" + }, + { + "method": "GET", + "path": "/api/machines/dashboard/outofservice", + "auth": "permission:machines.view", + "params": "none", + "purpose": "Dashboard card: machines whose status says they are not running", + "example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/machines/dashboard/outofservice" + } + ] + }, + { + "surface": "plugin-geenforce", + "endpoints": [ + { + "method": "GET", + "path": "/api/geenforce/manifest", + "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.", + "auth": "api-key (managed service token with geenforce.fetch scope via X-API-Key or Bearer PAT) OR source IP in geenforce_allowed_cidrs setting; resource-bound tokens restricted to their listed scopes (403 otherwise)", + "params": "query: pctype (required, =scopename), phase (default 'runtime'); header: If-None-Match for 304", + "example": "curl -H \"X-API-Key: $TOKEN\" 'http://localhost:5001/api/geenforce/manifest?pctype=cmm&phase=runtime'" + }, + { + "method": "GET", + "path": "/api/geenforce/payload/", + "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).", + "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)", + "example": "curl -H \"X-API-Key: $TOKEN\" -o installer.exe 'http://localhost:5001/api/geenforce/payload/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'" + }, + { + "method": "POST", + "path": "/api/geenforce/report", + "purpose": "Record one PC's enforcement cycle: applied manifest version plus per-entry self-heal outcomes (installed/skipped/failed); returns reportid + status.", + "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", + "example": "curl -X POST -H \"X-API-Key: $TOKEN\" -H 'Content-Type: application/json' -d '{\"hostname\":\"WKSTN0042\",\"scopename\":\"cmm\",\"appliedversion\":4,\"results\":[{\"entryname\":\"7zip\",\"action\":\"installed\"}]}' http://localhost:5001/api/geenforce/report" + }, + { + "method": "GET", + "path": "/api/geenforce/scopes", + "purpose": "List all imaging PC-type scopes with entry counts and current published version numbers, ordered by phase then scopename.", + "auth": "jwt + permission:geenforce.manage", + "params": "none", + "example": "curl -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/scopes" + }, + { + "method": "POST", + "path": "/api/geenforce/scopes", + "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.", + "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')", + "example": "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" + }, + { + "method": "GET", + "path": "/api/geenforce/scopes/", + "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).", + "auth": "jwt + permission:geenforce.manage", + "params": "path: scopeid", + "example": "curl -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/scopes/3" + }, + { + "method": "PUT", + "path": "/api/geenforce/scopes/", + "purpose": "Update scope metadata fields (only keys present in the body are changed); scopename and phase are immutable here.", + "auth": "jwt + permission:geenforce.manage", + "params": "JSON body (all optional): description, computertypeid, measuringtooltypeid, manifestversion (stringified), iscommon (bool-coerced)", + "example": "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" + }, + { + "method": "DELETE", + "path": "/api/geenforce/scopes/", + "purpose": "Delete a scope (and via cascade its entries); returns {deleted: scopeid}.", + "auth": "jwt + permission:geenforce.manage", + "params": "path: scopeid", + "example": "curl -X DELETE -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/scopes/3" + }, + { + "method": "GET", + "path": "/api/geenforce/scopes//preview", + "purpose": "Render the DRAFT manifest JSON that a publish would freeze, for admin review before shipping.", + "auth": "jwt + permission:geenforce.manage", + "params": "path: scopeid", + "example": "curl -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/scopes/3/preview" + }, + { + "method": "GET", + "path": "/api/geenforce/applications", + "purpose": "List the core active Applications catalog (appid + appname) for the curated entry-to-app link picker in the entry editor.", + "auth": "jwt + permission:geenforce.manage", + "params": "none", + "example": "curl -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/applications" + }, + { + "method": "POST", + "path": "/api/geenforce/scopes//entries", + "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.", + "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.)", + "example": "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" + }, + { + "method": "PUT", + "path": "/api/geenforce/entries/", + "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.", + "auth": "jwt + permission:geenforce.manage", + "params": "path: entryid; JSON body: Name (required), Type (required), optional appid, plus manifest fields", + "example": "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" + }, + { + "method": "DELETE", + "path": "/api/geenforce/entries/", + "purpose": "Delete a manifest entry; returns {deleted: entryid}.", + "auth": "jwt + permission:geenforce.manage", + "params": "path: entryid", + "example": "curl -X DELETE -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/entries/17" + }, + { + "method": "PUT", + "path": "/api/geenforce/scopes//entries/reorder", + "purpose": "Set entry ordering from an entryid list; 400 unless the list is exactly the set of this scope's entry ids.", + "auth": "jwt + permission:geenforce.manage", + "params": "path: scopeid; JSON body: order = [entryid, ...] (must match the scope's entry ids exactly)", + "example": "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" + }, + { + "method": "GET", + "path": "/api/geenforce/scopes//simulate", + "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.", + "auth": "jwt + permission:geenforce.manage", + "params": "path: scopeid; query (all optional): pctype (defaults to scopename), subtype, hostname, machinenumber, cmmversion; phase comes from the scope", + "example": "curl -H \"Authorization: Bearer $JWT\" 'http://localhost:5001/api/geenforce/scopes/3/simulate?hostname=WKSTN0042&cmmversion=2023.2'" + }, + { + "method": "GET", + "path": "/api/geenforce/scopes//compliance", + "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).", + "auth": "jwt + permission:geenforce.manage", + "params": "path: scopeid", + "example": "curl -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/scopes/3/compliance" + }, + { + "method": "POST", + "path": "/api/geenforce/entries//payload", + "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.", + "auth": "jwt + permission:geenforce.publish", + "params": "path: entryid; multipart/form-data: file (required)", + "example": "curl -X POST -H \"Authorization: Bearer $JWT\" -F 'file=@fix.ps1' http://localhost:5001/api/geenforce/entries/17/payload" + }, + { + "method": "GET", + "path": "/api/geenforce/entries//payload", + "purpose": "Download the stored inline payload bytes for an entry as an attachment (404 if the entry has no payload).", + "auth": "jwt + permission:geenforce.manage", + "params": "path: entryid", + "example": "curl -H \"Authorization: Bearer $JWT\" -o fix.ps1 http://localhost:5001/api/geenforce/entries/17/payload" + }, + { + "method": "POST", + "path": "/api/geenforce/scopes//publish", + "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.", + "auth": "jwt + permission:geenforce.publish", + "params": "path: scopeid; JSON body (optional): notes", + "example": "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" + }, + { + "method": "GET", + "path": "/api/geenforce/scopes//versions", + "purpose": "List published versions for a scope, newest first (versionnumber, iscurrent, publishedat, publishedby, notes).", + "auth": "jwt + permission:geenforce.manage", + "params": "path: scopeid", + "example": "curl -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/scopes/3/versions" + }, + { + "method": "GET", + "path": "/api/geenforce/scopes//versions/", + "purpose": "Fetch one published version's frozen manifest JSON (parsed and returned in the success envelope).", + "auth": "jwt + permission:geenforce.manage", + "params": "path: scopeid, versionnumber", + "example": "curl -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/scopes/3/versions/4" + }, + { + "method": "POST", + "path": "/api/geenforce/scopes//rollback", + "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.", + "auth": "jwt + permission:geenforce.publish", + "params": "path: scopeid; JSON body: versionnumber (required, int)", + "example": "curl -X POST -H \"Authorization: Bearer $JWT\" -H 'Content-Type: application/json' -d '{\"versionnumber\":3}' http://localhost:5001/api/geenforce/scopes/3/rollback" + }, + { + "method": "POST", + "path": "/api/geenforce/scopes//export-share", + "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.", + "auth": "jwt + permission:geenforce.publish", + "params": "path: scopeid; no body", + "example": "curl -X POST -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/scopes/3/export-share" + }, + { + "method": "GET", + "path": "/api/geenforce/config", + "purpose": "Read plugin config: the on-share export root (geenforce_share_root) and the client IP allowlist CIDRs (geenforce_allowed_cidrs).", + "auth": "jwt + permission:geenforce.manage", + "params": "none", + "example": "curl -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/config" + }, + { + "method": "PUT", + "path": "/api/geenforce/config", + "purpose": "Update plugin config settings; allowedcidrs is validated/normalized (comma/newline-separated CIDRs or bare IPs, 400 listing any bad entries) and only keys present in the body are written.", + "auth": "jwt + permission:geenforce.publish", + "params": "JSON body (both optional): shareroot (string path), allowedcidrs (CSV/newline CIDR list)", + "example": "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" + }, + { + "method": "GET", + "path": "/api/geenforce/reports", + "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.", + "auth": "jwt + permission:geenforce.manage", + "params": "query (optional): hostname (ILIKE match), scopename (exact)", + "example": "curl -H \"Authorization: Bearer $JWT\" 'http://localhost:5001/api/geenforce/reports?scopename=cmm'" + }, + { + "method": "GET", + "path": "/api/geenforce/reports/", + "purpose": "One enforcement report in detail with per-entry outcomes (entryname, action, selfhealed, exitcode, message) plus applied-vs-latest version comparison.", + "auth": "jwt + permission:geenforce.manage", + "params": "path: reportid", + "example": "curl -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/reports/42" + }, + { + "method": "GET", + "path": "/api/geenforce/dashboard/failures", + "auth": "permission:geenforce.manage", + "params": "none", + "purpose": "Dashboard card: entries that FAILED on their PC's most recent enforcement cycle, host and entry named", + "example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/geenforce/dashboard/failures" + }, + { + "method": "GET", + "path": "/api/geenforce/scopes//publish-preflight", + "auth": "permission:geenforce.manage", + "params": "scopeid in path", + "purpose": "Would publishing this scope outrun the fleet's enforcer library? Returns the required version, the lowest version reporting, the hosts behind it, and whether publish would be refused. The publish itself enforces the same gate; this is how the UI warns first", + "example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/geenforce/scopes/3/publish-preflight" + } + ] + }, + { + "surface": "plugin-notifications", + "endpoints": [ + { + "method": "GET", + "path": "/api/notifications/types", + "purpose": "List notification types, paginated, active-only by default", + "auth": "none", + "params": "page, per_page; active=false to include inactive types", + "example": "curl 'http://localhost:5001/api/notifications/types?active=false&page=1&per_page=50'" + }, + { + "method": "POST", + "path": "/api/notifications/types", + "purpose": "Create a notification type incl. expiry rule and shopfloor display config", + "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)", + "example": "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}'" + }, + { + "method": "PUT, PATCH", + "path": "/api/notifications/types/", + "purpose": "Update a notification type (name/desc/color/isactive plus expiry and display fields)", + "auth": "jwt + permission:notifications.create", + "params": "body: any of typename (unique-checked), typedescription/description, typecolor/color, isactive, expirymode, expirydays, expiryhour, expiryminute, splitperemployee, showemployeephoto, displaystyle", + "example": "curl -X PATCH http://localhost:5001/api/notifications/types/3 -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"expirymode\":\"duration\",\"expirydays\":14}'" + }, + { + "method": "GET", + "path": "/api/notifications", + "purpose": "List notifications with filters, newest-first, paginated", + "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)", + "example": "curl 'http://localhost:5001/api/notifications?current=true&typeid=2&search=outage&page=1'" + }, + { + "method": "GET", + "path": "/api/notifications/", + "purpose": "Get a single notification by ID", + "auth": "none", + "params": "path: notification_id", + "example": "curl http://localhost:5001/api/notifications/42" + }, + { + "method": "POST", + "path": "/api/notifications", + "purpose": "Create a notification; endtime auto-derived from type expiry rule when omitted", + "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", + "example": "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\"}'" + }, + { + "method": "PUT", + "path": "/api/notifications/", + "purpose": "Update any notification field; empty starttime resets to now, empty endtime clears it", + "auth": "jwt + permission:notifications.edit", + "params": "body: notification/message, notificationtypeid, businessunitid, appid, ticketnumber, link/linkurl, isactive, isshopfloor, employeesso, employeename, starttime/startdate, endtime/enddate", + "example": "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}'" + }, + { + "method": "DELETE", + "path": "/api/notifications/", + "purpose": "Soft-delete a notification (sets isactive=false, row kept)", + "auth": "jwt + permission:notifications.delete", + "params": "path: notification_id", + "example": "curl -X DELETE http://localhost:5001/api/notifications/42 -H \"Authorization: Bearer $TOKEN\"" + }, + { + "method": "GET", + "path": "/api/notifications/active", + "purpose": "Currently active notifications for display, incl. ones starting within a 10-day lookahead", + "auth": "none", + "params": "none", + "example": "curl http://localhost:5001/api/notifications/active" + }, + { + "method": "GET", + "path": "/api/notifications/calendar", + "purpose": "Active notifications as FullCalendar event objects for a date range", + "auth": "none", + "params": "start (ISO), end (ISO); invalid dates silently ignored", + "example": "curl 'http://localhost:5001/api/notifications/calendar?start=2026-07-01T00:00:00Z&end=2026-07-31T23:59:59Z'" + }, + { + "method": "GET", + "path": "/api/notifications/dashboard/summary", + "purpose": "Dashboard counts: total currently-active notifications plus active counts grouped by type/color", + "auth": "none", + "params": "none", + "example": "curl http://localhost:5001/api/notifications/dashboard/summary" + }, + { + "method": "GET", + "path": "/api/notifications/employee/", + "purpose": "All active recognition-type notifications mentioning an employee SSO (exact or within comma-separated employeesso list)", + "auth": "none", + "params": "path: sso (digits only, 400 otherwise)", + "example": "curl http://localhost:5001/api/notifications/employee/212345678" + }, + { + "method": "GET", + "path": "/api/notifications/shopfloor", + "purpose": "Shopfloor TV feed: current cards (active now, or ended <30 min ago flagged resolved) + 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", + "auth": "none", + "params": "businessunit (numeric BU id: returns that BU's plus null-BU notifications; omitted: null-BU only)", + "example": "curl 'http://localhost:5001/api/notifications/shopfloor?businessunit=3'" + }, + { + "method": "GET", + "path": "/api/notifications/dashboard/active", + "auth": "jwt", + "params": "none", + "purpose": "Dashboard card: the notifications currently showing on the shop-floor board, so the office can see what the floor sees", + "example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/notifications/dashboard/active" + } + ] + }, + { + "surface": "plugin-slides", + "endpoints": [ + { + "method": "GET", + "path": "/api/slides/feed", + "purpose": "Public flat playlist for a surface (lobby/shopfloor); raw jsonify, not the success_response envelope, so the screensaver parser works unchanged", + "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", + "example": "curl 'http://localhost:5001/api/slides/feed?surface=shopfloor'" + }, + { + "method": "GET", + "path": "/api/slides/img//", + "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", + "auth": "none", + "params": "path: surface (lobby|shopfloor), filename (must equal its basename)", + "example": "curl 'http://localhost:5001/api/slides/img/lobby/Slide1.png' -o Slide1.png" + }, + { + "method": "GET", + "path": "/api/slides/", + "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", + "auth": "jwt + permission:slides.manage", + "params": "path: surface (lobby|shopfloor; else VALIDATION_ERROR)", + "example": "curl -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/slides/lobby'" + }, + { + "method": "POST", + "path": "/api/slides//upload", + "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)", + "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]}", + "example": "curl -X POST -H \"Authorization: Bearer $TOKEN\" -F 'files=@Slide1.png' -F 'files=@Slide2.png' 'http://localhost:5001/api/slides/lobby/upload'" + }, + { + "method": "POST", + "path": "/api/slides//order", + "purpose": "Save play order: each filename in the order array gets sortorder set to its index; unknown filenames silently ignored", + "auth": "jwt + permission:slides.manage", + "params": "path: surface. JSON body: {order: [filename, ...]}", + "example": "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'" + }, + { + "method": "POST", + "path": "/api/slides//delete", + "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", + "auth": "jwt + permission:slides.manage", + "params": "path: surface. JSON body: {files: [filename, ...]}", + "example": "curl -X POST -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"files\":[\"Slide1.png\"]}' 'http://localhost:5001/api/slides/shopfloor/delete'" + }, + { + "method": "PATCH", + "path": "/api/slides//", + "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", + "auth": "jwt + permission:slides.manage", + "params": "path: surface, slideid (int). JSON body: {seconds: int} (non-int -> VALIDATION_ERROR)", + "example": "curl -X PATCH -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"seconds\":15}' 'http://localhost:5001/api/slides/lobby/12'" + } + ] + }, + { + "surface": "plugin-printedparts", + "endpoints": [ + { + "method": "GET", + "path": "/api/printedparts/items", + "purpose": "List printed items, paginated, with search and low-stock filter", + "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", + "example": "curl -H \"Authorization: Bearer $TOK\" 'http://localhost:5001/api/printedparts/items?search=bracket&lowstock=true&page=1&per_page=25'" + }, + { + "method": "GET", + "path": "/api/printedparts/items/", + "purpose": "Get one printed item plus its 25 most recent ledger transactions", + "auth": "jwt + permission:printedparts.view", + "params": "path: item_id", + "example": "curl -H \"Authorization: Bearer $TOK\" http://localhost:5001/api/printedparts/items/42" + }, + { + "method": "POST", + "path": "/api/printedparts/items", + "purpose": "Create a printed item; itemcode auto-minted from printedparts_code_prefix setting + row id; optional gagelabtag unique-checked (409 on clash)", + "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", + "example": "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" + }, + { + "method": "PUT", + "path": "/api/printedparts/items/", + "purpose": "Update catalog fields (itemname, itemdescription, lowstockthreshold, binlocation, printnotes, gagelabtag); rejects quantityonhand (ledger-managed) and duplicate gagelabtag (409)", + "auth": "jwt + permission:printedparts.edit", + "params": "path: item_id; json body: any of the editable fields; gagelabtag uppercased, empty string clears it", + "example": "curl -X PUT -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"binlocation\":\"C1\",\"lowstockthreshold\":8}' http://localhost:5001/api/printedparts/items/42" + }, + { + "method": "DELETE", + "path": "/api/printedparts/items/", + "purpose": "Soft-retire an item (isactive=false); ledger history preserved", + "auth": "jwt + permission:printedparts.delete", + "params": "path: item_id", + "example": "curl -X DELETE -H \"Authorization: Bearer $TOK\" http://localhost:5001/api/printedparts/items/42" + }, + { + "method": "POST", + "path": "/api/printedparts/items//restore", + "purpose": "Un-retire a soft-deleted item (isactive=true); code, photo, history intact", + "auth": "jwt + permission:printedparts.delete", + "params": "path: item_id", + "example": "curl -X POST -H \"Authorization: Bearer $TOK\" http://localhost:5001/api/printedparts/items/42/restore" + }, + { + "method": "POST", + "path": "/api/printedparts/items//image", + "purpose": "Upload or replace the item's photo (png/jpg/jpeg/gif/webp); old image files for the item are deleted first, imageurl updated", + "auth": "jwt + permission:printedparts.edit", + "params": "path: item_id; multipart/form-data: file=", + "example": "curl -X POST -H \"Authorization: Bearer $TOK\" -F 'file=@clip.jpg' http://localhost:5001/api/printedparts/items/42/image" + }, + { + "method": "GET", + "path": "/api/printedparts/image/", + "purpose": "Serve an uploaded item image from instance/printedpartsimages (public: fetched by tags on kiosk and lists)", + "auth": "none", + "params": "path: filename (e.g. printeditem-42.jpg)", + "example": "curl http://localhost:5001/api/printedparts/image/printeditem-42.jpg -o clip.jpg" + }, + { + "method": "DELETE", + "path": "/api/printedparts/items//image", + "purpose": "Clear the item's imageurl; deletes the file on disk only if the URL is plugin-owned (starts with /api/printedparts/image/)", + "auth": "jwt + permission:printedparts.delete", + "params": "path: item_id", + "example": "curl -X DELETE -H \"Authorization: Bearer $TOK\" http://localhost:5001/api/printedparts/items/42/image" + }, + { + "method": "POST", + "path": "/api/printedparts/items//restock", + "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", + "auth": "jwt + permission:printedparts.restock", + "params": "path: item_id; json body: quantity (positive int, required), badge (required, 422 BadgeError if unresolvable)", + "example": "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" + }, + { + "method": "POST", + "path": "/api/printedparts/items//adjust", + "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", + "auth": "jwt + permission:printedparts.restock", + "params": "path: item_id; json body: quantitychange (non-zero int, required), reason (required), badge (required)", + "example": "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" + }, + { + "method": "GET", + "path": "/api/printedparts/kiosk/item/", + "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", + "auth": "none (deliberately open per decision record; kiosk cannot carry JWT)", + "params": "path: itemcode (e.g. WJRP0042, 3DP0042, 42, or WJRP0042|3)", + "example": "curl http://localhost:5001/api/printedparts/kiosk/item/WJRP0042" + }, + { + "method": "POST", + "path": "/api/printedparts/kiosk/take", + "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", + "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)", + "example": "curl -X POST -H 'Content-Type: application/json' -d '{\"itemcode\":\"WJRP0042|3\",\"badge\":\"123456789\",\"quantity\":2}' http://localhost:5001/api/printedparts/kiosk/take" + }, + { + "method": "GET", + "path": "/api/printedparts/reports/stock", + "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)", + "auth": "jwt-optional (like all product reports)", + "params": "query: format=csv for CSV download (printedparts-stock.csv), else JSON {columns, rows}", + "example": "curl 'http://localhost:5001/api/printedparts/reports/stock?format=csv' -o stock.csv" + }, + { + "method": "GET", + "path": "/api/printedparts/reports/consumption", + "purpose": "Take-transactions aggregated per item (takes count + quantitytaken), sorted by quantity taken descending", + "auth": "jwt-optional", + "params": "query: days (default 30; 0 or negative = all time), format=csv (printedparts-consumption.csv)", + "example": "curl 'http://localhost:5001/api/printedparts/reports/consumption?days=90'" + }, + { + "method": "GET", + "path": "/api/printedparts/reports/by-person", + "purpose": "Take-transactions grouped by employee SSO (takes count + quantitytaken), sorted by quantity taken descending", + "auth": "jwt-optional", + "params": "query: days (default 30; 0 or negative = all time), format=csv (printedparts-by-person.csv)", + "example": "curl 'http://localhost:5001/api/printedparts/reports/by-person?days=30&format=csv' -o by-person.csv" + }, + { + "method": "GET", + "path": "/api/printedparts/items//files", + "purpose": "List the item's print-file revision history, newest revision first", + "auth": "jwt + permission:printedparts.view", + "params": "path: item_id", + "example": "curl -H \"Authorization: Bearer $TOK\" http://localhost:5001/api/printedparts/items/42/files" + }, + { + "method": "POST", + "path": "/api/printedparts/items//files", + "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", + "auth": "jwt + permission:printedparts.edit", + "params": "path: item_id; multipart/form-data: file= (required), note= (optional)", + "example": "curl -X POST -H \"Authorization: Bearer $TOK\" -F 'file=@clip-v2.stl' -F 'note=thicker wall' http://localhost:5001/api/printedparts/items/42/files" + }, + { + "method": "GET", + "path": "/api/printedparts/files//download", + "purpose": "Download a print-file revision as an attachment under its original filename (jwt-optional so plain anchor downloads work)", + "auth": "jwt-optional", + "params": "path: file_id", + "example": "curl -OJ http://localhost:5001/api/printedparts/files/7/download" + }, + { + "method": "DELETE", + "path": "/api/printedparts/files/", + "purpose": "Delete a bad print-file revision (wrong file uploaded): removes the stored file and the DB record", + "auth": "jwt + permission:printedparts.delete", + "params": "path: file_id", + "example": "curl -X DELETE -H \"Authorization: Bearer $TOK\" http://localhost:5001/api/printedparts/files/7" + } + ] + }, + { + "endpoints": [ + { + "method": "GET", + "path": "/api/network/types", + "purpose": "List network device types, paginated", + "auth": "jwt-optional", + "params": "page, per_page, active (default true; 'false' includes inactive), search (ilike on networkdevicetype)", + "example": "curl 'http://localhost:5001/api/network/types?search=switch&per_page=50'" + }, + { + "method": "GET", + "path": "/api/network/types/", + "purpose": "Get one network device type by ID", + "auth": "jwt-optional", + "params": "path: type_id", + "example": "curl http://localhost:5001/api/network/types/3" + }, + { + "method": "POST", + "path": "/api/network/types", + "purpose": "Create a network device type (reactivates a soft-deleted duplicate instead of 409ing)", + "auth": "jwt + permission:network.create", + "params": "body: networkdevicetype (required), description, icon, color", + "example": "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\"}'" + }, + { + "method": "PUT", + "path": "/api/network/types/", + "purpose": "Update a network device type; 409 on name collision", + "auth": "jwt + permission:network.edit", + "params": "body: networkdevicetype, description, icon, color, isactive", + "example": "curl -X PUT http://localhost:5001/api/network/types/3 -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"description\":\"Access switches\"}'" + }, + { + "method": "DELETE", + "path": "/api/network/types/", + "purpose": "Hard-delete a device type; 409 if any device still references it", + "auth": "jwt + permission:network.delete", + "params": "path: type_id", + "example": "curl -X DELETE http://localhost:5001/api/network/types/3 -H \"Authorization: Bearer $TOKEN\"" + }, + { + "method": "GET", + "path": "/api/network", + "purpose": "List network devices (Asset joined with NetworkDevice extension + primary IP), filtered/sorted/paginated", + "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)", + "example": "curl 'http://localhost:5001/api/network?typeid=2&poe=true&sort=hostname&dir=asc'" + }, + { + "method": "GET", + "path": "/api/network/", + "purpose": "Get one network device (asset dict + networkdevice sub-object + primary ipaddress)", + "auth": "jwt-optional", + "params": "path: device_id (networkdeviceid)", + "example": "curl http://localhost:5001/api/network/17" + }, + { + "method": "GET", + "path": "/api/network/by-asset/", + "purpose": "Look up a network device by its core assetid", + "auth": "jwt-optional", + "params": "path: asset_id", + "example": "curl http://localhost:5001/api/network/by-asset/1042" + }, + { + "method": "GET", + "path": "/api/network/by-hostname/", + "purpose": "Look up a network device by exact hostname", + "auth": "jwt-optional", + "params": "path: hostname", + "example": "curl http://localhost:5001/api/network/by-hostname/wjf-sw-idf3-01" + }, + { + "method": "POST", + "path": "/api/network", + "purpose": "Create a network device (creates core Asset + NetworkDevice extension; upserts primary-IP Communication; audit-logged; honors X-Import-Mode legacy timestamps)", + "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", + "example": "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\"}'" + }, + { + "method": "PUT", + "path": "/api/network/", + "purpose": "Update asset + network-device fields; 409 on assetnumber/hostname conflicts; change-diff audit log; upserts primary IP when ipaddress present", + "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", + "example": "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\"}'" + }, + { + "method": "DELETE", + "path": "/api/network/", + "purpose": "Soft-delete a network device (sets asset.isactive=false; audit-logged)", + "auth": "jwt + permission:network.delete", + "params": "path: device_id", + "example": "curl -X DELETE http://localhost:5001/api/network/17 -H \"Authorization: Bearer $TOKEN\"" + }, + { + "method": "GET", + "path": "/api/network/dashboard/summary", + "purpose": "Dashboard counts: total active devices, by type, by vendor, PoE vs non-PoE", + "auth": "jwt-optional", + "params": "none", + "example": "curl http://localhost:5001/api/network/dashboard/summary" + }, + { + "method": "GET", + "path": "/api/network/vlans", + "purpose": "List VLANs, paginated, ordered by vlannumber", + "auth": "jwt-optional", + "params": "page, per_page, active (default true), search (name/description/vlannumber), type (exact vlantype)", + "example": "curl 'http://localhost:5001/api/network/vlans?search=voice'" + }, + { + "method": "GET", + "path": "/api/network/vlans/", + "purpose": "Get one VLAN including its active subnets", + "auth": "jwt-optional", + "params": "path: vlan_id", + "example": "curl http://localhost:5001/api/network/vlans/5" + }, + { + "method": "POST", + "path": "/api/network/vlans", + "purpose": "Create a VLAN; 409 on duplicate vlannumber; audit-logged", + "auth": "jwt + permission:network.create", + "params": "body: vlannumber (required), name (required), description, vlantype", + "example": "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\"}'" + }, + { + "method": "PUT", + "path": "/api/network/vlans/", + "purpose": "Update a VLAN; 409 on vlannumber conflict; change-diff audit log", + "auth": "jwt + permission:network.edit", + "params": "body: vlannumber, name, description, vlantype, isactive", + "example": "curl -X PUT http://localhost:5001/api/network/vlans/5 -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"description\":\"CNC cell VLAN\"}'" + }, + { + "method": "DELETE", + "path": "/api/network/vlans/", + "purpose": "Soft-delete a VLAN; 400 if it still has active subnets; audit-logged", + "auth": "jwt + permission:network.delete", + "params": "path: vlan_id", + "example": "curl -X DELETE http://localhost:5001/api/network/vlans/5 -H \"Authorization: Bearer $TOKEN\"" + }, + { + "method": "GET", + "path": "/api/network/subnets", + "purpose": "List subnets, paginated, ordered by cidr", + "auth": "jwt-optional", + "params": "page, per_page, active (default true), search (cidr/name/description), vlanid, locationid, type (exact subnettype)", + "example": "curl 'http://localhost:5001/api/network/subnets?vlanid=5'" + }, + { + "method": "GET", + "path": "/api/network/subnets/", + "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", + "auth": "jwt-optional", + "params": "path: subnet_id", + "example": "curl http://localhost:5001/api/network/subnets/2" + }, + { + "method": "POST", + "path": "/api/network/subnets", + "purpose": "Create a subnet; validates CIDR notation and vlanid existence; 409 on duplicate cidr; audit-logged", + "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", + "example": "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\"}'" + }, + { + "method": "PUT", + "path": "/api/network/subnets/", + "purpose": "Update a subnet; 409 on cidr conflict; change-diff audit log", + "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", + "example": "curl -X PUT http://localhost:5001/api/network/subnets/2 -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"dhcpenabled\":false}'" + }, + { + "method": "DELETE", + "path": "/api/network/subnets/", + "purpose": "Soft-delete a subnet (isactive=false); audit-logged", + "auth": "jwt + permission:network.delete", + "params": "path: subnet_id", + "example": "curl -X DELETE http://localhost:5001/api/network/subnets/2 -H \"Authorization: Bearer $TOKEN\"" + } + ], + "surface": "plugin-network" + }, + { + "surface": "plugin-usb", + "endpoints": [ + { + "method": "GET", + "path": "/api/usb", + "purpose": "List USB devices with checkout status (paginated); dual-mode: selfhosted tables or external cmmc_usb DB per usb_directory_mode setting", + "auth": "jwt-optional", + "params": "query: page, per_page, status=available|checkedout|retired, search (matches device_id or device_desc)", + "example": "curl 'http://localhost:5001/api/usb?status=available&search=kingston&page=1&per_page=25' -H \"Authorization: Bearer $JWT\"" + }, + { + "method": "GET", + "path": "/api/usb/", + "purpose": "Get one device plus its last 20 check-in/out log rows; 404 if unknown", + "auth": "jwt-optional", + "params": "path: device_id", + "example": "curl 'http://localhost:5001/api/usb/USB-0042' -H \"Authorization: Bearer $JWT\"" + }, + { + "method": "POST", + "path": "/api/usb", + "purpose": "Create a device (starts in checked-in status); 409 on duplicate device_id", + "auth": "jwt + permission:usb.create", + "params": "body JSON: device_id (required), device_desc, device_owner (badge), locker_location", + "example": "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\"}'" + }, + { + "method": "PUT", + "path": "/api/usb/", + "purpose": "Edit device fields (device_desc / device_owner / locker_location / status); 404 if unknown", + "auth": "jwt + permission:usb.edit", + "params": "path: device_id; body JSON: any of device_desc, device_owner, locker_location, status", + "example": "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\"}'" + }, + { + "method": "POST", + "path": "/api/usb//retire", + "purpose": "Retire a device (sets status to retired); 404 if unknown", + "auth": "jwt + permission:usb.edit", + "params": "path: device_id; no body", + "example": "curl -X POST 'http://localhost:5001/api/usb/USB-0042/retire' -H \"Authorization: Bearer $JWT\"" + }, + { + "method": "POST", + "path": "/api/usb//checkout", + "purpose": "Check a device out to a badge (writes check-out log row, sets status checked-out, auto-creates user from HR directory); 409 if already checked out", + "auth": "jwt + permission:usb.create", + "params": "path: device_id; body JSON: badge (required), locker_location (optional, also updates device)", + "example": "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\"}'" + }, + { + "method": "POST", + "path": "/api/usb//checkin", + "purpose": "Check a device back in (writes check-in log row with sanitized/virus-scan flags, sets status checked-in); 400 if not currently checked out", + "auth": "jwt + permission:usb.create", + "params": "path: device_id; body JSON: badge (required), locker_location, sanitized (bool/1/0), scanned_viruses (bool/1/0)", + "example": "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}'" + }, + { + "method": "GET", + "path": "/api/usb//history", + "purpose": "Paginated check-in/out log for one device, newest first", + "auth": "jwt-optional", + "params": "path: device_id; query: page, per_page", + "example": "curl 'http://localhost:5001/api/usb/USB-0042/history?page=1&per_page=50' -H \"Authorization: Bearer $JWT\"" + }, + { + "method": "GET", + "path": "/api/usb/checkouts", + "purpose": "List check-out log rows (paginated), each with the device's current status joined in", + "auth": "jwt-optional", + "params": "query: page, per_page, active=true (only rows whose device is still checked out), badge (filter by badge_number)", + "example": "curl 'http://localhost:5001/api/usb/checkouts?active=true&badge=212345678' -H \"Authorization: Bearer $JWT\"" + }, + { + "method": "GET", + "path": "/api/usb/checkouts/active", + "purpose": "Latest check-out log row for every currently checked-out device (non-paginated list)", + "auth": "jwt-optional", + "params": "query: badge (filter by badge_number)", + "example": "curl 'http://localhost:5001/api/usb/checkouts/active?badge=212345678' -H \"Authorization: Bearer $JWT\"" + } + ] + }, + { + "surface": "plugin-printers", + "endpoints": [ + { + "method": "GET", + "path": "/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.", + "example": "curl -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/printers/types?search=laser&active=false'" + }, + { + "method": "GET", + "path": "/api/printers/types/", + "auth": "jwt-optional", + "params": "path: type_id (int)", + "purpose": "Get a single printer type by ID.", + "example": "curl http://localhost:5001/api/printers/types/3" + }, + { + "method": "POST", + "path": "/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).", + "example": "curl -X POST -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"printertype\":\"Label\",\"color\":\"#00f\"}' http://localhost:5001/api/printers/types" + }, + { + "method": "PUT", + "path": "/api/printers/types/", + "auth": "permission:printers.edit", + "params": "body: printertype, description, icon, color, isactive (any subset); 409 on name clash", + "purpose": "Update a printer type.", + "example": "curl -X PUT -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"isactive\":false}' http://localhost:5001/api/printers/types/3" + }, + { + "method": "DELETE", + "path": "/api/printers/types/", + "auth": "permission:printers.delete", + "params": "path: type_id; 409 if any printer still references the type", + "purpose": "Hard-delete a printer type when unused.", + "example": "curl -X DELETE -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/printers/types/3" + }, + { + "method": "GET", + "path": "/api/printers/drivers", + "auth": "jwt-optional", + "params": "active (default true; 'false' includes inactive). Unpaginated.", + "purpose": "List printer driver packages (named SMB/HTTP links).", + "example": "curl 'http://localhost:5001/api/printers/drivers?active=false'" + }, + { + "method": "POST", + "path": "/api/printers/drivers", + "auth": "permission:printers.create", + "params": "body: name (required), location (required), description, modelnumberid, isactive", + "purpose": "Create a driver entry.", + "example": "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" + }, + { + "method": "PUT", + "path": "/api/printers/drivers/", + "auth": "permission:printers.edit", + "params": "body: name, location, description, isactive, modelnumberid (any subset)", + "purpose": "Update a driver entry.", + "example": "curl -X PUT -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"isactive\":false}' http://localhost:5001/api/printers/drivers/5" + }, + { + "method": "DELETE", + "path": "/api/printers/drivers/", + "auth": "permission:printers.delete", + "params": "path: driver_id", + "purpose": "Hard-delete a driver entry.", + "example": "curl -X DELETE -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/printers/drivers/5" + }, + { + "method": "GET", + "path": "/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.", + "example": "curl 'http://localhost:5001/api/printers?search=csf&typeid=2&sort=assetnumber&dir=desc'" + }, + { + "method": "GET", + "path": "/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.", + "example": "curl 'http://localhost:5001/api/printers/install-list?format=text'" + }, + { + "method": "GET", + "path": "/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).", + "example": "curl -OJ 'http://localhost:5001/api/printers/install-batch?printerids=1,2,3'" + }, + { + "method": "GET", + "path": "/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.", + "example": "curl 'http://localhost:5001/api/printers/pc-default?machine=0421&format=text'" + }, + { + "method": "GET", + "path": "/api/printers/", + "auth": "jwt-optional", + "params": "path: printer_id (int)", + "purpose": "Get one printer with full asset details, communications, and active drivers matching its model.", + "example": "curl http://localhost:5001/api/printers/17" + }, + { + "method": "GET", + "path": "/api/printers/by-asset/", + "auth": "jwt-optional", + "params": "path: asset_id (int)", + "purpose": "Get printer data keyed by core asset ID.", + "example": "curl http://localhost:5001/api/printers/by-asset/204" + }, + { + "method": "POST", + "path": "/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).", + "example": "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" + }, + { + "method": "PUT", + "path": "/api/printers/", + "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.", + "example": "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" + }, + { + "method": "DELETE", + "path": "/api/printers/", + "auth": "permission:printers.delete", + "params": "path: printer_id", + "purpose": "Soft-delete a printer (sets the underlying asset isactive=false).", + "example": "curl -X DELETE -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/printers/17" + }, + { + "method": "GET", + "path": "/api/printers//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.", + "example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/printers/17/supplies" + }, + { + "method": "GET", + "path": "/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.", + "example": "curl http://localhost:5001/api/printers/lowsupplies" + }, + { + "method": "GET", + "path": "/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).", + "example": "curl 'http://localhost:5001/api/printers/lookup?ip=10.1.2.42'" + }, + { + "method": "POST", + "path": "/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).", + "example": "curl -X POST -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/printers/supplies/refresh" + }, + { + "method": "GET", + "path": "/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).", + "example": "curl http://localhost:5001/api/printers/dashboard/summary" + }, + { + "method": "GET", + "path": "/api/printers/supplies/meta", + "auth": "jwt-optional", + "params": "none", + "purpose": "Allowed enum values for supplytype, color, and capacitytier (for the UI forms).", + "example": "curl http://localhost:5001/api/printers/supplies/meta" + }, + { + "method": "GET", + "path": "/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).", + "example": "curl 'http://localhost:5001/api/printers/models?withsupplies=true&search=M404'" + }, + { + "method": "GET", + "path": "/api/printers/models//supplies", + "auth": "jwt-optional", + "params": "path: modelnumberid (int)", + "purpose": "List all active supplies (toner/drum/waste part numbers) mapped to a model.", + "example": "curl http://localhost:5001/api/printers/models/12/supplies" + }, + { + "method": "POST", + "path": "/api/printers/models//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.", + "example": "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" + }, + { + "method": "PUT", + "path": "/api/printers/supplies/", + "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.", + "example": "curl -X PUT -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"capacitytier\":\"high\"}' http://localhost:5001/api/printers/supplies/44" + }, + { + "method": "DELETE", + "path": "/api/printers/supplies/", + "auth": "permission:printers.delete", + "params": "path: modelsupplyid", + "purpose": "Hard-delete a model supply mapping.", + "example": "curl -X DELETE -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/printers/supplies/44" + }, + { + "method": "GET", + "path": "/api/printers/supplies/forecast", + "auth": "jwt-optional", + "params": "days (1-365, default 90)", + "purpose": "One row per CARTRIDGE: days-to-empty from Zabbix history (hourly trends over a long window), soonest first, banded empty/soon/month/later, each with its part numbers and replacement count. Also returns orderlist - what to buy within horizondays, grouped by part number with a quantity. Cartridges with no honest estimate come back separately with the reason. available=false when Zabbix is unreachable", + "example": "curl 'http://localhost:5001/api/printers/supplies/forecast?days=90'" + }, + { + "method": "GET", + "path": "/api/printers/dashboard/supplies", + "auth": "permission:printers.view", + "params": "none", + "purpose": "Dashboard card: printers needing a cartridge, one row per printer. Reuses the low-supplies query and its five-minute cache, so the card costs the same as the report", + "example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/printers/dashboard/supplies" + } + ] + }, + { + "surface": "plugin-warranty", + "endpoints": [ + { + "method": "GET", + "path": "/api/warranty", + "purpose": "List warranties with linked-asset summaries and derived status (batch asset fetch, ordered by enddate with null last).", + "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)", + "example": "curl -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/warranty?status=expiring&assetid=42'" + }, + { + "method": "GET", + "path": "/api/warranty/asset/", + "purpose": "Active warranties covering one asset, for the asset-detail panel.", + "auth": "jwt-optional", + "params": "path: assetid", + "example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/warranty/asset/42" + }, + { + "method": "GET", + "path": "/api/warranty/", + "purpose": "Fetch a single warranty by id with asset summaries; 404 if missing.", + "auth": "jwt-optional", + "params": "path: warrantyid", + "example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/warranty/7" + }, + { + "method": "POST", + "path": "/api/warranty", + "purpose": "Create a warranty (vendor required) and optionally link it to assets; returns 201.", + "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)", + "example": "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" + }, + { + "method": "PUT", + "path": "/api/warranty/", + "purpose": "Partial update of any warranty field (only keys present in body change), including isactive and replacing asset links via assetids.", + "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)", + "example": "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" + }, + { + "method": "DELETE", + "path": "/api/warranty/", + "purpose": "Hard-delete a warranty (and its asset links); 404 if missing.", + "auth": "jwt + permission:warranty.delete", + "params": "path: warrantyid", + "example": "curl -X DELETE -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/warranty/7" + }, + { + "method": "POST", + "path": "/api/warranty//refresh", + "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.", + "auth": "jwt + permission:warranty.edit", + "params": "path: warrantyid; no body", + "example": "curl -X POST -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/warranty/7/refresh" + }, + { + "method": "POST", + "path": "/api/warranty/sync/dell", + "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.", + "auth": "jwt + permission:warranty.edit", + "params": "query: all (default false; 'true' re-checks assets that already have a dated warranty); no body", + "example": "curl -X POST -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/warranty/sync/dell?all=true'" + }, + { + "method": "GET", + "path": "/api/warranty/report", + "purpose": "Report for the Reports hub: active warranties bucketed by derived status (expired/expiring/active/unknown) with per-bucket counts and full lists.", + "auth": "jwt-optional", + "params": "none", + "example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/warranty/report" + }, + { + "method": "POST", + "path": "/api/warranty//proof", + "auth": "jwt + warranty.edit", + "params": "multipart/form-data: file= (pdf/png/jpg/gif/webp/tif/msg/eml/doc/docx/xls/xlsx), max 25MB", + "purpose": "Upload or replace the proof-of-cover document (invoice, certificate)", + "example": "curl -X POST -H 'Authorization: Bearer ' -F file=@invoice.pdf http://localhost:5001/api/warranty/12/proof" + }, + { + "method": "GET", + "path": "/api/warranty/proof/", + "auth": "jwt + warranty.view", + "params": "none", + "purpose": "Download a proof document under the vendor's original filename. Authenticated: carries pricing and a service tag", + "example": "curl -OJ -H 'Authorization: Bearer ' http://localhost:5001/api/warranty/proof/warranty-12.pdf" + }, + { + "method": "DELETE", + "path": "/api/warranty//proof", + "auth": "jwt + warranty.edit", + "params": "none", + "purpose": "Remove a proof document and clear both columns", + "example": "curl -X DELETE -H 'Authorization: Bearer ' http://localhost:5001/api/warranty/12/proof" + }, + { + "method": "GET", + "path": "/api/warranty/dashboard/expiring", + "auth": "permission:warranty.view", + "params": "none", + "purpose": "Dashboard card: warranties running out and ones that already have, identified the way the floor identifies them - by the PC's hostname and the machine it drives", + "example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/warranty/dashboard/expiring" + } + ] + }, + { + "surface": "plugin-backups", + "endpoints": [ + { + "method": "GET", + "path": "/api/backups/kinds", + "auth": "permission:backups.view", + "params": "none", + "purpose": "Registered backup kinds (NTLARS/DNC, part marker, UDC, CMM ...) with each kind's storage backend and download formats, for UI that enumerates them rather than hardcoding the list", + "example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/backups/kinds" + }, + { + "method": "GET", + "path": "/api/backups/asset/", + "auth": "permission:backups.view", + "params": "assetid in path; optional ?kind= to filter to one backup kind", + "purpose": "Revision history for one asset, newest first. An unchanged configuration writes no revision, so the newest row may be old and still current - each row carries both when it was captured and when it was last confirmed", + "example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/backups/asset/42" + }, + { + "method": "GET", + "path": "/api/backups/asset//info", + "auth": "permission:backups.view", + "params": "assetid in path; ?kind= selects the kind whose card is wanted", + "purpose": "A kind's at-a-glance card built from its latest revision, for the asset detail page", + "example": "curl -H \"Authorization: Bearer $TOKEN\" \"http://localhost:5001/api/backups/asset/42/info?kind=ntlars\"" + }, + { + "method": "GET", + "path": "/api/backups/revisions/", + "auth": "permission:backups.view", + "params": "backuprevisionid in path", + "purpose": "One revision, including its parsed payload where the kind stores one (opaque kinds return metadata only)", + "example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/backups/revisions/1188" + }, + { + "method": "GET", + "path": "/api/backups/revisions//diff", + "auth": "permission:backups.view", + "params": "backuprevisionid in path; ?against= to compare a specific pair, otherwise the previous revision of the same asset and kind", + "purpose": "Diff two revisions of the same asset and kind, key by key, which is what answers \"what changed on this machine\"", + "example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/backups/revisions/1188/diff" + }, + { + "method": "GET", + "path": "/api/backups/revisions//download", + "auth": "permission:backups.download", + "params": "backuprevisionid in path; ?format= one of the kind's formats (see /api/backups/kinds)", + "purpose": "Render a revision back to its native file format - for NTLARS, a .reg in either registry dialect, so it can be loaded straight back onto a controller PC. Returns the file, NOT the success envelope", + "example": "curl -H \"Authorization: Bearer $TOKEN\" -OJ \"http://localhost:5001/api/backups/revisions/1188/download?format=ntlars\"" + }, + { + "method": "GET", + "path": "/api/backups/dashboard/stale", + "auth": "permission:backups.view", + "params": "none; the threshold is the backups_staledays setting, 0 disables the card", + "purpose": "Dashboard card (contract 0.19.0): chains whose backup has stopped running, judged on when a config was last CONFIRMED rather than last changed", + "example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/backups/dashboard/stale" + } + ] + }, + { + "surface": "core-docs", + "endpoints": [ + { + "method": "GET", + "path": "/api/docs/", + "auth": "none", + "params": "none", + "purpose": "Redoc page rendering the generated spec. Public, and asset URLs are built with url_for so it works under any mount path", + "example": "curl http://localhost:5001/api/docs/" + }, + { + "method": "GET", + "path": "/api/docs/openapi.json", + "auth": "none", + "params": "none", + "purpose": "The generated OpenAPI 3.1 spec - the machine-readable description of this API, and what MCP and other clients build from. Returns the spec, NOT the success envelope", + "example": "curl http://localhost:5001/api/docs/openapi.json" + }, + { + "method": "GET", + "path": "/api/docs/llms.txt", + "auth": "none", + "params": "none", + "purpose": "Concise orientation guide for an LLM or a new integrator: auth schemes, the response envelope, and the common recipes. Returns text/plain", + "example": "curl http://localhost:5001/api/docs/llms.txt" + }, + { + "method": "GET", + "path": "/api/docs/redoc.standalone.js", + "auth": "none", + "params": "none", + "purpose": "Vendored Redoc bundle, served locally so the documentation page works on an air-gapped server", + "example": "curl -I http://localhost:5001/api/docs/redoc.standalone.js" + } + ] + } +] diff --git a/docs/openapi.json b/docs/openapi.json index 7d3ae5f..0796684 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -167,6 +167,12 @@ }, { "name": "plugin-warranty" + }, + { + "name": "plugin-backups" + }, + { + "name": "core-docs" } ], "paths": { @@ -3749,6 +3755,57 @@ ] } }, + "/api/employees/resolve/{sso}": { + "get": { + "tags": [ + "plugin-employees" + ], + "summary": "Resolve one SSO to a name and photo, and say HOW it resolved or why it did not. Built because the failure is otherwise...", + "description": "Resolve one SSO to a name and photo, and say HOW it resolved or why it did not. Built because the failure is otherwise invisible: the shop-floor board resolves names through functions that returned None on any miss, so a directory problem and a typo looked identical\n\n**Auth:** jwt\n\n**Params:** sso in path\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/employees/resolve/123456789\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "sso", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, "/api/knowledgebase": { "get": { "tags": [ @@ -8960,6 +9017,128 @@ } }, "/api/computers/protocols/{protocol_id}": { + "put": { + "tags": [ + "plugin-computers" + ], + "summary": "Update one remote-access protocol in the catalog (VNC, WinRM, RDP) that PCs report against", + "description": "Update one remote-access protocol in the catalog (VNC, WinRM, RDP) that PCs report against\n\n**Auth:** permission:computers.edit (jwt_required)\n\n**Params:** protocol_id in path; body: any of protocolname, port, isactive\n\n**Example:**\n```\ncurl -X PATCH -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"port\":5900}' http://localhost:5001/api/computers/protocols/3\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "protocol_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "protocol_id in path; body: any of protocolname, port, isactive" + } + } + } + } + }, + "patch": { + "tags": [ + "plugin-computers" + ], + "summary": "Update one remote-access protocol in the catalog (VNC, WinRM, RDP) that PCs report against", + "description": "Update one remote-access protocol in the catalog (VNC, WinRM, RDP) that PCs report against\n\n**Auth:** permission:computers.edit (jwt_required)\n\n**Params:** protocol_id in path; body: any of protocolname, port, isactive\n\n**Example:**\n```\ncurl -X PATCH -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"port\":5900}' http://localhost:5001/api/computers/protocols/3\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "protocol_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "protocol_id in path; body: any of protocolname, port, isactive" + } + } + } + } + }, "delete": { "tags": [ "plugin-computers" @@ -9634,6 +9813,82 @@ } } }, + "/api/computers/dashboard/quiet": { + "get": { + "tags": [ + "plugin-computers" + ], + "summary": "Dashboard card: PCs that WERE reporting, have stopped, and are still meant to be in use - a PC that never reported is...", + "description": "Dashboard card: PCs that WERE reporting, have stopped, and are still meant to be in use - a PC that never reported is not evidence of a fault\n\n**Auth:** permission:computers.view\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/dashboard/quiet\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/computers/dashboard/sharedmachines": { + "get": { + "tags": [ + "plugin-computers" + ], + "summary": "Dashboard card: machine numbers claimed by more than one PC, which is either a swap in progress or a mis-typed number", + "description": "Dashboard card: machine numbers claimed by more than one PC, which is either a swap in progress or a mis-typed number\n\n**Auth:** permission:computers.view\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/dashboard/sharedmachines\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, "/api/measuringtools/types": { "get": { "tags": [ @@ -10773,6 +11028,44 @@ } } }, + "/api/machines/dashboard/outofservice": { + "get": { + "tags": [ + "plugin-machines" + ], + "summary": "Dashboard card: machines whose status says they are not running", + "description": "Dashboard card: machines whose status says they are not running\n\n**Auth:** permission:machines.view\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/machines/dashboard/outofservice\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, "/api/geenforce/manifest": { "get": { "tags": [ @@ -12174,6 +12467,95 @@ ] } }, + "/api/geenforce/dashboard/failures": { + "get": { + "tags": [ + "plugin-geenforce" + ], + "summary": "Dashboard card: entries that FAILED on their PC's most recent enforcement cycle, host and entry named", + "description": "Dashboard card: entries that FAILED on their PC's most recent enforcement cycle, host and entry named\n\n**Auth:** permission:geenforce.manage\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/geenforce/dashboard/failures\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/geenforce/scopes/{scopeid}/publish-preflight": { + "get": { + "tags": [ + "plugin-geenforce" + ], + "summary": "Would publishing this scope outrun the fleet's enforcer library? Returns the required version, the lowest version...", + "description": "Would publishing this scope outrun the fleet's enforcer library? Returns the required version, the lowest version reporting, the hosts behind it, and whether publish would be refused. The publish itself enforces the same gate; this is how the UI warns first\n\n**Auth:** permission:geenforce.manage\n\n**Params:** scopeid in path\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/geenforce/scopes/3/publish-preflight\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "scopeid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, "/api/notifications/types": { "get": { "tags": [ @@ -12754,6 +13136,44 @@ } } }, + "/api/notifications/dashboard/active": { + "get": { + "tags": [ + "plugin-notifications" + ], + "summary": "Dashboard card: the notifications currently showing on the shop-floor board, so the office can see what the floor sees", + "description": "Dashboard card: the notifications currently showing on the shop-floor board, so the office can see what the floor sees\n\n**Auth:** jwt\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/notifications/dashboard/active\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, "/api/slides/feed": { "get": { "tags": [ @@ -17009,6 +17429,44 @@ } } }, + "/api/printers/dashboard/supplies": { + "get": { + "tags": [ + "plugin-printers" + ], + "summary": "Dashboard card: printers needing a cartridge, one row per printer. Reuses the low-supplies query and its five-minute...", + "description": "Dashboard card: printers needing a cartridge, one row per printer. Reuses the low-supplies query and its five-minute cache, so the card costs the same as the report\n\n**Auth:** permission:printers.view\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/printers/dashboard/supplies\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, "/api/warranty": { "get": { "tags": [ @@ -17600,6 +18058,487 @@ } ] } + }, + "/api/warranty/dashboard/expiring": { + "get": { + "tags": [ + "plugin-warranty" + ], + "summary": "Dashboard card: warranties running out and ones that already have, identified the way the floor identifies them - by...", + "description": "Dashboard card: warranties running out and ones that already have, identified the way the floor identifies them - by the PC's hostname and the machine it drives\n\n**Auth:** permission:warranty.view\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/warranty/dashboard/expiring\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/backups/kinds": { + "get": { + "tags": [ + "plugin-backups" + ], + "summary": "Registered backup kinds (NTLARS/DNC, part marker, UDC, CMM ...) with each kind's storage backend and download formats...", + "description": "Registered backup kinds (NTLARS/DNC, part marker, UDC, CMM ...) with each kind's storage backend and download formats, for UI that enumerates them rather than hardcoding the list\n\n**Auth:** permission:backups.view\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/backups/kinds\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/backups/asset/{assetid}": { + "get": { + "tags": [ + "plugin-backups" + ], + "summary": "Revision history for one asset, newest first. An unchanged configuration writes no revision, so the newest row may be...", + "description": "Revision history for one asset, newest first. An unchanged configuration writes no revision, so the newest row may be old and still current - each row carries both when it was captured and when it was last confirmed\n\n**Auth:** permission:backups.view\n\n**Params:** assetid in path; optional ?kind= to filter to one backup kind\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/backups/asset/42\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "assetid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/backups/asset/{assetid}/info": { + "get": { + "tags": [ + "plugin-backups" + ], + "summary": "A kind's at-a-glance card built from its latest revision, for the asset detail page", + "description": "A kind's at-a-glance card built from its latest revision, for the asset detail page\n\n**Auth:** permission:backups.view\n\n**Params:** assetid in path; ?kind= selects the kind whose card is wanted\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" \"http://localhost:5001/api/backups/asset/42/info?kind=ntlars\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "assetid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/backups/revisions/{backuprevisionid}": { + "get": { + "tags": [ + "plugin-backups" + ], + "summary": "One revision, including its parsed payload where the kind stores one (opaque kinds return metadata only)", + "description": "One revision, including its parsed payload where the kind stores one (opaque kinds return metadata only)\n\n**Auth:** permission:backups.view\n\n**Params:** backuprevisionid in path\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/backups/revisions/1188\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "backuprevisionid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/backups/revisions/{backuprevisionid}/diff": { + "get": { + "tags": [ + "plugin-backups" + ], + "summary": "Diff two revisions of the same asset and kind, key by key, which is what answers \"what changed on this machine\"", + "description": "Diff two revisions of the same asset and kind, key by key, which is what answers \"what changed on this machine\"\n\n**Auth:** permission:backups.view\n\n**Params:** backuprevisionid in path; ?against= to compare a specific pair, otherwise the previous revision of the same asset and kind\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/backups/revisions/1188/diff\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "backuprevisionid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/backups/revisions/{backuprevisionid}/download": { + "get": { + "tags": [ + "plugin-backups" + ], + "summary": "Render a revision back to its native file format - for NTLARS, a .reg in either registry dialect, so it can be loaded...", + "description": "Render a revision back to its native file format - for NTLARS, a .reg in either registry dialect, so it can be loaded straight back onto a controller PC. Returns the file, NOT the success envelope\n\n**Auth:** permission:backups.download\n\n**Params:** backuprevisionid in path; ?format= one of the kind's formats (see /api/backups/kinds)\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" -OJ \"http://localhost:5001/api/backups/revisions/1188/download?format=ntlars\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "backuprevisionid", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/backups/dashboard/stale": { + "get": { + "tags": [ + "plugin-backups" + ], + "summary": "Dashboard card (contract 0.19.0): chains whose backup has stopped running, judged on when a config was last CONFIRMED...", + "description": "Dashboard card (contract 0.19.0): chains whose backup has stopped running, judged on when a config was last CONFIRMED rather than last changed\n\n**Auth:** permission:backups.view\n\n**Params:** none; the threshold is the backups_staledays setting, 0 disables the card\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/backups/dashboard/stale\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/docs/": { + "get": { + "tags": [ + "core-docs" + ], + "summary": "Redoc page rendering the generated spec. Public, and asset URLs are built with url_for so it works under any mount path", + "description": "Redoc page rendering the generated spec. Public, and asset URLs are built with url_for so it works under any mount path\n\n**Auth:** none\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/docs/\n```", + "security": [], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + }, + "/api/docs/openapi.json": { + "get": { + "tags": [ + "core-docs" + ], + "summary": "The generated OpenAPI 3.1 spec - the machine-readable description of this API, and what MCP and other clients build...", + "description": "The generated OpenAPI 3.1 spec - the machine-readable description of this API, and what MCP and other clients build from. Returns the spec, NOT the success envelope\n\n**Auth:** none\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/docs/openapi.json\n```", + "security": [], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + }, + "/api/docs/llms.txt": { + "get": { + "tags": [ + "core-docs" + ], + "summary": "Concise orientation guide for an LLM or a new integrator: auth schemes, the response envelope, and the common recipes...", + "description": "Concise orientation guide for an LLM or a new integrator: auth schemes, the response envelope, and the common recipes. Returns text/plain\n\n**Auth:** none\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/docs/llms.txt\n```", + "security": [], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + }, + "/api/docs/redoc.standalone.js": { + "get": { + "tags": [ + "core-docs" + ], + "summary": "Vendored Redoc bundle, served locally so the documentation page works on an air-gapped server", + "description": "Vendored Redoc bundle, served locally so the documentation page works on an air-gapped server\n\n**Auth:** none\n\n**Params:** none\n\n**Example:**\n```\ncurl -I http://localhost:5001/api/docs/redoc.standalone.js\n```", + "security": [], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } } } } \ No newline at end of file diff --git a/scripts/gen_openapi.py b/scripts/gen_openapi.py index 54da427..d86ffa0 100644 --- a/scripts/gen_openapi.py +++ b/scripts/gen_openapi.py @@ -1,9 +1,14 @@ """Generate docs/openapi.json from docs/api-inventory.json. The inventory is a list of {surface, endpoints:[{method,path,auth,params,purpose, -example}]} objects (one per API surface). Re-run after adding/changing endpoints -(update api-inventory.json first, or regenerate it). Served interactively at -/api/docs (see shopdb/core/api/docs.py). +example}]} objects (one per API surface). It is HAND-MAINTAINED - nothing +generates it, and the docstring here used to imply otherwise. That is deliberate: +its value is the prose, and what an endpoint is FOR cannot be derived from the +code. Coverage is enforced instead, by tests/test_api_inventory_parity.py, which +fails when a served route has no entry. + +So: describe the endpoint in api-inventory.json, then re-run this. Served +interactively at /api/docs (see shopdb/core/api/docs.py). venv/bin/python scripts/gen_openapi.py """ @@ -179,7 +184,10 @@ def build(surfaces): for s in surfaces: tags.append({'name': s['surface']}) for e in s.get('endpoints', []): - for verb in re.split(r'[\/,]', (e.get('method') or 'GET')): + # 'PUT|PATCH' is how the inventory writes a multi-verb route. + # Without the pipe here, neither verb matched VERBS and the + # operation vanished from the spec without a word. + for verb in re.split(r'[\/,|]', (e.get('method') or 'GET')): verb = verb.strip().lower() if verb not in VERBS: continue diff --git a/tests/test_api_inventory_parity.py b/tests/test_api_inventory_parity.py new file mode 100644 index 0000000..5742694 --- /dev/null +++ b/tests/test_api_inventory_parity.py @@ -0,0 +1,95 @@ +"""Every route the app serves is described in docs/api-inventory.json. + +The inventory is hand-maintained, and deliberately so: its value is the prose - +what each endpoint is FOR, what it accepts, a worked example - and none of that +can be derived from the code. An audit of all 372 documented operations found +zero phantom routes and zero wrong parameter names, so the maintenance itself is +in good order. + +What it cannot do is notice a route somebody added. Twenty-two were serving +traffic undocumented, including an entire plugin's surface and every one of the +dashboard card endpoints - each of them added after its surface was written. + +So the prose stays hand-written and the COVERAGE is enforced here. Adding an +endpoint now means describing it, in the same commit, which is when the author +still knows what it is for. +""" + +import json +import re +from pathlib import Path + +import pytest + +from shopdb import create_app + +REPO = Path(__file__).resolve().parents[1] +INVENTORY = REPO / 'docs' / 'api-inventory.json' + +pytestmark = pytest.mark.skipif( + not INVENTORY.is_file(), + reason='docs/ is excluded from publication; nothing to check here') + +# Flask's own machinery, not this product's API. +EXEMPT_ENDPOINTS = {'static'} + +# Documented as a family rather than per-file: these serve the frontend bundle +# and its assets, not JSON. +EXEMPT_RULE_PREFIXES = ('/static', '/assets') + + +def normalise(rule): + """Flask `` and inventory `` describe the same path. + + Converters are an implementation detail of the route, and the inventory was + written without them. Comparing on the converter would report a difference + that is not one. + """ + return re.sub(r'<(?:[^:>]+:)?([^>]+)>', r'<\1>', rule) + + +def served_operations(): + app = create_app('testing') + served = set() + for rule in app.url_map.iter_rules(): + if rule.endpoint in EXEMPT_ENDPOINTS: + continue + if any(rule.rule.startswith(prefix) for prefix in EXEMPT_RULE_PREFIXES): + continue + if not rule.rule.startswith('/api'): + continue + for method in sorted((rule.methods or set()) - {'HEAD', 'OPTIONS'}): + served.add((method, normalise(rule.rule))) + return served + + +def documented_operations(): + documented = set() + for surface in json.loads(INVENTORY.read_text()): + for entry in surface.get('endpoints', []): + path = normalise(entry.get('path') or '') + # The inventory writes multi-verb routes as 'PUT|PATCH'. Splitting + # on comma and slash alone left those undocumented in this check + # AND dropped from the generated spec entirely. + for method in re.split(r'[/,|]', entry.get('method') or 'GET'): + method = method.strip().upper() + if method: + documented.add((method, path)) + return documented + + +def test_every_served_route_is_documented(): + missing = sorted(served_operations() - documented_operations()) + assert not missing, ( + '%d route(s) serve traffic with no entry in docs/api-inventory.json. ' + 'Describe them there (purpose, auth, params, example) and regenerate ' + 'the spec with scripts/gen_openapi.py:\n %s' + % (len(missing), '\n '.join('%s %s' % row for row in missing))) + + +def test_nothing_documented_has_been_removed(): + """A documented route that no longer exists sends a reader to a 404.""" + stale = sorted(documented_operations() - served_operations()) + assert not stale, ( + '%d documented route(s) are no longer served:\n %s' + % (len(stale), '\n '.join('%s %s' % row for row in stale)))