[
 {
  "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/<userid>",
    "auth": "jwt (admin or self, checked inline)",
    "params": "path: userid (int)",
    "purpose": "Get one user; non-admins may only fetch their own record (403 otherwise)",
    "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/<userid>",
    "auth": "jwt (admin or self; isactive/roles/unlock fields admin-only)",
    "params": "path: userid; body: email, firstname, lastname, password; admin-only: isactive, roles[roleids], unlock (bool)",
    "purpose": "Update a user; email uniqueness enforced; admins can also change active state, roles, and unlock the account; changes audit-logged",
    "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/<userid>",
    "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/<roleid>",
    "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/<roleid>",
    "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/<tokenid>",
    "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/<tokenid>",
    "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/<key>",
    "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/<key>",
    "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_<theme> 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/<path:filename>",
    "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/<path:filename>",
    "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_<type>_enabled settings",
    "auth": "jwt-optional",
    "params": "q (required, 2-200 chars)",
    "example": "curl 'http://localhost:5001/api/search?q=tsgwp00525'"
   },
   {
    "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.wjs.geaerospace.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/<int:default_id>",
    "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/<int:default_id>",
    "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/<pluginname>",
    "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_<NAME> 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\":\"tsgwp00525\",\"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\":\"tsgwp00525\",\"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\":\"tsgwp00525\",\"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/<entitytype>/<int:entityid>",
    "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/<name>",
    "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/<sso>",
    "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/<int:sso>",
    "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/<int:sso>",
    "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=<csv> 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/<int:sso>/photo",
    "purpose": "Upload or replace a self-hosted employee's photo (saved as photo-<sso><ext> 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=<image>, 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/<path:filename>",
    "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/<int:sso>/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/<int:link_id>",
    "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/<int:link_id>/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/<int:link_id>",
    "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/<int:link_id>",
    "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/<type_id>",
    "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/<type_id>",
    "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/<status_id>",
    "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/<status_id>",
    "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/<status_id>",
    "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/<type_id>",
    "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/<type_id>",
    "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/<asset_id>",
    "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/<asset_id>",
    "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/<asset_id>",
    "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/<assetnumber>",
    "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/<asset_id>/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/<rel_id>",
    "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/<asset_id>/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/<type_id>",
    "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/<type_id>",
    "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/<location_id>",
    "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/<location_id>",
    "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/<location_id>",
    "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/<vendor_id>",
    "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/<vendor_id>",
    "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/<vendor_id>",
    "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/<model_id>",
    "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/<model_id>",
    "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/<model_id>",
    "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/<model_id>/image",
    "purpose": "Upload/replace model photo (saved as instance/modelimages/model-<id><ext>, one per model); sets imageurl",
    "auth": "admin",
    "params": "multipart/form-data: file=<image> (.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/<filename>",
    "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/<model_id>/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/<type_id>",
    "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/<type_id>",
    "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/<type_id>",
    "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/<bu_id>",
    "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/<bu_id>",
    "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/<bu_id>",
    "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/<os_id>",
    "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/<os_id>",
    "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/<os_id>",
    "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/<fieldid>",
    "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/<fieldid>",
    "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/<assetid>",
    "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/<assetid>",
    "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/<team_id>",
    "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/<team_id>",
    "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/<team_id>",
    "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/<team_id>/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/<team_id>/contacts/<contact_id>",
    "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/<team_id>/contacts/<contact_id>",
    "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/<app_id>",
    "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/<app_id>",
    "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/<app_id>",
    "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/<app_id>/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/<app_id>/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/<app_id>/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/<machine_id>",
    "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/<machine_id>",
    "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/<machine_id>/<app_id>",
    "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/<machine_id>/<app_id>",
    "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}'"
   }
  ]
 },
 {
  "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/<int:type_id>",
    "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/<int:type_id>",
    "auth": "permission:computers.edit (jwt_required)",
    "params": "body: computertype, description, icon, color, isactive; 409 on duplicate name",
    "purpose": "Update a computer type",
    "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/<int:type_id>",
    "auth": "permission:computers.delete (jwt_required)",
    "params": "type_id in path; 409 if any Computer still uses the type",
    "purpose": "Hard-delete a computer type when unused",
    "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/<int:protocol_id>",
    "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/<int:protocol_id>",
    "auth": "permission:computers.edit (jwt_required)",
    "params": "protocol_id in path; if referenced by any ComputerAccess it deactivates instead of deleting",
    "purpose": "Delete an access protocol (soft-deactivate when in use)",
    "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<serial>.<domain> 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/<int:computer_id>",
    "auth": "jwt-optional",
    "params": "computer_id in path",
    "purpose": "Get one computer with full detail: asset fields, computer extension, communications, resolved access links",
    "example": "curl 'http://localhost:5001/api/computers/42'"
   },
   {
    "method": "GET",
    "path": "/api/computers/by-asset/<int:asset_id>",
    "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/<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/tsgwp00525'"
   },
   {
    "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/<int:computer_id>",
    "auth": "permission:computers.edit (jwt_required)",
    "params": "body: any asset field (assetnumber, name, serialnumber, gaugelabreference, maintenancereference, statusid, locationid, businessunitid, mapx, mapy, notes, isactive) or computer field (computertypeid, hostname, osid, vendorid, modelnumberid, loggedinuser, lastreporteddate, lastboottime); ipaddress upserts/clears the primary IP communication; accessmethods replaces 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/<int:computer_id>",
    "auth": "permission:computers.delete (jwt_required)",
    "params": "computer_id in path",
    "purpose": "Soft-delete a computer (sets asset isactive=false), audit-logged",
    "example": "curl -X DELETE -H 'Authorization: Bearer $TOKEN' 'http://localhost:5001/api/computers/42'"
   },
   {
    "method": "GET",
    "path": "/api/computers/<int:computer_id>/apps",
    "auth": "jwt-optional",
    "params": "computer_id in path; returns active installs only",
    "purpose": "List installed applications on a computer",
    "example": "curl 'http://localhost:5001/api/computers/42/apps'"
   },
   {
    "method": "POST",
    "path": "/api/computers/<int:computer_id>/apps",
    "auth": "permission:computers.create (jwt_required)",
    "params": "body: appid (required, must exist in Applications), appversionid; reactivates a soft-deleted install; 409 if already installed",
    "purpose": "Record an application install on a computer",
    "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/<int:computer_id>/apps/<int:app_id>",
    "auth": "permission:computers.delete (jwt_required)",
    "params": "computer_id and app_id in path",
    "purpose": "Soft-remove an installed application (isactive=false)",
    "example": "curl -X DELETE -H 'Authorization: Bearer $TOKEN' 'http://localhost:5001/api/computers/42/apps/17'"
   },
   {
    "method": "POST",
    "path": "/api/computers/<int:computer_id>/report",
    "auth": "permission:computers.create (jwt_required)",
    "params": "body (all optional): loggedinuser, lastboottime; server sets lastreporteddate to now (UTC)",
    "purpose": "Agent status check-in: refresh last-reported timestamp plus logged-in user and boot time",
    "example": "curl -X POST -H 'Authorization: Bearer $TOKEN' -H 'Content-Type: application/json' -d '{\"loggedinuser\":\"cproudlock\",\"lastboottime\":\"2026-07-30T06:00:00\"}' 'http://localhost:5001/api/computers/42/report'"
   },
   {
    "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/<type_id>",
    "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/<type_id>",
    "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/<type_id>",
    "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/<tool_id>",
    "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/<asset_id>",
    "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/<tool_id>",
    "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/<tool_id>",
    "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/<type_id>",
    "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/<type_id>",
    "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/<type_id>",
    "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/<machine_id>",
    "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/<asset_id>",
    "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/<machine_id>",
    "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/<machine_id>",
    "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/<sha256>",
    "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\":\"tsgwp00525\",\"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/<int:scopeid>",
    "purpose": "Get one scope's summary plus its full draft entry list (each entry includes entryid, sortorder, curated appid/appname link, and inline-payload metadata).",
    "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/<int:scopeid>",
    "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/<int:scopeid>",
    "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/<int:scopeid>/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/<int:scopeid>/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/<int:entryid>",
    "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/<int:entryid>",
    "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/<int:scopeid>/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/<int:scopeid>/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=tsgwp00525&cmmversion=2023.2'"
   },
   {
    "method": "GET",
    "path": "/api/geenforce/scopes/<int:scopeid>/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/<int:entryid>/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/<int:entryid>/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/<int:scopeid>/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/<int:scopeid>/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/<int:scopeid>/versions/<int:versionnumber>",
    "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/<int:scopeid>/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/<int:scopeid>/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/<int:reportid>",
    "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/<int:type_id>",
    "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/<int:notification_id>",
    "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/<int:notification_id>",
    "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/<int:notification_id>",
    "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/<sso>",
    "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/<surface>/<path:filename>",
    "purpose": "Public serving of a single slide image from instance/slides/<surface>/, 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/<surface>",
    "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/<surface>/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/<surface>/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/<surface>/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/<surface>/<int:slideid>",
    "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/<int:item_id>",
    "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/<int:item_id>",
    "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/<int:item_id>",
    "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/<int:item_id>/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/<int:item_id>/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=<image>",
    "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/<path:filename>",
    "purpose": "Serve an uploaded item image from instance/printedpartsimages (public: fetched by <img> 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/<int:item_id>/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/<int:item_id>/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/<int:item_id>/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/<itemcode>",
    "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/<int:item_id>/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/<int:item_id>/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=<print file> (required), note=<what changed> (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/<int:file_id>/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/<int:file_id>",
    "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/<int:type_id>",
    "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/<int:type_id>",
    "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/<int:type_id>",
    "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/<int:device_id>",
    "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/<int:asset_id>",
    "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/<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/<int:device_id>",
    "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/<int:device_id>",
    "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/<int:vlan_id>",
    "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/<int:vlan_id>",
    "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/<int:vlan_id>",
    "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/<int:subnet_id>",
    "purpose": "Get one subnet plus 'devices': every asset of any type (PC/printer/network) whose primary-IP Communication falls inside the CIDR, with cross-plugin detail URLs",
    "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/<int:subnet_id>",
    "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/<int:subnet_id>",
    "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/<device_id>",
    "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/<device_id>",
    "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/<device_id>/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/<device_id>/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/<device_id>/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/<device_id>/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/<type_id>",
    "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/<type_id>",
    "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/<type_id>",
    "auth": "permission:printers.delete",
    "params": "path: type_id; 409 if any printer still references the type",
    "purpose": "Hard-delete a printer type when unused.",
    "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/<driver_id>",
    "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/<driver_id>",
    "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/<printer_id>",
    "auth": "jwt-optional",
    "params": "path: printer_id (int)",
    "purpose": "Get one printer with full asset details, communications, and active drivers matching its model.",
    "example": "curl http://localhost:5001/api/printers/17"
   },
   {
    "method": "GET",
    "path": "/api/printers/by-asset/<asset_id>",
    "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/<printer_id>",
    "auth": "permission:printers.edit",
    "params": "body: any subset of asset fields (assetnumber, name, serialnumber, gaugelabreference, maintenancereference, statusid, locationid, businessunitid, mapx, mapy, notes, isactive) + printer fields (printertypeid, vendorid, modelnumberid, hostname, windowsname, sharename, iscsf, installpath, pin, iscolor, isduplex, isnetwork) + ipaddress (upserts/clears primary IP comm); 409 on assetnumber clash",
    "purpose": "Update printer: asset, extension, and primary IP in one call.",
    "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/<printer_id>",
    "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/<printer_id>/supplies",
    "auth": "jwt-optional",
    "params": "path: printer_id; needs an IP communication on the printer",
    "purpose": "Real-time supply levels from Zabbix for one printer, annotated with status/color/part numbers; fails soft (empty supplies, pingstatus -1) when Zabbix is off/unreachable.",
    "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/<modelnumberid>/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/<modelnumberid>/supplies",
    "auth": "permission:printers.create",
    "params": "body: partnumber (required), supplytype (default toner), color (default none), capacitytier (default standard), marketingname, pageyield, notes; 409 if part already mapped to model",
    "purpose": "Add a supply part-number mapping to a model.",
    "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/<modelsupplyid>",
    "auth": "permission:printers.edit",
    "params": "body: any subset of supplytype, color, capacitytier, partnumber, marketingname, pageyield, notes; enum-validated, 409 on partnumber clash within model",
    "purpose": "Update a model supply mapping.",
    "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/<modelsupplyid>",
    "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"
   }
  ]
 },
 {
  "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/<int:assetid>",
    "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/<int:warrantyid>",
    "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/<int:warrantyid>",
    "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/<int:warrantyid>",
    "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/<int:warrantyid>/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"
   }
  ]
 }
]