diff --git a/.claude/QUICK-REFERENCE.md b/.claude/QUICK-REFERENCE.md index 75232567..bdc49bbd 100644 --- a/.claude/QUICK-REFERENCE.md +++ b/.claude/QUICK-REFERENCE.md @@ -11,29 +11,58 @@ Fast lookup for architectural decisions and contracts. - **ADR-008**: Plugin migration ownership - PLUGIN_TABLE_OWNERS, migration chains - **ADR-013**: Plugin catalog - Lean builds, plugin manifest - **ADR-015**: Site-specific configuration - Where site literals belong -- **ADR-016**: Credential delivery - Device tokens, kiosk auth +- **ADR-016**: Credential delivery to the fleet - cmdkey credentials for bays, + targeting and rotation. ACCEPTED but NOT built. It says nothing about device + tokens or kiosk auth, and nothing forbids the registry: it names registry + provisioning as a recommended first step ## Import Surface (stable API) +Every name below is in `shopdb/api/__init__.py` `__all__`. That list is the +surface; a name that is not in it is not importable from `shopdb.api`, whatever +else in the tree defines it. + ```python from shopdb.api import ( # Database db, - - # Core Models - Asset, Employee, User, Building, MapLevel, - - # Plugin Base - BasePlugin, - - # Auth/Authz - requires_role, requires_device_token, - + + # Core models + Asset, AssetType, AssetStatus, AssetRelationship, + Location, BusinessUnit, Model, OperatingSystem, Vendor, + User, Role, Setting, + + # Auth / authz + require_permission, require_role, require_any_permission, + caller_holds, caller_is_role, + client_ip, ip_in_cidrs, request_ip_allowlisted, + + # Responses and paging + success_response, error_response, ErrorCodes, + get_pagination_params, paginate_query, paginated_response, + # Utilities - badges, datevalues, queryargs + audit_log, cache, + normalise_badge, badge_kind, BadgeShapeError, + parse_date, parse_date_fields, DateValueError, + intarg, intargs, QueryArgError, + resolve_asset_position, ) ``` +NOT on this surface. This block used to name all six, so the first import a new +agent copied raised ImportError: + +- `BasePlugin`, `PluginMeta` - `from shopdb.plugins.base import ...`, which is + what every `plugins/*/plugin.py` in the tree does. +- `Building`, `MapLevel` - `shopdb.core.models.maplevel`. An asset's position + normally comes back through `resolve_asset_position`, which IS exported. +- `Employee` - no such model. The directory lives in a read-only external + database reached through `employee_connection`, and the plugin's own row type + is `DirectoryEmployee` in `plugins/employees/models/`. +- `badges`, `datevalues`, `queryargs` - module names under `shopdb/utils/`, not + exported. Import the functions listed above instead. + ## Plugin Structure ``` @@ -55,9 +84,12 @@ plugins/{name}/ ## Database Naming -- **Tables**: `printedpart`, `mappeddrives` (lowercase concatenated) -- **Columns**: `serial_number`, `last_seen` (snake_case) -- **Enforced**: `scripts/check-naming-and-style.sh` +- **Tables**: `machines`, `mappeddrives` (lowercase concatenated, plural) +- **Columns**: `machineid`, `lastzabbixsync`, `isactive` (lowercase concatenated, + singular). NOT snake_case +- **Indexes**: `idx_machines_locationid`. The only place an underscore is allowed +- **Enforced**: `scripts/check-naming-and-style.sh`, on model attributes AND on the + column names a migration creates ## Branch Model @@ -132,19 +164,98 @@ PLUGIN_TABLE_OWNERS = { } ``` -## Device Tokens (ADR-016) +## Kiosk Access -Kiosks require device authentication: +Not ADR-016 - that ADR is about cmdkey credentials for bays and is not built. +A kiosk route admits a caller on EITHER ground, both gated on the kiosk's own +enable flag: + +This is `plugins/usb/api/routes.py` as it stands, not a paraphrase: ```python -from shopdb.api import requires_device_token +from shopdb.api import Setting, client_ip, ip_in_cidrs -@bp.route('/kiosk/endpoint') -@requires_device_token(['usb_kiosk', 'parts_kiosk']) -def kiosk_view(): - return render_template('kiosk.html') +KIOSK_NEVER_A_CLIENT = '127.0.0.0/8, ::1/128' +KIOSK_TRUSTED_PROXIES = 'usb_kiosk_trusted_proxies' + +def _kiosk_client_ip(): + return client_ip(request) + +def _kiosk_address_is_infrastructure(address): + if ip_in_cidrs(address, KIOSK_NEVER_A_CLIENT): + return True + return ip_in_cidrs(address, Setting.get(KIOSK_TRUSTED_PROXIES)) + +def _kiosk_ip_allowlisted(): + address = _kiosk_client_ip() + if _kiosk_address_is_infrastructure(address): + return False + return ip_in_cidrs(address, Setting.get('usb_kiosk_allowed_cidrs')) + +def _kiosk_open(): + if not _kiosk_enabled(): # the site has to turn it on first + return False + return _kiosk_token_presented() or _kiosk_ip_allowlisted() ``` +THIS IS NOT `request_ip_allowlisted(request, cidrs)`. That helper is exactly +`ip_in_cidrs(client_ip(request_obj), rawcidrs)` and nothing else +(`shopdb/utils/ipallowlist.py`), so it has no infrastructure refusal. The USB +kiosk refuses an address that belongs to the path rather than to a caller BEFORE +it consults the allowlist, and that refusal is the whole point: see the topology +below. Writing the helper here instead of the real shape is how the two got +confused. + +- The allowlists are per kiosk (`usb_kiosk_allowed_cidrs`, + `printedparts_kiosk_allowed_cidrs`) and ship EMPTY, so a site that enters no + ranges stays token-only. +- `ip_in_cidrs` fails closed on an empty setting, an unparseable caller address + and a malformed entry. Never copy a CIDR parser into a plugin. +- A refused caller gets 404, not 403: these routes do not advertise themselves. +- The two kiosks HAVE DIVERGED. `plugins/printedparts/api/routes.py:556` still + calls `request_ip_allowlisted(request, Setting.get('printedparts_kiosk_allowed_cidrs'))` + with no infrastructure check, so a site that allowlists loopback to work + around the topology below opens that kiosk to every caller. Read the plugin + you are editing; do not assume the other one matches. + +### Why the infrastructure refusal exists (the shipped Windows path) + +Trace it, do not guess. IIS hands the request to HttpPlatformHandler, which +connects to waitress on 127.0.0.1 with +`--trusted-proxy=127.0.0.1 --trusted-proxy-headers=x-forwarded-for +--trusted-proxy-count=1` (`deploy/windows/web.config`). waitress, seeing the +trusted peer, overwrites `environ['REMOTE_ADDR']` from the LAST X-Forwarded-For +hop. `client_ip()` then returns `strip_port(request.remote_addr)` +(`shopdb/utils/clientip.py`) - it never reads the header itself. + +The catch: IIS does not set X-Forwarded-For on its own, and the rewrite rule +that would is commented out in `web.config` - it SHIPS DISABLED because it needs +the URL Rewrite module. So on a default install there is no header, waitress has +nothing to promote, and every caller arrives as 127.0.0.1. `TRUSTED_PROXY_COUNT` +defaults to 0 (`shopdb/config.py`), so ProxyFix is not installed either, and it +would not help: waitress already did that job. + +Consequences a kiosk change has to respect: + +- Until someone enables the rule (`shopdb-install.ps1 -ClientIpSource direct` + installs URL Rewrite, allows `HTTP_X_FORWARDED_FOR` at server level and + uncomments it), a CIDR allowlist admits nobody and the kiosk is token-only. + That is the fail-closed direction and it is correct. +- Allowlisting 127.0.0.0/8 to "fix" that would admit everyone, which is why + `KIOSK_NEVER_A_CLIENT` refuses it whatever the setting says. +- `-ClientIpSource proxy` leaves the rule off ON PURPOSE: behind ARR or a load + balancer, REMOTE_ADDR is the proxy and the rule would destroy the real client + address. There the upstream proxy sets the header. + +## Fleet Credentials + +- Agent and GE-Enforce reporters run as SYSTEM, so there is nobody to prompt. + The collector key, base URL and server pins come from + `HKLM\SOFTWARE\GE\ShopDB`. That hive is the design, not a violation. +- An enrolled PC's own per-PC credential is DPAPI-protected in a SYSTEM-only + directory and sent as `X-Endpoint-Credential`; the shared fleet key still + works for a PC that has not enrolled and is refused for one that has. + ## Wiki Location Full docs: `~/Documents/github/shopdb-flask.wiki/` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bfa99c3d..adf36346 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,8 +89,19 @@ jobs: lean-build: # ADR-013 Phase 5: prove a per-site build carries only its chosen plugins. - # Builds a lean site (machines + printers) and asserts an omitted plugin's - # code is absent from the bundle - the delete-a-plugin guarantee in CI. + # The profile is the WJ plugin set, so this job proves the build this branch + # actually ships, not a two-plugin toy. The omission guarantee is what is + # under test, so the assertions below name a component from EVERY omitted + # plugin: a set that grows without its check growing is how a leak ships. + # + # Chosen (12): backups computers endpoints guides knowledgebase machines + # network notifications printers slides tools warranty + # Omitted (5): employees geenforce measuringtools printedparts usb + # 'applications' is manifest-less core and ships in every build (ADR-013). + # endpoints hard-depends on computers, which is already chosen, so the + # closure adds nothing - if that changes, resolve_plugin_closure.py pulls + # the dependency in and the absence assertions below start failing, which is + # the signal to update this list rather than to loosen the check. runs-on: arc-runner-set steps: - uses: actions/checkout@v5 @@ -100,21 +111,28 @@ jobs: cache: npm cache-dependency-path: frontend/package-lock.json - run: cd frontend && npm ci - - name: Build lean site (machines + printers) + - name: Build lean site (WJ plugin set) run: | - printf '{ "site": "ci-lean", "plugins": ["machines", "printers"] }' \ + printf '%s' '{ "site": "ci-lean", "plugins": ["backups", "computers", "endpoints", "guides", "knowledgebase", "machines", "network", "notifications", "printers", "slides", "tools", "warranty"] }' \ > /tmp/lean-profile.json bash scripts/build-site.sh /tmp/lean-profile.json /tmp/leansite - name: Assert omitted plugin code is absent, chosen present run: | assets=/tmp/leansite/frontend-dist/assets - for code in PartsKiosk ManifestEditor USBLabelBatch KnowledgeBaseDetail; do + # One component per OMITTED plugin: employees, geenforce, + # measuringtools, printedparts, usb. + for code in EmployeeDirectory ManifestEditor MeasuringToolsList PartsKiosk USBLabelBatch; do if grep -rqoh "$code" "$assets"/*.js; then echo "FAIL: omitted-plugin code '$code' leaked into the lean bundle" exit 1 fi done - for code in MachineDetail PrinterDetail; do + # One component per CHOSEN plugin, so a plugin silently dropped from + # the bundle fails here instead of on the floor. + for code in BackupHistory PCsList EndpointsList GuidesList \ + KnowledgeBaseDetail MachineDetail NetworkDevicesList \ + NotificationsList PrinterDetail SlideManager ToolsIndex \ + WarrantiesList; do grep -rqoh "$code" "$assets"/*.js || { echo "FAIL: chosen-plugin code '$code' missing from the lean bundle" exit 1; } @@ -124,8 +142,18 @@ jobs: grep -rqoh "ApplicationsList" "$assets"/*.js || { echo "FAIL: core page 'ApplicationsList' missing from the lean bundle" exit 1; } - test -d /tmp/leansite/plugins/machines - test ! -d /tmp/leansite/plugins/printedparts + for plugin in backups computers endpoints guides knowledgebase \ + machines network notifications printers slides tools \ + warranty; do + test -d "/tmp/leansite/plugins/$plugin" || { + echo "FAIL: chosen plugin '$plugin' missing from the backend tree" + exit 1; } + done + for plugin in employees geenforce measuringtools printedparts usb; do + test ! -d "/tmp/leansite/plugins/$plugin" || { + echo "FAIL: omitted plugin '$plugin' staged into the backend tree" + exit 1; } + done echo "lean build verified: only chosen plugins present" # NOTE: the MySQL-8 migration/seed job (fresh `flask db upgrade` + every diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b6b5691..b618f80e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -702,6 +702,187 @@ ADR-007 and ADR-002. `plugins/tools/frontend/labelPages.js`, where the column contract and the two-sided page order are covered by tests. +### Added + +- **Every PC gets its own endpoint credential, and enrolls to get it.** The + fleet authenticated with one shared collector key: every bay held the same + secret, one of them leaking meant reissuing all of them, and a PC could post + a check-in naming any asset it liked. A PC now enrolls once and is + authenticated as itself thereafter. `POST /api/endpoints/enroll` issues a + credential to a PC that matches exactly one computer on hostname and BIOS + serial and holds none yet; anything else - unknown hostname, a duplicate, a + serial that disagrees, a PC already enrolled - is recorded as a request an + administrator approves or rejects, and the agent's persisted nonce is what + binds an approved request back to the PC that asked. Enroll never refuses, so + a PC installed with no key at all still turns up in the queue instead of + vanishing. One auth helper serves every agent route: with + `X-Endpoint-Credential` the asset comes from the credential, so a request + naming a different PC is refused with `ASSET_MISMATCH` rather than believed, + and a revoked credential answers `CREDENTIAL_INVALID` so the agent + re-enrolls rather than going quiet. The core collector takes the credential + for the computers, printers and endpoints collectors through two hooks on + `shopdb.api` (contract 0.33.0). + + Settings > Endpoints > Enrollment is where this is run. The Migration tab + shows readiness counts, each PC's state with the reasons it is blocked, + "Upgrade selected" and "Upgrade next 25", and live wave progress; a wave + packages the agent once and queues the existing per-PC upgrade. The + Credentials tab lists enrolled PCs with revoke, pending requests with + approve and reject, and the two switches. The fleet list gained a Credential + column and filter, and a PC's detail page shows its credential state, with + Revoke and Allow re-enrollment for an administrator. Migration 0023 creates + `endpointcredentials`, `endpointenrollrequests` and + `endpointmigrationwaves`. + +- **The agent pins the server it talks to, and the registry owns the pins.** + An air-gapped site runs its own certificate, so "trusted chain" says very + little; the agent now checks the server's SubjectPublicKeyInfo SHA-256 + against a pinned list before it will send a credential anywhere, and a + credential is only ever sent back to the server that issued it. Pins live in + `HKLM\SOFTWARE\GE\ShopDB` under `ServerPins`, with `appsettings.json` as the + fallback. The registry is AUTHORITATIVE whenever that value exists, even + when it is empty: the two used to be merged, so removing a pin from HKLM + never took effect anywhere. Two pins are carried at once, current and next, + so a certificate can be rotated without a flag day. + `Set-ShopDBServerPins.ps1` is the GE-Enforce entry point (`-Clear` deletes + the value, `-None` writes an empty one) and `Get-ShopDBServerPin.ps1` reads + what a PC has. `GET /settings/server-cert` returns the pin to put in that + entry, the Server Certificate page shows it, and the page also compares the + certificate the site actually presents at its public address with the + certificate file, warning when the two disagree. + +- **An agent on a one-off PC from a single command.** A PC outside GE-Enforce - + a one-off, a loaner, something rebuilt in a hurry - had no path to an agent + short of doing the whole managed install by hand. An administrator now + generates a short-lived token in Settings > Endpoints > Endpoints and hands a + technician one elevated PowerShell line. The script refuses to run + unelevated, refuses a PC that already has an agent (a managed bay gets its + agent from GE-Enforce), checks both binaries against hashes the server baked + into the package, and installs with NO shared key - the PC then asks to + enroll and the script watches locally for two minutes for an administrator to + approve it. The token routes answer 404 to anything wrong rather than + confirming a token exists, are rate limited per address and are audited with + the caller, and a bootstrap package can never carry a collector key. + Migration `0024_endpoint_bootstrap_tokens` adds the token table, which stores + a hash, an expiry and a use count. + +- **A kiosk admits a caller by where it is on the network, not only by its + token.** The device token assumes a handful of fixed screens provisioned + once, and that is not how these kiosks are used: most people open the page on + their own PC, there is no login, and per-browser provisioning would mean + touching every PC on the floor and losing it again at the next reimage. + Both the USB and printed-parts kiosks now admit a caller on either ground - + the per-device token, or a source address inside an allowlist - the same + shape the GE-Enforce client endpoints already use. `shopdb.api` gained + `ip_in_cidrs` and `request_ip_allowlisted` (contract 0.35.0) so the two + plugins share one gate rather than two copies of a CIDR parser; they fail + closed on an empty setting, an unparseable caller address and a malformed + entry, and they read `remote_addr`, so the IIS rewrite rule is what makes + them meaningful. The allowlists are separate per kiosk + (`usb_kiosk_allowed_cidrs`, `printedparts_kiosk_allowed_cidrs`) for the same + reason the tokens are, and both SHIP EMPTY: nothing opens by itself, and a + site that enters no ranges keeps exactly today's token-only behaviour. Both + settings pages get a picker listing the network plugin's subnets with their + VLANs, storing resolved CIDR strings rather than subnet ids so the setting + still works at a site that does not run that plugin. Stated plainly in the + admin UI: anyone on an allowed network can check a drive in or out against a + badge number they guess, and badges are attribution, not authorization. + +- **A kiosk says when it is not set up, and an administrator can copy the setup + link.** The settings API masks every key whose name contains `token`, so the + real kiosk token is on screen for exactly one moment - after Generate, before + Save - and a screen provisioned with the literal mask answers 404, which is + also how the route answers a caller it has never heard of. Each kiosk gained + a permission-gated provisioning-link route that returns the real setup URL to + somebody who already holds `settings.edit`, and both settings pages a "Copy + setup link" button that falls back to showing the link when the clipboard is + refused. Revealing a token writes an audit row. The USB kiosk now asks the + server on load whether this screen may be used and says plainly when it may + not, instead of rendering a keypad that only fails once somebody has typed + their SSO. + +- **The USB audit says whether a returned drive came back wiped and scanned.** + Removable-media custody is CMMC evidence, and custody without the state of + the media on return is half the record. Both stores already held the values - + `sanitized` and `scanned_viruses` in the external `checkinoutlog`, + `waswiped` and `scannedviruses` in the self-hosted `usbcheckouts` - and + neither mode selected them. Both now report them in the JSON, the CSV and the + column list. Only a return carries them, so a check-out row shows blanks + rather than asserting something about an event that had not happened yet, and + unknown stays blank rather than rendering as "No": both stores keep these + nullable deliberately, and "nobody wrote it down" is a different claim from + "it came back unwiped". + +### Changed + +- **Endpoint agent 1.20.1 and tray 1.7.1. Ship them together.** Every HTTP + client in the agent, script and package downloads included, is now built by + one `AgentHttpFactory`, so there is a single place the pins and the + credential are applied rather than one per caller. `EnrollmentService` + enrolls once using whatever shared key the PC already has, stores the + credential DPAPI-protected in a SYSTEM-only directory and swaps to it without + a restart; an old server with no enroll route simply leaves the agent on the + shared key. A named pipe lets the interactive user ask the agent to send + status, send a report, list alerts and acknowledge one, so `--once` and + `--report` run as a user become pipe clients, and `appsettings.json` is + restricted to SYSTEM and Administrators once the pipe is up. The tray holds + no key and reads no agent configuration at all - everything it does goes + through that pipe, an acknowledgement is remembered only once the agent + confirms it, and when the agent is not running the menu says so instead of + failing quietly. + + The version numbers moved from 1.20.0 and 1.7.0 with no behaviour change of + their own: those binaries were built and shipped to the display scope before + two fixes landed, so a rebuild carried different bytes under the same version + number. Detection is by hash and copes with that, but nobody reading a + version number can tell two 1.20.0.0 files apart, and the runbook asks for one + build across the share, the agent store and the display profile. + +- **A PC that has enrolled is refused the shared fleet key.** Once a PC holds + its own credential, presenting the fleet key is either a misconfiguration or + somebody else using a leaked secret, and either way the right answer is not + to accept it: that call is refused with `SHARED_KEY_REFUSED`. This only ever + applies to a PC that has already enrolled. `plugin.endpoints.sharedkeyallowed` + still DEFAULTS TO ON, so every un-migrated bay goes on reporting exactly as it + does today; a site turns it off deliberately once its fleet has moved, and the + page asks for confirmation and says how many PCs are not enrolled yet. + `plugin.endpoints.autoenroll` also defaults to on, so a clean hostname and + serial match is issued a credential without an administrator waiting on it. + +- **The shared-key page now reads as the migration state it is.** Settings > + Endpoints presents the key as what PCs use UNTIL they enroll, with enrollment + progress, a link to Enrollment, and rotate and revoke confirmations that say + how many PCs would lose their only credential. The settings cards describe + the server certificate as what PCs actually receive, on IIS or Docker. + `AGENT-CONTRACT.md` documents per-PC credentials, and the rollout runbook + covers the phases, the gates, rollback, the pin and wave entries, and every + place the shared key lives and how to clear it at cutover. + +### Removed + +- **Client certificate authentication, the 2026-09-14 attempt.** `client_cert.py`, + certificate renewal, expiry monitoring, the unauthenticated + `/test/client-cert` route, the nginx client-certificate forwarding and the + agent certificate script are all gone. The header carrying the client + certificate could be forged on IIS, the private key was readable by users, + and the shared key remained a fallback underneath it all, so it bought + nothing the per-PC credential above does not buy properly. Migration 0023 + drops the certificate columns from `assets` where a test database has them; + 0021 and 0022 keep their ids but no longer touch the core table. + +### Security + +- **The tray no longer holds a secret.** It ran as the interactive user and + carried enough configuration to talk to the server on its own. Everything it + needs now comes over the agent's pipe, which means a standard user on a bay + can read nothing out of it. + +- **`collect_log` refuses to collect the things that hold credentials.** Setup + answer files, `appsettings.json`, agent directories and key files are refused + at enqueue rather than shipped to the server inside a log bundle. The setup + token is no longer printed to the log either, and two VM notes that carried a + live collector key and advised plain http were deleted rather than corrected. + ## [0.12.0] - 2026-08-20 Printers become a property of the bay rather than of whoever last walked up to a diff --git a/CLAUDE.md b/CLAUDE.md index abd32fc9..83177715 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,12 +1,12 @@ # ShopDB Flask - Claude Code Context -ShopDB is a manufacturing floor asset management system built with Flask, Vue 3, and PostgreSQL, deployed across air-gapped GE Aerospace sites. +ShopDB is a manufacturing floor asset management system built with Flask, Vue 3, and MySQL, deployed across air-gapped GE Aerospace sites. ## Architecture - **Backend**: Flask 3.x with SQLAlchemy 2.x, plugin-based architecture - **Frontend**: Vue 3 with Vite, component-based UI with plugin panels -- **Database**: PostgreSQL 15+ with Alembic migrations per plugin +- **Database**: MySQL 8 with Alembic migrations per plugin (`docker-compose.yml` runs `mysql:8.4`; WJ prod is IIS in front of MySQL 8) - **Deployment**: Docker Compose with gunicorn, air-gapped sites via bundle workflow - **Plugins**: 20+ plugins (computers, endpoints, printers, machines, etc.) @@ -27,10 +27,13 @@ Each plugin owns its migration chain in `plugins/{name}/migrations/versions/`. - Migrations run automatically on deploy ### Database Naming Convention -- **Tables**: lowercase concatenated, no underscores (e.g., `printedpart`, `mappeddrives`) -- **Columns**: snake_case (e.g., `serial_number`, `last_seen`) -- **Indexes/constraints**: follow PostgreSQL conventions -- Enforced by `scripts/check-naming-and-style.sh` pre-commit hook +- **Tables**: lowercase concatenated, plural, no underscores (e.g., `machines`, `mappeddrives`) +- **Columns**: lowercase concatenated, singular, no underscores (e.g., `machineid`, + `lastzabbixsync`, `isactive`). NOT snake_case - see CONTRIBUTING.md +- **Indexes**: `idx__`. Underscores are allowed HERE ONLY, because an + index is infrastructure and not data +- Enforced by `scripts/check-naming-and-style.sh` pre-commit hook, on model attributes + AND on the column names a migration creates ### Branch and Deployment Model - `wj` -> prod (shop floor at WJ site) @@ -54,10 +57,20 @@ Plugins extend ShopDB via: - `frontend/views/` - Vue components auto-discovered by router - `migrations/versions/` - Alembic migrations -Import from `shopdb.api` for stable surfaces: +Import from `shopdb.api` for stable surfaces. Every name has to be in that +module's `__all__`; this line used to read +`db, Asset, Employee, BasePlugin` and two of the four do not resolve, so the +first block a new agent copied raised ImportError: ```python -from shopdb.api import db, Asset, Employee, BasePlugin +from shopdb.api import db, Asset, User, Setting, require_permission +# BasePlugin is NOT on that surface. plugin.py takes it from its own module, +# which is what every plugins/*/plugin.py in this tree does: +from shopdb.plugins.base import BasePlugin, PluginMeta ``` +There is no `Employee` model. The directory is an external read-only database +reached through `employee_connection`; the plugin's own row type is +`DirectoryEmployee`. See `.claude/QUICK-REFERENCE.md` for the full export list +and for the other names that are commonly assumed and are not there. ## Documentation @@ -69,7 +82,7 @@ Full documentation in the GitHub wiki at `../shopdb-flask.wiki/`: - ADR-006: Collector contract (endpoint agent) - ADR-007: Product versioning and releases - ADR-013: Plugin catalog and lean builds -- ADR-016: Credential delivery +- ADR-016: Credential delivery to the fleet (cmdkey credentials for bays; ACCEPTED, NOT built - it is not about kiosks or device tokens) **References**: - `API-REFERENCE.md` - Core API surface @@ -116,7 +129,24 @@ Full documentation in the GitHub wiki at `../shopdb-flask.wiki/`: - **Migration conflicts**: Keep plugin chains linear, coordinate with PLUGIN_TABLE_OWNERS - **Unicode**: ASCII only - check logs, comments, commit messages - **Import surface**: Use `shopdb.api`, not internal paths -- **Device tokens**: Kiosks require device_token authentication (ADR-016) +- **Kiosk access**: a kiosk route admits a caller on either of two grounds - the + per-kiosk device token, or a source address inside that kiosk's CIDR allowlist + (`usb_kiosk_allowed_cidrs`, `printedparts_kiosk_allowed_cidrs`) - and both are + gated on the kiosk's enable flag. Use `ip_in_cidrs` from `shopdb.api`; do not + hand-roll a CIDR parser. `request_ip_allowlisted` is only + `ip_in_cidrs(client_ip(request), cidrs)` - the USB kiosk does NOT use it, + because it first refuses an address that belongs to the path rather than to a + caller (loopback, or a configured trusted proxy). On the shipped Windows stack + the X-Forwarded-For rewrite rule is disabled, so every caller arrives as + 127.0.0.1 and that refusal is what keeps the door shut. The real shape and the + full trace are in `.claude/QUICK-REFERENCE.md`. There is no + `requires_device_token` decorator +- **Fleet credentials are in the registry, deliberately**: the endpoint agent and the + GE-Enforce reporters run as SYSTEM with no user to prompt, and read their collector + key, base URL and server pins from `HKLM\SOFTWARE\GE\ShopDB`. An enrolled PC's + own per-PC credential is DPAPI-protected in a SYSTEM-only directory instead. + Nothing forbids the registry hive - ADR-016 names registry provisioning as a + recommended first step ## Work PC Role diff --git a/deploy/windows/installer/shopdb-install.ps1 b/deploy/windows/installer/shopdb-install.ps1 index afe9e052..3701897e 100644 --- a/deploy/windows/installer/shopdb-install.ps1 +++ b/deploy/windows/installer/shopdb-install.ps1 @@ -124,8 +124,11 @@ param( # the proxy, so applying it would DESTROY the real client IP. # # Without one or the other, IIS sends no X-Forwarded-For at all and every - # client reads as 127.0.0.1: the GE-Enforce IP allowlist, the dashboard - # visitor-location lookup and per-host login rate limiting all break quietly. + # client reads as 127.0.0.1: the GE-Enforce IP allowlist, BOTH logged-out + # kiosk allowlists (usb_kiosk_allowed_cidrs, printedparts_kiosk_allowed_cidrs + # - these admit writes with no login), the dashboard visitor-location lookup + # and per-host login rate limiting all break quietly. Stage 4 verifies this + # in both modes; see the client-IP check there. [ValidateSet('direct','proxy')] [string] $ClientIpSource = 'direct', # Load the shared vendor/model catalog during stage 3. Offered by the # wizard rather than assumed: a site that machines nothing does not want @@ -2500,27 +2503,71 @@ public class ShopdbSmokeTestCertPolicy : ICertificatePolicy { } } - # Is the client-IP rule actually in effect? It is the control that makes - # remote_addr trustworthy: without it IIS forwards whatever X-Forwarded-For - # the CALLER sent, waitress trusts it because it arrives from loopback, and - # the GE-Enforce IP allowlist becomes bypassable from anywhere on the network. - # It does not fail closed. Verified here rather than assumed, because the - # failure is silent and looks exactly like a working server. + # Is the client-IP control actually in effect? Something has to make the + # address the app sees the CALLER's: waitress is launched with + # --trusted-proxy=127.0.0.1 --trusted-proxy-headers=x-forwarded-for, and + # HttpPlatformHandler always connects from loopback, so waitress ALWAYS + # believes the X-Forwarded-For it is handed. Whether that header is worth + # believing is decided in front of it, and nothing fails closed if it is not. + # + # THREE allowlists now ride on this, not one: the GE-Enforce client + # allowlist and the two kiosk allowlists (usb_kiosk_allowed_cidrs, + # printedparts_kiosk_allowed_cidrs), which admit UNAUTHENTICATED writes by + # network position. Naming only GE-Enforce here sent people to the wrong + # place - and left the kiosks out of the decision entirely. + # + # Checked in BOTH modes. It used to run only under -ClientIpSource direct, + # so the proxy deployment - the one where the header really does arrive from + # outside - had nothing verified at all. + $liveCfg = Join-Path $AppRoot 'web.config' + $cfgText = if (Test-Path $liveCfg) { Get-Content $liveCfg -Raw } else { '' } + $ruleLive = ($cfgText -match '') -and ($cfgText -notmatch 'SHOPDB-CLIENTIP-BEGIN') + $moduleThere = Test-Path (Join-Path $env:windir 'system32\inetsrv\rewrite.dll') if ($ClientIpSource -eq 'direct') { - $liveCfg = Join-Path $AppRoot 'web.config' - $cfgText = if (Test-Path $liveCfg) { Get-Content $liveCfg -Raw } else { '' } - $ruleLive = ($cfgText -match '') -and ($cfgText -notmatch 'SHOPDB-CLIENTIP-BEGIN') - $moduleThere = Test-Path (Join-Path $env:windir 'system32\inetsrv\rewrite.dll') if ($ruleLive -and $moduleThere) { Write-Log 'client-IP rule is live; remote_addr is trustworthy' 'OK' } else { Write-Log 'CLIENT-IP RULE IS NOT IN EFFECT' 'WARN' if (-not $moduleThere) { Write-Log ' URL Rewrite module is not installed' 'WARN' } if (-not $ruleLive) { Write-Log ' web.config does not have an active block' 'WARN' } - Write-Log ' every client will be recorded as 127.0.0.1, and the GE-Enforce IP' 'WARN' - Write-Log ' allowlist is BYPASSABLE - a caller can send its own X-Forwarded-For' 'WARN' - Write-Log ' re-run with -ClientIpSource direct, or use token authentication' 'WARN' + Write-Log ' a caller can send its own X-Forwarded-For and be believed, so EVERY' 'WARN' + Write-Log ' address-based control is BYPASSABLE from anywhere on the network:' 'WARN' + Write-Log ' - the GE-Enforce client IP allowlist' 'WARN' + Write-Log ' - usb_kiosk_allowed_cidrs (logged-out USB kiosk writes)' 'WARN' + Write-Log ' - printedparts_kiosk_allowed_cidrs (logged-out parts kiosk writes)' 'WARN' + Write-Log ' and with no header at all every client is recorded as 127.0.0.1.' 'WARN' + Write-Log ' the USB kiosk refuses a loopback caller outright rather than' 'WARN' + Write-Log ' admitting one, so that case reads as "not set up", not as access.' 'WARN' + Write-Log ' install URL Rewrite and enable the SHOPDB-CLIENTIP block, or leave the' 'WARN' + Write-Log ' two kiosk allowlists EMPTY and provision each screen with its token' 'WARN' } + } else { + # Proxy mode. The rule must be OFF here - it would overwrite the real + # client with the proxy's address - and the thing that has to be true + # instead is about the proxy, which this script cannot reach out and + # test. So: check what can be checked, and state the rest plainly. + if ($ruleLive) { + Write-Log 'web.config OVERWRITES X-Forwarded-For but -ClientIpSource is proxy' 'WARN' + Write-Log ' every request will be attributed to the proxy, not the caller:' 'WARN' + Write-Log ' the GE-Enforce and kiosk allowlists then match nothing at all' 'WARN' + Write-Log ' remove the block from web.config' 'WARN' + } else { + Write-Log 'client IP comes from the proxy; no rewrite rule in web.config' 'OK' + } + Write-Log 'PROXY MODE: two things this installer cannot verify for you' 'WARN' + Write-Log ' 1. the proxy must APPEND the caller to X-Forwarded-For (ARR does).' 'WARN' + Write-Log ' waitress takes the LAST hop and puts it in REMOTE_ADDR, so a proxy' 'WARN' + Write-Log ' that passes a caller-sent header through unchanged makes every' 'WARN' + Write-Log ' address control bypassable from anywhere that can reach the proxy.' 'WARN' + Write-Log ' 2. set usb_kiosk_trusted_proxies (Settings > USB) to where the proxy' 'WARN' + Write-Log ' is, BEFORE filling in usb_kiosk_allowed_cidrs. The app does not' 'WARN' + Write-Log ' read forwarding headers itself - waitress already promoted the' 'WARN' + Write-Log ' caller into REMOTE_ADDR, so the allowlist matches the CALLER, not' 'WARN' + Write-Log ' the proxy. What it is for is the opposite case: a request that' 'WARN' + Write-Log ' arrives wearing the PROXY address, because the proxy forwarded' 'WARN' + Write-Log ' nothing. A proxy normally sits on the same plant network the' 'WARN' + Write-Log ' allowlist names, so that request would otherwise be admitted on no' 'WARN' + Write-Log ' evidence at all. Naming the proxy here subtracts it and fails closed.' 'WARN' } # The self-hosted API documentation. It is the thing an operator - or the diff --git a/docs/api-inventory.json b/docs/api-inventory.json new file mode 100644 index 00000000..53b0d85a --- /dev/null +++ b/docs/api-inventory.json @@ -0,0 +1,4787 @@ +[ + { + "surface": "core-apitokens", + "endpoints": [ + { + "method": "GET", + "path": "/api/apitokens", + "auth": "jwt", + "params": "not documented", + "purpose": "List the caller's own tokens. Admins may pass ?all=true for everyone's.", + "example": "curl http://localhost:5001/api/apitokens -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/apitokens", + "auth": "permission:apitokens.create", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create a token for the caller. Returns the full secret ONCE.", + "example": "curl -X POST http://localhost:5001/api/apitokens -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/apitokens/", + "auth": "permission:apitokens.create", + "params": "path: tokenid (int)", + "purpose": "Revoke (deactivate) a token. Own token, or any if admin.", + "example": "curl -X DELETE http://localhost:5001/api/apitokens/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/apitokens/", + "auth": "permission:apitokens.create", + "params": "path: tokenid (int); body: JSON (fields not yet documented)", + "purpose": "Rename, rescope, or deactivate a token. Own token, or any if admin.", + "example": "curl -X PUT http://localhost:5001/api/apitokens/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + } + ] + }, + { + "surface": "core-applications", + "endpoints": [ + { + "method": "GET", + "path": "/api/applications", + "auth": "permission:applications.view", + "params": "not documented", + "purpose": "List all applications.", + "example": "curl http://localhost:5001/api/applications -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/applications", + "auth": "permission:applications.create", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create a new application.", + "example": "curl -X POST http://localhost:5001/api/applications -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/applications/", + "auth": "permission:applications.delete", + "params": "path: app_id (int)", + "purpose": "Permanently delete an application, its versions, and its uploads.", + "example": "curl -X DELETE http://localhost:5001/api/applications/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/applications/", + "auth": "permission:applications.view", + "params": "path: app_id (int)", + "purpose": "Get a single application with details.", + "example": "curl http://localhost:5001/api/applications/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/applications/", + "auth": "permission:applications.edit", + "params": "path: app_id (int); body: JSON (fields not yet documented)", + "purpose": "Update an application.", + "example": "curl -X PUT http://localhost:5001/api/applications/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/applications//image", + "auth": "permission:applications.edit", + "params": "path: app_id (int)", + "purpose": "Remove an application's image and clear the field.", + "example": "curl -X DELETE http://localhost:5001/api/applications/1/image -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/applications//image", + "auth": "permission:applications.edit", + "params": "path: app_id (int); body: JSON (fields not yet documented)", + "purpose": "Upload (or replace) an application's image.", + "example": "curl -X POST http://localhost:5001/api/applications/1/image -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/applications//installed", + "auth": "permission:applications.view", + "params": "path: app_id (int)", + "purpose": "List all computers that have this application installed.", + "example": "curl http://localhost:5001/api/applications/1/installed -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "DELETE", + "path": "/api/applications//package", + "auth": "permission:applications.edit", + "params": "path: app_id (int)", + "purpose": "Remove an uploaded installer and clear the path it set.", + "example": "curl -X DELETE http://localhost:5001/api/applications/1/package -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/applications//package", + "auth": "permission:applications.edit", + "params": "path: app_id (int); body: JSON (fields not yet documented)", + "purpose": "Upload (or replace) the installer for an application.", + "example": "curl -X POST http://localhost:5001/api/applications/1/package -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/applications//revert", + "auth": "permission:applications.edit", + "params": "path: app_id (int); body: JSON (fields not yet documented)", + "purpose": "Strip an application back to a catalogue record.", + "example": "curl -X POST http://localhost:5001/api/applications/1/revert -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/applications//versions", + "auth": "permission:applications.view", + "params": "path: app_id (int)", + "purpose": "List all versions for an application.", + "example": "curl http://localhost:5001/api/applications/1/versions -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/applications//versions", + "auth": "permission:applications.create", + "params": "path: app_id (int); body: JSON (fields not yet documented)", + "purpose": "Create a new version for an application.", + "example": "curl -X POST http://localhost:5001/api/applications/1/versions -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/applications/image/", + "auth": "none (reviewed: named in the public-read or mutation sweep)", + "params": "path: filename (path)", + "purpose": "Serve an uploaded application image.", + "example": "curl http://localhost:5001/api/applications/image/example.png" + }, + { + "method": "GET", + "path": "/api/applications/machines/", + "auth": "permission:applications.view", + "params": "path: machine_id (int)", + "purpose": "List all applications installed on a computer.", + "example": "curl http://localhost:5001/api/applications/machines/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/applications/machines/", + "auth": "permission:applications.create", + "params": "path: machine_id (int); body: JSON (fields not yet documented)", + "purpose": "Install an application on a computer.", + "example": "curl -X POST http://localhost:5001/api/applications/machines/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/applications/machines//", + "auth": "permission:applications.delete", + "params": "path: machine_id (int), app_id (int)", + "purpose": "Uninstall an application from a computer.", + "example": "curl -X DELETE http://localhost:5001/api/applications/machines/1/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/applications/machines//", + "auth": "permission:applications.edit", + "params": "path: machine_id (int), app_id (int); body: JSON (fields not yet documented)", + "purpose": "Update installed application (e.g., change version).", + "example": "curl -X PUT http://localhost:5001/api/applications/machines/1/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/applications/package/", + "auth": "permission:applications.download", + "params": "path: filename (path)", + "purpose": "Download an uploaded installer.", + "example": "curl http://localhost:5001/api/applications/package/example.png -H \"Authorization: Bearer $TOK\"" + } + ] + }, + { + "surface": "core-assets", + "endpoints": [ + { + "method": "GET", + "path": "/api/assets", + "auth": "permission:assets.view", + "params": "not documented", + "purpose": "List all assets with filtering and pagination.", + "example": "curl http://localhost:5001/api/assets -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/assets", + "auth": "permission:assets.create", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create a new asset.", + "example": "curl -X POST http://localhost:5001/api/assets -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/assets/", + "auth": "permission:assets.delete", + "params": "path: asset_id (int)", + "purpose": "Delete (soft delete) an asset.", + "example": "curl -X DELETE http://localhost:5001/api/assets/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/assets/", + "auth": "permission:assets.view", + "params": "path: asset_id (int)", + "purpose": "Get a single asset with full details.", + "example": "curl http://localhost:5001/api/assets/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/assets/", + "auth": "permission:assets.edit", + "params": "path: asset_id (int); body: JSON (fields not yet documented)", + "purpose": "Update an asset.", + "example": "curl -X PUT http://localhost:5001/api/assets/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/assets//communications", + "auth": "permission:assets.view", + "params": "path: asset_id (int)", + "purpose": "Get all communications for an asset.", + "example": "curl http://localhost:5001/api/assets/1/communications -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/assets//relationships", + "auth": "permission:assets.view", + "params": "path: asset_id (int)", + "purpose": "Get all relationships for an asset.", + "example": "curl http://localhost:5001/api/assets/1/relationships -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/assets/lookup/", + "auth": "permission:assets.view", + "params": "path: assetnumber (string)", + "purpose": "Look up an asset by its asset number.", + "example": "curl http://localhost:5001/api/assets/lookup/x -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/assets/map", + "auth": "permission:assets.view", + "params": "not documented", + "purpose": "Get all assets with map positions for unified floor map display.", + "example": "curl http://localhost:5001/api/assets/map -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/assets/relationships", + "auth": "permission:assets.create", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create a relationship between two assets.", + "example": "curl -X POST http://localhost:5001/api/assets/relationships -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/assets/relationships/", + "auth": "permission:assets.delete", + "params": "path: rel_id (int)", + "purpose": "Delete an asset relationship.", + "example": "curl -X DELETE http://localhost:5001/api/assets/relationships/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/assets/relationshiptypes", + "auth": "permission:assets.view", + "params": "not documented", + "purpose": "List all asset relationship types.", + "example": "curl http://localhost:5001/api/assets/relationshiptypes -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/assets/relationshiptypes", + "auth": "permission:assets.create", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create a new asset relationship type.", + "example": "curl -X POST http://localhost:5001/api/assets/relationshiptypes -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/assets/relationshiptypes/", + "auth": "permission:assets.delete", + "params": "path: type_id (int)", + "purpose": "Delete a relationship type. Refused if any relationship still uses it.", + "example": "curl -X DELETE http://localhost:5001/api/assets/relationshiptypes/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/assets/relationshiptypes/", + "auth": "permission:assets.edit", + "params": "path: type_id (int); body: JSON (fields not yet documented)", + "purpose": "Update a relationship type.", + "example": "curl -X PUT http://localhost:5001/api/assets/relationshiptypes/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/assets/statuses", + "auth": "permission:assets.view", + "params": "not documented", + "purpose": "List all asset statuses.", + "example": "curl http://localhost:5001/api/assets/statuses -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/assets/statuses", + "auth": "permission:assets.create", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create a new asset status.", + "example": "curl -X POST http://localhost:5001/api/assets/statuses -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/assets/statuses/", + "auth": "permission:assets.delete", + "params": "path: status_id (int)", + "purpose": "Delete an asset status. Refused if any asset still uses it.", + "example": "curl -X DELETE http://localhost:5001/api/assets/statuses/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/assets/statuses/", + "auth": "permission:assets.view", + "params": "path: status_id (int)", + "purpose": "Get a single asset status.", + "example": "curl http://localhost:5001/api/assets/statuses/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/assets/statuses/", + "auth": "permission:assets.edit", + "params": "path: status_id (int); body: JSON (fields not yet documented)", + "purpose": "Update an asset status.", + "example": "curl -X PUT http://localhost:5001/api/assets/statuses/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/assets/types", + "auth": "permission:assets.view", + "params": "not documented", + "purpose": "List all asset types.", + "example": "curl http://localhost:5001/api/assets/types -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/assets/types", + "auth": "permission:assets.create", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create a new asset type.", + "example": "curl -X POST http://localhost:5001/api/assets/types -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/assets/types/", + "auth": "permission:assets.view", + "params": "path: type_id (int)", + "purpose": "Get a single asset type.", + "example": "curl http://localhost:5001/api/assets/types/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/assets/types/", + "auth": "permission:assets.edit", + "params": "path: type_id (int); body: JSON (fields not yet documented)", + "purpose": "Update an asset type's display fields (color/icon/description). The name, plugin, and table are structural and not editable here.", + "example": "curl -X PUT http://localhost:5001/api/assets/types/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + } + ] + }, + { + "surface": "core-auditlogs", + "endpoints": [ + { + "method": "GET", + "path": "/api/auditlogs", + "auth": "permission:audit.view", + "params": "not documented", + "purpose": "List audit logs with filtering and pagination.", + "example": "curl http://localhost:5001/api/auditlogs -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/auditlogs/entity//", + "auth": "permission:audit.view", + "params": "path: entitytype (string), entityid (int)", + "purpose": "Get audit history for a specific entity.", + "example": "curl http://localhost:5001/api/auditlogs/entity/x/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/auditlogs/purge", + "auth": "permission:audit.purge", + "params": "body: JSON (fields not yet documented)", + "purpose": "Delete entries older than the retention window.", + "example": "curl -X POST http://localhost:5001/api/auditlogs/purge -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/auditlogs/retention", + "auth": "permission:audit.view", + "params": "not documented", + "purpose": "Size of the log and what the configured retention would remove.", + "example": "curl http://localhost:5001/api/auditlogs/retention -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/auditlogs/stats", + "auth": "permission:audit.view", + "params": "not documented", + "purpose": "Get audit log statistics.", + "example": "curl http://localhost:5001/api/auditlogs/stats -H \"Authorization: Bearer $TOK\"" + } + ] + }, + { + "surface": "core-auth", + "endpoints": [ + { + "method": "POST", + "path": "/api/auth/change-password", + "auth": "jwt", + "params": "body: JSON (fields not yet documented)", + "purpose": "Change the authenticated user's own password.", + "example": "curl -X POST http://localhost:5001/api/auth/change-password -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/auth/login", + "auth": "none (the login surface itself; rate limited, 5-strike lockout)", + "params": "body: JSON (fields not yet documented)", + "purpose": "Authenticate user and return JWT tokens.", + "example": "curl -X POST http://localhost:5001/api/auth/login -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/auth/logout", + "auth": "jwt", + "params": "body: JSON (fields not yet documented)", + "purpose": "Logout user (for frontend token cleanup).", + "example": "curl -X POST http://localhost:5001/api/auth/logout -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/auth/me", + "auth": "jwt", + "params": "not documented", + "purpose": "Get current authenticated user info.", + "example": "curl http://localhost:5001/api/auth/me -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/auth/refresh", + "auth": "jwt (refresh token)", + "params": "body: JSON (fields not yet documented)", + "purpose": "Refresh access token using refresh token.", + "example": "curl -X POST http://localhost:5001/api/auth/refresh -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + } + ] + }, + { + "surface": "core-businessunits", + "endpoints": [ + { + "method": "GET", + "path": "/api/businessunits", + "auth": "jwt-optional (works signed out; the handler decides what shows)", + "params": "not documented", + "purpose": "List all business units.", + "example": "curl http://localhost:5001/api/businessunits -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/businessunits", + "auth": "jwt + role:admin", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create a new business unit.", + "example": "curl -X POST http://localhost:5001/api/businessunits -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/businessunits/", + "auth": "jwt + role:admin", + "params": "path: bu_id (int)", + "purpose": "Delete (deactivate) a business unit.", + "example": "curl -X DELETE http://localhost:5001/api/businessunits/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/businessunits/", + "auth": "permission:assets.view", + "params": "path: bu_id (int)", + "purpose": "Get a single business unit.", + "example": "curl http://localhost:5001/api/businessunits/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/businessunits/", + "auth": "jwt + role:admin", + "params": "path: bu_id (int); body: JSON (fields not yet documented)", + "purpose": "Update a business unit.", + "example": "curl -X PUT http://localhost:5001/api/businessunits/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + } + ] + }, + { + "surface": "core-collector", + "endpoints": [ + { + "method": "POST", + "path": "/api/collector/", + "auth": "api-key (per-plugin collector token)", + "params": "path: pluginname (string); body: JSON (fields not yet documented)", + "purpose": "Generic collector ingest for any plugin (ADR-006).", + "example": "curl -X POST http://localhost:5001/api/collector/x -H \"X-API-Key: $KEY\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/collector/_schemas", + "auth": "jwt", + "params": "not documented", + "purpose": "List collector schemas for all enabled plugins (JWT-protected).", + "example": "curl http://localhost:5001/api/collector/_schemas -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/collector/apps", + "auth": "api-key (collector token, legacy shared key)", + "params": "body: JSON (fields not yet documented)", + "purpose": "Update installed applications for a PC (matched by hostname).", + "example": "curl -X POST http://localhost:5001/api/collector/apps -H \"X-API-Key: $KEY\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/collector/bulk", + "auth": "api-key (collector token, legacy shared key)", + "params": "body: JSON (fields not yet documented)", + "purpose": "Bulk update multiple PCs at once.", + "example": "curl -X POST http://localhost:5001/api/collector/bulk -H \"X-API-Key: $KEY\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/collector/heartbeat", + "auth": "api-key (collector token, legacy shared key)", + "params": "body: JSON (fields not yet documented)", + "purpose": "Record PC online status / heartbeat (single or batch).", + "example": "curl -X POST http://localhost:5001/api/collector/heartbeat -H \"X-API-Key: $KEY\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/collector/pc", + "auth": "api-key (collector token, legacy shared key)", + "params": "body: JSON (fields not yet documented)", + "purpose": "Update one PC from the collection script (matched by hostname).", + "example": "curl -X POST http://localhost:5001/api/collector/pc -H \"X-API-Key: $KEY\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/collector/status", + "auth": "api-key (per-plugin collector token)", + "params": "not documented", + "purpose": "Check collector API status.", + "example": "curl http://localhost:5001/api/collector/status -H \"X-API-Key: $KEY\"" + } + ] + }, + { + "surface": "core-customfields", + "endpoints": [ + { + "method": "GET", + "path": "/api/customfields", + "auth": "permission:assets.view", + "params": "not documented", + "purpose": "List custom-field definitions. Filter by ?assettypeid=. ?active=false includes inactive ones.", + "example": "curl http://localhost:5001/api/customfields -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/customfields", + "auth": "jwt + role:admin", + "params": "body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: customfields.create_field has no docstring.", + "example": "curl -X POST http://localhost:5001/api/customfields -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/customfields/", + "auth": "jwt + role:admin", + "params": "path: fieldid (int)", + "purpose": "UNDOCUMENTED: customfields.delete_field has no docstring.", + "example": "curl -X DELETE http://localhost:5001/api/customfields/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/customfields/", + "auth": "jwt + role:admin", + "params": "path: fieldid (int); body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: customfields.update_field has no docstring.", + "example": "curl -X PUT http://localhost:5001/api/customfields/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/customfields/asset/", + "auth": "permission:assets.view", + "params": "path: assetid (int)", + "purpose": "Active custom fields for an asset's type, merged with its values.", + "example": "curl http://localhost:5001/api/customfields/asset/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/customfields/asset/", + "auth": "jwt + role:admin", + "params": "path: assetid (int); body: JSON (fields not yet documented)", + "purpose": "Upsert values for an asset. Body: {values: {fieldid: value, ...}}.", + "example": "curl -X PUT http://localhost:5001/api/customfields/asset/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + } + ] + }, + { + "surface": "core-dashboard", + "endpoints": [ + { + "method": "GET", + "path": "/api/dashboard", + "auth": "permission:assets.view", + "params": "not documented", + "purpose": "Get dashboard summary data (asset-based).", + "example": "curl http://localhost:5001/api/dashboard -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/dashboard/capabilities", + "auth": "jwt-optional (works signed out; the handler decides what shows)", + "params": "not documented", + "purpose": "What THIS caller can reach, for chrome that would otherwise only 401.", + "example": "curl http://localhost:5001/api/dashboard/capabilities -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/dashboard/health", + "auth": "none (reviewed: named in the public-read or mutation sweep)", + "params": "not documented", + "purpose": "Health check endpoint (no auth required).", + "example": "curl http://localhost:5001/api/dashboard/health" + }, + { + "method": "GET", + "path": "/api/dashboard/navigation", + "auth": "jwt-optional (works signed out; the handler decides what shows)", + "params": "not documented", + "purpose": "Get navigation items from all loaded plugins.", + "example": "curl http://localhost:5001/api/dashboard/navigation -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/dashboard/stats", + "auth": "permission:assets.view", + "params": "not documented", + "purpose": "Get detailed statistics by asset type.", + "example": "curl http://localhost:5001/api/dashboard/stats -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/dashboard/summary", + "auth": "permission:assets.view", + "params": "not documented", + "purpose": "Get dashboard summary data (asset-based).", + "example": "curl http://localhost:5001/api/dashboard/summary -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/dashboard/widgets", + "auth": "permission:assets.view", + "params": "not documented", + "purpose": "Aggregate dashboard widget definitions from all enabled plugins.", + "example": "curl http://localhost:5001/api/dashboard/widgets -H \"Authorization: Bearer $TOK\"" + } + ] + }, + { + "surface": "core-dashboarddefaults", + "endpoints": [ + { + "method": "GET", + "path": "/api/dashboarddefaults", + "auth": "permission:settings.view", + "params": "not documented", + "purpose": "List all visitor-IP -> business-unit mappings.", + "example": "curl http://localhost:5001/api/dashboarddefaults -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/dashboarddefaults", + "auth": "jwt + role:admin", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create a visitor-IP -> business-unit mapping.", + "example": "curl -X POST http://localhost:5001/api/dashboarddefaults -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/dashboarddefaults/", + "auth": "jwt + role:admin", + "params": "path: default_id (int)", + "purpose": "Delete (deactivate) a mapping.", + "example": "curl -X DELETE http://localhost:5001/api/dashboarddefaults/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/dashboarddefaults/", + "auth": "jwt + role:admin", + "params": "path: default_id (int); body: JSON (fields not yet documented)", + "purpose": "Update a mapping.", + "example": "curl -X PUT http://localhost:5001/api/dashboarddefaults/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/dashboarddefaults/display-role", + "auth": "none (reviewed: named in the public-read or mutation sweep)", + "params": "not documented", + "purpose": "Resolve what a single 'display' PC should show, from its own IP.", + "example": "curl http://localhost:5001/api/dashboarddefaults/display-role" + }, + { + "method": "GET", + "path": "/api/dashboarddefaults/visitor-location", + "auth": "none (reviewed: named in the public-read or mutation sweep)", + "params": "not documented", + "purpose": "Resolve the business unit for the calling display by its IP.", + "example": "curl http://localhost:5001/api/dashboarddefaults/visitor-location" + } + ] + }, + { + "surface": "core-docs", + "endpoints": [ + { + "method": "GET", + "path": "/api/docs/", + "auth": "none (reviewed: named in the public-read or mutation sweep)", + "params": "not documented", + "purpose": "Redoc page. url_for keeps the asset URLs correct under any mount.", + "example": "curl http://localhost:5001/api/docs/" + }, + { + "method": "GET", + "path": "/api/docs/llms.txt", + "auth": "none (reviewed: named in the public-read or mutation sweep)", + "params": "not documented", + "purpose": "Concise LLM-oriented API guide.", + "example": "curl http://localhost:5001/api/docs/llms.txt" + }, + { + "method": "GET", + "path": "/api/docs/openapi.json", + "auth": "none (reviewed: named in the public-read or mutation sweep)", + "params": "not documented", + "purpose": "The generated OpenAPI 3.1 spec (machine + LLM readable).", + "example": "curl http://localhost:5001/api/docs/openapi.json" + }, + { + "method": "GET", + "path": "/api/docs/redoc.standalone.js", + "auth": "none (reviewed: named in the public-read or mutation sweep)", + "params": "not documented", + "purpose": "Vendored Redoc bundle (offline).", + "example": "curl http://localhost:5001/api/docs/redoc.standalone.js" + } + ] + }, + { + "surface": "core-locations", + "endpoints": [ + { + "method": "GET", + "path": "/api/locations", + "auth": "permission:assets.view", + "params": "not documented", + "purpose": "List all locations.", + "example": "curl http://localhost:5001/api/locations -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/locations", + "auth": "jwt + role:admin", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create a new location.", + "example": "curl -X POST http://localhost:5001/api/locations -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/locations/", + "auth": "jwt + role:admin", + "params": "path: location_id (int)", + "purpose": "Delete (deactivate) a location.", + "example": "curl -X DELETE http://localhost:5001/api/locations/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/locations/", + "auth": "permission:assets.view", + "params": "path: location_id (int)", + "purpose": "Get a single location.", + "example": "curl http://localhost:5001/api/locations/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/locations/", + "auth": "jwt + role:admin", + "params": "path: location_id (int); body: JSON (fields not yet documented)", + "purpose": "Update a location.", + "example": "curl -X PUT http://localhost:5001/api/locations/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/locations/types", + "auth": "permission:assets.view", + "params": "not documented", + "purpose": "List location types. ?active=false includes inactive ones.", + "example": "curl http://localhost:5001/api/locations/types -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/locations/types", + "auth": "jwt + role:admin", + "params": "body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: locations.create_location_type has no docstring.", + "example": "curl -X POST http://localhost:5001/api/locations/types -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/locations/types/", + "auth": "jwt + role:admin", + "params": "path: type_id (int)", + "purpose": "UNDOCUMENTED: locations.delete_location_type has no docstring.", + "example": "curl -X DELETE http://localhost:5001/api/locations/types/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/locations/types/", + "auth": "jwt + role:admin", + "params": "path: type_id (int); body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: locations.update_location_type has no docstring.", + "example": "curl -X PUT http://localhost:5001/api/locations/types/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + } + ] + }, + { + "surface": "core-maplevels", + "endpoints": [ + { + "method": "GET", + "path": "/api/maplevels", + "auth": "jwt-optional (works signed out; the handler decides what shows)", + "params": "not documented", + "purpose": "Every active level, grouped by building, in display order.", + "example": "curl http://localhost:5001/api/maplevels -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/maplevels", + "auth": "jwt + role:admin", + "params": "body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: maplevels.create_level has no docstring.", + "example": "curl -X POST http://localhost:5001/api/maplevels -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/maplevels/", + "auth": "jwt + role:admin", + "params": "path: levelid (int)", + "purpose": "Deactivate a level, refusing while assets are still placed on it.", + "example": "curl -X DELETE http://localhost:5001/api/maplevels/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/maplevels/", + "auth": "jwt-optional (works signed out; the handler decides what shows)", + "params": "path: levelid (int)", + "purpose": "UNDOCUMENTED: maplevels.get_level has no docstring.", + "example": "curl http://localhost:5001/api/maplevels/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PATCH|PUT", + "path": "/api/maplevels/", + "auth": "jwt + role:admin", + "params": "path: levelid (int); body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: maplevels.update_level has no docstring.", + "example": "curl -X PATCH http://localhost:5001/api/maplevels/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/maplevels//blueprint", + "auth": "jwt + role:admin", + "params": "path: levelid (int); body: JSON (fields not yet documented)", + "purpose": "Upload this level's blueprint for one theme.", + "example": "curl -X POST http://localhost:5001/api/maplevels/1/blueprint -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/maplevels//blueprint/", + "auth": "jwt-optional (works signed out; the handler decides what shows)", + "params": "path: levelid (int), filename (path)", + "purpose": "Serve a level's blueprint image. Public, for the same reason as above.", + "example": "curl http://localhost:5001/api/maplevels/1/blueprint/example.png -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/maplevels/buildings", + "auth": "jwt + role:admin", + "params": "body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: maplevels.create_building has no docstring.", + "example": "curl -X POST http://localhost:5001/api/maplevels/buildings -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "PATCH|PUT", + "path": "/api/maplevels/buildings/", + "auth": "jwt + role:admin", + "params": "path: buildingid (int); body: JSON (fields not yet documented)", + "purpose": "Rename or reorder a building. Levels move with it; nothing repositions.", + "example": "curl -X PATCH http://localhost:5001/api/maplevels/buildings/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + } + ] + }, + { + "surface": "core-mappositions", + "endpoints": [ + { + "method": "POST", + "path": "/api/mappositions/positions", + "auth": "permission:assets.edit", + "params": "body: JSON (fields not yet documented)", + "purpose": "Set positions for many assets at once.", + "example": "curl -X POST http://localhost:5001/api/mappositions/positions -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/mappositions/snapshots", + "auth": "permission:assets.view", + "params": "not documented", + "purpose": "Snapshots, newest first. Metadata only - the positions are large.", + "example": "curl http://localhost:5001/api/mappositions/snapshots -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/mappositions/snapshots//restore", + "auth": "permission:assets.edit", + "params": "path: snapshotid (int); body: JSON (fields not yet documented)", + "purpose": "Put the positions in this snapshot back.", + "example": "curl -X POST http://localhost:5001/api/mappositions/snapshots/1/restore -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/mappositions/transform", + "auth": "permission:assets.edit", + "params": "body: JSON (fields not yet documented)", + "purpose": "Move every placed marker on a level by a transform read off landmarks.", + "example": "curl -X POST http://localhost:5001/api/mappositions/transform -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/mappositions/verify", + "auth": "permission:assets.edit", + "params": "body: JSON (fields not yet documented)", + "purpose": "Mark markers as reviewed against the current drawing without moving them.", + "example": "curl -X POST http://localhost:5001/api/mappositions/verify -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + } + ] + }, + { + "surface": "core-models", + "endpoints": [ + { + "method": "GET", + "path": "/api/models", + "auth": "permission:assets.view", + "params": "not documented", + "purpose": "List all vendor catalog models.", + "example": "curl http://localhost:5001/api/models -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/models", + "auth": "jwt + role:admin", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create a new model.", + "example": "curl -X POST http://localhost:5001/api/models -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/models/", + "auth": "jwt + role:admin", + "params": "path: model_id (int)", + "purpose": "Delete (deactivate) a model.", + "example": "curl -X DELETE http://localhost:5001/api/models/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/models/", + "auth": "permission:assets.view", + "params": "path: model_id (int)", + "purpose": "Get a single model.", + "example": "curl http://localhost:5001/api/models/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/models/", + "auth": "jwt + role:admin", + "params": "path: model_id (int); body: JSON (fields not yet documented)", + "purpose": "Update a model.", + "example": "curl -X PUT http://localhost:5001/api/models/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/models//image", + "auth": "jwt + role:admin", + "params": "path: model_id (int)", + "purpose": "Clear a model image and delete the uploaded file if we own it.", + "example": "curl -X DELETE http://localhost:5001/api/models/1/image -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/models//image", + "auth": "jwt + role:admin", + "params": "path: model_id (int); body: JSON (fields not yet documented)", + "purpose": "Upload (or replace) the photo for a model.", + "example": "curl -X POST http://localhost:5001/api/models/1/image -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/models/image/", + "auth": "none (reviewed: named in the public-read or mutation sweep)", + "params": "path: filename (path)", + "purpose": "Serve an uploaded model image (public - asset detail pages read it).", + "example": "curl http://localhost:5001/api/models/image/example.png" + } + ] + }, + { + "surface": "core-modeltypes", + "endpoints": [ + { + "method": "GET", + "path": "/api/modeltypes", + "auth": "permission:assets.view", + "params": "not documented", + "purpose": "List all model types with optional filtering.", + "example": "curl http://localhost:5001/api/modeltypes -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/modeltypes", + "auth": "jwt + role:admin", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create a new model type.", + "example": "curl -X POST http://localhost:5001/api/modeltypes -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/modeltypes/", + "auth": "jwt + role:admin", + "params": "path: type_id (int)", + "purpose": "Delete (deactivate) a model type.", + "example": "curl -X DELETE http://localhost:5001/api/modeltypes/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/modeltypes/", + "auth": "permission:assets.view", + "params": "path: type_id (int)", + "purpose": "Get a single model type.", + "example": "curl http://localhost:5001/api/modeltypes/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/modeltypes/", + "auth": "jwt + role:admin", + "params": "path: type_id (int); body: JSON (fields not yet documented)", + "purpose": "Update a model type.", + "example": "curl -X PUT http://localhost:5001/api/modeltypes/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + } + ] + }, + { + "surface": "core-operatingsystems", + "endpoints": [ + { + "method": "GET", + "path": "/api/operatingsystems", + "auth": "permission:assets.view", + "params": "not documented", + "purpose": "List all operating systems.", + "example": "curl http://localhost:5001/api/operatingsystems -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/operatingsystems", + "auth": "jwt + role:admin", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create a new operating system.", + "example": "curl -X POST http://localhost:5001/api/operatingsystems -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/operatingsystems/", + "auth": "jwt + role:admin", + "params": "path: os_id (int)", + "purpose": "Delete (deactivate) an operating system.", + "example": "curl -X DELETE http://localhost:5001/api/operatingsystems/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/operatingsystems/", + "auth": "permission:assets.view", + "params": "path: os_id (int)", + "purpose": "Get a single operating system.", + "example": "curl http://localhost:5001/api/operatingsystems/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/operatingsystems/", + "auth": "jwt + role:admin", + "params": "path: os_id (int); body: JSON (fields not yet documented)", + "purpose": "Update an operating system.", + "example": "curl -X PUT http://localhost:5001/api/operatingsystems/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + } + ] + }, + { + "surface": "core-plugins", + "endpoints": [ + { + "method": "GET", + "path": "/api/plugins", + "auth": "permission:settings.view", + "params": "not documented", + "purpose": "List all discovered plugins (enabled or not) with their metadata and the framework contract version.", + "example": "curl http://localhost:5001/api/plugins -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/plugins/", + "auth": "jwt + role:admin", + "params": "path: name (string); body: JSON (fields not yet documented)", + "purpose": "Enable or disable a plugin. Takes effect on the next app restart for route/navigation changes.", + "example": "curl -X PUT http://localhost:5001/api/plugins/x -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/plugins/enabled", + "auth": "jwt-optional (works signed out; the handler decides what shows)", + "params": "not documented", + "purpose": "Return just the names of enabled plugins as a flat array.", + "example": "curl http://localhost:5001/api/plugins/enabled -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/plugins/settings-routes", + "auth": "permission:settings.view", + "params": "not documented", + "purpose": "Return settings routes contributed by all enabled plugins.", + "example": "curl http://localhost:5001/api/plugins/settings-routes -H \"Authorization: Bearer $TOK\"" + } + ] + }, + { + "surface": "core-pluginui", + "endpoints": [ + { + "method": "GET", + "path": "/api/pluginui/asset-panels", + "auth": "permission:assets.view", + "params": "not documented", + "purpose": "Return the asset-detail panels matching one asset's type (get_asset_panels).", + "example": "curl http://localhost:5001/api/pluginui/asset-panels -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/pluginui/asset-presentation", + "auth": "permission:assets.view", + "params": "not documented", + "purpose": "Merge enabled plugins' asset-type presentation entries (hook).", + "example": "curl http://localhost:5001/api/pluginui/asset-presentation -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/pluginui/asset-type-descriptors", + "auth": "permission:assets.view", + "params": "not documented", + "purpose": "Merge enabled plugins' asset-type subtype descriptors.", + "example": "curl http://localhost:5001/api/pluginui/asset-type-descriptors -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/pluginui/core-type-descriptors", + "auth": "permission:assets.view", + "params": "not documented", + "purpose": "Return type descriptors for core-owned non-asset types.", + "example": "curl http://localhost:5001/api/pluginui/core-type-descriptors -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/pluginui/map-overlays", + "auth": "permission:assets.view", + "params": "not documented", + "purpose": "Merge enabled plugins' map overlay declarations (get_map_overlays hook).", + "example": "curl http://localhost:5001/api/pluginui/map-overlays -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/pluginui/settings-cards", + "auth": "permission:assets.view", + "params": "not documented", + "purpose": "Merge enabled plugins' settings-catalog cards (get_settings_cards hook).", + "example": "curl http://localhost:5001/api/pluginui/settings-cards -H \"Authorization: Bearer $TOK\"" + } + ] + }, + { + "surface": "core-reports", + "endpoints": [ + { + "method": "GET", + "path": "/api/reports", + "auth": "permission:reports.view", + "params": "not documented", + "purpose": "List all available reports.", + "example": "curl http://localhost:5001/api/reports -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/reports/asset-inventory", + "auth": "permission:reports.view", + "params": "not documented", + "purpose": "Report: Complete asset inventory summary.", + "example": "curl http://localhost:5001/api/reports/asset-inventory -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/reports/assets-by-status", + "auth": "permission:reports.view", + "params": "not documented", + "purpose": "Report: Asset count grouped by status.", + "example": "curl http://localhost:5001/api/reports/assets-by-status -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/reports/email", + "auth": "permission:reports.export", + "params": "body: JSON (fields not yet documented)", + "purpose": "Email a report's data as an HTML table (on-demand delivery).", + "example": "curl -X POST http://localhost:5001/api/reports/email -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/reports/kb-popularity", + "auth": "permission:reports.view", + "params": "not documented", + "purpose": "Report: Most clicked knowledge base articles.", + "example": "curl http://localhost:5001/api/reports/kb-popularity -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/reports/machines-by-type", + "auth": "permission:reports.view", + "params": "not documented", + "purpose": "Report: Machine count grouped by machine type.", + "example": "curl http://localhost:5001/api/reports/machines-by-type -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/reports/pc-relationships", + "auth": "permission:reports.view", + "params": "not documented", + "purpose": "Report: PCs with relationships to shop floor machines.", + "example": "curl http://localhost:5001/api/reports/pc-relationships -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/reports/software-compliance", + "auth": "permission:reports.view", + "params": "not documented", + "purpose": "Report: Required applications vs installed (per PC).", + "example": "curl http://localhost:5001/api/reports/software-compliance -H \"Authorization: Bearer $TOK\"" + } + ] + }, + { + "surface": "core-scripts", + "endpoints": [ + { + "method": "GET", + "path": "/api/scripts", + "auth": "permission:endpoints.runscript", + "params": "not documented", + "purpose": "List scripts (active by default).", + "example": "curl http://localhost:5001/api/scripts -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/scripts", + "auth": "permission:endpoints.runscript", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create a script catalog entry (body uploaded separately).", + "example": "curl -X POST http://localhost:5001/api/scripts -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/scripts/", + "auth": "permission:endpoints.runscript", + "params": "path: script_id (int)", + "purpose": "UNDOCUMENTED: scripts.delete_script has no docstring.", + "example": "curl -X DELETE http://localhost:5001/api/scripts/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/scripts/", + "auth": "permission:endpoints.runscript", + "params": "path: script_id (int)", + "purpose": "UNDOCUMENTED: scripts.get_script has no docstring.", + "example": "curl http://localhost:5001/api/scripts/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/scripts/", + "auth": "permission:endpoints.runscript", + "params": "path: script_id (int); body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: scripts.update_script has no docstring.", + "example": "curl -X PUT http://localhost:5001/api/scripts/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/scripts//body", + "auth": "permission:endpoints.runscript", + "params": "path: script_id (int); body: JSON (fields not yet documented)", + "purpose": "Upload (or replace) the .ps1 body for a script.", + "example": "curl -X POST http://localhost:5001/api/scripts/1/body -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/scripts/body/", + "auth": "permission:endpoints.runscript", + "params": "path: filename (path)", + "purpose": "Operator-facing script body download (JWT/RBAC).", + "example": "curl http://localhost:5001/api/scripts/body/example.png -H \"Authorization: Bearer $TOK\"" + } + ] + }, + { + "surface": "core-search", + "endpoints": [ + { + "method": "GET", + "path": "/api/search", + "auth": "permission:assets.view", + "params": "not documented", + "purpose": "Global search across multiple entity types.", + "example": "curl http://localhost:5001/api/search -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/search/suggest", + "auth": "permission:assets.view", + "params": "not documented", + "purpose": "A better spelling for a query that found nothing, or null.", + "example": "curl http://localhost:5001/api/search/suggest -H \"Authorization: Bearer $TOK\"" + } + ] + }, + { + "surface": "core-settings", + "endpoints": [ + { + "method": "GET", + "path": "/api/settings", + "auth": "jwt-optional (works signed out; the handler decides what shows)", + "params": "not documented", + "purpose": "List all settings, optionally filtered by category.", + "example": "curl http://localhost:5001/api/settings -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/settings", + "auth": "permission:settings.edit", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create a new setting (admin only).", + "example": "curl -X POST http://localhost:5001/api/settings -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/settings/", + "auth": "jwt-optional (works signed out; the handler decides what shows)", + "params": "path: key (string)", + "purpose": "Get a single setting by key.", + "example": "curl http://localhost:5001/api/settings/x -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/settings/", + "auth": "permission:settings.edit", + "params": "path: key (string); body: JSON (fields not yet documented)", + "purpose": "Update a setting value.", + "example": "curl -X PUT http://localhost:5001/api/settings/x -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/settings/asset-import/import", + "auth": "jwt + role:admin", + "params": "body: JSON (fields not yet documented)", + "purpose": "Validate uploaded CSVs and, when commit=true, load them.", + "example": "curl -X POST http://localhost:5001/api/settings/asset-import/import -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/settings/asset-import/script", + "auth": "jwt + role:admin", + "params": "not documented", + "purpose": "Download Collect-ShopDB.ps1 with this site's CSV path embedded.", + "example": "curl http://localhost:5001/api/settings/asset-import/script -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/settings/asset-import/templates", + "auth": "jwt + role:admin", + "params": "not documented", + "purpose": "List the importable CSV tables in the order they must be uploaded.", + "example": "curl http://localhost:5001/api/settings/asset-import/templates -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/settings/asset-import/templates/", + "auth": "jwt + role:admin", + "params": "path: tablename (string)", + "purpose": "Download a generated CSV template for a single importable table.", + "example": "curl http://localhost:5001/api/settings/asset-import/templates/x -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/settings/asset-import/winc/batches", + "auth": "jwt + role:admin", + "params": "not documented", + "purpose": "List applied/undone WINC import batches, newest first (for the undo UI).", + "example": "curl http://localhost:5001/api/settings/asset-import/winc/batches -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/settings/asset-import/winc/commit", + "auth": "jwt + role:admin", + "params": "body: JSON (fields not yet documented)", + "purpose": "Apply a WINC plan reversibly. Requires acknowledgment for needs-create.", + "example": "curl -X POST http://localhost:5001/api/settings/asset-import/winc/commit -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/settings/asset-import/winc/preview", + "auth": "jwt + role:admin", + "params": "body: JSON (fields not yet documented)", + "purpose": "Read-only WINC analysis: report what a commit would do. Writes nothing.", + "example": "curl -X POST http://localhost:5001/api/settings/asset-import/winc/preview -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/settings/asset-import/winc/undo", + "auth": "jwt + role:admin", + "params": "body: JSON (fields not yet documented)", + "purpose": "Reverse an applied WINC import batch by id. Conservative and reversible.", + "example": "curl -X POST http://localhost:5001/api/settings/asset-import/winc/undo -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/settings/branding-logo", + "auth": "jwt + role:admin", + "params": "body: JSON (fields not yet documented)", + "purpose": "Upload a branding logo image and point the matching setting at it.", + "example": "curl -X POST http://localhost:5001/api/settings/branding-logo -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/settings/branding/", + "auth": "none (reviewed: named in the public-read or mutation sweep)", + "params": "path: filename (path)", + "purpose": "Serve an uploaded branding logo (public - kiosks/print pages read it).", + "example": "curl http://localhost:5001/api/settings/branding/example.png" + }, + { + "method": "POST", + "path": "/api/settings/map-blueprint", + "auth": "jwt + role:admin", + "params": "body: JSON (fields not yet documented)", + "purpose": "Upload a floor-map blueprint image and point the setting at it.", + "example": "curl -X POST http://localhost:5001/api/settings/map-blueprint -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/settings/map-blueprint/", + "auth": "none (reviewed: named in the public-read or mutation sweep)", + "params": "path: filename (path)", + "purpose": "Serve an uploaded blueprint image (public - kiosks read it).", + "example": "curl http://localhost:5001/api/settings/map-blueprint/example.png" + }, + { + "method": "POST", + "path": "/api/settings/seed", + "auth": "permission:settings.edit", + "params": "body: JSON (fields not yet documented)", + "purpose": "Seed default settings if they don't exist.", + "example": "curl -X POST http://localhost:5001/api/settings/seed -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/settings/test-email", + "auth": "permission:settings.edit", + "params": "body: JSON (fields not yet documented)", + "purpose": "Send a test email to verify SMTP configuration.", + "example": "curl -X POST http://localhost:5001/api/settings/test-email -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + } + ] + }, + { + "surface": "core-setup", + "endpoints": [ + { + "method": "POST", + "path": "/api/setup/complete", + "auth": "jwt + role:admin", + "params": "body: JSON (fields not yet documented)", + "purpose": "Flag the first-run setup as finished.", + "example": "curl -X POST http://localhost:5001/api/setup/complete -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/setup/create-admin", + "auth": "none (first run only: refused once an admin exists)", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create the first admin. Only works while there are zero users, setup has not been marked complete, and the caller proves operator control (setup token or a loopback request).", + "example": "curl -X POST http://localhost:5001/api/setup/create-admin -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/setup/needs-admin", + "auth": "none (reviewed: named in the public-read or mutation sweep)", + "params": "not documented", + "purpose": "True when no users exist yet - the app's very first run. Public so the login screen can offer to create the first admin.", + "example": "curl http://localhost:5001/api/setup/needs-admin" + }, + { + "method": "POST", + "path": "/api/setup/seed-reference", + "auth": "jwt + role:admin", + "params": "body: JSON (fields not yet documented)", + "purpose": "Seed core reference data + permissions + default settings. Idempotent - reuses the same routines as the `flask seed ...` CLI commands.", + "example": "curl -X POST http://localhost:5001/api/setup/seed-reference -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/setup/seed-starter", + "auth": "jwt + role:admin", + "params": "body: JSON (fields not yet documented)", + "purpose": "Add common vendors that are not already present. Idempotent.", + "example": "curl -X POST http://localhost:5001/api/setup/seed-starter -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + } + ] + }, + { + "surface": "core-shopdbbackups", + "endpoints": [ + { + "method": "GET", + "path": "/api/shopdbbackups", + "auth": "jwt + role:admin", + "params": "not documented", + "purpose": "List every retained backup set, newest first, with a compatibility flag.", + "example": "curl http://localhost:5001/api/shopdbbackups -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/shopdbbackups", + "auth": "jwt + role:admin", + "params": "body: JSON (fields not yet documented)", + "purpose": "Kick off an on-demand backup run.", + "example": "curl -X POST http://localhost:5001/api/shopdbbackups -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/shopdbbackups/", + "auth": "jwt + role:admin", + "params": "path: snapshotid (string)", + "purpose": "Return one set's full manifest plus a resolved compatibility verdict.", + "example": "curl http://localhost:5001/api/shopdbbackups/x -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/shopdbbackups//restore", + "auth": "jwt + role:admin", + "params": "path: snapshotid (string); body: JSON (fields not yet documented)", + "purpose": "Queue a restore of a set, gated on schema compatibility.", + "example": "curl -X POST http://localhost:5001/api/shopdbbackups/x/restore -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/shopdbbackups/finalize-restore", + "auth": "jwt + role:admin", + "params": "body: JSON (fields not yet documented)", + "purpose": "Run the post-restore schema upgrade in the app context.", + "example": "curl -X POST http://localhost:5001/api/shopdbbackups/finalize-restore -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/shopdbbackups/history", + "auth": "jwt + role:admin", + "params": "not documented", + "purpose": "Return the recorded history of backup and restore runs, newest first.", + "example": "curl http://localhost:5001/api/shopdbbackups/history -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/shopdbbackups/settings", + "auth": "jwt + role:admin", + "params": "not documented", + "purpose": "Return destination + retention config for the settings pane.", + "example": "curl http://localhost:5001/api/shopdbbackups/settings -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/shopdbbackups/settings", + "auth": "jwt + role:admin", + "params": "body: JSON (fields not yet documented)", + "purpose": "Persist the backup destination kind and its credential/config fields.", + "example": "curl -X PUT http://localhost:5001/api/shopdbbackups/settings -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/shopdbbackups/status", + "auth": "jwt + role:admin", + "params": "not documented", + "purpose": "Return the live status of the most recent backup and restore runs.", + "example": "curl http://localhost:5001/api/shopdbbackups/status -H \"Authorization: Bearer $TOK\"" + } + ] + }, + { + "surface": "core-supportteams", + "endpoints": [ + { + "method": "GET", + "path": "/api/supportteams", + "auth": "permission:assets.view", + "params": "not documented", + "purpose": "List support teams (with contacts). ?active and ?teamname filters.", + "example": "curl http://localhost:5001/api/supportteams -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/supportteams", + "auth": "jwt + role:admin", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create a new support team.", + "example": "curl -X POST http://localhost:5001/api/supportteams -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/supportteams/", + "auth": "jwt + role:admin", + "params": "path: team_id (int)", + "purpose": "Delete a support team; 409 while any application references it.", + "example": "curl -X DELETE http://localhost:5001/api/supportteams/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/supportteams/", + "auth": "permission:assets.view", + "params": "path: team_id (int)", + "purpose": "Get a single support team with its contacts.", + "example": "curl http://localhost:5001/api/supportteams/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/supportteams/", + "auth": "jwt + role:admin", + "params": "path: team_id (int); body: JSON (fields not yet documented)", + "purpose": "Update a support team.", + "example": "curl -X PUT http://localhost:5001/api/supportteams/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/supportteams//contacts", + "auth": "jwt + role:admin", + "params": "path: team_id (int); body: JSON (fields not yet documented)", + "purpose": "Add a contact to a support team.", + "example": "curl -X POST http://localhost:5001/api/supportteams/1/contacts -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/supportteams//contacts/", + "auth": "jwt + role:admin", + "params": "path: team_id (int), contact_id (int)", + "purpose": "Delete a support-team contact.", + "example": "curl -X DELETE http://localhost:5001/api/supportteams/1/contacts/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/supportteams//contacts/", + "auth": "jwt + role:admin", + "params": "path: team_id (int), contact_id (int); body: JSON (fields not yet documented)", + "purpose": "Update a support-team contact.", + "example": "curl -X PUT http://localhost:5001/api/supportteams/1/contacts/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + } + ] + }, + { + "surface": "core-users", + "endpoints": [ + { + "method": "GET", + "path": "/api/users", + "auth": "jwt + role:admin", + "params": "not documented", + "purpose": "List all users.", + "example": "curl http://localhost:5001/api/users -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/users", + "auth": "jwt + role:admin", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create a new user.", + "example": "curl -X POST http://localhost:5001/api/users -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/users/", + "auth": "jwt + role:admin", + "params": "path: userid (int)", + "purpose": "Delete a user.", + "example": "curl -X DELETE http://localhost:5001/api/users/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/users/", + "auth": "jwt", + "params": "path: userid (int)", + "purpose": "Get a single user.", + "example": "curl http://localhost:5001/api/users/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/users/", + "auth": "jwt", + "params": "path: userid (int); body: JSON (fields not yet documented)", + "purpose": "Update a user.", + "example": "curl -X PUT http://localhost:5001/api/users/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/users/permissions", + "auth": "jwt", + "params": "not documented", + "purpose": "List assignable permissions grouped by category.", + "example": "curl http://localhost:5001/api/users/permissions -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/users/roles", + "auth": "jwt", + "params": "not documented", + "purpose": "List all roles with their permissions.", + "example": "curl http://localhost:5001/api/users/roles -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/users/roles", + "auth": "jwt + role:admin", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create a new role.", + "example": "curl -X POST http://localhost:5001/api/users/roles -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/users/roles/", + "auth": "jwt + role:admin", + "params": "path: roleid (int)", + "purpose": "Delete a role.", + "example": "curl -X DELETE http://localhost:5001/api/users/roles/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/users/roles/", + "auth": "jwt + role:admin", + "params": "path: roleid (int); body: JSON (fields not yet documented)", + "purpose": "Update a role.", + "example": "curl -X PUT http://localhost:5001/api/users/roles/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + } + ] + }, + { + "surface": "core-vendors", + "endpoints": [ + { + "method": "GET", + "path": "/api/vendors", + "auth": "permission:assets.view", + "params": "not documented", + "purpose": "List all vendors.", + "example": "curl http://localhost:5001/api/vendors -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/vendors", + "auth": "jwt + role:admin", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create a new vendor.", + "example": "curl -X POST http://localhost:5001/api/vendors -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/vendors/", + "auth": "jwt + role:admin", + "params": "path: vendor_id (int)", + "purpose": "Delete (deactivate) a vendor.", + "example": "curl -X DELETE http://localhost:5001/api/vendors/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/vendors/", + "auth": "permission:assets.view", + "params": "path: vendor_id (int)", + "purpose": "Get a single vendor.", + "example": "curl http://localhost:5001/api/vendors/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/vendors/", + "auth": "jwt + role:admin", + "params": "path: vendor_id (int); body: JSON (fields not yet documented)", + "purpose": "Update a vendor.", + "example": "curl -X PUT http://localhost:5001/api/vendors/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + } + ] + }, + { + "surface": "plugin-backups", + "endpoints": [ + { + "method": "GET", + "path": "/api/backups/asset/", + "auth": "permission:backups.view", + "params": "path: assetid (int)", + "purpose": "Revision history for one asset, newest first.", + "example": "curl http://localhost:5001/api/backups/asset/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/backups/asset//info", + "auth": "permission:backups.view", + "params": "path: assetid (int)", + "purpose": "A kind's 'at a glance' card, built from its LATEST revision.", + "example": "curl http://localhost:5001/api/backups/asset/1/info -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/backups/dashboard/stale", + "auth": "permission:backups.view", + "params": "not documented", + "purpose": "Chains whose backup has stopped running.", + "example": "curl http://localhost:5001/api/backups/dashboard/stale -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/backups/kinds", + "auth": "permission:backups.view", + "params": "not documented", + "purpose": "Registered backup kinds, for UI that needs to enumerate them.", + "example": "curl http://localhost:5001/api/backups/kinds -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/backups/revisions/", + "auth": "permission:backups.view", + "params": "path: backuprevisionid (int)", + "purpose": "One revision, including its parsed payload where there is one.", + "example": "curl http://localhost:5001/api/backups/revisions/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/backups/revisions//diff", + "auth": "permission:backups.view", + "params": "path: backuprevisionid (int)", + "purpose": "Diff two revisions of the same asset+kind.", + "example": "curl http://localhost:5001/api/backups/revisions/1/diff -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/backups/revisions//download", + "auth": "permission:backups.download", + "params": "path: backuprevisionid (int)", + "purpose": "Render a revision back to its native file format.", + "example": "curl http://localhost:5001/api/backups/revisions/1/download -H \"Authorization: Bearer $TOK\"" + } + ] + }, + { + "surface": "plugin-computers", + "endpoints": [ + { + "method": "GET", + "path": "/api/computers", + "auth": "permission:computers.view", + "params": "not documented", + "purpose": "List all computers with filtering and pagination.", + "example": "curl http://localhost:5001/api/computers -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/computers", + "auth": "permission:computers.create", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create new computer (creates both Asset and Computer records).", + "example": "curl -X POST http://localhost:5001/api/computers -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/computers/", + "auth": "permission:computers.delete", + "params": "path: computer_id (int)", + "purpose": "Delete (soft delete) computer.", + "example": "curl -X DELETE http://localhost:5001/api/computers/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/computers/", + "auth": "permission:computers.view", + "params": "path: computer_id (int)", + "purpose": "Get a single computer with full details.", + "example": "curl http://localhost:5001/api/computers/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/computers/", + "auth": "permission:computers.edit", + "params": "path: computer_id (int); body: JSON (fields not yet documented)", + "purpose": "Update computer (both Asset and Computer records).", + "example": "curl -X PUT http://localhost:5001/api/computers/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/computers//apps", + "auth": "permission:computers.view", + "params": "path: computer_id (int)", + "purpose": "Get all installed applications for a computer.", + "example": "curl http://localhost:5001/api/computers/1/apps -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/computers//apps", + "auth": "permission:computers.create", + "params": "path: computer_id (int); body: JSON (fields not yet documented)", + "purpose": "Add an installed application to a computer.", + "example": "curl -X POST http://localhost:5001/api/computers/1/apps -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/computers//apps/", + "auth": "permission:computers.delete", + "params": "path: computer_id (int), app_id (int)", + "purpose": "Remove an installed application from a computer.", + "example": "curl -X DELETE http://localhost:5001/api/computers/1/apps/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/computers//report", + "auth": "permission:computers.create", + "params": "path: computer_id (int); body: JSON (fields not yet documented)", + "purpose": "Report computer status (for agent-based reporting).", + "example": "curl -X POST http://localhost:5001/api/computers/1/report -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/computers/asset//policypanel", + "auth": "permission:computers.view", + "params": "path: asset_id (int)", + "purpose": "Keyvalue panel: which policy governs this endpoint, and what it allows.", + "example": "curl http://localhost:5001/api/computers/asset/1/policypanel -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/computers/by-asset/", + "auth": "permission:computers.view", + "params": "path: asset_id (int)", + "purpose": "Get computer data by asset ID.", + "example": "curl http://localhost:5001/api/computers/by-asset/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/computers/by-hostname/", + "auth": "permission:computers.view", + "params": "path: hostname (string)", + "purpose": "Get computer by hostname.", + "example": "curl http://localhost:5001/api/computers/by-hostname/x -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/computers/client-script", + "auth": "jwt + role:admin", + "params": "not documented", + "purpose": "The collector reporter, stamped with THIS site's values.", + "example": "curl http://localhost:5001/api/computers/client-script -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/computers/dashboard/quiet", + "auth": "permission:computers.view", + "params": "not documented", + "purpose": "PCs that WERE reporting, have stopped, and are still meant to be in use.", + "example": "curl http://localhost:5001/api/computers/dashboard/quiet -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/computers/dashboard/sharedmachines", + "auth": "permission:computers.view", + "params": "not documented", + "purpose": "Machine numbers claimed by more than one PC, with nothing filed under them.", + "example": "curl http://localhost:5001/api/computers/dashboard/sharedmachines -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/computers/dashboard/summary", + "auth": "permission:computers.view", + "params": "not documented", + "purpose": "Get computer dashboard summary data.", + "example": "curl http://localhost:5001/api/computers/dashboard/summary -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/computers/display-kiosks", + "auth": "permission:computers.view", + "params": "not documented", + "purpose": "Reporting display kiosks, for the Dashboard Defaults picker.", + "example": "curl http://localhost:5001/api/computers/display-kiosks -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/computers/policies/actions", + "auth": "permission:computers.view", + "params": "not documented", + "purpose": "The allowlisted action names + labels a policy may reference.", + "example": "curl http://localhost:5001/api/computers/policies/actions -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/computers/policies/business", + "auth": "permission:computers.view", + "params": "not documented", + "purpose": "UNDOCUMENTED: computers.list_business_policies has no docstring.", + "example": "curl http://localhost:5001/api/computers/policies/business -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/computers/policies/business", + "auth": "permission:computers.create", + "params": "body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: computers.create_business_policy has no docstring.", + "example": "curl -X POST http://localhost:5001/api/computers/policies/business -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/computers/policies/business/", + "auth": "permission:computers.delete", + "params": "path: policy_id (int)", + "purpose": "UNDOCUMENTED: computers.delete_business_policy has no docstring.", + "example": "curl -X DELETE http://localhost:5001/api/computers/policies/business/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/computers/policies/business/", + "auth": "permission:computers.view", + "params": "path: policy_id (int)", + "purpose": "UNDOCUMENTED: computers.get_business_policy has no docstring.", + "example": "curl http://localhost:5001/api/computers/policies/business/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/computers/policies/business/", + "auth": "permission:computers.edit", + "params": "path: policy_id (int); body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: computers.update_business_policy has no docstring.", + "example": "curl -X PUT http://localhost:5001/api/computers/policies/business/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/computers/policies/configuration", + "auth": "permission:computers.view", + "params": "not documented", + "purpose": "UNDOCUMENTED: computers.list_configuration_policies has no docstring.", + "example": "curl http://localhost:5001/api/computers/policies/configuration -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/computers/policies/configuration", + "auth": "permission:computers.create", + "params": "body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: computers.create_configuration_policy has no docstring.", + "example": "curl -X POST http://localhost:5001/api/computers/policies/configuration -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/computers/policies/configuration/", + "auth": "permission:computers.delete", + "params": "path: policy_id (int)", + "purpose": "UNDOCUMENTED: computers.delete_configuration_policy has no docstring.", + "example": "curl -X DELETE http://localhost:5001/api/computers/policies/configuration/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/computers/policies/configuration/", + "auth": "permission:computers.view", + "params": "path: policy_id (int)", + "purpose": "UNDOCUMENTED: computers.get_configuration_policy has no docstring.", + "example": "curl http://localhost:5001/api/computers/policies/configuration/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/computers/policies/configuration/", + "auth": "permission:computers.edit", + "params": "path: policy_id (int); body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: computers.update_configuration_policy has no docstring.", + "example": "curl -X PUT http://localhost:5001/api/computers/policies/configuration/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/computers/policies/effective/", + "auth": "permission:computers.view", + "params": "path: asset_id (int)", + "purpose": "Preview the resolved effective policy for one endpoint (debug/UI).", + "example": "curl http://localhost:5001/api/computers/policies/effective/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/computers/protocols", + "auth": "permission:computers.view", + "params": "not documented", + "purpose": "List access protocols. ?active=false includes disabled ones.", + "example": "curl http://localhost:5001/api/computers/protocols -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/computers/protocols", + "auth": "permission:computers.edit", + "params": "body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: computers.create_protocol has no docstring.", + "example": "curl -X POST http://localhost:5001/api/computers/protocols -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/computers/protocols/", + "auth": "permission:computers.edit", + "params": "path: protocol_id (int)", + "purpose": "UNDOCUMENTED: computers.delete_protocol has no docstring.", + "example": "curl -X DELETE http://localhost:5001/api/computers/protocols/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PATCH|PUT", + "path": "/api/computers/protocols/", + "auth": "permission:computers.edit", + "params": "path: protocol_id (int); body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: computers.update_protocol has no docstring.", + "example": "curl -X PATCH http://localhost:5001/api/computers/protocols/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/computers/types", + "auth": "permission:computers.view", + "params": "not documented", + "purpose": "List all computer types.", + "example": "curl http://localhost:5001/api/computers/types -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/computers/types", + "auth": "permission:computers.create", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create a new computer type.", + "example": "curl -X POST http://localhost:5001/api/computers/types -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/computers/types/", + "auth": "permission:computers.delete", + "params": "path: type_id (int)", + "purpose": "Delete a computer type. Refused if any PC still uses it.", + "example": "curl -X DELETE http://localhost:5001/api/computers/types/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/computers/types/", + "auth": "permission:computers.view", + "params": "path: type_id (int)", + "purpose": "Get a single computer type.", + "example": "curl http://localhost:5001/api/computers/types/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/computers/types/", + "auth": "permission:computers.edit", + "params": "path: type_id (int); body: JSON (fields not yet documented)", + "purpose": "Update a computer type.", + "example": "curl -X PUT http://localhost:5001/api/computers/types/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + } + ] + }, + { + "surface": "plugin-employees", + "endpoints": [ + { + "method": "DELETE", + "path": "/api/employees//photo", + "auth": "jwt + role:admin", + "params": "path: sso (int)", + "purpose": "Clear an employee photo and delete the uploaded file. External mode 409s.", + "example": "curl -X DELETE http://localhost:5001/api/employees/1/photo -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/employees//photo", + "auth": "jwt + role:admin", + "params": "path: sso (int); body: JSON (fields not yet documented)", + "purpose": "Upload (or replace) the photo for a self-hosted directory employee.", + "example": "curl -X POST http://localhost:5001/api/employees/1/photo -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/employees/directory", + "auth": "permission:employees.view", + "params": "not documented", + "purpose": "Full self-hosted directory (for the management page).", + "example": "curl http://localhost:5001/api/employees/directory -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/employees/directory", + "auth": "jwt + role:admin", + "params": "body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: employees.create_directory_employee has no docstring.", + "example": "curl -X POST http://localhost:5001/api/employees/directory -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/employees/directory/", + "auth": "jwt + role:admin", + "params": "path: sso (int)", + "purpose": "UNDOCUMENTED: employees.delete_directory_employee has no docstring.", + "example": "curl -X DELETE http://localhost:5001/api/employees/directory/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/employees/directory/", + "auth": "jwt + role:admin", + "params": "path: sso (int); body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: employees.update_directory_employee has no docstring.", + "example": "curl -X PUT http://localhost:5001/api/employees/directory/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/employees/directory/import", + "auth": "jwt + role:admin", + "params": "body: JSON (fields not yet documented)", + "purpose": "Bulk upsert from CSV. Accepts headers SSO,First_Name,Last_Name,Team,Role, Picture (case-insensitive; sso/firstname/... also accepted).", + "example": "curl -X POST http://localhost:5001/api/employees/directory/import -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/employees/lookup", + "auth": "permission:employees.view", + "params": "not documented", + "purpose": "Look up multiple employees by SSO list.", + "example": "curl http://localhost:5001/api/employees/lookup -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/employees/lookup/", + "auth": "permission:employees.view", + "params": "path: sso (string)", + "purpose": "Look up a single employee by SSO.", + "example": "curl http://localhost:5001/api/employees/lookup/x -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/employees/photo/", + "auth": "none (reviewed: named in the public-read or mutation sweep)", + "params": "path: filename (path)", + "purpose": "Serve an uploaded employee photo (public - kiosk cards read it).", + "example": "curl http://localhost:5001/api/employees/photo/example.png" + }, + { + "method": "GET", + "path": "/api/employees/resolve/", + "auth": "permission:employees.view", + "params": "path: sso (string)", + "purpose": "Resolve one SSO to a name and photo, and say HOW.", + "example": "curl http://localhost:5001/api/employees/resolve/x -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/employees/search", + "auth": "permission:employees.view", + "params": "not documented", + "purpose": "Search employees by name.", + "example": "curl http://localhost:5001/api/employees/search -H \"Authorization: Bearer $TOK\"" + } + ] + }, + { + "surface": "plugin-endpoints", + "endpoints": [ + { + "method": "GET", + "path": "/api/endpoints", + "auth": "permission:endpoints.fleet", + "params": "not documented", + "purpose": "List reported endpoint states.", + "example": "curl http://localhost:5001/api/endpoints -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/endpoints/", + "auth": "permission:endpoints.fleet", + "params": "not documented", + "purpose": "List reported endpoint states.", + "example": "curl http://localhost:5001/api/endpoints/ -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/endpoints/", + "auth": "permission:endpoints.view", + "params": "path: asset_id (int)", + "purpose": "Get the current state for one endpoint by asset id.", + "example": "curl http://localhost:5001/api/endpoints/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/endpoints//agentupgrade", + "auth": "permission:endpoints.agentupdate", + "params": "path: asset_id (int); body: JSON (fields not yet documented)", + "purpose": "Push the published agent build to one endpoint.", + "example": "curl -X POST http://localhost:5001/api/endpoints/1/agentupgrade -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/endpoints//commands", + "auth": "permission:endpoints.view", + "params": "path: asset_id (int)", + "purpose": "Recent commands for one endpoint, newest first (operator view).", + "example": "curl http://localhost:5001/api/endpoints/1/commands -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/endpoints//commands", + "auth": "permission:endpoints.command", + "params": "path: asset_id (int); body: JSON (fields not yet documented)", + "purpose": "Queue an allowlisted command for one endpoint.", + "example": "curl -X POST http://localhost:5001/api/endpoints/1/commands -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/endpoints//events", + "auth": "permission:endpoints.view", + "params": "path: asset_id (int)", + "purpose": "List curated Windows events for one endpoint, newest first.", + "example": "curl http://localhost:5001/api/endpoints/1/events -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/endpoints//holdableactions", + "auth": "permission:endpoints.view", + "params": "path: asset_id (int)", + "purpose": "Desired-mode actions on this endpoint, with current hold state.", + "example": "curl http://localhost:5001/api/endpoints/1/holdableactions -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "DELETE", + "path": "/api/endpoints//lock", + "auth": "permission:endpoints.command", + "params": "path: asset_id (int)", + "purpose": "Lift the quarantine on this endpoint.", + "example": "curl -X DELETE http://localhost:5001/api/endpoints/1/lock -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/endpoints//lock", + "auth": "permission:endpoints.view", + "params": "path: asset_id (int)", + "purpose": "The endpoint lock standing on this endpoint, or null.", + "example": "curl http://localhost:5001/api/endpoints/1/lock -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/endpoints//lock", + "auth": "permission:endpoints.command", + "params": "path: asset_id (int); body: JSON (fields not yet documented)", + "purpose": "Quarantine this endpoint against non-admin commands.", + "example": "curl -X POST http://localhost:5001/api/endpoints/1/lock -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/endpoints//logcollections", + "auth": "permission:endpoints.view", + "params": "path: asset_id (int)", + "purpose": "Collected-log history for one endpoint, newest first.", + "example": "curl http://localhost:5001/api/endpoints/1/logcollections -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/endpoints//policyholds", + "auth": "permission:endpoints.view", + "params": "path: asset_id (int)", + "purpose": "Active break-glass holds for one endpoint.", + "example": "curl http://localhost:5001/api/endpoints/1/policyholds -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/endpoints//policyholds", + "auth": "permission:endpoints.command", + "params": "path: asset_id (int); body: JSON (fields not yet documented)", + "purpose": "Deliberately suspend desired-state reconciliation for one or more actions.", + "example": "curl -X POST http://localhost:5001/api/endpoints/1/policyholds -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/endpoints//policyholds/", + "auth": "permission:endpoints.command", + "params": "path: asset_id (int), action (string)", + "purpose": "End a hold early, resuming reconciliation of one action.", + "example": "curl -X DELETE http://localhost:5001/api/endpoints/1/policyholds/x -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/endpoints//scriptruns", + "auth": "permission:endpoints.view", + "params": "path: asset_id (int)", + "purpose": "The last few run_script commands for one endpoint, newest first.", + "example": "curl http://localhost:5001/api/endpoints/1/scriptruns -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/endpoints/adoptiontool/package", + "auth": "permission:endpoints.agentpackage", + "params": "body: JSON (fields not yet documented)", + "purpose": "Bundle the adoption tool as a downloadable ZIP.", + "example": "curl -X POST http://localhost:5001/api/endpoints/adoptiontool/package -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/endpoints/agentkey", + "auth": "jwt + role:admin", + "params": "not documented", + "purpose": "Revoke the endpoint agent key: stop all managed-token endpoint traffic.", + "example": "curl -X DELETE http://localhost:5001/api/endpoints/agentkey -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/endpoints/agentkey", + "auth": "jwt + role:admin", + "params": "not documented", + "purpose": "Report the endpoint agent key state (no secret).", + "example": "curl http://localhost:5001/api/endpoints/agentkey -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/endpoints/agentkey/regenerate", + "auth": "jwt + role:admin", + "params": "body: JSON (fields not yet documented)", + "purpose": "Mint a fresh endpoint agent key, revoking any previous one.", + "example": "curl -X POST http://localhost:5001/api/endpoints/agentkey/regenerate -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/endpoints/alerts", + "auth": "api-key (endpoint agent credential, or the fleet key while sharedkeyallowed is on)", + "params": "not documented", + "purpose": "Fleet alerts for one endpoint by hostname (collector-token auth).", + "example": "curl http://localhost:5001/api/endpoints/alerts -H \"X-API-Key: $KEY\"" + }, + { + "method": "POST", + "path": "/api/endpoints/alerts//acknowledge", + "auth": "api-key (endpoint agent credential, or the fleet key while sharedkeyallowed is on)", + "params": "path: notification_id (int); body: JSON (fields not yet documented)", + "purpose": "Agent acknowledges a fleet alert (collector-token auth).", + "example": "curl -X POST http://localhost:5001/api/endpoints/alerts/1/acknowledge -H \"X-API-Key: $KEY\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/endpoints/apppackage/", + "auth": "api-key (endpoint agent credential, or the fleet key while sharedkeyallowed is on)", + "params": "path: appversionid (int)", + "purpose": "Agent-facing installer download (collector-token auth).", + "example": "curl http://localhost:5001/api/endpoints/apppackage/1 -H \"X-API-Key: $KEY\"" + }, + { + "method": "GET", + "path": "/api/endpoints/apppackage/app/", + "auth": "api-key (endpoint agent credential, or the fleet key while sharedkeyallowed is on)", + "params": "path: app_id (int)", + "purpose": "Agent-facing installer download by APP (collector-token auth).", + "example": "curl http://localhost:5001/api/endpoints/apppackage/app/1 -H \"X-API-Key: $KEY\"" + }, + { + "method": "GET", + "path": "/api/endpoints/apppackage/app//", + "auth": "api-key (endpoint agent credential, or the fleet key while sharedkeyallowed is on)", + "params": "path: app_id (int), filename (path)", + "purpose": "Agent-facing installer download by APP (collector-token auth).", + "example": "curl http://localhost:5001/api/endpoints/apppackage/app/1/example.png -H \"X-API-Key: $KEY\"" + }, + { + "method": "GET", + "path": "/api/endpoints/asset//activity", + "auth": "permission:endpoints.view", + "params": "path: asset_id (int)", + "purpose": "List panel: one chronological feed of what happened to this endpoint.", + "example": "curl http://localhost:5001/api/endpoints/asset/1/activity -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/endpoints/asset//applications", + "auth": "permission:endpoints.view", + "params": "path: asset_id (int)", + "purpose": "Table panel: the installed-software (SBOM) snapshot for this endpoint.", + "example": "curl http://localhost:5001/api/endpoints/asset/1/applications -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/endpoints/asset//diskhealth", + "auth": "permission:endpoints.view", + "params": "path: asset_id (int)", + "purpose": "Physical drive health rows.", + "example": "curl http://localhost:5001/api/endpoints/asset/1/diskhealth -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/endpoints/asset//hardware", + "auth": "permission:endpoints.view", + "params": "path: asset_id (int)", + "purpose": "Keyvalue panel: processor, memory and system chassis inventory.", + "example": "curl http://localhost:5001/api/endpoints/asset/1/hardware -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/endpoints/asset//interfaces", + "auth": "permission:endpoints.view", + "params": "path: asset_id (int)", + "purpose": "Table panel: every hardware NIC the agent last reported.", + "example": "curl http://localhost:5001/api/endpoints/asset/1/interfaces -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/endpoints/asset//iphistory", + "auth": "permission:endpoints.view", + "params": "path: asset_id (int)", + "purpose": "Table panel: every address this asset has held, newest change first.", + "example": "curl http://localhost:5001/api/endpoints/asset/1/iphistory -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/endpoints/asset//listeners", + "auth": "permission:endpoints.view", + "params": "path: asset_id (int)", + "purpose": "Table panel: TCP sockets in LISTEN, with the owning process.", + "example": "curl http://localhost:5001/api/endpoints/asset/1/listeners -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/endpoints/asset//mappeddrives", + "auth": "permission:endpoints.view", + "params": "path: asset_id (int)", + "purpose": "List panel: mapped network drives with a health badge each.", + "example": "curl http://localhost:5001/api/endpoints/asset/1/mappeddrives -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/endpoints/asset//osinfo", + "auth": "permission:endpoints.view", + "params": "path: asset_id (int)", + "purpose": "Keyvalue panel: operating system, firmware and domain membership.", + "example": "curl http://localhost:5001/api/endpoints/asset/1/osinfo -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/endpoints/asset//recentevents", + "auth": "permission:endpoints.view", + "params": "path: asset_id (int)", + "purpose": "List panel: the endpoint's most recent curated events, newest first.", + "example": "curl http://localhost:5001/api/endpoints/asset/1/recentevents -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/endpoints/asset//services", + "auth": "permission:endpoints.view", + "params": "path: asset_id (int)", + "purpose": "List panel: monitored services/processes with a run-state badge per row.", + "example": "curl http://localhost:5001/api/endpoints/asset/1/services -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/endpoints/asset//status", + "auth": "permission:endpoints.view", + "params": "path: asset_id (int)", + "purpose": "Keyvalue panel: the endpoint's current status snapshot.", + "example": "curl http://localhost:5001/api/endpoints/asset/1/status -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/endpoints/asset//storage", + "auth": "permission:endpoints.view", + "params": "path: asset_id (int)", + "purpose": "Tabs panel: volume capacity and physical drive health as two sections.", + "example": "curl http://localhost:5001/api/endpoints/asset/1/storage -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/endpoints/asset//telemetrymeta", + "auth": "permission:endpoints.view", + "params": "path: asset_id (int)", + "purpose": "Lightweight freshness probe for the panels.", + "example": "curl http://localhost:5001/api/endpoints/asset/1/telemetrymeta -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/endpoints/asset//usercontext", + "auth": "permission:endpoints.view", + "params": "path: asset_id (int)", + "purpose": "Keyvalue panel: session-scoped data from the interactive user probe.", + "example": "curl http://localhost:5001/api/endpoints/asset/1/usercontext -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/endpoints/collect", + "auth": "api-key (endpoints collector token)", + "params": "body: JSON (fields not yet documented)", + "purpose": "Agent telemetry ingest. Same handler as POST /api/collector/endpoints.", + "example": "curl -X POST http://localhost:5001/api/endpoints/collect -H \"X-API-Key: $KEY\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/endpoints/commands//result", + "auth": "api-key (endpoint agent credential, or the fleet key while sharedkeyallowed is on)", + "params": "path: command_id (int); body: JSON (fields not yet documented)", + "purpose": "Agent reports a claimed command's outcome (collector-token auth).", + "example": "curl -X POST http://localhost:5001/api/endpoints/commands/1/result -H \"X-API-Key: $KEY\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/endpoints/commands/next", + "auth": "api-key (endpoint agent credential, or the fleet key while sharedkeyallowed is on)", + "params": "not documented", + "purpose": "Agent long-poll: claim the next pending command for a hostname.", + "example": "curl http://localhost:5001/api/endpoints/commands/next -H \"X-API-Key: $KEY\"" + }, + { + "method": "GET", + "path": "/api/endpoints/dashboard/enrollmentpending", + "auth": "permission:endpoints.fleet", + "params": "not documented", + "purpose": "One queue row while enroll requests wait for an admin, else nothing.", + "example": "curl http://localhost:5001/api/endpoints/dashboard/enrollmentpending -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/endpoints/dashboard/quiet", + "auth": "permission:endpoints.fleet", + "params": "not documented", + "purpose": "Endpoints that reported before, then went quiet past the window.", + "example": "curl http://localhost:5001/api/endpoints/dashboard/quiet -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/endpoints/dashboard/reenrollblocked", + "auth": "permission:endpoints.fleet", + "params": "not documented", + "purpose": "PCs kept from re-enrolling for more than 7 days.", + "example": "curl http://localhost:5001/api/endpoints/dashboard/reenrollblocked -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/endpoints/dashboard/stability", + "auth": "permission:endpoints.fleet", + "params": "not documented", + "purpose": "Endpoints with recent crash/shutdown/corruption events.", + "example": "curl http://localhost:5001/api/endpoints/dashboard/stability -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/endpoints/dashboard/summary", + "auth": "permission:endpoints.fleet", + "params": "not documented", + "purpose": "High-level fleet counts for the endpoints dashboard.", + "example": "curl http://localhost:5001/api/endpoints/dashboard/summary -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/endpoints/enroll", + "auth": "none (per-IP rate limited and capped; a valid fleet key only decides auto-enroll)", + "params": "body: JSON (fields not yet documented)", + "purpose": "A PC asks for its own credential.", + "example": "curl -X POST http://localhost:5001/api/endpoints/enroll -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/endpoints/enrollment", + "auth": "permission:endpoints.fleet", + "params": "not documented", + "purpose": "Credential state of every computer, plus pending and recent requests.", + "example": "curl http://localhost:5001/api/endpoints/enrollment -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/endpoints/enrollment/assets//allow-reenroll", + "auth": "jwt + role:admin", + "params": "path: assetid (int); body: JSON (fields not yet documented)", + "purpose": "Lift a re-enrollment block. The PC enrolls on its next attempt.", + "example": "curl -X POST http://localhost:5001/api/endpoints/enrollment/assets/1/allow-reenroll -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/endpoints/enrollment/assets//revoke", + "auth": "jwt + role:admin", + "params": "path: assetid (int); body: JSON (fields not yet documented)", + "purpose": "Un-enroll a PC. Its agent gets CREDENTIAL_INVALID and enrolls again.", + "example": "curl -X POST http://localhost:5001/api/endpoints/enrollment/assets/1/revoke -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/endpoints/enrollment/migration", + "auth": "permission:endpoints.fleet", + "params": "not documented", + "purpose": "Per-computer readiness to move onto per-PC credentials, plus waves.", + "example": "curl http://localhost:5001/api/endpoints/enrollment/migration -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/endpoints/enrollment/migration/wave", + "auth": "jwt + role:admin", + "params": "body: JSON (fields not yet documented)", + "purpose": "Push the agent upgrade to a batch of READY PCs.", + "example": "curl -X POST http://localhost:5001/api/endpoints/enrollment/migration/wave -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/endpoints/enrollment/requests//approve", + "auth": "jwt + role:admin", + "params": "path: requestid (int); body: JSON (fields not yet documented)", + "purpose": "Bind a pending request to the asset the admin picked.", + "example": "curl -X POST http://localhost:5001/api/endpoints/enrollment/requests/1/approve -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/endpoints/enrollment/requests//reject", + "auth": "jwt + role:admin", + "params": "path: requestid (int); body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: endpoints.reject_enroll_request has no docstring.", + "example": "curl -X POST http://localhost:5001/api/endpoints/enrollment/requests/1/reject -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "PUT", + "path": "/api/endpoints/enrollment/settings", + "auth": "jwt + role:admin", + "params": "body: JSON (fields not yet documented)", + "purpose": "Change the enrollment settings.", + "example": "curl -X PUT http://localhost:5001/api/endpoints/enrollment/settings -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/endpoints/logcollections/", + "auth": "permission:endpoints.updateapp", + "params": "path: collection_id (int)", + "purpose": "One collected log including its content, for the scrollable pane.", + "example": "curl http://localhost:5001/api/endpoints/logcollections/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/endpoints/logcollections//download", + "auth": "permission:endpoints.updateapp", + "params": "path: collection_id (int)", + "purpose": "Serve a collected log as a file attachment.", + "example": "curl http://localhost:5001/api/endpoints/logcollections/1/download -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/endpoints/monitors", + "auth": "permission:endpoints.view", + "params": "not documented", + "purpose": "List monitor definitions (shipped and local).", + "example": "curl http://localhost:5001/api/endpoints/monitors -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/endpoints/monitors", + "auth": "jwt + role:admin", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create a user-defined (local) monitor definition.", + "example": "curl -X POST http://localhost:5001/api/endpoints/monitors -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/endpoints/monitors/", + "auth": "jwt + role:admin", + "params": "path: monitor_id (int)", + "purpose": "Delete a local monitor definition. Shipped rows are protected.", + "example": "curl -X DELETE http://localhost:5001/api/endpoints/monitors/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/endpoints/monitors/", + "auth": "jwt + role:admin", + "params": "path: monitor_id (int); body: JSON (fields not yet documented)", + "purpose": "Update a local monitor definition. Shipped rows are read-only.", + "example": "curl -X PUT http://localhost:5001/api/endpoints/monitors/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/endpoints/monitors/resolve", + "auth": "permission:endpoints.view", + "params": "not documented", + "purpose": "Resolve the monitor set for a PC type (agent runtime fetch).", + "example": "curl http://localhost:5001/api/endpoints/monitors/resolve -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/endpoints/monitors/resolved", + "auth": "api-key (endpoint agent credential, or the fleet key while sharedkeyallowed is on)", + "params": "not documented", + "purpose": "Agent watch list: the monitors this endpoint should report on.", + "example": "curl http://localhost:5001/api/endpoints/monitors/resolved -H \"X-API-Key: $KEY\"" + }, + { + "method": "POST", + "path": "/api/endpoints/operatorreports", + "auth": "api-key (endpoint agent credential, or the fleet key while sharedkeyallowed is on)", + "params": "body: JSON (fields not yet documented)", + "purpose": "Agent uploads one operator issue report (collector-token auth).", + "example": "curl -X POST http://localhost:5001/api/endpoints/operatorreports -H \"X-API-Key: $KEY\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/endpoints/operatorreports//image", + "auth": "permission:endpoints.operatorreports", + "params": "path: operatorreportid (int)", + "purpose": "Serve one operator report's screenshot.", + "example": "curl http://localhost:5001/api/endpoints/operatorreports/1/image -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/endpoints/panel//operatorreports", + "auth": "permission:endpoints.operatorreports", + "params": "path: asset_id (int)", + "purpose": "Operator Reports panel: this PC's recent operator issue reports.", + "example": "curl http://localhost:5001/api/endpoints/panel/1/operatorreports -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/endpoints/scriptbody/", + "auth": "api-key (endpoint agent credential, or the fleet key while sharedkeyallowed is on)", + "params": "path: script_id (int)", + "purpose": "Agent-facing script body download (collector-token auth).", + "example": "curl http://localhost:5001/api/endpoints/scriptbody/1 -H \"X-API-Key: $KEY\"" + }, + { + "method": "GET", + "path": "/api/endpoints/settings/server-cert", + "auth": "jwt + role:admin", + "params": "not documented", + "purpose": "Get details about the current server certificate.", + "example": "curl http://localhost:5001/api/endpoints/settings/server-cert -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/endpoints/settings/server-cert/download", + "auth": "permission:endpoints.fleet", + "params": "not documented", + "purpose": "The PUBLIC server certificate as a DER .cer file, never the key.", + "example": "curl http://localhost:5001/api/endpoints/settings/server-cert/download -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/endpoints/settings/server-cert/generate", + "auth": "jwt + role:admin", + "params": "body: JSON (fields not yet documented)", + "purpose": "Generate a new self-signed server certificate.", + "example": "curl -X POST http://localhost:5001/api/endpoints/settings/server-cert/generate -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/endpoints/settings/server-cert/live", + "auth": "permission:endpoints.fleet", + "params": "not documented", + "purpose": "The certificate PCs actually receive from this site's public address.", + "example": "curl http://localhost:5001/api/endpoints/settings/server-cert/live -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/endpoints/settings/server-cert/upload", + "auth": "jwt + role:admin", + "params": "body: JSON (fields not yet documented)", + "purpose": "Upload a new server certificate and private key.", + "example": "curl -X POST http://localhost:5001/api/endpoints/settings/server-cert/upload -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/endpoints/siteagent", + "auth": "permission:endpoints.agentpackage", + "params": "not documented", + "purpose": "Report whether the site agent build has a prebuilt binary staged.", + "example": "curl http://localhost:5001/api/endpoints/siteagent -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/endpoints/siteagent/binaries/", + "auth": "permission:endpoints.agentpackage", + "params": "path: binkey (string)", + "purpose": "List staged versions for a binary (agent or tray), newest first.", + "example": "curl http://localhost:5001/api/endpoints/siteagent/binaries/x -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/endpoints/siteagent/binaries/", + "auth": "permission:endpoints.agentpackage", + "params": "path: binkey (string); body: JSON (fields not yet documented)", + "purpose": "Stage an uploaded binary as a new version.", + "example": "curl -X POST http://localhost:5001/api/endpoints/siteagent/binaries/x -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/endpoints/siteagent/binaries//adopt", + "auth": "permission:endpoints.agentpackage", + "params": "path: binkey (string); body: JSON (fields not yet documented)", + "purpose": "Adopt the unmanaged legacy flat file into the version store.", + "example": "curl -X POST http://localhost:5001/api/endpoints/siteagent/binaries/x/adopt -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "PUT", + "path": "/api/endpoints/siteagent/binaries//current", + "auth": "permission:endpoints.agentpackage", + "params": "path: binkey (string); body: JSON (fields not yet documented)", + "purpose": "Point 'current' at an already-staged version (roll back or forward).", + "example": "curl -X PUT http://localhost:5001/api/endpoints/siteagent/binaries/x/current -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/endpoints/siteagent/binaries//detect", + "auth": "permission:endpoints.agentpackage", + "params": "path: binkey (string); body: JSON (fields not yet documented)", + "purpose": "Read the PE file version of an uploaded EXE without staging it.", + "example": "curl -X POST http://localhost:5001/api/endpoints/siteagent/binaries/x/detect -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/endpoints/siteagent/bootstrap.ps1", + "auth": "api-key (single-use bootstrap token in the X-Endpoint-Bootstrap-Token header, or ?t= while that is enabled; per-IP rate limited, 404 when not usable)", + "params": "not documented", + "purpose": "The PowerShell the one-line command downloads, runs and deletes.", + "example": "curl http://localhost:5001/api/endpoints/siteagent/bootstrap.ps1" + }, + { + "method": "GET", + "path": "/api/endpoints/siteagent/bootstrap/package", + "auth": "api-key (single-use bootstrap token in the X-Endpoint-Bootstrap-Token header, or ?t= while that is enabled; per-IP rate limited, 404 when not usable)", + "params": "not documented", + "purpose": "The keyless agent package the bootstrap script installs.", + "example": "curl http://localhost:5001/api/endpoints/siteagent/bootstrap/package" + }, + { + "method": "GET", + "path": "/api/endpoints/siteagent/bootstrap/tokens", + "auth": "jwt + role:admin", + "params": "not documented", + "purpose": "Every bootstrap token, newest first. Never a secret.", + "example": "curl http://localhost:5001/api/endpoints/siteagent/bootstrap/tokens -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/endpoints/siteagent/bootstrap/tokens", + "auth": "jwt + role:admin", + "params": "body: JSON (fields not yet documented)", + "purpose": "Mint one short-lived bootstrap token. The secret is returned ONCE.", + "example": "curl -X POST http://localhost:5001/api/endpoints/siteagent/bootstrap/tokens -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/endpoints/siteagent/bootstrap/tokens//revoke", + "auth": "jwt + role:admin", + "params": "path: tokenid (int); body: JSON (fields not yet documented)", + "purpose": "Kill a bootstrap token now. Already-revoked is a no-op, not an error.", + "example": "curl -X POST http://localhost:5001/api/endpoints/siteagent/bootstrap/tokens/1/revoke -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/endpoints/siteagent/package", + "auth": "permission:endpoints.agentpackage", + "params": "body: JSON (fields not yet documented)", + "purpose": "Bundle the site agent as a downloadable ZIP.", + "example": "curl -X POST http://localhost:5001/api/endpoints/siteagent/package -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + } + ] + }, + { + "surface": "plugin-geenforce", + "endpoints": [ + { + "method": "GET", + "path": "/api/geenforce/applications", + "auth": "permission:geenforce.view or geenforce.manage", + "params": "not documented", + "purpose": "The core Applications catalog (id + name) for the curated entry->app link picker in the entry editor.", + "example": "curl http://localhost:5001/api/geenforce/applications -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/geenforce/config", + "auth": "permission:geenforce.view or geenforce.manage", + "params": "not documented", + "purpose": "Plugin config: the on-share export root + the client IP allowlist.", + "example": "curl http://localhost:5001/api/geenforce/config -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/geenforce/config", + "auth": "permission:geenforce.publish", + "params": "body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: geenforce.put_config has no docstring.", + "example": "curl -X PUT http://localhost:5001/api/geenforce/config -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/geenforce/dashboard/failures", + "auth": "permission:geenforce.view or geenforce.manage", + "params": "not documented", + "purpose": "Entries that FAILED on their PC's most recent enforcement cycle.", + "example": "curl http://localhost:5001/api/geenforce/dashboard/failures -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "DELETE", + "path": "/api/geenforce/entries/", + "auth": "permission:geenforce.manage", + "params": "path: entryid (int)", + "purpose": "UNDOCUMENTED: geenforce.delete_entry has no docstring.", + "example": "curl -X DELETE http://localhost:5001/api/geenforce/entries/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/geenforce/entries/", + "auth": "permission:geenforce.manage", + "params": "path: entryid (int); body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: geenforce.update_entry has no docstring.", + "example": "curl -X PUT http://localhost:5001/api/geenforce/entries/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/geenforce/entries//payload", + "auth": "permission:geenforce.view or geenforce.manage", + "params": "path: entryid (int)", + "purpose": "Return the stored inline payload bytes for an entry (404 if none).", + "example": "curl http://localhost:5001/api/geenforce/entries/1/payload -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/geenforce/entries//payload", + "auth": "permission:geenforce.publish", + "params": "path: entryid (int); body: JSON (fields not yet documented)", + "purpose": "Store an inline payload (<= 1 MB) for an entry and point the entry at it.", + "example": "curl -X POST http://localhost:5001/api/geenforce/entries/1/payload -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/geenforce/manifest", + "auth": "api-key (geenforce fetch token, or the IP allowlist)", + "params": "not documented", + "purpose": "Serve the current published manifest for a scope (fat-client: full scope).", + "example": "curl http://localhost:5001/api/geenforce/manifest -H \"X-API-Key: $KEY\"" + }, + { + "method": "GET", + "path": "/api/geenforce/payload/", + "auth": "api-key (geenforce fetch token, or the IP allowlist)", + "params": "path: sha256 (string)", + "purpose": "Serve a payload blob by content hash over HTTPS.", + "example": "curl http://localhost:5001/api/geenforce/payload/x -H \"X-API-Key: $KEY\"" + }, + { + "method": "POST", + "path": "/api/geenforce/report", + "auth": "api-key (geenforce report token)", + "params": "body: JSON (fields not yet documented)", + "purpose": "Record one PC's enforcement cycle: applied version (did it receive the update?) + per-entry self-heal outcomes (installed/skipped/failed).", + "example": "curl -X POST http://localhost:5001/api/geenforce/report -H \"X-API-Key: $KEY\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/geenforce/reports", + "auth": "permission:geenforce.view or geenforce.manage", + "params": "not documented", + "purpose": "Latest enforcement report per PC (fleet compliance view).", + "example": "curl http://localhost:5001/api/geenforce/reports -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/geenforce/reports/", + "auth": "permission:geenforce.view or geenforce.manage", + "params": "path: reportid (int)", + "purpose": "One report with per-entry outcomes (self-heal / failure detail).", + "example": "curl http://localhost:5001/api/geenforce/reports/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/geenforce/scopes", + "auth": "permission:geenforce.view or geenforce.manage", + "params": "not documented", + "purpose": "List imaging PC-type scopes with entry + published-version counts.", + "example": "curl http://localhost:5001/api/geenforce/scopes -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/geenforce/scopes", + "auth": "permission:geenforce.manage", + "params": "body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: geenforce.create_scope has no docstring.", + "example": "curl -X POST http://localhost:5001/api/geenforce/scopes -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/geenforce/scopes/", + "auth": "permission:geenforce.manage", + "params": "path: scopeid (int)", + "purpose": "UNDOCUMENTED: geenforce.delete_scope has no docstring.", + "example": "curl -X DELETE http://localhost:5001/api/geenforce/scopes/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/geenforce/scopes/", + "auth": "permission:geenforce.view or geenforce.manage", + "params": "path: scopeid (int)", + "purpose": "UNDOCUMENTED: geenforce.get_scope has no docstring.", + "example": "curl http://localhost:5001/api/geenforce/scopes/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/geenforce/scopes/", + "auth": "permission:geenforce.manage", + "params": "path: scopeid (int); body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: geenforce.update_scope has no docstring.", + "example": "curl -X PUT http://localhost:5001/api/geenforce/scopes/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/geenforce/scopes//compliance", + "auth": "permission:geenforce.view or geenforce.manage", + "params": "path: scopeid (int)", + "purpose": "Fleet-install coverage per app-linked entry (from collected PC data).", + "example": "curl http://localhost:5001/api/geenforce/scopes/1/compliance -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/geenforce/scopes//entries", + "auth": "permission:geenforce.manage", + "params": "path: scopeid (int); body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: geenforce.create_entry has no docstring.", + "example": "curl -X POST http://localhost:5001/api/geenforce/scopes/1/entries -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "PUT", + "path": "/api/geenforce/scopes//entries/reorder", + "auth": "permission:geenforce.manage", + "params": "path: scopeid (int); body: JSON (fields not yet documented)", + "purpose": "Set entry order from a list of entryids (the ordering contract).", + "example": "curl -X PUT http://localhost:5001/api/geenforce/scopes/1/entries/reorder -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/geenforce/scopes//export-share", + "auth": "permission:geenforce.publish", + "params": "path: scopeid (int); body: JSON (fields not yet documented)", + "purpose": "Write the scope's current published JSON to the configured share root (backing up the old file to _meta/history first).", + "example": "curl -X POST http://localhost:5001/api/geenforce/scopes/1/export-share -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/geenforce/scopes//preview", + "auth": "permission:geenforce.view or geenforce.manage", + "params": "path: scopeid (int)", + "purpose": "Render the DRAFT manifest JSON a publish would freeze (review before ship).", + "example": "curl http://localhost:5001/api/geenforce/scopes/1/preview -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/geenforce/scopes//publish", + "auth": "permission:geenforce.publish", + "params": "path: scopeid (int); body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: geenforce.publish_scope_route has no docstring.", + "example": "curl -X POST http://localhost:5001/api/geenforce/scopes/1/publish -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/geenforce/scopes//publish-preflight", + "auth": "permission:geenforce.view or geenforce.manage", + "params": "path: scopeid (int)", + "purpose": "Would publishing this scope outrun the fleet's enforcer lib?", + "example": "curl http://localhost:5001/api/geenforce/scopes/1/publish-preflight -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/geenforce/scopes//rollback", + "auth": "permission:geenforce.publish", + "params": "path: scopeid (int); body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: geenforce.rollback_scope_route has no docstring.", + "example": "curl -X POST http://localhost:5001/api/geenforce/scopes/1/rollback -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/geenforce/scopes//simulate", + "auth": "permission:geenforce.view or geenforce.manage", + "params": "path: scopeid (int)", + "purpose": "Which draft entries apply to a given machine profile, and why the rest are filtered out (reuses the engine-mirror filters).", + "example": "curl http://localhost:5001/api/geenforce/scopes/1/simulate -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/geenforce/scopes//versions", + "auth": "permission:geenforce.view or geenforce.manage", + "params": "path: scopeid (int)", + "purpose": "UNDOCUMENTED: geenforce.list_versions has no docstring.", + "example": "curl http://localhost:5001/api/geenforce/scopes/1/versions -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/geenforce/scopes//versions/", + "auth": "permission:geenforce.view or geenforce.manage", + "params": "path: scopeid (int), versionnumber (int)", + "purpose": "UNDOCUMENTED: geenforce.get_version has no docstring.", + "example": "curl http://localhost:5001/api/geenforce/scopes/1/versions/1 -H \"Authorization: Bearer $TOK\"" + } + ] + }, + { + "surface": "plugin-guides", + "endpoints": [ + { + "method": "GET", + "path": "/api/guides", + "auth": "permission:guides.view", + "params": "not documented", + "purpose": "Published guides, plus the caller's own drafts if they can author.", + "example": "curl http://localhost:5001/api/guides -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/guides", + "auth": "permission:guides.create", + "params": "body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: guides.create_guide has no docstring.", + "example": "curl -X POST http://localhost:5001/api/guides -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/guides/", + "auth": "permission:guides.delete", + "params": "path: guideid (int)", + "purpose": "UNDOCUMENTED: guides.delete_guide has no docstring.", + "example": "curl -X DELETE http://localhost:5001/api/guides/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/guides/", + "auth": "permission:guides.view", + "params": "path: guideid (int); body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: guides.update_guide has no docstring.", + "example": "curl -X PUT http://localhost:5001/api/guides/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/guides//publish", + "auth": "permission:guides.publish", + "params": "path: guideid (int); body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: guides.publish_guide has no docstring.", + "example": "curl -X POST http://localhost:5001/api/guides/1/publish -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/guides//restore/", + "auth": "permission:guides.view", + "params": "path: guideid (int), revisionid (int); body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: guides.restore_revision has no docstring.", + "example": "curl -X POST http://localhost:5001/api/guides/1/restore/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/guides//revisions", + "auth": "permission:guides.view", + "params": "path: guideid (int)", + "purpose": "UNDOCUMENTED: guides.list_revisions has no docstring.", + "example": "curl http://localhost:5001/api/guides/1/revisions -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/guides//unpublish", + "auth": "permission:guides.publish", + "params": "path: guideid (int); body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: guides.unpublish_guide has no docstring.", + "example": "curl -X POST http://localhost:5001/api/guides/1/unpublish -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/guides//working", + "auth": "permission:guides.view", + "params": "path: guideid (int)", + "purpose": "The working copy, for the editor.", + "example": "curl http://localhost:5001/api/guides/1/working -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/guides/", + "auth": "permission:guides.view", + "params": "path: slug (string)", + "purpose": "What a reader gets: the published revision.", + "example": "curl http://localhost:5001/api/guides/x -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/guides/images", + "auth": "permission:guides.create", + "params": "body: JSON (fields not yet documented)", + "purpose": "Take a pasted or dropped image.", + "example": "curl -X POST http://localhost:5001/api/guides/images -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/guides/images/", + "auth": "permission:guides.view", + "params": "path: imageid (int)", + "purpose": "The image itself, under the same rule as the guide that holds it.", + "example": "curl http://localhost:5001/api/guides/images/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/guides/revisions/", + "auth": "permission:guides.view", + "params": "path: revisionid (int)", + "purpose": "One revision by id.", + "example": "curl http://localhost:5001/api/guides/revisions/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/guides/tags", + "auth": "permission:guides.view", + "params": "not documented", + "purpose": "Every tag in use, with a count, for the filter chips.", + "example": "curl http://localhost:5001/api/guides/tags -H \"Authorization: Bearer $TOK\"" + } + ] + }, + { + "surface": "plugin-knowledgebase", + "endpoints": [ + { + "method": "GET", + "path": "/api/knowledgebase", + "auth": "permission:kb.view", + "params": "not documented", + "purpose": "List all knowledge base articles.", + "example": "curl http://localhost:5001/api/knowledgebase -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/knowledgebase", + "auth": "permission:kb.create", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create a new knowledge base article.", + "example": "curl -X POST http://localhost:5001/api/knowledgebase -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/knowledgebase/", + "auth": "permission:kb.delete", + "params": "path: link_id (int)", + "purpose": "Delete (deactivate) a knowledge base article.", + "example": "curl -X DELETE http://localhost:5001/api/knowledgebase/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/knowledgebase/", + "auth": "permission:kb.view", + "params": "path: link_id (int)", + "purpose": "Get a single knowledge base article.", + "example": "curl http://localhost:5001/api/knowledgebase/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/knowledgebase/", + "auth": "permission:kb.edit", + "params": "path: link_id (int); body: JSON (fields not yet documented)", + "purpose": "Update a knowledge base article.", + "example": "curl -X PUT http://localhost:5001/api/knowledgebase/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/knowledgebase//click", + "auth": "permission:kb.view", + "params": "path: link_id (int); body: JSON (fields not yet documented)", + "purpose": "Increment the click counter for an article the caller is allowed to read.", + "example": "curl -X POST http://localhost:5001/api/knowledgebase/1/click -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/knowledgebase/stats", + "auth": "permission:kb.view", + "params": "not documented", + "purpose": "Get knowledge base statistics.", + "example": "curl http://localhost:5001/api/knowledgebase/stats -H \"Authorization: Bearer $TOK\"" + } + ] + }, + { + "surface": "plugin-machines", + "endpoints": [ + { + "method": "GET", + "path": "/api/machines", + "auth": "permission:machines.view", + "params": "not documented", + "purpose": "List all machines with filtering and pagination.", + "example": "curl http://localhost:5001/api/machines -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/machines", + "auth": "permission:machines.create", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create new machine (creates both Asset and Machine records).", + "example": "curl -X POST http://localhost:5001/api/machines -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/machines/", + "auth": "permission:machines.delete", + "params": "path: machine_id (int)", + "purpose": "Delete (soft delete) machine.", + "example": "curl -X DELETE http://localhost:5001/api/machines/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/machines/", + "auth": "permission:machines.view", + "params": "path: machine_id (int)", + "purpose": "Get a single machine item with full details.", + "example": "curl http://localhost:5001/api/machines/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/machines/", + "auth": "permission:machines.edit", + "params": "path: machine_id (int); body: JSON (fields not yet documented)", + "purpose": "Update machine (both Asset and Machine records).", + "example": "curl -X PUT http://localhost:5001/api/machines/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/machines/by-asset/", + "auth": "permission:machines.view", + "params": "path: asset_id (int)", + "purpose": "Get machine data by asset ID.", + "example": "curl http://localhost:5001/api/machines/by-asset/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/machines/dashboard/outofservice", + "auth": "permission:machines.view", + "params": "not documented", + "purpose": "Machines whose status says they are not running.", + "example": "curl http://localhost:5001/api/machines/dashboard/outofservice -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/machines/dashboard/summary", + "auth": "permission:machines.view", + "params": "not documented", + "purpose": "Get machine dashboard summary data.", + "example": "curl http://localhost:5001/api/machines/dashboard/summary -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/machines/types", + "auth": "permission:machines.view", + "params": "not documented", + "purpose": "List all machine types.", + "example": "curl http://localhost:5001/api/machines/types -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/machines/types", + "auth": "permission:machines.create", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create a new machine type.", + "example": "curl -X POST http://localhost:5001/api/machines/types -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/machines/types/", + "auth": "permission:machines.delete", + "params": "path: type_id (int)", + "purpose": "Delete a machine type. Refused if any asset still uses it.", + "example": "curl -X DELETE http://localhost:5001/api/machines/types/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/machines/types/", + "auth": "permission:machines.view", + "params": "path: type_id (int)", + "purpose": "Get a single machine type.", + "example": "curl http://localhost:5001/api/machines/types/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/machines/types/", + "auth": "permission:machines.edit", + "params": "path: type_id (int); body: JSON (fields not yet documented)", + "purpose": "Update a machine type.", + "example": "curl -X PUT http://localhost:5001/api/machines/types/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + } + ] + }, + { + "surface": "plugin-measuringtools", + "endpoints": [ + { + "method": "GET", + "path": "/api/measuringtools", + "auth": "permission:measuringtools.view", + "params": "not documented", + "purpose": "List measuring tools with filters + pagination.", + "example": "curl http://localhost:5001/api/measuringtools -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/measuringtools", + "auth": "permission:measuringtools.create", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create a measuring tool: one Asset core row + one extension row.", + "example": "curl -X POST http://localhost:5001/api/measuringtools -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/measuringtools/", + "auth": "permission:measuringtools.delete", + "params": "path: tool_id (int)", + "purpose": "Soft delete a measuring tool (deactivates its asset).", + "example": "curl -X DELETE http://localhost:5001/api/measuringtools/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/measuringtools/", + "auth": "permission:measuringtools.view", + "params": "path: tool_id (int)", + "purpose": "Get one measuring tool (asset core merged with the extension).", + "example": "curl http://localhost:5001/api/measuringtools/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/measuringtools/", + "auth": "permission:measuringtools.edit", + "params": "path: tool_id (int); body: JSON (fields not yet documented)", + "purpose": "Update a measuring tool (asset core fields + extension in one payload).", + "example": "curl -X PUT http://localhost:5001/api/measuringtools/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/measuringtools/by-asset/", + "auth": "permission:measuringtools.view", + "params": "path: asset_id (int)", + "purpose": "Get a measuring tool by its core asset id.", + "example": "curl http://localhost:5001/api/measuringtools/by-asset/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/measuringtools/map-overlay", + "auth": "permission:measuringtools.view", + "params": "not documented", + "purpose": "Calibration-status overlay for active measuring tools.", + "example": "curl http://localhost:5001/api/measuringtools/map-overlay -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/measuringtools/report/calibration", + "auth": "permission:measuringtools.view", + "params": "not documented", + "purpose": "Counts + lists bucketed by derived calibration status.", + "example": "curl http://localhost:5001/api/measuringtools/report/calibration -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/measuringtools/types", + "auth": "permission:measuringtools.view", + "params": "not documented", + "purpose": "List measuring-tool types.", + "example": "curl http://localhost:5001/api/measuringtools/types -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/measuringtools/types", + "auth": "permission:measuringtools.create", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create a measuring-tool type (reactivates a soft-deleted same-named one).", + "example": "curl -X POST http://localhost:5001/api/measuringtools/types -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/measuringtools/types/", + "auth": "permission:measuringtools.delete", + "params": "path: type_id (int)", + "purpose": "Delete a measuring-tool type. Refused if any tool still uses it.", + "example": "curl -X DELETE http://localhost:5001/api/measuringtools/types/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/measuringtools/types/", + "auth": "permission:measuringtools.view", + "params": "path: type_id (int)", + "purpose": "Get one measuring-tool type.", + "example": "curl http://localhost:5001/api/measuringtools/types/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/measuringtools/types/", + "auth": "permission:measuringtools.edit", + "params": "path: type_id (int); body: JSON (fields not yet documented)", + "purpose": "Update a measuring-tool type.", + "example": "curl -X PUT http://localhost:5001/api/measuringtools/types/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + } + ] + }, + { + "surface": "plugin-network", + "endpoints": [ + { + "method": "GET", + "path": "/api/network", + "auth": "permission:network.view", + "params": "not documented", + "purpose": "List all network devices with filtering and pagination.", + "example": "curl http://localhost:5001/api/network -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/network", + "auth": "permission:network.create", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create new network device (creates both Asset and NetworkDevice records).", + "example": "curl -X POST http://localhost:5001/api/network -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/network/", + "auth": "permission:network.delete", + "params": "path: device_id (int)", + "purpose": "Delete (soft delete) network device.", + "example": "curl -X DELETE http://localhost:5001/api/network/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/network/", + "auth": "permission:network.view", + "params": "path: device_id (int)", + "purpose": "Get a single network device with full details.", + "example": "curl http://localhost:5001/api/network/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/network/", + "auth": "permission:network.edit", + "params": "path: device_id (int); body: JSON (fields not yet documented)", + "purpose": "Update network device (both Asset and NetworkDevice records).", + "example": "curl -X PUT http://localhost:5001/api/network/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/network/by-asset/", + "auth": "permission:network.view", + "params": "path: asset_id (int)", + "purpose": "Get network device data by asset ID.", + "example": "curl http://localhost:5001/api/network/by-asset/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/network/by-hostname/", + "auth": "permission:network.view", + "params": "path: hostname (string)", + "purpose": "Get network device by hostname.", + "example": "curl http://localhost:5001/api/network/by-hostname/x -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/network/dashboard/summary", + "auth": "permission:network.view", + "params": "not documented", + "purpose": "Get network device dashboard summary data.", + "example": "curl http://localhost:5001/api/network/dashboard/summary -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/network/subnets", + "auth": "permission:network.view", + "params": "not documented", + "purpose": "List all subnets with filtering and pagination.", + "example": "curl http://localhost:5001/api/network/subnets -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/network/subnets", + "auth": "permission:network.create", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create a new subnet.", + "example": "curl -X POST http://localhost:5001/api/network/subnets -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/network/subnets/", + "auth": "permission:network.delete", + "params": "path: subnet_id (int)", + "purpose": "Delete (soft delete) a subnet.", + "example": "curl -X DELETE http://localhost:5001/api/network/subnets/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/network/subnets/", + "auth": "permission:network.view", + "params": "path: subnet_id (int)", + "purpose": "Get a single subnet plus the network devices whose primary IP falls in its CIDR range.", + "example": "curl http://localhost:5001/api/network/subnets/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/network/subnets/", + "auth": "permission:network.edit", + "params": "path: subnet_id (int); body: JSON (fields not yet documented)", + "purpose": "Update a subnet.", + "example": "curl -X PUT http://localhost:5001/api/network/subnets/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/network/types", + "auth": "permission:network.view", + "params": "not documented", + "purpose": "List all network device types.", + "example": "curl http://localhost:5001/api/network/types -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/network/types", + "auth": "permission:network.create", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create a new network device type.", + "example": "curl -X POST http://localhost:5001/api/network/types -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/network/types/", + "auth": "permission:network.delete", + "params": "path: type_id (int)", + "purpose": "Delete a network device type. Refused if any device still uses it.", + "example": "curl -X DELETE http://localhost:5001/api/network/types/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/network/types/", + "auth": "permission:network.view", + "params": "path: type_id (int)", + "purpose": "Get a single network device type.", + "example": "curl http://localhost:5001/api/network/types/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/network/types/", + "auth": "permission:network.edit", + "params": "path: type_id (int); body: JSON (fields not yet documented)", + "purpose": "Update a network device type.", + "example": "curl -X PUT http://localhost:5001/api/network/types/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/network/vlans", + "auth": "permission:network.view", + "params": "not documented", + "purpose": "List all VLANs with filtering and pagination.", + "example": "curl http://localhost:5001/api/network/vlans -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/network/vlans", + "auth": "permission:network.create", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create a new VLAN.", + "example": "curl -X POST http://localhost:5001/api/network/vlans -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/network/vlans/", + "auth": "permission:network.delete", + "params": "path: vlan_id (int)", + "purpose": "Delete (soft delete) a VLAN.", + "example": "curl -X DELETE http://localhost:5001/api/network/vlans/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/network/vlans/", + "auth": "permission:network.view", + "params": "path: vlan_id (int)", + "purpose": "Get a single VLAN with its subnets.", + "example": "curl http://localhost:5001/api/network/vlans/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/network/vlans/", + "auth": "permission:network.edit", + "params": "path: vlan_id (int); body: JSON (fields not yet documented)", + "purpose": "Update a VLAN.", + "example": "curl -X PUT http://localhost:5001/api/network/vlans/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + } + ] + }, + { + "surface": "plugin-notifications", + "endpoints": [ + { + "method": "GET", + "path": "/api/notifications", + "auth": "permission:notifications.view", + "params": "not documented", + "purpose": "List all notifications with filtering and pagination.", + "example": "curl http://localhost:5001/api/notifications -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/notifications", + "auth": "permission:notifications.create", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create a new notification.", + "example": "curl -X POST http://localhost:5001/api/notifications -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/notifications/", + "auth": "permission:notifications.delete", + "params": "path: notification_id (int)", + "purpose": "Delete (soft delete) a notification.", + "example": "curl -X DELETE http://localhost:5001/api/notifications/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/notifications/", + "auth": "permission:notifications.view", + "params": "path: notification_id (int)", + "purpose": "Get a single notification.", + "example": "curl http://localhost:5001/api/notifications/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/notifications/", + "auth": "permission:notifications.edit", + "params": "path: notification_id (int); body: JSON (fields not yet documented)", + "purpose": "Update a notification.", + "example": "curl -X PUT http://localhost:5001/api/notifications/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/notifications/active", + "auth": "permission:notifications.view", + "params": "not documented", + "purpose": "Get currently active notifications for display.", + "example": "curl http://localhost:5001/api/notifications/active -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/notifications/calendar", + "auth": "permission:notifications.view", + "params": "not documented", + "purpose": "Get notifications in FullCalendar event format.", + "example": "curl http://localhost:5001/api/notifications/calendar -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/notifications/dashboard/active", + "auth": "permission:notifications.view", + "params": "not documented", + "purpose": "Notifications currently showing on the floor.", + "example": "curl http://localhost:5001/api/notifications/dashboard/active -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/notifications/dashboard/summary", + "auth": "permission:notifications.view", + "params": "not documented", + "purpose": "Get notifications dashboard summary.", + "example": "curl http://localhost:5001/api/notifications/dashboard/summary -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/notifications/employee/", + "auth": "none (reviewed: named in the public-read or mutation sweep)", + "params": "path: sso (string)", + "purpose": "Get recognitions for a specific employee by SSO.", + "example": "curl http://localhost:5001/api/notifications/employee/x" + }, + { + "method": "GET", + "path": "/api/notifications/shopfloor", + "auth": "none (reviewed: named in the public-read or mutation sweep)", + "params": "not documented", + "purpose": "Get notifications for shopfloor TV dashboard.", + "example": "curl http://localhost:5001/api/notifications/shopfloor" + }, + { + "method": "GET", + "path": "/api/notifications/types", + "auth": "permission:notifications.view", + "params": "not documented", + "purpose": "List all notification types.", + "example": "curl http://localhost:5001/api/notifications/types -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/notifications/types", + "auth": "permission:notifications.create", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create a new notification type.", + "example": "curl -X POST http://localhost:5001/api/notifications/types -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "PATCH|PUT", + "path": "/api/notifications/types/", + "auth": "permission:notifications.create", + "params": "path: type_id (int); body: JSON (fields not yet documented)", + "purpose": "Update a notification type, including its auto-expiry rule.", + "example": "curl -X PATCH http://localhost:5001/api/notifications/types/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + } + ] + }, + { + "surface": "plugin-printedparts", + "endpoints": [ + { + "method": "DELETE", + "path": "/api/printedparts/files/", + "auth": "permission:printedparts.delete", + "params": "path: file_id (int)", + "purpose": "Remove a bad revision (wrong file uploaded). History otherwise stays.", + "example": "curl -X DELETE http://localhost:5001/api/printedparts/files/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/printedparts/files//download", + "auth": "permission:printedparts.view", + "params": "path: file_id (int)", + "purpose": "Download a revision under its original filename.", + "example": "curl http://localhost:5001/api/printedparts/files/1/download -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/printedparts/image/", + "auth": "none (reviewed: named in the public-read or mutation sweep)", + "params": "path: filename (path)", + "purpose": "Serve an uploaded item image (public - kiosk and list read it).", + "example": "curl http://localhost:5001/api/printedparts/image/example.png" + }, + { + "method": "GET", + "path": "/api/printedparts/items", + "auth": "permission:printedparts.view", + "params": "not documented", + "purpose": "List printed items, paginated; search + low-stock filter.", + "example": "curl http://localhost:5001/api/printedparts/items -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/printedparts/items", + "auth": "permission:printedparts.create", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create a printed item.", + "example": "curl -X POST http://localhost:5001/api/printedparts/items -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/printedparts/items/", + "auth": "permission:printedparts.delete", + "params": "path: item_id (int)", + "purpose": "Soft-retire an item; its ledger history stays.", + "example": "curl -X DELETE http://localhost:5001/api/printedparts/items/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/printedparts/items/", + "auth": "permission:printedparts.view", + "params": "path: item_id (int)", + "purpose": "Get one printed item with its recent transactions.", + "example": "curl http://localhost:5001/api/printedparts/items/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/printedparts/items/", + "auth": "permission:printedparts.edit", + "params": "path: item_id (int); body: JSON (fields not yet documented)", + "purpose": "Update catalog fields. Quantity moves ONLY through the ledger.", + "example": "curl -X PUT http://localhost:5001/api/printedparts/items/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/printedparts/items//adjust", + "auth": "permission:printedparts.restock", + "params": "path: item_id (int); body: JSON (fields not yet documented)", + "purpose": "Correct the count (damage, recount). Body: {quantitychange, reason, badge}.", + "example": "curl -X POST http://localhost:5001/api/printedparts/items/1/adjust -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/printedparts/items//files", + "auth": "permission:printedparts.view", + "params": "path: item_id (int)", + "purpose": "Revision history, newest first.", + "example": "curl http://localhost:5001/api/printedparts/items/1/files -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/printedparts/items//files", + "auth": "permission:printedparts.edit", + "params": "path: item_id (int); body: JSON (fields not yet documented)", + "purpose": "Upload the next revision of the item's print file.", + "example": "curl -X POST http://localhost:5001/api/printedparts/items/1/files -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/printedparts/items//image", + "auth": "permission:printedparts.delete", + "params": "path: item_id (int)", + "purpose": "Clear an item image; delete the file only if this plugin owns it.", + "example": "curl -X DELETE http://localhost:5001/api/printedparts/items/1/image -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/printedparts/items//image", + "auth": "permission:printedparts.edit", + "params": "path: item_id (int); body: JSON (fields not yet documented)", + "purpose": "Upload (or replace) the photo for an item (multipart file=).", + "example": "curl -X POST http://localhost:5001/api/printedparts/items/1/image -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/printedparts/items//restock", + "auth": "permission:printedparts.restock", + "params": "path: item_id (int); body: JSON (fields not yet documented)", + "purpose": "Add freshly printed stock. Body: {quantity, badge}.", + "example": "curl -X POST http://localhost:5001/api/printedparts/items/1/restock -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/printedparts/items//restore", + "auth": "permission:printedparts.delete", + "params": "path: item_id (int); body: JSON (fields not yet documented)", + "purpose": "Bring a retired item back; code, photo, and history are intact.", + "example": "curl -X POST http://localhost:5001/api/printedparts/items/1/restore -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/printedparts/kiosk/item/", + "auth": "api-key-optional (kiosk: the per-device token in the X-Kiosk-Token header, OR a caller address inside the site's kiosk CIDR allowlist - an allowlisted caller sends no credential; 404 while the site's kiosk flag is off)", + "params": "path: itemcode (string)", + "purpose": "Item summary for a scanned bin barcode. Kiosk-token gated.", + "example": "curl http://localhost:5001/api/printedparts/kiosk/item/x" + }, + { + "method": "GET", + "path": "/api/printedparts/kiosk/provisioning-link", + "auth": "permission:settings.edit", + "params": "not documented", + "purpose": "The URL that provisions a screen, token included, for an administrator.", + "example": "curl http://localhost:5001/api/printedparts/kiosk/provisioning-link -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/printedparts/kiosk/take", + "auth": "api-key-optional (kiosk: the per-device token in the X-Kiosk-Token header, OR a caller address inside the site's kiosk CIDR allowlist - an allowlisted caller sends no credential; 404 while the site's kiosk flag is off)", + "params": "body: JSON (fields not yet documented)", + "purpose": "Take parts from a bin. Body: {itemcode, badge, quantity}.", + "example": "curl -X POST http://localhost:5001/api/printedparts/kiosk/take -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/printedparts/reports/by-person", + "auth": "permission:printedparts.view", + "params": "not documented", + "purpose": "Takes grouped by employee, optionally bounded by ?days= (default 30).", + "example": "curl http://localhost:5001/api/printedparts/reports/by-person -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/printedparts/reports/consumption", + "auth": "permission:printedparts.view", + "params": "not documented", + "purpose": "Takes per item, optionally bounded by ?days= (default 30).", + "example": "curl http://localhost:5001/api/printedparts/reports/consumption -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/printedparts/reports/stock", + "auth": "permission:printedparts.view", + "params": "not documented", + "purpose": "Stock levels with low-stock flags and the cache-vs-ledger reconcile.", + "example": "curl http://localhost:5001/api/printedparts/reports/stock -H \"Authorization: Bearer $TOK\"" + } + ] + }, + { + "surface": "plugin-printers", + "endpoints": [ + { + "method": "GET", + "path": "/api/printers", + "auth": "permission:printers.view", + "params": "not documented", + "purpose": "List all printers with filtering and pagination.", + "example": "curl http://localhost:5001/api/printers -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/printers", + "auth": "permission:printers.create", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create new printer (creates both Asset and Printer records).", + "example": "curl -X POST http://localhost:5001/api/printers -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/printers/", + "auth": "permission:printers.delete", + "params": "path: printer_id (int)", + "purpose": "Delete (soft delete) printer.", + "example": "curl -X DELETE http://localhost:5001/api/printers/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/printers/", + "auth": "permission:printers.view", + "params": "path: printer_id (int)", + "purpose": "Get a single printer with full details.", + "example": "curl http://localhost:5001/api/printers/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/printers/", + "auth": "permission:printers.edit", + "params": "path: printer_id (int); body: JSON (fields not yet documented)", + "purpose": "Update printer (both Asset and Printer records).", + "example": "curl -X PUT http://localhost:5001/api/printers/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/printers//supplies", + "auth": "permission:printers.view", + "params": "path: printer_id (int)", + "purpose": "Get supply levels from Zabbix (real-time lookup).", + "example": "curl http://localhost:5001/api/printers/1/supplies -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/printers/assignments/for-asset/", + "auth": "permission:printers.view", + "params": "path: asset_id (int)", + "purpose": "What THIS asset is assigned, without inheritance.", + "example": "curl http://localhost:5001/api/printers/assignments/for-asset/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/printers/assignments/for-asset/", + "auth": "permission:printers.edit", + "params": "path: asset_id (int); body: JSON (fields not yet documented)", + "purpose": "Reconcile one asset's whole printer assignment in a single call.", + "example": "curl -X PUT http://localhost:5001/api/printers/assignments/for-asset/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/printers/assignments/seed-from-observed/", + "auth": "permission:printers.edit", + "params": "path: asset_id (int); body: JSON (fields not yet documented)", + "purpose": "Write what a PC observed as the assignment of the asset in the path.", + "example": "curl -X POST http://localhost:5001/api/printers/assignments/seed-from-observed/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/printers/by-asset/", + "auth": "permission:printers.view", + "params": "path: asset_id (int)", + "purpose": "Get printer data by asset ID.", + "example": "curl http://localhost:5001/api/printers/by-asset/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/printers/dashboard/summary", + "auth": "permission:printers.view", + "params": "not documented", + "purpose": "Get printer dashboard summary data.", + "example": "curl http://localhost:5001/api/printers/dashboard/summary -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/printers/dashboard/supplies", + "auth": "permission:printers.view", + "params": "not documented", + "purpose": "Printers needing a cartridge, flattened to one row per printer.", + "example": "curl http://localhost:5001/api/printers/dashboard/supplies -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/printers/drivers", + "auth": "permission:printers.view", + "params": "not documented", + "purpose": "List printer drivers. ?active=false includes inactive ones.", + "example": "curl http://localhost:5001/api/printers/drivers -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/printers/drivers", + "auth": "permission:printers.create", + "params": "body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: printers_asset.create_driver has no docstring.", + "example": "curl -X POST http://localhost:5001/api/printers/drivers -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/printers/drivers/", + "auth": "permission:printers.delete", + "params": "path: driver_id (int)", + "purpose": "UNDOCUMENTED: printers_asset.delete_driver has no docstring.", + "example": "curl -X DELETE http://localhost:5001/api/printers/drivers/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/printers/drivers/", + "auth": "permission:printers.edit", + "params": "path: driver_id (int); body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: printers_asset.update_driver has no docstring.", + "example": "curl -X PUT http://localhost:5001/api/printers/drivers/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/printers/for-host/", + "auth": "jwt-optional (works signed out; the handler decides what shows)", + "params": "path: hostname (string)", + "purpose": "Printers assigned to a PC, by hostname, with what it takes to install one.", + "example": "curl http://localhost:5001/api/printers/for-host/x -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/printers/install-batch", + "auth": "jwt-optional (works signed out; the handler decides what shows)", + "params": "not documented", + "purpose": "Generate a Windows .bat that installs the selected printers.", + "example": "curl http://localhost:5001/api/printers/install-batch -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/printers/install-list", + "auth": "jwt-optional (works signed out; the handler decides what shows)", + "params": "not documented", + "purpose": "Flat, unpaginated list of network printers for the printer installer.", + "example": "curl http://localhost:5001/api/printers/install-list -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/printers/lookup", + "auth": "permission:printers.view", + "params": "not documented", + "purpose": "Find a printer by IP or FQDN. Parity with the classic printerlookup.asp.", + "example": "curl http://localhost:5001/api/printers/lookup -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/printers/lowsupplies", + "auth": "permission:printers.view", + "params": "not documented", + "purpose": "Get printers with low or critical supply levels.", + "example": "curl http://localhost:5001/api/printers/lowsupplies -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/printers/models", + "auth": "permission:printers.view", + "params": "not documented", + "purpose": "List models with a supply count, for the supply-management picker.", + "example": "curl http://localhost:5001/api/printers/models -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/printers/models//supplies", + "auth": "permission:printers.view", + "params": "path: modelnumberid (int)", + "purpose": "List all supplies mapped to a model.", + "example": "curl http://localhost:5001/api/printers/models/1/supplies -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/printers/models//supplies", + "auth": "permission:printers.create", + "params": "path: modelnumberid (int); body: JSON (fields not yet documented)", + "purpose": "Add a supply to a model.", + "example": "curl -X POST http://localhost:5001/api/printers/models/1/supplies -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/printers/observed/", + "auth": "permission:printers.view", + "params": "path: hostname (string)", + "purpose": "What a host last reported, each queue judged against what it is assigned.", + "example": "curl http://localhost:5001/api/printers/observed/x -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/printers/observed/for-asset/", + "auth": "permission:printers.view", + "params": "path: asset_id (int)", + "purpose": "The same comparison, reached from an asset page: one block per host.", + "example": "curl http://localhost:5001/api/printers/observed/for-asset/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/printers/pc-default", + "auth": "jwt-optional (works signed out; the handler decides what shows)", + "params": "not documented", + "purpose": "Default printer for a PC, by machine (asset) number.", + "example": "curl http://localhost:5001/api/printers/pc-default -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "DELETE", + "path": "/api/printers/supplies/", + "auth": "permission:printers.delete", + "params": "path: modelsupplyid (int)", + "purpose": "Delete a model supply.", + "example": "curl -X DELETE http://localhost:5001/api/printers/supplies/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/printers/supplies/", + "auth": "permission:printers.edit", + "params": "path: modelsupplyid (int); body: JSON (fields not yet documented)", + "purpose": "Update a model supply.", + "example": "curl -X PUT http://localhost:5001/api/printers/supplies/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/printers/supplies/forecast", + "auth": "permission:printers.view", + "params": "not documented", + "purpose": "Days-to-empty and replacement counts per printer.", + "example": "curl http://localhost:5001/api/printers/supplies/forecast -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/printers/supplies/meta", + "auth": "permission:printers.view", + "params": "not documented", + "purpose": "Allowed values for supply type, color, and capacity tier (for the UI).", + "example": "curl http://localhost:5001/api/printers/supplies/meta -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/printers/supplies/refresh", + "auth": "permission:printers.create", + "params": "body: JSON (fields not yet documented)", + "purpose": "Clear cached Zabbix supply data so the next read pulls fresh values.", + "example": "curl -X POST http://localhost:5001/api/printers/supplies/refresh -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/printers/types", + "auth": "permission:printers.view", + "params": "not documented", + "purpose": "List all printer types.", + "example": "curl http://localhost:5001/api/printers/types -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/printers/types", + "auth": "permission:printers.create", + "params": "body: JSON (fields not yet documented)", + "purpose": "Create a new printer type.", + "example": "curl -X POST http://localhost:5001/api/printers/types -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/printers/types/", + "auth": "permission:printers.delete", + "params": "path: type_id (int)", + "purpose": "Delete a printer type. Refused if any printer still uses it.", + "example": "curl -X DELETE http://localhost:5001/api/printers/types/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/printers/types/", + "auth": "permission:printers.view", + "params": "path: type_id (int)", + "purpose": "Get a single printer type.", + "example": "curl http://localhost:5001/api/printers/types/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/printers/types/", + "auth": "permission:printers.edit", + "params": "path: type_id (int); body: JSON (fields not yet documented)", + "purpose": "Update a printer type.", + "example": "curl -X PUT http://localhost:5001/api/printers/types/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + } + ] + }, + { + "surface": "plugin-slides", + "endpoints": [ + { + "method": "GET", + "path": "/api/slides/", + "auth": "permission:slides.view or slides.manage", + "params": "path: surface (string)", + "purpose": "UNDOCUMENTED: slides.list_slides has no docstring.", + "example": "curl http://localhost:5001/api/slides/x -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PATCH", + "path": "/api/slides//", + "auth": "permission:slides.manage", + "params": "path: surface (string), slideid (int); body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: slides.update_slide has no docstring.", + "example": "curl -X PATCH http://localhost:5001/api/slides/x/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/slides//delete", + "auth": "permission:slides.manage", + "params": "path: surface (string); body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: slides.delete_slides has no docstring.", + "example": "curl -X POST http://localhost:5001/api/slides/x/delete -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/slides//order", + "auth": "permission:slides.manage", + "params": "path: surface (string); body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: slides.reorder_slides has no docstring.", + "example": "curl -X POST http://localhost:5001/api/slides/x/order -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/slides//upload", + "auth": "permission:slides.manage", + "params": "path: surface (string); body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: slides.upload_slides has no docstring.", + "example": "curl -X POST http://localhost:5001/api/slides/x/upload -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/slides/feed", + "auth": "none (reviewed: named in the public-read or mutation sweep)", + "params": "not documented", + "purpose": "Flat playlist for a surface. NOT wrapped in the success_response envelope so the screensaver's existing parser needs no change.", + "example": "curl http://localhost:5001/api/slides/feed" + }, + { + "method": "GET", + "path": "/api/slides/img//", + "auth": "none (reviewed: named in the public-read or mutation sweep)", + "params": "path: surface (string), filename (path)", + "purpose": "Serve a slide image with a path-traversal guard.", + "example": "curl http://localhost:5001/api/slides/img/x/example.png" + } + ] + }, + { + "surface": "plugin-tools", + "endpoints": [ + { + "method": "POST", + "path": "/api/tools/secrets", + "auth": "jwt-optional (works signed out; the handler decides what shows)", + "params": "body: JSON (fields not yet documented)", + "purpose": "Store an already-encrypted secret and return its token.", + "example": "curl -X POST http://localhost:5001/api/tools/secrets -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/tools/secrets/", + "auth": "none (the unguessable token in the path is the authorisation)", + "params": "path: token (string)", + "purpose": "Whether a link is still live, WITHOUT spending it.", + "example": "curl http://localhost:5001/api/tools/secrets/x" + }, + { + "method": "POST", + "path": "/api/tools/secrets/", + "auth": "none (the unguessable token in the path is the authorisation; redeeming spends the secret)", + "params": "path: token (string); body: JSON (fields not yet documented)", + "purpose": "Hand over the ciphertext once, then spend the view.", + "example": "curl -X POST http://localhost:5001/api/tools/secrets/x -H 'Content-Type: application/json' -d '{}'" + } + ] + }, + { + "surface": "plugin-usb", + "endpoints": [ + { + "method": "GET", + "path": "/api/usb", + "auth": "permission:usb.view", + "params": "not documented", + "purpose": "List USB devices with checkout status.", + "example": "curl http://localhost:5001/api/usb -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/usb", + "auth": "permission:usb.create", + "params": "body: JSON (fields not yet documented)", + "purpose": "Add a device (starts checked-in).", + "example": "curl -X POST http://localhost:5001/api/usb -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/usb/", + "auth": "permission:usb.view", + "params": "path: device_id (string)", + "purpose": "Get a single device plus its last 20 check-in/out log rows.", + "example": "curl http://localhost:5001/api/usb/x -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/usb/", + "auth": "permission:usb.edit", + "params": "path: device_id (string); body: JSON (fields not yet documented)", + "purpose": "Edit device_desc / device_owner / locker_location / status.", + "example": "curl -X PUT http://localhost:5001/api/usb/x -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/usb//checkin", + "auth": "permission:usb.create", + "params": "path: device_id (string); body: JSON (fields not yet documented)", + "purpose": "Check a device back in from a badge.", + "example": "curl -X POST http://localhost:5001/api/usb/x/checkin -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/usb//checkout", + "auth": "permission:usb.create", + "params": "path: device_id (string); body: JSON (fields not yet documented)", + "purpose": "Check a device out to a badge.", + "example": "curl -X POST http://localhost:5001/api/usb/x/checkout -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/usb//history", + "auth": "permission:usb.view", + "params": "path: device_id (string)", + "purpose": "Paginated check-in/out log for one device.", + "example": "curl http://localhost:5001/api/usb/x/history -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/usb//retire", + "auth": "permission:usb.edit", + "params": "path: device_id (string); body: JSON (fields not yet documented)", + "purpose": "Retire a device (status -> retired).", + "example": "curl -X POST http://localhost:5001/api/usb/x/retire -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/usb/checkouts", + "auth": "permission:usb.view", + "params": "not documented", + "purpose": "List check-out log rows.", + "example": "curl http://localhost:5001/api/usb/checkouts -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/usb/checkouts/active", + "auth": "permission:usb.view", + "params": "not documented", + "purpose": "List the latest check-out log row for every currently checked-out device.", + "example": "curl http://localhost:5001/api/usb/checkouts/active -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/usb/kiosk/checkin", + "auth": "api-key-optional (kiosk: the per-device token in the X-Kiosk-Token header, OR a caller address inside the site's kiosk CIDR allowlist - an allowlisted caller sends no credential; 404 while the site's kiosk flag is off)", + "params": "body: JSON (fields not yet documented)", + "purpose": "Check a device in at the kiosk. Body: {device_id, badge, forsomeoneelse}.", + "example": "curl -X POST http://localhost:5001/api/usb/kiosk/checkin -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/usb/kiosk/checkout", + "auth": "api-key-optional (kiosk: the per-device token in the X-Kiosk-Token header, OR a caller address inside the site's kiosk CIDR allowlist - an allowlisted caller sends no credential; 404 while the site's kiosk flag is off)", + "params": "body: JSON (fields not yet documented)", + "purpose": "Check a device out at the kiosk. Body: {device_id, badge}.", + "example": "curl -X POST http://localhost:5001/api/usb/kiosk/checkout -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/usb/kiosk/device/", + "auth": "api-key-optional (kiosk: the per-device token in the X-Kiosk-Token header, OR a caller address inside the site's kiosk CIDR allowlist - an allowlisted caller sends no credential; 404 while the site's kiosk flag is off)", + "params": "path: device_id (string)", + "purpose": "What the kiosk shows after a device is scanned.", + "example": "curl http://localhost:5001/api/usb/kiosk/device/x" + }, + { + "method": "POST", + "path": "/api/usb/kiosk/others", + "auth": "api-key-optional (kiosk: the per-device token in the X-Kiosk-Token header, OR a caller address inside the site's kiosk CIDR allowlist - an allowlisted caller sends no credential; 404 while the site's kiosk flag is off)", + "params": "body: JSON (fields not yet documented)", + "purpose": "Drives checked out to OTHER people, with holder names. Body: {badge}.", + "example": "curl -X POST http://localhost:5001/api/usb/kiosk/others -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/usb/kiosk/provisioning-link", + "auth": "permission:settings.edit", + "params": "not documented", + "purpose": "The URL that provisions a screen, token included, for an administrator.", + "example": "curl http://localhost:5001/api/usb/kiosk/provisioning-link -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/usb/kiosk/session", + "auth": "api-key-optional (kiosk: the per-device token in the X-Kiosk-Token header, OR a caller address inside the site's kiosk CIDR allowlist - an allowlisted caller sends no credential; 404 while the site's kiosk flag is off)", + "params": "body: JSON (fields not yet documented)", + "purpose": "What one badge may do right now. Body: {badge}.", + "example": "curl -X POST http://localhost:5001/api/usb/kiosk/session -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/usb/kiosk/setup", + "auth": "setup-code (single-use kiosk setup code in ?code=, issued by the permission-gated provisioning link; spent on any attempt, 410 once used or expired)", + "params": "not documented", + "purpose": "Trade a setup code for this browser's kiosk cookie, then open the kiosk.", + "example": "curl http://localhost:5001/api/usb/kiosk/setup" + }, + { + "method": "GET", + "path": "/api/usb/reports/audit", + "auth": "permission:usb.view", + "params": "not documented", + "purpose": "Every check-out and check-in in the window, newest first.", + "example": "curl http://localhost:5001/api/usb/reports/audit -H \"Authorization: Bearer $TOK\"" + } + ] + }, + { + "surface": "plugin-warranty", + "endpoints": [ + { + "method": "GET", + "path": "/api/warranty", + "auth": "permission:warranty.view", + "params": "not documented", + "purpose": "List warranties. Filters: ?status=, ?assetid=, ?active=false.", + "example": "curl http://localhost:5001/api/warranty -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/warranty", + "auth": "permission:warranty.create", + "params": "body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: warranty.create_warranty has no docstring.", + "example": "curl -X POST http://localhost:5001/api/warranty -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/warranty/", + "auth": "permission:warranty.delete", + "params": "path: warrantyid (int)", + "purpose": "UNDOCUMENTED: warranty.delete_warranty has no docstring.", + "example": "curl -X DELETE http://localhost:5001/api/warranty/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/warranty/", + "auth": "permission:warranty.view", + "params": "path: warrantyid (int)", + "purpose": "UNDOCUMENTED: warranty.get_warranty has no docstring.", + "example": "curl http://localhost:5001/api/warranty/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "PUT", + "path": "/api/warranty/", + "auth": "permission:warranty.edit", + "params": "path: warrantyid (int); body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: warranty.update_warranty has no docstring.", + "example": "curl -X PUT http://localhost:5001/api/warranty/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "DELETE", + "path": "/api/warranty//proof", + "auth": "permission:warranty.edit", + "params": "path: warrantyid (int)", + "purpose": "Remove a warranty's proof document.", + "example": "curl -X DELETE http://localhost:5001/api/warranty/1/proof -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/warranty//proof", + "auth": "permission:warranty.edit", + "params": "path: warrantyid (int); body: JSON (fields not yet documented)", + "purpose": "Upload (or replace) the proof-of-cover document for a warranty.", + "example": "curl -X POST http://localhost:5001/api/warranty/1/proof -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "POST", + "path": "/api/warranty//refresh", + "auth": "permission:warranty.edit", + "params": "path: warrantyid (int); body: JSON (fields not yet documented)", + "purpose": "UNDOCUMENTED: warranty.refresh_warranty has no docstring.", + "example": "curl -X POST http://localhost:5001/api/warranty/1/refresh -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + }, + { + "method": "GET", + "path": "/api/warranty/asset/", + "auth": "permission:warranty.view", + "params": "path: assetid (int)", + "purpose": "Warranties covering one asset (for the asset-detail panel).", + "example": "curl http://localhost:5001/api/warranty/asset/1 -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/warranty/dashboard/expiring", + "auth": "permission:warranty.view", + "params": "not documented", + "purpose": "Warranties running out, and ones that already have.", + "example": "curl http://localhost:5001/api/warranty/dashboard/expiring -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/warranty/proof/", + "auth": "permission:warranty.view", + "params": "path: filename (path)", + "purpose": "Download a proof document.", + "example": "curl http://localhost:5001/api/warranty/proof/example.png -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "GET", + "path": "/api/warranty/report", + "auth": "permission:warranty.view", + "params": "not documented", + "purpose": "Counts + lists bucketed by derived status.", + "example": "curl http://localhost:5001/api/warranty/report -H \"Authorization: Bearer $TOK\"" + }, + { + "method": "POST", + "path": "/api/warranty/sync/dell", + "auth": "permission:warranty.edit", + "params": "body: JSON (fields not yet documented)", + "purpose": "Look up every asset serial (= Dell service tag) that has no warranty yet and create warranties for the ones Dell recognizes.", + "example": "curl -X POST http://localhost:5001/api/warranty/sync/dell -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'" + } + ] + } +] diff --git a/docs/openapi.json b/docs/openapi.json index 683ad40b..0dd38f91 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -3,7 +3,7 @@ "info": { "title": "ShopDB Flask API", "version": "0.12.0", - "description": "Asset-management API (core + plugins). Responses use a `success_response` envelope: `{status, data, meta}`. Auth: Bearer JWT (login or a managed PAT) for `jwt`/`admin`/`permission:*`; `X-API-Key` for collector/managed-token endpoints; public endpoints need neither." + "description": "Asset-management API (core + plugins). Responses use a `success_response` envelope: `{status, data, meta}`. Auth: Bearer JWT (login or a managed PAT) for `jwt`/`admin`/`permission:*`; `X-API-Key` for collector/managed-token endpoints; public endpoints need neither. A permission-gated route is published as bearer-required, which is what a default install does - a site that grants the permission to the `guest` role makes that same route answer a caller with no token." }, "servers": [ { @@ -115,809 +115,161 @@ "type": "apiKey", "in": "header", "name": "X-API-Key", - "description": "Managed service token (e.g. collector.ingest, geenforce.fetch)." + "description": "A managed service or device token in a header. X-API-Key for collector and managed tokens (collector.ingest, geenforce.fetch); X-Endpoint-Bootstrap-Token for agent bootstrap; X-Kiosk-Token for a kiosk screen. The operation description names the header and, where the gate is token-OR-network, the allowlist that is the other half." + }, + "setupCodeAuth": { + "type": "apiKey", + "in": "query", + "name": "code", + "description": "A single-use provisioning code, redeemed in the query string because the caller is a browser being pointed at a URL. Spent on any attempt, right or wrong, and short-lived." } } }, "tags": [ { - "name": "core-identity" + "name": "core-apitokens" }, { - "name": "core-platform" + "name": "core-applications" }, { - "name": "plugin-employees" + "name": "core-assets" }, { - "name": "plugin-knowledgebase" + "name": "core-auditlogs" }, { - "name": "core-catalog" + "name": "core-auth" }, { - "name": "plugin-computers" + "name": "core-businessunits" }, { - "name": "plugin-measuringtools" + "name": "core-collector" }, { - "name": "plugin-machines" + "name": "core-customfields" }, { - "name": "plugin-geenforce" + "name": "core-dashboard" }, { - "name": "plugin-notifications" - }, - { - "name": "plugin-slides" - }, - { - "name": "plugin-printedparts" - }, - { - "name": "plugin-network" - }, - { - "name": "plugin-usb" - }, - { - "name": "plugin-printers" - }, - { - "name": "plugin-warranty" - }, - { - "name": "plugin-backups" - }, - { - "name": "plugin-tools" + "name": "core-dashboarddefaults" }, { "name": "core-docs" }, { - "name": "core-map" + "name": "core-locations" }, { - "name": "guides" + "name": "core-maplevels" }, { - "name": "plugin-endpoints" + "name": "core-mappositions" + }, + { + "name": "core-models" + }, + { + "name": "core-modeltypes" + }, + { + "name": "core-operatingsystems" + }, + { + "name": "core-plugins" + }, + { + "name": "core-pluginui" + }, + { + "name": "core-reports" }, { "name": "core-scripts" }, { - "name": "endpoints-delivery" + "name": "core-search" }, { - "name": "endpoints-siteagent" + "name": "core-settings" + }, + { + "name": "core-setup" }, { "name": "core-shopdbbackups" + }, + { + "name": "core-supportteams" + }, + { + "name": "core-users" + }, + { + "name": "core-vendors" + }, + { + "name": "plugin-backups" + }, + { + "name": "plugin-computers" + }, + { + "name": "plugin-employees" + }, + { + "name": "plugin-endpoints" + }, + { + "name": "plugin-geenforce" + }, + { + "name": "plugin-guides" + }, + { + "name": "plugin-knowledgebase" + }, + { + "name": "plugin-machines" + }, + { + "name": "plugin-measuringtools" + }, + { + "name": "plugin-network" + }, + { + "name": "plugin-notifications" + }, + { + "name": "plugin-printedparts" + }, + { + "name": "plugin-printers" + }, + { + "name": "plugin-slides" + }, + { + "name": "plugin-tools" + }, + { + "name": "plugin-usb" + }, + { + "name": "plugin-warranty" } ], "paths": { - "/api/auth/login": { - "post": { - "tags": [ - "core-identity" - ], - "summary": "Authenticate and issue JWT access+refresh tokens; per-IP fixed-window rate limit (429) and 5-strike/15-min account...", - "description": "Authenticate and issue JWT access+refresh tokens; per-IP fixed-window rate limit (429) and 5-strike/15-min account lockout\n\n**Auth:** none\n\n**Params:** body: username, password (both required)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/auth/login -H 'Content-Type: application/json' -d '{\"username\":\"admin\",\"password\":\"secret123\"}'\n```", - "security": [], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: username, password (both required)" - } - } - } - } - } - }, - "/api/auth/refresh": { - "post": { - "tags": [ - "core-identity" - ], - "summary": "Exchange a refresh token for a new access token (rejects inactive/deleted users)", - "description": "Exchange a refresh token for a new access token (rejects inactive/deleted users)\n\n**Auth:** jwt (refresh token)\n\n**Params:** none; refresh token in Authorization header\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/auth/refresh -H \"Authorization: Bearer $REFRESH_TOKEN\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "none; refresh token in Authorization header" - } - } - } - } - } - }, - "/api/auth/me": { - "get": { - "tags": [ - "core-identity" - ], - "summary": "Return the authenticated user's profile, roles, permissions, mustchangepassword flag", - "description": "Return the authenticated user's profile, roles, permissions, mustchangepassword flag\n\n**Auth:** jwt\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/auth/me -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - } - }, - "/api/auth/change-password": { - "post": { - "tags": [ - "core-identity" - ], - "summary": "Self-service password change; forced first-login change skips current_password; clears lockout state", - "description": "Self-service password change; forced first-login change skips current_password; clears lockout state\n\n**Auth:** jwt\n\n**Params:** body: new_password (min 8, required), current_password (required unless mustchangepassword is set)\n\n**Example:**\n```\ncurl -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\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: new_password (min 8, required), current_password (required unless mustchangepassword is set)" - } - } - } - } - } - }, - "/api/auth/logout": { - "post": { - "tags": [ - "core-identity" - ], - "summary": "Logout stub for frontend token cleanup (no server-side blacklist yet)", - "description": "Logout stub for frontend token cleanup (no server-side blacklist yet)\n\n**Auth:** jwt\n\n**Params:** none\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/auth/logout -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "none" - } - } - } - } - } - }, - "/api/users": { - "get": { - "tags": [ - "core-identity" - ], - "summary": "List all users ordered by username", - "description": "List all users ordered by username\n\n**Auth:** jwt + require_role admin\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/users -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - }, - "post": { - "tags": [ - "core-identity" - ], - "summary": "Create a user, assign roles, audit-log, and best-effort send a welcome email with the temp password", - "description": "Create a user, assign roles, audit-log, and best-effort send a welcome email with the temp password\n\n**Auth:** jwt + require_role admin\n\n**Params:** body: username, email, password (required); firstname, lastname, isactive, roles[roleids], mustchangepassword (default true), sendwelcome (default true)\n\n**Example:**\n```\ncurl -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]}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: username, email, password (required); firstname, lastname, isactive, roles[roleids], mustchangepassword (default true), sendwelcome (default true)" - } - } - } - } - } - }, - "/api/users/{userid}": { - "get": { - "tags": [ - "core-identity" - ], - "summary": "Get one user; non-admins may only fetch their own record (403 otherwise)", - "description": "Get one user; non-admins may only fetch their own record (403 otherwise)\n\n**Auth:** jwt (admin or self, checked inline)\n\n**Params:** path: userid (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/users/7 -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "userid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - }, - "put": { - "tags": [ - "core-identity" - ], - "summary": "Update a user; email uniqueness enforced; admins can also change active state, roles, and unlock the account; changes...", - "description": "Update a user; email uniqueness enforced; admins can also change active state, roles, and unlock the account; changes audit-logged\n\n**Auth:** jwt (admin or self; isactive/roles/unlock fields admin-only)\n\n**Params:** path: userid; body: email, firstname, lastname, password; admin-only: isactive, roles[roleids], unlock (bool)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/users/7 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"firstname\":\"Jane\",\"unlock\":true}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "userid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "path: userid; body: email, firstname, lastname, password; admin-only: isactive, roles[roleids], unlock (bool)" - } - } - } - } - }, - "delete": { - "tags": [ - "core-identity" - ], - "summary": "Delete a user; revokes their API tokens and detaches (nulls userid on) their audit-log rows first", - "description": "Delete a user; revokes their API tokens and detaches (nulls userid on) their audit-log rows first\n\n**Auth:** jwt + require_role admin\n\n**Params:** path: userid (cannot be your own account)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/users/7 -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "userid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/users/permissions": { - "get": { - "tags": [ - "core-identity" - ], - "summary": "List assignable permissions (core + enabled plugins) both flat and grouped by category, for the role grid", - "description": "List assignable permissions (core + enabled plugins) both flat and grouped by category, for the role grid\n\n**Auth:** jwt\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/users/permissions -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - } - }, - "/api/users/roles": { - "get": { - "tags": [ - "core-identity" - ], - "summary": "List all roles with description, color, user count, permission names, and isadmin flag", - "description": "List all roles with description, color, user count, permission names, and isadmin flag\n\n**Auth:** jwt\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/users/roles -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - }, - "post": { - "tags": [ - "core-identity" - ], - "summary": "Create a role and assign permissions by name; 409 if rolename exists", - "description": "Create a role and assign permissions by name; 409 if rolename exists\n\n**Auth:** jwt + require_role admin\n\n**Params:** body: rolename (required), description, color, permissions[names]\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/users/roles -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"rolename\":\"viewer\",\"permissions\":[\"assets.view\"]}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: rolename (required), description, color, permissions[names]" - } - } - } - } - } - }, - "/api/users/roles/{roleid}": { - "put": { - "tags": [ - "core-identity" - ], - "summary": "Update a role's description/color/permissions; admin role's permission set cannot be modified; audit-logged", - "description": "Update a role's description/color/permissions; admin role's permission set cannot be modified; audit-logged\n\n**Auth:** jwt + require_role admin\n\n**Params:** path: roleid; body: description, color, permissions[names] (permissions immutable on the admin role)\n\n**Example:**\n```\ncurl -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\"]}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "roleid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "path: roleid; body: description, color, permissions[names] (permissions immutable on the admin role)" - } - } - } - } - }, - "delete": { - "tags": [ - "core-identity" - ], - "summary": "Delete a role; refuses for the admin role or any role still assigned to users", - "description": "Delete a role; refuses for the admin role or any role still assigned to users\n\n**Auth:** jwt + require_role admin\n\n**Params:** path: roleid\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/users/roles/3 -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "roleid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, "/api/apitokens": { "get": { "tags": [ - "core-identity" + "core-apitokens" ], - "summary": "List the caller's own API tokens (never returns hashes or secrets); admins may list all", - "description": "List the caller's own API tokens (never returns hashes or secrets); admins may list all\n\n**Auth:** jwt\n\n**Params:** query: all=true (admin only, lists everyone's tokens with owner info)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/apitokens?all=true' -H \"Authorization: Bearer $TOK\"\n```", + "summary": "List the caller's own tokens. Admins may pass ?all=true for everyone's.", + "description": "List the caller's own tokens. Admins may pass ?all=true for everyone's.\n\n**Auth:** jwt\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/apitokens -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -950,10 +302,10 @@ }, "post": { "tags": [ - "core-identity" + "core-apitokens" ], - "summary": "Create a personal API token for the caller; the full secret is returned once in this response and never again", - "description": "Create a personal API token for the caller; the full secret is returned once in this response and never again\n\n**Auth:** jwt + require_permission apitokens.create\n\n**Params:** body: name (required), expiresat (date/datetime), scopes[permission names, ceiling = owner's permissions], resourcescopes[resource names, plugin-defined]\n\n**Example:**\n```\ncurl -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\"}'\n```", + "summary": "Create a token for the caller. Returns the full secret ONCE.", + "description": "Create a token for the caller. Returns the full secret ONCE.\n\n**Auth:** permission:apitokens.create\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/apitokens -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -990,7 +342,7 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "body: name (required), expiresat (date/datetime), scopes[permission names, ceiling = owner's permissions], resourcescopes[resource names, plugin-defined]" + "description": "body: JSON (fields not yet documented)" } } } @@ -998,12 +350,12 @@ } }, "/api/apitokens/{tokenid}": { - "put": { + "delete": { "tags": [ - "core-identity" + "core-apitokens" ], - "summary": "Rename, rescope, or (de)activate a token; scope ceiling is always the owner, even when an admin edits", - "description": "Rename, rescope, or (de)activate a token; scope ceiling is always the owner, even when an admin edits\n\n**Auth:** jwt + require_permission apitokens.create (own token, or any if admin)\n\n**Params:** path: tokenid; body: name, isactive (bool), scopes[names, validated against the token OWNER's permissions], resourcescopes[names]\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/apitokens/4 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"isactive\":false}'\n```", + "summary": "Revoke (deactivate) a token. Own token, or any if admin.", + "description": "Revoke (deactivate) a token. Own token, or any if admin.\n\n**Auth:** permission:apitokens.create\n\n**Params:** path: tokenid (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/apitokens/1 -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -1042,29 +394,17 @@ "in": "path", "required": true, "schema": { - "type": "string" + "type": "integer" } } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "path: tokenid; body: name, isactive (bool), scopes[names, validated against the token OWNER's permissions], resourcescopes[names]" - } - } - } - } + ] }, - "delete": { + "put": { "tags": [ - "core-identity" + "core-apitokens" ], - "summary": "Revoke (deactivate, not delete) a token; audit-logged", - "description": "Revoke (deactivate, not delete) a token; audit-logged\n\n**Auth:** jwt + require_permission apitokens.create (own token, or any if admin)\n\n**Params:** path: tokenid\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/apitokens/4 -H \"Authorization: Bearer $TOK\"\n```", + "summary": "Rename, rescope, or deactivate a token. Own token, or any if admin.", + "description": "Rename, rescope, or deactivate a token. Own token, or any if admin.\n\n**Auth:** permission:apitokens.create\n\n**Params:** path: tokenid (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/apitokens/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -1102,1812 +442,6 @@ "name": "tokenid", "in": "path", "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/setup/needs-admin": { - "get": { - "tags": [ - "core-identity" - ], - "summary": "Return {needsadmin: true} when zero users exist, so the login screen can offer first-run setup", - "description": "Return {needsadmin: true} when zero users exist, so the login screen can offer first-run setup\n\n**Auth:** none\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/setup/needs-admin\n```", - "security": [], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/setup/create-admin": { - "post": { - "tags": [ - "core-identity" - ], - "summary": "Bootstrap the very first admin account (creates the admin role if missing)", - "description": "Bootstrap the very first admin account (creates the admin role if missing)\n\n**Auth:** none (only functions while zero users exist; 403 afterwards)\n\n**Params:** body: username, email, password (all required)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/setup/create-admin -H 'Content-Type: application/json' -d '{\"username\":\"admin\",\"email\":\"admin@example.com\",\"password\":\"ChangeMe123\"}'\n```", - "security": [], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: username, email, password (all required)" - } - } - } - } - } - }, - "/api/setup/seed-reference": { - "post": { - "tags": [ - "core-identity" - ], - "summary": "Idempotently seed core reference data, permissions, and default settings (runs the flask seed CLI routines)", - "description": "Idempotently seed core reference data, permissions, and default settings (runs the flask seed CLI routines)\n\n**Auth:** jwt + require_role admin\n\n**Params:** none\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/setup/seed-reference -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "none" - } - } - } - } - } - }, - "/api/setup/seed-starter": { - "post": { - "tags": [ - "core-identity" - ], - "summary": "Idempotently add a starter list of common hardware vendors (Dell, HP, Lenovo, ...)", - "description": "Idempotently add a starter list of common hardware vendors (Dell, HP, Lenovo, ...)\n\n**Auth:** jwt + require_role admin\n\n**Params:** none\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/setup/seed-starter -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "none" - } - } - } - } - } - }, - "/api/setup/complete": { - "post": { - "tags": [ - "core-identity" - ], - "summary": "Set the setup_complete setting to true, marking the first-run wizard finished", - "description": "Set the setup_complete setting to true, marking the first-run wizard finished\n\n**Auth:** jwt + require_role admin\n\n**Params:** none\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/setup/complete -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "none" - } - } - } - } - } - }, - "/api/settings": { - "get": { - "tags": [ - "core-identity" - ], - "summary": "List settings; unauthenticated callers see only the public allowlist (branding+map categories plus named bootstrap...", - "description": "List settings; unauthenticated callers see only the public allowlist (branding+map categories plus named bootstrap keys); secrets always masked as ********\n\n**Auth:** jwt-optional\n\n**Params:** query: category (filter)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/settings?category=branding'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - }, - "post": { - "tags": [ - "core-identity" - ], - "summary": "Create a new setting; 409 if the key exists", - "description": "Create a new setting; 409 if the key exists\n\n**Auth:** jwt + require_permission settings.edit\n\n**Params:** body: key (required), value, valuetype (default string), category (default general), description\n\n**Example:**\n```\ncurl -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\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: key (required), value, valuetype (default string), category (default general), description" - } - } - } - } - } - }, - "/api/settings/{key}": { - "get": { - "tags": [ - "core-identity" - ], - "summary": "Get one setting; non-public keys return 404 (not 403) to unauthenticated callers; secret values masked", - "description": "Get one setting; non-public keys return 404 (not 403) to unauthenticated callers; secret values masked\n\n**Auth:** jwt-optional\n\n**Params:** path: key\n\n**Example:**\n```\ncurl http://localhost:5001/api/settings/facility_name\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "key", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - }, - "put": { - "tags": [ - "core-identity" - ], - "summary": "Update a setting (upserts a plugin-scoped string row if the key is new); audit-logged with secrets masked; invalidates...", - "description": "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\n\n**Auth:** jwt + require_permission settings.edit\n\n**Params:** path: key; body: value (required; bool coerced to 'true'/'false'; secret mask ******** means leave unchanged)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/settings/facility_name -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"value\":\"West Jefferson\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "key", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "path: key; body: value (required; bool coerced to 'true'/'false'; secret mask ******** means leave unchanged)" - } - } - } - } - } - }, - "/api/settings/seed": { - "post": { - "tags": [ - "core-identity" - ], - "summary": "Idempotently create any missing default settings (identifier toggles, search toggles, map, SMTP, SAML, etc.)", - "description": "Idempotently create any missing default settings (identifier toggles, search toggles, map, SMTP, SAML, etc.)\n\n**Auth:** jwt + require_permission settings.edit\n\n**Params:** none\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/settings/seed -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "none" - } - } - } - } - } - }, - "/api/settings/test-email": { - "post": { - "tags": [ - "core-identity" - ], - "summary": "Send a test email to verify SMTP config; always 200 with a sent flag, SMTP errors returned with credentials scrubbed", - "description": "Send a test email to verify SMTP config; always 200 with a sent flag, SMTP errors returned with credentials scrubbed\n\n**Auth:** jwt + require_permission settings.edit\n\n**Params:** body: to (optional; falls back to alert_recipients setting)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/settings/test-email -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"to\":\"me@example.com\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: to (optional; falls back to alert_recipients setting)" - } - } - } - } - } - }, - "/api/settings/map-blueprint": { - "post": { - "tags": [ - "core-identity" - ], - "summary": "Upload a floor-map blueprint image to the instance maps dir and point map_blueprint_ at its served URL", - "description": "Upload a floor-map blueprint image to the instance maps dir and point map_blueprint_ at its served URL\n\n**Auth:** jwt + require_role admin\n\n**Params:** multipart/form-data: file (png/jpg/jpeg/gif/webp/svg), theme=light|dark\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/settings/map-blueprint -H \"Authorization: Bearer $TOK\" -F 'file=@floor.png' -F 'theme=light'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "multipart/form-data: file (png/jpg/jpeg/gif/webp/svg), theme=light|dark" - } - } - } - } - } - }, - "/api/settings/map-blueprint/{filename}": { - "get": { - "tags": [ - "core-identity" - ], - "summary": "Serve an uploaded floor-map blueprint image (public so kiosk dashboards can load it)", - "description": "Serve an uploaded floor-map blueprint image (public so kiosk dashboards can load it)\n\n**Auth:** none\n\n**Params:** path: filename\n\n**Example:**\n```\ncurl -O http://localhost:5001/api/settings/map-blueprint/blueprint-light.png\n```", - "security": [], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "filename", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "may contain slashes" - } - } - ] - } - }, - "/api/settings/branding-logo": { - "post": { - "tags": [ - "core-identity" - ], - "summary": "Upload a branding logo/favicon to the instance branding dir and set the matching branding setting...", - "description": "Upload a branding logo/favicon to the instance branding dir and set the matching branding setting (site_logo/qr_logo/badge_logo/site_favicon)\n\n**Auth:** jwt + require_role admin\n\n**Params:** multipart/form-data: file (map image types plus .ico), kind=site|qr|badge|favicon\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/settings/branding-logo -H \"Authorization: Bearer $TOK\" -F 'file=@logo.svg' -F 'kind=site'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "multipart/form-data: file (map image types plus .ico), kind=site|qr|badge|favicon" - } - } - } - } - } - }, - "/api/settings/branding/{filename}": { - "get": { - "tags": [ - "core-identity" - ], - "summary": "Serve an uploaded branding logo (public - kiosks and print pages read it)", - "description": "Serve an uploaded branding logo (public - kiosks and print pages read it)\n\n**Auth:** none\n\n**Params:** path: filename\n\n**Example:**\n```\ncurl -O http://localhost:5001/api/settings/branding/logo-site.svg\n```", - "security": [], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "filename", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "may contain slashes" - } - } - ] - } - }, - "/api/settings/asset-import/script": { - "get": { - "tags": [ - "core-identity" - ], - "summary": "Download Collect-ShopDB.ps1 with this site CSV path already embedded, so the collector script needs no hand editing...", - "description": "Download Collect-ShopDB.ps1 with this site CSV path already embedded, so the collector script needs no hand editing before it runs\n\n**Auth:** admin role\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/settings/asset-import/script -o Collect-ShopDB.ps1\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - } - }, - "/api/settings/asset-import/templates": { - "get": { - "tags": [ - "core-identity" - ], - "summary": "List the importable CSV tables in the order they must be uploaded - foreign keys mean the order is not optional", - "description": "List the importable CSV tables in the order they must be uploaded - foreign keys mean the order is not optional\n\n**Auth:** admin role\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/settings/asset-import/templates\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - } - }, - "/api/settings/asset-import/templates/{tablename}": { - "get": { - "tags": [ - "core-identity" - ], - "summary": "Download a generated CSV template for one importable table, with its real column names as the header row", - "description": "Download a generated CSV template for one importable table, with its real column names as the header row\n\n**Auth:** admin role\n\n**Params:** path: tablename\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/settings/asset-import/templates/assets -o assets.csv\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "tablename", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/settings/asset-import/import": { - "post": { - "tags": [ - "core-identity" - ], - "summary": "Validate uploaded CSVs and, only when commit=true, load them. The default is a dry run that reports what WOULD change...", - "description": "Validate uploaded CSVs and, only when commit=true, load them. The default is a dry run that reports what WOULD change; a committing run writes an AuditLog row with the reconciled count\n\n**Auth:** admin role\n\n**Params:** multipart: one file per table; form: commit (false = validate only)\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" -F \"assets=@assets.csv\" -F \"commit=true\" http://localhost:5001/api/settings/asset-import/import\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "multipart: one file per table; form: commit (false = validate only)" - } - } - } - } - } - }, - "/api/settings/asset-import/winc/preview": { - "post": { - "tags": [ - "core-identity" - ], - "summary": "Dry run a WINC machine-adoption import. Writes nothing; returns the plan, a summary, the needs-create groups an admin...", - "description": "Dry run a WINC machine-adoption import. Writes nothing; returns the plan, a summary, the needs-create groups an admin must acknowledge, and the planhash that authorises the matching commit\n\n**Auth:** jwt + require_role admin\n\n**Params:** multipart/form-data: files (one WINC/Maximo CSV export)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/settings/asset-import/winc/preview -H \"Authorization: Bearer $TOK\" -F files=@winc.csv\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "multipart/form-data: files (one WINC/Maximo CSV export)" - } - } - } - } - } - }, - "/api/settings/asset-import/winc/commit": { - "post": { - "tags": [ - "core-identity" - ], - "summary": "Apply a previewed WINC plan in one transaction, journalled so it can be undone. planhash is required and must match a...", - "description": "Apply a previewed WINC plan in one transaction, journalled so it can be undone. planhash is required and must match a freshly rebuilt plan: without it the drift check compared a packet against the plan it was built from and could never fail, so an admin could approve one plan and apply another\n\n**Auth:** jwt + require_role admin\n\n**Params:** multipart/form-data: files (the same export); form: planhash (REQUIRED, from the preview), acknowledge (comma-separated needs-create group names)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/settings/asset-import/winc/commit -H \"Authorization: Bearer $TOK\" -F files=@winc.csv -F planhash=$HASH -F acknowledge=businessunit\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "multipart/form-data: files (the same export); form: planhash (REQUIRED, from the preview), acknowledge (comma-separated needs-create group names)" - } - } - } - } - } - }, - "/api/settings/asset-import/winc/batches": { - "get": { - "tags": [ - "core-identity" - ], - "summary": "List applied and undone import batches, newest first, with kind, row count, who applied it and when", - "description": "List applied and undone import batches, newest first, with kind, row count, who applied it and when\n\n**Auth:** jwt + require_role admin\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/settings/asset-import/winc/batches -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - } - }, - "/api/settings/asset-import/winc/undo": { - "post": { - "tags": [ - "core-identity" - ], - "summary": "Reverse an applied batch by replaying its journal backwards. Conservative: a cell is put back only if it still holds...", - "description": "Reverse an applied batch by replaying its journal backwards. Conservative: a cell is put back only if it still holds what the import wrote, and a created row is deleted only when nothing outside the batch references it\n\n**Auth:** jwt + require_role admin\n\n**Params:** body: batchid (required)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/settings/asset-import/winc/undo -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"batchid\":\"...\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: batchid (required)" - } - } - } - } - } - }, - "/api/reports": { - "get": { - "tags": [ - "core-platform" - ], - "summary": "List all available reports (6 core cards plus cards contributed by enabled plugins via the get_reports hook)", - "description": "List all available reports (6 core cards plus cards contributed by enabled plugins via the get_reports hook)\n\n**Auth:** jwt-optional\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/reports\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/reports/machines-by-type": { - "get": { - "tags": [ - "core-platform" - ], - "summary": "Machine count grouped by machine type (dualpath secondary bays collapsed when the site setting is on); 404 if machines...", - "description": "Machine count grouped by machine type (dualpath secondary bays collapsed when the site setting is on); 404 if machines plugin absent\n\n**Auth:** jwt-optional\n\n**Params:** businessunitid (int filter), format=json|csv\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/reports/machines-by-type?businessunitid=2&format=csv'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/reports/assets-by-status": { - "get": { - "tags": [ - "core-platform" - ], - "summary": "Asset count grouped by status with status colors", - "description": "Asset count grouped by status with status colors\n\n**Auth:** jwt-optional\n\n**Params:** assettypeid, businessunitid, format=json|csv\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/reports/assets-by-status?assettypeid=1'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/reports/kb-popularity": { - "get": { - "tags": [ - "core-platform" - ], - "summary": "Most-clicked knowledge base articles; 503 if knowledgebase plugin absent", - "description": "Most-clicked knowledge base articles; 503 if knowledgebase plugin absent\n\n**Auth:** jwt-optional\n\n**Params:** limit (default 20, max 100), format=json|csv\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/reports/kb-popularity?limit=10'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/reports/software-compliance": { - "get": { - "tags": [ - "core-platform" - ], - "summary": "Required applications vs installed per PC with compliance rate and up to 100 non-compliant PCs per app; 503 if...", - "description": "Required applications vs installed per PC with compliance rate and up to 100 non-compliant PCs per app; 503 if computers plugin absent\n\n**Auth:** jwt-optional\n\n**Params:** appid (filter to one app), format=json|csv\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/reports/software-compliance?appid=5'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/reports/asset-inventory": { - "get": { - "tags": [ - "core-platform" - ], - "summary": "Complete asset inventory summary broken down by type, status, and location", - "description": "Complete asset inventory summary broken down by type, status, and location\n\n**Auth:** jwt-optional\n\n**Params:** businessunitid, locationid, format=json|csv\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/reports/asset-inventory?locationid=3&format=csv'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/reports/pc-relationships": { - "get": { - "tags": [ - "core-platform" - ], - "summary": "PC-to-shop-floor-machine relationships (machine number, vendor, model, hostname, IP) matched in both edge directions...", - "description": "PC-to-shop-floor-machine relationships (machine number, vendor, model, hostname, IP) matched in both edge directions via raw SQL UNION\n\n**Auth:** jwt-optional\n\n**Params:** format=json|csv\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/reports/pc-relationships?format=csv'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/reports/email": { - "post": { - "tags": [ - "core-platform" - ], - "summary": "Email a report's rows as an HTML table on demand; recipient defaults to the site Alert Recipients setting; intended...", - "description": "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\n\n**Auth:** jwt + permission:reports.export\n\n**Params:** body: subject, columns [{key,label}], rows [{..}], intro (optional), to (optional email)\n\n**Example:**\n```\ncurl -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\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: subject, columns [{key,label}], rows [{..}], intro (optional), to (optional email)" - } - } - } - } - } - }, - "/api/search": { - "get": { - "tags": [ - "core-platform" - ], - "summary": "Global search across assets (including the gaugelabreference and maintenancereference identifiers), applications, KB...", - "description": "Global search across assets (including the gaugelabreference and maintenancereference identifiers), applications, KB, employees, USB devices, printed items, notifications, custom fields, hostnames, IPs/subnets, vendor/model/type; ServiceNOW ticket prefixes and smart redirects; results capped at 50, types filterable via search__enabled settings. Retired (isactive=0) rows are excluded everywhere, including KB articles whose topic application is retired\n\n**Auth:** jwt-optional\n\n**Params:** q (required, 2-200 chars)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/search?q=WKSTN0042'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/search/suggest": { - "get": { - "tags": [ - "core-platform" - ], - "summary": "A better spelling for a query that found nothing, or null. Its own endpoint so any list page can ask after rendering no...", - "description": "A better spelling for a query that found nothing, or null. Its own endpoint so any list page can ask after rendering no rows, rather than a suggestion key added to every search route. The vocabulary is harvested from searched columns (asset numbers and names, vendors, models, applications, asset types) and cached, so site-specific words are correctable; digits must match exactly, which keeps CSF16 from being 'corrected' to CSF17. Suggests, never rewrites\n\n**Auth:** jwt-optional\n\n**Params:** q (required, 2-200 chars)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/search/suggest?q=keyecne'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/dashboard": { - "get": { - "tags": [ - "core-platform" - ], - "summary": "Dashboard summary: asset counts by type (machines/PCs/network/printers/measuring tools, dualpath-collapsed), counts by...", - "description": "Dashboard summary: asset counts by type (machines/PCs/network/printers/measuring tools, dualpath-collapsed), counts by status, 10 most recent assets\n\n**Auth:** jwt-optional\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/dashboard\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/dashboard/summary": { - "get": { - "tags": [ - "core-platform" - ], - "summary": "Alias route for the same dashboard summary handler as GET /api/dashboard", - "description": "Alias route for the same dashboard summary handler as GET /api/dashboard\n\n**Auth:** jwt-optional\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/dashboard/summary\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/dashboard/stats": { - "get": { - "tags": [ - "core-platform" - ], - "summary": "Asset counts grouped by every asset type with display category labels", - "description": "Asset counts grouped by every asset type with display category labels\n\n**Auth:** jwt-optional\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/dashboard/stats\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/dashboard/navigation": { - "get": { - "tags": [ - "core-platform" - ], - "summary": "Sidebar navigation items: core entries merged with get_navigation_items from every enabled plugin, sorted by position", - "description": "Sidebar navigation items: core entries merged with get_navigation_items from every enabled plugin, sorted by position\n\n**Auth:** none\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/dashboard/navigation\n```", - "security": [], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/dashboard/capabilities": { - "get": { - "tags": [ - "core-platform" - ], - "summary": "Booleans describing what THIS caller may reach, so the app chrome can avoid drawing controls that would only answer...", - "description": "Booleans describing what THIS caller may reach, so the app chrome can avoid drawing controls that would only answer 401; cansearch reports whether the caller holds assets.view, which a signed-out visitor does when the guest role has been granted it\n\n**Auth:** none\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/dashboard/capabilities\n```", - "security": [], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/dashboard/widgets": { - "get": { - "tags": [ - "core-platform" - ], - "summary": "Dashboard widget definitions aggregated from enabled plugins (get_dashboard_widgets hook), sorted by position", - "description": "Dashboard widget definitions aggregated from enabled plugins (get_dashboard_widgets hook), sorted by position\n\n**Auth:** jwt-optional\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/dashboard/widgets\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/dashboard/health": { - "get": { - "tags": [ - "core-platform" - ], - "summary": "Health check: runs SELECT 1 against the DB, returns ok/degraded plus app version", - "description": "Health check: runs SELECT 1 against the DB, returns ok/degraded plus app version\n\n**Auth:** none\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/dashboard/health\n```", - "security": [], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/dashboarddefaults/visitor-location": { - "get": { - "tags": [ - "core-platform" - ], - "summary": "Resolve the business unit for a kiosk/lobby display by FQDN first then IP (caller IP used when ipaddress omitted); null...", - "description": "Resolve the business unit for a kiosk/lobby display by FQDN first then IP (caller IP used when ipaddress omitted); null businessunitid when unmapped\n\n**Auth:** none\n\n**Params:** fqdn (optional), ipaddress (optional, defaults to client IP)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/dashboarddefaults/visitor-location?fqdn=display01.example.net'\n```", - "security": [], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/dashboarddefaults/display-role": { - "get": { - "tags": [ - "core-platform" - ], - "summary": "Resolve what a display PC should show at boot: role (dashboard/lobby/partskiosk), frontend path, and business unit...", - "description": "Resolve what a display PC should show at boot: role (dashboard/lobby/partskiosk), frontend path, and business unit; null role when unmapped\n\n**Auth:** none\n\n**Params:** fqdn (optional), ipaddress (optional, defaults to client IP)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/dashboarddefaults/display-role?ipaddress=10.1.2.3'\n```", - "security": [], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/dashboarddefaults": { - "get": { - "tags": [ - "core-platform" - ], - "summary": "List all active display-to-business-unit mappings", - "description": "List all active display-to-business-unit mappings\n\n**Auth:** jwt-optional\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/dashboarddefaults\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - }, - "post": { - "tags": [ - "core-platform" - ], - "summary": "Create a display mapping; requires fqdn or ipaddress, dashboard role requires businessunitid, 409 on duplicate fqdn/IP...", - "description": "Create a display mapping; requires fqdn or ipaddress, dashboard role requires businessunitid, 409 on duplicate fqdn/IP; audit-logged\n\n**Auth:** jwt + role:admin\n\n**Params:** body: fqdn, ipaddress, displayrole (dashboard|lobby|partskiosk, default dashboard), businessunitid, description\n\n**Example:**\n```\ncurl -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}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: fqdn, ipaddress, displayrole (dashboard|lobby|partskiosk, default dashboard), businessunitid, description" - } - } - } - } - } - }, - "/api/dashboarddefaults/{default_id}": { - "put": { - "tags": [ - "core-platform" - ], - "summary": "Update a display mapping; non-dashboard roles get businessunitid nulled, dashboard role must keep one", - "description": "Update a display mapping; non-dashboard roles get businessunitid nulled, dashboard role must keep one\n\n**Auth:** jwt + role:admin\n\n**Params:** body: any of fqdn, ipaddress, displayrole, businessunitid, description\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/dashboarddefaults/7 -H \"Authorization: Bearer $JWT\" -H 'Content-Type: application/json' -d '{\"displayrole\":\"lobby\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "default_id", - "in": "path", - "required": true, "schema": { "type": "integer" } @@ -2920,18 +454,106 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "body: any of fqdn, ipaddress, displayrole, businessunitid, description" + "description": "path: tokenid (int); body: JSON (fields not yet documented)" } } } } + } + }, + "/api/applications": { + "get": { + "tags": [ + "core-applications" + ], + "summary": "List all applications.", + "description": "List all applications.\n\n**Auth:** permission:applications.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/applications -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } }, + "post": { + "tags": [ + "core-applications" + ], + "summary": "Create a new application.", + "description": "Create a new application.\n\n**Auth:** permission:applications.create\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/applications -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/applications/{app_id}": { "delete": { "tags": [ - "core-platform" + "core-applications" ], - "summary": "Soft-delete (deactivate) a display mapping", - "description": "Soft-delete (deactivate) a display mapping\n\n**Auth:** jwt + role:admin\n\n**Params:** none\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/dashboarddefaults/7 -H \"Authorization: Bearer $JWT\"\n```", + "summary": "Permanently delete an application, its versions, and its uploads.", + "description": "Permanently delete an application, its versions, and its uploads.\n\n**Auth:** permission:applications.delete\n\n**Params:** path: app_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/applications/1 -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -2966,7 +588,280 @@ }, "parameters": [ { - "name": "default_id", + "name": "app_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "get": { + "tags": [ + "core-applications" + ], + "summary": "Get a single application with details.", + "description": "Get a single application with details.\n\n**Auth:** permission:applications.view\n\n**Params:** path: app_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/applications/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "app_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "core-applications" + ], + "summary": "Update an application.", + "description": "Update an application.\n\n**Auth:** permission:applications.edit\n\n**Params:** path: app_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/applications/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "app_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: app_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/applications/{app_id}/image": { + "delete": { + "tags": [ + "core-applications" + ], + "summary": "Remove an application's image and clear the field.", + "description": "Remove an application's image and clear the field.\n\n**Auth:** permission:applications.edit\n\n**Params:** path: app_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/applications/1/image -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "app_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "post": { + "tags": [ + "core-applications" + ], + "summary": "Upload (or replace) an application's image.", + "description": "Upload (or replace) an application's image.\n\n**Auth:** permission:applications.edit\n\n**Params:** path: app_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/applications/1/image -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "app_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: app_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/applications/{app_id}/installed": { + "get": { + "tags": [ + "core-applications" + ], + "summary": "List all computers that have this application installed.", + "description": "List all computers that have this application installed.\n\n**Auth:** permission:applications.view\n\n**Params:** path: app_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/applications/1/installed -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "app_id", "in": "path", "required": true, "schema": { @@ -2976,16 +871,16 @@ ] } }, - "/api/collector/{pluginname}": { - "post": { + "/api/applications/{app_id}/package": { + "delete": { "tags": [ - "core-platform" + "core-applications" ], - "summary": "Generic collector ingest (ADR-006): schema-validated identity field, idempotent upsert via the plugin's...", - "description": "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\n\n**Auth:** api-key (X-API-Key: per-plugin COLLECTOR_API_KEY_ or shared COLLECTOR_API_KEY, or a collector.ingest-scoped managed PAT via Bearer/X-API-Key)\n\n**Params:** body: JSON payload whose schema identityfield (e.g. hostname) is required; rest is plugin-defined\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/collector/computers -H \"X-API-Key: $KEY\" -H 'Content-Type: application/json' -d '{\"hostname\":\"WKSTN0042\",\"serialnumber\":\"ABC123\"}'\n```", + "summary": "Remove an uploaded installer and clear the path it set.", + "description": "Remove an uploaded installer and clear the path it set.\n\n**Auth:** permission:applications.edit\n\n**Params:** path: app_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/applications/1/package -H \"Authorization: Bearer $TOK\"\n```", "security": [ { - "apiKeyAuth": [] + "bearerAuth": [] } ], "responses": { @@ -3017,11 +912,60 @@ }, "parameters": [ { - "name": "pluginname", + "name": "app_id", "in": "path", "required": true, "schema": { - "type": "string" + "type": "integer" + } + } + ] + }, + "post": { + "tags": [ + "core-applications" + ], + "summary": "Upload (or replace) the installer for an application.", + "description": "Upload (or replace) the installer for an application.\n\n**Auth:** permission:applications.edit\n\n**Params:** path: app_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/applications/1/package -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "app_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" } } ], @@ -3032,20 +976,929 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "body: JSON payload whose schema identityfield (e.g. hostname) is required; rest is plugin-defined" + "description": "path: app_id (int); body: JSON (fields not yet documented)" } } } } } }, - "/api/collector/_schemas": { + "/api/applications/{app_id}/revert": { + "post": { + "tags": [ + "core-applications" + ], + "summary": "Strip an application back to a catalogue record.", + "description": "Strip an application back to a catalogue record.\n\n**Auth:** permission:applications.edit\n\n**Params:** path: app_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/applications/1/revert -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "app_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: app_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/applications/{app_id}/versions": { "get": { "tags": [ - "core-platform" + "core-applications" ], - "summary": "List collector schemas for all enabled plugins that accept collector input", - "description": "List collector schemas for all enabled plugins that accept collector input\n\n**Auth:** jwt\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/collector/_schemas -H \"Authorization: Bearer $JWT\"\n```", + "summary": "List all versions for an application.", + "description": "List all versions for an application.\n\n**Auth:** permission:applications.view\n\n**Params:** path: app_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/applications/1/versions -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "app_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "post": { + "tags": [ + "core-applications" + ], + "summary": "Create a new version for an application.", + "description": "Create a new version for an application.\n\n**Auth:** permission:applications.create\n\n**Params:** path: app_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/applications/1/versions -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "app_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: app_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/applications/image/{filename}": { + "get": { + "tags": [ + "core-applications" + ], + "summary": "Serve an uploaded application image.", + "description": "Serve an uploaded application image.\n\n**Auth:** none (reviewed: named in the public-read or mutation sweep)\n\n**Params:** path: filename (path)\n\n**Example:**\n```\ncurl http://localhost:5001/api/applications/image/example.png\n```", + "security": [], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "filename", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "may contain slashes" + } + } + ] + } + }, + "/api/applications/machines/{machine_id}": { + "get": { + "tags": [ + "core-applications" + ], + "summary": "List all applications installed on a computer.", + "description": "List all applications installed on a computer.\n\n**Auth:** permission:applications.view\n\n**Params:** path: machine_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/applications/machines/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "machine_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "post": { + "tags": [ + "core-applications" + ], + "summary": "Install an application on a computer.", + "description": "Install an application on a computer.\n\n**Auth:** permission:applications.create\n\n**Params:** path: machine_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/applications/machines/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "machine_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: machine_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/applications/machines/{machine_id}/{app_id}": { + "delete": { + "tags": [ + "core-applications" + ], + "summary": "Uninstall an application from a computer.", + "description": "Uninstall an application from a computer.\n\n**Auth:** permission:applications.delete\n\n**Params:** path: machine_id (int), app_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/applications/machines/1/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "machine_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "app_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "core-applications" + ], + "summary": "Update installed application (e.g., change version).", + "description": "Update installed application (e.g., change version).\n\n**Auth:** permission:applications.edit\n\n**Params:** path: machine_id (int), app_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/applications/machines/1/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "machine_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "app_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: machine_id (int), app_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/applications/package/{filename}": { + "get": { + "tags": [ + "core-applications" + ], + "summary": "Download an uploaded installer.", + "description": "Download an uploaded installer.\n\n**Auth:** permission:applications.download\n\n**Params:** path: filename (path)\n\n**Example:**\n```\ncurl http://localhost:5001/api/applications/package/example.png -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "filename", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "may contain slashes" + } + } + ] + } + }, + "/api/assets": { + "get": { + "tags": [ + "core-assets" + ], + "summary": "List all assets with filtering and pagination.", + "description": "List all assets with filtering and pagination.\n\n**Auth:** permission:assets.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/assets -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "post": { + "tags": [ + "core-assets" + ], + "summary": "Create a new asset.", + "description": "Create a new asset.\n\n**Auth:** permission:assets.create\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/assets -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/assets/{asset_id}": { + "delete": { + "tags": [ + "core-assets" + ], + "summary": "Delete (soft delete) an asset.", + "description": "Delete (soft delete) an asset.\n\n**Auth:** permission:assets.delete\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/assets/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "get": { + "tags": [ + "core-assets" + ], + "summary": "Get a single asset with full details.", + "description": "Get a single asset with full details.\n\n**Auth:** permission:assets.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/assets/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "core-assets" + ], + "summary": "Update an asset.", + "description": "Update an asset.\n\n**Auth:** permission:assets.edit\n\n**Params:** path: asset_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/assets/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: asset_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/assets/{asset_id}/communications": { + "get": { + "tags": [ + "core-assets" + ], + "summary": "Get all communications for an asset.", + "description": "Get all communications for an asset.\n\n**Auth:** permission:assets.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/assets/1/communications -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/assets/{asset_id}/relationships": { + "get": { + "tags": [ + "core-assets" + ], + "summary": "Get all relationships for an asset.", + "description": "Get all relationships for an asset.\n\n**Auth:** permission:assets.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/assets/1/relationships -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/assets/lookup/{assetnumber}": { + "get": { + "tags": [ + "core-assets" + ], + "summary": "Look up an asset by its asset number.", + "description": "Look up an asset by its asset number.\n\n**Auth:** permission:assets.view\n\n**Params:** path: assetnumber (string)\n\n**Example:**\n```\ncurl http://localhost:5001/api/assets/lookup/x -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "assetnumber", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/assets/map": { + "get": { + "tags": [ + "core-assets" + ], + "summary": "Get all assets with map positions for unified floor map display.", + "description": "Get all assets with map positions for unified floor map display.\n\n**Auth:** permission:assets.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/assets/map -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -3077,16 +1930,16 @@ } } }, - "/api/collector/pc": { + "/api/assets/relationships": { "post": { "tags": [ - "core-platform" + "core-assets" ], - "summary": "Legacy computers-specific ingest: update one PC matched by hostname (or asset number) - lastreporteddate, lastboottime...", - "description": "Legacy computers-specific ingest: update one PC matched by hostname (or asset number) - lastreporteddate, lastboottime, loggedinuser, serialnumber\n\n**Auth:** api-key\n\n**Params:** body: hostname (required), lastboottime (ISO), currentuser, serialnumber\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/collector/pc -H \"X-API-Key: $KEY\" -H 'Content-Type: application/json' -d '{\"hostname\":\"WKSTN0042\",\"currentuser\":\"212345678\"}'\n```", + "summary": "Create a relationship between two assets.", + "description": "Create a relationship between two assets.\n\n**Auth:** permission:assets.create\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/assets/relationships -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { - "apiKeyAuth": [] + "bearerAuth": [] } ], "responses": { @@ -3120,23 +1973,23 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "body: hostname (required), lastboottime (ISO), currentuser, serialnumber" + "description": "body: JSON (fields not yet documented)" } } } } } }, - "/api/collector/apps": { - "post": { + "/api/assets/relationships/{rel_id}": { + "delete": { "tags": [ - "core-platform" + "core-assets" ], - "summary": "Update installed applications for one PC; only apps already in the Application table are tracked, others skipped...", - "description": "Update installed applications for one PC; only apps already in the Application table are tracked, others skipped; returns created/updated/skipped counts\n\n**Auth:** api-key\n\n**Params:** body: hostname (required), apps [{appname, version}] (required)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/collector/apps -H \"X-API-Key: $KEY\" -H 'Content-Type: application/json' -d '{\"hostname\":\"WKSTN0042\",\"apps\":[{\"appname\":\"PC-DMIS\",\"version\":\"2023.2\"}]}'\n```", + "summary": "Delete an asset relationship.", + "description": "Delete an asset relationship.\n\n**Auth:** permission:assets.delete\n\n**Params:** path: rel_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/assets/relationships/1 -H \"Authorization: Bearer $TOK\"\n```", "security": [ { - "apiKeyAuth": [] + "bearerAuth": [] } ], "responses": { @@ -3161,132 +2014,33 @@ }, "403": { "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." } }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: hostname (required), apps [{appname, version}] (required)" - } - } - } - } - } - }, - "/api/collector/heartbeat": { - "post": { - "tags": [ - "core-platform" - ], - "summary": "Record PC online heartbeat (single hostname or batch); stamps lastreporteddate, returns updated count and notfound list", - "description": "Record PC online heartbeat (single hostname or batch); stamps lastreporteddate, returns updated count and notfound list\n\n**Auth:** api-key\n\n**Params:** body: hostname (string) or hostnames (array)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/collector/heartbeat -H \"X-API-Key: $KEY\" -H 'Content-Type: application/json' -d '{\"hostnames\":[\"pc1\",\"pc2\"]}'\n```", - "security": [ + "parameters": [ { - "apiKeyAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: hostname (string) or hostnames (array)" - } + "name": "rel_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" } } - } + ] } }, - "/api/collector/bulk": { - "post": { - "tags": [ - "core-platform" - ], - "summary": "Bulk update many PCs in one call (lastreporteddate, currentuser, lastboottime per entry); returns...", - "description": "Bulk update many PCs in one call (lastreporteddate, currentuser, lastboottime per entry); returns updated/notfound/errors\n\n**Auth:** api-key\n\n**Params:** body: pcs [{hostname (required), currentuser, lastboottime}]\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/collector/bulk -H \"X-API-Key: $KEY\" -H 'Content-Type: application/json' -d '{\"pcs\":[{\"hostname\":\"pc1\",\"currentuser\":\"212345678\"}]}'\n```", - "security": [ - { - "apiKeyAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: pcs [{hostname (required), currentuser, lastboottime}]" - } - } - } - } - } - }, - "/api/collector/status": { + "/api/assets/relationshiptypes": { "get": { "tags": [ - "core-platform" + "core-assets" ], - "summary": "Collector API liveness/credential check; returns timestamp and the collector endpoint list", - "description": "Collector API liveness/credential check; returns timestamp and the collector endpoint list\n\n**Auth:** api-key\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/collector/status -H \"X-API-Key: $KEY\"\n```", + "summary": "List all asset relationship types.", + "description": "List all asset relationship types.\n\n**Auth:** permission:assets.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/assets/relationshiptypes -H \"Authorization: Bearer $TOK\"\n```", "security": [ { - "apiKeyAuth": [] + "bearerAuth": [] } ], "responses": { @@ -3313,15 +2067,620 @@ "description": "Authenticated, but not permitted." } } + }, + "post": { + "tags": [ + "core-assets" + ], + "summary": "Create a new asset relationship type.", + "description": "Create a new asset relationship type.\n\n**Auth:** permission:assets.create\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/assets/relationshiptypes -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/assets/relationshiptypes/{type_id}": { + "delete": { + "tags": [ + "core-assets" + ], + "summary": "Delete a relationship type. Refused if any relationship still uses it.", + "description": "Delete a relationship type. Refused if any relationship still uses it.\n\n**Auth:** permission:assets.delete\n\n**Params:** path: type_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/assets/relationshiptypes/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "type_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "core-assets" + ], + "summary": "Update a relationship type.", + "description": "Update a relationship type.\n\n**Auth:** permission:assets.edit\n\n**Params:** path: type_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/assets/relationshiptypes/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "type_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: type_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/assets/statuses": { + "get": { + "tags": [ + "core-assets" + ], + "summary": "List all asset statuses.", + "description": "List all asset statuses.\n\n**Auth:** permission:assets.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/assets/statuses -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "post": { + "tags": [ + "core-assets" + ], + "summary": "Create a new asset status.", + "description": "Create a new asset status.\n\n**Auth:** permission:assets.create\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/assets/statuses -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/assets/statuses/{status_id}": { + "delete": { + "tags": [ + "core-assets" + ], + "summary": "Delete an asset status. Refused if any asset still uses it.", + "description": "Delete an asset status. Refused if any asset still uses it.\n\n**Auth:** permission:assets.delete\n\n**Params:** path: status_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/assets/statuses/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "status_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "get": { + "tags": [ + "core-assets" + ], + "summary": "Get a single asset status.", + "description": "Get a single asset status.\n\n**Auth:** permission:assets.view\n\n**Params:** path: status_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/assets/statuses/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "status_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "core-assets" + ], + "summary": "Update an asset status.", + "description": "Update an asset status.\n\n**Auth:** permission:assets.edit\n\n**Params:** path: status_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/assets/statuses/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "status_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: status_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/assets/types": { + "get": { + "tags": [ + "core-assets" + ], + "summary": "List all asset types.", + "description": "List all asset types.\n\n**Auth:** permission:assets.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/assets/types -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "post": { + "tags": [ + "core-assets" + ], + "summary": "Create a new asset type.", + "description": "Create a new asset type.\n\n**Auth:** permission:assets.create\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/assets/types -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/assets/types/{type_id}": { + "get": { + "tags": [ + "core-assets" + ], + "summary": "Get a single asset type.", + "description": "Get a single asset type.\n\n**Auth:** permission:assets.view\n\n**Params:** path: type_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/assets/types/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "type_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "core-assets" + ], + "summary": "Update an asset type's display fields (color/icon/description). The name, plugin, and table are structural and not...", + "description": "Update an asset type's display fields (color/icon/description). The name, plugin, and table are structural and not editable here.\n\n**Auth:** permission:assets.edit\n\n**Params:** path: type_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/assets/types/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "type_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: type_id (int); body: JSON (fields not yet documented)" + } + } + } + } } }, "/api/auditlogs": { "get": { "tags": [ - "core-platform" + "core-auditlogs" ], - "summary": "List audit logs with filtering and pagination, newest first; rows enriched with best-effort SSO-to-full-name resolution", - "description": "List audit logs with filtering and pagination, newest first; rows enriched with best-effort SSO-to-full-name resolution\n\n**Auth:** jwt + permission:audit.view\n\n**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)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/auditlogs?action=deleted&perpage=100' -H \"Authorization: Bearer $JWT\"\n```", + "summary": "List audit logs with filtering and pagination.", + "description": "List audit logs with filtering and pagination.\n\n**Auth:** permission:audit.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/auditlogs -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -3356,10 +2715,10 @@ "/api/auditlogs/entity/{entitytype}/{entityid}": { "get": { "tags": [ - "core-platform" + "core-auditlogs" ], - "summary": "Full audit history for one entity, newest first", - "description": "Full audit history for one entity, newest first\n\n**Auth:** jwt + permission:audit.view\n\n**Params:** path only\n\n**Example:**\n```\ncurl http://localhost:5001/api/auditlogs/entity/Asset/42 -H \"Authorization: Bearer $JWT\"\n```", + "summary": "Get audit history for a specific entity.", + "description": "Get audit history for a specific entity.\n\n**Auth:** permission:audit.view\n\n**Params:** path: entitytype (string), entityid (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/auditlogs/entity/x/1 -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -3412,89 +2771,13 @@ ] } }, - "/api/auditlogs/stats": { - "get": { - "tags": [ - "core-platform" - ], - "summary": "Audit statistics: counts by action and entity type, last-7-days activity count, top 5 most active users", - "description": "Audit statistics: counts by action and entity type, last-7-days activity count, top 5 most active users\n\n**Auth:** jwt + permission:audit.view\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/auditlogs/stats -H \"Authorization: Bearer $JWT\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - } - }, - "/api/auditlogs/retention": { - "get": { - "tags": [ - "core-platform" - ], - "summary": "Audit log size (total entries, oldest/newest timestamps) and, when a retention period is configured, how many entries a...", - "description": "Audit log size (total entries, oldest/newest timestamps) and, when a retention period is configured, how many entries a purge would delete. Drives the Site & Facility retention card so the count is seen before anything is deleted\n\n**Auth:** jwt + audit.view\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/auditlogs/retention -H \"Authorization: Bearer $TOKEN\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - } - }, "/api/auditlogs/purge": { "post": { "tags": [ - "core-platform" + "core-auditlogs" ], - "summary": "Delete audit entries older than the retention window, in committed batches. Requires audit.purge, not merely...", - "description": "Delete audit entries older than the retention window, in committed batches. Requires audit.purge, not merely audit.view. Batch-capped so the request cannot outlive a proxy timeout; an incomplete run returns complete=false with the number still remaining rather than reporting a finished job. Refuses with 400 when no retention period is configured\n\n**Auth:** jwt + audit.purge\n\n**Params:** body: days (optional, defaults to the audit_retentiondays setting), dryrun (optional bool)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/auditlogs/purge -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"days\":365,\"dryrun\":true}'\n```", + "summary": "Delete entries older than the retention window.", + "description": "Delete entries older than the retention window.\n\n**Auth:** permission:audit.purge\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/auditlogs/purge -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -3531,7 +2814,3995 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "body: days (optional, defaults to the audit_retentiondays setting), dryrun (optional bool)" + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/auditlogs/retention": { + "get": { + "tags": [ + "core-auditlogs" + ], + "summary": "Size of the log and what the configured retention would remove.", + "description": "Size of the log and what the configured retention would remove.\n\n**Auth:** permission:audit.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/auditlogs/retention -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/auditlogs/stats": { + "get": { + "tags": [ + "core-auditlogs" + ], + "summary": "Get audit log statistics.", + "description": "Get audit log statistics.\n\n**Auth:** permission:audit.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/auditlogs/stats -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/auth/change-password": { + "post": { + "tags": [ + "core-auth" + ], + "summary": "Change the authenticated user's own password.", + "description": "Change the authenticated user's own password.\n\n**Auth:** jwt\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/auth/change-password -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/auth/login": { + "post": { + "tags": [ + "core-auth" + ], + "summary": "Authenticate user and return JWT tokens.", + "description": "Authenticate user and return JWT tokens.\n\n**Auth:** none (the login surface itself; rate limited, 5-strike lockout)\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/auth/login -H 'Content-Type: application/json' -d '{}'\n```", + "security": [], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/auth/logout": { + "post": { + "tags": [ + "core-auth" + ], + "summary": "Logout user (for frontend token cleanup).", + "description": "Logout user (for frontend token cleanup).\n\n**Auth:** jwt\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/auth/logout -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/auth/me": { + "get": { + "tags": [ + "core-auth" + ], + "summary": "Get current authenticated user info.", + "description": "Get current authenticated user info.\n\n**Auth:** jwt\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/auth/me -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/auth/refresh": { + "post": { + "tags": [ + "core-auth" + ], + "summary": "Refresh access token using refresh token.", + "description": "Refresh access token using refresh token.\n\n**Auth:** jwt (refresh token)\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/auth/refresh -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/businessunits": { + "get": { + "tags": [ + "core-businessunits" + ], + "summary": "List all business units.", + "description": "List all business units.\n\n**Auth:** jwt-optional (works signed out; the handler decides what shows)\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/businessunits -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + }, + {} + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "post": { + "tags": [ + "core-businessunits" + ], + "summary": "Create a new business unit.", + "description": "Create a new business unit.\n\n**Auth:** jwt + role:admin\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/businessunits -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/businessunits/{bu_id}": { + "delete": { + "tags": [ + "core-businessunits" + ], + "summary": "Delete (deactivate) a business unit.", + "description": "Delete (deactivate) a business unit.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: bu_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/businessunits/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "bu_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "get": { + "tags": [ + "core-businessunits" + ], + "summary": "Get a single business unit.", + "description": "Get a single business unit.\n\n**Auth:** permission:assets.view\n\n**Params:** path: bu_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/businessunits/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "bu_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "core-businessunits" + ], + "summary": "Update a business unit.", + "description": "Update a business unit.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: bu_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/businessunits/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "bu_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: bu_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/collector/{pluginname}": { + "post": { + "tags": [ + "core-collector" + ], + "summary": "Generic collector ingest for any plugin (ADR-006).", + "description": "Generic collector ingest for any plugin (ADR-006).\n\n**Auth:** api-key (per-plugin collector token)\n\n**Params:** path: pluginname (string); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/collector/x -H \"X-API-Key: $KEY\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "apiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "pluginname", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: pluginname (string); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/collector/_schemas": { + "get": { + "tags": [ + "core-collector" + ], + "summary": "List collector schemas for all enabled plugins (JWT-protected).", + "description": "List collector schemas for all enabled plugins (JWT-protected).\n\n**Auth:** jwt\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/collector/_schemas -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/collector/apps": { + "post": { + "tags": [ + "core-collector" + ], + "summary": "Update installed applications for a PC (matched by hostname).", + "description": "Update installed applications for a PC (matched by hostname).\n\n**Auth:** api-key (collector token, legacy shared key)\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/collector/apps -H \"X-API-Key: $KEY\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "apiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/collector/bulk": { + "post": { + "tags": [ + "core-collector" + ], + "summary": "Bulk update multiple PCs at once.", + "description": "Bulk update multiple PCs at once.\n\n**Auth:** api-key (collector token, legacy shared key)\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/collector/bulk -H \"X-API-Key: $KEY\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "apiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/collector/heartbeat": { + "post": { + "tags": [ + "core-collector" + ], + "summary": "Record PC online status / heartbeat (single or batch).", + "description": "Record PC online status / heartbeat (single or batch).\n\n**Auth:** api-key (collector token, legacy shared key)\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/collector/heartbeat -H \"X-API-Key: $KEY\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "apiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/collector/pc": { + "post": { + "tags": [ + "core-collector" + ], + "summary": "Update one PC from the collection script (matched by hostname).", + "description": "Update one PC from the collection script (matched by hostname).\n\n**Auth:** api-key (collector token, legacy shared key)\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/collector/pc -H \"X-API-Key: $KEY\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "apiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/collector/status": { + "get": { + "tags": [ + "core-collector" + ], + "summary": "Check collector API status.", + "description": "Check collector API status.\n\n**Auth:** api-key (per-plugin collector token)\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/collector/status -H \"X-API-Key: $KEY\"\n```", + "security": [ + { + "apiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/customfields": { + "get": { + "tags": [ + "core-customfields" + ], + "summary": "List custom-field definitions. Filter by ?assettypeid=. ?active=false includes inactive ones.", + "description": "List custom-field definitions. Filter by ?assettypeid=. ?active=false includes inactive ones.\n\n**Auth:** permission:assets.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/customfields -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "post": { + "tags": [ + "core-customfields" + ], + "summary": "UNDOCUMENTED: customfields.create_field has no docstring.", + "description": "UNDOCUMENTED: customfields.create_field has no docstring.\n\n**Auth:** jwt + role:admin\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/customfields -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/customfields/{fieldid}": { + "delete": { + "tags": [ + "core-customfields" + ], + "summary": "UNDOCUMENTED: customfields.delete_field has no docstring.", + "description": "UNDOCUMENTED: customfields.delete_field has no docstring.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: fieldid (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/customfields/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "fieldid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "core-customfields" + ], + "summary": "UNDOCUMENTED: customfields.update_field has no docstring.", + "description": "UNDOCUMENTED: customfields.update_field has no docstring.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: fieldid (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/customfields/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "fieldid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: fieldid (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/customfields/asset/{assetid}": { + "get": { + "tags": [ + "core-customfields" + ], + "summary": "Active custom fields for an asset's type, merged with its values.", + "description": "Active custom fields for an asset's type, merged with its values.\n\n**Auth:** permission:assets.view\n\n**Params:** path: assetid (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/customfields/asset/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "assetid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "core-customfields" + ], + "summary": "Upsert values for an asset. Body: {values: {fieldid: value, ...}}.", + "description": "Upsert values for an asset. Body: {values: {fieldid: value, ...}}.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: assetid (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/customfields/asset/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "assetid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: assetid (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/dashboard": { + "get": { + "tags": [ + "core-dashboard" + ], + "summary": "Get dashboard summary data (asset-based).", + "description": "Get dashboard summary data (asset-based).\n\n**Auth:** permission:assets.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/dashboard -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/dashboard/capabilities": { + "get": { + "tags": [ + "core-dashboard" + ], + "summary": "What THIS caller can reach, for chrome that would otherwise only 401.", + "description": "What THIS caller can reach, for chrome that would otherwise only 401.\n\n**Auth:** jwt-optional (works signed out; the handler decides what shows)\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/dashboard/capabilities -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + }, + {} + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + }, + "/api/dashboard/health": { + "get": { + "tags": [ + "core-dashboard" + ], + "summary": "Health check endpoint (no auth required).", + "description": "Health check endpoint (no auth required).\n\n**Auth:** none (reviewed: named in the public-read or mutation sweep)\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/dashboard/health\n```", + "security": [], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + }, + "/api/dashboard/navigation": { + "get": { + "tags": [ + "core-dashboard" + ], + "summary": "Get navigation items from all loaded plugins.", + "description": "Get navigation items from all loaded plugins.\n\n**Auth:** jwt-optional (works signed out; the handler decides what shows)\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/dashboard/navigation -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + }, + {} + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + }, + "/api/dashboard/stats": { + "get": { + "tags": [ + "core-dashboard" + ], + "summary": "Get detailed statistics by asset type.", + "description": "Get detailed statistics by asset type.\n\n**Auth:** permission:assets.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/dashboard/stats -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/dashboard/summary": { + "get": { + "tags": [ + "core-dashboard" + ], + "summary": "Get dashboard summary data (asset-based).", + "description": "Get dashboard summary data (asset-based).\n\n**Auth:** permission:assets.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/dashboard/summary -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/dashboard/widgets": { + "get": { + "tags": [ + "core-dashboard" + ], + "summary": "Aggregate dashboard widget definitions from all enabled plugins.", + "description": "Aggregate dashboard widget definitions from all enabled plugins.\n\n**Auth:** permission:assets.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/dashboard/widgets -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/dashboarddefaults": { + "get": { + "tags": [ + "core-dashboarddefaults" + ], + "summary": "List all visitor-IP -> business-unit mappings.", + "description": "List all visitor-IP -> business-unit mappings.\n\n**Auth:** permission:settings.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/dashboarddefaults -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "post": { + "tags": [ + "core-dashboarddefaults" + ], + "summary": "Create a visitor-IP -> business-unit mapping.", + "description": "Create a visitor-IP -> business-unit mapping.\n\n**Auth:** jwt + role:admin\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/dashboarddefaults -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/dashboarddefaults/{default_id}": { + "delete": { + "tags": [ + "core-dashboarddefaults" + ], + "summary": "Delete (deactivate) a mapping.", + "description": "Delete (deactivate) a mapping.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: default_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/dashboarddefaults/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "default_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "core-dashboarddefaults" + ], + "summary": "Update a mapping.", + "description": "Update a mapping.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: default_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/dashboarddefaults/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "default_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: default_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/dashboarddefaults/display-role": { + "get": { + "tags": [ + "core-dashboarddefaults" + ], + "summary": "Resolve what a single 'display' PC should show, from its own IP.", + "description": "Resolve what a single 'display' PC should show, from its own IP.\n\n**Auth:** none (reviewed: named in the public-read or mutation sweep)\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/dashboarddefaults/display-role\n```", + "security": [], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + }, + "/api/dashboarddefaults/visitor-location": { + "get": { + "tags": [ + "core-dashboarddefaults" + ], + "summary": "Resolve the business unit for the calling display by its IP.", + "description": "Resolve the business unit for the calling display by its IP.\n\n**Auth:** none (reviewed: named in the public-read or mutation sweep)\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/dashboarddefaults/visitor-location\n```", + "security": [], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + }, + "/api/docs": { + "get": { + "tags": [ + "core-docs" + ], + "summary": "Redoc page. url_for keeps the asset URLs correct under any mount.", + "description": "Redoc page. url_for keeps the asset URLs correct under any mount.\n\n**Auth:** none (reviewed: named in the public-read or mutation sweep)\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/docs/\n```", + "security": [], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + }, + "/api/docs/llms.txt": { + "get": { + "tags": [ + "core-docs" + ], + "summary": "Concise LLM-oriented API guide.", + "description": "Concise LLM-oriented API guide.\n\n**Auth:** none (reviewed: named in the public-read or mutation sweep)\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/docs/llms.txt\n```", + "security": [], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + }, + "/api/docs/openapi.json": { + "get": { + "tags": [ + "core-docs" + ], + "summary": "The generated OpenAPI 3.1 spec (machine + LLM readable).", + "description": "The generated OpenAPI 3.1 spec (machine + LLM readable).\n\n**Auth:** none (reviewed: named in the public-read or mutation sweep)\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/docs/openapi.json\n```", + "security": [], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + }, + "/api/docs/redoc.standalone.js": { + "get": { + "tags": [ + "core-docs" + ], + "summary": "Vendored Redoc bundle (offline).", + "description": "Vendored Redoc bundle (offline).\n\n**Auth:** none (reviewed: named in the public-read or mutation sweep)\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/docs/redoc.standalone.js\n```", + "security": [], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + }, + "/api/locations": { + "get": { + "tags": [ + "core-locations" + ], + "summary": "List all locations.", + "description": "List all locations.\n\n**Auth:** permission:assets.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/locations -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "post": { + "tags": [ + "core-locations" + ], + "summary": "Create a new location.", + "description": "Create a new location.\n\n**Auth:** jwt + role:admin\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/locations -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/locations/{location_id}": { + "delete": { + "tags": [ + "core-locations" + ], + "summary": "Delete (deactivate) a location.", + "description": "Delete (deactivate) a location.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: location_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/locations/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "location_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "get": { + "tags": [ + "core-locations" + ], + "summary": "Get a single location.", + "description": "Get a single location.\n\n**Auth:** permission:assets.view\n\n**Params:** path: location_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/locations/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "location_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "core-locations" + ], + "summary": "Update a location.", + "description": "Update a location.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: location_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/locations/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "location_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: location_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/locations/types": { + "get": { + "tags": [ + "core-locations" + ], + "summary": "List location types. ?active=false includes inactive ones.", + "description": "List location types. ?active=false includes inactive ones.\n\n**Auth:** permission:assets.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/locations/types -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "post": { + "tags": [ + "core-locations" + ], + "summary": "UNDOCUMENTED: locations.create_location_type has no docstring.", + "description": "UNDOCUMENTED: locations.create_location_type has no docstring.\n\n**Auth:** jwt + role:admin\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/locations/types -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/locations/types/{type_id}": { + "delete": { + "tags": [ + "core-locations" + ], + "summary": "UNDOCUMENTED: locations.delete_location_type has no docstring.", + "description": "UNDOCUMENTED: locations.delete_location_type has no docstring.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: type_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/locations/types/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "type_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "core-locations" + ], + "summary": "UNDOCUMENTED: locations.update_location_type has no docstring.", + "description": "UNDOCUMENTED: locations.update_location_type has no docstring.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: type_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/locations/types/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "type_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: type_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/maplevels": { + "get": { + "tags": [ + "core-maplevels" + ], + "summary": "Every active level, grouped by building, in display order.", + "description": "Every active level, grouped by building, in display order.\n\n**Auth:** jwt-optional (works signed out; the handler decides what shows)\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/maplevels -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + }, + {} + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "post": { + "tags": [ + "core-maplevels" + ], + "summary": "UNDOCUMENTED: maplevels.create_level has no docstring.", + "description": "UNDOCUMENTED: maplevels.create_level has no docstring.\n\n**Auth:** jwt + role:admin\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/maplevels -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/maplevels/{levelid}": { + "delete": { + "tags": [ + "core-maplevels" + ], + "summary": "Deactivate a level, refusing while assets are still placed on it.", + "description": "Deactivate a level, refusing while assets are still placed on it.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: levelid (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/maplevels/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "levelid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "get": { + "tags": [ + "core-maplevels" + ], + "summary": "UNDOCUMENTED: maplevels.get_level has no docstring.", + "description": "UNDOCUMENTED: maplevels.get_level has no docstring.\n\n**Auth:** jwt-optional (works signed out; the handler decides what shows)\n\n**Params:** path: levelid (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/maplevels/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + }, + {} + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "levelid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "patch": { + "tags": [ + "core-maplevels" + ], + "summary": "UNDOCUMENTED: maplevels.update_level has no docstring.", + "description": "UNDOCUMENTED: maplevels.update_level has no docstring.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: levelid (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PATCH http://localhost:5001/api/maplevels/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "levelid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: levelid (int); body: JSON (fields not yet documented)" + } + } + } + } + }, + "put": { + "tags": [ + "core-maplevels" + ], + "summary": "UNDOCUMENTED: maplevels.update_level has no docstring.", + "description": "UNDOCUMENTED: maplevels.update_level has no docstring.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: levelid (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PATCH http://localhost:5001/api/maplevels/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "levelid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: levelid (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/maplevels/{levelid}/blueprint": { + "post": { + "tags": [ + "core-maplevels" + ], + "summary": "Upload this level's blueprint for one theme.", + "description": "Upload this level's blueprint for one theme.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: levelid (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/maplevels/1/blueprint -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "levelid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: levelid (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/maplevels/{levelid}/blueprint/{filename}": { + "get": { + "tags": [ + "core-maplevels" + ], + "summary": "Serve a level's blueprint image. Public, for the same reason as above.", + "description": "Serve a level's blueprint image. Public, for the same reason as above.\n\n**Auth:** jwt-optional (works signed out; the handler decides what shows)\n\n**Params:** path: levelid (int), filename (path)\n\n**Example:**\n```\ncurl http://localhost:5001/api/maplevels/1/blueprint/example.png -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + }, + {} + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "levelid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "filename", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "may contain slashes" + } + } + ] + } + }, + "/api/maplevels/buildings": { + "post": { + "tags": [ + "core-maplevels" + ], + "summary": "UNDOCUMENTED: maplevels.create_building has no docstring.", + "description": "UNDOCUMENTED: maplevels.create_building has no docstring.\n\n**Auth:** jwt + role:admin\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/maplevels/buildings -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/maplevels/buildings/{buildingid}": { + "patch": { + "tags": [ + "core-maplevels" + ], + "summary": "Rename or reorder a building. Levels move with it; nothing repositions.", + "description": "Rename or reorder a building. Levels move with it; nothing repositions.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: buildingid (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PATCH http://localhost:5001/api/maplevels/buildings/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "buildingid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: buildingid (int); body: JSON (fields not yet documented)" + } + } + } + } + }, + "put": { + "tags": [ + "core-maplevels" + ], + "summary": "Rename or reorder a building. Levels move with it; nothing repositions.", + "description": "Rename or reorder a building. Levels move with it; nothing repositions.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: buildingid (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PATCH http://localhost:5001/api/maplevels/buildings/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "buildingid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: buildingid (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/mappositions/positions": { + "post": { + "tags": [ + "core-mappositions" + ], + "summary": "Set positions for many assets at once.", + "description": "Set positions for many assets at once.\n\n**Auth:** permission:assets.edit\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/mappositions/positions -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/mappositions/snapshots": { + "get": { + "tags": [ + "core-mappositions" + ], + "summary": "Snapshots, newest first. Metadata only - the positions are large.", + "description": "Snapshots, newest first. Metadata only - the positions are large.\n\n**Auth:** permission:assets.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/mappositions/snapshots -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/mappositions/snapshots/{snapshotid}/restore": { + "post": { + "tags": [ + "core-mappositions" + ], + "summary": "Put the positions in this snapshot back.", + "description": "Put the positions in this snapshot back.\n\n**Auth:** permission:assets.edit\n\n**Params:** path: snapshotid (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/mappositions/snapshots/1/restore -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "snapshotid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: snapshotid (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/mappositions/transform": { + "post": { + "tags": [ + "core-mappositions" + ], + "summary": "Move every placed marker on a level by a transform read off landmarks.", + "description": "Move every placed marker on a level by a transform read off landmarks.\n\n**Auth:** permission:assets.edit\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/mappositions/transform -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/mappositions/verify": { + "post": { + "tags": [ + "core-mappositions" + ], + "summary": "Mark markers as reviewed against the current drawing without moving them.", + "description": "Mark markers as reviewed against the current drawing without moving them.\n\n**Auth:** permission:assets.edit\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/mappositions/verify -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/models": { + "get": { + "tags": [ + "core-models" + ], + "summary": "List all vendor catalog models.", + "description": "List all vendor catalog models.\n\n**Auth:** permission:assets.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/models -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "post": { + "tags": [ + "core-models" + ], + "summary": "Create a new model.", + "description": "Create a new model.\n\n**Auth:** jwt + role:admin\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/models -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/models/{model_id}": { + "delete": { + "tags": [ + "core-models" + ], + "summary": "Delete (deactivate) a model.", + "description": "Delete (deactivate) a model.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: model_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/models/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "model_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "get": { + "tags": [ + "core-models" + ], + "summary": "Get a single model.", + "description": "Get a single model.\n\n**Auth:** permission:assets.view\n\n**Params:** path: model_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/models/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "model_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "core-models" + ], + "summary": "Update a model.", + "description": "Update a model.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: model_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/models/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "model_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: model_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/models/{model_id}/image": { + "delete": { + "tags": [ + "core-models" + ], + "summary": "Clear a model image and delete the uploaded file if we own it.", + "description": "Clear a model image and delete the uploaded file if we own it.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: model_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/models/1/image -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "model_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "post": { + "tags": [ + "core-models" + ], + "summary": "Upload (or replace) the photo for a model.", + "description": "Upload (or replace) the photo for a model.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: model_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/models/1/image -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "model_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: model_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/models/image/{filename}": { + "get": { + "tags": [ + "core-models" + ], + "summary": "Serve an uploaded model image (public - asset detail pages read it).", + "description": "Serve an uploaded model image (public - asset detail pages read it).\n\n**Auth:** none (reviewed: named in the public-read or mutation sweep)\n\n**Params:** path: filename (path)\n\n**Example:**\n```\ncurl http://localhost:5001/api/models/image/example.png\n```", + "security": [], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "filename", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "may contain slashes" + } + } + ] + } + }, + "/api/modeltypes": { + "get": { + "tags": [ + "core-modeltypes" + ], + "summary": "List all model types with optional filtering.", + "description": "List all model types with optional filtering.\n\n**Auth:** permission:assets.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/modeltypes -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "post": { + "tags": [ + "core-modeltypes" + ], + "summary": "Create a new model type.", + "description": "Create a new model type.\n\n**Auth:** jwt + role:admin\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/modeltypes -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/modeltypes/{type_id}": { + "delete": { + "tags": [ + "core-modeltypes" + ], + "summary": "Delete (deactivate) a model type.", + "description": "Delete (deactivate) a model type.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: type_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/modeltypes/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "type_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "get": { + "tags": [ + "core-modeltypes" + ], + "summary": "Get a single model type.", + "description": "Get a single model type.\n\n**Auth:** permission:assets.view\n\n**Params:** path: type_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/modeltypes/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "type_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "core-modeltypes" + ], + "summary": "Update a model type.", + "description": "Update a model type.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: type_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/modeltypes/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "type_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: type_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/operatingsystems": { + "get": { + "tags": [ + "core-operatingsystems" + ], + "summary": "List all operating systems.", + "description": "List all operating systems.\n\n**Auth:** permission:assets.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/operatingsystems -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "post": { + "tags": [ + "core-operatingsystems" + ], + "summary": "Create a new operating system.", + "description": "Create a new operating system.\n\n**Auth:** jwt + role:admin\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/operatingsystems -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/operatingsystems/{os_id}": { + "delete": { + "tags": [ + "core-operatingsystems" + ], + "summary": "Delete (deactivate) an operating system.", + "description": "Delete (deactivate) an operating system.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: os_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/operatingsystems/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "os_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "get": { + "tags": [ + "core-operatingsystems" + ], + "summary": "Get a single operating system.", + "description": "Get a single operating system.\n\n**Auth:** permission:assets.view\n\n**Params:** path: os_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/operatingsystems/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "os_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "core-operatingsystems" + ], + "summary": "Update an operating system.", + "description": "Update an operating system.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: os_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/operatingsystems/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "os_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: os_id (int); body: JSON (fields not yet documented)" } } } @@ -3541,12 +6812,11 @@ "/api/plugins": { "get": { "tags": [ - "core-platform" + "core-plugins" ], - "summary": "List all discovered plugins (enabled or not) with metadata plus the framework contract version", - "description": "List all discovered plugins (enabled or not) with metadata plus the framework contract version\n\n**Auth:** jwt-optional\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/plugins\n```", + "summary": "List all discovered plugins (enabled or not) with their metadata and the framework contract version.", + "description": "List all discovered plugins (enabled or not) with their metadata and the framework contract version.\n\n**Auth:** permission:settings.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/plugins -H \"Authorization: Bearer $TOK\"\n```", "security": [ - {}, { "bearerAuth": [] } @@ -3567,72 +6837,12 @@ "$ref": "#/components/schemas/ErrorEnvelope" } } - } - } - } - }, - "/api/plugins/enabled": { - "get": { - "tags": [ - "core-platform" - ], - "summary": "Flat sorted array of enabled plugin names (registry read, no DB); deliberately anonymous so kiosk routes can gate...", - "description": "Flat sorted array of enabled plugin names (registry read, no DB); deliberately anonymous so kiosk routes can gate plugin-owned frontend routes\n\n**Auth:** none (jwt-optional decorator, no claims used)\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/plugins/enabled\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/plugins/settings-routes": { - "get": { - "tags": [ - "core-platform" - ], - "summary": "Settings routes contributed by enabled plugins for dynamic UI extension", - "description": "Returns settings routes from all enabled plugins via get_settings_routes() hook. Frontend router merges these to add plugin tabs/pages to settings hubs like Model Catalog.\n\n**Auth:** none (jwt-optional decorator, deliberately anonymous so unauthenticated pages can load plugin routes)\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/plugins/settings-routes\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } + "401": { + "description": "Missing or invalid credentials." }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } + "403": { + "description": "Authenticated, but not permitted." } } } @@ -3640,10 +6850,10 @@ "/api/plugins/{name}": { "put": { "tags": [ - "core-platform" + "core-plugins" ], - "summary": "Enable or disable a plugin (route changes need an app restart); 409 when unknown or a dependency conflict refuses the...", - "description": "Enable or disable a plugin (route changes need an app restart); 409 when unknown or a dependency conflict refuses the change\n\n**Auth:** jwt + role:admin\n\n**Params:** body: enabled (bool, required)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/plugins/warranty -H \"Authorization: Bearer $JWT\" -H 'Content-Type: application/json' -d '{\"enabled\":false}'\n```", + "summary": "Enable or disable a plugin. Takes effect on the next app restart for route/navigation changes.", + "description": "Enable or disable a plugin. Takes effect on the next app restart for route/navigation changes.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: name (string); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/plugins/x -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -3693,22 +6903,54 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "body: enabled (bool, required)" + "description": "path: name (string); body: JSON (fields not yet documented)" } } } } } }, - "/api/pluginui/settings-cards": { + "/api/plugins/enabled": { "get": { "tags": [ - "core-platform" + "core-plugins" ], - "summary": "Merge enabled plugins' settings-catalog cards (get_settings_cards hook) sorted by position for the settings...", - "description": "Merge enabled plugins' settings-catalog cards (get_settings_cards hook) sorted by position for the settings rail/overview\n\n**Auth:** jwt-optional\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/pluginui/settings-cards\n```", + "summary": "Return just the names of enabled plugins as a flat array.", + "description": "Return just the names of enabled plugins as a flat array.\n\n**Auth:** jwt-optional (works signed out; the handler decides what shows)\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/plugins/enabled -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + }, + {} + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + }, + "/api/plugins/settings-routes": { + "get": { + "tags": [ + "core-plugins" + ], + "summary": "Return settings routes contributed by all enabled plugins.", + "description": "Return settings routes contributed by all enabled plugins.\n\n**Auth:** permission:settings.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/plugins/settings-routes -H \"Authorization: Bearer $TOK\"\n```", "security": [ - {}, { "bearerAuth": [] } @@ -3729,6 +6971,12 @@ "$ref": "#/components/schemas/ErrorEnvelope" } } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." } } } @@ -3736,12 +6984,11 @@ "/api/pluginui/asset-panels": { "get": { "tags": [ - "core-platform" + "core-pluginui" ], - "summary": "Asset-detail panels from enabled plugins (get_asset_panels hook) matching one asset's type ('*' wildcard supported)...", - "description": "Asset-detail panels from enabled plugins (get_asset_panels hook) matching one asset's type ('*' wildcard supported), sorted by position\n\n**Auth:** jwt-optional\n\n**Params:** assetid (int, required); 400 without it, 404 if asset missing\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/pluginui/asset-panels?assetid=42'\n```", + "summary": "Return the asset-detail panels matching one asset's type (get_asset_panels).", + "description": "Return the asset-detail panels matching one asset's type (get_asset_panels).\n\n**Auth:** permission:assets.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/pluginui/asset-panels -H \"Authorization: Bearer $TOK\"\n```", "security": [ - {}, { "bearerAuth": [] } @@ -3762,39 +7009,12 @@ "$ref": "#/components/schemas/ErrorEnvelope" } } - } - } - } - }, - "/api/pluginui/map-overlays": { - "get": { - "tags": [ - "core-platform" - ], - "summary": "Merge enabled plugins' map overlay declarations (get_map_overlays hook), sorted by position", - "description": "Merge enabled plugins' map overlay declarations (get_map_overlays hook), sorted by position\n\n**Auth:** jwt-optional\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/pluginui/map-overlays\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." } } } @@ -3802,12 +7022,11 @@ "/api/pluginui/asset-presentation": { "get": { "tags": [ - "core-platform" + "core-pluginui" ], - "summary": "Merge enabled plugins' asset-type presentation entries (icon + detail route per type) used by search rows and...", - "description": "Merge enabled plugins' asset-type presentation entries (icon + detail route per type) used by search rows and cross-links\n\n**Auth:** jwt-optional\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/pluginui/asset-presentation\n```", + "summary": "Merge enabled plugins' asset-type presentation entries (hook).", + "description": "Merge enabled plugins' asset-type presentation entries (hook).\n\n**Auth:** permission:assets.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/pluginui/asset-presentation -H \"Authorization: Bearer $TOK\"\n```", "security": [ - {}, { "bearerAuth": [] } @@ -3828,18 +7047,935 @@ "$ref": "#/components/schemas/ErrorEnvelope" } } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." } } } }, - "/api/employees/search": { + "/api/pluginui/asset-type-descriptors": { "get": { "tags": [ - "plugin-employees" + "core-pluginui" + ], + "summary": "Merge enabled plugins' asset-type subtype descriptors.", + "description": "Merge enabled plugins' asset-type subtype descriptors.\n\n**Auth:** permission:assets.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/pluginui/asset-type-descriptors -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/pluginui/core-type-descriptors": { + "get": { + "tags": [ + "core-pluginui" + ], + "summary": "Return type descriptors for core-owned non-asset types.", + "description": "Return type descriptors for core-owned non-asset types.\n\n**Auth:** permission:assets.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/pluginui/core-type-descriptors -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/pluginui/map-overlays": { + "get": { + "tags": [ + "core-pluginui" + ], + "summary": "Merge enabled plugins' map overlay declarations (get_map_overlays hook).", + "description": "Merge enabled plugins' map overlay declarations (get_map_overlays hook).\n\n**Auth:** permission:assets.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/pluginui/map-overlays -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/pluginui/settings-cards": { + "get": { + "tags": [ + "core-pluginui" + ], + "summary": "Merge enabled plugins' settings-catalog cards (get_settings_cards hook).", + "description": "Merge enabled plugins' settings-catalog cards (get_settings_cards hook).\n\n**Auth:** permission:assets.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/pluginui/settings-cards -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/reports": { + "get": { + "tags": [ + "core-reports" + ], + "summary": "List all available reports.", + "description": "List all available reports.\n\n**Auth:** permission:reports.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/reports -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/reports/asset-inventory": { + "get": { + "tags": [ + "core-reports" + ], + "summary": "Report: Complete asset inventory summary.", + "description": "Report: Complete asset inventory summary.\n\n**Auth:** permission:reports.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/reports/asset-inventory -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/reports/assets-by-status": { + "get": { + "tags": [ + "core-reports" + ], + "summary": "Report: Asset count grouped by status.", + "description": "Report: Asset count grouped by status.\n\n**Auth:** permission:reports.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/reports/assets-by-status -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/reports/email": { + "post": { + "tags": [ + "core-reports" + ], + "summary": "Email a report's data as an HTML table (on-demand delivery).", + "description": "Email a report's data as an HTML table (on-demand delivery).\n\n**Auth:** permission:reports.export\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/reports/email -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/reports/kb-popularity": { + "get": { + "tags": [ + "core-reports" + ], + "summary": "Report: Most clicked knowledge base articles.", + "description": "Report: Most clicked knowledge base articles.\n\n**Auth:** permission:reports.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/reports/kb-popularity -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/reports/machines-by-type": { + "get": { + "tags": [ + "core-reports" + ], + "summary": "Report: Machine count grouped by machine type.", + "description": "Report: Machine count grouped by machine type.\n\n**Auth:** permission:reports.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/reports/machines-by-type -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/reports/pc-relationships": { + "get": { + "tags": [ + "core-reports" + ], + "summary": "Report: PCs with relationships to shop floor machines.", + "description": "Report: PCs with relationships to shop floor machines.\n\n**Auth:** permission:reports.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/reports/pc-relationships -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/reports/software-compliance": { + "get": { + "tags": [ + "core-reports" + ], + "summary": "Report: Required applications vs installed (per PC).", + "description": "Report: Required applications vs installed (per PC).\n\n**Auth:** permission:reports.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/reports/software-compliance -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/scripts": { + "get": { + "tags": [ + "core-scripts" + ], + "summary": "List scripts (active by default).", + "description": "List scripts (active by default).\n\n**Auth:** permission:endpoints.runscript\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/scripts -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "post": { + "tags": [ + "core-scripts" + ], + "summary": "Create a script catalog entry (body uploaded separately).", + "description": "Create a script catalog entry (body uploaded separately).\n\n**Auth:** permission:endpoints.runscript\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/scripts -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/scripts/{script_id}": { + "delete": { + "tags": [ + "core-scripts" + ], + "summary": "UNDOCUMENTED: scripts.delete_script has no docstring.", + "description": "UNDOCUMENTED: scripts.delete_script has no docstring.\n\n**Auth:** permission:endpoints.runscript\n\n**Params:** path: script_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/scripts/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "script_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "get": { + "tags": [ + "core-scripts" + ], + "summary": "UNDOCUMENTED: scripts.get_script has no docstring.", + "description": "UNDOCUMENTED: scripts.get_script has no docstring.\n\n**Auth:** permission:endpoints.runscript\n\n**Params:** path: script_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/scripts/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "script_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "core-scripts" + ], + "summary": "UNDOCUMENTED: scripts.update_script has no docstring.", + "description": "UNDOCUMENTED: scripts.update_script has no docstring.\n\n**Auth:** permission:endpoints.runscript\n\n**Params:** path: script_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/scripts/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "script_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: script_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/scripts/{script_id}/body": { + "post": { + "tags": [ + "core-scripts" + ], + "summary": "Upload (or replace) the .ps1 body for a script.", + "description": "Upload (or replace) the .ps1 body for a script.\n\n**Auth:** permission:endpoints.runscript\n\n**Params:** path: script_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/scripts/1/body -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "script_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: script_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/scripts/body/{filename}": { + "get": { + "tags": [ + "core-scripts" + ], + "summary": "Operator-facing script body download (JWT/RBAC).", + "description": "Operator-facing script body download (JWT/RBAC).\n\n**Auth:** permission:endpoints.runscript\n\n**Params:** path: filename (path)\n\n**Example:**\n```\ncurl http://localhost:5001/api/scripts/body/example.png -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "filename", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "may contain slashes" + } + } + ] + } + }, + "/api/search": { + "get": { + "tags": [ + "core-search" + ], + "summary": "Global search across multiple entity types.", + "description": "Global search across multiple entity types.\n\n**Auth:** permission:assets.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/search -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/search/suggest": { + "get": { + "tags": [ + "core-search" + ], + "summary": "A better spelling for a query that found nothing, or null.", + "description": "A better spelling for a query that found nothing, or null.\n\n**Auth:** permission:assets.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/search/suggest -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/settings": { + "get": { + "tags": [ + "core-settings" + ], + "summary": "List all settings, optionally filtered by category.", + "description": "List all settings, optionally filtered by category.\n\n**Auth:** jwt-optional (works signed out; the handler decides what shows)\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/settings -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + }, + {} ], - "summary": "Search employees by first name, last name, or SSO substring (self-hosted table or external HR DB depending on...", - "description": "Search employees by first name, last name, or SSO substring (self-hosted table or external HR DB depending on employee_directory_mode setting).\n\n**Auth:** none\n\n**Params:** q (query string, min 2 chars, required), limit (max results, default 10, capped 50)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/employees/search?q=smith&limit=5'\n```", - "security": [], "responses": { "200": { "description": "Success. Body is the success_response envelope: {status, data, meta}.", @@ -3858,16 +7994,69 @@ } } } + }, + "post": { + "tags": [ + "core-settings" + ], + "summary": "Create a new setting (admin only).", + "description": "Create a new setting (admin only).\n\n**Auth:** permission:settings.edit\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/settings -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } } }, - "/api/employees/lookup/{sso}": { + "/api/settings/{key}": { "get": { "tags": [ - "plugin-employees" + "core-settings" + ], + "summary": "Get a single setting by key.", + "description": "Get a single setting by key.\n\n**Auth:** jwt-optional (works signed out; the handler decides what shows)\n\n**Params:** path: key (string)\n\n**Example:**\n```\ncurl http://localhost:5001/api/settings/x -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + }, + {} ], - "summary": "Look up a single employee by numeric SSO; returns directory fields plus resolved photourl.", - "description": "Look up a single employee by numeric SSO; returns directory fields plus resolved photourl.\n\n**Auth:** none\n\n**Params:** sso (path, numeric)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/employees/lookup/210009518'\n```", - "security": [], "responses": { "200": { "description": "Success. Body is the success_response envelope: {status, data, meta}.", @@ -3891,7 +8080,245 @@ }, "parameters": [ { - "name": "sso", + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "put": { + "tags": [ + "core-settings" + ], + "summary": "Update a setting value.", + "description": "Update a setting value.\n\n**Auth:** permission:settings.edit\n\n**Params:** path: key (string); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/settings/x -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: key (string); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/settings/asset-import/import": { + "post": { + "tags": [ + "core-settings" + ], + "summary": "Validate uploaded CSVs and, when commit=true, load them.", + "description": "Validate uploaded CSVs and, when commit=true, load them.\n\n**Auth:** jwt + role:admin\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/settings/asset-import/import -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/settings/asset-import/script": { + "get": { + "tags": [ + "core-settings" + ], + "summary": "Download Collect-ShopDB.ps1 with this site's CSV path embedded.", + "description": "Download Collect-ShopDB.ps1 with this site's CSV path embedded.\n\n**Auth:** jwt + role:admin\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/settings/asset-import/script -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/settings/asset-import/templates": { + "get": { + "tags": [ + "core-settings" + ], + "summary": "List the importable CSV tables in the order they must be uploaded.", + "description": "List the importable CSV tables in the order they must be uploaded.\n\n**Auth:** jwt + role:admin\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/settings/asset-import/templates -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/settings/asset-import/templates/{tablename}": { + "get": { + "tags": [ + "core-settings" + ], + "summary": "Download a generated CSV template for a single importable table.", + "description": "Download a generated CSV template for a single importable table.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: tablename (string)\n\n**Example:**\n```\ncurl http://localhost:5001/api/settings/asset-import/templates/x -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "tablename", "in": "path", "required": true, "schema": { @@ -3901,14 +8328,18 @@ ] } }, - "/api/employees/lookup": { + "/api/settings/asset-import/winc/batches": { "get": { "tags": [ - "plugin-employees" + "core-settings" + ], + "summary": "List applied/undone WINC import batches, newest first (for the undo UI).", + "description": "List applied/undone WINC import batches, newest first (for the undo UI).\n\n**Auth:** jwt + role:admin\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/settings/asset-import/winc/batches -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } ], - "summary": "Bulk lookup of multiple employees by SSO list; returns employees array plus a joined names string.", - "description": "Bulk lookup of multiple employees by SSO list; returns employees array plus a joined names string.\n\n**Auth:** none\n\n**Params:** sso (query, comma-separated numeric SSOs, at least one required)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/employees/lookup?sso=210009518,210001234'\n```", - "security": [], "responses": { "200": { "description": "Success. Body is the success_response envelope: {status, data, meta}.", @@ -3925,372 +8356,223 @@ "$ref": "#/components/schemas/ErrorEnvelope" } } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." } } } }, - "/api/employees/directory": { + "/api/settings/asset-import/winc/commit": { + "post": { + "tags": [ + "core-settings" + ], + "summary": "Apply a WINC plan reversibly. Requires acknowledgment for needs-create.", + "description": "Apply a WINC plan reversibly. Requires acknowledgment for needs-create.\n\n**Auth:** jwt + role:admin\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/settings/asset-import/winc/commit -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/settings/asset-import/winc/preview": { + "post": { + "tags": [ + "core-settings" + ], + "summary": "Read-only WINC analysis: report what a commit would do. Writes nothing.", + "description": "Read-only WINC analysis: report what a commit would do. Writes nothing.\n\n**Auth:** jwt + role:admin\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/settings/asset-import/winc/preview -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/settings/asset-import/winc/undo": { + "post": { + "tags": [ + "core-settings" + ], + "summary": "Reverse an applied WINC import batch by id. Conservative and reversible.", + "description": "Reverse an applied WINC import batch by id. Conservative and reversible.\n\n**Auth:** jwt + role:admin\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/settings/asset-import/winc/undo -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/settings/branding-logo": { + "post": { + "tags": [ + "core-settings" + ], + "summary": "Upload a branding logo image and point the matching setting at it.", + "description": "Upload a branding logo image and point the matching setting at it.\n\n**Auth:** jwt + role:admin\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/settings/branding-logo -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/settings/branding/{filename}": { "get": { "tags": [ - "plugin-employees" + "core-settings" ], - "summary": "List the full self-hosted directory for the management page; 400 when directory mode is external.", - "description": "List the full self-hosted directory for the management page; 400 when directory mode is external.\n\n**Auth:** jwt-optional\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/employees/directory'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - }, - "post": { - "tags": [ - "plugin-employees" - ], - "summary": "Create a self-hosted directory employee; 409 if SSO exists, 400 in external mode.", - "description": "Create a self-hosted directory employee; 409 if SSO exists, 400 in external mode.\n\n**Auth:** jwt + require_role admin\n\n**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)\n\n**Example:**\n```\ncurl -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'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "JSON body: sso (numeric, required), firstname, lastname (required), team, role, picture (also accepts external-style keys SSO/First_Name/Last_Name/Team/Role/Picture)" - } - } - } - } - } - }, - "/api/employees/directory/{sso}": { - "put": { - "tags": [ - "plugin-employees" - ], - "summary": "Update a self-hosted directory employee's name/team/role/picture; 404 if missing, 400 in external mode.", - "description": "Update a self-hosted directory employee's name/team/role/picture; 404 if missing, 400 in external mode.\n\n**Auth:** jwt + require_role admin\n\n**Params:** sso (path); JSON body: firstname, lastname, team, role, picture (external-style keys also accepted; team/role/picture can be cleared)\n\n**Example:**\n```\ncurl -X PUT -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"team\":\"Quality\"}' 'http://localhost:5001/api/employees/directory/210009518'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "sso", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "sso (path); JSON body: firstname, lastname, team, role, picture (external-style keys also accepted; team/role/picture can be cleared)" - } - } - } - } - }, - "delete": { - "tags": [ - "plugin-employees" - ], - "summary": "Delete a self-hosted directory employee; 404 if missing, 400 in external mode.", - "description": "Delete a self-hosted directory employee; 404 if missing, 400 in external mode.\n\n**Auth:** jwt + require_role admin\n\n**Params:** sso (path)\n\n**Example:**\n```\ncurl -X DELETE -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/employees/directory/210009518'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "sso", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ] - } - }, - "/api/employees/directory/import": { - "post": { - "tags": [ - "plugin-employees" - ], - "summary": "Bulk upsert the self-hosted directory from CSV (headers SSO,First_Name,Last_Name,Team,Role,Picture case-insensitive...", - "description": "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.\n\n**Auth:** jwt + require_role admin\n\n**Params:** multipart file= OR JSON body {\"csv\": \"...\"}; rows missing numeric sso or names are skipped\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" -F 'file=@employees.csv' 'http://localhost:5001/api/employees/directory/import'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "multipart file= OR JSON body {\"csv\": \"...\"}; rows missing numeric sso or names are skipped" - } - } - } - } - } - }, - "/api/employees/{sso}/photo": { - "post": { - "tags": [ - "plugin-employees" - ], - "summary": "Upload or replace a self-hosted employee's photo (saved as photo- in instance employeephotos dir; old file...", - "description": "Upload or replace a self-hosted employee's photo (saved as photo- in instance employeephotos dir; old file wiped even on extension change); 409 in external mode, 404 if employee missing.\n\n**Auth:** jwt + require_role admin\n\n**Params:** sso (path); multipart/form-data file=, extensions .png/.jpg/.jpeg/.gif/.webp only\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" -F 'file=@jane.jpg' 'http://localhost:5001/api/employees/210009518/photo'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "sso", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "sso (path); multipart/form-data file=, extensions .png/.jpg/.jpeg/.gif/.webp only" - } - } - } - } - }, - "delete": { - "tags": [ - "plugin-employees" - ], - "summary": "Clear a self-hosted employee's photo record and delete the uploaded file; 409 in external mode, 404 if employee missing.", - "description": "Clear a self-hosted employee's photo record and delete the uploaded file; 409 in external mode, 404 if employee missing.\n\n**Auth:** jwt + require_role admin\n\n**Params:** sso (path)\n\n**Example:**\n```\ncurl -X DELETE -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/employees/210009518/photo'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "sso", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ] - } - }, - "/api/employees/photo/{filename}": { - "get": { - "tags": [ - "plugin-employees" - ], - "summary": "Serve an uploaded employee photo file from the instance employeephotos dir (public so kiosk recognition cards can read...", - "description": "Serve an uploaded employee photo file from the instance employeephotos dir (public so kiosk recognition cards can read it).\n\n**Auth:** none\n\n**Params:** filename (path, e.g. photo-210009518.jpg)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/employees/photo/photo-210009518.jpg'\n```", + "summary": "Serve an uploaded branding logo (public - kiosks/print pages read it).", + "description": "Serve an uploaded branding logo (public - kiosks/print pages read it).\n\n**Auth:** none (reviewed: named in the public-read or mutation sweep)\n\n**Params:** path: filename (path)\n\n**Example:**\n```\ncurl http://localhost:5001/api/settings/branding/example.png\n```", "security": [], "responses": { "200": { @@ -4326,95 +8608,13 @@ ] } }, - "/api/employees/resolve/{sso}": { - "get": { - "tags": [ - "plugin-employees" - ], - "summary": "Resolve one SSO to a name and photo, and say HOW it resolved or why it did not. Built because the failure is otherwise...", - "description": "Resolve one SSO to a name and photo, and say HOW it resolved or why it did not. Built because the failure is otherwise invisible: the shop-floor board resolves names through functions that returned None on any miss, so a directory problem and a typo looked identical\n\n**Auth:** jwt\n\n**Params:** sso in path\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/employees/resolve/123456789\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "sso", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/knowledgebase": { - "get": { - "tags": [ - "plugin-knowledgebase" - ], - "summary": "List active KB articles with search, topic filter, natural-key lookup, sorting, and pagination; each item includes its...", - "description": "List active KB articles with search, topic filter, natural-key lookup, sorting, and pagination; each item includes its linked application (appid/appname) or null.\n\n**Auth:** jwt-optional\n\n**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)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/knowledgebase?search=printer&sort=clicks&order=desc&page=1&per_page=20'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - }, + "/api/settings/map-blueprint": { "post": { "tags": [ - "plugin-knowledgebase" + "core-settings" ], - "summary": "Create a new KB article; validates required fields and that appid (if given) exists; honors X-Import-Mode timestamp...", - "description": "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.\n\n**Auth:** permission:kb.create (jwt required)\n\n**Params:** body JSON: shortdescription (required), linkurl (required), appid (optional int, must exist), keywords (optional); import-mode may pass timestamp fields\n\n**Example:**\n```\ncurl -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\"}'\n```", + "summary": "Upload a floor-map blueprint image and point the setting at it.", + "description": "Upload a floor-map blueprint image and point the setting at it.\n\n**Auth:** jwt + role:admin\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/settings/map-blueprint -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -4451,2410 +8651,20 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "body JSON: shortdescription (required), linkurl (required), appid (optional int, must exist), keywords (optional); import-mode may pass timestamp fields" + "description": "body: JSON (fields not yet documented)" } } } } } }, - "/api/knowledgebase/stats": { + "/api/settings/map-blueprint/{filename}": { "get": { "tags": [ - "plugin-knowledgebase" + "core-settings" ], - "summary": "Return aggregate stats for active articles: totalclicks (sum of clicks) and totalarticles (count).", - "description": "Return aggregate stats for active articles: totalclicks (sum of clicks) and totalarticles (count).\n\n**Auth:** jwt-optional\n\n**Params:** none\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/knowledgebase/stats'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/knowledgebase/{link_id}": { - "get": { - "tags": [ - "plugin-knowledgebase" - ], - "summary": "Fetch a single active article by id with its application (appid/appname) or null; 404 if missing or inactive.", - "description": "Fetch a single active article by id with its application (appid/appname) or null; 404 if missing or inactive.\n\n**Auth:** jwt-optional\n\n**Params:** path: link_id (int)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/knowledgebase/42'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "link_id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ] - }, - "put": { - "tags": [ - "plugin-knowledgebase" - ], - "summary": "Update an article's shortdescription, linkurl, appid, keywords, and/or isactive; validates appid if changed; honors...", - "description": "Update an article's shortdescription, linkurl, appid, keywords, and/or isactive; validates appid if changed; honors import timestamps; 404 if article missing.\n\n**Auth:** permission:kb.edit (jwt required)\n\n**Params:** path: link_id (int); body JSON: any of shortdescription, linkurl, appid, keywords, isactive\n\n**Example:**\n```\ncurl -X PUT 'http://localhost:5001/api/knowledgebase/42' -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"keywords\":\"vpn,zscaler\",\"isactive\":true}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "link_id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "path: link_id (int); body JSON: any of shortdescription, linkurl, appid, keywords, isactive" - } - } - } - } - }, - "delete": { - "tags": [ - "plugin-knowledgebase" - ], - "summary": "Soft-delete an article by setting isactive=false (row is retained); 404 if article missing.", - "description": "Soft-delete an article by setting isactive=false (row is retained); 404 if article missing.\n\n**Auth:** permission:kb.delete (jwt required)\n\n**Params:** path: link_id (int)\n\n**Example:**\n```\ncurl -X DELETE 'http://localhost:5001/api/knowledgebase/42' -H \"Authorization: Bearer $TOKEN\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "link_id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ] - } - }, - "/api/knowledgebase/{link_id}/click": { - "post": { - "tags": [ - "plugin-knowledgebase" - ], - "summary": "Increment the article's click counter and return the target linkurl plus new click count (used for redirect tracking)...", - "description": "Increment the article's click counter and return the target linkurl plus new click count (used for redirect tracking); 404 if missing or inactive.\n\n**Auth:** jwt-optional\n\n**Params:** path: link_id (int); no body\n\n**Example:**\n```\ncurl -X POST 'http://localhost:5001/api/knowledgebase/42/click'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "link_id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "path: link_id (int); no body" - } - } - } - } - } - }, - "/api/assets/types": { - "get": { - "tags": [ - "core-catalog" - ], - "summary": "List asset types (paginated, active-only by default)", - "description": "List asset types (paginated, active-only by default)\n\n**Auth:** jwt-optional\n\n**Params:** page, per_page, active=false to include inactive\n\n**Example:**\n```\ncurl http://localhost:5001/api/assets/types?active=false\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - }, - "post": { - "tags": [ - "core-catalog" - ], - "summary": "Create asset type (409 on duplicate name)", - "description": "Create asset type (409 on duplicate name)\n\n**Auth:** permission:assets.create\n\n**Params:** body: assettype (req), pluginname, tablename, description, icon, color\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/assets/types -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"assettype\":\"Robot\",\"icon\":\"mdi-robot\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: assettype (req), pluginname, tablename, description, icon, color" - } - } - } - } - } - }, - "/api/assets/types/{type_id}": { - "get": { - "tags": [ - "core-catalog" - ], - "summary": "Get one asset type", - "description": "Get one asset type\n\n**Auth:** jwt-optional\n\n**Params:** path: type_id\n\n**Example:**\n```\ncurl http://localhost:5001/api/assets/types/1\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "type_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - }, - "put": { - "tags": [ - "core-catalog" - ], - "summary": "Update asset type display fields only (name/plugin/table are structural, not editable)", - "description": "Update asset type display fields only (name/plugin/table are structural, not editable)\n\n**Auth:** permission:assets.edit\n\n**Params:** body: description, icon, color, isactive\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/assets/types/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"color\":\"#ff0000\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "type_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: description, icon, color, isactive" - } - } - } - } - } - }, - "/api/assets/statuses": { - "get": { - "tags": [ - "core-catalog" - ], - "summary": "List asset statuses (paginated, active-only by default)", - "description": "List asset statuses (paginated, active-only by default)\n\n**Auth:** jwt-optional\n\n**Params:** page, per_page, active=false\n\n**Example:**\n```\ncurl http://localhost:5001/api/assets/statuses\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - }, - "post": { - "tags": [ - "core-catalog" - ], - "summary": "Create asset status (409 on duplicate)", - "description": "Create asset status (409 on duplicate)\n\n**Auth:** permission:assets.create\n\n**Params:** body: status (req), description, color\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/assets/statuses -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"status\":\"In Repair\",\"color\":\"#f90\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: status (req), description, color" - } - } - } - } - } - }, - "/api/assets/statuses/{status_id}": { - "get": { - "tags": [ - "core-catalog" - ], - "summary": "Get one asset status", - "description": "Get one asset status\n\n**Auth:** jwt-optional\n\n**Params:** path: status_id\n\n**Example:**\n```\ncurl http://localhost:5001/api/assets/statuses/1\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "status_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - }, - "put": { - "tags": [ - "core-catalog" - ], - "summary": "Update asset status (rename conflict-checked)", - "description": "Update asset status (rename conflict-checked)\n\n**Auth:** permission:assets.edit\n\n**Params:** body: status, description, color, isactive\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/assets/statuses/2 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"isactive\":false}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "status_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: status, description, color, isactive" - } - } - } - } - }, - "delete": { - "tags": [ - "core-catalog" - ], - "summary": "Hard-delete asset status; 409 if any asset still uses it", - "description": "Hard-delete asset status; 409 if any asset still uses it\n\n**Auth:** permission:assets.delete\n\n**Params:** path: status_id\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/assets/statuses/9 -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "status_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/assets/relationshiptypes": { - "get": { - "tags": [ - "core-catalog" - ], - "summary": "List relationship types incl. read-only propagatesthrough rails", - "description": "List relationship types incl. read-only propagatesthrough rails\n\n**Auth:** jwt-optional\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/assets/relationshiptypes\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - }, - "post": { - "tags": [ - "core-catalog" - ], - "summary": "Create relationship type (409 on duplicate)", - "description": "Create relationship type (409 on duplicate)\n\n**Auth:** permission:assets.create\n\n**Params:** body: relationshiptype (req), description, color, isdirectional (default true)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/assets/relationshiptypes -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"relationshiptype\":\"controls\",\"isdirectional\":true}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: relationshiptype (req), description, color, isdirectional (default true)" - } - } - } - } - } - }, - "/api/assets/relationshiptypes/{type_id}": { - "put": { - "tags": [ - "core-catalog" - ], - "summary": "Update relationship type (rename conflict-checked)", - "description": "Update relationship type (rename conflict-checked)\n\n**Auth:** permission:assets.edit\n\n**Params:** body: relationshiptype, description, color, isdirectional\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/assets/relationshiptypes/3 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"color\":\"#00f\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "type_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: relationshiptype, description, color, isdirectional" - } - } - } - } - }, - "delete": { - "tags": [ - "core-catalog" - ], - "summary": "Hard-delete relationship type; 409 while relationships use it", - "description": "Hard-delete relationship type; 409 while relationships use it\n\n**Auth:** permission:assets.delete\n\n**Params:** path: type_id\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/assets/relationshiptypes/3 -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "type_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/assets": { - "get": { - "tags": [ - "core-catalog" - ], - "summary": "List assets with filtering, search, sorting, pagination", - "description": "List assets with filtering, search, sorting, pagination\n\n**Auth:** jwt-optional\n\n**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\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/assets?type=machine&search=205&include_type_data=true'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - }, - "post": { - "tags": [ - "core-catalog" - ], - "summary": "Create asset (duplicate assetnumber 409, assettypeid validated); honors X-Import-Mode timestamps", - "description": "Create asset (duplicate assetnumber 409, assettypeid validated); honors X-Import-Mode timestamps\n\n**Auth:** permission:assets.create\n\n**Params:** body: assetnumber (req), assettypeid (req), name, serialnumber, statusid (default 1), locationid, businessunitid, mapx, mapy, notes\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/assets -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"assetnumber\":\"0205\",\"assettypeid\":1,\"name\":\"Grinder 5\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: assetnumber (req), assettypeid (req), name, serialnumber, statusid (default 1), locationid, businessunitid, mapx, mapy, notes" - } - } - } - } - } - }, - "/api/assets/{asset_id}": { - "get": { - "tags": [ - "core-catalog" - ], - "summary": "Get one asset with full details", - "description": "Get one asset with full details\n\n**Auth:** jwt-optional\n\n**Params:** include_type_data (default true)\n\n**Example:**\n```\ncurl http://localhost:5001/api/assets/42\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "asset_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - }, - "put": { - "tags": [ - "core-catalog" - ], - "summary": "Update asset (allowed fields incl. isactive); honors X-Import-Mode", - "description": "Update asset (allowed fields incl. isactive); honors X-Import-Mode\n\n**Auth:** permission:assets.edit\n\n**Params:** body: assetnumber, name, serialnumber, assettypeid, statusid, locationid, businessunitid, mapx, mapy, notes, isactive\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/assets/42 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"locationid\":3}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "asset_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: assetnumber, name, serialnumber, assettypeid, statusid, locationid, businessunitid, mapx, mapy, notes, isactive" - } - } - } - } - }, - "delete": { - "tags": [ - "core-catalog" - ], - "summary": "Soft-delete asset (isactive=false)", - "description": "Soft-delete asset (isactive=false)\n\n**Auth:** permission:assets.delete\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/assets/42 -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "asset_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/assets/lookup/{assetnumber}": { - "get": { - "tags": [ - "core-catalog" - ], - "summary": "Look up an active asset by asset number (returns full type data)", - "description": "Look up an active asset by asset number (returns full type data)\n\n**Auth:** jwt-optional\n\n**Params:** path: assetnumber (string)\n\n**Example:**\n```\ncurl http://localhost:5001/api/assets/lookup/0205\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "assetnumber", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/assets/{asset_id}/relationships": { - "get": { - "tags": [ - "core-catalog" - ], - "summary": "Get outgoing + incoming active relationships for an asset with partner asset dicts", - "description": "Get outgoing + incoming active relationships for an asset with partner asset dicts\n\n**Auth:** jwt-optional\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl http://localhost:5001/api/assets/42/relationships\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "asset_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/assets/relationships": { - "post": { - "tags": [ - "core-catalog" - ], - "summary": "Create relationship, then fan out across symmetric propagation rails (Dualpath); response carries propagated...", - "description": "Create relationship, then fan out across symmetric propagation rails (Dualpath); response carries propagated + propagatedcount\n\n**Auth:** permission:assets.create\n\n**Params:** body: sourceassetid (req), targetassetid (req), relationshiptypeid (req), notes; X-Import-Mode honored\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/assets/relationships -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"sourceassetid\":10,\"targetassetid\":42,\"relationshiptypeid\":1}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: sourceassetid (req), targetassetid (req), relationshiptypeid (req), notes; X-Import-Mode honored" - } - } - } - } - } - }, - "/api/assets/relationships/{rel_id}": { - "delete": { - "tags": [ - "core-catalog" - ], - "summary": "Soft-delete one relationship row (no cascade to propagated partner rows)", - "description": "Soft-delete one relationship row (no cascade to propagated partner rows)\n\n**Auth:** permission:assets.delete\n\n**Params:** path: rel_id\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/assets/relationships/7 -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "rel_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/assets/map": { - "get": { - "tags": [ - "core-catalog" - ], - "summary": "Unified floor-map payload: all mapped assets (with type data, primary IP, dualpath collapse) plus filter option lists", - "description": "Unified floor-map payload: all mapped assets (with type data, primary IP, dualpath collapse) plus filter option lists\n\n**Auth:** jwt-optional\n\n**Params:** assettype (name), subtype (id, per-type), businessunitid, statusid, locationid, search\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/assets/map?assettype=machine&statusid=1'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/assets/{asset_id}/communications": { - "get": { - "tags": [ - "core-catalog" - ], - "summary": "List active communications (IPs etc.) for an asset with comtype_name", - "description": "List active communications (IPs etc.) for an asset with comtype_name\n\n**Auth:** jwt-optional\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl http://localhost:5001/api/assets/42/communications\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "asset_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/locations/types": { - "get": { - "tags": [ - "core-catalog" - ], - "summary": "List location types", - "description": "List location types\n\n**Auth:** jwt-optional\n\n**Params:** active=false includes inactive\n\n**Example:**\n```\ncurl http://localhost:5001/api/locations/types\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - }, - "post": { - "tags": [ - "core-catalog" - ], - "summary": "Create location type; reactivates a soft-deleted same-name type instead of 409", - "description": "Create location type; reactivates a soft-deleted same-name type instead of 409\n\n**Auth:** admin\n\n**Params:** body: locationtype (req), description, color\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/locations/types -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"locationtype\":\"Operation\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: locationtype (req), description, color" - } - } - } - } - } - }, - "/api/locations/types/{type_id}": { - "put": { - "tags": [ - "core-catalog" - ], - "summary": "Update location type (rename conflict-checked)", - "description": "Update location type (rename conflict-checked)\n\n**Auth:** admin\n\n**Params:** body: locationtype, description, color, isactive\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/locations/types/2 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"color\":\"#0a0\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "type_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: locationtype, description, color, isactive" - } - } - } - } - }, - "delete": { - "tags": [ - "core-catalog" - ], - "summary": "Hard-delete location type; 409 while locations use it", - "description": "Hard-delete location type; 409 while locations use it\n\n**Auth:** admin\n\n**Params:** path: type_id\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/locations/types/2 -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "type_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/locations": { - "get": { - "tags": [ - "core-catalog" - ], - "summary": "List locations (paginated); exact locationname lookup for idempotent import", - "description": "List locations (paginated); exact locationname lookup for idempotent import\n\n**Auth:** jwt-optional\n\n**Params:** page, per_page, active, locationname (exact), search (name/building ilike)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/locations?locationname=Building%201'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - }, - "post": { - "tags": [ - "core-catalog" - ], - "summary": "Create location (409 on duplicate name); honors X-Import-Mode", - "description": "Create location (409 on duplicate name); honors X-Import-Mode\n\n**Auth:** admin\n\n**Params:** body: locationname (req), building, floor, room, description, locationtypeid, parentlocationid, mapimage, mapwidth, mapheight\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/locations -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"locationname\":\"Cell 12\",\"building\":\"B1\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: locationname (req), building, floor, room, description, locationtypeid, parentlocationid, mapimage, mapwidth, mapheight" - } - } - } - } - } - }, - "/api/locations/{location_id}": { - "get": { - "tags": [ - "core-catalog" - ], - "summary": "Get one location", - "description": "Get one location\n\n**Auth:** jwt-optional\n\n**Params:** path: location_id\n\n**Example:**\n```\ncurl http://localhost:5001/api/locations/3\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "location_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - }, - "put": { - "tags": [ - "core-catalog" - ], - "summary": "Update location (rename conflict-checked); honors X-Import-Mode", - "description": "Update location (rename conflict-checked); honors X-Import-Mode\n\n**Auth:** admin\n\n**Params:** body: locationname, building, floor, room, description, locationtypeid, parentlocationid, mapimage, mapwidth, mapheight, isactive\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/locations/3 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"room\":\"104\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "location_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: locationname, building, floor, room, description, locationtypeid, parentlocationid, mapimage, mapwidth, mapheight, isactive" - } - } - } - } - }, - "delete": { - "tags": [ - "core-catalog" - ], - "summary": "Soft-delete location", - "description": "Soft-delete location\n\n**Auth:** admin\n\n**Params:** path: location_id\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/locations/3 -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "location_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/vendors": { - "get": { - "tags": [ - "core-catalog" - ], - "summary": "List vendors (paginated); exact vendor lookup for idempotent import", - "description": "List vendors (paginated); exact vendor lookup for idempotent import\n\n**Auth:** jwt-optional\n\n**Params:** page, per_page, active, vendor (exact), search (ilike)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/vendors?search=fanuc'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - }, - "post": { - "tags": [ - "core-catalog" - ], - "summary": "Create vendor (409 on duplicate); honors X-Import-Mode", - "description": "Create vendor (409 on duplicate); honors X-Import-Mode\n\n**Auth:** admin\n\n**Params:** body: vendor (req), description, website, supportphone, supportemail, notes\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/vendors -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"vendor\":\"Fanuc\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: vendor (req), description, website, supportphone, supportemail, notes" - } - } - } - } - } - }, - "/api/vendors/{vendor_id}": { - "get": { - "tags": [ - "core-catalog" - ], - "summary": "Get one vendor", - "description": "Get one vendor\n\n**Auth:** jwt-optional\n\n**Params:** path: vendor_id\n\n**Example:**\n```\ncurl http://localhost:5001/api/vendors/5\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "vendor_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - }, - "put": { - "tags": [ - "core-catalog" - ], - "summary": "Update vendor (rename conflict-checked); honors X-Import-Mode", - "description": "Update vendor (rename conflict-checked); honors X-Import-Mode\n\n**Auth:** admin\n\n**Params:** body: vendor, description, website, supportphone, supportemail, notes, isactive\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/vendors/5 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"website\":\"https://fanuc.com\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "vendor_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: vendor, description, website, supportphone, supportemail, notes, isactive" - } - } - } - } - }, - "delete": { - "tags": [ - "core-catalog" - ], - "summary": "Soft-delete vendor", - "description": "Soft-delete vendor\n\n**Auth:** admin\n\n**Params:** path: vendor_id\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/vendors/5 -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "vendor_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/models": { - "get": { - "tags": [ - "core-catalog" - ], - "summary": "List vendor catalog models (paginated) with flattened vendor/modeltype names; exact modelnumber+vendor lookup for import", - "description": "List vendor catalog models (paginated) with flattened vendor/modeltype names; exact modelnumber+vendor lookup for import\n\n**Auth:** jwt-optional\n\n**Params:** page, per_page, active, vendor (id), modeltype (id), modelnumber (exact), search (ilike)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/models?vendor=5&search=30i'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - }, - "post": { - "tags": [ - "core-catalog" - ], - "summary": "Create model (409 on duplicate modelnumber+vendorid); honors X-Import-Mode", - "description": "Create model (409 on duplicate modelnumber+vendorid); honors X-Import-Mode\n\n**Auth:** admin\n\n**Params:** body: modelnumber (req), vendorid, modeltypeid, description, imageurl, documentationurl, notes\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/models -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"modelnumber\":\"R-30iB\",\"vendorid\":5}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: modelnumber (req), vendorid, modeltypeid, description, imageurl, documentationurl, notes" - } - } - } - } - } - }, - "/api/models/{model_id}": { - "get": { - "tags": [ - "core-catalog" - ], - "summary": "Get one model with nested vendor + modeltype dicts", - "description": "Get one model with nested vendor + modeltype dicts\n\n**Auth:** jwt-optional\n\n**Params:** path: model_id\n\n**Example:**\n```\ncurl http://localhost:5001/api/models/12\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "model_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - }, - "put": { - "tags": [ - "core-catalog" - ], - "summary": "Update model; honors X-Import-Mode", - "description": "Update model; honors X-Import-Mode\n\n**Auth:** admin\n\n**Params:** body: modelnumber, vendorid, modeltypeid, description, imageurl, documentationurl, notes, isactive\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/models/12 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"notes\":\"EOL 2027\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "model_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: modelnumber, vendorid, modeltypeid, description, imageurl, documentationurl, notes, isactive" - } - } - } - } - }, - "delete": { - "tags": [ - "core-catalog" - ], - "summary": "Soft-delete model", - "description": "Soft-delete model\n\n**Auth:** admin\n\n**Params:** path: model_id\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/models/12 -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "model_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/models/{model_id}/image": { - "post": { - "tags": [ - "core-catalog" - ], - "summary": "Upload/replace model photo (saved as instance/modelimages/model-, one per model); sets imageurl", - "description": "Upload/replace model photo (saved as instance/modelimages/model-, one per model); sets imageurl\n\n**Auth:** admin\n\n**Params:** multipart/form-data: file= (.png/.jpg/.jpeg/.gif/.webp/.svg)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/models/12/image -H \"Authorization: Bearer $TOK\" -F file=@robot.jpg\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "model_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "multipart/form-data: file= (.png/.jpg/.jpeg/.gif/.webp/.svg)" - } - } - } - } - }, - "delete": { - "tags": [ - "core-catalog" - ], - "summary": "Clear imageurl and delete the uploaded file only if it lives under /api/models/image/ (external URLs untouched)", - "description": "Clear imageurl and delete the uploaded file only if it lives under /api/models/image/ (external URLs untouched)\n\n**Auth:** admin\n\n**Params:** path: model_id\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/models/12/image -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "model_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/models/image/{filename}": { - "get": { - "tags": [ - "core-catalog" - ], - "summary": "Serve an uploaded model image (deliberately public - asset detail pages read it without auth)", - "description": "Serve an uploaded model image (deliberately public - asset detail pages read it without auth)\n\n**Auth:** none\n\n**Params:** path: filename\n\n**Example:**\n```\ncurl http://localhost:5001/api/models/image/model-12.jpg\n```", + "summary": "Serve an uploaded blueprint image (public - kiosks read it).", + "description": "Serve an uploaded blueprint image (public - kiosks read it).\n\n**Auth:** none (reviewed: named in the public-read or mutation sweep)\n\n**Params:** path: filename (path)\n\n**Example:**\n```\ncurl http://localhost:5001/api/settings/map-blueprint/example.png\n```", "security": [], "responses": { "200": { @@ -6883,21 +8693,21 @@ "in": "path", "required": true, "schema": { - "type": "string" + "type": "string", + "description": "may contain slashes" } } ] } }, - "/api/modeltypes": { - "get": { + "/api/settings/seed": { + "post": { "tags": [ - "core-catalog" + "core-settings" ], - "summary": "List model types (types the vendor MODELS catalog, not machines); exact modeltype lookup for import", - "description": "List model types (types the vendor MODELS catalog, not machines); exact modeltype lookup for import\n\n**Auth:** jwt-optional\n\n**Params:** page, per_page, active, category, modeltype (exact), search (ilike)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/modeltypes?category=Equipment'\n```", + "summary": "Seed default settings if they don't exist.", + "description": "Seed default settings if they don't exist.\n\n**Auth:** permission:settings.edit\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/settings/seed -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ - {}, { "bearerAuth": [] } @@ -6918,15 +8728,339 @@ "$ref": "#/components/schemas/ErrorEnvelope" } } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/settings/test-email": { + "post": { + "tags": [ + "core-settings" + ], + "summary": "Send a test email to verify SMTP configuration.", + "description": "Send a test email to verify SMTP configuration.\n\n**Auth:** permission:settings.edit\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/settings/test-email -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/setup/complete": { + "post": { + "tags": [ + "core-setup" + ], + "summary": "Flag the first-run setup as finished.", + "description": "Flag the first-run setup as finished.\n\n**Auth:** jwt + role:admin\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/setup/complete -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/setup/create-admin": { + "post": { + "tags": [ + "core-setup" + ], + "summary": "Create the first admin. Only works while there are zero users, setup has not been marked complete, and the caller...", + "description": "Create the first admin. Only works while there are zero users, setup has not been marked complete, and the caller proves operator control (setup token or a loopback request).\n\n**Auth:** none (first run only: refused once an admin exists)\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/setup/create-admin -H 'Content-Type: application/json' -d '{}'\n```", + "security": [], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/setup/needs-admin": { + "get": { + "tags": [ + "core-setup" + ], + "summary": "True when no users exist yet - the app's very first run. Public so the login screen can offer to create the first admin.", + "description": "True when no users exist yet - the app's very first run. Public so the login screen can offer to create the first admin.\n\n**Auth:** none (reviewed: named in the public-read or mutation sweep)\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/setup/needs-admin\n```", + "security": [], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + }, + "/api/setup/seed-reference": { + "post": { + "tags": [ + "core-setup" + ], + "summary": "Seed core reference data + permissions + default settings. Idempotent - reuses the same routines as the `flask seed...", + "description": "Seed core reference data + permissions + default settings. Idempotent - reuses the same routines as the `flask seed ...` CLI commands.\n\n**Auth:** jwt + role:admin\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/setup/seed-reference -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/setup/seed-starter": { + "post": { + "tags": [ + "core-setup" + ], + "summary": "Add common vendors that are not already present. Idempotent.", + "description": "Add common vendors that are not already present. Idempotent.\n\n**Auth:** jwt + role:admin\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/setup/seed-starter -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/shopdbbackups": { + "get": { + "tags": [ + "core-shopdbbackups" + ], + "summary": "List every retained backup set, newest first, with a compatibility flag.", + "description": "List every retained backup set, newest first, with a compatibility flag.\n\n**Auth:** jwt + role:admin\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/shopdbbackups -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." } } }, "post": { "tags": [ - "core-catalog" + "core-shopdbbackups" ], - "summary": "Create model type (409 on duplicate); category defaults to Equipment; honors X-Import-Mode", - "description": "Create model type (409 on duplicate); category defaults to Equipment; honors X-Import-Mode\n\n**Auth:** admin\n\n**Params:** body: modeltype (req), category, description, icon\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/modeltypes -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"modeltype\":\"Controller\"}'\n```", + "summary": "Kick off an on-demand backup run.", + "description": "Kick off an on-demand backup run.\n\n**Auth:** jwt + role:admin\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/shopdbbackups -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -6963,64 +9097,20 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "body: modeltype (req), category, description, icon" + "description": "body: JSON (fields not yet documented)" } } } } } }, - "/api/modeltypes/{type_id}": { + "/api/shopdbbackups/{snapshotid}": { "get": { "tags": [ - "core-catalog" + "core-shopdbbackups" ], - "summary": "Get one model type", - "description": "Get one model type\n\n**Auth:** jwt-optional\n\n**Params:** path: type_id\n\n**Example:**\n```\ncurl http://localhost:5001/api/modeltypes/2\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "type_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - }, - "put": { - "tags": [ - "core-catalog" - ], - "summary": "Update model type (rename conflict-checked); honors X-Import-Mode", - "description": "Update model type (rename conflict-checked); honors X-Import-Mode\n\n**Auth:** admin\n\n**Params:** body: modeltype, category, description, icon, isactive\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/modeltypes/2 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"icon\":\"mdi-chip\"}'\n```", + "summary": "Return one set's full manifest plus a resolved compatibility verdict.", + "description": "Return one set's full manifest plus a resolved compatibility verdict.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: snapshotid (string)\n\n**Example:**\n```\ncurl http://localhost:5001/api/shopdbbackups/x -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -7055,68 +9145,7 @@ }, "parameters": [ { - "name": "type_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: modeltype, category, description, icon, isactive" - } - } - } - } - }, - "delete": { - "tags": [ - "core-catalog" - ], - "summary": "Soft-delete model type; 409 while models use it", - "description": "Soft-delete model type; 409 while models use it\n\n**Auth:** admin\n\n**Params:** path: type_id\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/modeltypes/2 -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "type_id", + "name": "snapshotid", "in": "path", "required": true, "schema": { @@ -7126,138 +9155,13 @@ ] } }, - "/api/businessunits": { - "get": { - "tags": [ - "core-catalog" - ], - "summary": "List business units (paginated); exact businessunit lookup for import", - "description": "List business units (paginated); exact businessunit lookup for import\n\n**Auth:** jwt-optional\n\n**Params:** page, per_page, active, businessunit (exact), search (name/code ilike)\n\n**Example:**\n```\ncurl http://localhost:5001/api/businessunits\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - }, + "/api/shopdbbackups/{snapshotid}/restore": { "post": { "tags": [ - "core-catalog" + "core-shopdbbackups" ], - "summary": "Create business unit (409 on duplicate); honors X-Import-Mode", - "description": "Create business unit (409 on duplicate); honors X-Import-Mode\n\n**Auth:** admin\n\n**Params:** body: businessunit (req), code, description, parentid\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/businessunits -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"businessunit\":\"Blades\",\"code\":\"BLD\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: businessunit (req), code, description, parentid" - } - } - } - } - } - }, - "/api/businessunits/{bu_id}": { - "get": { - "tags": [ - "core-catalog" - ], - "summary": "Get one business unit with parent + children", - "description": "Get one business unit with parent + children\n\n**Auth:** jwt-optional\n\n**Params:** path: bu_id\n\n**Example:**\n```\ncurl http://localhost:5001/api/businessunits/1\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "bu_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - }, - "put": { - "tags": [ - "core-catalog" - ], - "summary": "Update business unit (rename conflict-checked); honors X-Import-Mode", - "description": "Update business unit (rename conflict-checked); honors X-Import-Mode\n\n**Auth:** admin\n\n**Params:** body: businessunit, code, description, parentid, isactive\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/businessunits/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"code\":\"BL\"}'\n```", + "summary": "Queue a restore of a set, gated on schema compatibility.", + "description": "Queue a restore of a set, gated on schema compatibility.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: snapshotid (string); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/shopdbbackups/x/restore -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -7292,7 +9196,7 @@ }, "parameters": [ { - "name": "bu_id", + "name": "snapshotid", "in": "path", "required": true, "schema": { @@ -7307,100 +9211,20 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "body: businessunit, code, description, parentid, isactive" + "description": "path: snapshotid (string); body: JSON (fields not yet documented)" } } } } - }, - "delete": { - "tags": [ - "core-catalog" - ], - "summary": "Soft-delete business unit", - "description": "Soft-delete business unit\n\n**Auth:** admin\n\n**Params:** path: bu_id\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/businessunits/1 -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "bu_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] } }, - "/api/operatingsystems": { - "get": { - "tags": [ - "core-catalog" - ], - "summary": "List operating systems (paginated); exact osname/osversion lookup for import", - "description": "List operating systems (paginated); exact osname/osversion lookup for import\n\n**Auth:** jwt-optional\n\n**Params:** page, per_page, active, osname (exact), osversion (exact), search (osname ilike)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/operatingsystems?osname=Windows%2011'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - }, + "/api/shopdbbackups/finalize-restore": { "post": { "tags": [ - "core-catalog" + "core-shopdbbackups" ], - "summary": "Create OS (409 on duplicate osname+osversion); honors X-Import-Mode", - "description": "Create OS (409 on duplicate osname+osversion); honors X-Import-Mode\n\n**Auth:** admin\n\n**Params:** body: osname (req), osversion, architecture, endoflife\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/operatingsystems -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"osname\":\"Windows 11\",\"osversion\":\"24H2\"}'\n```", + "summary": "Run the post-restore schema upgrade in the app context.", + "description": "Run the post-restore schema upgrade in the app context.\n\n**Auth:** jwt + role:admin\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/shopdbbackups/finalize-restore -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -7437,22 +9261,21 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "body: osname (req), osversion, architecture, endoflife" + "description": "body: JSON (fields not yet documented)" } } } } } }, - "/api/operatingsystems/{os_id}": { + "/api/shopdbbackups/history": { "get": { "tags": [ - "core-catalog" + "core-shopdbbackups" ], - "summary": "Get one operating system", - "description": "Get one operating system\n\n**Auth:** jwt-optional\n\n**Params:** path: os_id\n\n**Example:**\n```\ncurl http://localhost:5001/api/operatingsystems/4\n```", + "summary": "Return the recorded history of backup and restore runs, newest first.", + "description": "Return the recorded history of backup and restore runs, newest first.\n\n**Auth:** jwt + role:admin\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/shopdbbackups/history -H \"Authorization: Bearer $TOK\"\n```", "security": [ - {}, { "bearerAuth": [] } @@ -7474,27 +9297,58 @@ } } }, - "404": { - "description": "No such record." + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." } - }, - "parameters": [ + } + } + }, + "/api/shopdbbackups/settings": { + "get": { + "tags": [ + "core-shopdbbackups" + ], + "summary": "Return destination + retention config for the settings pane.", + "description": "Return destination + retention config for the settings pane.\n\n**Auth:** jwt + role:admin\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/shopdbbackups/settings -H \"Authorization: Bearer $TOK\"\n```", + "security": [ { - "name": "os_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } + "bearerAuth": [] } - ] + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } }, "put": { "tags": [ - "core-catalog" + "core-shopdbbackups" ], - "summary": "Update OS; honors X-Import-Mode", - "description": "Update OS; honors X-Import-Mode\n\n**Auth:** admin\n\n**Params:** body: osname, osversion, architecture, endoflife, isactive\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/operatingsystems/4 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"endoflife\":\"2031-10-14\"}'\n```", + "summary": "Persist the backup destination kind and its credential/config fields.", + "description": "Persist the backup destination kind and its credential/config fields.\n\n**Auth:** jwt + role:admin\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/shopdbbackups/settings -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -7522,21 +9376,8 @@ }, "403": { "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." } }, - "parameters": [ - { - "name": "os_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], "requestBody": { "required": true, "content": { @@ -7544,100 +9385,20 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "body: osname, osversion, architecture, endoflife, isactive" + "description": "body: JSON (fields not yet documented)" } } } } - }, - "delete": { - "tags": [ - "core-catalog" - ], - "summary": "Soft-delete OS", - "description": "Soft-delete OS\n\n**Auth:** admin\n\n**Params:** path: os_id\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/operatingsystems/4 -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "os_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] } }, - "/api/customfields": { + "/api/shopdbbackups/status": { "get": { "tags": [ - "core-catalog" + "core-shopdbbackups" ], - "summary": "List custom-field definitions, ordered by sortorder", - "description": "List custom-field definitions, ordered by sortorder\n\n**Auth:** jwt-optional\n\n**Params:** assettypeid (int), active=false includes inactive\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/customfields?assettypeid=1'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - }, - "post": { - "tags": [ - "core-catalog" - ], - "summary": "Create field definition; fieldkey auto-slugged from label; reactivates a soft-deleted same-key field instead of 409", - "description": "Create field definition; fieldkey auto-slugged from label; reactivates a soft-deleted same-key field instead of 409\n\n**Auth:** admin\n\n**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\n\n**Example:**\n```\ncurl -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\"]}'\n```", + "summary": "Return the live status of the most recent backup and restore runs.", + "description": "Return the live status of the most recent backup and restore runs.\n\n**Auth:** jwt + role:admin\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/shopdbbackups/status -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -7666,249 +9427,17 @@ "403": { "description": "Authenticated, but not permitted." } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: assettypeid (req), label (req), datatype (one of CUSTOM_FIELD_DATATYPES, default text), fieldkey, options (list or newline/comma text), showondetail, showonform, searchable, sortorder" - } - } - } - } - } - }, - "/api/customfields/{fieldid}": { - "put": { - "tags": [ - "core-catalog" - ], - "summary": "Update field definition (label/datatype/options/flags/sortorder)", - "description": "Update field definition (label/datatype/options/flags/sortorder)\n\n**Auth:** admin\n\n**Params:** body: label, datatype, options, showondetail, showonform, isactive, searchable, sortorder\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/customfields/7 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"searchable\":true}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "fieldid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: label, datatype, options, showondetail, showonform, isactive, searchable, sortorder" - } - } - } - } - }, - "delete": { - "tags": [ - "core-catalog" - ], - "summary": "Hard-delete field definition AND all stored values for it", - "description": "Hard-delete field definition AND all stored values for it\n\n**Auth:** admin\n\n**Params:** path: fieldid\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/customfields/7 -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "fieldid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/customfields/asset/{assetid}": { - "get": { - "tags": [ - "core-catalog" - ], - "summary": "Active field defs for the asset's type merged with the asset's stored values", - "description": "Active field defs for the asset's type merged with the asset's stored values\n\n**Auth:** jwt-optional\n\n**Params:** path: assetid\n\n**Example:**\n```\ncurl http://localhost:5001/api/customfields/asset/42\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "assetid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - }, - "put": { - "tags": [ - "core-catalog" - ], - "summary": "Upsert per-asset values; empty string clears a value; only fields of the asset's type accepted", - "description": "Upsert per-asset values; empty string clears a value; only fields of the asset's type accepted\n\n**Auth:** admin\n\n**Params:** body: {values: {fieldid: value, ...}}\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/customfields/asset/42 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"values\":{\"7\":\"Oil\"}}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "assetid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: {values: {fieldid: value, ...}}" - } - } - } } } }, "/api/supportteams": { "get": { "tags": [ - "core-catalog" + "core-supportteams" ], - "summary": "List support teams with contacts; exact teamname lookup for import (not paginated)", - "description": "List support teams with contacts; exact teamname lookup for import (not paginated)\n\n**Auth:** jwt-optional\n\n**Params:** active, teamname (exact)\n\n**Example:**\n```\ncurl http://localhost:5001/api/supportteams\n```", + "summary": "List support teams (with contacts). ?active and ?teamname filters.", + "description": "List support teams (with contacts). ?active and ?teamname filters.\n\n**Auth:** permission:assets.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/supportteams -H \"Authorization: Bearer $TOK\"\n```", "security": [ - {}, { "bearerAuth": [] } @@ -7929,15 +9458,21 @@ "$ref": "#/components/schemas/ErrorEnvelope" } } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." } } }, "post": { "tags": [ - "core-catalog" + "core-supportteams" ], - "summary": "Create support team (409 on duplicate); audit-logged; honors X-Import-Mode", - "description": "Create support team (409 on duplicate); audit-logged; honors X-Import-Mode\n\n**Auth:** admin\n\n**Params:** body: teamname (req), teamurl, webhookurl, isactive\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/supportteams -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"teamname\":\"CNC Support\"}'\n```", + "summary": "Create a new support team.", + "description": "Create a new support team.\n\n**Auth:** jwt + role:admin\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/supportteams -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -7974,7 +9509,7 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "body: teamname (req), teamurl, webhookurl, isactive" + "description": "body: JSON (fields not yet documented)" } } } @@ -7982,14 +9517,13 @@ } }, "/api/supportteams/{team_id}": { - "get": { + "delete": { "tags": [ - "core-catalog" + "core-supportteams" ], - "summary": "Get one support team with contacts", - "description": "Get one support team with contacts\n\n**Auth:** jwt-optional\n\n**Params:** path: team_id\n\n**Example:**\n```\ncurl http://localhost:5001/api/supportteams/2\n```", + "summary": "Delete a support team; 409 while any application references it.", + "description": "Delete a support team; 409 while any application references it.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: team_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/supportteams/1 -H \"Authorization: Bearer $TOK\"\n```", "security": [ - {}, { "bearerAuth": [] } @@ -8011,6 +9545,12 @@ } } }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, "404": { "description": "No such record." } @@ -8021,17 +9561,66 @@ "in": "path", "required": true, "schema": { - "type": "string" + "type": "integer" + } + } + ] + }, + "get": { + "tags": [ + "core-supportteams" + ], + "summary": "Get a single support team with its contacts.", + "description": "Get a single support team with its contacts.\n\n**Auth:** permission:assets.view\n\n**Params:** path: team_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/supportteams/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "team_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" } } ] }, "put": { "tags": [ - "core-catalog" + "core-supportteams" ], - "summary": "Update support team (rename conflict-checked); audit-logged; honors X-Import-Mode", - "description": "Update support team (rename conflict-checked); audit-logged; honors X-Import-Mode\n\n**Auth:** admin\n\n**Params:** body: teamname, teamurl, webhookurl, isactive\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/supportteams/2 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"webhookurl\":\"https://hooks/x\"}'\n```", + "summary": "Update a support team.", + "description": "Update a support team.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: team_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/supportteams/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -8070,7 +9659,7 @@ "in": "path", "required": true, "schema": { - "type": "string" + "type": "integer" } } ], @@ -8081,69 +9670,20 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "body: teamname, teamurl, webhookurl, isactive" + "description": "path: team_id (int); body: JSON (fields not yet documented)" } } } } - }, - "delete": { - "tags": [ - "core-catalog" - ], - "summary": "Hard-delete team (cascade removes contacts); 409 while applications reference it; audit-logged", - "description": "Hard-delete team (cascade removes contacts); 409 while applications reference it; audit-logged\n\n**Auth:** admin\n\n**Params:** path: team_id\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/supportteams/2 -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "team_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] } }, "/api/supportteams/{team_id}/contacts": { "post": { "tags": [ - "core-catalog" + "core-supportteams" ], - "summary": "Add contact to a team; audit-logged; honors X-Import-Mode", - "description": "Add contact to a team; audit-logged; honors X-Import-Mode\n\n**Auth:** admin\n\n**Params:** body: name (req), sso, sortorder, isactive\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/supportteams/2/contacts -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"name\":\"Jane Doe\",\"sso\":\"212345678\"}'\n```", + "summary": "Add a contact to a support team.", + "description": "Add a contact to a support team.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: team_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/supportteams/1/contacts -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -8182,7 +9722,7 @@ "in": "path", "required": true, "schema": { - "type": "string" + "type": "integer" } } ], @@ -8193,7 +9733,7 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "body: name (req), sso, sortorder, isactive" + "description": "path: team_id (int); body: JSON (fields not yet documented)" } } } @@ -8201,12 +9741,12 @@ } }, "/api/supportteams/{team_id}/contacts/{contact_id}": { - "put": { + "delete": { "tags": [ - "core-catalog" + "core-supportteams" ], - "summary": "Update a team contact; audit-logged; honors X-Import-Mode", - "description": "Update a team contact; audit-logged; honors X-Import-Mode\n\n**Auth:** admin\n\n**Params:** body: name, sso, sortorder, isactive\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/supportteams/2/contacts/9 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"sortorder\":1}'\n```", + "summary": "Delete a support-team contact.", + "description": "Delete a support-team contact.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: team_id (int), contact_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/supportteams/1/contacts/1 -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -8245,7 +9785,7 @@ "in": "path", "required": true, "schema": { - "type": "string" + "type": "integer" } }, { @@ -8253,29 +9793,17 @@ "in": "path", "required": true, "schema": { - "type": "string" + "type": "integer" } } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: name, sso, sortorder, isactive" - } - } - } - } + ] }, - "delete": { + "put": { "tags": [ - "core-catalog" + "core-supportteams" ], - "summary": "Hard-delete a team contact; audit-logged", - "description": "Hard-delete a team contact; audit-logged\n\n**Auth:** admin\n\n**Params:** path: team_id, contact_id\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/supportteams/2/contacts/9 -H \"Authorization: Bearer $TOK\"\n```", + "summary": "Update a support-team contact.", + "description": "Update a support-team contact.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: team_id (int), contact_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/supportteams/1/contacts/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -8314,7 +9842,7 @@ "in": "path", "required": true, "schema": { - "type": "string" + "type": "integer" } }, { @@ -8322,21 +9850,32 @@ "in": "path", "required": true, "schema": { - "type": "string" + "type": "integer" } } - ] + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: team_id (int), contact_id (int); body: JSON (fields not yet documented)" + } + } + } + } } }, - "/api/applications": { + "/api/users": { "get": { "tags": [ - "core-catalog" + "core-users" ], - "summary": "List applications (paginated) with installedcount; hidden apps excluded unless showhidden=true; exact appname lookup...", - "description": "List applications (paginated) with installedcount; hidden apps excluded unless showhidden=true; exact appname lookup for import\n\n**Auth:** jwt-optional\n\n**Params:** page, per_page, active, showhidden, installable (true/false), appname (exact), search (name/description ilike)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/applications?installable=true'\n```", + "summary": "List all users.", + "description": "List all users.\n\n**Auth:** jwt + role:admin\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/users -H \"Authorization: Bearer $TOK\"\n```", "security": [ - {}, { "bearerAuth": [] } @@ -8357,15 +9896,21 @@ "$ref": "#/components/schemas/ErrorEnvelope" } } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." } } }, "post": { "tags": [ - "core-catalog" + "core-users" ], - "summary": "Create application (409 on duplicate name); audit-logged; honors X-Import-Mode", - "description": "Create application (409 on duplicate name); audit-logged; honors X-Import-Mode\n\n**Auth:** permission:applications.create\n\n**Params:** body: appname (req), appdescription, supportteamid, isinstallable, applicationnotes, installpath, applicationlink, documentationpath, ishidden, isprinter, islicenced, isrequired, image\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/applications -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"appname\":\"PC-DMIS\",\"isinstallable\":true}'\n```", + "summary": "Create a new user.", + "description": "Create a new user.\n\n**Auth:** jwt + role:admin\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/users -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -8402,125 +9947,20 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "body: appname (req), appdescription, supportteamid, isinstallable, applicationnotes, installpath, applicationlink, documentationpath, ishidden, isprinter, islicenced, isrequired, image" + "description": "body: JSON (fields not yet documented)" } } } } } }, - "/api/applications/{app_id}": { - "get": { - "tags": [ - "core-catalog" - ], - "summary": "Get one application with active versions, installedcount, and linked KB articles", - "description": "Get one application with active versions, installedcount, and linked KB articles\n\n**Auth:** jwt-optional\n\n**Params:** path: app_id\n\n**Example:**\n```\ncurl http://localhost:5001/api/applications/15\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "app_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - }, - "put": { - "tags": [ - "core-catalog" - ], - "summary": "Update application (rename conflict-checked); field-level change diff audit-logged; honors X-Import-Mode", - "description": "Update application (rename conflict-checked); field-level change diff audit-logged; honors X-Import-Mode\n\n**Auth:** permission:applications.edit\n\n**Params:** body: any of appname, appdescription, supportteamid, isinstallable, applicationnotes, installpath, applicationlink, documentationpath, ishidden, isprinter, islicenced, isrequired, image, isactive\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/applications/15 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"supportteamid\":2}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "app_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: any of appname, appdescription, supportteamid, isinstallable, applicationnotes, installpath, applicationlink, documentationpath, ishidden, isprinter, islicenced, isrequired, image, isactive" - } - } - } - } - }, + "/api/users/{userid}": { "delete": { "tags": [ - "core-catalog" + "core-users" ], - "summary": "Soft-delete application; audit-logged", - "description": "Soft-delete application; audit-logged\n\n**Auth:** permission:applications.delete\n\n**Params:** path: app_id\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/applications/15 -H \"Authorization: Bearer $TOK\"\n```", + "summary": "Delete a user.", + "description": "Delete a user.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: userid (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/users/1 -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -8555,220 +9995,21 @@ }, "parameters": [ { - "name": "app_id", + "name": "userid", "in": "path", "required": true, "schema": { - "type": "string" - } - } - ] - } - }, - "/api/applications/{app_id}/versions": { - "get": { - "tags": [ - "core-catalog" - ], - "summary": "List active versions of an application (desc)", - "description": "List active versions of an application (desc)\n\n**Auth:** jwt-optional\n\n**Params:** path: app_id\n\n**Example:**\n```\ncurl http://localhost:5001/api/applications/15/versions\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "app_id", - "in": "path", - "required": true, - "schema": { - "type": "string" + "type": "integer" } } ] }, - "post": { - "tags": [ - "core-catalog" - ], - "summary": "Create app version (409 on duplicate version per app); honors X-Import-Mode for legacy dates", - "description": "Create app version (409 on duplicate version per app); honors X-Import-Mode for legacy dates\n\n**Auth:** permission:applications.create\n\n**Params:** body: version (req), releasedate, notes\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/applications/15/versions -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"version\":\"2024.2\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "app_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: version (req), releasedate, notes" - } - } - } - } - } - }, - "/api/applications/{app_id}/installed": { "get": { "tags": [ - "core-catalog" + "core-users" ], - "summary": "List computers with this app installed (503 if computers plugin absent)", - "description": "List computers with this app installed (503 if computers plugin absent)\n\n**Auth:** jwt-optional\n\n**Params:** path: app_id\n\n**Example:**\n```\ncurl http://localhost:5001/api/applications/15/installed\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "app_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/applications/machines/{machine_id}": { - "get": { - "tags": [ - "core-catalog" - ], - "summary": "List apps installed on a computer (machine_id is a computerid; 503 without computers plugin)", - "description": "List apps installed on a computer (machine_id is a computerid; 503 without computers plugin)\n\n**Auth:** jwt-optional\n\n**Params:** path: machine_id\n\n**Example:**\n```\ncurl http://localhost:5001/api/applications/machines/8\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "machine_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - }, - "post": { - "tags": [ - "core-catalog" - ], - "summary": "Install an app on a computer; reactivates a prior soft-deleted install; 409 if already installed", - "description": "Install an app on a computer; reactivates a prior soft-deleted install; 409 if already installed\n\n**Auth:** permission:applications.create\n\n**Params:** body: appid (req), appversionid\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/applications/machines/8 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"appid\":15}'\n```", + "summary": "Get a single user.", + "description": "Get a single user.\n\n**Auth:** jwt\n\n**Params:** path: userid (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/users/1 -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -8803,590 +10044,7 @@ }, "parameters": [ { - "name": "machine_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: appid (req), appversionid" - } - } - } - } - } - }, - "/api/applications/machines/{machine_id}/{app_id}": { - "delete": { - "tags": [ - "core-catalog" - ], - "summary": "Uninstall (soft-delete install row) an app from a computer", - "description": "Uninstall (soft-delete install row) an app from a computer\n\n**Auth:** permission:applications.delete\n\n**Params:** path: machine_id, app_id\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/applications/machines/8/15 -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "machine_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "app_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - }, - "put": { - "tags": [ - "core-catalog" - ], - "summary": "Update an installed-app row (change appversionid)", - "description": "Update an installed-app row (change appversionid)\n\n**Auth:** permission:applications.edit\n\n**Params:** body: appversionid\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/applications/machines/8/15 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"appversionid\":3}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "machine_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "app_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: appversionid" - } - } - } - } - } - }, - "/api/applications/{app_id}/image": { - "post": { - "tags": [ - "core-catalog" - ], - "summary": "Upload or replace an application's image; sets application.image to the served URL", - "description": "Upload or replace an application's image; sets application.image to the served URL\n\n**Auth:** jwt + applications.edit\n\n**Params:** multipart/form-data: file= (png/jpg/gif/webp/svg/ico)\n\n**Example:**\n```\ncurl -X POST -H 'Authorization: Bearer ' -F file=@logo.png http://localhost:5001/api/applications/5/image\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "app_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "multipart/form-data: file= (png/jpg/gif/webp/svg/ico)" - } - } - } - } - }, - "delete": { - "tags": [ - "core-catalog" - ], - "summary": "Remove an application's image and clear the field", - "description": "Remove an application's image and clear the field\n\n**Auth:** jwt + applications.edit\n\n**Params:** none\n\n**Example:**\n```\ncurl -X DELETE -H 'Authorization: Bearer ' http://localhost:5001/api/applications/5/image\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "app_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/applications/image/{filename}": { - "get": { - "tags": [ - "core-catalog" - ], - "summary": "Serve an uploaded application image. Public: application tiles render before login", - "description": "Serve an uploaded application image. Public: application tiles render before login\n\n**Auth:** none\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/applications/image/application-5.png\n```", - "security": [], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "filename", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/applications/{app_id}/package": { - "post": { - "tags": [ - "core-catalog" - ], - "summary": "Upload or replace the installer; sets installpath to the download URL", - "description": "Upload or replace the installer; sets installpath to the download URL\n\n**Auth:** jwt + applications.edit\n\n**Params:** multipart/form-data: file= (exe/msi/msp/zip/7z/cab/iso/appx/msix/ps1/bat/txt/pdf), max 500MB\n\n**Example:**\n```\ncurl -X POST -H 'Authorization: Bearer ' -F file=@setup.msi http://localhost:5001/api/applications/5/package\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "app_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "multipart/form-data: file= (exe/msi/msp/zip/7z/cab/iso/appx/msix/ps1/bat/txt/pdf), max 500MB" - } - } - } - } - }, - "delete": { - "tags": [ - "core-catalog" - ], - "summary": "Remove an uploaded installer; clears installpath only when it pointed at the upload", - "description": "Remove an uploaded installer; clears installpath only when it pointed at the upload\n\n**Auth:** jwt + applications.edit\n\n**Params:** none\n\n**Example:**\n```\ncurl -X DELETE -H 'Authorization: Bearer ' http://localhost:5001/api/applications/5/package\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "app_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/applications/package/{filename}": { - "get": { - "tags": [ - "core-catalog" - ], - "summary": "Download an uploaded installer as an attachment. Authenticated: licensed vendor software", - "description": "Download an uploaded installer as an attachment. Authenticated: licensed vendor software\n\n**Auth:** jwt + applications.view\n\n**Params:** none\n\n**Example:**\n```\ncurl -OJ -H 'Authorization: Bearer ' http://localhost:5001/api/applications/package/application-5.msi\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "filename", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/computers/types": { - "get": { - "tags": [ - "plugin-computers" - ], - "summary": "List computer types, paginated, active-only by default", - "description": "List computer types, paginated, active-only by default\n\n**Auth:** jwt-optional\n\n**Params:** page, per_page, active (default true; 'false' includes inactive), search (ilike on computertype)\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/computers/types?search=kiosk&per_page=50'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - }, - "post": { - "tags": [ - "plugin-computers" - ], - "summary": "Create (or reactivate) a computer type", - "description": "Create (or reactivate) a computer type\n\n**Auth:** permission:computers.create (jwt_required)\n\n**Params:** body: computertype (required), description, icon, color; matching a deactivated type revives it instead of 409\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"computertype\":\"Shopfloor\",\"color\":\"#0066cc\"}' 'http://localhost:5001/api/computers/types'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: computertype (required), description, icon, color; matching a deactivated type revives it instead of 409" - } - } - } - } - } - }, - "/api/computers/types/{type_id}": { - "get": { - "tags": [ - "plugin-computers" - ], - "summary": "Get a single computer type by ID", - "description": "Get a single computer type by ID\n\n**Auth:** jwt-optional\n\n**Params:** type_id in path\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/computers/types/3'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "type_id", + "name": "userid", "in": "path", "required": true, "schema": { @@ -9397,10 +10055,10 @@ }, "put": { "tags": [ - "plugin-computers" + "core-users" ], - "summary": "Update a computer type", - "description": "Update a computer type\n\n**Auth:** permission:computers.edit (jwt_required)\n\n**Params:** body: computertype, description, icon, color, isactive; 409 on duplicate name\n\n**Example:**\n```\ncurl -X PUT -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"isactive\":false}' 'http://localhost:5001/api/computers/types/3'\n```", + "summary": "Update a user.", + "description": "Update a user.\n\n**Auth:** jwt\n\n**Params:** path: userid (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/users/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -9435,7 +10093,7 @@ }, "parameters": [ { - "name": "type_id", + "name": "userid", "in": "path", "required": true, "schema": { @@ -9450,18 +10108,20 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "body: computertype, description, icon, color, isactive; 409 on duplicate name" + "description": "path: userid (int); body: JSON (fields not yet documented)" } } } } - }, - "delete": { + } + }, + "/api/users/permissions": { + "get": { "tags": [ - "plugin-computers" + "core-users" ], - "summary": "Hard-delete a computer type when unused", - "description": "Hard-delete a computer type when unused\n\n**Auth:** permission:computers.delete (jwt_required)\n\n**Params:** type_id in path; 409 if any Computer still uses the type\n\n**Example:**\n```\ncurl -X DELETE -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/computers/types/3'\n```", + "summary": "List assignable permissions grouped by category.", + "description": "List assignable permissions grouped by category.\n\n**Auth:** jwt\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/users/permissions -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -9489,32 +10149,18 @@ }, "403": { "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." } - }, - "parameters": [ - { - "name": "type_id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ] + } } }, - "/api/computers/protocols": { + "/api/users/roles": { "get": { "tags": [ - "plugin-computers" + "core-users" ], - "summary": "List remote-access protocols (VNC/WinRM/RDP catalog), unpaginated", - "description": "List remote-access protocols (VNC/WinRM/RDP catalog), unpaginated\n\n**Auth:** jwt-optional\n\n**Params:** active (default true; 'false' includes disabled)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/computers/protocols?active=false'\n```", + "summary": "List all roles with their permissions.", + "description": "List all roles with their permissions.\n\n**Auth:** jwt\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/users/roles -H \"Authorization: Bearer $TOK\"\n```", "security": [ - {}, { "bearerAuth": [] } @@ -9535,15 +10181,21 @@ "$ref": "#/components/schemas/ErrorEnvelope" } } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." } } }, "post": { "tags": [ - "plugin-computers" + "core-users" ], - "summary": "Create an access protocol", - "description": "Create an access protocol\n\n**Auth:** permission:computers.edit (jwt_required)\n\n**Params:** body: name, scheme, linktemplate (all required), defaultport, isactive; 409 on duplicate name\n\n**Example:**\n```\ncurl -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'\n```", + "summary": "Create a new role.", + "description": "Create a new role.\n\n**Auth:** jwt + role:admin\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/users/roles -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -9580,20 +10232,20 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "body: name, scheme, linktemplate (all required), defaultport, isactive; 409 on duplicate name" + "description": "body: JSON (fields not yet documented)" } } } } } }, - "/api/computers/protocols/{protocol_id}": { - "put": { + "/api/users/roles/{roleid}": { + "delete": { "tags": [ - "plugin-computers" + "core-users" ], - "summary": "Update one remote-access protocol in the catalog (VNC, WinRM, RDP) that PCs report against", - "description": "Update one remote-access protocol in the catalog (VNC, WinRM, RDP) that PCs report against\n\n**Auth:** permission:computers.edit (jwt_required)\n\n**Params:** protocol_id in path; body: any of protocolname, port, isactive\n\n**Example:**\n```\ncurl -X PATCH -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"port\":5900}' http://localhost:5001/api/computers/protocols/3\n```", + "summary": "Delete a role.", + "description": "Delete a role.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: roleid (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/users/roles/1 -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -9628,11 +10280,60 @@ }, "parameters": [ { - "name": "protocol_id", + "name": "roleid", "in": "path", "required": true, "schema": { - "type": "string" + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "core-users" + ], + "summary": "Update a role.", + "description": "Update a role.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: roleid (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/users/roles/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "roleid", + "in": "path", + "required": true, + "schema": { + "type": "integer" } } ], @@ -9643,18 +10344,20 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "protocol_id in path; body: any of protocolname, port, isactive" + "description": "path: roleid (int); body: JSON (fields not yet documented)" } } } } - }, - "patch": { + } + }, + "/api/vendors": { + "get": { "tags": [ - "plugin-computers" + "core-vendors" ], - "summary": "Update one remote-access protocol in the catalog (VNC, WinRM, RDP) that PCs report against", - "description": "Update one remote-access protocol in the catalog (VNC, WinRM, RDP) that PCs report against\n\n**Auth:** permission:computers.edit (jwt_required)\n\n**Params:** protocol_id in path; body: any of protocolname, port, isactive\n\n**Example:**\n```\ncurl -X PATCH -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"port\":5900}' http://localhost:5001/api/computers/protocols/3\n```", + "summary": "List all vendors.", + "description": "List all vendors.\n\n**Auth:** permission:assets.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/vendors -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -9682,40 +10385,65 @@ }, "403": { "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." } - }, - "parameters": [ + } + }, + "post": { + "tags": [ + "core-vendors" + ], + "summary": "Create a new vendor.", + "description": "Create a new vendor.\n\n**Auth:** jwt + role:admin\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/vendors -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ { - "name": "protocol_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } + "bearerAuth": [] } ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, "requestBody": { - "required": false, + "required": true, "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true, - "description": "protocol_id in path; body: any of protocolname, port, isactive" + "description": "body: JSON (fields not yet documented)" } } } } - }, + } + }, + "/api/vendors/{vendor_id}": { "delete": { "tags": [ - "plugin-computers" + "core-vendors" ], - "summary": "Delete an access protocol (soft-deactivate when in use)", - "description": "Delete an access protocol (soft-deactivate when in use)\n\n**Auth:** permission:computers.edit (jwt_required)\n\n**Params:** protocol_id in path; if referenced by any ComputerAccess it deactivates instead of deleting\n\n**Example:**\n```\ncurl -X DELETE -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/computers/protocols/2'\n```", + "summary": "Delete (deactivate) a vendor.", + "description": "Delete (deactivate) a vendor.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: vendor_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/vendors/1 -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -9750,7 +10478,168 @@ }, "parameters": [ { - "name": "protocol_id", + "name": "vendor_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "get": { + "tags": [ + "core-vendors" + ], + "summary": "Get a single vendor.", + "description": "Get a single vendor.\n\n**Auth:** permission:assets.view\n\n**Params:** path: vendor_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/vendors/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "vendor_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "core-vendors" + ], + "summary": "Update a vendor.", + "description": "Update a vendor.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: vendor_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/vendors/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "vendor_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: vendor_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/backups/asset/{assetid}": { + "get": { + "tags": [ + "plugin-backups" + ], + "summary": "Revision history for one asset, newest first.", + "description": "Revision history for one asset, newest first.\n\n**Auth:** permission:backups.view\n\n**Params:** path: assetid (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/backups/asset/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "assetid", "in": "path", "required": true, "schema": { @@ -9760,15 +10649,14 @@ ] } }, - "/api/computers/display-kiosks": { + "/api/backups/asset/{assetid}/info": { "get": { "tags": [ - "plugin-computers" + "plugin-backups" ], - "summary": "List display-kiosk computers with derived F. FQDN for the Dashboard Defaults picker", - "description": "List display-kiosk computers with derived F. FQDN for the Dashboard Defaults picker\n\n**Auth:** jwt-optional\n\n**Params:** none; uses pctype mapping for gea-shopfloor-display (default 'Kiosk') and display_fqdn_domain setting\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/computers/display-kiosks'\n```", + "summary": "A kind's 'at a glance' card, built from its LATEST revision.", + "description": "A kind's 'at a glance' card, built from its LATEST revision.\n\n**Auth:** permission:backups.view\n\n**Params:** path: assetid (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/backups/asset/1/info -H \"Authorization: Bearer $TOK\"\n```", "security": [ - {}, { "bearerAuth": [] } @@ -9789,19 +10677,266 @@ "$ref": "#/components/schemas/ErrorEnvelope" } } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "assetid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/backups/dashboard/stale": { + "get": { + "tags": [ + "plugin-backups" + ], + "summary": "Chains whose backup has stopped running.", + "description": "Chains whose backup has stopped running.\n\n**Auth:** permission:backups.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/backups/dashboard/stale -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." } } } }, + "/api/backups/kinds": { + "get": { + "tags": [ + "plugin-backups" + ], + "summary": "Registered backup kinds, for UI that needs to enumerate them.", + "description": "Registered backup kinds, for UI that needs to enumerate them.\n\n**Auth:** permission:backups.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/backups/kinds -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/backups/revisions/{backuprevisionid}": { + "get": { + "tags": [ + "plugin-backups" + ], + "summary": "One revision, including its parsed payload where there is one.", + "description": "One revision, including its parsed payload where there is one.\n\n**Auth:** permission:backups.view\n\n**Params:** path: backuprevisionid (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/backups/revisions/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "backuprevisionid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/backups/revisions/{backuprevisionid}/diff": { + "get": { + "tags": [ + "plugin-backups" + ], + "summary": "Diff two revisions of the same asset+kind.", + "description": "Diff two revisions of the same asset+kind.\n\n**Auth:** permission:backups.view\n\n**Params:** path: backuprevisionid (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/backups/revisions/1/diff -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "backuprevisionid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/backups/revisions/{backuprevisionid}/download": { + "get": { + "tags": [ + "plugin-backups" + ], + "summary": "Render a revision back to its native file format.", + "description": "Render a revision back to its native file format.\n\n**Auth:** permission:backups.download\n\n**Params:** path: backuprevisionid (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/backups/revisions/1/download -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "backuprevisionid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, "/api/computers": { "get": { "tags": [ "plugin-computers" ], - "summary": "List computers (asset + computer + resolved access links), filtered and paginated", - "description": "List computers (asset + computer + resolved access links), filtered and paginated\n\n**Auth:** jwt-optional\n\n**Params:** page, per_page, active, search (assetnumber/name/serialnumber/hostname/gaugelabreference/maintenancereference 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)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/computers?shopfloor=true&sort=lastreporteddate&dir=desc&per_page=25'\n```", + "summary": "List all computers with filtering and pagination.", + "description": "List all computers with filtering and pagination.\n\n**Auth:** permission:computers.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/computers -H \"Authorization: Bearer $TOK\"\n```", "security": [ - {}, { "bearerAuth": [] } @@ -9822,6 +10957,12 @@ "$ref": "#/components/schemas/ErrorEnvelope" } } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." } } }, @@ -9829,8 +10970,8 @@ "tags": [ "plugin-computers" ], - "summary": "Create a computer (Asset + Computer records, optional primary IP and access methods), audit-logged", - "description": "Create a computer (Asset + Computer records, optional primary IP and access methods), audit-logged\n\n**Auth:** permission:computers.create (jwt_required)\n\n**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\n\n**Example:**\n```\ncurl -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'\n```", + "summary": "Create new computer (creates both Asset and Computer records).", + "description": "Create new computer (creates both Asset and Computer records).\n\n**Auth:** permission:computers.create\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/computers -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -9867,7 +11008,7 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "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" + "description": "body: JSON (fields not yet documented)" } } } @@ -9875,14 +11016,13 @@ } }, "/api/computers/{computer_id}": { - "get": { + "delete": { "tags": [ "plugin-computers" ], - "summary": "Get one computer with full detail: asset fields, computer extension, communications, resolved access links", - "description": "Get one computer with full detail: asset fields, computer extension, communications, resolved access links\n\n**Auth:** jwt-optional\n\n**Params:** computer_id in path\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/computers/42'\n```", + "summary": "Delete (soft delete) computer.", + "description": "Delete (soft delete) computer.\n\n**Auth:** permission:computers.delete\n\n**Params:** path: computer_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/computers/1 -H \"Authorization: Bearer $TOK\"\n```", "security": [ - {}, { "bearerAuth": [] } @@ -9904,6 +11044,61 @@ } } }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "computer_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "get": { + "tags": [ + "plugin-computers" + ], + "summary": "Get a single computer with full details.", + "description": "Get a single computer with full details.\n\n**Auth:** permission:computers.view\n\n**Params:** path: computer_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/computers/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, "404": { "description": "No such record." } @@ -9923,8 +11118,8 @@ "tags": [ "plugin-computers" ], - "summary": "Update a computer's asset and extension fields", - "description": "Update a computer's asset and extension fields\n\n**Auth:** permission:computers.edit (jwt_required)\n\n**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\n\n**Example:**\n```\ncurl -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'\n```", + "summary": "Update computer (both Asset and Computer records).", + "description": "Update computer (both Asset and Computer records).\n\n**Auth:** permission:computers.edit\n\n**Params:** path: computer_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/computers/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -9974,18 +11169,20 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "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" + "description": "path: computer_id (int); body: JSON (fields not yet documented)" } } } } - }, - "delete": { + } + }, + "/api/computers/{computer_id}/apps": { + "get": { "tags": [ "plugin-computers" ], - "summary": "Soft-delete a computer (sets asset isactive=false), audit-logged", - "description": "Soft-delete a computer (sets asset isactive=false), audit-logged\n\n**Auth:** permission:computers.delete (jwt_required)\n\n**Params:** computer_id in path\n\n**Example:**\n```\ncurl -X DELETE -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/computers/42'\n```", + "summary": "Get all installed applications for a computer.", + "description": "Get all installed applications for a computer.\n\n**Auth:** permission:computers.view\n\n**Params:** path: computer_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/computers/1/apps -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -10028,151 +11225,13 @@ } } ] - } - }, - "/api/computers/by-asset/{asset_id}": { - "get": { - "tags": [ - "plugin-computers" - ], - "summary": "Get computer record by its core asset ID", - "description": "Get computer record by its core asset ID\n\n**Auth:** jwt-optional\n\n**Params:** asset_id in path\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/computers/by-asset/1234'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "asset_id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ] - } - }, - "/api/computers/by-hostname/{hostname}": { - "get": { - "tags": [ - "plugin-computers" - ], - "summary": "Get computer record by hostname", - "description": "Get computer record by hostname\n\n**Auth:** jwt-optional\n\n**Params:** hostname in path (exact match)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/computers/by-hostname/WKSTN0042'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "hostname", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/computers/{computer_id}/apps": { - "get": { - "tags": [ - "plugin-computers" - ], - "summary": "List installed applications on a computer", - "description": "List installed applications on a computer\n\n**Auth:** jwt-optional\n\n**Params:** computer_id in path; returns active installs only\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/computers/42/apps'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "computer_id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ] }, "post": { "tags": [ "plugin-computers" ], - "summary": "Record an application install on a computer", - "description": "Record an application install on a computer\n\n**Auth:** permission:computers.create (jwt_required)\n\n**Params:** body: appid (required, must exist in Applications), appversionid; reactivates a soft-deleted install; 409 if already installed\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"appid\":17,\"appversionid\":3}' 'http://localhost:5001/api/computers/42/apps'\n```", + "summary": "Add an installed application to a computer.", + "description": "Add an installed application to a computer.\n\n**Auth:** permission:computers.create\n\n**Params:** path: computer_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/computers/1/apps -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -10222,7 +11281,7 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "body: appid (required, must exist in Applications), appversionid; reactivates a soft-deleted install; 409 if already installed" + "description": "path: computer_id (int); body: JSON (fields not yet documented)" } } } @@ -10234,8 +11293,8 @@ "tags": [ "plugin-computers" ], - "summary": "Soft-remove an installed application (isactive=false)", - "description": "Soft-remove an installed application (isactive=false)\n\n**Auth:** permission:computers.delete (jwt_required)\n\n**Params:** computer_id and app_id in path\n\n**Example:**\n```\ncurl -X DELETE -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/computers/42/apps/17'\n```", + "summary": "Remove an installed application from a computer.", + "description": "Remove an installed application from a computer.\n\n**Auth:** permission:computers.delete\n\n**Params:** path: computer_id (int), app_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/computers/1/apps/1 -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -10293,8 +11352,8 @@ "tags": [ "plugin-computers" ], - "summary": "Agent status check-in: refresh last-reported timestamp plus logged-in user and boot time", - "description": "Agent status check-in: refresh last-reported timestamp plus logged-in user and boot time\n\n**Auth:** permission:computers.create (jwt_required)\n\n**Params:** body (all optional): loggedinuser, lastboottime; server sets lastreporteddate to now (UTC)\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"loggedinuser\":\"jsmith\",\"lastboottime\":\"2026-07-30T06:00:00\"}' 'http://localhost:5001/api/computers/42/report'\n```", + "summary": "Report computer status (for agent-based reporting).", + "description": "Report computer status (for agent-based reporting).\n\n**Auth:** permission:computers.create\n\n**Params:** path: computer_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/computers/1/report -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -10344,22 +11403,21 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "body (all optional): loggedinuser, lastboottime; server sets lastreporteddate to now (UTC)" + "description": "path: computer_id (int); body: JSON (fields not yet documented)" } } } } } }, - "/api/computers/dashboard/summary": { + "/api/computers/asset/{asset_id}/policypanel": { "get": { "tags": [ "plugin-computers" ], - "summary": "Dashboard counts: total active computers, breakdown by type and OS, shopfloor vs non-shopfloor", - "description": "Dashboard counts: total active computers, breakdown by type and OS, shopfloor vs non-shopfloor\n\n**Auth:** jwt-optional\n\n**Params:** none\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/computers/dashboard/summary'\n```", + "summary": "Keyvalue panel: which policy governs this endpoint, and what it allows.", + "description": "Keyvalue panel: which policy governs this endpoint, and what it allows.\n\n**Auth:** permission:computers.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/computers/asset/1/policypanel -H \"Authorization: Bearer $TOK\"\n```", "security": [ - {}, { "bearerAuth": [] } @@ -10380,6 +11438,165 @@ "$ref": "#/components/schemas/ErrorEnvelope" } } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/computers/by-asset/{asset_id}": { + "get": { + "tags": [ + "plugin-computers" + ], + "summary": "Get computer data by asset ID.", + "description": "Get computer data by asset ID.\n\n**Auth:** permission:computers.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/computers/by-asset/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/computers/by-hostname/{hostname}": { + "get": { + "tags": [ + "plugin-computers" + ], + "summary": "Get computer by hostname.", + "description": "Get computer by hostname.\n\n**Auth:** permission:computers.view\n\n**Params:** path: hostname (string)\n\n**Example:**\n```\ncurl http://localhost:5001/api/computers/by-hostname/x -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "hostname", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/computers/client-script": { + "get": { + "tags": [ + "plugin-computers" + ], + "summary": "The collector reporter, stamped with THIS site's values.", + "description": "The collector reporter, stamped with THIS site's values.\n\n**Auth:** jwt + role:admin\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/computers/client-script -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." } } } @@ -10389,8 +11606,8 @@ "tags": [ "plugin-computers" ], - "summary": "Dashboard card: PCs that WERE reporting, have stopped, and are still meant to be in use - a PC that never reported is...", - "description": "Dashboard card: PCs that WERE reporting, have stopped, and are still meant to be in use - a PC that never reported is not evidence of a fault\n\n**Auth:** permission:computers.view\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/dashboard/quiet\n```", + "summary": "PCs that WERE reporting, have stopped, and are still meant to be in use.", + "description": "PCs that WERE reporting, have stopped, and are still meant to be in use.\n\n**Auth:** permission:computers.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/computers/dashboard/quiet -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -10427,8 +11644,8 @@ "tags": [ "plugin-computers" ], - "summary": "Dashboard card: machine numbers claimed by more than one PC, which is either a swap in progress or a mis-typed number", - "description": "Dashboard card: machine numbers claimed by more than one PC, which is either a swap in progress or a mis-typed number\n\n**Auth:** permission:computers.view\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/dashboard/sharedmachines\n```", + "summary": "Machine numbers claimed by more than one PC, with nothing filed under them.", + "description": "Machine numbers claimed by more than one PC, with nothing filed under them.\n\n**Auth:** permission:computers.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/computers/dashboard/sharedmachines -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -10460,13 +11677,13 @@ } } }, - "/api/computers/client-script": { + "/api/computers/dashboard/summary": { "get": { "tags": [ "plugin-computers" ], - "summary": "The collector reporter (Report-AssetToShopDB.ps1) stamped with THIS site's values: the site base URL becomes the...", - "description": "The collector reporter (Report-AssetToShopDB.ps1) stamped with THIS site's values: the site base URL becomes the -ApiUrl default and computers_routableranges becomes -AllowedRanges, so it downloads ready to deploy. Only the parameter DEFAULTS are substituted - the copy in plugins/computers/client/ stays runnable, so there is no second version to drift - and everything stamped stays overridable by argument or registry. The collector key is deliberately NOT included: this file lands on every shop-floor PC, and a token spread across hundreds of bays cannot be rotated quietly. Serves text/plain as an attachment, with the SHA-256 in X-Script-Sha256 so a deployment can verify what it fetched\n\n**Auth:** jwt + role:admin\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/client-script\n```", + "summary": "Get computer dashboard summary data.", + "description": "Get computer dashboard summary data.\n\n**Auth:** permission:computers.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/computers/dashboard/summary -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -10498,13 +11715,336 @@ } } }, + "/api/computers/display-kiosks": { + "get": { + "tags": [ + "plugin-computers" + ], + "summary": "Reporting display kiosks, for the Dashboard Defaults picker.", + "description": "Reporting display kiosks, for the Dashboard Defaults picker.\n\n**Auth:** permission:computers.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/computers/display-kiosks -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/computers/policies/actions": { + "get": { + "tags": [ + "plugin-computers" + ], + "summary": "The allowlisted action names + labels a policy may reference.", + "description": "The allowlisted action names + labels a policy may reference.\n\n**Auth:** permission:computers.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/computers/policies/actions -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/computers/policies/business": { + "get": { + "tags": [ + "plugin-computers" + ], + "summary": "UNDOCUMENTED: computers.list_business_policies has no docstring.", + "description": "UNDOCUMENTED: computers.list_business_policies has no docstring.\n\n**Auth:** permission:computers.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/computers/policies/business -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "post": { + "tags": [ + "plugin-computers" + ], + "summary": "UNDOCUMENTED: computers.create_business_policy has no docstring.", + "description": "UNDOCUMENTED: computers.create_business_policy has no docstring.\n\n**Auth:** permission:computers.create\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/computers/policies/business -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/computers/policies/business/{policy_id}": { + "delete": { + "tags": [ + "plugin-computers" + ], + "summary": "UNDOCUMENTED: computers.delete_business_policy has no docstring.", + "description": "UNDOCUMENTED: computers.delete_business_policy has no docstring.\n\n**Auth:** permission:computers.delete\n\n**Params:** path: policy_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/computers/policies/business/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "policy_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "get": { + "tags": [ + "plugin-computers" + ], + "summary": "UNDOCUMENTED: computers.get_business_policy has no docstring.", + "description": "UNDOCUMENTED: computers.get_business_policy has no docstring.\n\n**Auth:** permission:computers.view\n\n**Params:** path: policy_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/computers/policies/business/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "policy_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "plugin-computers" + ], + "summary": "UNDOCUMENTED: computers.update_business_policy has no docstring.", + "description": "UNDOCUMENTED: computers.update_business_policy has no docstring.\n\n**Auth:** permission:computers.edit\n\n**Params:** path: policy_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/computers/policies/business/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "policy_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: policy_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, "/api/computers/policies/configuration": { "get": { "tags": [ "plugin-computers" ], - "summary": "List Configuration Policies (scope + the actions applied to matching endpoints); 404 while the...", - "description": "List Configuration Policies (scope + the actions applied to matching endpoints); 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.view\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/configuration\n```", + "summary": "UNDOCUMENTED: computers.list_configuration_policies has no docstring.", + "description": "UNDOCUMENTED: computers.list_configuration_policies has no docstring.\n\n**Auth:** permission:computers.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/computers/policies/configuration -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -10539,8 +12079,8 @@ "tags": [ "plugin-computers" ], - "summary": "Create a Configuration Policy. Scope trees go through validate_scope and every action name is checked against the...", - "description": "Create a Configuration Policy. Scope trees go through validate_scope and every action name is checked against the endpoints command allowlist, so a bad tree or unknown action is rejected before it reaches the database; 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.create\n\n**Params:** body: name (required), description, scopejson (scope AST), actions[] of {action, mode: desired|gate, params}\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"name\":\"Shopfloor MTConnect\",\"actions\":[{\"action\":\"mtconnect_start\",\"mode\":\"desired\"}]}' http://localhost:5001/api/computers/policies/configuration\n```", + "summary": "UNDOCUMENTED: computers.create_configuration_policy has no docstring.", + "description": "UNDOCUMENTED: computers.create_configuration_policy has no docstring.\n\n**Auth:** permission:computers.create\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/computers/policies/configuration -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -10577,7 +12117,7 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "body: name (required), description, scopejson (scope AST), actions[] of {action, mode: desired|gate, params}" + "description": "body: JSON (fields not yet documented)" } } } @@ -10585,12 +12125,12 @@ } }, "/api/computers/policies/configuration/{policy_id}": { - "get": { + "delete": { "tags": [ "plugin-computers" ], - "summary": "One Configuration Policy with its actions expanded; 404 while the computers_configpolicies_enabled setting is off", - "description": "One Configuration Policy with its actions expanded; 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.view\n\n**Params:** path: policy_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/configuration/1policy_id\n```", + "summary": "UNDOCUMENTED: computers.delete_configuration_policy has no docstring.", + "description": "UNDOCUMENTED: computers.delete_configuration_policy has no docstring.\n\n**Auth:** permission:computers.delete\n\n**Params:** path: policy_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/computers/policies/configuration/1 -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -10629,7 +12169,56 @@ "in": "path", "required": true, "schema": { - "type": "string" + "type": "integer" + } + } + ] + }, + "get": { + "tags": [ + "plugin-computers" + ], + "summary": "UNDOCUMENTED: computers.get_configuration_policy has no docstring.", + "description": "UNDOCUMENTED: computers.get_configuration_policy has no docstring.\n\n**Auth:** permission:computers.view\n\n**Params:** path: policy_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/computers/policies/configuration/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "policy_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" } } ] @@ -10638,8 +12227,8 @@ "tags": [ "plugin-computers" ], - "summary": "Update a Configuration Policy; same scope and action validation as create; 404 while the...", - "description": "Update a Configuration Policy; same scope and action validation as create; 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.edit\n\n**Params:** path: policy_id; body: any of name, description, scopejson, actions[] (actions replace wholesale)\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/configuration/1policy_id\n```", + "summary": "UNDOCUMENTED: computers.update_configuration_policy has no docstring.", + "description": "UNDOCUMENTED: computers.update_configuration_policy has no docstring.\n\n**Auth:** permission:computers.edit\n\n**Params:** path: policy_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/computers/policies/configuration/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -10678,7 +12267,7 @@ "in": "path", "required": true, "schema": { - "type": "string" + "type": "integer" } } ], @@ -10689,345 +12278,11 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "path: policy_id; body: any of name, description, scopejson, actions[] (actions replace wholesale)" + "description": "path: policy_id (int); body: JSON (fields not yet documented)" } } } } - }, - "delete": { - "tags": [ - "plugin-computers" - ], - "summary": "Delete a Configuration Policy and its actions; 404 while the computers_configpolicies_enabled setting is off", - "description": "Delete a Configuration Policy and its actions; 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.delete\n\n**Params:** path: policy_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/configuration/1policy_id\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "policy_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/computers/policies/business": { - "get": { - "tags": [ - "plugin-computers" - ], - "summary": "List Business Policies (ordered bundles of Configuration Policies); 404 while the computers_configpolicies_enabled...", - "description": "List Business Policies (ordered bundles of Configuration Policies); 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.view\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/business\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - }, - "post": { - "tags": [ - "plugin-computers" - ], - "summary": "Create a Business Policy. Every referenced Configuration Policy must exist; priority decides which policy wins when two...", - "description": "Create a Business Policy. Every referenced Configuration Policy must exist; priority decides which policy wins when two match the same endpoint; 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.create\n\n**Params:** body: name (required), description, priority, items[] of {configurationpolicyid, sortorder}\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/business\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: name (required), description, priority, items[] of {configurationpolicyid, sortorder}" - } - } - } - } - } - }, - "/api/computers/policies/business/{policy_id}": { - "get": { - "tags": [ - "plugin-computers" - ], - "summary": "One Business Policy with its member Configuration Policies in sort order; 404 while the...", - "description": "One Business Policy with its member Configuration Policies in sort order; 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.view\n\n**Params:** path: policy_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/business/1policy_id\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "policy_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - }, - "put": { - "tags": [ - "plugin-computers" - ], - "summary": "Update a Business Policy or reorder its members; 404 while the computers_configpolicies_enabled setting is off", - "description": "Update a Business Policy or reorder its members; 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.edit\n\n**Params:** path: policy_id; body: any of name, description, priority, items[] (items replace wholesale)\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/business/1policy_id\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "policy_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "path: policy_id; body: any of name, description, priority, items[] (items replace wholesale)" - } - } - } - } - }, - "delete": { - "tags": [ - "plugin-computers" - ], - "summary": "Delete a Business Policy and its item rows; 404 while the computers_configpolicies_enabled setting is off", - "description": "Delete a Business Policy and its item rows; 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.delete\n\n**Params:** path: policy_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/business/1policy_id\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "policy_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/computers/policies/actions": { - "get": { - "tags": [ - "plugin-computers" - ], - "summary": "The allowlisted action names and display labels a policy may reference, read from the endpoints command allowlist. The...", - "description": "The allowlisted action names and display labels a policy may reference, read from the endpoints command allowlist. The UI builds its action picker from this rather than hardcoding a list that would drift; 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.view\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/actions\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } } }, "/api/computers/policies/effective/{asset_id}": { @@ -11035,8 +12290,8 @@ "tags": [ "plugin-computers" ], - "summary": "Preview the resolved effective policy for one endpoint: which Business Policy won, which Configuration Policies...", - "description": "Preview the resolved effective policy for one endpoint: which Business Policy won, which Configuration Policies matched, and the resulting desired/gate actions. Read-only, for debugging a scope that is not matching what its author expected; 404 while the computers_configpolicies_enabled setting is off\n\n**Auth:** computers.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/policies/effective/1asset_id\n```", + "summary": "Preview the resolved effective policy for one endpoint (debug/UI).", + "description": "Preview the resolved effective policy for one endpoint (debug/UI).\n\n**Auth:** permission:computers.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/computers/policies/effective/1 -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -11075,19 +12330,1143 @@ "in": "path", "required": true, "schema": { - "type": "string" + "type": "integer" } } ] } }, - "/api/computers/asset/{asset_id}/policypanel": { + "/api/computers/protocols": { "get": { "tags": [ "plugin-computers" ], - "summary": "Asset-detail panel: which policy governs this endpoint, how many configuration policies folded into it, and how many...", - "description": "Asset-detail panel: which policy governs this endpoint, how many configuration policies folded into it, and how many actions it allows or drives to a desired state. Provenance rather than a control - the effective policy narrows the command allowlist, so this is the answer to why an action is unavailable on a given PC. Returns an empty field list (not the feature-gate 404) when the policy feature is off, so the card self-hides instead of erroring on every site that has not enabled it\n\n**Auth:** computers.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/computers/asset/1/policypanel\n```", + "summary": "List access protocols. ?active=false includes disabled ones.", + "description": "List access protocols. ?active=false includes disabled ones.\n\n**Auth:** permission:computers.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/computers/protocols -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "post": { + "tags": [ + "plugin-computers" + ], + "summary": "UNDOCUMENTED: computers.create_protocol has no docstring.", + "description": "UNDOCUMENTED: computers.create_protocol has no docstring.\n\n**Auth:** permission:computers.edit\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/computers/protocols -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/computers/protocols/{protocol_id}": { + "delete": { + "tags": [ + "plugin-computers" + ], + "summary": "UNDOCUMENTED: computers.delete_protocol has no docstring.", + "description": "UNDOCUMENTED: computers.delete_protocol has no docstring.\n\n**Auth:** permission:computers.edit\n\n**Params:** path: protocol_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/computers/protocols/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "protocol_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "patch": { + "tags": [ + "plugin-computers" + ], + "summary": "UNDOCUMENTED: computers.update_protocol has no docstring.", + "description": "UNDOCUMENTED: computers.update_protocol has no docstring.\n\n**Auth:** permission:computers.edit\n\n**Params:** path: protocol_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PATCH http://localhost:5001/api/computers/protocols/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "protocol_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: protocol_id (int); body: JSON (fields not yet documented)" + } + } + } + } + }, + "put": { + "tags": [ + "plugin-computers" + ], + "summary": "UNDOCUMENTED: computers.update_protocol has no docstring.", + "description": "UNDOCUMENTED: computers.update_protocol has no docstring.\n\n**Auth:** permission:computers.edit\n\n**Params:** path: protocol_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PATCH http://localhost:5001/api/computers/protocols/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "protocol_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: protocol_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/computers/types": { + "get": { + "tags": [ + "plugin-computers" + ], + "summary": "List all computer types.", + "description": "List all computer types.\n\n**Auth:** permission:computers.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/computers/types -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "post": { + "tags": [ + "plugin-computers" + ], + "summary": "Create a new computer type.", + "description": "Create a new computer type.\n\n**Auth:** permission:computers.create\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/computers/types -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/computers/types/{type_id}": { + "delete": { + "tags": [ + "plugin-computers" + ], + "summary": "Delete a computer type. Refused if any PC still uses it.", + "description": "Delete a computer type. Refused if any PC still uses it.\n\n**Auth:** permission:computers.delete\n\n**Params:** path: type_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/computers/types/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "type_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "get": { + "tags": [ + "plugin-computers" + ], + "summary": "Get a single computer type.", + "description": "Get a single computer type.\n\n**Auth:** permission:computers.view\n\n**Params:** path: type_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/computers/types/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "type_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "plugin-computers" + ], + "summary": "Update a computer type.", + "description": "Update a computer type.\n\n**Auth:** permission:computers.edit\n\n**Params:** path: type_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/computers/types/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "type_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: type_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/employees/{sso}/photo": { + "delete": { + "tags": [ + "plugin-employees" + ], + "summary": "Clear an employee photo and delete the uploaded file. External mode 409s.", + "description": "Clear an employee photo and delete the uploaded file. External mode 409s.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: sso (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/employees/1/photo -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "sso", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "post": { + "tags": [ + "plugin-employees" + ], + "summary": "Upload (or replace) the photo for a self-hosted directory employee.", + "description": "Upload (or replace) the photo for a self-hosted directory employee.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: sso (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/employees/1/photo -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "sso", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: sso (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/employees/directory": { + "get": { + "tags": [ + "plugin-employees" + ], + "summary": "Full self-hosted directory (for the management page).", + "description": "Full self-hosted directory (for the management page).\n\n**Auth:** permission:employees.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/employees/directory -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "post": { + "tags": [ + "plugin-employees" + ], + "summary": "UNDOCUMENTED: employees.create_directory_employee has no docstring.", + "description": "UNDOCUMENTED: employees.create_directory_employee has no docstring.\n\n**Auth:** jwt + role:admin\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/employees/directory -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/employees/directory/{sso}": { + "delete": { + "tags": [ + "plugin-employees" + ], + "summary": "UNDOCUMENTED: employees.delete_directory_employee has no docstring.", + "description": "UNDOCUMENTED: employees.delete_directory_employee has no docstring.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: sso (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/employees/directory/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "sso", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "plugin-employees" + ], + "summary": "UNDOCUMENTED: employees.update_directory_employee has no docstring.", + "description": "UNDOCUMENTED: employees.update_directory_employee has no docstring.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: sso (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/employees/directory/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "sso", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: sso (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/employees/directory/import": { + "post": { + "tags": [ + "plugin-employees" + ], + "summary": "Bulk upsert from CSV. Accepts headers SSO,First_Name,Last_Name,Team,Role, Picture (case-insensitive; sso/firstname...", + "description": "Bulk upsert from CSV. Accepts headers SSO,First_Name,Last_Name,Team,Role, Picture (case-insensitive; sso/firstname/... also accepted).\n\n**Auth:** jwt + role:admin\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/employees/directory/import -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/employees/lookup": { + "get": { + "tags": [ + "plugin-employees" + ], + "summary": "Look up multiple employees by SSO list.", + "description": "Look up multiple employees by SSO list.\n\n**Auth:** permission:employees.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/employees/lookup -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/employees/lookup/{sso}": { + "get": { + "tags": [ + "plugin-employees" + ], + "summary": "Look up a single employee by SSO.", + "description": "Look up a single employee by SSO.\n\n**Auth:** permission:employees.view\n\n**Params:** path: sso (string)\n\n**Example:**\n```\ncurl http://localhost:5001/api/employees/lookup/x -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "sso", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/employees/photo/{filename}": { + "get": { + "tags": [ + "plugin-employees" + ], + "summary": "Serve an uploaded employee photo (public - kiosk cards read it).", + "description": "Serve an uploaded employee photo (public - kiosk cards read it).\n\n**Auth:** none (reviewed: named in the public-read or mutation sweep)\n\n**Params:** path: filename (path)\n\n**Example:**\n```\ncurl http://localhost:5001/api/employees/photo/example.png\n```", + "security": [], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "filename", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "may contain slashes" + } + } + ] + } + }, + "/api/employees/resolve/{sso}": { + "get": { + "tags": [ + "plugin-employees" + ], + "summary": "Resolve one SSO to a name and photo, and say HOW.", + "description": "Resolve one SSO to a name and photo, and say HOW.\n\n**Auth:** permission:employees.view\n\n**Params:** path: sso (string)\n\n**Example:**\n```\ncurl http://localhost:5001/api/employees/resolve/x -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "sso", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/employees/search": { + "get": { + "tags": [ + "plugin-employees" + ], + "summary": "Search employees by name.", + "description": "Search employees by name.\n\n**Auth:** permission:employees.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/employees/search -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/endpoints": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "List reported endpoint states.", + "description": "List reported endpoint states.\n\n**Auth:** permission:endpoints.fleet\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/ -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/endpoints/{asset_id}": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Get the current state for one endpoint by asset id.", + "description": "Get the current state for one endpoint by asset id.\n\n**Auth:** permission:endpoints.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/1 -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -11126,50 +13505,19 @@ "in": "path", "required": true, "schema": { - "type": "string" + "type": "integer" } } ] } }, - "/api/measuringtools/types": { - "get": { - "tags": [ - "plugin-measuringtools" - ], - "summary": "List measuring-tool types (active-only by default), paginated, name-sorted.", - "description": "List measuring-tool types (active-only by default), paginated, name-sorted.\n\n**Auth:** jwt-optional\n\n**Params:** query: active (default true; 'false' includes inactive), search (name ilike), page, perpage\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/measuringtools/types?search=caliper&page=1&perpage=25'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - }, + "/api/endpoints/{asset_id}/agentupgrade": { "post": { "tags": [ - "plugin-measuringtools" + "plugin-endpoints" ], - "summary": "Create a measuring-tool type; reactivates a soft-deleted same-named one, 409 if an active one exists.", - "description": "Create a measuring-tool type; reactivates a soft-deleted same-named one, 409 if an active one exists.\n\n**Auth:** jwt + permission:measuringtools.create\n\n**Params:** body JSON: name (required), description, color\n\n**Example:**\n```\ncurl -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\"}'\n```", + "summary": "Push the published agent build to one endpoint.", + "description": "Push the published agent build to one endpoint.\n\n**Auth:** permission:endpoints.agentupdate\n\n**Params:** path: asset_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/1/agentupgrade -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -11197,444 +13545,6 @@ }, "403": { "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body JSON: name (required), description, color" - } - } - } - } - } - }, - "/api/measuringtools/types/{type_id}": { - "get": { - "tags": [ - "plugin-measuringtools" - ], - "summary": "Get one measuring-tool type by id (404 if missing).", - "description": "Get one measuring-tool type by id (404 if missing).\n\n**Auth:** jwt-optional\n\n**Params:** path: type_id (int)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/measuringtools/types/3'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "type_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - }, - "put": { - "tags": [ - "plugin-measuringtools" - ], - "summary": "Update a measuring-tool type; 409 on rename collision with an existing name.", - "description": "Update a measuring-tool type; 409 on rename collision with an existing name.\n\n**Auth:** jwt + permission:measuringtools.edit\n\n**Params:** path: type_id; body JSON: name, description, color, isactive (only keys present are applied)\n\n**Example:**\n```\ncurl -X PUT 'http://localhost:5001/api/measuringtools/types/3' -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"description\":\"Updated\",\"isactive\":true}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "type_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "path: type_id; body JSON: name, description, color, isactive (only keys present are applied)" - } - } - } - } - }, - "delete": { - "tags": [ - "plugin-measuringtools" - ], - "summary": "Hard-delete a measuring-tool type; refused with 409 if any tool still references it.", - "description": "Hard-delete a measuring-tool type; refused with 409 if any tool still references it.\n\n**Auth:** jwt + permission:measuringtools.delete\n\n**Params:** path: type_id (int)\n\n**Example:**\n```\ncurl -X DELETE 'http://localhost:5001/api/measuringtools/types/3' -H \"Authorization: Bearer $TOKEN\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "type_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/measuringtools": { - "get": { - "tags": [ - "plugin-measuringtools" - ], - "summary": "List measuring tools (Asset core merged with extension), filtered + paginated; derived calibrationstatus filter applied...", - "description": "List measuring tools (Asset core merged with extension), filtered + paginated; derived calibrationstatus filter applied post-pagination.\n\n**Auth:** jwt-optional\n\n**Params:** query: active (default true), assetnumber (exact match, for idempotent import), search (assetnumber/name/serialnumber/gaugelabreference/maintenancereference ilike), typeid, locationid, statusid, calibrationstatus (overdue|duesoon|current|unknown), page, perpage\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/measuringtools?typeid=2&calibrationstatus=overdue&page=1&perpage=50'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - }, - "post": { - "tags": [ - "plugin-measuringtools" - ], - "summary": "Create a measuring tool: one Asset core row (assettype 'measuring_tool') plus one measuringtools extension row in a...", - "description": "Create a measuring tool: one Asset core row (assettype 'measuring_tool') plus one measuringtools extension row in a single payload; audit-logged.\n\n**Auth:** jwt + permission:measuringtools.create\n\n**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\n\n**Example:**\n```\ncurl -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\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "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" - } - } - } - } - } - }, - "/api/measuringtools/{tool_id}": { - "get": { - "tags": [ - "plugin-measuringtools" - ], - "summary": "Get one measuring tool by measuringtoolid, asset core dict with extension nested under 'measuringtool'.", - "description": "Get one measuring tool by measuringtoolid, asset core dict with extension nested under 'measuringtool'.\n\n**Auth:** jwt-optional\n\n**Params:** path: tool_id (int)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/measuringtools/17'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "tool_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - }, - "put": { - "tags": [ - "plugin-measuringtools" - ], - "summary": "Update asset core fields and extension fields in one payload; 409 on assetnumber collision; changes audit-logged.", - "description": "Update asset core fields and extension fields in one payload; 409 on assetnumber collision; changes audit-logged.\n\n**Auth:** jwt + permission:measuringtools.edit\n\n**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)\n\n**Example:**\n```\ncurl -X PUT 'http://localhost:5001/api/measuringtools/17' -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"locationid\":5,\"nextcalibrationdate\":\"2026-12-01\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "tool_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "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)" - } - } - } - } - }, - "delete": { - "tags": [ - "plugin-measuringtools" - ], - "summary": "Soft-delete a measuring tool by setting its asset isactive=false; audit-logged.", - "description": "Soft-delete a measuring tool by setting its asset isactive=false; audit-logged.\n\n**Auth:** jwt + permission:measuringtools.delete\n\n**Params:** path: tool_id (int)\n\n**Example:**\n```\ncurl -X DELETE 'http://localhost:5001/api/measuringtools/17' -H \"Authorization: Bearer $TOKEN\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "tool_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/measuringtools/by-asset/{asset_id}": { - "get": { - "tags": [ - "plugin-measuringtools" - ], - "summary": "Get a measuring tool by its core assetid (404 if the asset has no extension row).", - "description": "Get a measuring tool by its core assetid (404 if the asset has no extension row).\n\n**Auth:** jwt-optional\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/measuringtools/by-asset/1042'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } }, "404": { "description": "No such record." @@ -11646,249 +13556,7 @@ "in": "path", "required": true, "schema": { - "type": "string" - } - } - ] - } - }, - "/api/measuringtools/map-overlay": { - "get": { - "tags": [ - "plugin-measuringtools" - ], - "summary": "ADR-010 map overlay: per-asset calibration badges [{assetid, color, label}] for active tools that are overdue or due...", - "description": "ADR-010 map overlay: per-asset calibration badges [{assetid, color, label}] for active tools that are overdue or due soon only; no coordinates returned.\n\n**Auth:** jwt-optional\n\n**Params:** none\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/measuringtools/map-overlay'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/measuringtools/report/calibration": { - "get": { - "tags": [ - "plugin-measuringtools" - ], - "summary": "Calibration report for the Reports hub: counts and full tool lists bucketed by derived status...", - "description": "Calibration report for the Reports hub: counts and full tool lists bucketed by derived status (overdue/duesoon/current/unknown) plus statuscolors map.\n\n**Auth:** jwt-optional\n\n**Params:** none\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/measuringtools/report/calibration'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/machines/types": { - "get": { - "tags": [ - "plugin-machines" - ], - "summary": "List machine types (active by default) with pagination and name search", - "description": "List machine types (active by default) with pagination and name search\n\n**Auth:** jwt-optional\n\n**Params:** query: page, per_page, active (pass 'false' to include inactive), search (ilike on machinetype)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/machines/types?search=cnc&page=1&per_page=25'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - }, - "post": { - "tags": [ - "plugin-machines" - ], - "summary": "Create a machine type; reactivates a soft-deleted type of the same name instead of 409ing", - "description": "Create a machine type; reactivates a soft-deleted type of the same name instead of 409ing\n\n**Auth:** jwt + permission:machines.create\n\n**Params:** body: machinetype (required), description, icon, color\n\n**Example:**\n```\ncurl -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\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: machinetype (required), description, icon, color" - } - } - } - } - } - }, - "/api/machines/types/{type_id}": { - "get": { - "tags": [ - "plugin-machines" - ], - "summary": "Get a single machine type by ID", - "description": "Get a single machine type by ID\n\n**Auth:** jwt-optional\n\n**Params:** path: type_id (int)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/machines/types/3'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "type_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - }, - "put": { - "tags": [ - "plugin-machines" - ], - "summary": "Update a machine type (rename guarded by 409 on duplicate name)", - "description": "Update a machine type (rename guarded by 409 on duplicate name)\n\n**Auth:** jwt + permission:machines.edit\n\n**Params:** path: type_id; body: any of machinetype, description, icon, color, isactive\n\n**Example:**\n```\ncurl -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}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "type_id", - "in": "path", - "required": true, - "schema": { - "type": "string" + "type": "integer" } } ], @@ -11899,194 +13567,20 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "path: type_id; body: any of machinetype, description, icon, color, isactive" - } - } - } - } - }, - "delete": { - "tags": [ - "plugin-machines" - ], - "summary": "Hard-delete a machine type; refused with 409 if any machine still references it", - "description": "Hard-delete a machine type; refused with 409 if any machine still references it\n\n**Auth:** jwt + permission:machines.delete\n\n**Params:** path: type_id (int)\n\n**Example:**\n```\ncurl -X DELETE 'http://localhost:5001/api/machines/types/3' -H \"Authorization: Bearer $TOKEN\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "type_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/machines": { - "get": { - "tags": [ - "plugin-machines" - ], - "summary": "List machines (Asset+Machine join) with filters, sorting, pagination; collapses Dualpath dual-bay pairs to one row...", - "description": "List machines (Asset+Machine join) with filters, sorting, pagination; collapses Dualpath dual-bay pairs to one row (annotated with dualpathpartner) when site setting enabled\n\n**Auth:** jwt-optional\n\n**Params:** query: page, per_page, active ('false' includes inactive), assetnumber (exact-match for idempotent import), search (assetnumber/name/serialnumber/gaugelabreference/maintenancereference ilike), typeid|type_id, vendorid|vendor_id, locationid|location_id, businessunitid|businessunit_id, sort (assetnumber|name), dir (asc|desc)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/machines?search=2007&typeid=2&sort=name&dir=desc&page=1&per_page=50'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - }, - "post": { - "tags": [ - "plugin-machines" - ], - "summary": "Create a machine (creates both core Asset row and Machine extension row); audit-logged; honors X-Import-Mode legacy...", - "description": "Create a machine (creates both core Asset row and Machine extension row); audit-logged; honors X-Import-Mode legacy timestamps via apply_import_timestamps\n\n**Auth:** jwt + permission:machines.create\n\n**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\n\n**Example:**\n```\ncurl -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}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "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" + "description": "path: asset_id (int); body: JSON (fields not yet documented)" } } } } } }, - "/api/machines/{machine_id}": { + "/api/endpoints/{asset_id}/commands": { "get": { "tags": [ - "plugin-machines" + "plugin-endpoints" ], - "summary": "Get one machine with full asset details, nested machine dict, and dualpathpartner info", - "description": "Get one machine with full asset details, nested machine dict, and dualpathpartner info\n\n**Auth:** jwt-optional\n\n**Params:** path: machine_id (int)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/machines/42'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "machine_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - }, - "put": { - "tags": [ - "plugin-machines" - ], - "summary": "Update machine (asset + machine fields), with per-field change tracking to AuditLog and 409 on assetnumber conflict", - "description": "Update machine (asset + machine fields), with per-field change tracking to AuditLog and 409 on assetnumber conflict\n\n**Auth:** jwt + permission:machines.edit\n\n**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\n\n**Example:**\n```\ncurl -X PUT 'http://localhost:5001/api/machines/42' -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"locationid\":7,\"nextmaintenancedate\":\"2026-09-01\"}'\n```", + "summary": "Recent commands for one endpoint, newest first (operator view).", + "description": "Recent commands for one endpoint, newest first (operator view).\n\n**Auth:** permission:endpoints.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/1/commands -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -12119,165 +13613,23 @@ "description": "No such record." } }, - "parameters": [ - { - "name": "machine_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "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" - } - } - } - } - }, - "delete": { - "tags": [ - "plugin-machines" - ], - "summary": "Soft-delete a machine (sets asset.isactive=False, keeps Machine row linked); audit-logged", - "description": "Soft-delete a machine (sets asset.isactive=False, keeps Machine row linked); audit-logged\n\n**Auth:** jwt + permission:machines.delete\n\n**Params:** path: machine_id (int)\n\n**Example:**\n```\ncurl -X DELETE 'http://localhost:5001/api/machines/42' -H \"Authorization: Bearer $TOKEN\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "machine_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/machines/by-asset/{asset_id}": { - "get": { - "tags": [ - "plugin-machines" - ], - "summary": "Get machine data looked up by core asset ID instead of machine ID", - "description": "Get machine data looked up by core asset ID instead of machine ID\n\n**Auth:** jwt-optional\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/machines/by-asset/1001'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, "parameters": [ { "name": "asset_id", "in": "path", "required": true, "schema": { - "type": "string" + "type": "integer" } } ] - } - }, - "/api/machines/dashboard/summary": { - "get": { + }, + "post": { "tags": [ - "plugin-machines" + "plugin-endpoints" ], - "summary": "Dashboard summary: total active machine count plus counts grouped by machine type and by asset status (total and...", - "description": "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)\n\n**Auth:** jwt-optional\n\n**Params:** none\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/machines/dashboard/summary'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/machines/dashboard/outofservice": { - "get": { - "tags": [ - "plugin-machines" - ], - "summary": "Dashboard card: machines whose status says they are not running", - "description": "Dashboard card: machines whose status says they are not running\n\n**Auth:** permission:machines.view\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/machines/dashboard/outofservice\n```", + "summary": "Queue an allowlisted command for one endpoint.", + "description": "Queue an allowlisted command for one endpoint.\n\n**Auth:** permission:endpoints.command\n\n**Params:** path: asset_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/1/commands -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -12305,6 +13657,4371 @@ }, "403": { "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: asset_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/endpoints/{asset_id}/events": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "List curated Windows events for one endpoint, newest first.", + "description": "List curated Windows events for one endpoint, newest first.\n\n**Auth:** permission:endpoints.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/1/events -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/endpoints/{asset_id}/holdableactions": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Desired-mode actions on this endpoint, with current hold state.", + "description": "Desired-mode actions on this endpoint, with current hold state.\n\n**Auth:** permission:endpoints.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/1/holdableactions -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/endpoints/{asset_id}/lock": { + "delete": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Lift the quarantine on this endpoint.", + "description": "Lift the quarantine on this endpoint.\n\n**Auth:** permission:endpoints.command\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/endpoints/1/lock -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "The endpoint lock standing on this endpoint, or null.", + "description": "The endpoint lock standing on this endpoint, or null.\n\n**Auth:** permission:endpoints.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/1/lock -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "post": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Quarantine this endpoint against non-admin commands.", + "description": "Quarantine this endpoint against non-admin commands.\n\n**Auth:** permission:endpoints.command\n\n**Params:** path: asset_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/1/lock -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: asset_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/endpoints/{asset_id}/logcollections": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Collected-log history for one endpoint, newest first.", + "description": "Collected-log history for one endpoint, newest first.\n\n**Auth:** permission:endpoints.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/1/logcollections -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/endpoints/{asset_id}/policyholds": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Active break-glass holds for one endpoint.", + "description": "Active break-glass holds for one endpoint.\n\n**Auth:** permission:endpoints.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/1/policyholds -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "post": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Deliberately suspend desired-state reconciliation for one or more actions.", + "description": "Deliberately suspend desired-state reconciliation for one or more actions.\n\n**Auth:** permission:endpoints.command\n\n**Params:** path: asset_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/1/policyholds -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: asset_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/endpoints/{asset_id}/policyholds/{action}": { + "delete": { + "tags": [ + "plugin-endpoints" + ], + "summary": "End a hold early, resuming reconciliation of one action.", + "description": "End a hold early, resuming reconciliation of one action.\n\n**Auth:** permission:endpoints.command\n\n**Params:** path: asset_id (int), action (string)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/endpoints/1/policyholds/x -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "action", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/endpoints/{asset_id}/scriptruns": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "The last few run_script commands for one endpoint, newest first.", + "description": "The last few run_script commands for one endpoint, newest first.\n\n**Auth:** permission:endpoints.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/1/scriptruns -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/endpoints/adoptiontool/package": { + "post": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Bundle the adoption tool as a downloadable ZIP.", + "description": "Bundle the adoption tool as a downloadable ZIP.\n\n**Auth:** permission:endpoints.agentpackage\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/adoptiontool/package -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/endpoints/agentkey": { + "delete": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Revoke the endpoint agent key: stop all managed-token endpoint traffic.", + "description": "Revoke the endpoint agent key: stop all managed-token endpoint traffic.\n\n**Auth:** jwt + role:admin\n\n**Params:** not documented\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/endpoints/agentkey -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Report the endpoint agent key state (no secret).", + "description": "Report the endpoint agent key state (no secret).\n\n**Auth:** jwt + role:admin\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/agentkey -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/endpoints/agentkey/regenerate": { + "post": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Mint a fresh endpoint agent key, revoking any previous one.", + "description": "Mint a fresh endpoint agent key, revoking any previous one.\n\n**Auth:** jwt + role:admin\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/agentkey/regenerate -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/endpoints/alerts": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Fleet alerts for one endpoint by hostname (collector-token auth).", + "description": "Fleet alerts for one endpoint by hostname (collector-token auth).\n\n**Auth:** api-key (endpoint agent credential, or the fleet key while sharedkeyallowed is on)\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/alerts -H \"X-API-Key: $KEY\"\n```", + "security": [ + { + "apiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/endpoints/alerts/{notification_id}/acknowledge": { + "post": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Agent acknowledges a fleet alert (collector-token auth).", + "description": "Agent acknowledges a fleet alert (collector-token auth).\n\n**Auth:** api-key (endpoint agent credential, or the fleet key while sharedkeyallowed is on)\n\n**Params:** path: notification_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/alerts/1/acknowledge -H \"X-API-Key: $KEY\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "apiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "notification_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: notification_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/endpoints/apppackage/{appversionid}": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Agent-facing installer download (collector-token auth).", + "description": "Agent-facing installer download (collector-token auth).\n\n**Auth:** api-key (endpoint agent credential, or the fleet key while sharedkeyallowed is on)\n\n**Params:** path: appversionid (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/apppackage/1 -H \"X-API-Key: $KEY\"\n```", + "security": [ + { + "apiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "appversionid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/endpoints/apppackage/app/{app_id}": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Agent-facing installer download by APP (collector-token auth).", + "description": "Agent-facing installer download by APP (collector-token auth).\n\n**Auth:** api-key (endpoint agent credential, or the fleet key while sharedkeyallowed is on)\n\n**Params:** path: app_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/apppackage/app/1 -H \"X-API-Key: $KEY\"\n```", + "security": [ + { + "apiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "app_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/endpoints/apppackage/app/{app_id}/{filename}": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Agent-facing installer download by APP (collector-token auth).", + "description": "Agent-facing installer download by APP (collector-token auth).\n\n**Auth:** api-key (endpoint agent credential, or the fleet key while sharedkeyallowed is on)\n\n**Params:** path: app_id (int), filename (path)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/apppackage/app/1/example.png -H \"X-API-Key: $KEY\"\n```", + "security": [ + { + "apiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "app_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "filename", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "may contain slashes" + } + } + ] + } + }, + "/api/endpoints/asset/{asset_id}/activity": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "List panel: one chronological feed of what happened to this endpoint.", + "description": "List panel: one chronological feed of what happened to this endpoint.\n\n**Auth:** permission:endpoints.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/asset/1/activity -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/endpoints/asset/{asset_id}/applications": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Table panel: the installed-software (SBOM) snapshot for this endpoint.", + "description": "Table panel: the installed-software (SBOM) snapshot for this endpoint.\n\n**Auth:** permission:endpoints.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/asset/1/applications -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/endpoints/asset/{asset_id}/diskhealth": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Physical drive health rows.", + "description": "Physical drive health rows.\n\n**Auth:** permission:endpoints.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/asset/1/diskhealth -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/endpoints/asset/{asset_id}/hardware": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Keyvalue panel: processor, memory and system chassis inventory.", + "description": "Keyvalue panel: processor, memory and system chassis inventory.\n\n**Auth:** permission:endpoints.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/asset/1/hardware -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/endpoints/asset/{asset_id}/interfaces": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Table panel: every hardware NIC the agent last reported.", + "description": "Table panel: every hardware NIC the agent last reported.\n\n**Auth:** permission:endpoints.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/asset/1/interfaces -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/endpoints/asset/{asset_id}/iphistory": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Table panel: every address this asset has held, newest change first.", + "description": "Table panel: every address this asset has held, newest change first.\n\n**Auth:** permission:endpoints.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/asset/1/iphistory -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/endpoints/asset/{asset_id}/listeners": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Table panel: TCP sockets in LISTEN, with the owning process.", + "description": "Table panel: TCP sockets in LISTEN, with the owning process.\n\n**Auth:** permission:endpoints.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/asset/1/listeners -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/endpoints/asset/{asset_id}/mappeddrives": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "List panel: mapped network drives with a health badge each.", + "description": "List panel: mapped network drives with a health badge each.\n\n**Auth:** permission:endpoints.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/asset/1/mappeddrives -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/endpoints/asset/{asset_id}/osinfo": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Keyvalue panel: operating system, firmware and domain membership.", + "description": "Keyvalue panel: operating system, firmware and domain membership.\n\n**Auth:** permission:endpoints.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/asset/1/osinfo -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/endpoints/asset/{asset_id}/recentevents": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "List panel: the endpoint's most recent curated events, newest first.", + "description": "List panel: the endpoint's most recent curated events, newest first.\n\n**Auth:** permission:endpoints.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/asset/1/recentevents -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/endpoints/asset/{asset_id}/services": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "List panel: monitored services/processes with a run-state badge per row.", + "description": "List panel: monitored services/processes with a run-state badge per row.\n\n**Auth:** permission:endpoints.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/asset/1/services -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/endpoints/asset/{asset_id}/status": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Keyvalue panel: the endpoint's current status snapshot.", + "description": "Keyvalue panel: the endpoint's current status snapshot.\n\n**Auth:** permission:endpoints.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/asset/1/status -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/endpoints/asset/{asset_id}/storage": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Tabs panel: volume capacity and physical drive health as two sections.", + "description": "Tabs panel: volume capacity and physical drive health as two sections.\n\n**Auth:** permission:endpoints.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/asset/1/storage -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/endpoints/asset/{asset_id}/telemetrymeta": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Lightweight freshness probe for the panels.", + "description": "Lightweight freshness probe for the panels.\n\n**Auth:** permission:endpoints.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/asset/1/telemetrymeta -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/endpoints/asset/{asset_id}/usercontext": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Keyvalue panel: session-scoped data from the interactive user probe.", + "description": "Keyvalue panel: session-scoped data from the interactive user probe.\n\n**Auth:** permission:endpoints.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/asset/1/usercontext -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/endpoints/collect": { + "post": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Agent telemetry ingest. Same handler as POST /api/collector/endpoints.", + "description": "Agent telemetry ingest. Same handler as POST /api/collector/endpoints.\n\n**Auth:** api-key (endpoints collector token)\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/collect -H \"X-API-Key: $KEY\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "apiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/endpoints/commands/{command_id}/result": { + "post": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Agent reports a claimed command's outcome (collector-token auth).", + "description": "Agent reports a claimed command's outcome (collector-token auth).\n\n**Auth:** api-key (endpoint agent credential, or the fleet key while sharedkeyallowed is on)\n\n**Params:** path: command_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/commands/1/result -H \"X-API-Key: $KEY\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "apiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "command_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: command_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/endpoints/commands/next": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Agent long-poll: claim the next pending command for a hostname.", + "description": "Agent long-poll: claim the next pending command for a hostname.\n\n**Auth:** api-key (endpoint agent credential, or the fleet key while sharedkeyallowed is on)\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/commands/next -H \"X-API-Key: $KEY\"\n```", + "security": [ + { + "apiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/endpoints/dashboard/enrollmentpending": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "One queue row while enroll requests wait for an admin, else nothing.", + "description": "One queue row while enroll requests wait for an admin, else nothing.\n\n**Auth:** permission:endpoints.fleet\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/dashboard/enrollmentpending -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/endpoints/dashboard/quiet": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Endpoints that reported before, then went quiet past the window.", + "description": "Endpoints that reported before, then went quiet past the window.\n\n**Auth:** permission:endpoints.fleet\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/dashboard/quiet -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/endpoints/dashboard/reenrollblocked": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "PCs kept from re-enrolling for more than 7 days.", + "description": "PCs kept from re-enrolling for more than 7 days.\n\n**Auth:** permission:endpoints.fleet\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/dashboard/reenrollblocked -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/endpoints/dashboard/stability": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Endpoints with recent crash/shutdown/corruption events.", + "description": "Endpoints with recent crash/shutdown/corruption events.\n\n**Auth:** permission:endpoints.fleet\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/dashboard/stability -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/endpoints/dashboard/summary": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "High-level fleet counts for the endpoints dashboard.", + "description": "High-level fleet counts for the endpoints dashboard.\n\n**Auth:** permission:endpoints.fleet\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/dashboard/summary -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/endpoints/enroll": { + "post": { + "tags": [ + "plugin-endpoints" + ], + "summary": "A PC asks for its own credential.", + "description": "A PC asks for its own credential.\n\n**Auth:** none (per-IP rate limited and capped; a valid fleet key only decides auto-enroll)\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/enroll -H 'Content-Type: application/json' -d '{}'\n```", + "security": [], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/endpoints/enrollment": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Credential state of every computer, plus pending and recent requests.", + "description": "Credential state of every computer, plus pending and recent requests.\n\n**Auth:** permission:endpoints.fleet\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/enrollment -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/endpoints/enrollment/assets/{assetid}/allow-reenroll": { + "post": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Lift a re-enrollment block. The PC enrolls on its next attempt.", + "description": "Lift a re-enrollment block. The PC enrolls on its next attempt.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: assetid (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/enrollment/assets/1/allow-reenroll -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "assetid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: assetid (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/endpoints/enrollment/assets/{assetid}/revoke": { + "post": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Un-enroll a PC. Its agent gets CREDENTIAL_INVALID and enrolls again.", + "description": "Un-enroll a PC. Its agent gets CREDENTIAL_INVALID and enrolls again.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: assetid (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/enrollment/assets/1/revoke -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "assetid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: assetid (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/endpoints/enrollment/migration": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Per-computer readiness to move onto per-PC credentials, plus waves.", + "description": "Per-computer readiness to move onto per-PC credentials, plus waves.\n\n**Auth:** permission:endpoints.fleet\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/enrollment/migration -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/endpoints/enrollment/migration/wave": { + "post": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Push the agent upgrade to a batch of READY PCs.", + "description": "Push the agent upgrade to a batch of READY PCs.\n\n**Auth:** jwt + role:admin\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/enrollment/migration/wave -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/endpoints/enrollment/requests/{requestid}/approve": { + "post": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Bind a pending request to the asset the admin picked.", + "description": "Bind a pending request to the asset the admin picked.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: requestid (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/enrollment/requests/1/approve -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "requestid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: requestid (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/endpoints/enrollment/requests/{requestid}/reject": { + "post": { + "tags": [ + "plugin-endpoints" + ], + "summary": "UNDOCUMENTED: endpoints.reject_enroll_request has no docstring.", + "description": "UNDOCUMENTED: endpoints.reject_enroll_request has no docstring.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: requestid (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/enrollment/requests/1/reject -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "requestid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: requestid (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/endpoints/enrollment/settings": { + "put": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Change the enrollment settings.", + "description": "Change the enrollment settings.\n\n**Auth:** jwt + role:admin\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/endpoints/enrollment/settings -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/endpoints/logcollections/{collection_id}": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "One collected log including its content, for the scrollable pane.", + "description": "One collected log including its content, for the scrollable pane.\n\n**Auth:** permission:endpoints.updateapp\n\n**Params:** path: collection_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/logcollections/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "collection_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/endpoints/logcollections/{collection_id}/download": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Serve a collected log as a file attachment.", + "description": "Serve a collected log as a file attachment.\n\n**Auth:** permission:endpoints.updateapp\n\n**Params:** path: collection_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/logcollections/1/download -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "collection_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/endpoints/monitors": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "List monitor definitions (shipped and local).", + "description": "List monitor definitions (shipped and local).\n\n**Auth:** permission:endpoints.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/monitors -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "post": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Create a user-defined (local) monitor definition.", + "description": "Create a user-defined (local) monitor definition.\n\n**Auth:** jwt + role:admin\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/monitors -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/endpoints/monitors/{monitor_id}": { + "delete": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Delete a local monitor definition. Shipped rows are protected.", + "description": "Delete a local monitor definition. Shipped rows are protected.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: monitor_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/endpoints/monitors/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "monitor_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Update a local monitor definition. Shipped rows are read-only.", + "description": "Update a local monitor definition. Shipped rows are read-only.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: monitor_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/endpoints/monitors/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "monitor_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: monitor_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/endpoints/monitors/resolve": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Resolve the monitor set for a PC type (agent runtime fetch).", + "description": "Resolve the monitor set for a PC type (agent runtime fetch).\n\n**Auth:** permission:endpoints.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/monitors/resolve -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/endpoints/monitors/resolved": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Agent watch list: the monitors this endpoint should report on.", + "description": "Agent watch list: the monitors this endpoint should report on.\n\n**Auth:** api-key (endpoint agent credential, or the fleet key while sharedkeyallowed is on)\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/monitors/resolved -H \"X-API-Key: $KEY\"\n```", + "security": [ + { + "apiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/endpoints/operatorreports": { + "post": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Agent uploads one operator issue report (collector-token auth).", + "description": "Agent uploads one operator issue report (collector-token auth).\n\n**Auth:** api-key (endpoint agent credential, or the fleet key while sharedkeyallowed is on)\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/operatorreports -H \"X-API-Key: $KEY\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "apiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/endpoints/operatorreports/{operatorreportid}/image": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Serve one operator report's screenshot.", + "description": "Serve one operator report's screenshot.\n\n**Auth:** permission:endpoints.operatorreports\n\n**Params:** path: operatorreportid (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/operatorreports/1/image -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "operatorreportid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/endpoints/panel/{asset_id}/operatorreports": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Operator Reports panel: this PC's recent operator issue reports.", + "description": "Operator Reports panel: this PC's recent operator issue reports.\n\n**Auth:** permission:endpoints.operatorreports\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/panel/1/operatorreports -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/endpoints/scriptbody/{script_id}": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Agent-facing script body download (collector-token auth).", + "description": "Agent-facing script body download (collector-token auth).\n\n**Auth:** api-key (endpoint agent credential, or the fleet key while sharedkeyallowed is on)\n\n**Params:** path: script_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/scriptbody/1 -H \"X-API-Key: $KEY\"\n```", + "security": [ + { + "apiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "script_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/endpoints/settings/server-cert": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Get details about the current server certificate.", + "description": "Get details about the current server certificate.\n\n**Auth:** jwt + role:admin\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/settings/server-cert -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/endpoints/settings/server-cert/download": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "The PUBLIC server certificate as a DER .cer file, never the key.", + "description": "The PUBLIC server certificate as a DER .cer file, never the key.\n\n**Auth:** permission:endpoints.fleet\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/settings/server-cert/download -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/endpoints/settings/server-cert/generate": { + "post": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Generate a new self-signed server certificate.", + "description": "Generate a new self-signed server certificate.\n\n**Auth:** jwt + role:admin\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/settings/server-cert/generate -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/endpoints/settings/server-cert/live": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "The certificate PCs actually receive from this site's public address.", + "description": "The certificate PCs actually receive from this site's public address.\n\n**Auth:** permission:endpoints.fleet\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/settings/server-cert/live -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/endpoints/settings/server-cert/upload": { + "post": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Upload a new server certificate and private key.", + "description": "Upload a new server certificate and private key.\n\n**Auth:** jwt + role:admin\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/settings/server-cert/upload -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/endpoints/siteagent": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Report whether the site agent build has a prebuilt binary staged.", + "description": "Report whether the site agent build has a prebuilt binary staged.\n\n**Auth:** permission:endpoints.agentpackage\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/siteagent -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/endpoints/siteagent/binaries/{binkey}": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "List staged versions for a binary (agent or tray), newest first.", + "description": "List staged versions for a binary (agent or tray), newest first.\n\n**Auth:** permission:endpoints.agentpackage\n\n**Params:** path: binkey (string)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/siteagent/binaries/x -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "binkey", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "post": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Stage an uploaded binary as a new version.", + "description": "Stage an uploaded binary as a new version.\n\n**Auth:** permission:endpoints.agentpackage\n\n**Params:** path: binkey (string); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/siteagent/binaries/x -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "binkey", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: binkey (string); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/endpoints/siteagent/binaries/{binkey}/adopt": { + "post": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Adopt the unmanaged legacy flat file into the version store.", + "description": "Adopt the unmanaged legacy flat file into the version store.\n\n**Auth:** permission:endpoints.agentpackage\n\n**Params:** path: binkey (string); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/siteagent/binaries/x/adopt -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "binkey", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: binkey (string); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/endpoints/siteagent/binaries/{binkey}/current": { + "put": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Point 'current' at an already-staged version (roll back or forward).", + "description": "Point 'current' at an already-staged version (roll back or forward).\n\n**Auth:** permission:endpoints.agentpackage\n\n**Params:** path: binkey (string); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/endpoints/siteagent/binaries/x/current -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "binkey", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: binkey (string); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/endpoints/siteagent/binaries/{binkey}/detect": { + "post": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Read the PE file version of an uploaded EXE without staging it.", + "description": "Read the PE file version of an uploaded EXE without staging it.\n\n**Auth:** permission:endpoints.agentpackage\n\n**Params:** path: binkey (string); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/siteagent/binaries/x/detect -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "binkey", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: binkey (string); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/endpoints/siteagent/bootstrap.ps1": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "The PowerShell the one-line command downloads, runs and deletes.", + "description": "The PowerShell the one-line command downloads, runs and deletes.\n\n**Auth:** api-key (single-use bootstrap token in the X-Endpoint-Bootstrap-Token header, or ?t= while that is enabled; per-IP rate limited, 404 when not usable)\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/siteagent/bootstrap.ps1\n```", + "security": [ + { + "apiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/endpoints/siteagent/bootstrap/package": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "The keyless agent package the bootstrap script installs.", + "description": "The keyless agent package the bootstrap script installs.\n\n**Auth:** api-key (single-use bootstrap token in the X-Endpoint-Bootstrap-Token header, or ?t= while that is enabled; per-IP rate limited, 404 when not usable)\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/siteagent/bootstrap/package\n```", + "security": [ + { + "apiKeyAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/endpoints/siteagent/bootstrap/tokens": { + "get": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Every bootstrap token, newest first. Never a secret.", + "description": "Every bootstrap token, newest first. Never a secret.\n\n**Auth:** jwt + role:admin\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/siteagent/bootstrap/tokens -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "post": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Mint one short-lived bootstrap token. The secret is returned ONCE.", + "description": "Mint one short-lived bootstrap token. The secret is returned ONCE.\n\n**Auth:** jwt + role:admin\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/siteagent/bootstrap/tokens -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/endpoints/siteagent/bootstrap/tokens/{tokenid}/revoke": { + "post": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Kill a bootstrap token now. Already-revoked is a no-op, not an error.", + "description": "Kill a bootstrap token now. Already-revoked is a no-op, not an error.\n\n**Auth:** jwt + role:admin\n\n**Params:** path: tokenid (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/siteagent/bootstrap/tokens/1/revoke -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "tokenid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: tokenid (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/endpoints/siteagent/package": { + "post": { + "tags": [ + "plugin-endpoints" + ], + "summary": "Bundle the site agent as a downloadable ZIP.", + "description": "Bundle the site agent as a downloadable ZIP.\n\n**Auth:** permission:endpoints.agentpackage\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/siteagent/package -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/geenforce/applications": { + "get": { + "tags": [ + "plugin-geenforce" + ], + "summary": "The core Applications catalog (id + name) for the curated entry->app link picker in the entry editor.", + "description": "The core Applications catalog (id + name) for the curated entry->app link picker in the entry editor.\n\n**Auth:** permission:geenforce.view or geenforce.manage\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/geenforce/applications -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/geenforce/config": { + "get": { + "tags": [ + "plugin-geenforce" + ], + "summary": "Plugin config: the on-share export root + the client IP allowlist.", + "description": "Plugin config: the on-share export root + the client IP allowlist.\n\n**Auth:** permission:geenforce.view or geenforce.manage\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/geenforce/config -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "put": { + "tags": [ + "plugin-geenforce" + ], + "summary": "UNDOCUMENTED: geenforce.put_config has no docstring.", + "description": "UNDOCUMENTED: geenforce.put_config has no docstring.\n\n**Auth:** permission:geenforce.publish\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/geenforce/config -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/geenforce/dashboard/failures": { + "get": { + "tags": [ + "plugin-geenforce" + ], + "summary": "Entries that FAILED on their PC's most recent enforcement cycle.", + "description": "Entries that FAILED on their PC's most recent enforcement cycle.\n\n**Auth:** permission:geenforce.view or geenforce.manage\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/geenforce/dashboard/failures -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/geenforce/entries/{entryid}": { + "delete": { + "tags": [ + "plugin-geenforce" + ], + "summary": "UNDOCUMENTED: geenforce.delete_entry has no docstring.", + "description": "UNDOCUMENTED: geenforce.delete_entry has no docstring.\n\n**Auth:** permission:geenforce.manage\n\n**Params:** path: entryid (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/geenforce/entries/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "entryid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "plugin-geenforce" + ], + "summary": "UNDOCUMENTED: geenforce.update_entry has no docstring.", + "description": "UNDOCUMENTED: geenforce.update_entry has no docstring.\n\n**Auth:** permission:geenforce.manage\n\n**Params:** path: entryid (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/geenforce/entries/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "entryid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: entryid (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/geenforce/entries/{entryid}/payload": { + "get": { + "tags": [ + "plugin-geenforce" + ], + "summary": "Return the stored inline payload bytes for an entry (404 if none).", + "description": "Return the stored inline payload bytes for an entry (404 if none).\n\n**Auth:** permission:geenforce.view or geenforce.manage\n\n**Params:** path: entryid (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/geenforce/entries/1/payload -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "entryid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "post": { + "tags": [ + "plugin-geenforce" + ], + "summary": "Store an inline payload (<= 1 MB) for an entry and point the entry at it.", + "description": "Store an inline payload (<= 1 MB) for an entry and point the entry at it.\n\n**Auth:** permission:geenforce.publish\n\n**Params:** path: entryid (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/geenforce/entries/1/payload -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "entryid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: entryid (int); body: JSON (fields not yet documented)" + } + } } } } @@ -12314,8 +18031,8 @@ "tags": [ "plugin-geenforce" ], - "summary": "Serve the current PUBLISHED manifest JSON snapshot for a scope (never the live draft) to the GE-Enforce client, with...", - "description": "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.\n\n**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)\n\n**Params:** query: pctype (required, =scopename), phase (default 'runtime'); header: If-None-Match for 304\n\n**Example:**\n```\ncurl -H \"X-API-Key: $TOKEN\" 'http://localhost:5001/api/geenforce/manifest?pctype=cmm&phase=runtime'\n```", + "summary": "Serve the current published manifest for a scope (fat-client: full scope).", + "description": "Serve the current published manifest for a scope (fat-client: full scope).\n\n**Auth:** api-key (geenforce fetch token, or the IP allowlist)\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/geenforce/manifest -H \"X-API-Key: $KEY\"\n```", "security": [ { "apiKeyAuth": [] @@ -12352,8 +18069,8 @@ "tags": [ "plugin-geenforce" ], - "summary": "Download a payload blob by content hash (blob store first, then inline DB payload) so share-less PCs can pull...", - "description": "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).\n\n**Auth:** api-key (geenforce.fetch service token) OR IP allowlist; resource-bound tokens get 404 for blobs not referenced by their scopes\n\n**Params:** path: sha256 (64 lowercase hex chars, 400 otherwise); header: If-None-Match (ETag = the hash)\n\n**Example:**\n```\ncurl -H \"X-API-Key: $TOKEN\" -o installer.exe 'http://localhost:5001/api/geenforce/payload/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'\n```", + "summary": "Serve a payload blob by content hash over HTTPS.", + "description": "Serve a payload blob by content hash over HTTPS.\n\n**Auth:** api-key (geenforce fetch token, or the IP allowlist)\n\n**Params:** path: sha256 (string)\n\n**Example:**\n```\ncurl http://localhost:5001/api/geenforce/payload/x -H \"X-API-Key: $KEY\"\n```", "security": [ { "apiKeyAuth": [] @@ -12403,8 +18120,8 @@ "tags": [ "plugin-geenforce" ], - "summary": "Record one PC's enforcement cycle: applied manifest version plus per-entry self-heal outcomes...", - "description": "Record one PC's enforcement cycle: applied manifest version plus per-entry self-heal outcomes (installed/skipped/failed); returns reportid + status.\n\n**Auth:** api-key (managed service token with geenforce.report scope) OR IP allowlist\n\n**Params:** JSON body: hostname (required); remainder parsed by service.record_enforcement_report (scopename, phase, appliedversion, enforcerversion, status, per-entry results, counts); 400 on ValueError\n\n**Example:**\n```\ncurl -X POST -H \"X-API-Key: $TOKEN\" -H 'Content-Type: application/json' -d '{\"hostname\":\"WKSTN0042\",\"scopename\":\"cmm\",\"appliedversion\":4,\"results\":[{\"entryname\":\"7zip\",\"action\":\"installed\"}]}' http://localhost:5001/api/geenforce/report\n```", + "summary": "Record one PC's enforcement cycle: applied version (did it receive the update?) + per-entry self-heal outcomes...", + "description": "Record one PC's enforcement cycle: applied version (did it receive the update?) + per-entry self-heal outcomes (installed/skipped/failed).\n\n**Auth:** api-key (geenforce report token)\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/geenforce/report -H \"X-API-Key: $KEY\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "apiKeyAuth": [] @@ -12441,20 +18158,109 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "JSON body: hostname (required); remainder parsed by service.record_enforcement_report (scopename, phase, appliedversion, enforcerversion, status, per-entry results, counts); 400 on ValueError" + "description": "body: JSON (fields not yet documented)" } } } } } }, + "/api/geenforce/reports": { + "get": { + "tags": [ + "plugin-geenforce" + ], + "summary": "Latest enforcement report per PC (fleet compliance view).", + "description": "Latest enforcement report per PC (fleet compliance view).\n\n**Auth:** permission:geenforce.view or geenforce.manage\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/geenforce/reports -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/geenforce/reports/{reportid}": { + "get": { + "tags": [ + "plugin-geenforce" + ], + "summary": "One report with per-entry outcomes (self-heal / failure detail).", + "description": "One report with per-entry outcomes (self-heal / failure detail).\n\n**Auth:** permission:geenforce.view or geenforce.manage\n\n**Params:** path: reportid (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/geenforce/reports/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "reportid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, "/api/geenforce/scopes": { "get": { "tags": [ "plugin-geenforce" ], - "summary": "List all imaging PC-type scopes with entry counts and current published version numbers, ordered by phase then...", - "description": "List all imaging PC-type scopes with entry counts and current published version numbers, ordered by phase then scopename.\n\n**Auth:** jwt + permission:geenforce.manage\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/scopes\n```", + "summary": "List imaging PC-type scopes with entry + published-version counts.", + "description": "List imaging PC-type scopes with entry + published-version counts.\n\n**Auth:** permission:geenforce.view or geenforce.manage\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/geenforce/scopes -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -12489,8 +18295,8 @@ "tags": [ "plugin-geenforce" ], - "summary": "Create a new manifest scope; 400 if scopename missing, phase invalid, or scope already exists for that scopename+phase...", - "description": "Create a new manifest scope; 400 if scopename missing, phase invalid, or scope already exists for that scopename+phase; returns 201 with scope summary.\n\n**Auth:** jwt + permission:geenforce.manage\n\n**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')\n\n**Example:**\n```\ncurl -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\n```", + "summary": "UNDOCUMENTED: geenforce.create_scope has no docstring.", + "description": "UNDOCUMENTED: geenforce.create_scope has no docstring.\n\n**Auth:** permission:geenforce.manage\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/geenforce/scopes -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -12527,7 +18333,7 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "JSON body: scopename (required), phase (default 'runtime', must be in PHASES), manifestversion (default '1.0'), description, computertypeid, measuringtooltypeid, iscommon (defaults true when scopename=='common')" + "description": "body: JSON (fields not yet documented)" } } } @@ -12535,122 +18341,12 @@ } }, "/api/geenforce/scopes/{scopeid}": { - "get": { - "tags": [ - "plugin-geenforce" - ], - "summary": "Get one scope's summary plus its full draft entry list (each entry includes entryid, sortorder, curated appid/appname...", - "description": "Get one scope's summary plus its full draft entry list (each entry includes entryid, sortorder, curated appid/appname link, and inline-payload metadata).\n\n**Auth:** jwt + permission:geenforce.manage\n\n**Params:** path: scopeid\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/scopes/3\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "scopeid", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ] - }, - "put": { - "tags": [ - "plugin-geenforce" - ], - "summary": "Update scope metadata fields (only keys present in the body are changed); scopename and phase are immutable here.", - "description": "Update scope metadata fields (only keys present in the body are changed); scopename and phase are immutable here.\n\n**Auth:** jwt + permission:geenforce.manage\n\n**Params:** JSON body (all optional): description, computertypeid, measuringtooltypeid, manifestversion (stringified), iscommon (bool-coerced)\n\n**Example:**\n```\ncurl -X PUT -H \"Authorization: Bearer $JWT\" -H 'Content-Type: application/json' -d '{\"description\":\"updated\",\"manifestversion\":\"1.1\"}' http://localhost:5001/api/geenforce/scopes/3\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "scopeid", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "JSON body (all optional): description, computertypeid, measuringtooltypeid, manifestversion (stringified), iscommon (bool-coerced)" - } - } - } - } - }, "delete": { "tags": [ "plugin-geenforce" ], - "summary": "Delete a scope (and via cascade its entries); returns {deleted: scopeid}.", - "description": "Delete a scope (and via cascade its entries); returns {deleted: scopeid}.\n\n**Auth:** jwt + permission:geenforce.manage\n\n**Params:** path: scopeid\n\n**Example:**\n```\ncurl -X DELETE -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/scopes/3\n```", + "summary": "UNDOCUMENTED: geenforce.delete_scope has no docstring.", + "description": "UNDOCUMENTED: geenforce.delete_scope has no docstring.\n\n**Auth:** permission:geenforce.manage\n\n**Params:** path: scopeid (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/geenforce/scopes/1 -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -12693,228 +18389,13 @@ } } ] - } - }, - "/api/geenforce/scopes/{scopeid}/preview": { - "get": { - "tags": [ - "plugin-geenforce" - ], - "summary": "Render the DRAFT manifest JSON that a publish would freeze, for admin review before shipping.", - "description": "Render the DRAFT manifest JSON that a publish would freeze, for admin review before shipping.\n\n**Auth:** jwt + permission:geenforce.manage\n\n**Params:** path: scopeid\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/scopes/3/preview\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "scopeid", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ] - } - }, - "/api/geenforce/applications": { - "get": { - "tags": [ - "plugin-geenforce" - ], - "summary": "List the core active Applications catalog (appid + appname) for the curated entry-to-app link picker in the entry...", - "description": "List the core active Applications catalog (appid + appname) for the curated entry-to-app link picker in the entry editor.\n\n**Auth:** jwt + permission:geenforce.manage\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/applications\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - } - }, - "/api/geenforce/scopes/{scopeid}/entries": { - "post": { - "tags": [ - "plugin-geenforce" - ], - "summary": "Create a manifest entry in a scope at the next sortorder; validates Name (required) and Type (must be in ENTRY_TYPES)...", - "description": "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.\n\n**Auth:** jwt + permission:geenforce.manage\n\n**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.)\n\n**Example:**\n```\ncurl -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\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "scopeid", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "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.)" - } - } - } - } - } - }, - "/api/geenforce/entries/{entryid}": { - "put": { - "tags": [ - "plugin-geenforce" - ], - "summary": "Replace an entry's fields from the payload (re-populates via populate_entry, re-creating the one-to-one InUseCheck)...", - "description": "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.\n\n**Auth:** jwt + permission:geenforce.manage\n\n**Params:** path: entryid; JSON body: Name (required), Type (required), optional appid, plus manifest fields\n\n**Example:**\n```\ncurl -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\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "entryid", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "path: entryid; JSON body: Name (required), Type (required), optional appid, plus manifest fields" - } - } - } - } }, - "delete": { + "get": { "tags": [ "plugin-geenforce" ], - "summary": "Delete a manifest entry; returns {deleted: entryid}.", - "description": "Delete a manifest entry; returns {deleted: entryid}.\n\n**Auth:** jwt + permission:geenforce.manage\n\n**Params:** path: entryid\n\n**Example:**\n```\ncurl -X DELETE -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/entries/17\n```", + "summary": "UNDOCUMENTED: geenforce.get_scope has no docstring.", + "description": "UNDOCUMENTED: geenforce.get_scope has no docstring.\n\n**Auth:** permission:geenforce.view or geenforce.manage\n\n**Params:** path: scopeid (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/geenforce/scopes/1 -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -12949,7 +18430,7 @@ }, "parameters": [ { - "name": "entryid", + "name": "scopeid", "in": "path", "required": true, "schema": { @@ -12957,15 +18438,13 @@ } } ] - } - }, - "/api/geenforce/scopes/{scopeid}/entries/reorder": { + }, "put": { "tags": [ "plugin-geenforce" ], - "summary": "Set entry ordering from an entryid list; 400 unless the list is exactly the set of this scope's entry ids.", - "description": "Set entry ordering from an entryid list; 400 unless the list is exactly the set of this scope's entry ids.\n\n**Auth:** jwt + permission:geenforce.manage\n\n**Params:** path: scopeid; JSON body: order = [entryid, ...] (must match the scope's entry ids exactly)\n\n**Example:**\n```\ncurl -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\n```", + "summary": "UNDOCUMENTED: geenforce.update_scope has no docstring.", + "description": "UNDOCUMENTED: geenforce.update_scope has no docstring.\n\n**Auth:** permission:geenforce.manage\n\n**Params:** path: scopeid (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/geenforce/scopes/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -13015,71 +18494,20 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "path: scopeid; JSON body: order = [entryid, ...] (must match the scope's entry ids exactly)" + "description": "path: scopeid (int); body: JSON (fields not yet documented)" } } } } } }, - "/api/geenforce/scopes/{scopeid}/simulate": { - "get": { - "tags": [ - "plugin-geenforce" - ], - "summary": "Simulate which draft entries would apply to a given machine profile and why the rest are filtered out...", - "description": "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.\n\n**Auth:** jwt + permission:geenforce.manage\n\n**Params:** path: scopeid; query (all optional): pctype (defaults to scopename), subtype, hostname, machinenumber, cmmversion; phase comes from the scope\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $JWT\" 'http://localhost:5001/api/geenforce/scopes/3/simulate?hostname=WKSTN0042&cmmversion=2023.2'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "scopeid", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ] - } - }, "/api/geenforce/scopes/{scopeid}/compliance": { "get": { "tags": [ "plugin-geenforce" ], - "summary": "Fleet install coverage per app-linked entry (installed/version-match counts from the computers plugin's...", - "description": "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).\n\n**Auth:** jwt + permission:geenforce.manage\n\n**Params:** path: scopeid\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/scopes/3/compliance\n```", + "summary": "Fleet-install coverage per app-linked entry (from collected PC data).", + "description": "Fleet-install coverage per app-linked entry (from collected PC data).\n\n**Auth:** permission:geenforce.view or geenforce.manage\n\n**Params:** path: scopeid (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/geenforce/scopes/1/compliance -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -13124,13 +18552,13 @@ ] } }, - "/api/geenforce/entries/{entryid}/payload": { + "/api/geenforce/scopes/{scopeid}/entries": { "post": { "tags": [ "plugin-geenforce" ], - "summary": "Upload an inline payload file (max 1 MB, rejects empty) for an entry and point the entry at it (stores sha256...", - "description": "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.\n\n**Auth:** jwt + permission:geenforce.publish\n\n**Params:** path: entryid; multipart/form-data: file (required)\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $JWT\" -F 'file=@fix.ps1' http://localhost:5001/api/geenforce/entries/17/payload\n```", + "summary": "UNDOCUMENTED: geenforce.create_entry has no docstring.", + "description": "UNDOCUMENTED: geenforce.create_entry has no docstring.\n\n**Auth:** permission:geenforce.manage\n\n**Params:** path: scopeid (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/geenforce/scopes/1/entries -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -13165,7 +18593,7 @@ }, "parameters": [ { - "name": "entryid", + "name": "scopeid", "in": "path", "required": true, "schema": { @@ -13180,18 +18608,20 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "path: entryid; multipart/form-data: file (required)" + "description": "path: scopeid (int); body: JSON (fields not yet documented)" } } } } - }, - "get": { + } + }, + "/api/geenforce/scopes/{scopeid}/entries/reorder": { + "put": { "tags": [ "plugin-geenforce" ], - "summary": "Download the stored inline payload bytes for an entry as an attachment (404 if the entry has no payload).", - "description": "Download the stored inline payload bytes for an entry as an attachment (404 if the entry has no payload).\n\n**Auth:** jwt + permission:geenforce.manage\n\n**Params:** path: entryid\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $JWT\" -o fix.ps1 http://localhost:5001/api/geenforce/entries/17/payload\n```", + "summary": "Set entry order from a list of entryids (the ordering contract).", + "description": "Set entry order from a list of entryids (the ordering contract).\n\n**Auth:** permission:geenforce.manage\n\n**Params:** path: scopeid (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/geenforce/scopes/1/entries/reorder -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -13226,7 +18656,133 @@ }, "parameters": [ { - "name": "entryid", + "name": "scopeid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: scopeid (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/geenforce/scopes/{scopeid}/export-share": { + "post": { + "tags": [ + "plugin-geenforce" + ], + "summary": "Write the scope's current published JSON to the configured share root (backing up the old file to _meta/history first).", + "description": "Write the scope's current published JSON to the configured share root (backing up the old file to _meta/history first).\n\n**Auth:** permission:geenforce.publish\n\n**Params:** path: scopeid (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/geenforce/scopes/1/export-share -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "scopeid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: scopeid (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/geenforce/scopes/{scopeid}/preview": { + "get": { + "tags": [ + "plugin-geenforce" + ], + "summary": "Render the DRAFT manifest JSON a publish would freeze (review before ship).", + "description": "Render the DRAFT manifest JSON a publish would freeze (review before ship).\n\n**Auth:** permission:geenforce.view or geenforce.manage\n\n**Params:** path: scopeid (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/geenforce/scopes/1/preview -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "scopeid", "in": "path", "required": true, "schema": { @@ -13241,8 +18797,8 @@ "tags": [ "plugin-geenforce" ], - "summary": "Freeze the scope's draft into a new published version (service.publish_scope), recording the publishing user from the...", - "description": "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.\n\n**Auth:** jwt + permission:geenforce.publish\n\n**Params:** path: scopeid; JSON body (optional): notes\n\n**Example:**\n```\ncurl -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\n```", + "summary": "UNDOCUMENTED: geenforce.publish_scope_route has no docstring.", + "description": "UNDOCUMENTED: geenforce.publish_scope_route has no docstring.\n\n**Auth:** permission:geenforce.publish\n\n**Params:** path: scopeid (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/geenforce/scopes/1/publish -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -13292,20 +18848,185 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "path: scopeid; JSON body (optional): notes" + "description": "path: scopeid (int); body: JSON (fields not yet documented)" } } } } } }, + "/api/geenforce/scopes/{scopeid}/publish-preflight": { + "get": { + "tags": [ + "plugin-geenforce" + ], + "summary": "Would publishing this scope outrun the fleet's enforcer lib?", + "description": "Would publishing this scope outrun the fleet's enforcer lib?\n\n**Auth:** permission:geenforce.view or geenforce.manage\n\n**Params:** path: scopeid (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/geenforce/scopes/1/publish-preflight -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "scopeid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/geenforce/scopes/{scopeid}/rollback": { + "post": { + "tags": [ + "plugin-geenforce" + ], + "summary": "UNDOCUMENTED: geenforce.rollback_scope_route has no docstring.", + "description": "UNDOCUMENTED: geenforce.rollback_scope_route has no docstring.\n\n**Auth:** permission:geenforce.publish\n\n**Params:** path: scopeid (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/geenforce/scopes/1/rollback -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "scopeid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: scopeid (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/geenforce/scopes/{scopeid}/simulate": { + "get": { + "tags": [ + "plugin-geenforce" + ], + "summary": "Which draft entries apply to a given machine profile, and why the rest are filtered out (reuses the engine-mirror...", + "description": "Which draft entries apply to a given machine profile, and why the rest are filtered out (reuses the engine-mirror filters).\n\n**Auth:** permission:geenforce.view or geenforce.manage\n\n**Params:** path: scopeid (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/geenforce/scopes/1/simulate -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "scopeid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, "/api/geenforce/scopes/{scopeid}/versions": { "get": { "tags": [ "plugin-geenforce" ], - "summary": "List published versions for a scope, newest first (versionnumber, iscurrent, publishedat, publishedby, notes).", - "description": "List published versions for a scope, newest first (versionnumber, iscurrent, publishedat, publishedby, notes).\n\n**Auth:** jwt + permission:geenforce.manage\n\n**Params:** path: scopeid\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/scopes/3/versions\n```", + "summary": "UNDOCUMENTED: geenforce.list_versions has no docstring.", + "description": "UNDOCUMENTED: geenforce.list_versions has no docstring.\n\n**Auth:** permission:geenforce.view or geenforce.manage\n\n**Params:** path: scopeid (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/geenforce/scopes/1/versions -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -13355,8 +19076,8 @@ "tags": [ "plugin-geenforce" ], - "summary": "Fetch one published version's frozen manifest JSON (parsed and returned in the success envelope).", - "description": "Fetch one published version's frozen manifest JSON (parsed and returned in the success envelope).\n\n**Auth:** jwt + permission:geenforce.manage\n\n**Params:** path: scopeid, versionnumber\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/scopes/3/versions/4\n```", + "summary": "UNDOCUMENTED: geenforce.get_version has no docstring.", + "description": "UNDOCUMENTED: geenforce.get_version has no docstring.\n\n**Auth:** permission:geenforce.view or geenforce.manage\n\n**Params:** path: scopeid (int), versionnumber (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/geenforce/scopes/1/versions/1 -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -13409,139 +19130,13 @@ ] } }, - "/api/geenforce/scopes/{scopeid}/rollback": { - "post": { - "tags": [ - "plugin-geenforce" - ], - "summary": "Roll the scope's current published pointer back to an earlier versionnumber (service.rollback_scope); 400 with the...", - "description": "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.\n\n**Auth:** jwt + permission:geenforce.publish\n\n**Params:** path: scopeid; JSON body: versionnumber (required, int)\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $JWT\" -H 'Content-Type: application/json' -d '{\"versionnumber\":3}' http://localhost:5001/api/geenforce/scopes/3/rollback\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "scopeid", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "path: scopeid; JSON body: versionnumber (required, int)" - } - } - } - } - } - }, - "/api/geenforce/scopes/{scopeid}/export-share": { - "post": { - "tags": [ - "plugin-geenforce" - ], - "summary": "Write the scope's current published JSON to the configured share root (geenforce_share_root setting), backing up the...", - "description": "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.\n\n**Auth:** jwt + permission:geenforce.publish\n\n**Params:** path: scopeid; no body\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/scopes/3/export-share\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "scopeid", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "path: scopeid; no body" - } - } - } - } - } - }, - "/api/geenforce/config": { + "/api/guides": { "get": { "tags": [ - "plugin-geenforce" + "plugin-guides" ], - "summary": "Read plugin config: the on-share export root (geenforce_share_root) and the client IP allowlist CIDRs...", - "description": "Read plugin config: the on-share export root (geenforce_share_root) and the client IP allowlist CIDRs (geenforce_allowed_cidrs).\n\n**Auth:** jwt + permission:geenforce.manage\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/config\n```", + "summary": "Published guides, plus the caller's own drafts if they can author.", + "description": "Published guides, plus the caller's own drafts if they can author.\n\n**Auth:** permission:guides.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/guides -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -13572,12 +19167,12 @@ } } }, - "put": { + "post": { "tags": [ - "plugin-geenforce" + "plugin-guides" ], - "summary": "Update plugin config settings; allowedcidrs is validated/normalized (comma/newline-separated CIDRs or bare IPs, 400...", - "description": "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.\n\n**Auth:** jwt + permission:geenforce.publish\n\n**Params:** JSON body (both optional): shareroot (string path), allowedcidrs (CSV/newline CIDR list)\n\n**Example:**\n```\ncurl -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\n```", + "summary": "UNDOCUMENTED: guides.create_guide has no docstring.", + "description": "UNDOCUMENTED: guides.create_guide has no docstring.\n\n**Auth:** permission:guides.create\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/guides -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -13614,58 +19209,20 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "JSON body (both optional): shareroot (string path), allowedcidrs (CSV/newline CIDR list)" + "description": "body: JSON (fields not yet documented)" } } } } } }, - "/api/geenforce/reports": { - "get": { + "/api/guides/{guideid}": { + "delete": { "tags": [ - "plugin-geenforce" + "plugin-guides" ], - "summary": "Latest enforcement report per PC (iscurrent rows) for the fleet compliance view: applied vs latest published version...", - "description": "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.\n\n**Auth:** jwt + permission:geenforce.manage\n\n**Params:** query (optional): hostname (ILIKE match), scopename (exact)\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $JWT\" 'http://localhost:5001/api/geenforce/reports?scopename=cmm'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - } - }, - "/api/geenforce/reports/{reportid}": { - "get": { - "tags": [ - "plugin-geenforce" - ], - "summary": "One enforcement report in detail with per-entry outcomes (entryname, action, selfhealed, exitcode, message) plus...", - "description": "One enforcement report in detail with per-entry outcomes (entryname, action, selfhealed, exitcode, message) plus applied-vs-latest version comparison.\n\n**Auth:** jwt + permission:geenforce.manage\n\n**Params:** path: reportid\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $JWT\" http://localhost:5001/api/geenforce/reports/42\n```", + "summary": "UNDOCUMENTED: guides.delete_guide has no docstring.", + "description": "UNDOCUMENTED: guides.delete_guide has no docstring.\n\n**Auth:** permission:guides.delete\n\n**Params:** path: guideid (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/guides/1 -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -13700,7 +19257,253 @@ }, "parameters": [ { - "name": "reportid", + "name": "guideid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "plugin-guides" + ], + "summary": "UNDOCUMENTED: guides.update_guide has no docstring.", + "description": "UNDOCUMENTED: guides.update_guide has no docstring.\n\n**Auth:** permission:guides.view\n\n**Params:** path: guideid (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/guides/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "guideid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: guideid (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/guides/{guideid}/publish": { + "post": { + "tags": [ + "plugin-guides" + ], + "summary": "UNDOCUMENTED: guides.publish_guide has no docstring.", + "description": "UNDOCUMENTED: guides.publish_guide has no docstring.\n\n**Auth:** permission:guides.publish\n\n**Params:** path: guideid (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/guides/1/publish -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "guideid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: guideid (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/guides/{guideid}/restore/{revisionid}": { + "post": { + "tags": [ + "plugin-guides" + ], + "summary": "UNDOCUMENTED: guides.restore_revision has no docstring.", + "description": "UNDOCUMENTED: guides.restore_revision has no docstring.\n\n**Auth:** permission:guides.view\n\n**Params:** path: guideid (int), revisionid (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/guides/1/restore/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "guideid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "revisionid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: guideid (int), revisionid (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/guides/{guideid}/revisions": { + "get": { + "tags": [ + "plugin-guides" + ], + "summary": "UNDOCUMENTED: guides.list_revisions has no docstring.", + "description": "UNDOCUMENTED: guides.list_revisions has no docstring.\n\n**Auth:** permission:guides.view\n\n**Params:** path: guideid (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/guides/1/revisions -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "guideid", "in": "path", "required": true, "schema": { @@ -13710,51 +19513,13 @@ ] } }, - "/api/geenforce/dashboard/failures": { - "get": { + "/api/guides/{guideid}/unpublish": { + "post": { "tags": [ - "plugin-geenforce" + "plugin-guides" ], - "summary": "Dashboard card: entries that FAILED on their PC's most recent enforcement cycle, host and entry named", - "description": "Dashboard card: entries that FAILED on their PC's most recent enforcement cycle, host and entry named\n\n**Auth:** permission:geenforce.manage\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/geenforce/dashboard/failures\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - } - }, - "/api/geenforce/scopes/{scopeid}/publish-preflight": { - "get": { - "tags": [ - "plugin-geenforce" - ], - "summary": "Would publishing this scope outrun the fleet's enforcer library? Returns the required version, the lowest version...", - "description": "Would publishing this scope outrun the fleet's enforcer library? Returns the required version, the lowest version reporting, the hosts behind it, and whether publish would be refused. The publish itself enforces the same gate; this is how the UI warns first\n\n**Auth:** permission:geenforce.manage\n\n**Params:** scopeid in path\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/geenforce/scopes/3/publish-preflight\n```", + "summary": "UNDOCUMENTED: guides.unpublish_guide has no docstring.", + "description": "UNDOCUMENTED: guides.unpublish_guide has no docstring.\n\n**Auth:** permission:guides.publish\n\n**Params:** path: guideid (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/guides/1/unpublish -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -13789,7 +19554,121 @@ }, "parameters": [ { - "name": "scopeid", + "name": "guideid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: guideid (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/guides/{guideid}/working": { + "get": { + "tags": [ + "plugin-guides" + ], + "summary": "The working copy, for the editor.", + "description": "The working copy, for the editor.\n\n**Auth:** permission:guides.view\n\n**Params:** path: guideid (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/guides/1/working -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "guideid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/guides/{slug}": { + "get": { + "tags": [ + "plugin-guides" + ], + "summary": "What a reader gets: the published revision.", + "description": "What a reader gets: the published revision.\n\n**Auth:** permission:guides.view\n\n**Params:** path: slug (string)\n\n**Example:**\n```\ncurl http://localhost:5001/api/guides/x -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "slug", "in": "path", "required": true, "schema": { @@ -13799,39 +19678,13 @@ ] } }, - "/api/notifications/types": { - "get": { - "tags": [ - "plugin-notifications" - ], - "summary": "List notification types, paginated, active-only by default", - "description": "List notification types, paginated, active-only by default\n\n**Auth:** none\n\n**Params:** page, per_page; active=false to include inactive types\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/notifications/types?active=false&page=1&per_page=50'\n```", - "security": [], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - }, + "/api/guides/images": { "post": { "tags": [ - "plugin-notifications" + "plugin-guides" ], - "summary": "Create a notification type incl. expiry rule and shopfloor display config", - "description": "Create a notification type incl. expiry rule and shopfloor display config\n\n**Auth:** jwt + permission:notifications.create\n\n**Params:** body: typename (required, unique), typedescription/description, typecolor/color, expirymode (none|duration|dailytime), expirydays, expiryhour, expiryminute, splitperemployee, showemployeephoto, displaystyle (standard|carousel|grid|banner)\n\n**Example:**\n```\ncurl -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}'\n```", + "summary": "Take a pasted or dropped image.", + "description": "Take a pasted or dropped image.\n\n**Auth:** permission:guides.create\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/guides/images -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -13868,20 +19721,1066 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "body: typename (required, unique), typedescription/description, typecolor/color, expirymode (none|duration|dailytime), expirydays, expiryhour, expiryminute, splitperemployee, showemployeephoto, displaystyle (standard|carousel|grid|banner)" + "description": "body: JSON (fields not yet documented)" } } } } } }, - "/api/notifications/types/{type_id}": { + "/api/guides/images/{imageid}": { + "get": { + "tags": [ + "plugin-guides" + ], + "summary": "The image itself, under the same rule as the guide that holds it.", + "description": "The image itself, under the same rule as the guide that holds it.\n\n**Auth:** permission:guides.view\n\n**Params:** path: imageid (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/guides/images/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "imageid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/guides/revisions/{revisionid}": { + "get": { + "tags": [ + "plugin-guides" + ], + "summary": "One revision by id.", + "description": "One revision by id.\n\n**Auth:** permission:guides.view\n\n**Params:** path: revisionid (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/guides/revisions/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "revisionid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/guides/tags": { + "get": { + "tags": [ + "plugin-guides" + ], + "summary": "Every tag in use, with a count, for the filter chips.", + "description": "Every tag in use, with a count, for the filter chips.\n\n**Auth:** permission:guides.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/guides/tags -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/knowledgebase": { + "get": { + "tags": [ + "plugin-knowledgebase" + ], + "summary": "List all knowledge base articles.", + "description": "List all knowledge base articles.\n\n**Auth:** permission:kb.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/knowledgebase -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "post": { + "tags": [ + "plugin-knowledgebase" + ], + "summary": "Create a new knowledge base article.", + "description": "Create a new knowledge base article.\n\n**Auth:** permission:kb.create\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/knowledgebase -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/knowledgebase/{link_id}": { + "delete": { + "tags": [ + "plugin-knowledgebase" + ], + "summary": "Delete (deactivate) a knowledge base article.", + "description": "Delete (deactivate) a knowledge base article.\n\n**Auth:** permission:kb.delete\n\n**Params:** path: link_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/knowledgebase/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "link_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "get": { + "tags": [ + "plugin-knowledgebase" + ], + "summary": "Get a single knowledge base article.", + "description": "Get a single knowledge base article.\n\n**Auth:** permission:kb.view\n\n**Params:** path: link_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/knowledgebase/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "link_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, "put": { "tags": [ - "plugin-notifications" + "plugin-knowledgebase" ], - "summary": "Update a notification type (name/desc/color/isactive plus expiry and display fields)", - "description": "Update a notification type (name/desc/color/isactive plus expiry and display fields)\n\n**Auth:** jwt + permission:notifications.create\n\n**Params:** body: any of typename (unique-checked), typedescription/description, typecolor/color, isactive, expirymode, expirydays, expiryhour, expiryminute, splitperemployee, showemployeephoto, displaystyle\n\n**Example:**\n```\ncurl -X PATCH http://localhost:5001/api/notifications/types/3 -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"expirymode\":\"duration\",\"expirydays\":14}'\n```", + "summary": "Update a knowledge base article.", + "description": "Update a knowledge base article.\n\n**Auth:** permission:kb.edit\n\n**Params:** path: link_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/knowledgebase/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "link_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: link_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/knowledgebase/{link_id}/click": { + "post": { + "tags": [ + "plugin-knowledgebase" + ], + "summary": "Increment the click counter for an article the caller is allowed to read.", + "description": "Increment the click counter for an article the caller is allowed to read.\n\n**Auth:** permission:kb.view\n\n**Params:** path: link_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/knowledgebase/1/click -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "link_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: link_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/knowledgebase/stats": { + "get": { + "tags": [ + "plugin-knowledgebase" + ], + "summary": "Get knowledge base statistics.", + "description": "Get knowledge base statistics.\n\n**Auth:** permission:kb.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/knowledgebase/stats -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/machines": { + "get": { + "tags": [ + "plugin-machines" + ], + "summary": "List all machines with filtering and pagination.", + "description": "List all machines with filtering and pagination.\n\n**Auth:** permission:machines.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/machines -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "post": { + "tags": [ + "plugin-machines" + ], + "summary": "Create new machine (creates both Asset and Machine records).", + "description": "Create new machine (creates both Asset and Machine records).\n\n**Auth:** permission:machines.create\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/machines -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/machines/{machine_id}": { + "delete": { + "tags": [ + "plugin-machines" + ], + "summary": "Delete (soft delete) machine.", + "description": "Delete (soft delete) machine.\n\n**Auth:** permission:machines.delete\n\n**Params:** path: machine_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/machines/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "machine_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "get": { + "tags": [ + "plugin-machines" + ], + "summary": "Get a single machine item with full details.", + "description": "Get a single machine item with full details.\n\n**Auth:** permission:machines.view\n\n**Params:** path: machine_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/machines/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "machine_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "plugin-machines" + ], + "summary": "Update machine (both Asset and Machine records).", + "description": "Update machine (both Asset and Machine records).\n\n**Auth:** permission:machines.edit\n\n**Params:** path: machine_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/machines/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "machine_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: machine_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/machines/by-asset/{asset_id}": { + "get": { + "tags": [ + "plugin-machines" + ], + "summary": "Get machine data by asset ID.", + "description": "Get machine data by asset ID.\n\n**Auth:** permission:machines.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/machines/by-asset/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/machines/dashboard/outofservice": { + "get": { + "tags": [ + "plugin-machines" + ], + "summary": "Machines whose status says they are not running.", + "description": "Machines whose status says they are not running.\n\n**Auth:** permission:machines.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/machines/dashboard/outofservice -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/machines/dashboard/summary": { + "get": { + "tags": [ + "plugin-machines" + ], + "summary": "Get machine dashboard summary data.", + "description": "Get machine dashboard summary data.\n\n**Auth:** permission:machines.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/machines/dashboard/summary -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/machines/types": { + "get": { + "tags": [ + "plugin-machines" + ], + "summary": "List all machine types.", + "description": "List all machine types.\n\n**Auth:** permission:machines.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/machines/types -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "post": { + "tags": [ + "plugin-machines" + ], + "summary": "Create a new machine type.", + "description": "Create a new machine type.\n\n**Auth:** permission:machines.create\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/machines/types -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/machines/types/{type_id}": { + "delete": { + "tags": [ + "plugin-machines" + ], + "summary": "Delete a machine type. Refused if any asset still uses it.", + "description": "Delete a machine type. Refused if any asset still uses it.\n\n**Auth:** permission:machines.delete\n\n**Params:** path: type_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/machines/types/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "type_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "get": { + "tags": [ + "plugin-machines" + ], + "summary": "Get a single machine type.", + "description": "Get a single machine type.\n\n**Auth:** permission:machines.view\n\n**Params:** path: type_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/machines/types/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "type_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "plugin-machines" + ], + "summary": "Update a machine type.", + "description": "Update a machine type.\n\n**Auth:** permission:machines.edit\n\n**Params:** path: type_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/machines/types/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -13931,18 +20830,2323 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "body: any of typename (unique-checked), typedescription/description, typecolor/color, isactive, expirymode, expirydays, expiryhour, expiryminute, splitperemployee, showemployeephoto, displaystyle" + "description": "path: type_id (int); body: JSON (fields not yet documented)" } } } } + } + }, + "/api/measuringtools": { + "get": { + "tags": [ + "plugin-measuringtools" + ], + "summary": "List measuring tools with filters + pagination.", + "description": "List measuring tools with filters + pagination.\n\n**Auth:** permission:measuringtools.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/measuringtools -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } }, + "post": { + "tags": [ + "plugin-measuringtools" + ], + "summary": "Create a measuring tool: one Asset core row + one extension row.", + "description": "Create a measuring tool: one Asset core row + one extension row.\n\n**Auth:** permission:measuringtools.create\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/measuringtools -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/measuringtools/{tool_id}": { + "delete": { + "tags": [ + "plugin-measuringtools" + ], + "summary": "Soft delete a measuring tool (deactivates its asset).", + "description": "Soft delete a measuring tool (deactivates its asset).\n\n**Auth:** permission:measuringtools.delete\n\n**Params:** path: tool_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/measuringtools/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "tool_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "get": { + "tags": [ + "plugin-measuringtools" + ], + "summary": "Get one measuring tool (asset core merged with the extension).", + "description": "Get one measuring tool (asset core merged with the extension).\n\n**Auth:** permission:measuringtools.view\n\n**Params:** path: tool_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/measuringtools/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "tool_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "plugin-measuringtools" + ], + "summary": "Update a measuring tool (asset core fields + extension in one payload).", + "description": "Update a measuring tool (asset core fields + extension in one payload).\n\n**Auth:** permission:measuringtools.edit\n\n**Params:** path: tool_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/measuringtools/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "tool_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: tool_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/measuringtools/by-asset/{asset_id}": { + "get": { + "tags": [ + "plugin-measuringtools" + ], + "summary": "Get a measuring tool by its core asset id.", + "description": "Get a measuring tool by its core asset id.\n\n**Auth:** permission:measuringtools.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/measuringtools/by-asset/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/measuringtools/map-overlay": { + "get": { + "tags": [ + "plugin-measuringtools" + ], + "summary": "Calibration-status overlay for active measuring tools.", + "description": "Calibration-status overlay for active measuring tools.\n\n**Auth:** permission:measuringtools.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/measuringtools/map-overlay -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/measuringtools/report/calibration": { + "get": { + "tags": [ + "plugin-measuringtools" + ], + "summary": "Counts + lists bucketed by derived calibration status.", + "description": "Counts + lists bucketed by derived calibration status.\n\n**Auth:** permission:measuringtools.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/measuringtools/report/calibration -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/measuringtools/types": { + "get": { + "tags": [ + "plugin-measuringtools" + ], + "summary": "List measuring-tool types.", + "description": "List measuring-tool types.\n\n**Auth:** permission:measuringtools.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/measuringtools/types -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "post": { + "tags": [ + "plugin-measuringtools" + ], + "summary": "Create a measuring-tool type (reactivates a soft-deleted same-named one).", + "description": "Create a measuring-tool type (reactivates a soft-deleted same-named one).\n\n**Auth:** permission:measuringtools.create\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/measuringtools/types -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/measuringtools/types/{type_id}": { + "delete": { + "tags": [ + "plugin-measuringtools" + ], + "summary": "Delete a measuring-tool type. Refused if any tool still uses it.", + "description": "Delete a measuring-tool type. Refused if any tool still uses it.\n\n**Auth:** permission:measuringtools.delete\n\n**Params:** path: type_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/measuringtools/types/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "type_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "get": { + "tags": [ + "plugin-measuringtools" + ], + "summary": "Get one measuring-tool type.", + "description": "Get one measuring-tool type.\n\n**Auth:** permission:measuringtools.view\n\n**Params:** path: type_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/measuringtools/types/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "type_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "plugin-measuringtools" + ], + "summary": "Update a measuring-tool type.", + "description": "Update a measuring-tool type.\n\n**Auth:** permission:measuringtools.edit\n\n**Params:** path: type_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/measuringtools/types/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "type_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: type_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/network": { + "get": { + "tags": [ + "plugin-network" + ], + "summary": "List all network devices with filtering and pagination.", + "description": "List all network devices with filtering and pagination.\n\n**Auth:** permission:network.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/network -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "post": { + "tags": [ + "plugin-network" + ], + "summary": "Create new network device (creates both Asset and NetworkDevice records).", + "description": "Create new network device (creates both Asset and NetworkDevice records).\n\n**Auth:** permission:network.create\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/network -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/network/{device_id}": { + "delete": { + "tags": [ + "plugin-network" + ], + "summary": "Delete (soft delete) network device.", + "description": "Delete (soft delete) network device.\n\n**Auth:** permission:network.delete\n\n**Params:** path: device_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/network/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "device_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "get": { + "tags": [ + "plugin-network" + ], + "summary": "Get a single network device with full details.", + "description": "Get a single network device with full details.\n\n**Auth:** permission:network.view\n\n**Params:** path: device_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/network/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "device_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "plugin-network" + ], + "summary": "Update network device (both Asset and NetworkDevice records).", + "description": "Update network device (both Asset and NetworkDevice records).\n\n**Auth:** permission:network.edit\n\n**Params:** path: device_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/network/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "device_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: device_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/network/by-asset/{asset_id}": { + "get": { + "tags": [ + "plugin-network" + ], + "summary": "Get network device data by asset ID.", + "description": "Get network device data by asset ID.\n\n**Auth:** permission:network.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/network/by-asset/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/network/by-hostname/{hostname}": { + "get": { + "tags": [ + "plugin-network" + ], + "summary": "Get network device by hostname.", + "description": "Get network device by hostname.\n\n**Auth:** permission:network.view\n\n**Params:** path: hostname (string)\n\n**Example:**\n```\ncurl http://localhost:5001/api/network/by-hostname/x -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "hostname", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/network/dashboard/summary": { + "get": { + "tags": [ + "plugin-network" + ], + "summary": "Get network device dashboard summary data.", + "description": "Get network device dashboard summary data.\n\n**Auth:** permission:network.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/network/dashboard/summary -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/network/subnets": { + "get": { + "tags": [ + "plugin-network" + ], + "summary": "List all subnets with filtering and pagination.", + "description": "List all subnets with filtering and pagination.\n\n**Auth:** permission:network.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/network/subnets -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "post": { + "tags": [ + "plugin-network" + ], + "summary": "Create a new subnet.", + "description": "Create a new subnet.\n\n**Auth:** permission:network.create\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/network/subnets -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/network/subnets/{subnet_id}": { + "delete": { + "tags": [ + "plugin-network" + ], + "summary": "Delete (soft delete) a subnet.", + "description": "Delete (soft delete) a subnet.\n\n**Auth:** permission:network.delete\n\n**Params:** path: subnet_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/network/subnets/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "subnet_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "get": { + "tags": [ + "plugin-network" + ], + "summary": "Get a single subnet plus the network devices whose primary IP falls in its CIDR range.", + "description": "Get a single subnet plus the network devices whose primary IP falls in its CIDR range.\n\n**Auth:** permission:network.view\n\n**Params:** path: subnet_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/network/subnets/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "subnet_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "plugin-network" + ], + "summary": "Update a subnet.", + "description": "Update a subnet.\n\n**Auth:** permission:network.edit\n\n**Params:** path: subnet_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/network/subnets/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "subnet_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: subnet_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/network/types": { + "get": { + "tags": [ + "plugin-network" + ], + "summary": "List all network device types.", + "description": "List all network device types.\n\n**Auth:** permission:network.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/network/types -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "post": { + "tags": [ + "plugin-network" + ], + "summary": "Create a new network device type.", + "description": "Create a new network device type.\n\n**Auth:** permission:network.create\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/network/types -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/network/types/{type_id}": { + "delete": { + "tags": [ + "plugin-network" + ], + "summary": "Delete a network device type. Refused if any device still uses it.", + "description": "Delete a network device type. Refused if any device still uses it.\n\n**Auth:** permission:network.delete\n\n**Params:** path: type_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/network/types/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "type_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "get": { + "tags": [ + "plugin-network" + ], + "summary": "Get a single network device type.", + "description": "Get a single network device type.\n\n**Auth:** permission:network.view\n\n**Params:** path: type_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/network/types/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "type_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "plugin-network" + ], + "summary": "Update a network device type.", + "description": "Update a network device type.\n\n**Auth:** permission:network.edit\n\n**Params:** path: type_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/network/types/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "type_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: type_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/network/vlans": { + "get": { + "tags": [ + "plugin-network" + ], + "summary": "List all VLANs with filtering and pagination.", + "description": "List all VLANs with filtering and pagination.\n\n**Auth:** permission:network.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/network/vlans -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "post": { + "tags": [ + "plugin-network" + ], + "summary": "Create a new VLAN.", + "description": "Create a new VLAN.\n\n**Auth:** permission:network.create\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/network/vlans -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/network/vlans/{vlan_id}": { + "delete": { + "tags": [ + "plugin-network" + ], + "summary": "Delete (soft delete) a VLAN.", + "description": "Delete (soft delete) a VLAN.\n\n**Auth:** permission:network.delete\n\n**Params:** path: vlan_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/network/vlans/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "vlan_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "get": { + "tags": [ + "plugin-network" + ], + "summary": "Get a single VLAN with its subnets.", + "description": "Get a single VLAN with its subnets.\n\n**Auth:** permission:network.view\n\n**Params:** path: vlan_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/network/vlans/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "vlan_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "plugin-network" + ], + "summary": "Update a VLAN.", + "description": "Update a VLAN.\n\n**Auth:** permission:network.edit\n\n**Params:** path: vlan_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/network/vlans/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "vlan_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: vlan_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/notifications": { + "get": { + "tags": [ + "plugin-notifications" + ], + "summary": "List all notifications with filtering and pagination.", + "description": "List all notifications with filtering and pagination.\n\n**Auth:** permission:notifications.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/notifications -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "post": { + "tags": [ + "plugin-notifications" + ], + "summary": "Create a new notification.", + "description": "Create a new notification.\n\n**Auth:** permission:notifications.create\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/notifications -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/notifications/{notification_id}": { + "delete": { + "tags": [ + "plugin-notifications" + ], + "summary": "Delete (soft delete) a notification.", + "description": "Delete (soft delete) a notification.\n\n**Auth:** permission:notifications.delete\n\n**Params:** path: notification_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/notifications/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "notification_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "get": { + "tags": [ + "plugin-notifications" + ], + "summary": "Get a single notification.", + "description": "Get a single notification.\n\n**Auth:** permission:notifications.view\n\n**Params:** path: notification_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/notifications/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "notification_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "plugin-notifications" + ], + "summary": "Update a notification.", + "description": "Update a notification.\n\n**Auth:** permission:notifications.edit\n\n**Params:** path: notification_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/notifications/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "notification_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: notification_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/notifications/active": { + "get": { + "tags": [ + "plugin-notifications" + ], + "summary": "Get currently active notifications for display.", + "description": "Get currently active notifications for display.\n\n**Auth:** permission:notifications.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/notifications/active -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/notifications/calendar": { + "get": { + "tags": [ + "plugin-notifications" + ], + "summary": "Get notifications in FullCalendar event format.", + "description": "Get notifications in FullCalendar event format.\n\n**Auth:** permission:notifications.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/notifications/calendar -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/notifications/dashboard/active": { + "get": { + "tags": [ + "plugin-notifications" + ], + "summary": "Notifications currently showing on the floor.", + "description": "Notifications currently showing on the floor.\n\n**Auth:** permission:notifications.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/notifications/dashboard/active -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/notifications/dashboard/summary": { + "get": { + "tags": [ + "plugin-notifications" + ], + "summary": "Get notifications dashboard summary.", + "description": "Get notifications dashboard summary.\n\n**Auth:** permission:notifications.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/notifications/dashboard/summary -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/notifications/employee/{sso}": { + "get": { + "tags": [ + "plugin-notifications" + ], + "summary": "Get recognitions for a specific employee by SSO.", + "description": "Get recognitions for a specific employee by SSO.\n\n**Auth:** none (reviewed: named in the public-read or mutation sweep)\n\n**Params:** path: sso (string)\n\n**Example:**\n```\ncurl http://localhost:5001/api/notifications/employee/x\n```", + "security": [], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "sso", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/notifications/shopfloor": { + "get": { + "tags": [ + "plugin-notifications" + ], + "summary": "Get notifications for shopfloor TV dashboard.", + "description": "Get notifications for shopfloor TV dashboard.\n\n**Auth:** none (reviewed: named in the public-read or mutation sweep)\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/notifications/shopfloor\n```", + "security": [], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + }, + "/api/notifications/types": { + "get": { + "tags": [ + "plugin-notifications" + ], + "summary": "List all notification types.", + "description": "List all notification types.\n\n**Auth:** permission:notifications.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/notifications/types -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "post": { + "tags": [ + "plugin-notifications" + ], + "summary": "Create a new notification type.", + "description": "Create a new notification type.\n\n**Auth:** permission:notifications.create\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/notifications/types -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/notifications/types/{type_id}": { "patch": { "tags": [ "plugin-notifications" ], - "summary": "Update a notification type (name/desc/color/isactive plus expiry and display fields)", - "description": "Update a notification type (name/desc/color/isactive plus expiry and display fields)\n\n**Auth:** jwt + permission:notifications.create\n\n**Params:** body: any of typename (unique-checked), typedescription/description, typecolor/color, isactive, expirymode, expirydays, expiryhour, expiryminute, splitperemployee, showemployeephoto, displaystyle\n\n**Example:**\n```\ncurl -X PATCH http://localhost:5001/api/notifications/types/3 -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"expirymode\":\"duration\",\"expirydays\":14}'\n```", + "summary": "Update a notification type, including its auto-expiry rule.", + "description": "Update a notification type, including its auto-expiry rule.\n\n**Auth:** permission:notifications.create\n\n**Params:** path: type_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PATCH http://localhost:5001/api/notifications/types/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -13992,135 +23196,18 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "body: any of typename (unique-checked), typedescription/description, typecolor/color, isactive, expirymode, expirydays, expiryhour, expiryminute, splitperemployee, showemployeephoto, displaystyle" + "description": "path: type_id (int); body: JSON (fields not yet documented)" } } } } - } - }, - "/api/notifications": { - "get": { - "tags": [ - "plugin-notifications" - ], - "summary": "List notifications with filters, newest-first, paginated", - "description": "List notifications with filters, newest-first, paginated\n\n**Auth:** none\n\n**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)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/notifications?current=true&typeid=2&search=outage&page=1'\n```", - "security": [], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - }, - "post": { - "tags": [ - "plugin-notifications" - ], - "summary": "Create a notification; endtime auto-derived from type expiry rule when omitted", - "description": "Create a notification; endtime auto-derived from type expiry rule when omitted\n\n**Auth:** jwt + permission:notifications.create\n\n**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\n\n**Example:**\n```\ncurl -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\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "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" - } - } - } - } - } - }, - "/api/notifications/{notification_id}": { - "get": { - "tags": [ - "plugin-notifications" - ], - "summary": "Get a single notification by ID", - "description": "Get a single notification by ID\n\n**Auth:** none\n\n**Params:** path: notification_id\n\n**Example:**\n```\ncurl http://localhost:5001/api/notifications/42\n```", - "security": [], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "notification_id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ] }, "put": { "tags": [ "plugin-notifications" ], - "summary": "Update any notification field; empty starttime resets to now, empty endtime clears it", - "description": "Update any notification field; empty starttime resets to now, empty endtime clears it\n\n**Auth:** jwt + permission:notifications.edit\n\n**Params:** body: notification/message, notificationtypeid, businessunitid, appid, ticketnumber, link/linkurl, isactive, isshopfloor, employeesso, employeename, starttime/startdate, endtime/enddate\n\n**Example:**\n```\ncurl -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}'\n```", + "summary": "Update a notification type, including its auto-expiry rule.", + "description": "Update a notification type, including its auto-expiry rule.\n\n**Auth:** permission:notifications.create\n\n**Params:** path: type_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PATCH http://localhost:5001/api/notifications/types/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -14155,7 +23242,7 @@ }, "parameters": [ { - "name": "notification_id", + "name": "type_id", "in": "path", "required": true, "schema": { @@ -14170,18 +23257,20 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "body: notification/message, notificationtypeid, businessunitid, appid, ticketnumber, link/linkurl, isactive, isshopfloor, employeesso, employeename, starttime/startdate, endtime/enddate" + "description": "path: type_id (int); body: JSON (fields not yet documented)" } } } } - }, + } + }, + "/api/printedparts/files/{file_id}": { "delete": { "tags": [ - "plugin-notifications" + "plugin-printedparts" ], - "summary": "Soft-delete a notification (sets isactive=false, row kept)", - "description": "Soft-delete a notification (sets isactive=false, row kept)\n\n**Auth:** jwt + permission:notifications.delete\n\n**Params:** path: notification_id\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/notifications/42 -H \"Authorization: Bearer $TOKEN\"\n```", + "summary": "Remove a bad revision (wrong file uploaded). History otherwise stays.", + "description": "Remove a bad revision (wrong file uploaded). History otherwise stays.\n\n**Auth:** permission:printedparts.delete\n\n**Params:** path: file_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/printedparts/files/1 -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -14216,7 +23305,7 @@ }, "parameters": [ { - "name": "notification_id", + "name": "file_id", "in": "path", "required": true, "schema": { @@ -14226,166 +23315,13 @@ ] } }, - "/api/notifications/active": { + "/api/printedparts/files/{file_id}/download": { "get": { "tags": [ - "plugin-notifications" + "plugin-printedparts" ], - "summary": "Currently active notifications for display, incl. ones starting within a 10-day lookahead", - "description": "Currently active notifications for display, incl. ones starting within a 10-day lookahead\n\n**Auth:** none\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/notifications/active\n```", - "security": [], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/notifications/calendar": { - "get": { - "tags": [ - "plugin-notifications" - ], - "summary": "Active notifications as FullCalendar event objects for a date range", - "description": "Active notifications as FullCalendar event objects for a date range\n\n**Auth:** none\n\n**Params:** start (ISO), end (ISO); invalid dates silently ignored\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/notifications/calendar?start=2026-07-01T00:00:00Z&end=2026-07-31T23:59:59Z'\n```", - "security": [], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/notifications/dashboard/summary": { - "get": { - "tags": [ - "plugin-notifications" - ], - "summary": "Dashboard counts: total currently-active notifications plus active counts grouped by type/color", - "description": "Dashboard counts: total currently-active notifications plus active counts grouped by type/color\n\n**Auth:** none\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/notifications/dashboard/summary\n```", - "security": [], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/notifications/employee/{sso}": { - "get": { - "tags": [ - "plugin-notifications" - ], - "summary": "All active recognition-type notifications mentioning an employee SSO (exact or within comma-separated employeesso list)", - "description": "All active recognition-type notifications mentioning an employee SSO (exact or within comma-separated employeesso list)\n\n**Auth:** none\n\n**Params:** path: sso (digits only, 400 otherwise)\n\n**Example:**\n```\ncurl http://localhost:5001/api/notifications/employee/212345678\n```", - "security": [], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "sso", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/notifications/shopfloor": { - "get": { - "tags": [ - "plugin-notifications" - ], - "summary": "Shopfloor TV feed: current cards (active now, or ended <30 min ago flagged resolved) + upcoming (starts within 5 days)...", - "description": "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\n\n**Auth:** none\n\n**Params:** businessunit (numeric BU id: returns that BU's plus null-BU notifications; omitted: null-BU only)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/notifications/shopfloor?businessunit=3'\n```", - "security": [], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/notifications/dashboard/active": { - "get": { - "tags": [ - "plugin-notifications" - ], - "summary": "Dashboard card: the notifications currently showing on the shop-floor board, so the office can see what the floor sees", - "description": "Dashboard card: the notifications currently showing on the shop-floor board, so the office can see what the floor sees\n\n**Auth:** jwt\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/notifications/dashboard/active\n```", + "summary": "Download a revision under its original filename.", + "description": "Download a revision under its original filename.\n\n**Auth:** permission:printedparts.view\n\n**Params:** path: file_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/printedparts/files/1/download -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -14413,45 +23349,30 @@ }, "403": { "description": "Authenticated, but not permitted." - } - } - } - }, - "/api/slides/feed": { - "get": { - "tags": [ - "plugin-slides" - ], - "summary": "Public flat playlist for a surface (lobby/shopfloor); raw jsonify, not the success_response envelope, so the...", - "description": "Public flat playlist for a surface (lobby/shopfloor); raw jsonify, not the success_response envelope, so the screensaver parser works unchanged\n\n**Auth:** none\n\n**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\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/slides/feed?surface=shopfloor'\n```", - "security": [], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "file_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" } } - } + ] } }, - "/api/slides/img/{surface}/{filename}": { + "/api/printedparts/image/{filename}": { "get": { "tags": [ - "plugin-slides" + "plugin-printedparts" ], - "summary": "Public serving of a single slide image from instance/slides//, with basename path-traversal guard; 404 on...", - "description": "Public serving of a single slide image from instance/slides//, with basename path-traversal guard; 404 on unknown surface, traversal attempt, or missing file\n\n**Auth:** none\n\n**Params:** path: surface (lobby|shopfloor), filename (must equal its basename)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/slides/img/lobby/Slide1.png' -o Slide1.png\n```", + "summary": "Serve an uploaded item image (public - kiosk and list read it).", + "description": "Serve an uploaded item image (public - kiosk and list read it).\n\n**Auth:** none (reviewed: named in the public-read or mutation sweep)\n\n**Params:** path: filename (path)\n\n**Example:**\n```\ncurl http://localhost:5001/api/printedparts/image/example.png\n```", "security": [], "responses": { "200": { @@ -14475,14 +23396,6 @@ } }, "parameters": [ - { - "name": "surface", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, { "name": "filename", "in": "path", @@ -14495,13 +23408,2672 @@ ] } }, + "/api/printedparts/items": { + "get": { + "tags": [ + "plugin-printedparts" + ], + "summary": "List printed items, paginated; search + low-stock filter.", + "description": "List printed items, paginated; search + low-stock filter.\n\n**Auth:** permission:printedparts.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/printedparts/items -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "post": { + "tags": [ + "plugin-printedparts" + ], + "summary": "Create a printed item.", + "description": "Create a printed item.\n\n**Auth:** permission:printedparts.create\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/printedparts/items -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/printedparts/items/{item_id}": { + "delete": { + "tags": [ + "plugin-printedparts" + ], + "summary": "Soft-retire an item; its ledger history stays.", + "description": "Soft-retire an item; its ledger history stays.\n\n**Auth:** permission:printedparts.delete\n\n**Params:** path: item_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/printedparts/items/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "get": { + "tags": [ + "plugin-printedparts" + ], + "summary": "Get one printed item with its recent transactions.", + "description": "Get one printed item with its recent transactions.\n\n**Auth:** permission:printedparts.view\n\n**Params:** path: item_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/printedparts/items/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "plugin-printedparts" + ], + "summary": "Update catalog fields. Quantity moves ONLY through the ledger.", + "description": "Update catalog fields. Quantity moves ONLY through the ledger.\n\n**Auth:** permission:printedparts.edit\n\n**Params:** path: item_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/printedparts/items/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: item_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/printedparts/items/{item_id}/adjust": { + "post": { + "tags": [ + "plugin-printedparts" + ], + "summary": "Correct the count (damage, recount). Body: {quantitychange, reason, badge}.", + "description": "Correct the count (damage, recount). Body: {quantitychange, reason, badge}.\n\n**Auth:** permission:printedparts.restock\n\n**Params:** path: item_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/printedparts/items/1/adjust -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: item_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/printedparts/items/{item_id}/files": { + "get": { + "tags": [ + "plugin-printedparts" + ], + "summary": "Revision history, newest first.", + "description": "Revision history, newest first.\n\n**Auth:** permission:printedparts.view\n\n**Params:** path: item_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/printedparts/items/1/files -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "post": { + "tags": [ + "plugin-printedparts" + ], + "summary": "Upload the next revision of the item's print file.", + "description": "Upload the next revision of the item's print file.\n\n**Auth:** permission:printedparts.edit\n\n**Params:** path: item_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/printedparts/items/1/files -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: item_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/printedparts/items/{item_id}/image": { + "delete": { + "tags": [ + "plugin-printedparts" + ], + "summary": "Clear an item image; delete the file only if this plugin owns it.", + "description": "Clear an item image; delete the file only if this plugin owns it.\n\n**Auth:** permission:printedparts.delete\n\n**Params:** path: item_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/printedparts/items/1/image -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "post": { + "tags": [ + "plugin-printedparts" + ], + "summary": "Upload (or replace) the photo for an item (multipart file=).", + "description": "Upload (or replace) the photo for an item (multipart file=).\n\n**Auth:** permission:printedparts.edit\n\n**Params:** path: item_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/printedparts/items/1/image -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: item_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/printedparts/items/{item_id}/restock": { + "post": { + "tags": [ + "plugin-printedparts" + ], + "summary": "Add freshly printed stock. Body: {quantity, badge}.", + "description": "Add freshly printed stock. Body: {quantity, badge}.\n\n**Auth:** permission:printedparts.restock\n\n**Params:** path: item_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/printedparts/items/1/restock -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: item_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/printedparts/items/{item_id}/restore": { + "post": { + "tags": [ + "plugin-printedparts" + ], + "summary": "Bring a retired item back; code, photo, and history are intact.", + "description": "Bring a retired item back; code, photo, and history are intact.\n\n**Auth:** permission:printedparts.delete\n\n**Params:** path: item_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/printedparts/items/1/restore -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: item_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/printedparts/kiosk/item/{itemcode}": { + "get": { + "tags": [ + "plugin-printedparts" + ], + "summary": "Item summary for a scanned bin barcode. Kiosk-token gated.", + "description": "Item summary for a scanned bin barcode. Kiosk-token gated.\n\n**Auth:** api-key-optional (kiosk: the per-device token in the X-Kiosk-Token header, OR a caller address inside the site's kiosk CIDR allowlist - an allowlisted caller sends no credential; 404 while the site's kiosk flag is off)\n\n**Params:** path: itemcode (string)\n\n**Example:**\n```\ncurl http://localhost:5001/api/printedparts/kiosk/item/x\n```", + "security": [ + { + "apiKeyAuth": [] + }, + {} + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "itemcode", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/printedparts/kiosk/provisioning-link": { + "get": { + "tags": [ + "plugin-printedparts" + ], + "summary": "The URL that provisions a screen, token included, for an administrator.", + "description": "The URL that provisions a screen, token included, for an administrator.\n\n**Auth:** permission:settings.edit\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/printedparts/kiosk/provisioning-link -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/printedparts/kiosk/take": { + "post": { + "tags": [ + "plugin-printedparts" + ], + "summary": "Take parts from a bin. Body: {itemcode, badge, quantity}.", + "description": "Take parts from a bin. Body: {itemcode, badge, quantity}.\n\n**Auth:** api-key-optional (kiosk: the per-device token in the X-Kiosk-Token header, OR a caller address inside the site's kiosk CIDR allowlist - an allowlisted caller sends no credential; 404 while the site's kiosk flag is off)\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/printedparts/kiosk/take -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "apiKeyAuth": [] + }, + {} + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/printedparts/reports/by-person": { + "get": { + "tags": [ + "plugin-printedparts" + ], + "summary": "Takes grouped by employee, optionally bounded by ?days= (default 30).", + "description": "Takes grouped by employee, optionally bounded by ?days= (default 30).\n\n**Auth:** permission:printedparts.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/printedparts/reports/by-person -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/printedparts/reports/consumption": { + "get": { + "tags": [ + "plugin-printedparts" + ], + "summary": "Takes per item, optionally bounded by ?days= (default 30).", + "description": "Takes per item, optionally bounded by ?days= (default 30).\n\n**Auth:** permission:printedparts.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/printedparts/reports/consumption -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/printedparts/reports/stock": { + "get": { + "tags": [ + "plugin-printedparts" + ], + "summary": "Stock levels with low-stock flags and the cache-vs-ledger reconcile.", + "description": "Stock levels with low-stock flags and the cache-vs-ledger reconcile.\n\n**Auth:** permission:printedparts.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/printedparts/reports/stock -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/printers": { + "get": { + "tags": [ + "plugin-printers" + ], + "summary": "List all printers with filtering and pagination.", + "description": "List all printers with filtering and pagination.\n\n**Auth:** permission:printers.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/printers -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "post": { + "tags": [ + "plugin-printers" + ], + "summary": "Create new printer (creates both Asset and Printer records).", + "description": "Create new printer (creates both Asset and Printer records).\n\n**Auth:** permission:printers.create\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/printers -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/printers/{printer_id}": { + "delete": { + "tags": [ + "plugin-printers" + ], + "summary": "Delete (soft delete) printer.", + "description": "Delete (soft delete) printer.\n\n**Auth:** permission:printers.delete\n\n**Params:** path: printer_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/printers/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "printer_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "get": { + "tags": [ + "plugin-printers" + ], + "summary": "Get a single printer with full details.", + "description": "Get a single printer with full details.\n\n**Auth:** permission:printers.view\n\n**Params:** path: printer_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/printers/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "printer_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "plugin-printers" + ], + "summary": "Update printer (both Asset and Printer records).", + "description": "Update printer (both Asset and Printer records).\n\n**Auth:** permission:printers.edit\n\n**Params:** path: printer_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/printers/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "printer_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: printer_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/printers/{printer_id}/supplies": { + "get": { + "tags": [ + "plugin-printers" + ], + "summary": "Get supply levels from Zabbix (real-time lookup).", + "description": "Get supply levels from Zabbix (real-time lookup).\n\n**Auth:** permission:printers.view\n\n**Params:** path: printer_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/printers/1/supplies -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "printer_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/printers/assignments/for-asset/{asset_id}": { + "get": { + "tags": [ + "plugin-printers" + ], + "summary": "What THIS asset is assigned, without inheritance.", + "description": "What THIS asset is assigned, without inheritance.\n\n**Auth:** permission:printers.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/printers/assignments/for-asset/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "plugin-printers" + ], + "summary": "Reconcile one asset's whole printer assignment in a single call.", + "description": "Reconcile one asset's whole printer assignment in a single call.\n\n**Auth:** permission:printers.edit\n\n**Params:** path: asset_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/printers/assignments/for-asset/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: asset_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/printers/assignments/seed-from-observed/{asset_id}": { + "post": { + "tags": [ + "plugin-printers" + ], + "summary": "Write what a PC observed as the assignment of the asset in the path.", + "description": "Write what a PC observed as the assignment of the asset in the path.\n\n**Auth:** permission:printers.edit\n\n**Params:** path: asset_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/printers/assignments/seed-from-observed/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: asset_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/printers/by-asset/{asset_id}": { + "get": { + "tags": [ + "plugin-printers" + ], + "summary": "Get printer data by asset ID.", + "description": "Get printer data by asset ID.\n\n**Auth:** permission:printers.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/printers/by-asset/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/printers/dashboard/summary": { + "get": { + "tags": [ + "plugin-printers" + ], + "summary": "Get printer dashboard summary data.", + "description": "Get printer dashboard summary data.\n\n**Auth:** permission:printers.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/printers/dashboard/summary -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/printers/dashboard/supplies": { + "get": { + "tags": [ + "plugin-printers" + ], + "summary": "Printers needing a cartridge, flattened to one row per printer.", + "description": "Printers needing a cartridge, flattened to one row per printer.\n\n**Auth:** permission:printers.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/printers/dashboard/supplies -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/printers/drivers": { + "get": { + "tags": [ + "plugin-printers" + ], + "summary": "List printer drivers. ?active=false includes inactive ones.", + "description": "List printer drivers. ?active=false includes inactive ones.\n\n**Auth:** permission:printers.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/printers/drivers -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "post": { + "tags": [ + "plugin-printers" + ], + "summary": "UNDOCUMENTED: printers_asset.create_driver has no docstring.", + "description": "UNDOCUMENTED: printers_asset.create_driver has no docstring.\n\n**Auth:** permission:printers.create\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/printers/drivers -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/printers/drivers/{driver_id}": { + "delete": { + "tags": [ + "plugin-printers" + ], + "summary": "UNDOCUMENTED: printers_asset.delete_driver has no docstring.", + "description": "UNDOCUMENTED: printers_asset.delete_driver has no docstring.\n\n**Auth:** permission:printers.delete\n\n**Params:** path: driver_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/printers/drivers/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "driver_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "plugin-printers" + ], + "summary": "UNDOCUMENTED: printers_asset.update_driver has no docstring.", + "description": "UNDOCUMENTED: printers_asset.update_driver has no docstring.\n\n**Auth:** permission:printers.edit\n\n**Params:** path: driver_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/printers/drivers/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "driver_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: driver_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/printers/for-host/{hostname}": { + "get": { + "tags": [ + "plugin-printers" + ], + "summary": "Printers assigned to a PC, by hostname, with what it takes to install one.", + "description": "Printers assigned to a PC, by hostname, with what it takes to install one.\n\n**Auth:** jwt-optional (works signed out; the handler decides what shows)\n\n**Params:** path: hostname (string)\n\n**Example:**\n```\ncurl http://localhost:5001/api/printers/for-host/x -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + }, + {} + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "hostname", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/printers/install-batch": { + "get": { + "tags": [ + "plugin-printers" + ], + "summary": "Generate a Windows .bat that installs the selected printers.", + "description": "Generate a Windows .bat that installs the selected printers.\n\n**Auth:** jwt-optional (works signed out; the handler decides what shows)\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/printers/install-batch -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + }, + {} + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + }, + "/api/printers/install-list": { + "get": { + "tags": [ + "plugin-printers" + ], + "summary": "Flat, unpaginated list of network printers for the printer installer.", + "description": "Flat, unpaginated list of network printers for the printer installer.\n\n**Auth:** jwt-optional (works signed out; the handler decides what shows)\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/printers/install-list -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + }, + {} + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + }, + "/api/printers/lookup": { + "get": { + "tags": [ + "plugin-printers" + ], + "summary": "Find a printer by IP or FQDN. Parity with the classic printerlookup.asp.", + "description": "Find a printer by IP or FQDN. Parity with the classic printerlookup.asp.\n\n**Auth:** permission:printers.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/printers/lookup -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/printers/lowsupplies": { + "get": { + "tags": [ + "plugin-printers" + ], + "summary": "Get printers with low or critical supply levels.", + "description": "Get printers with low or critical supply levels.\n\n**Auth:** permission:printers.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/printers/lowsupplies -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/printers/models": { + "get": { + "tags": [ + "plugin-printers" + ], + "summary": "List models with a supply count, for the supply-management picker.", + "description": "List models with a supply count, for the supply-management picker.\n\n**Auth:** permission:printers.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/printers/models -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/printers/models/{modelnumberid}/supplies": { + "get": { + "tags": [ + "plugin-printers" + ], + "summary": "List all supplies mapped to a model.", + "description": "List all supplies mapped to a model.\n\n**Auth:** permission:printers.view\n\n**Params:** path: modelnumberid (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/printers/models/1/supplies -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "modelnumberid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "post": { + "tags": [ + "plugin-printers" + ], + "summary": "Add a supply to a model.", + "description": "Add a supply to a model.\n\n**Auth:** permission:printers.create\n\n**Params:** path: modelnumberid (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/printers/models/1/supplies -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "modelnumberid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: modelnumberid (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/printers/observed/{hostname}": { + "get": { + "tags": [ + "plugin-printers" + ], + "summary": "What a host last reported, each queue judged against what it is assigned.", + "description": "What a host last reported, each queue judged against what it is assigned.\n\n**Auth:** permission:printers.view\n\n**Params:** path: hostname (string)\n\n**Example:**\n```\ncurl http://localhost:5001/api/printers/observed/x -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "hostname", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/api/printers/observed/for-asset/{asset_id}": { + "get": { + "tags": [ + "plugin-printers" + ], + "summary": "The same comparison, reached from an asset page: one block per host.", + "description": "The same comparison, reached from an asset page: one block per host.\n\n**Auth:** permission:printers.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/printers/observed/for-asset/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "asset_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + } + }, + "/api/printers/pc-default": { + "get": { + "tags": [ + "plugin-printers" + ], + "summary": "Default printer for a PC, by machine (asset) number.", + "description": "Default printer for a PC, by machine (asset) number.\n\n**Auth:** jwt-optional (works signed out; the handler decides what shows)\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/printers/pc-default -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + }, + {} + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + }, + "/api/printers/supplies/{modelsupplyid}": { + "delete": { + "tags": [ + "plugin-printers" + ], + "summary": "Delete a model supply.", + "description": "Delete a model supply.\n\n**Auth:** permission:printers.delete\n\n**Params:** path: modelsupplyid (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/printers/supplies/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "modelsupplyid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "plugin-printers" + ], + "summary": "Update a model supply.", + "description": "Update a model supply.\n\n**Auth:** permission:printers.edit\n\n**Params:** path: modelsupplyid (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/printers/supplies/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "modelsupplyid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: modelsupplyid (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/printers/supplies/forecast": { + "get": { + "tags": [ + "plugin-printers" + ], + "summary": "Days-to-empty and replacement counts per printer.", + "description": "Days-to-empty and replacement counts per printer.\n\n**Auth:** permission:printers.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/printers/supplies/forecast -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/printers/supplies/meta": { + "get": { + "tags": [ + "plugin-printers" + ], + "summary": "Allowed values for supply type, color, and capacity tier (for the UI).", + "description": "Allowed values for supply type, color, and capacity tier (for the UI).\n\n**Auth:** permission:printers.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/printers/supplies/meta -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/printers/supplies/refresh": { + "post": { + "tags": [ + "plugin-printers" + ], + "summary": "Clear cached Zabbix supply data so the next read pulls fresh values.", + "description": "Clear cached Zabbix supply data so the next read pulls fresh values.\n\n**Auth:** permission:printers.create\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/printers/supplies/refresh -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/printers/types": { + "get": { + "tags": [ + "plugin-printers" + ], + "summary": "List all printer types.", + "description": "List all printer types.\n\n**Auth:** permission:printers.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/printers/types -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + }, + "post": { + "tags": [ + "plugin-printers" + ], + "summary": "Create a new printer type.", + "description": "Create a new printer type.\n\n**Auth:** permission:printers.create\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/printers/types -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/printers/types/{type_id}": { + "delete": { + "tags": [ + "plugin-printers" + ], + "summary": "Delete a printer type. Refused if any printer still uses it.", + "description": "Delete a printer type. Refused if any printer still uses it.\n\n**Auth:** permission:printers.delete\n\n**Params:** path: type_id (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/printers/types/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "type_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "get": { + "tags": [ + "plugin-printers" + ], + "summary": "Get a single printer type.", + "description": "Get a single printer type.\n\n**Auth:** permission:printers.view\n\n**Params:** path: type_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/printers/types/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "type_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "plugin-printers" + ], + "summary": "Update a printer type.", + "description": "Update a printer type.\n\n**Auth:** permission:printers.edit\n\n**Params:** path: type_id (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/printers/types/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "type_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: type_id (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, "/api/slides/{surface}": { "get": { "tags": [ "plugin-slides" ], - "summary": "Admin list of a surface's slides (TvSlide rows ordered by sortorder,slideid, filtered to files present on disk), each...", - "description": "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\n\n**Auth:** jwt + permission:slides.manage\n\n**Params:** path: surface (lobby|shopfloor; else VALIDATION_ERROR)\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/slides/lobby'\n```", + "summary": "UNDOCUMENTED: slides.list_slides has no docstring.", + "description": "UNDOCUMENTED: slides.list_slides has no docstring.\n\n**Auth:** permission:slides.view or slides.manage\n\n**Params:** path: surface (string)\n\n**Example:**\n```\ncurl http://localhost:5001/api/slides/x -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -14546,202 +26118,13 @@ ] } }, - "/api/slides/{surface}/upload": { - "post": { - "tags": [ - "plugin-slides" - ], - "summary": "Upload one or more slide images; non-image extensions skipped, names secure_filename'd and unique-renamed (stem_N.ext)...", - "description": "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)\n\n**Auth:** jwt + permission:slides.manage\n\n**Params:** path: surface. multipart/form-data body: files (repeatable) or single file. Allowed extensions: .jpg .jpeg .png .gif .bmp .webp. Returns {added:[names]}\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" -F 'files=@Slide1.png' -F 'files=@Slide2.png' 'http://localhost:5001/api/slides/lobby/upload'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "surface", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "path: surface. multipart/form-data body: files (repeatable) or single file. Allowed extensions: .jpg .jpeg .png .gif .bmp .webp. Returns {added:[names]}" - } - } - } - } - } - }, - "/api/slides/{surface}/order": { - "post": { - "tags": [ - "plugin-slides" - ], - "summary": "Save play order: each filename in the order array gets sortorder set to its index; unknown filenames silently ignored", - "description": "Save play order: each filename in the order array gets sortorder set to its index; unknown filenames silently ignored\n\n**Auth:** jwt + permission:slides.manage\n\n**Params:** path: surface. JSON body: {order: [filename, ...]}\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"order\":[\"Slide2.png\",\"Slide1.png\"]}' 'http://localhost:5001/api/slides/lobby/order'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "surface", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "path: surface. JSON body: {order: [filename, ...]}" - } - } - } - } - } - }, - "/api/slides/{surface}/delete": { - "post": { - "tags": [ - "plugin-slides" - ], - "summary": "Delete named slides: removes file from disk (OSError swallowed) and the TvSlide row; filenames reduced to basename...", - "description": "Delete named slides: removes file from disk (OSError swallowed) and the TvSlide row; filenames reduced to basename first; returns count of DB rows removed\n\n**Auth:** jwt + permission:slides.manage\n\n**Params:** path: surface. JSON body: {files: [filename, ...]}\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"files\":[\"Slide1.png\"]}' 'http://localhost:5001/api/slides/shopfloor/delete'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "surface", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "path: surface. JSON body: {files: [filename, ...]}" - } - } - } - } - } - }, "/api/slides/{surface}/{slideid}": { "patch": { "tags": [ "plugin-slides" ], - "summary": "Update a single slide's per-slide duration; seconds clamped to >=0 (0 means use the 10s surface default); 404 if slide...", - "description": "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\n\n**Auth:** jwt + permission:slides.manage\n\n**Params:** path: surface, slideid (int). JSON body: {seconds: int} (non-int -> VALIDATION_ERROR)\n\n**Example:**\n```\ncurl -X PATCH -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"seconds\":15}' 'http://localhost:5001/api/slides/lobby/12'\n```", + "summary": "UNDOCUMENTED: slides.update_slide has no docstring.", + "description": "UNDOCUMENTED: slides.update_slide has no docstring.\n\n**Auth:** permission:slides.manage\n\n**Params:** path: surface (string), slideid (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PATCH http://localhost:5001/api/slides/x/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -14799,106 +26182,20 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "path: surface, slideid (int). JSON body: {seconds: int} (non-int -> VALIDATION_ERROR)" + "description": "path: surface (string), slideid (int); body: JSON (fields not yet documented)" } } } } } }, - "/api/printedparts/items": { - "get": { - "tags": [ - "plugin-printedparts" - ], - "summary": "List printed items, paginated, with search and low-stock filter", - "description": "List printed items, paginated, with search and low-stock filter\n\n**Auth:** jwt + permission:printedparts.view\n\n**Params:** query: page, per_page, search (matches itemcode/gagelabtag/itemname/itemdescription/binlocation), active (default true; 'false' includes retired), lowstock=true\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOK\" 'http://localhost:5001/api/printedparts/items?search=bracket&lowstock=true&page=1&per_page=25'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - }, + "/api/slides/{surface}/delete": { "post": { "tags": [ - "plugin-printedparts" + "plugin-slides" ], - "summary": "Create a printed item; itemcode auto-minted from printedparts_code_prefix setting + row id; optional gagelabtag...", - "description": "Create a printed item; itemcode auto-minted from printedparts_code_prefix setting + row id; optional gagelabtag unique-checked (409 on clash)\n\n**Auth:** jwt + permission:printedparts.create\n\n**Params:** json body: itemname (required), gagelabtag, itemdescription, lowstockthreshold (default from printedparts_default_threshold setting, fallback 5), binlocation, printnotes; quantityonhand starts at 0\n\n**Example:**\n```\ncurl -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\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "json body: itemname (required), gagelabtag, itemdescription, lowstockthreshold (default from printedparts_default_threshold setting, fallback 5), binlocation, printnotes; quantityonhand starts at 0" - } - } - } - } - } - }, - "/api/printedparts/items/{item_id}": { - "get": { - "tags": [ - "plugin-printedparts" - ], - "summary": "Get one printed item plus its 25 most recent ledger transactions", - "description": "Get one printed item plus its 25 most recent ledger transactions\n\n**Auth:** jwt + permission:printedparts.view\n\n**Params:** path: item_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOK\" http://localhost:5001/api/printedparts/items/42\n```", + "summary": "UNDOCUMENTED: slides.delete_slides has no docstring.", + "description": "UNDOCUMENTED: slides.delete_slides has no docstring.\n\n**Auth:** permission:slides.manage\n\n**Params:** path: surface (string); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/slides/x/delete -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -14933,60 +26230,11 @@ }, "parameters": [ { - "name": "item_id", + "name": "surface", "in": "path", "required": true, "schema": { - "type": "integer" - } - } - ] - }, - "put": { - "tags": [ - "plugin-printedparts" - ], - "summary": "Update catalog fields (itemname, itemdescription, lowstockthreshold, binlocation, printnotes, gagelabtag); rejects...", - "description": "Update catalog fields (itemname, itemdescription, lowstockthreshold, binlocation, printnotes, gagelabtag); rejects quantityonhand (ledger-managed) and duplicate gagelabtag (409)\n\n**Auth:** jwt + permission:printedparts.edit\n\n**Params:** path: item_id; json body: any of the editable fields; gagelabtag uppercased, empty string clears it\n\n**Example:**\n```\ncurl -X PUT -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"binlocation\":\"C1\",\"lowstockthreshold\":8}' http://localhost:5001/api/printedparts/items/42\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "item_id", - "in": "path", - "required": true, - "schema": { - "type": "integer" + "type": "string" } } ], @@ -14997,69 +26245,20 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "path: item_id; json body: any of the editable fields; gagelabtag uppercased, empty string clears it" + "description": "path: surface (string); body: JSON (fields not yet documented)" } } } } - }, - "delete": { - "tags": [ - "plugin-printedparts" - ], - "summary": "Soft-retire an item (isactive=false); ledger history preserved", - "description": "Soft-retire an item (isactive=false); ledger history preserved\n\n**Auth:** jwt + permission:printedparts.delete\n\n**Params:** path: item_id\n\n**Example:**\n```\ncurl -X DELETE -H \"Authorization: Bearer $TOK\" http://localhost:5001/api/printedparts/items/42\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "item_id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ] } }, - "/api/printedparts/items/{item_id}/restore": { + "/api/slides/{surface}/order": { "post": { "tags": [ - "plugin-printedparts" + "plugin-slides" ], - "summary": "Un-retire a soft-deleted item (isactive=true); code, photo, history intact", - "description": "Un-retire a soft-deleted item (isactive=true); code, photo, history intact\n\n**Auth:** jwt + permission:printedparts.delete\n\n**Params:** path: item_id\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $TOK\" http://localhost:5001/api/printedparts/items/42/restore\n```", + "summary": "UNDOCUMENTED: slides.reorder_slides has no docstring.", + "description": "UNDOCUMENTED: slides.reorder_slides has no docstring.\n\n**Auth:** permission:slides.manage\n\n**Params:** path: surface (string); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/slides/x/order -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -15094,11 +26293,11 @@ }, "parameters": [ { - "name": "item_id", + "name": "surface", "in": "path", "required": true, "schema": { - "type": "integer" + "type": "string" } } ], @@ -15109,20 +26308,20 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "path: item_id" + "description": "path: surface (string); body: JSON (fields not yet documented)" } } } } } }, - "/api/printedparts/items/{item_id}/image": { + "/api/slides/{surface}/upload": { "post": { "tags": [ - "plugin-printedparts" + "plugin-slides" ], - "summary": "Upload or replace the item's photo (png/jpg/jpeg/gif/webp); old image files for the item are deleted first, imageurl...", - "description": "Upload or replace the item's photo (png/jpg/jpeg/gif/webp); old image files for the item are deleted first, imageurl updated\n\n**Auth:** jwt + permission:printedparts.edit\n\n**Params:** path: item_id; multipart/form-data: file=\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $TOK\" -F 'file=@clip.jpg' http://localhost:5001/api/printedparts/items/42/image\n```", + "summary": "UNDOCUMENTED: slides.upload_slides has no docstring.", + "description": "UNDOCUMENTED: slides.upload_slides has no docstring.\n\n**Auth:** permission:slides.manage\n\n**Params:** path: surface (string); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/slides/x/upload -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -15157,11 +26356,11 @@ }, "parameters": [ { - "name": "item_id", + "name": "surface", "in": "path", "required": true, "schema": { - "type": "integer" + "type": "string" } } ], @@ -15172,23 +26371,21 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "path: item_id; multipart/form-data: file=" + "description": "path: surface (string); body: JSON (fields not yet documented)" } } } } - }, - "delete": { + } + }, + "/api/slides/feed": { + "get": { "tags": [ - "plugin-printedparts" - ], - "summary": "Clear the item's imageurl; deletes the file on disk only if the URL is plugin-owned (starts with...", - "description": "Clear the item's imageurl; deletes the file on disk only if the URL is plugin-owned (starts with /api/printedparts/image/)\n\n**Auth:** jwt + permission:printedparts.delete\n\n**Params:** path: item_id\n\n**Example:**\n```\ncurl -X DELETE -H \"Authorization: Bearer $TOK\" http://localhost:5001/api/printedparts/items/42/image\n```", - "security": [ - { - "bearerAuth": [] - } + "plugin-slides" ], + "summary": "Flat playlist for a surface. NOT wrapped in the success_response envelope so the screensaver's existing parser needs no...", + "description": "Flat playlist for a surface. NOT wrapped in the success_response envelope so the screensaver's existing parser needs no change.\n\n**Auth:** none (reviewed: named in the public-read or mutation sweep)\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/slides/feed\n```", + "security": [], "responses": { "200": { "description": "Success. Body is the success_response envelope: {status, data, meta}.", @@ -15205,36 +26402,17 @@ "$ref": "#/components/schemas/ErrorEnvelope" } } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." } - }, - "parameters": [ - { - "name": "item_id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ] + } } }, - "/api/printedparts/image/{filename}": { + "/api/slides/img/{surface}/{filename}": { "get": { "tags": [ - "plugin-printedparts" + "plugin-slides" ], - "summary": "Serve an uploaded item image from instance/printedpartsimages (public: fetched by tags on kiosk and lists)", - "description": "Serve an uploaded item image from instance/printedpartsimages (public: fetched by tags on kiosk and lists)\n\n**Auth:** none\n\n**Params:** path: filename (e.g. printeditem-42.jpg)\n\n**Example:**\n```\ncurl http://localhost:5001/api/printedparts/image/printeditem-42.jpg -o clip.jpg\n```", + "summary": "Serve a slide image with a path-traversal guard.", + "description": "Serve a slide image with a path-traversal guard.\n\n**Auth:** none (reviewed: named in the public-read or mutation sweep)\n\n**Params:** path: surface (string), filename (path)\n\n**Example:**\n```\ncurl http://localhost:5001/api/slides/img/x/example.png\n```", "security": [], "responses": { "200": { @@ -15258,6 +26436,14 @@ } }, "parameters": [ + { + "name": "surface", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, { "name": "filename", "in": "path", @@ -15270,17 +26456,18 @@ ] } }, - "/api/printedparts/items/{item_id}/restock": { + "/api/tools/secrets": { "post": { "tags": [ - "plugin-printedparts" + "plugin-tools" ], - "summary": "Add freshly printed stock via a 'restock' ledger write (single-commit ledger + cached quantity); badge resolved...", - "description": "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\n\n**Auth:** jwt + permission:printedparts.restock\n\n**Params:** path: item_id; json body: quantity (positive int, required), badge (required, 422 BadgeError if unresolvable)\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"quantity\":20,\"badge\":\"123456789\"}' http://localhost:5001/api/printedparts/items/42/restock\n```", + "summary": "Store an already-encrypted secret and return its token.", + "description": "Store an already-encrypted secret and return its token.\n\n**Auth:** jwt-optional (works signed out; the handler decides what shows)\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/tools/secrets -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] - } + }, + {} ], "responses": { "200": { @@ -15298,27 +26485,8 @@ "$ref": "#/components/schemas/ErrorEnvelope" } } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." } }, - "parameters": [ - { - "name": "item_id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], "requestBody": { "required": true, "content": { @@ -15326,83 +26494,20 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "path: item_id; json body: quantity (positive int, required), badge (required, 422 BadgeError if unresolvable)" + "description": "body: JSON (fields not yet documented)" } } } } } }, - "/api/printedparts/items/{item_id}/adjust": { - "post": { - "tags": [ - "plugin-printedparts" - ], - "summary": "Correct the count (damage, recount) via an 'adjust' ledger write; reason mandatory; rejects driving stock below zero...", - "description": "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\n\n**Auth:** jwt + permission:printedparts.restock\n\n**Params:** path: item_id; json body: quantitychange (non-zero int, required), reason (required), badge (required)\n\n**Example:**\n```\ncurl -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\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "item_id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "path: item_id; json body: quantitychange (non-zero int, required), reason (required), badge (required)" - } - } - } - } - } - }, - "/api/printedparts/kiosk/item/{itemcode}": { + "/api/tools/secrets/{token}": { "get": { "tags": [ - "plugin-printedparts" + "plugin-tools" ], - "summary": "Item summary for a scanned bin barcode; resolves full code (itemcode or gagelabtag), 'TAG|rev' QR payloads (rev...", - "description": "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\n\n**Auth:** none (deliberately open per decision record; kiosk cannot carry JWT)\n\n**Params:** path: itemcode (e.g. WJRP0042, 3DP0042, 42, or WJRP0042|3)\n\n**Example:**\n```\ncurl http://localhost:5001/api/printedparts/kiosk/item/WJRP0042\n```", + "summary": "Whether a link is still live, WITHOUT spending it.", + "description": "Whether a link is still live, WITHOUT spending it.\n\n**Auth:** none (the unguessable token in the path is the authorisation)\n\n**Params:** path: token (string)\n\n**Example:**\n```\ncurl http://localhost:5001/api/tools/secrets/x\n```", "security": [], "responses": { "200": { @@ -15427,7 +26532,7 @@ }, "parameters": [ { - "name": "itemcode", + "name": "token", "in": "path", "required": true, "schema": { @@ -15435,15 +26540,13 @@ } } ] - } - }, - "/api/printedparts/kiosk/take": { + }, "post": { "tags": [ - "plugin-printedparts" + "plugin-tools" ], - "summary": "Kiosk checkout: decrement-only 'take' ledger write, badge-attributed, bounded by quantityonhand; records optional...", - "description": "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\n\n**Auth:** none (deliberately open per decision record)\n\n**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)\n\n**Example:**\n```\ncurl -X POST -H 'Content-Type: application/json' -d '{\"itemcode\":\"WJRP0042|3\",\"badge\":\"123456789\",\"quantity\":2}' http://localhost:5001/api/printedparts/kiosk/take\n```", + "summary": "Hand over the ciphertext once, then spend the view.", + "description": "Hand over the ciphertext once, then spend the view.\n\n**Auth:** none (the unguessable token in the path is the authorisation; redeeming spends the secret)\n\n**Params:** path: token (string); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/tools/secrets/x -H 'Content-Type: application/json' -d '{}'\n```", "security": [], "responses": { "200": { @@ -15461,155 +26564,6 @@ "$ref": "#/components/schemas/ErrorEnvelope" } } - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "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)" - } - } - } - } - } - }, - "/api/printedparts/reports/stock": { - "get": { - "tags": [ - "plugin-printedparts" - ], - "summary": "Stock-level report for active items with low-stock flags and ledgerdelta (cache-vs-ledger reconcile; nonzero means a...", - "description": "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)\n\n**Auth:** jwt-optional (like all product reports)\n\n**Params:** query: format=csv for CSV download (printedparts-stock.csv), else JSON {columns, rows}\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/printedparts/reports/stock?format=csv' -o stock.csv\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/printedparts/reports/consumption": { - "get": { - "tags": [ - "plugin-printedparts" - ], - "summary": "Take-transactions aggregated per item (takes count + quantitytaken), sorted by quantity taken descending", - "description": "Take-transactions aggregated per item (takes count + quantitytaken), sorted by quantity taken descending\n\n**Auth:** jwt-optional\n\n**Params:** query: days (default 30; 0 or negative = all time), format=csv (printedparts-consumption.csv)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/printedparts/reports/consumption?days=90'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/printedparts/reports/by-person": { - "get": { - "tags": [ - "plugin-printedparts" - ], - "summary": "Take-transactions grouped by employee SSO (takes count + quantitytaken), sorted by quantity taken descending", - "description": "Take-transactions grouped by employee SSO (takes count + quantitytaken), sorted by quantity taken descending\n\n**Auth:** jwt-optional\n\n**Params:** query: days (default 30; 0 or negative = all time), format=csv (printedparts-by-person.csv)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/printedparts/reports/by-person?days=30&format=csv' -o by-person.csv\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/printedparts/items/{item_id}/files": { - "get": { - "tags": [ - "plugin-printedparts" - ], - "summary": "List the item's print-file revision history, newest revision first", - "description": "List the item's print-file revision history, newest revision first\n\n**Auth:** jwt + permission:printedparts.view\n\n**Params:** path: item_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOK\" http://localhost:5001/api/printedparts/items/42/files\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." }, "404": { "description": "No such record." @@ -15617,946 +26571,13 @@ }, "parameters": [ { - "name": "item_id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ] - }, - "post": { - "tags": [ - "plugin-printedparts" - ], - "summary": "Upload the next print-file revision (append-only, auto-numbered max+1); allowed ext...", - "description": "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\n\n**Auth:** jwt + permission:printedparts.edit\n\n**Params:** path: item_id; multipart/form-data: file= (required), note= (optional)\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $TOK\" -F 'file=@clip-v2.stl' -F 'note=thicker wall' http://localhost:5001/api/printedparts/items/42/files\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "item_id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "path: item_id; multipart/form-data: file= (required), note= (optional)" - } - } - } - } - } - }, - "/api/printedparts/files/{file_id}/download": { - "get": { - "tags": [ - "plugin-printedparts" - ], - "summary": "Download a print-file revision as an attachment under its original filename (jwt-optional so plain anchor downloads...", - "description": "Download a print-file revision as an attachment under its original filename (jwt-optional so plain anchor downloads work)\n\n**Auth:** jwt-optional\n\n**Params:** path: file_id\n\n**Example:**\n```\ncurl -OJ http://localhost:5001/api/printedparts/files/7/download\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "file_id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ] - } - }, - "/api/printedparts/files/{file_id}": { - "delete": { - "tags": [ - "plugin-printedparts" - ], - "summary": "Delete a bad print-file revision (wrong file uploaded): removes the stored file and the DB record", - "description": "Delete a bad print-file revision (wrong file uploaded): removes the stored file and the DB record\n\n**Auth:** jwt + permission:printedparts.delete\n\n**Params:** path: file_id\n\n**Example:**\n```\ncurl -X DELETE -H \"Authorization: Bearer $TOK\" http://localhost:5001/api/printedparts/files/7\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "file_id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ] - } - }, - "/api/network/types": { - "get": { - "tags": [ - "plugin-network" - ], - "summary": "List network device types, paginated", - "description": "List network device types, paginated\n\n**Auth:** jwt-optional\n\n**Params:** page, per_page, active (default true; 'false' includes inactive), search (ilike on networkdevicetype)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/network/types?search=switch&per_page=50'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - }, - "post": { - "tags": [ - "plugin-network" - ], - "summary": "Create a network device type (reactivates a soft-deleted duplicate instead of 409ing)", - "description": "Create a network device type (reactivates a soft-deleted duplicate instead of 409ing)\n\n**Auth:** jwt + permission:network.create\n\n**Params:** body: networkdevicetype (required), description, icon, color\n\n**Example:**\n```\ncurl -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\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: networkdevicetype (required), description, icon, color" - } - } - } - } - } - }, - "/api/network/types/{type_id}": { - "get": { - "tags": [ - "plugin-network" - ], - "summary": "Get one network device type by ID", - "description": "Get one network device type by ID\n\n**Auth:** jwt-optional\n\n**Params:** path: type_id\n\n**Example:**\n```\ncurl http://localhost:5001/api/network/types/3\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "type_id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ] - }, - "put": { - "tags": [ - "plugin-network" - ], - "summary": "Update a network device type; 409 on name collision", - "description": "Update a network device type; 409 on name collision\n\n**Auth:** jwt + permission:network.edit\n\n**Params:** body: networkdevicetype, description, icon, color, isactive\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/network/types/3 -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"description\":\"Access switches\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "type_id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: networkdevicetype, description, icon, color, isactive" - } - } - } - } - }, - "delete": { - "tags": [ - "plugin-network" - ], - "summary": "Hard-delete a device type; 409 if any device still references it", - "description": "Hard-delete a device type; 409 if any device still references it\n\n**Auth:** jwt + permission:network.delete\n\n**Params:** path: type_id\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/network/types/3 -H \"Authorization: Bearer $TOKEN\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "type_id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ] - } - }, - "/api/network": { - "get": { - "tags": [ - "plugin-network" - ], - "summary": "List network devices (Asset joined with NetworkDevice extension + primary IP), filtered/sorted/paginated", - "description": "List network devices (Asset joined with NetworkDevice extension + primary IP), filtered/sorted/paginated\n\n**Auth:** jwt-optional\n\n**Params:** page, per_page, active, assetnumber (exact match for idempotent import), search (assetnumber/name/serialnumber/hostname/gaugelabreference/maintenancereference), 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)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/network?typeid=2&poe=true&sort=hostname&dir=asc'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - }, - "post": { - "tags": [ - "plugin-network" - ], - "summary": "Create a network device (creates core Asset + NetworkDevice extension; upserts primary-IP Communication; audit-logged...", - "description": "Create a network device (creates core Asset + NetworkDevice extension; upserts primary-IP Communication; audit-logged; honors X-Import-Mode legacy timestamps)\n\n**Auth:** jwt + permission:network.create\n\n**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\n\n**Example:**\n```\ncurl -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\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "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" - } - } - } - } - } - }, - "/api/network/{device_id}": { - "get": { - "tags": [ - "plugin-network" - ], - "summary": "Get one network device (asset dict + networkdevice sub-object + primary ipaddress)", - "description": "Get one network device (asset dict + networkdevice sub-object + primary ipaddress)\n\n**Auth:** jwt-optional\n\n**Params:** path: device_id (networkdeviceid)\n\n**Example:**\n```\ncurl http://localhost:5001/api/network/17\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "device_id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ] - }, - "put": { - "tags": [ - "plugin-network" - ], - "summary": "Update asset + network-device fields; 409 on assetnumber/hostname conflicts; change-diff audit log; upserts primary IP...", - "description": "Update asset + network-device fields; 409 on assetnumber/hostname conflicts; change-diff audit log; upserts primary IP when ipaddress present\n\n**Auth:** jwt + permission:network.edit\n\n**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\n\n**Example:**\n```\ncurl -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\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "device_id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: any of assetnumber, name, serialnumber, gaugelabreference, maintenancereference, statusid, locationid, businessunitid, mapx, mapy, notes, isactive, networkdevicetypeid, vendorid, hostname, firmwareversion, portcount, ispoe, ismanaged, rackunit, ipaddress" - } - } - } - } - }, - "delete": { - "tags": [ - "plugin-network" - ], - "summary": "Soft-delete a network device (sets asset.isactive=false; audit-logged)", - "description": "Soft-delete a network device (sets asset.isactive=false; audit-logged)\n\n**Auth:** jwt + permission:network.delete\n\n**Params:** path: device_id\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/network/17 -H \"Authorization: Bearer $TOKEN\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "device_id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ] - } - }, - "/api/network/by-asset/{asset_id}": { - "get": { - "tags": [ - "plugin-network" - ], - "summary": "Look up a network device by its core assetid", - "description": "Look up a network device by its core assetid\n\n**Auth:** jwt-optional\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl http://localhost:5001/api/network/by-asset/1042\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "asset_id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ] - } - }, - "/api/network/by-hostname/{hostname}": { - "get": { - "tags": [ - "plugin-network" - ], - "summary": "Look up a network device by exact hostname", - "description": "Look up a network device by exact hostname\n\n**Auth:** jwt-optional\n\n**Params:** path: hostname\n\n**Example:**\n```\ncurl http://localhost:5001/api/network/by-hostname/wjf-sw-idf3-01\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "hostname", + "name": "token", "in": "path", "required": true, "schema": { "type": "string" } } - ] - } - }, - "/api/network/dashboard/summary": { - "get": { - "tags": [ - "plugin-network" - ], - "summary": "Dashboard counts: total active devices, by type, by vendor, PoE vs non-PoE", - "description": "Dashboard counts: total active devices, by type, by vendor, PoE vs non-PoE\n\n**Auth:** jwt-optional\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/network/dashboard/summary\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/network/vlans": { - "get": { - "tags": [ - "plugin-network" - ], - "summary": "List VLANs, paginated, ordered by vlannumber", - "description": "List VLANs, paginated, ordered by vlannumber\n\n**Auth:** jwt-optional\n\n**Params:** page, per_page, active (default true), search (name/description/vlannumber), type (exact vlantype)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/network/vlans?search=voice'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - }, - "post": { - "tags": [ - "plugin-network" - ], - "summary": "Create a VLAN; 409 on duplicate vlannumber; audit-logged", - "description": "Create a VLAN; 409 on duplicate vlannumber; audit-logged\n\n**Auth:** jwt + permission:network.create\n\n**Params:** body: vlannumber (required), name (required), description, vlantype\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/network/vlans -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"vlannumber\":120,\"name\":\"Shopfloor\",\"vlantype\":\"production\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: vlannumber (required), name (required), description, vlantype" - } - } - } - } - } - }, - "/api/network/vlans/{vlan_id}": { - "get": { - "tags": [ - "plugin-network" - ], - "summary": "Get one VLAN including its active subnets", - "description": "Get one VLAN including its active subnets\n\n**Auth:** jwt-optional\n\n**Params:** path: vlan_id\n\n**Example:**\n```\ncurl http://localhost:5001/api/network/vlans/5\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "vlan_id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ] - }, - "put": { - "tags": [ - "plugin-network" - ], - "summary": "Update a VLAN; 409 on vlannumber conflict; change-diff audit log", - "description": "Update a VLAN; 409 on vlannumber conflict; change-diff audit log\n\n**Auth:** jwt + permission:network.edit\n\n**Params:** body: vlannumber, name, description, vlantype, isactive\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/network/vlans/5 -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"description\":\"CNC cell VLAN\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "vlan_id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } ], "requestBody": { "required": true, @@ -16565,297 +26586,11 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "body: vlannumber, name, description, vlantype, isactive" + "description": "path: token (string); body: JSON (fields not yet documented)" } } } } - }, - "delete": { - "tags": [ - "plugin-network" - ], - "summary": "Soft-delete a VLAN; 400 if it still has active subnets; audit-logged", - "description": "Soft-delete a VLAN; 400 if it still has active subnets; audit-logged\n\n**Auth:** jwt + permission:network.delete\n\n**Params:** path: vlan_id\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/network/vlans/5 -H \"Authorization: Bearer $TOKEN\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "vlan_id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ] - } - }, - "/api/network/subnets": { - "get": { - "tags": [ - "plugin-network" - ], - "summary": "List subnets, paginated, ordered by cidr", - "description": "List subnets, paginated, ordered by cidr\n\n**Auth:** jwt-optional\n\n**Params:** page, per_page, active (default true), search (cidr/name/description), vlanid, locationid, type (exact subnettype)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/network/subnets?vlanid=5'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - }, - "post": { - "tags": [ - "plugin-network" - ], - "summary": "Create a subnet; validates CIDR notation and vlanid existence; 409 on duplicate cidr; audit-logged", - "description": "Create a subnet; validates CIDR notation and vlanid existence; 409 on duplicate cidr; audit-logged\n\n**Auth:** jwt + permission:network.create\n\n**Params:** body: cidr (required, must contain '/'), name (required), description, gatewayip, subnetmask, networkaddress, broadcastaddress, vlanid, subnettype, locationid, dhcpenabled (default true), dhcprangestart, dhcprangeend, dns1, dns2\n\n**Example:**\n```\ncurl -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\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: cidr (required, must contain '/'), name (required), description, gatewayip, subnetmask, networkaddress, broadcastaddress, vlanid, subnettype, locationid, dhcpenabled (default true), dhcprangestart, dhcprangeend, dns1, dns2" - } - } - } - } - } - }, - "/api/network/subnets/{subnet_id}": { - "get": { - "tags": [ - "plugin-network" - ], - "summary": "Get one subnet plus 'devices': every asset of any type (PC/printer/network) whose primary-IP Communication falls inside...", - "description": "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\n\n**Auth:** jwt-optional\n\n**Params:** path: subnet_id\n\n**Example:**\n```\ncurl http://localhost:5001/api/network/subnets/2\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "subnet_id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ] - }, - "put": { - "tags": [ - "plugin-network" - ], - "summary": "Update a subnet; 409 on cidr conflict; change-diff audit log", - "description": "Update a subnet; 409 on cidr conflict; change-diff audit log\n\n**Auth:** jwt + permission:network.edit\n\n**Params:** body: any of cidr, name, description, gatewayip, subnetmask, networkaddress, broadcastaddress, vlanid, subnettype, locationid, dhcpenabled, dhcprangestart, dhcprangeend, dns1, dns2, isactive\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/network/subnets/2 -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"dhcpenabled\":false}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "subnet_id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: any of cidr, name, description, gatewayip, subnetmask, networkaddress, broadcastaddress, vlanid, subnettype, locationid, dhcpenabled, dhcprangestart, dhcprangeend, dns1, dns2, isactive" - } - } - } - } - }, - "delete": { - "tags": [ - "plugin-network" - ], - "summary": "Soft-delete a subnet (isactive=false); audit-logged", - "description": "Soft-delete a subnet (isactive=false); audit-logged\n\n**Auth:** jwt + permission:network.delete\n\n**Params:** path: subnet_id\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/network/subnets/2 -H \"Authorization: Bearer $TOKEN\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "subnet_id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ] } }, "/api/usb": { @@ -16863,10 +26598,9 @@ "tags": [ "plugin-usb" ], - "summary": "List USB devices with checkout status (paginated); dual-mode: selfhosted tables or external cmmc_usb DB per...", - "description": "List USB devices with checkout status (paginated); dual-mode: selfhosted tables or external cmmc_usb DB per usb_directory_mode setting\n\n**Auth:** jwt-optional\n\n**Params:** query: page, per_page, status=available|checkedout|retired, search (matches device_id or device_desc)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/usb?status=available&search=kingston&page=1&per_page=25' -H \"Authorization: Bearer $JWT\"\n```", + "summary": "List USB devices with checkout status.", + "description": "List USB devices with checkout status.\n\n**Auth:** permission:usb.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/usb -H \"Authorization: Bearer $TOK\"\n```", "security": [ - {}, { "bearerAuth": [] } @@ -16887,6 +26621,12 @@ "$ref": "#/components/schemas/ErrorEnvelope" } } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." } } }, @@ -16894,8 +26634,8 @@ "tags": [ "plugin-usb" ], - "summary": "Create a device (starts in checked-in status); 409 on duplicate device_id", - "description": "Create a device (starts in checked-in status); 409 on duplicate device_id\n\n**Auth:** jwt + permission:usb.create\n\n**Params:** body JSON: device_id (required), device_desc, device_owner (badge), locker_location\n\n**Example:**\n```\ncurl -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\"}'\n```", + "summary": "Add a device (starts checked-in).", + "description": "Add a device (starts checked-in).\n\n**Auth:** permission:usb.create\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/usb -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -16932,7 +26672,7 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "body JSON: device_id (required), device_desc, device_owner (badge), locker_location" + "description": "body: JSON (fields not yet documented)" } } } @@ -16944,10 +26684,9 @@ "tags": [ "plugin-usb" ], - "summary": "Get one device plus its last 20 check-in/out log rows; 404 if unknown", - "description": "Get one device plus its last 20 check-in/out log rows; 404 if unknown\n\n**Auth:** jwt-optional\n\n**Params:** path: device_id\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/usb/USB-0042' -H \"Authorization: Bearer $JWT\"\n```", + "summary": "Get a single device plus its last 20 check-in/out log rows.", + "description": "Get a single device plus its last 20 check-in/out log rows.\n\n**Auth:** permission:usb.view\n\n**Params:** path: device_id (string)\n\n**Example:**\n```\ncurl http://localhost:5001/api/usb/x -H \"Authorization: Bearer $TOK\"\n```", "security": [ - {}, { "bearerAuth": [] } @@ -16969,6 +26708,12 @@ } } }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, "404": { "description": "No such record." } @@ -16988,8 +26733,8 @@ "tags": [ "plugin-usb" ], - "summary": "Edit device fields (device_desc / device_owner / locker_location / status); 404 if unknown", - "description": "Edit device fields (device_desc / device_owner / locker_location / status); 404 if unknown\n\n**Auth:** jwt + permission:usb.edit\n\n**Params:** path: device_id; body JSON: any of device_desc, device_owner, locker_location, status\n\n**Example:**\n```\ncurl -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\"}'\n```", + "summary": "Edit device_desc / device_owner / locker_location / status.", + "description": "Edit device_desc / device_owner / locker_location / status.\n\n**Auth:** permission:usb.edit\n\n**Params:** path: device_id (string); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/usb/x -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -17039,133 +26784,7 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "path: device_id; body JSON: any of device_desc, device_owner, locker_location, status" - } - } - } - } - } - }, - "/api/usb/{device_id}/retire": { - "post": { - "tags": [ - "plugin-usb" - ], - "summary": "Retire a device (sets status to retired); 404 if unknown", - "description": "Retire a device (sets status to retired); 404 if unknown\n\n**Auth:** jwt + permission:usb.edit\n\n**Params:** path: device_id; no body\n\n**Example:**\n```\ncurl -X POST 'http://localhost:5001/api/usb/USB-0042/retire' -H \"Authorization: Bearer $JWT\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "device_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "path: device_id; no body" - } - } - } - } - } - }, - "/api/usb/{device_id}/checkout": { - "post": { - "tags": [ - "plugin-usb" - ], - "summary": "Check a device out to a badge (writes check-out log row, sets status checked-out, auto-creates user from HR directory)...", - "description": "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\n\n**Auth:** jwt + permission:usb.create\n\n**Params:** path: device_id; body JSON: badge (required), locker_location (optional, also updates device)\n\n**Example:**\n```\ncurl -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\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "device_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "path: device_id; body JSON: badge (required), locker_location (optional, also updates device)" + "description": "path: device_id (string); body: JSON (fields not yet documented)" } } } @@ -17177,8 +26796,8 @@ "tags": [ "plugin-usb" ], - "summary": "Check a device back in (writes check-in log row with sanitized/virus-scan flags, sets status checked-in); 400 if not...", - "description": "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\n\n**Auth:** jwt + permission:usb.create\n\n**Params:** path: device_id; body JSON: badge (required), locker_location, sanitized (bool/1/0), scanned_viruses (bool/1/0)\n\n**Example:**\n```\ncurl -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}'\n```", + "summary": "Check a device back in from a badge.", + "description": "Check a device back in from a badge.\n\n**Auth:** permission:usb.create\n\n**Params:** path: device_id (string); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/usb/x/checkin -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -17228,7 +26847,70 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "path: device_id; body JSON: badge (required), locker_location, sanitized (bool/1/0), scanned_viruses (bool/1/0)" + "description": "path: device_id (string); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/usb/{device_id}/checkout": { + "post": { + "tags": [ + "plugin-usb" + ], + "summary": "Check a device out to a badge.", + "description": "Check a device out to a badge.\n\n**Auth:** permission:usb.create\n\n**Params:** path: device_id (string); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/usb/x/checkout -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "device_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: device_id (string); body: JSON (fields not yet documented)" } } } @@ -17240,10 +26922,9 @@ "tags": [ "plugin-usb" ], - "summary": "Paginated check-in/out log for one device, newest first", - "description": "Paginated check-in/out log for one device, newest first\n\n**Auth:** jwt-optional\n\n**Params:** path: device_id; query: page, per_page\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/usb/USB-0042/history?page=1&per_page=50' -H \"Authorization: Bearer $JWT\"\n```", + "summary": "Paginated check-in/out log for one device.", + "description": "Paginated check-in/out log for one device.\n\n**Auth:** permission:usb.view\n\n**Params:** path: device_id (string)\n\n**Example:**\n```\ncurl http://localhost:5001/api/usb/x/history -H \"Authorization: Bearer $TOK\"\n```", "security": [ - {}, { "bearerAuth": [] } @@ -17265,6 +26946,12 @@ } } }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, "404": { "description": "No such record." } @@ -17281,15 +26968,14 @@ ] } }, - "/api/usb/checkouts": { - "get": { + "/api/usb/{device_id}/retire": { + "post": { "tags": [ "plugin-usb" ], - "summary": "List check-out log rows (paginated), each with the device's current status joined in", - "description": "List check-out log rows (paginated), each with the device's current status joined in\n\n**Auth:** jwt-optional\n\n**Params:** query: page, per_page, active=true (only rows whose device is still checked out), badge (filter by badge_number)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/usb/checkouts?active=true&badge=212345678' -H \"Authorization: Bearer $JWT\"\n```", + "summary": "Retire a device (status -> retired).", + "description": "Retire a device (status -> retired).\n\n**Auth:** permission:usb.edit\n\n**Params:** path: device_id (string); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/usb/x/retire -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ - {}, { "bearerAuth": [] } @@ -17310,6 +26996,75 @@ "$ref": "#/components/schemas/ErrorEnvelope" } } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "device_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: device_id (string); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/usb/checkouts": { + "get": { + "tags": [ + "plugin-usb" + ], + "summary": "List check-out log rows.", + "description": "List check-out log rows.\n\n**Auth:** permission:usb.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/usb/checkouts -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." } } } @@ -17319,10 +27074,9 @@ "tags": [ "plugin-usb" ], - "summary": "Latest check-out log row for every currently checked-out device (non-paginated list)", - "description": "Latest check-out log row for every currently checked-out device (non-paginated list)\n\n**Auth:** jwt-optional\n\n**Params:** query: badge (filter by badge_number)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/usb/checkouts/active?badge=212345678' -H \"Authorization: Bearer $JWT\"\n```", + "summary": "List the latest check-out log row for every currently checked-out device.", + "description": "List the latest check-out log row for every currently checked-out device.\n\n**Auth:** permission:usb.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/usb/checkouts/active -H \"Authorization: Bearer $TOK\"\n```", "security": [ - {}, { "bearerAuth": [] } @@ -17343,6 +27097,102 @@ "$ref": "#/components/schemas/ErrorEnvelope" } } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + } + } + } + }, + "/api/usb/kiosk/checkin": { + "post": { + "tags": [ + "plugin-usb" + ], + "summary": "Check a device in at the kiosk. Body: {device_id, badge, forsomeoneelse}.", + "description": "Check a device in at the kiosk. Body: {device_id, badge, forsomeoneelse}.\n\n**Auth:** api-key-optional (kiosk: the per-device token in the X-Kiosk-Token header, OR a caller address inside the site's kiosk CIDR allowlist - an allowlisted caller sends no credential; 404 while the site's kiosk flag is off)\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/usb/kiosk/checkin -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "apiKeyAuth": [] + }, + {} + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/usb/kiosk/checkout": { + "post": { + "tags": [ + "plugin-usb" + ], + "summary": "Check a device out at the kiosk. Body: {device_id, badge}.", + "description": "Check a device out at the kiosk. Body: {device_id, badge}.\n\n**Auth:** api-key-optional (kiosk: the per-device token in the X-Kiosk-Token header, OR a caller address inside the site's kiosk CIDR allowlist - an allowlisted caller sends no credential; 404 while the site's kiosk flag is off)\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/usb/kiosk/checkout -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "apiKeyAuth": [] + }, + {} + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "body: JSON (fields not yet documented)" + } + } } } } @@ -17352,9 +27202,14 @@ "tags": [ "plugin-usb" ], - "summary": "What the logged-out kiosk shows after a drive is scanned: description, checked-in/out status, locker, and the NAME of...", - "description": "What the logged-out kiosk shows after a drive is scanned: description, checked-in/out status, locker, and the NAME of the current holder (needed for the return-for-someone-else confirm; the holder's badge is never returned). 404 when the kiosk setting is off, so a disabled kiosk does not advertise the route\n\n**Auth:** none (kiosk; requires usb_kiosk_enabled)\n\n**Params:** path: device_id (the code on the drive label)\n\n**Example:**\n```\ncurl http://localhost:5001/api/usb/kiosk/device/USB-1\n```", - "security": [], + "summary": "What the kiosk shows after a device is scanned.", + "description": "What the kiosk shows after a device is scanned.\n\n**Auth:** api-key-optional (kiosk: the per-device token in the X-Kiosk-Token header, OR a caller address inside the site's kiosk CIDR allowlist - an allowlisted caller sends no credential; 404 while the site's kiosk flag is off)\n\n**Params:** path: device_id (string)\n\n**Example:**\n```\ncurl http://localhost:5001/api/usb/kiosk/device/x\n```", + "security": [ + { + "apiKeyAuth": [] + }, + {} + ], "responses": { "200": { "description": "Success. Body is the success_response envelope: {status, data, meta}.", @@ -17388,14 +27243,19 @@ ] } }, - "/api/usb/kiosk/checkout": { + "/api/usb/kiosk/others": { "post": { "tags": [ "plugin-usb" ], - "summary": "Check a drive out at the kiosk, attributed to the scanned badge. A badge that resolves to no employee is refused with...", - "description": "Check a drive out at the kiosk, attributed to the scanned badge. A badge that resolves to no employee is refused with 422 (unlike the signed-in route, which creates the user row). 409 when the drive is already out\n\n**Auth:** none (kiosk; requires usb_kiosk_enabled)\n\n**Params:** body: device_id, badge\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/usb/kiosk/checkout -H 'Content-Type: application/json' -d '{\"device_id\":\"USB-1\",\"badge\":\"270015376\"}'\n```", - "security": [], + "summary": "Drives checked out to OTHER people, with holder names. Body: {badge}.", + "description": "Drives checked out to OTHER people, with holder names. Body: {badge}.\n\n**Auth:** api-key-optional (kiosk: the per-device token in the X-Kiosk-Token header, OR a caller address inside the site's kiosk CIDR allowlist - an allowlisted caller sends no credential; 404 while the site's kiosk flag is off)\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/usb/kiosk/others -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "apiKeyAuth": [] + }, + {} + ], "responses": { "200": { "description": "Success. Body is the success_response envelope: {status, data, meta}.", @@ -17421,21 +27281,25 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "body: device_id, badge" + "description": "body: JSON (fields not yet documented)" } } } } } }, - "/api/usb/kiosk/checkin": { - "post": { + "/api/usb/kiosk/provisioning-link": { + "get": { "tags": [ "plugin-usb" ], - "summary": "Return a drive at the kiosk. Holder by default: a badge that does not match the current holder gets 409 with...", - "description": "Return a drive at the kiosk. Holder by default: a badge that does not match the current holder gets 409 with details.needsconfirm and the holder's name, and the caller re-posts with forsomeoneelse=true. The badge RECORDED is the one scanned, so the log shows who handed it back. Set usb_kiosk_allow_return_for_others=false to refuse a mismatch with 403 instead\n\n**Auth:** none (kiosk; requires usb_kiosk_enabled)\n\n**Params:** body: device_id, badge, forsomeoneelse (optional bool)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/usb/kiosk/checkin -H 'Content-Type: application/json' -d '{\"device_id\":\"USB-1\",\"badge\":\"270015376\"}'\n```", - "security": [], + "summary": "The URL that provisions a screen, token included, for an administrator.", + "description": "The URL that provisions a screen, token included, for an administrator.\n\n**Auth:** permission:settings.edit\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/usb/kiosk/provisioning-link -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], "responses": { "200": { "description": "Success. Body is the success_response envelope: {status, data, meta}.", @@ -17452,18 +27316,12 @@ "$ref": "#/components/schemas/ErrorEnvelope" } } - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: device_id, badge, forsomeoneelse (optional bool)" - } - } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." } } } @@ -17473,9 +27331,14 @@ "tags": [ "plugin-usb" ], - "summary": "What one badge may do at the kiosk: the caller's own drives (to return), the drives that are free (to take), and a...", - "description": "What one badge may do at the kiosk: the caller's own drives (to return), the drives that are free (to take), and a COUNT of everything else. The drives carry no barcode so the kiosk must offer a list; requiring a badge that resolves to a real person keeps that list from being anonymous, and a POST keeps the badge out of proxy access logs. 422 for an unresolvable badge\n\n**Auth:** none (kiosk; requires usb_kiosk_enabled)\n\n**Params:** body: badge\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/usb/kiosk/session -H 'Content-Type: application/json' -d '{\"badge\":\"270015376\"}'\n```", - "security": [], + "summary": "What one badge may do right now. Body: {badge}.", + "description": "What one badge may do right now. Body: {badge}.\n\n**Auth:** api-key-optional (kiosk: the per-device token in the X-Kiosk-Token header, OR a caller address inside the site's kiosk CIDR allowlist - an allowlisted caller sends no credential; 404 while the site's kiosk flag is off)\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/usb/kiosk/session -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "apiKeyAuth": [] + }, + {} + ], "responses": { "200": { "description": "Success. Body is the success_response envelope: {status, data, meta}.", @@ -17501,21 +27364,25 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "body: badge" + "description": "body: JSON (fields not yet documented)" } } } } } }, - "/api/usb/kiosk/others": { - "post": { + "/api/usb/kiosk/setup": { + "get": { "tags": [ "plugin-usb" ], - "summary": "Drives checked out to OTHER people, with holder names, so somebody can return one left behind. Behind its own step...", - "description": "Drives checked out to OTHER people, with holder names, so somebody can return one left behind. Behind its own step rather than on the main kiosk screen, and 404 when usb_kiosk_allow_return_for_others is off - there is no point naming holders of drives the kiosk will not let anyone return\n\n**Auth:** none (kiosk; requires usb_kiosk_enabled)\n\n**Params:** body: badge\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/usb/kiosk/others -H 'Content-Type: application/json' -d '{\"badge\":\"270015376\"}'\n```", - "security": [], + "summary": "Trade a setup code for this browser's kiosk cookie, then open the kiosk.", + "description": "Trade a setup code for this browser's kiosk cookie, then open the kiosk.\n\n**Auth:** setup-code (single-use kiosk setup code in ?code=, issued by the permission-gated provisioning link; spent on any attempt, 410 once used or expired)\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/usb/kiosk/setup\n```", + "security": [ + { + "setupCodeAuth": [] + } + ], "responses": { "200": { "description": "Success. Body is the success_response envelope: {status, data, meta}.", @@ -17532,18 +27399,12 @@ "$ref": "#/components/schemas/ErrorEnvelope" } } - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: badge" - } - } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." } } } @@ -17553,1694 +27414,8 @@ "tags": [ "plugin-usb" ], - "summary": "Removable-media audit: every check-out and check-in in the window, newest first, with who, which drive, when and which...", - "description": "Removable-media audit: every check-out and check-in in the window, newest first, with who, which drive, when and which locker - plus summary counts. Rows rather than totals, because an audit asks who had a specific drive on a specific day. Works against the external cmmc_usb log or the self-hosted tables; in self-hosted mode one stored loan expands into a take and a return so a returned drive is distinguishable from one still out\n\n**Auth:** jwt + usb.view\n\n**Params:** query: period (day|month|sixmonths|year|all, default month) or days= (0 = all time); format=csv for a download\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/usb/reports/audit?period=year' -H \"Authorization: Bearer $TOKEN\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - } - }, - "/api/printers/types": { - "get": { - "tags": [ - "plugin-printers" - ], - "summary": "List printer types, paginated, with active filter and name search.", - "description": "List printer types, paginated, with active filter and name search.\n\n**Auth:** jwt-optional\n\n**Params:** page, per_page, active (default true; 'false' includes inactive), search\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/printers/types?search=laser&active=false'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - }, - "post": { - "tags": [ - "plugin-printers" - ], - "summary": "Create a new printer type (or reactivate an inactive duplicate).", - "description": "Create a new printer type (or reactivate an inactive duplicate).\n\n**Auth:** permission:printers.create\n\n**Params:** body: printertype (required), description, icon, color; reactivates a matching inactive type instead of 409\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"printertype\":\"Label\",\"color\":\"#00f\"}' http://localhost:5001/api/printers/types\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: printertype (required), description, icon, color; reactivates a matching inactive type instead of 409" - } - } - } - } - } - }, - "/api/printers/types/{type_id}": { - "get": { - "tags": [ - "plugin-printers" - ], - "summary": "Get a single printer type by ID.", - "description": "Get a single printer type by ID.\n\n**Auth:** jwt-optional\n\n**Params:** path: type_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/printers/types/3\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "type_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - }, - "put": { - "tags": [ - "plugin-printers" - ], - "summary": "Update a printer type.", - "description": "Update a printer type.\n\n**Auth:** permission:printers.edit\n\n**Params:** body: printertype, description, icon, color, isactive (any subset); 409 on name clash\n\n**Example:**\n```\ncurl -X PUT -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"isactive\":false}' http://localhost:5001/api/printers/types/3\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "type_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: printertype, description, icon, color, isactive (any subset); 409 on name clash" - } - } - } - } - }, - "delete": { - "tags": [ - "plugin-printers" - ], - "summary": "Hard-delete a printer type when unused.", - "description": "Hard-delete a printer type when unused.\n\n**Auth:** permission:printers.delete\n\n**Params:** path: type_id; 409 if any printer still references the type\n\n**Example:**\n```\ncurl -X DELETE -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/printers/types/3\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "type_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/printers/drivers": { - "get": { - "tags": [ - "plugin-printers" - ], - "summary": "List printer driver packages (named SMB/HTTP links).", - "description": "List printer driver packages (named SMB/HTTP links).\n\n**Auth:** jwt-optional\n\n**Params:** active (default true; 'false' includes inactive). Unpaginated.\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/printers/drivers?active=false'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - }, - "post": { - "tags": [ - "plugin-printers" - ], - "summary": "Create a driver entry.", - "description": "Create a driver entry.\n\n**Auth:** permission:printers.create\n\n**Params:** body: name (required), location (required), description, modelnumberid, isactive\n\n**Example:**\n```\ncurl -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\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: name (required), location (required), description, modelnumberid, isactive" - } - } - } - } - } - }, - "/api/printers/drivers/{driver_id}": { - "put": { - "tags": [ - "plugin-printers" - ], - "summary": "Update a driver entry.", - "description": "Update a driver entry.\n\n**Auth:** permission:printers.edit\n\n**Params:** body: name, location, description, isactive, modelnumberid (any subset)\n\n**Example:**\n```\ncurl -X PUT -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"isactive\":false}' http://localhost:5001/api/printers/drivers/5\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "driver_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: name, location, description, isactive, modelnumberid (any subset)" - } - } - } - } - }, - "delete": { - "tags": [ - "plugin-printers" - ], - "summary": "Hard-delete a driver entry.", - "description": "Hard-delete a driver entry.\n\n**Auth:** permission:printers.delete\n\n**Params:** path: driver_id\n\n**Example:**\n```\ncurl -X DELETE -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/printers/drivers/5\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "driver_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/printers": { - "get": { - "tags": [ - "plugin-printers" - ], - "summary": "List printers (joined Asset+Printer) with filters, search, sorting, pagination; each row includes primary IP.", - "description": "List printers (joined Asset+Printer) with filters, search, sorting, pagination; each row includes primary IP.\n\n**Auth:** jwt-optional\n\n**Params:** page, per_page, active, assetnumber (exact-match for import idempotency), search (assetnumber/name/serial/hostname/windowsname/gaugelabreference/maintenancereference), typeid|type_id, vendorid|vendor_id, locationid|location_id, businessunitid|businessunit_id, sort (hostname|assetnumber|name), dir (asc|desc)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/printers?search=csf&typeid=2&sort=assetnumber&dir=desc'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - }, - "post": { - "tags": [ - "plugin-printers" - ], - "summary": "Create a printer (Asset + Printer extension + optional primary IP communication).", - "description": "Create a printer (Asset + Printer extension + optional primary IP communication).\n\n**Auth:** permission:printers.create\n\n**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\n\n**Example:**\n```\ncurl -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\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "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" - } - } - } - } - } - }, - "/api/printers/install-list": { - "get": { - "tags": [ - "plugin-printers" - ], - "summary": "Flat unpaginated list of active network printers (with map positions) for the signed printer-installer EXE; replaces...", - "description": "Flat unpaginated list of active network printers (with map positions) for the signed printer-installer EXE; replaces classic apiprinters.asp.\n\n**Auth:** jwt-optional\n\n**Params:** format=text for pipe-delimited installer variant (printerid|windowsname|vendorname|modelnumber|hostname|ipaddress|mapx|mapy); default JSON\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/printers/install-list?format=text'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/printers/install-batch": { - "get": { - "tags": [ - "plugin-printers" - ], - "summary": "Generate and download a Windows .bat that installs selected printers (HP/Xerox via universal PrinterInstaller.exe...", - "description": "Generate and download a Windows .bat that installs selected printers (HP/Xerox via universal PrinterInstaller.exe, per-printer .exe /SILENT, or flags manual).\n\n**Auth:** jwt-optional\n\n**Params:** printerids (required, comma-separated printer IDs)\n\n**Example:**\n```\ncurl -OJ 'http://localhost:5001/api/printers/install-batch?printerids=1,2,3'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/printers/pc-default": { - "get": { - "tags": [ - "plugin-printers" - ], - "summary": "Look up a PC's default printer via the defaultprinter asset relationship (parity with classic apipcdefaultprinter.asp)...", - "description": "Look up a PC's default printer via the defaultprinter asset relationship (parity with classic apipcdefaultprinter.asp); used by installer to preselect map hotspot.\n\n**Auth:** jwt-optional\n\n**Params:** machine (PC asset number), format=text for pipe-delimited variant; returns {} / empty body if no default set\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/printers/pc-default?machine=0421&format=text'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/printers/for-host/{hostname}": { - "get": { - "tags": [ - "plugin-printers" - ], - "summary": "Desired printer set for one PC: its own usesprinter/defaultprinter relationships, or, when it has none, the ones...", - "description": "Desired printer set for one PC: its own usesprinter/defaultprinter relationships, or, when it has none, the ones inherited through its controls edge to the machine it drives. Each entry carries what a client needs to install the queue (queue name, hostname, ipaddress, port, drivername, driverlocation, isdefault, inherited). A known host with nothing assigned returns an empty list and a null default (the client's designed no-op); an unknown hostname, or a site without the computers plugin, is a 404.\n\n**Auth:** jwt-optional\n\n**Params:** path: hostname (matched case-insensitively, as sent by the client's COMPUTERNAME); no query parameters\n\n**Example:**\n```\ncurl http://localhost:5001/api/printers/for-host/workstation01\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "hostname", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/printers/observed/{hostname}": { - "get": { - "tags": [ - "plugin-printers" - ], - "summary": "The observed half of the loop and the mirror of /for-host: what this bay last REPORTED it has (POST...", - "description": "The observed half of the loop and the mirror of /for-host: what this bay last REPORTED it has (POST /api/collector/printers), each queue judged against the resolved assignment it SHOULD have. Read-only - nothing here changes an assignment, however wrong the bay looks. Every queue is classified matching / drifted (same printer, different port address or driver) / extra (a printer ShopDB knows, installed unassigned) / missing (assigned, not reported) / unknown (matches no printer, never guessed), each entry carrying both sides plus driftfields; the response adds observedat, the assignment source (own rows or the machine this PC controls), a per-classification summary, and a seedcandidate preview of what a seed would write. 404 only when the hostname has neither a report nor a computer record; a known PC that never reported returns an empty queue list.\n\n**Auth:** permission:printers.view\n\n**Params:** path: hostname (case-insensitive, short name or FQDN, as the reporting client sent it); no query parameters\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/printers/observed/workstation01\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "hostname", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/printers/observed/for-asset/{asset_id}": { - "get": { - "tags": [ - "plugin-printers" - ], - "summary": "The same observed-against-assigned comparison as /api/printers/observed/, reached from an asset page, with...", - "description": "The same observed-against-assigned comparison as /api/printers/observed/, reached from an asset page, with one block per reporting host. Assigned state lives on the MACHINE while observed state is reported by the PCs, so a machine answers with a block for each active PC that controls it (its own hostname first when the asset is itself a PC); blocks rather than one merged list because two PCs legitimately share one machine and the actionable part of drift is which box to walk to. Each block carries the same fields as the by-hostname route: queues classified matching / drifted / extra / missing / unknown, observedat, the assignment source, a summary, and a seedcandidate preview. Read-only. An asset nothing has reported for is an empty hosts list and a 200, not a 404.\n\n**Auth:** permission:printers.view\n\n**Params:** path: asset_id (a machine or a PC); no query parameters\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/printers/observed/for-asset/312\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "asset_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/printers/assignments/for-asset/{asset_id}": { - "put": { - "tags": [ - "plugin-printers" - ], - "summary": "Reconcile an asset's whole printer assignment in one write: soft-deletes usesprinter rows that went away, reactivates...", - "description": "Reconcile an asset's whole printer assignment in one write: soft-deletes usesprinter rows that went away, reactivates previously removed ones, creates new ones, and replaces the single defaultprinter row (exactly one per asset, optional, always one of the assigned printers). Removing an assignment has no side effects and never uninstalls anything on a client.\n\n**Auth:** permission:printers.edit\n\n**Params:** path: asset_id (the machine or PC asset the printers belong to); body: printerassetids (list of printer asset IDs, required, empty list clears the assignment), defaultprinterassetid (int or null; 400 unless it is one of printerassetids)\n\n**Example:**\n```\ncurl -X PUT -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"printerassetids\":[204,205],\"defaultprinterassetid\":204}' http://localhost:5001/api/printers/assignments/for-asset/312\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "asset_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "path: asset_id (the machine or PC asset the printers belong to); body: printerassetids (list of printer asset IDs, required, empty list clears the assignment), defaultprinterassetid (int or null; 400 unless it is one of printerassetids)" - } - } - } - } - }, - "get": { - "tags": [ - "plugin-printers" - ], - "summary": "This asset's OWN printer assignment - printerassetids and defaultprinterassetid - deliberately WITHOUT inheritance. The...", - "description": "This asset's OWN printer assignment - printerassetids and defaultprinterassetid - deliberately WITHOUT inheritance. The editor has to show what this asset's own rows say, or a machine's printers would appear ticked on the PC that inherits them and unticking one would silently create an override. /printers/for-host is the resolved view the client uses; this is the editable one\n\n**Auth:** optional jwt\n\n**Params:** asset_id in path\n\n**Example:**\n```\ncurl http://localhost:5001/api/printers/assignments/for-asset/14\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "asset_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/printers/assignments/seed-from-observed/{asset_id}": { - "post": { - "tags": [ - "plugin-printers" - ], - "summary": "The ONE path from observed state to assigned state, and a person has to ask for it: no collector, cycle or background...", - "description": "The ONE path from observed state to assigned state, and a person has to ask for it: no collector, cycle or background job reaches this route, so a bay that installed the wrong printer can never make itself right by reporting it. Writes the matched observed queues through the same reconcile as PUT /printers/assignments/for-asset (usesprinter rows plus the single defaultprinter, set only when the observed default itself matched a printer). Returns what was written, the skipped queues, and warnings - including one when the target controls another asset, because its own rows then shadow that machine's assignment for good. Normally posted against the MACHINE so the assignment survives a reimage.\n\n**Auth:** permission:printers.edit\n\n**Params:** path: asset_id (the machine or PC to write the assignment on); body, all optional: hostname (which report to seed from; omitted, the asset's own hostname or its single controlling PC), allowunmatched (bool, seed the matched queues anyway when some queue matches no printer). 404 when no named or derived host has reported; 409 when several controlling PCs have reported, or when any queue matches no printer and allowunmatched is unset; 400 when nothing matched, since writing the empty set would silently unassign the asset. Nothing is written on any of those.\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"hostname\":\"workstation01\"}' http://localhost:5001/api/printers/assignments/seed-from-observed/312\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "asset_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "path: asset_id (the machine or PC to write the assignment on); body, all optional: hostname (which report to seed from; omitted, the asset's own hostname or its single controlling PC), allowunmatched (bool, seed the matched queues anyway when some queue matches no printer). 404 when no named or derived host has reported; 409 when several controlling PCs have reported, or when any queue matches no printer and allowunmatched is unset; 400 when nothing matched, since writing the empty set would silently unassign the asset. Nothing is written on any of those." - } - } - } - } - } - }, - "/api/printers/{printer_id}": { - "get": { - "tags": [ - "plugin-printers" - ], - "summary": "Get one printer with full asset details, communications, and active drivers matching its model.", - "description": "Get one printer with full asset details, communications, and active drivers matching its model.\n\n**Auth:** jwt-optional\n\n**Params:** path: printer_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/printers/17\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "printer_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - }, - "put": { - "tags": [ - "plugin-printers" - ], - "summary": "Update printer: asset, extension, and primary IP in one call.", - "description": "Update printer: asset, extension, and primary IP in one call.\n\n**Auth:** permission:printers.edit\n\n**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\n\n**Example:**\n```\ncurl -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\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "printer_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "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" - } - } - } - } - }, - "delete": { - "tags": [ - "plugin-printers" - ], - "summary": "Soft-delete a printer (sets the underlying asset isactive=false).", - "description": "Soft-delete a printer (sets the underlying asset isactive=false).\n\n**Auth:** permission:printers.delete\n\n**Params:** path: printer_id\n\n**Example:**\n```\ncurl -X DELETE -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/printers/17\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "printer_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/printers/by-asset/{asset_id}": { - "get": { - "tags": [ - "plugin-printers" - ], - "summary": "Get printer data keyed by core asset ID.", - "description": "Get printer data keyed by core asset ID.\n\n**Auth:** jwt-optional\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/printers/by-asset/204\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "asset_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/printers/{printer_id}/supplies": { - "get": { - "tags": [ - "plugin-printers" - ], - "summary": "Real-time supply levels from Zabbix for one printer, annotated with status/color/part numbers; fails soft (empty...", - "description": "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.\n\n**Auth:** jwt-optional\n\n**Params:** path: printer_id; needs an IP communication on the printer\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/printers/17/supplies\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "printer_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/printers/lowsupplies": { - "get": { - "tags": [ - "plugin-printers" - ], - "summary": "Fleet-wide report of printers with low/critical supply levels from Zabbix, with summary counts.", - "description": "Fleet-wide report of printers with low/critical supply levels from Zabbix, with summary counts.\n\n**Auth:** jwt-optional\n\n**Params:** none (data cached 5 min)\n\n**Example:**\n```\ncurl http://localhost:5001/api/printers/lowsupplies\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/printers/lookup": { - "get": { - "tags": [ - "plugin-printers" - ], - "summary": "Find a printer by IP/FQDN (parity with classic printerlookup.asp; used by Zabbix to deep-link to a printer record).", - "description": "Find a printer by IP/FQDN (parity with classic printerlookup.asp; used by Zabbix to deep-link to a printer record).\n\n**Auth:** printers.view\n\n**Params:** ip or fqdn (one required; ip matches a communication address, fqdn matches the printer's hostname)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/printers/lookup?ip=10.1.2.42'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - } - }, - "/api/printers/supplies/refresh": { - "post": { - "tags": [ - "plugin-printers" - ], - "summary": "Clear the cached Zabbix supply data so next reads pull fresh values (toner-report Refresh button).", - "description": "Clear the cached Zabbix supply data so next reads pull fresh values (toner-report Refresh button).\n\n**Auth:** permission:printers.create\n\n**Params:** none\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/printers/supplies/refresh\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "none" - } - } - } - } - } - }, - "/api/printers/dashboard/summary": { - "get": { - "tags": [ - "plugin-printers" - ], - "summary": "Dashboard summary: total active printers, counts by type and vendor, low/critical supply counts (Zabbix, if reachable).", - "description": "Dashboard summary: total active printers, counts by type and vendor, low/critical supply counts (Zabbix, if reachable).\n\n**Auth:** jwt-optional\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/printers/dashboard/summary\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/printers/supplies/meta": { - "get": { - "tags": [ - "plugin-printers" - ], - "summary": "Allowed enum values for supplytype, color, and capacitytier (for the UI forms).", - "description": "Allowed enum values for supplytype, color, and capacitytier (for the UI forms).\n\n**Auth:** jwt-optional\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/printers/supplies/meta\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/printers/models": { - "get": { - "tags": [ - "plugin-printers" - ], - "summary": "List printer models with supply counts for the supply-management picker (restricted to models attached to printers or...", - "description": "List printer models with supply counts for the supply-management picker (restricted to models attached to printers or already carrying supplies).\n\n**Auth:** jwt-optional\n\n**Params:** page, per_page, search (modelnumber), vendorid|vendor_id, withsupplies=true (only models that already have supplies)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/printers/models?withsupplies=true&search=M404'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/printers/models/{modelnumberid}/supplies": { - "get": { - "tags": [ - "plugin-printers" - ], - "summary": "List all active supplies (toner/drum/waste part numbers) mapped to a model.", - "description": "List all active supplies (toner/drum/waste part numbers) mapped to a model.\n\n**Auth:** jwt-optional\n\n**Params:** path: modelnumberid (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/printers/models/12/supplies\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "modelnumberid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - }, - "post": { - "tags": [ - "plugin-printers" - ], - "summary": "Add a supply part-number mapping to a model.", - "description": "Add a supply part-number mapping to a model.\n\n**Auth:** permission:printers.create\n\n**Params:** body: partnumber (required), supplytype (default toner), color (default none), capacitytier (default standard), marketingname, pageyield, notes; 409 if part already mapped to model\n\n**Example:**\n```\ncurl -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\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "modelnumberid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: partnumber (required), supplytype (default toner), color (default none), capacitytier (default standard), marketingname, pageyield, notes; 409 if part already mapped to model" - } - } - } - } - } - }, - "/api/printers/supplies/{modelsupplyid}": { - "put": { - "tags": [ - "plugin-printers" - ], - "summary": "Update a model supply mapping.", - "description": "Update a model supply mapping.\n\n**Auth:** permission:printers.edit\n\n**Params:** body: any subset of supplytype, color, capacitytier, partnumber, marketingname, pageyield, notes; enum-validated, 409 on partnumber clash within model\n\n**Example:**\n```\ncurl -X PUT -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"capacitytier\":\"high\"}' http://localhost:5001/api/printers/supplies/44\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "modelsupplyid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: any subset of supplytype, color, capacitytier, partnumber, marketingname, pageyield, notes; enum-validated, 409 on partnumber clash within model" - } - } - } - } - }, - "delete": { - "tags": [ - "plugin-printers" - ], - "summary": "Hard-delete a model supply mapping.", - "description": "Hard-delete a model supply mapping.\n\n**Auth:** permission:printers.delete\n\n**Params:** path: modelsupplyid\n\n**Example:**\n```\ncurl -X DELETE -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/printers/supplies/44\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "modelsupplyid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/printers/supplies/forecast": { - "get": { - "tags": [ - "plugin-printers" - ], - "summary": "One row per CARTRIDGE: days-to-empty from Zabbix history (hourly trends over a long window), soonest first, banded...", - "description": "One row per CARTRIDGE: days-to-empty from Zabbix history (hourly trends over a long window), soonest first, banded empty/soon/month/later, each with its part numbers and replacement count. Also returns orderlist - what to buy within horizondays, grouped by part number with a quantity. Cartridges with no honest estimate come back separately with the reason. available=false when Zabbix is unreachable\n\n**Auth:** jwt-optional\n\n**Params:** days (1-365, default 90)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/printers/supplies/forecast?days=90'\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/printers/dashboard/supplies": { - "get": { - "tags": [ - "plugin-printers" - ], - "summary": "Dashboard card: printers needing a cartridge, one row per printer. Reuses the low-supplies query and its five-minute...", - "description": "Dashboard card: printers needing a cartridge, one row per printer. Reuses the low-supplies query and its five-minute cache, so the card costs the same as the report\n\n**Auth:** permission:printers.view\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/printers/dashboard/supplies\n```", + "summary": "Every check-out and check-in in the window, newest first.", + "description": "Every check-out and check-in in the window, newest first.\n\n**Auth:** permission:usb.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/usb/reports/audit -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -19277,10 +27452,9 @@ "tags": [ "plugin-warranty" ], - "summary": "List warranties with linked-asset summaries and derived status (batch asset fetch, ordered by enddate with null last).", - "description": "List warranties with linked-asset summaries and derived status (batch asset fetch, ordered by enddate with null last).\n\n**Auth:** jwt-optional\n\n**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)\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/warranty?status=expiring&assetid=42'\n```", + "summary": "List warranties. Filters: ?status=, ?assetid=, ?active=false.", + "description": "List warranties. Filters: ?status=, ?assetid=, ?active=false.\n\n**Auth:** permission:warranty.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/warranty -H \"Authorization: Bearer $TOK\"\n```", "security": [ - {}, { "bearerAuth": [] } @@ -19301,6 +27475,12 @@ "$ref": "#/components/schemas/ErrorEnvelope" } } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." } } }, @@ -19308,8 +27488,8 @@ "tags": [ "plugin-warranty" ], - "summary": "Create a warranty (vendor required) and optionally link it to assets; returns 201.", - "description": "Create a warranty (vendor required) and optionally link it to assets; returns 201.\n\n**Auth:** jwt + permission:warranty.create\n\n**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)\n\n**Example:**\n```\ncurl -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\n```", + "summary": "UNDOCUMENTED: warranty.create_warranty has no docstring.", + "description": "UNDOCUMENTED: warranty.create_warranty has no docstring.\n\n**Auth:** permission:warranty.create\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/warranty -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -19346,7 +27526,343 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "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)" + "description": "body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/warranty/{warrantyid}": { + "delete": { + "tags": [ + "plugin-warranty" + ], + "summary": "UNDOCUMENTED: warranty.delete_warranty has no docstring.", + "description": "UNDOCUMENTED: warranty.delete_warranty has no docstring.\n\n**Auth:** permission:warranty.delete\n\n**Params:** path: warrantyid (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/warranty/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "warrantyid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "get": { + "tags": [ + "plugin-warranty" + ], + "summary": "UNDOCUMENTED: warranty.get_warranty has no docstring.", + "description": "UNDOCUMENTED: warranty.get_warranty has no docstring.\n\n**Auth:** permission:warranty.view\n\n**Params:** path: warrantyid (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/warranty/1 -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "warrantyid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "put": { + "tags": [ + "plugin-warranty" + ], + "summary": "UNDOCUMENTED: warranty.update_warranty has no docstring.", + "description": "UNDOCUMENTED: warranty.update_warranty has no docstring.\n\n**Auth:** permission:warranty.edit\n\n**Params:** path: warrantyid (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/warranty/1 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "warrantyid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: warrantyid (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/warranty/{warrantyid}/proof": { + "delete": { + "tags": [ + "plugin-warranty" + ], + "summary": "Remove a warranty's proof document.", + "description": "Remove a warranty's proof document.\n\n**Auth:** permission:warranty.edit\n\n**Params:** path: warrantyid (int)\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/warranty/1/proof -H \"Authorization: Bearer $TOK\"\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "warrantyid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ] + }, + "post": { + "tags": [ + "plugin-warranty" + ], + "summary": "Upload (or replace) the proof-of-cover document for a warranty.", + "description": "Upload (or replace) the proof-of-cover document for a warranty.\n\n**Auth:** permission:warranty.edit\n\n**Params:** path: warrantyid (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/warranty/1/proof -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "warrantyid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: warrantyid (int); body: JSON (fields not yet documented)" + } + } + } + } + } + }, + "/api/warranty/{warrantyid}/refresh": { + "post": { + "tags": [ + "plugin-warranty" + ], + "summary": "UNDOCUMENTED: warranty.refresh_warranty has no docstring.", + "description": "UNDOCUMENTED: warranty.refresh_warranty has no docstring.\n\n**Auth:** permission:warranty.edit\n\n**Params:** path: warrantyid (int); body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/warranty/1/refresh -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success. Body is the success_response envelope: {status, data, meta}.", + "content": { + "application/json": { + "$ref": "#/components/schemas/SuccessEnvelope" + } + } + }, + "default": { + "description": "Error. Body is the error envelope; the code and message are nested under data.error.", + "content": { + "application/json": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, + "404": { + "description": "No such record." + } + }, + "parameters": [ + { + "name": "warrantyid", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "description": "path: warrantyid (int); body: JSON (fields not yet documented)" } } } @@ -19358,10 +27874,9 @@ "tags": [ "plugin-warranty" ], - "summary": "Active warranties covering one asset, for the asset-detail panel.", - "description": "Active warranties covering one asset, for the asset-detail panel.\n\n**Auth:** jwt-optional\n\n**Params:** path: assetid\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/warranty/asset/42\n```", + "summary": "Warranties covering one asset (for the asset-detail panel).", + "description": "Warranties covering one asset (for the asset-detail panel).\n\n**Auth:** permission:warranty.view\n\n**Params:** path: assetid (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/warranty/asset/1 -H \"Authorization: Bearer $TOK\"\n```", "security": [ - {}, { "bearerAuth": [] } @@ -19383,6 +27898,12 @@ } } }, + "401": { + "description": "Missing or invalid credentials." + }, + "403": { + "description": "Authenticated, but not permitted." + }, "404": { "description": "No such record." } @@ -19399,478 +27920,13 @@ ] } }, - "/api/warranty/{warrantyid}": { - "get": { - "tags": [ - "plugin-warranty" - ], - "summary": "Fetch a single warranty by id with asset summaries; 404 if missing.", - "description": "Fetch a single warranty by id with asset summaries; 404 if missing.\n\n**Auth:** jwt-optional\n\n**Params:** path: warrantyid\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/warranty/7\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "warrantyid", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ] - }, - "put": { - "tags": [ - "plugin-warranty" - ], - "summary": "Partial update of any warranty field (only keys present in body change), including isactive and replacing asset links...", - "description": "Partial update of any warranty field (only keys present in body change), including isactive and replacing asset links via assetids.\n\n**Auth:** jwt + permission:warranty.edit\n\n**Params:** path: warrantyid; JSON body (all optional): vendor, servicetag, provider, servicelevel, startdate, enddate, notes, isactive (bool), assetids (full replacement list)\n\n**Example:**\n```\ncurl -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\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "warrantyid", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "path: warrantyid; JSON body (all optional): vendor, servicetag, provider, servicelevel, startdate, enddate, notes, isactive (bool), assetids (full replacement list)" - } - } - } - } - }, - "delete": { - "tags": [ - "plugin-warranty" - ], - "summary": "Hard-delete a warranty (and its asset links); 404 if missing.", - "description": "Hard-delete a warranty (and its asset links); 404 if missing.\n\n**Auth:** jwt + permission:warranty.delete\n\n**Params:** path: warrantyid\n\n**Example:**\n```\ncurl -X DELETE -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/warranty/7\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "warrantyid", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ] - } - }, - "/api/warranty/{warrantyid}/refresh": { - "post": { - "tags": [ - "plugin-warranty" - ], - "summary": "Re-query the warranty's provider (dell etc.) by service tag and update servicelevel/startdate/enddate...", - "description": "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.\n\n**Auth:** jwt + permission:warranty.edit\n\n**Params:** path: warrantyid; no body\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/warranty/7/refresh\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "warrantyid", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "path: warrantyid; no body" - } - } - } - } - } - }, - "/api/warranty/sync/dell": { - "post": { - "tags": [ - "plugin-warranty" - ], - "summary": "Bulk Dell sync: look up serials of active assets lacking a dated warranty via Dell bulk_lookup, creating or updating...", - "description": "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.\n\n**Auth:** jwt + permission:warranty.edit\n\n**Params:** query: all (default false; 'true' re-checks assets that already have a dated warranty); no body\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" 'http://localhost:5001/api/warranty/sync/dell?all=true'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "query: all (default false; 'true' re-checks assets that already have a dated warranty); no body" - } - } - } - } - } - }, - "/api/warranty/report": { - "get": { - "tags": [ - "plugin-warranty" - ], - "summary": "Report for the Reports hub: active warranties bucketed by derived status (expired/expiring/active/unknown) with...", - "description": "Report for the Reports hub: active warranties bucketed by derived status (expired/expiring/active/unknown) with per-bucket counts and full lists.\n\n**Auth:** jwt-optional\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/warranty/report\n```", - "security": [ - {}, - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/warranty/{warrantyid}/proof": { - "post": { - "tags": [ - "plugin-warranty" - ], - "summary": "Upload or replace the proof-of-cover document (invoice, certificate)", - "description": "Upload or replace the proof-of-cover document (invoice, certificate)\n\n**Auth:** jwt + warranty.edit\n\n**Params:** multipart/form-data: file= (pdf/png/jpg/gif/webp/tif/msg/eml/doc/docx/xls/xlsx), max 25MB\n\n**Example:**\n```\ncurl -X POST -H 'Authorization: Bearer ' -F file=@invoice.pdf http://localhost:5001/api/warranty/12/proof\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "warrantyid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "multipart/form-data: file= (pdf/png/jpg/gif/webp/tif/msg/eml/doc/docx/xls/xlsx), max 25MB" - } - } - } - } - }, - "delete": { - "tags": [ - "plugin-warranty" - ], - "summary": "Remove a proof document and clear both columns", - "description": "Remove a proof document and clear both columns\n\n**Auth:** jwt + warranty.edit\n\n**Params:** none\n\n**Example:**\n```\ncurl -X DELETE -H 'Authorization: Bearer ' http://localhost:5001/api/warranty/12/proof\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "warrantyid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/warranty/proof/{filename}": { - "get": { - "tags": [ - "plugin-warranty" - ], - "summary": "Download a proof document under the vendor's original filename. Authenticated: carries pricing and a service tag", - "description": "Download a proof document under the vendor's original filename. Authenticated: carries pricing and a service tag\n\n**Auth:** jwt + warranty.view\n\n**Params:** none\n\n**Example:**\n```\ncurl -OJ -H 'Authorization: Bearer ' http://localhost:5001/api/warranty/proof/warranty-12.pdf\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "filename", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, "/api/warranty/dashboard/expiring": { "get": { "tags": [ "plugin-warranty" ], - "summary": "Dashboard card: warranties running out and ones that already have, identified the way the floor identifies them - by...", - "description": "Dashboard card: warranties running out and ones that already have, identified the way the floor identifies them - by the PC's hostname and the machine it drives\n\n**Auth:** permission:warranty.view\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/warranty/dashboard/expiring\n```", + "summary": "Warranties running out, and ones that already have.", + "description": "Warranties running out, and ones that already have.\n\n**Auth:** permission:warranty.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/warranty/dashboard/expiring -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -19902,4202 +27958,13 @@ } } }, - "/api/backups/kinds": { + "/api/warranty/proof/{filename}": { "get": { "tags": [ - "plugin-backups" + "plugin-warranty" ], - "summary": "Registered backup kinds (NTLARS/DNC, part marker, UDC, CMM ...) with each kind's storage backend and download formats...", - "description": "Registered backup kinds (NTLARS/DNC, part marker, UDC, CMM ...) with each kind's storage backend and download formats, for UI that enumerates them rather than hardcoding the list\n\n**Auth:** permission:backups.view\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/backups/kinds\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - } - }, - "/api/backups/asset/{assetid}": { - "get": { - "tags": [ - "plugin-backups" - ], - "summary": "Revision history for one asset, newest first. An unchanged configuration writes no revision, so the newest row may be...", - "description": "Revision history for one asset, newest first. An unchanged configuration writes no revision, so the newest row may be old and still current - each row carries both when it was captured and when it was last confirmed\n\n**Auth:** permission:backups.view\n\n**Params:** assetid in path; optional ?kind= to filter to one backup kind\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/backups/asset/42\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "assetid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/backups/asset/{assetid}/info": { - "get": { - "tags": [ - "plugin-backups" - ], - "summary": "A kind's at-a-glance card built from its latest revision, for the asset detail page", - "description": "A kind's at-a-glance card built from its latest revision, for the asset detail page\n\n**Auth:** permission:backups.view\n\n**Params:** assetid in path; ?kind= selects the kind whose card is wanted\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" \"http://localhost:5001/api/backups/asset/42/info?kind=ntlars\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "assetid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/backups/revisions/{backuprevisionid}": { - "get": { - "tags": [ - "plugin-backups" - ], - "summary": "One revision, including its parsed payload where the kind stores one (opaque kinds return metadata only)", - "description": "One revision, including its parsed payload where the kind stores one (opaque kinds return metadata only)\n\n**Auth:** permission:backups.view\n\n**Params:** backuprevisionid in path\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/backups/revisions/1188\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "backuprevisionid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/backups/revisions/{backuprevisionid}/diff": { - "get": { - "tags": [ - "plugin-backups" - ], - "summary": "Diff two revisions of the same asset and kind, key by key, which is what answers \"what changed on this machine\"", - "description": "Diff two revisions of the same asset and kind, key by key, which is what answers \"what changed on this machine\"\n\n**Auth:** permission:backups.view\n\n**Params:** backuprevisionid in path; ?against= to compare a specific pair, otherwise the previous revision of the same asset and kind\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/backups/revisions/1188/diff\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "backuprevisionid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/backups/revisions/{backuprevisionid}/download": { - "get": { - "tags": [ - "plugin-backups" - ], - "summary": "Render a revision back to its native file format - for NTLARS, a .reg in either registry dialect, so it can be loaded...", - "description": "Render a revision back to its native file format - for NTLARS, a .reg in either registry dialect, so it can be loaded straight back onto a controller PC. Returns the file, NOT the success envelope\n\n**Auth:** permission:backups.download\n\n**Params:** backuprevisionid in path; ?format= one of the kind's formats (see /api/backups/kinds)\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" -OJ \"http://localhost:5001/api/backups/revisions/1188/download?format=ntlars\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "backuprevisionid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/backups/dashboard/stale": { - "get": { - "tags": [ - "plugin-backups" - ], - "summary": "Dashboard card (contract 0.19.0): chains whose backup has stopped running, judged on when a config was last CONFIRMED...", - "description": "Dashboard card (contract 0.19.0): chains whose backup has stopped running, judged on when a config was last CONFIRMED rather than last changed\n\n**Auth:** permission:backups.view\n\n**Params:** none; the threshold is the backups_staledays setting, 0 disables the card\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/backups/dashboard/stale\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - } - }, - "/api/tools/secrets": { - "post": { - "tags": [ - "plugin-tools" - ], - "summary": "Store an already-encrypted secret and return its one-view link token. The AES key lives in the URL fragment and never...", - "description": "Store an already-encrypted secret and return its one-view link token. The AES key lives in the URL fragment and never reaches the server, so what is held here is ciphertext nobody on this side can read - which is why SIZE is the only limit the server can enforce, and why this is for passwords and short text rather than documents\n\n**Auth:** permission:tools.secrets\n\n**Params:** body: ciphertext (base64, max 4096 chars), iv, expiryhours (1-168, default 24), views (1-5, default 1)\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" -d '{\"ciphertext\":\"...\",\"iv\":\"...\"}' http://localhost:5001/api/tools/secrets\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: ciphertext (base64, max 4096 chars), iv, expiryhours (1-168, default 24), views (1-5, default 1)" - } - } - } - } - } - }, - "/api/tools/secrets/{token}": { - "post": { - "tags": [ - "plugin-tools" - ], - "summary": "Redeem a secret: returns the ciphertext once and spends a view, deleting the row when the last view goes. POST rather...", - "description": "Redeem a secret: returns the ciphertext once and spends a view, deleting the row when the last view goes. POST rather than GET because chat and mail clients prefetch GET urls to build previews, and a preview must not consume a one-view secret. Missing, expired and spent answer identically so a guessed token learns nothing\n\n**Auth:** public\n\n**Params:** none\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/tools/secrets/9f8e7d6c5b4a39281706f5e4d3c2b1a0\n```", - "security": [], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "token", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "none" - } - } - } - } - }, - "get": { - "tags": [ - "plugin-tools" - ], - "summary": "Whether a link is still live, WITHOUT spending it. Lets the read page say \"a secret is waiting and opening it will...", - "description": "Whether a link is still live, WITHOUT spending it. Lets the read page say \"a secret is waiting and opening it will destroy it\" instead of consuming the only view to find out, and gives link scanners something harmless to fetch\n\n**Auth:** public\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/tools/secrets/9f8e7d6c5b4a39281706f5e4d3c2b1a0\n```", - "security": [], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "token", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/docs": { - "get": { - "tags": [ - "core-docs" - ], - "summary": "Redoc page rendering the generated spec. Public, and asset URLs are built with url_for so it works under any mount path", - "description": "Redoc page rendering the generated spec. Public, and asset URLs are built with url_for so it works under any mount path\n\n**Auth:** none\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/docs/\n```", - "security": [], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/docs/openapi.json": { - "get": { - "tags": [ - "core-docs" - ], - "summary": "The generated OpenAPI 3.1 spec - the machine-readable description of this API, and what MCP and other clients build...", - "description": "The generated OpenAPI 3.1 spec - the machine-readable description of this API, and what MCP and other clients build from. Returns the spec, NOT the success envelope\n\n**Auth:** none\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/docs/openapi.json\n```", - "security": [], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/docs/llms.txt": { - "get": { - "tags": [ - "core-docs" - ], - "summary": "Concise orientation guide for an LLM or a new integrator: auth schemes, the response envelope, and the common recipes...", - "description": "Concise orientation guide for an LLM or a new integrator: auth schemes, the response envelope, and the common recipes. Returns text/plain\n\n**Auth:** none\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/docs/llms.txt\n```", - "security": [], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/docs/redoc.standalone.js": { - "get": { - "tags": [ - "core-docs" - ], - "summary": "Vendored Redoc bundle, served locally so the documentation page works on an air-gapped server", - "description": "Vendored Redoc bundle, served locally so the documentation page works on an air-gapped server\n\n**Auth:** none\n\n**Params:** none\n\n**Example:**\n```\ncurl -I http://localhost:5001/api/docs/redoc.standalone.js\n```", - "security": [], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - } - }, - "/api/maplevels": { - "get": { - "tags": [ - "core-map" - ], - "summary": "Every building with its levels in display order, each carrying blueprint paths, native pixel size and marker count...", - "description": "Every building with its levels in display order, each carrying blueprint paths, native pixel size and marker count, plus defaultlevelid. PUBLIC: the printer installer map draws a blueprint before anyone logs in\n\n**Auth:** none\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/maplevels\n```", - "security": [], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - } - } - }, - "post": { - "tags": [ - "core-map" - ], - "summary": "Create a level. Name and sort order are separate because levels are not reliably numbered (basement, mezzanine, roof)...", - "description": "Create a level. Name and sort order are separate because levels are not reliably numbered (basement, mezzanine, roof), and gaps let one be inserted later without renumbering\n\n**Auth:** jwt + role:admin\n\n**Params:** body: buildingid and levelname (required), sortorder, blueprintlight, blueprintdark, mapwidth, mapheight, isdefault\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" -X POST http://localhost:5001/api/maplevels\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: buildingid and levelname (required), sortorder, blueprintlight, blueprintdark, mapwidth, mapheight, isdefault" - } - } - } - } - } - }, - "/api/maplevels/{levelid}": { - "get": { - "tags": [ - "core-map" - ], - "summary": "One level: name, building, blueprints and native size. That size is what mapx/mapy on this level are pixels of (ADR-017)", - "description": "One level: name, building, blueprints and native size. That size is what mapx/mapy on this level are pixels of (ADR-017)\n\n**Auth:** none\n\n**Params:** levelid in path\n\n**Example:**\n```\ncurl http://localhost:5001/api/maplevels/2\n```", - "security": [], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "levelid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - }, - "put": { - "tags": [ - "core-map" - ], - "summary": "Update a level. Changing mapwidth/mapheight returns a warning naming what it affects: the dimensions are the coordinate...", - "description": "Update a level. Changing mapwidth/mapheight returns a warning naming what it affects: the dimensions are the coordinate space every marker is expressed in, so a resize moves them all relative to the drawing - use a landmark transform instead\n\n**Auth:** jwt + role:admin\n\n**Params:** levelid in path; body: levelname, sortorder, blueprintlight, blueprintdark, mapwidth, mapheight, isdefault, isactive\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" -X PATCH http://localhost:5001/api/maplevels/1\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "levelid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "levelid in path; body: levelname, sortorder, blueprintlight, blueprintdark, mapwidth, mapheight, isdefault, isactive" - } - } - } - } - }, - "patch": { - "tags": [ - "core-map" - ], - "summary": "Update a level. Changing mapwidth/mapheight returns a warning naming what it affects: the dimensions are the coordinate...", - "description": "Update a level. Changing mapwidth/mapheight returns a warning naming what it affects: the dimensions are the coordinate space every marker is expressed in, so a resize moves them all relative to the drawing - use a landmark transform instead\n\n**Auth:** jwt + role:admin\n\n**Params:** levelid in path; body: levelname, sortorder, blueprintlight, blueprintdark, mapwidth, mapheight, isdefault, isactive\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" -X PATCH http://localhost:5001/api/maplevels/1\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "levelid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "levelid in path; body: levelname, sortorder, blueprintlight, blueprintdark, mapwidth, mapheight, isdefault, isactive" - } - } - } - } - }, - "delete": { - "tags": [ - "core-map" - ], - "summary": "Deactivate a level. Refused with 409 while assets are placed on it, and refused for the default level: deleting a...", - "description": "Deactivate a level. Refused with 409 while assets are placed on it, and refused for the default level: deleting a drawing out from under a marker leaves a position in a coordinate space that no longer exists\n\n**Auth:** jwt + role:admin\n\n**Params:** levelid in path\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" -X DELETE http://localhost:5001/api/maplevels/3\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "levelid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/maplevels/{levelid}/blueprint/{filename}": { - "get": { - "tags": [ - "core-map" - ], - "summary": "Serve a level's blueprint image, with sandbox headers so an SVG floor plan cannot execute as script", - "description": "Serve a level's blueprint image, with sandbox headers so an SVG floor plan cannot execute as script\n\n**Auth:** none\n\n**Params:** levelid and filename in path\n\n**Example:**\n```\ncurl -I http://localhost:5001/api/maplevels/2/blueprint/level-2-light.png\n```", - "security": [], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "levelid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "filename", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/maplevels/buildings": { - "post": { - "tags": [ - "core-map" - ], - "summary": "Create a building. An asset references the level, never the building, so the two cannot disagree", - "description": "Create a building. An asset references the level, never the building, so the two cannot disagree\n\n**Auth:** jwt + role:admin\n\n**Params:** body: buildingname (required), sortorder\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"buildingname\":\"Annex\"}' -X POST http://localhost:5001/api/maplevels/buildings\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: buildingname (required), sortorder" - } - } - } - } - } - }, - "/api/maplevels/buildings/{buildingid}": { - "put": { - "tags": [ - "core-map" - ], - "summary": "Rename or reorder a building. Levels move with it and nothing repositions", - "description": "Rename or reorder a building. Levels move with it and nothing repositions\n\n**Auth:** jwt + role:admin\n\n**Params:** buildingid in path; body: buildingname, sortorder, isactive\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" -X PATCH http://localhost:5001/api/maplevels/buildings/1\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "buildingid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "buildingid in path; body: buildingname, sortorder, isactive" - } - } - } - } - }, - "patch": { - "tags": [ - "core-map" - ], - "summary": "Rename or reorder a building. Levels move with it and nothing repositions", - "description": "Rename or reorder a building. Levels move with it and nothing repositions\n\n**Auth:** jwt + role:admin\n\n**Params:** buildingid in path; body: buildingname, sortorder, isactive\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" -X PATCH http://localhost:5001/api/maplevels/buildings/1\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "buildingid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "buildingid in path; body: buildingname, sortorder, isactive" - } - } - } - } - } - }, - "/api/maplevels/{levelid}/blueprint": { - "post": { - "tags": [ - "core-map" - ], - "summary": "Upload a level's blueprint. Reads the image's real pixel size from its header and adopts it when the level is EMPTY...", - "description": "Upload a level's blueprint. Reads the image's real pixel size from its header and adopts it when the level is EMPTY; with markers already placed it reports the mismatch and changes nothing, because adopting a new coordinate space silently moves every marker while looking like a successful upload\n\n**Auth:** jwt + role:admin\n\n**Params:** levelid in path; multipart/form-data: file=, theme=light|dark\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" -F file=@second-floor.png -F theme=light -X POST http://localhost:5001/api/maplevels/2/blueprint\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "levelid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "levelid in path; multipart/form-data: file=, theme=light|dark" - } - } - } - } - } - }, - "/api/mappositions/transform": { - "post": { - "tags": [ - "core-map" - ], - "summary": "Move every placed marker on a level by a transform derived per axis from landmark pairs. NEVER from image dimensions: a...", - "description": "Move every placed marker on a level by a transform derived per axis from landmark pairs. NEVER from image dimensions: a level added below another changes canvas height without rescaling anything, and a dimension-derived scale would stretch Y and be wrong everywhere. Dry run returns every old and new position plus which land outside the target. Applying snapshots first and clears mapverifiedat, because a transformed position is a guess awaiting review\n\n**Auth:** permission:assets.edit\n\n**Params:** body: levelid (required), landmarks [{fromx,fromy,tox,toy}] (two or more), tolevelid, assetids, dryrun (defaults TRUE)\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"levelid\":1,\"landmarks\":[{\"fromx\":0,\"fromy\":0,\"tox\":0,\"toy\":1450},{\"fromx\":1000,\"fromy\":1000,\"tox\":1000,\"toy\":2450}]}' -X POST http://localhost:5001/api/mappositions/transform\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: levelid (required), landmarks [{fromx,fromy,tox,toy}] (two or more), tolevelid, assetids, dryrun (defaults TRUE)" - } - } - } - } - } - }, - "/api/mappositions/positions": { - "post": { - "tags": [ - "core-map" - ], - "summary": "Set many positions at once, snapshotting first. levelid is per position rather than per request because a bulk save can...", - "description": "Set many positions at once, snapshotting first. levelid is per position rather than per request because a bulk save can span levels and inferring it is the guess this model exists to remove. Placing by hand counts as review, so mapverifiedat is stamped\n\n**Auth:** permission:assets.edit\n\n**Params:** body: positions [{assetid, mapx, mapy, levelid}] - levelid required per row - and verified\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"positions\":[{\"assetid\":42,\"mapx\":1200,\"mapy\":900,\"levelid\":2}]}' -X POST http://localhost:5001/api/mappositions/positions\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: positions [{assetid, mapx, mapy, levelid}] - levelid required per row - and verified" - } - } - } - } - } - }, - "/api/mappositions/verify": { - "post": { - "tags": [ - "core-map" - ], - "summary": "Mark markers reviewed against the current drawing without moving them - the common case in a review pass. No snapshot...", - "description": "Mark markers reviewed against the current drawing without moving them - the common case in a review pass. No snapshot, because no position changes\n\n**Auth:** permission:assets.edit\n\n**Params:** body: assetids (required), unverify\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"assetids\":[42,43]}' -X POST http://localhost:5001/api/mappositions/verify\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: assetids (required), unverify" - } - } - } - } - } - }, - "/api/mappositions/snapshots": { - "get": { - "tags": [ - "core-map" - ], - "summary": "Position snapshots, newest first, with what caused each and whether it has been restored. Metadata only - the positions...", - "description": "Position snapshots, newest first, with what caused each and whether it has been restored. Metadata only - the positions are large\n\n**Auth:** permission:assets.view\n\n**Params:** none; newest 50\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/mappositions/snapshots\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - } - }, - "/api/mappositions/snapshots/{snapshotid}/restore": { - "post": { - "tags": [ - "core-map" - ], - "summary": "Put a snapshot back, snapshotting first so an undo is itself undoable. Restores level and review state, not just...", - "description": "Put a snapshot back, snapshotting first so an undo is itself undoable. Restores level and review state, not just coordinates, and reports assets that no longer exist rather than failing the whole restore\n\n**Auth:** permission:assets.edit\n\n**Params:** snapshotid in path\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" -X POST http://localhost:5001/api/mappositions/snapshots/7/restore\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "snapshotid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "snapshotid in path" - } - } - } - } - } - }, - "/api/guides": { - "get": { - "tags": [ - "guides" - ], - "summary": "List guides. Published guides plus the caller's own drafts; someone with guides.edit sees every draft", - "description": "List guides. Published guides plus the caller's own drafts; someone with guides.edit sees every draft\n\n**Auth:** jwt + guides.view\n\n**Params:** query: q (searches title, summary, tags and body), tag\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/guides?tag=partmarker' -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - }, - "post": { - "tags": [ - "guides" - ], - "summary": "Create a guide as a draft. Every block needs a unique, stable id: the id is what a reader's tick is remembered against...", - "description": "Create a guide as a draft. Every block needs a unique, stable id: the id is what a reader's tick is remembered against, and a save without one is refused\n\n**Auth:** jwt + guides.create\n\n**Params:** body: title (required), summary, tags (comma separated), blocks (array), note\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/guides -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"title\":\"Swap a controller\",\"tags\":\"partmarker\",\"blocks\":[{\"id\":\"b1\",\"type\":\"paragraph\",\"text\":\"Power it off.\"}]}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: title (required), summary, tags (comma separated), blocks (array), note" - } - } - } - } - } - }, - "/api/guides/tags": { - "get": { - "tags": [ - "guides" - ], - "summary": "Every tag in use with a count, for filter chips", - "description": "Every tag in use with a count, for filter chips\n\n**Auth:** jwt + guides.view\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/guides/tags -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - } - }, - "/api/guides/{slug}": { - "get": { - "tags": [ - "guides" - ], - "summary": "Read a guide as a READER: returns the PUBLISHED revision, not the working copy, so an edit in progress does not change...", - "description": "Read a guide as a READER: returns the PUBLISHED revision, not the working copy, so an edit in progress does not change what somebody following the procedure sees. 404 (not 403) for an unpublished guide the caller cannot edit\n\n**Auth:** jwt + guides.view\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/guides/replace-a-marker -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "slug", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/guides/{guideid}/working": { - "get": { - "tags": [ - "guides" - ], - "summary": "The working copy, for the editor. Requires guides.edit, or guides.create while the guide is your own and unpublished", - "description": "The working copy, for the editor. Requires guides.edit, or guides.create while the guide is your own and unpublished\n\n**Auth:** jwt + guides.view (+ edit rights)\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/guides/12/working -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "guideid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/guides/{guideid}/revisions": { - "get": { - "tags": [ - "guides" - ], - "summary": "Version history, newest first, flagging which revision is published", - "description": "Version history, newest first, flagging which revision is published\n\n**Auth:** jwt + guides.view\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/guides/12/revisions -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "guideid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/guides/revisions/{revisionid}": { - "get": { - "tags": [ - "guides" - ], - "summary": "One revision including its blocks, for viewing or comparing an old version", - "description": "One revision including its blocks, for viewing or comparing an old version\n\n**Auth:** jwt + guides.view\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/guides/revisions/34 -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "revisionid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/guides/{guideid}": { - "put": { - "tags": [ - "guides" - ], - "summary": "Save the working copy. Writes a new revision ONLY when the content hash changes, so an autosave that changed nothing...", - "description": "Save the working copy. Writes a new revision ONLY when the content hash changes, so an autosave that changed nothing records nothing; the response carries saved=false in that case\n\n**Auth:** jwt + guides.view (+ edit rights)\n\n**Params:** body: any of title, summary, tags, blocks, note\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/guides/12 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"blocks\":[]}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "guideid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: any of title, summary, tags, blocks, note" - } - } - } - } - }, - "delete": { - "tags": [ - "guides" - ], - "summary": "Delete a guide and its revisions and images", - "description": "Delete a guide and its revisions and images\n\n**Auth:** jwt + guides.delete\n\n**Params:** none\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/guides/12 -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "guideid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/guides/{guideid}/publish": { - "post": { - "tags": [ - "guides" - ], - "summary": "Point readers at the latest saved revision. Promotes an existing revision rather than snapshotting, so a publish cannot...", - "description": "Point readers at the latest saved revision. Promotes an existing revision rather than snapshotting, so a publish cannot ship unsaved edits nobody reviewed\n\n**Auth:** jwt + guides.publish\n\n**Params:** none\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/guides/12/publish -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "guideid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "none" - } - } - } - } - } - }, - "/api/guides/{guideid}/unpublish": { - "post": { - "tags": [ - "guides" - ], - "summary": "Withdraw from readers without deleting anything; revisions survive so the guide can be corrected and published again", - "description": "Withdraw from readers without deleting anything; revisions survive so the guide can be corrected and published again\n\n**Auth:** jwt + guides.publish\n\n**Params:** none\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/guides/12/unpublish -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "guideid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "none" - } - } - } - } - } - }, - "/api/guides/{guideid}/restore/{revisionid}": { - "post": { - "tags": [ - "guides" - ], - "summary": "Bring an old revision back as a NEW revision. History is never rewritten, so what was published in between stays on...", - "description": "Bring an old revision back as a NEW revision. History is never rewritten, so what was published in between stays on record\n\n**Auth:** jwt + guides.view (+ edit rights)\n\n**Params:** none\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/guides/12/restore/34 -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "guideid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "revisionid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "none" - } - } - } - } - } - }, - "/api/guides/images": { - "post": { - "tags": [ - "guides" - ], - "summary": "Upload an image for a guide. guideid is optional because an image is pasted before a new guide has an id; the editor...", - "description": "Upload an image for a guide. guideid is optional because an image is pasted before a new guide has an id; the editor attaches it on save\n\n**Auth:** jwt + guides.create\n\n**Params:** multipart: file (png/jpeg/gif/webp, max 8 MB), guideid (optional)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/guides/images -H \"Authorization: Bearer $TOK\" -F file=@screenshot.png -F guideid=12\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "multipart: file (png/jpeg/gif/webp, max 8 MB), guideid (optional)" - } - } - } - } - } - }, - "/api/guides/images/{imageid}": { - "get": { - "tags": [ - "guides" - ], - "summary": "Serve an uploaded image; cached immutably since an image is replaced rather than edited", - "description": "Serve an uploaded image; cached immutably since an image is replaced rather than edited\n\n**Auth:** jwt + guides.view\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/guides/images/5 -H \"Authorization: Bearer $TOK\" --output shot.png\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "imageid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/endpoints": { - "get": { - "tags": [ - "plugin-endpoints" - ], - "summary": "List endpoint states. Registered both with and without the trailing slash, so both forms are described here", - "description": "List endpoint states. Registered both with and without the trailing slash, so both forms are described here\n\n**Auth:** endpoints.view\n\n**Params:** query: search (hostname substring)\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - } - }, - "/api/endpoints/{asset_id}": { - "get": { - "tags": [ - "plugin-endpoints" - ], - "summary": "One endpoint: its agent state, version, and last check-in", - "description": "One endpoint: its agent state, version, and last check-in\n\n**Auth:** endpoints.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/1asset_id\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "asset_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/endpoints/{asset_id}/events": { - "get": { - "tags": [ - "plugin-endpoints" - ], - "summary": "Agent event history for one endpoint, newest first", - "description": "Agent event history for one endpoint, newest first\n\n**Auth:** endpoints.view\n\n**Params:** path: asset_id; query: eventid (page from this id)\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/1asset_id/events\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "asset_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/endpoints/asset/{asset_id}/status": { - "get": { - "tags": [ - "plugin-endpoints" - ], - "summary": "Asset-detail panel: online state, uptime, and agent version for one endpoint", - "description": "Asset-detail panel: online state, uptime, and agent version for one endpoint\n\n**Auth:** endpoints.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/asset/1asset_id/status\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "asset_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/endpoints/asset/{asset_id}/telemetrymeta": { - "get": { - "tags": [ - "plugin-endpoints" - ], - "summary": "Asset-detail panel: what telemetry this endpoint reports and when each series was last updated", - "description": "Asset-detail panel: what telemetry this endpoint reports and when each series was last updated\n\n**Auth:** endpoints.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/asset/1asset_id/telemetrymeta\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "asset_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/endpoints/asset/{asset_id}/services": { - "get": { - "tags": [ - "plugin-endpoints" - ], - "summary": "Asset-detail panel: Windows services the agent watches on this endpoint, with running state", - "description": "Asset-detail panel: Windows services the agent watches on this endpoint, with running state\n\n**Auth:** endpoints.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/asset/1asset_id/services\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "asset_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/endpoints/asset/{asset_id}/recentevents": { - "get": { - "tags": [ - "plugin-endpoints" - ], - "summary": "Asset-detail panel: the most recent agent events for one endpoint", - "description": "Asset-detail panel: the most recent agent events for one endpoint\n\n**Auth:** endpoints.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/asset/1asset_id/recentevents\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "asset_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/endpoints/asset/{asset_id}/activity": { - "get": { - "tags": [ - "plugin-endpoints" - ], - "summary": "Asset-detail panel: one chronological feed merging curated Windows events with operator commands, newest first, capped...", - "description": "Asset-detail panel: one chronological feed merging curated Windows events with operator commands, newest first, capped at 15. Both are keyed by assetid, so this is two indexed reads and a merge rather than a join. A failed command is marked critical. The requester name is withheld from callers who do not themselves hold endpoints.command, so a read-only viewer gets the history without the roster of who restarted what\n\n**Auth:** endpoints.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/asset/1/activity\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "asset_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/endpoints/asset/{asset_id}/interfaces": { - "get": { - "tags": [ - "plugin-endpoints" - ], - "summary": "Asset-detail panel: network interfaces as the collector reports them ({alias, ipv4, mac, gateway, subnet, dhcp})", - "description": "Asset-detail panel: network interfaces as the collector reports them ({alias, ipv4, mac, gateway, subnet, dhcp})\n\n**Auth:** endpoints.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/asset/1asset_id/interfaces\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "asset_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/endpoints/dashboard/summary": { - "get": { - "tags": [ - "plugin-endpoints" - ], - "summary": "Dashboard card: endpoint counts by online/offline/never-seen", - "description": "Dashboard card: endpoint counts by online/offline/never-seen\n\n**Auth:** endpoints.view\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/dashboard/summary\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - } - }, - "/api/endpoints/dashboard/quiet": { - "get": { - "tags": [ - "plugin-endpoints" - ], - "summary": "Dashboard card: endpoints that have gone quiet - no check-in inside the configured quiet-hours window", - "description": "Dashboard card: endpoints that have gone quiet - no check-in inside the configured quiet-hours window\n\n**Auth:** endpoints.view\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/dashboard/quiet\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - } - }, - "/api/endpoints/dashboard/stability": { - "get": { - "tags": [ - "plugin-endpoints" - ], - "summary": "Dashboard card: endpoints ranked by reboot/agent-restart churn, to surface a machine that is flapping", - "description": "Dashboard card: endpoints ranked by reboot/agent-restart churn, to surface a machine that is flapping\n\n**Auth:** endpoints.view\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/dashboard/stability\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - } - }, - "/api/endpoints/agentkey": { - "get": { - "tags": [ - "plugin-endpoints" - ], - "summary": "Status of the endpoints agent key: whether one exists, when it was issued, and its last use. Never returns the key...", - "description": "Status of the endpoints agent key: whether one exists, when it was issued, and its last use. Never returns the key itself\n\n**Auth:** admin role\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/agentkey\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - }, - "delete": { - "tags": [ - "plugin-endpoints" - ], - "summary": "Revoke the endpoints agent key. The kill switch for agent ingest: reports and command polls stop being authorized...", - "description": "Revoke the endpoints agent key. The kill switch for agent ingest: reports and command polls stop being authorized immediately\n\n**Auth:** admin role\n\n**Params:** none\n\n**Example:**\n```\ncurl -X DELETE -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/agentkey\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - } - }, - "/api/endpoints/agentkey/regenerate": { - "post": { - "tags": [ - "plugin-endpoints" - ], - "summary": "Issue a new endpoints agent key and return it ONCE. The key is scoped to collector.ingest.endpoints only, so rotating...", - "description": "Issue a new endpoints agent key and return it ONCE. The key is scoped to collector.ingest.endpoints only, so rotating it cannot disturb the computers or GE-Enforce collectors. Every agent must be reconfigured with the new value\n\n**Auth:** admin role\n\n**Params:** none\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/agentkey/regenerate\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "none" - } - } - } - } - } - }, - "/api/endpoints/{asset_id}/commands": { - "get": { - "tags": [ - "plugin-endpoints" - ], - "summary": "Command history for one endpoint: what was queued, claimed, and what it returned", - "description": "Command history for one endpoint: what was queued, claimed, and what it returned\n\n**Auth:** endpoints.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/1asset_id/commands\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "asset_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - }, - "post": { - "tags": [ - "plugin-endpoints" - ], - "summary": "Queue one allowlisted command for an endpoint. Unknown actions are rejected, destructive ones (system_restart) demand a...", - "description": "Queue one allowlisted command for an endpoint. Unknown actions are rejected, destructive ones (system_restart) demand a stricter permission, the effective policy must permit the action for this endpoint, and a second command will not stack while one is pending or running\n\n**Auth:** endpoints.command\n\n**Params:** path: asset_id; body: action (required, allowlisted), target, targetkind\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"action\":\"service_restart\",\"target\":\"MTConnectAgent\",\"targetkind\":\"service\"}' http://localhost:5001/api/endpoints/1/commands\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "asset_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "path: asset_id; body: action (required, allowlisted), target, targetkind" - } - } - } - } - } - }, - "/api/endpoints/commands/next": { - "get": { - "tags": [ - "plugin-endpoints" - ], - "summary": "Agent long-poll. Claims the oldest pending command for the calling host and atomically marks it running so no second...", - "description": "Agent long-poll. Claims the oldest pending command for the calling host and atomically marks it running so no second poll claims it; 204 when nothing is queued inside the hold window, and the agent re-polls. Collector-token auth, not JWT\n\n**Auth:** collector token (collector.ingest.endpoints)\n\n**Params:** query: hostname (required)\n\n**Example:**\n```\ncurl -H \"X-Collector-Key: $KEY\" \"http://localhost:5001/api/endpoints/commands/next?hostname=WJSF-PC-01\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - } - }, - "/api/endpoints/commands/{command_id}/result": { - "post": { - "tags": [ - "plugin-endpoints" - ], - "summary": "Agent reports a claimed command result. Finalizes the command row and, for MTConnect actions that observed run state...", - "description": "Agent reports a claimed command result. Finalizes the command row and, for MTConnect actions that observed run state, caches that on the EndpointState so the UI toggle reflects reality between polls - both in one commit. Collector-token auth, not JWT\n\n**Auth:** collector token (collector.ingest.endpoints)\n\n**Params:** path: command_id; body: the command outcome (status, output)\n\n**Example:**\n```\ncurl -X POST -H \"X-Collector-Key: $KEY\" -H 'Content-Type: application/json' -d '{\"status\":\"succeeded\",\"output\":\"ok\"}' http://localhost:5001/api/endpoints/commands/1/result\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "command_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "path: command_id; body: the command outcome (status, output)" - } - } - } - } - } - }, - "/api/endpoints/monitors": { - "get": { - "tags": [ - "plugin-endpoints" - ], - "summary": "List every monitor definition, shipped and local, ordered shipped-first then by display name", - "description": "List every monitor definition, shipped and local, ordered shipped-first then by display name\n\n**Auth:** jwt + require_permission endpoints.view\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/monitors -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - }, - "post": { - "tags": [ - "plugin-endpoints" - ], - "summary": "Create a local monitor definition; source is forced to 'local' so a caller cannot mint a shipped row", - "description": "Create a local monitor definition; source is forced to 'local' so a caller cannot mint a shipped row\n\n**Auth:** jwt + require_role admin\n\n**Params:** body: kind (process|service, required), displayname (required), processname (required for process), servicename (required for service), launchpath, datacollectorpath, logcollectorpath, isactive (default true)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/monitors -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"kind\":\"service\",\"displayname\":\"Spooler\",\"servicename\":\"Spooler\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: kind (process|service, required), displayname (required), processname (required for process), servicename (required for service), launchpath, datacollectorpath, logcollectorpath, isactive (default true)" - } - } - } - } - } - }, - "/api/endpoints/monitors/{monitor_id}": { - "put": { - "tags": [ - "plugin-endpoints" - ], - "summary": "Update a local monitor definition; 403 on a shipped row, which is product-owned and read-only", - "description": "Update a local monitor definition; 403 on a shipped row, which is product-owned and read-only\n\n**Auth:** jwt + require_role admin\n\n**Params:** path: monitor_id; body: same fields as create, all optional\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/endpoints/monitors/4 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"isactive\":false}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "monitor_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "path: monitor_id; body: same fields as create, all optional" - } - } - } - } - }, - "delete": { - "tags": [ - "plugin-endpoints" - ], - "summary": "Delete a local monitor definition; 403 on a shipped row", - "description": "Delete a local monitor definition; 403 on a shipped row\n\n**Auth:** jwt + require_role admin\n\n**Params:** path: monitor_id\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/endpoints/monitors/4 -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "monitor_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/endpoints/monitors/resolve": { - "get": { - "tags": [ - "plugin-endpoints" - ], - "summary": "Resolve the monitor set a PC type should watch: shipped defaults for 'Shopfloor PC' plus whatever active site policy...", - "description": "Resolve the monitor set a PC type should watch: shipped defaults for 'Shopfloor PC' plus whatever active site policy attaches to that type. The human-facing view of what the agent will be told\n\n**Auth:** jwt + require_permission endpoints.view\n\n**Params:** query: pctype (required)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/endpoints/monitors/resolve?pctype=Shopfloor+PC' -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - } - }, - "/api/endpoints/monitors/resolved": { - "get": { - "tags": [ - "plugin-endpoints" - ], - "summary": "Agent runtime watch-list. The agent calls this each telemetry cycle and receives {processes[], services[]} shaped like...", - "description": "Agent runtime watch-list. The agent calls this each telemetry cycle and receives {processes[], services[]} shaped like its own MonitorConfig, so a policy-attached monitor is watched without editing appsettings.json on the bay. Unknown hostname is a 404; a PC type with no monitors returns empty lists\n\n**Auth:** endpoints collector API key (X-API-Key/Bearer), not JWT\n\n**Params:** query: hostname (required)\n\n**Example:**\n```\ncurl 'http://localhost:5001/api/endpoints/monitors/resolved?hostname=F7MHPF24' -H \"X-API-Key: $ENDPOINTS_KEY\"\n```", - "security": [ - { - "apiKeyAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - } - }, - "/api/endpoints/operatorreports": { - "post": { - "tags": [ - "plugin-endpoints" - ], - "summary": "Agent uploads one operator issue report captured from the tray. The hostname must resolve to a computer asset. The...", - "description": "Agent uploads one operator issue report captured from the tray. The hostname must resolve to a computer asset. The screenshot is stored under a server-generated uuid filename; a retention sweep runs opportunistically so the feed stays bounded without a scheduler\n\n**Auth:** endpoints collector API key (X-API-Key/Bearer), not JWT\n\n**Params:** multipart/form-data: hostname (required), reason (default 'Other (free text)', truncated to 128), details, screenshot (png, 10 MB cap)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/operatorreports -H \"X-API-Key: $ENDPOINTS_KEY\" -F hostname=F7MHPF24 -F 'details=Machine will not home' -F screenshot=@shot.png\n```", - "security": [ - { - "apiKeyAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "multipart/form-data: hostname (required), reason (default 'Other (free text)', truncated to 128), details, screenshot (png, 10 MB cap)" - } - } - } - } - } - }, - "/api/endpoints/panel/{asset_id}/operatorreports": { - "get": { - "tags": [ - "plugin-endpoints" - ], - "summary": "Operator Reports asset panel: this endpoint's reports newest first, each carrying a hasimage flag so the viewer knows...", - "description": "Operator Reports asset panel: this endpoint's reports newest first, each carrying a hasimage flag so the viewer knows whether to request the screenshot\n\n**Auth:** jwt + require_permission endpoints.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/panel/46/operatorreports -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "asset_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/endpoints/operatorreports/{operatorreportid}/image": { - "get": { - "tags": [ - "plugin-endpoints" - ], - "summary": "Serve one operator report's screenshot, hardened with the shared upload headers (CSP sandbox + nosniff); 404 when the...", - "description": "Serve one operator report's screenshot, hardened with the shared upload headers (CSP sandbox + nosniff); 404 when the report carries no image\n\n**Auth:** jwt + require_permission endpoints.view\n\n**Params:** path: operatorreportid\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/operatorreports/12/image -H \"Authorization: Bearer $TOK\" -o shot.png\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "operatorreportid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/endpoints/asset/{asset_id}/hardware": { - "get": { - "tags": [ - "plugin-endpoints" - ], - "summary": "Asset-detail panel: processor, memory and chassis inventory. Agent 1.7.0 and later only; an older agent leaves the...", - "description": "Asset-detail panel: processor, memory and chassis inventory. Agent 1.7.0 and later only; an older agent leaves the columns null and the panel hides itself rather than rendering blanks\n\n**Auth:** endpoints.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/asset/1/hardware\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "asset_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/endpoints/asset/{asset_id}/osinfo": { - "get": { - "tags": [ - "plugin-endpoints" - ], - "summary": "Asset-detail panel: operating system, firmware and domain membership as one card (both answer \"what is this box...", - "description": "Asset-detail panel: operating system, firmware and domain membership as one card (both answer \"what is this box running\")\n\n**Auth:** endpoints.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/asset/1/osinfo\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "asset_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/endpoints/asset/{asset_id}/storage": { - "get": { - "tags": [ - "plugin-endpoints" - ], - "summary": "Asset-detail panel: volume capacity and physical drive health as two sections. They share no key (volumes vs physical...", - "description": "Asset-detail panel: volume capacity and physical drive health as two sections. They share no key (volumes vs physical devices) and are never reconciled across sections\n\n**Auth:** endpoints.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/asset/1/storage\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "asset_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/endpoints/asset/{asset_id}/diskhealth": { - "get": { - "tags": [ - "plugin-endpoints" - ], - "summary": "Physical drive health rows, anything predicting failure sorted first. No longer backs a panel of its own (Storage...", - "description": "Physical drive health rows, anything predicting failure sorted first. No longer backs a panel of its own (Storage renders it as a section) but kept as a stable read for callers outside the panel renderer\n\n**Auth:** endpoints.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/asset/1/diskhealth\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "asset_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/endpoints/asset/{asset_id}/listeners": { - "get": { - "tags": [ - "plugin-endpoints" - ], - "summary": "Asset-detail panel: TCP sockets in LISTEN with the owning process, sorted by port. The full connection table is an...", - "description": "Asset-detail panel: TCP sockets in LISTEN with the owning process, sorted by port. The full connection table is an on-demand command, not standard telemetry\n\n**Auth:** endpoints.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/asset/1/listeners\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "asset_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/endpoints/asset/{asset_id}/mappeddrives": { - "get": { - "tags": [ - "plugin-endpoints" - ], - "summary": "Asset-detail panel: mapped network drives with a health badge each. Session-scoped, so an empty list when nobody is...", - "description": "Asset-detail panel: mapped network drives with a health badge each. Session-scoped, so an empty list when nobody is signed in is a valid state, not a failure\n\n**Auth:** endpoints.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/asset/1/mappeddrives\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "asset_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/endpoints/asset/{asset_id}/usercontext": { - "get": { - "tags": [ - "plugin-endpoints" - ], - "summary": "Asset-detail panel: session-scoped data from the interactive user probe. Mapped drives arrive from two sources that can...", - "description": "Asset-detail panel: session-scoped data from the interactive user probe. Mapped drives arrive from two sources that can disagree and the worse status wins, so a half-broken share is never shown healthy\n\n**Auth:** endpoints.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/asset/1/usercontext\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "asset_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/endpoints/asset/{asset_id}/iphistory": { - "get": { - "tags": [ - "plugin-endpoints" - ], - "summary": "Asset-detail panel: every address this asset has held, newest change first. \"Last changed\" is when a field on the row...", - "description": "Asset-detail panel: every address this asset has held, newest change first. \"Last changed\" is when a field on the row moved, NOT when the address was last seen\n\n**Auth:** endpoints.view\n\n**Params:** path: asset_id\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/endpoints/asset/1/iphistory\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "asset_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/scripts": { - "get": { - "tags": [ - "core-scripts" - ], - "summary": "List script catalog entries, active only by default", - "description": "List script catalog entries, active only by default\n\n**Auth:** jwt + require_permission applications.view\n\n**Params:** query: includeinactive (default false)\n\n**Example:**\n```\ncurl http://localhost:5001/api/scripts -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - }, - "post": { - "tags": [ - "core-scripts" - ], - "summary": "Create a script catalog entry; the body is uploaded separately so metadata and content version independently", - "description": "Create a script catalog entry; the body is uploaded separately so metadata and content version independently\n\n**Auth:** jwt + require_permission applications.create\n\n**Params:** body: name (required), description, runcontext (system|user, default system)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/scripts -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"name\":\"Restart spooler\",\"runcontext\":\"system\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: name (required), description, runcontext (system|user, default system)" - } - } - } - } - } - }, - "/api/scripts/{script_id}": { - "get": { - "tags": [ - "core-scripts" - ], - "summary": "Get one script entry with its metadata and run context", - "description": "Get one script entry with its metadata and run context\n\n**Auth:** jwt + require_permission applications.view\n\n**Params:** path: script_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/scripts/3 -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "script_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - }, - "put": { - "tags": [ - "core-scripts" - ], - "summary": "Update a script entry's metadata; the stored body is untouched", - "description": "Update a script entry's metadata; the stored body is untouched\n\n**Auth:** jwt + require_permission applications.edit\n\n**Params:** path: script_id; body: name, description, runcontext, isactive\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/scripts/3 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"description\":\"Clears the print queue\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "script_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "path: script_id; body: name, description, runcontext, isactive" - } - } - } - } - }, - "delete": { - "tags": [ - "core-scripts" - ], - "summary": "Remove a script catalog entry", - "description": "Remove a script catalog entry\n\n**Auth:** jwt + require_permission applications.delete\n\n**Params:** path: script_id\n\n**Example:**\n```\ncurl -X DELETE http://localhost:5001/api/scripts/3 -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "script_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/scripts/{script_id}/body": { - "post": { - "tags": [ - "core-scripts" - ], - "summary": "Upload or replace the script body; stored on disk and hashed so a delivery can pin the exact bytes", - "description": "Upload or replace the script body; stored on disk and hashed so a delivery can pin the exact bytes\n\n**Auth:** jwt + require_permission applications.edit\n\n**Params:** path: script_id; multipart/form-data: file (the .ps1)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/scripts/3/body -H \"Authorization: Bearer $TOK\" -F 'file=@restart-spooler.ps1'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "script_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "path: script_id; multipart/form-data: file (the .ps1)" - } - } - } - } - } - }, - "/api/scripts/body/{filename}": { - "get": { - "tags": [ - "core-scripts" - ], - "summary": "Download a stored script body for review in the UI", - "description": "Download a stored script body for review in the UI\n\n**Auth:** jwt + require_permission applications.view\n\n**Params:** path: filename (as stored)\n\n**Example:**\n```\ncurl http://localhost:5001/api/scripts/body/restart-spooler.ps1 -H \"Authorization: Bearer $TOK\" -o restart-spooler.ps1\n```", + "summary": "Download a proof document.", + "description": "Download a proof document.\n\n**Auth:** permission:warranty.view\n\n**Params:** path: filename (path)\n\n**Example:**\n```\ncurl http://localhost:5001/api/warranty/proof/example.png -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -24136,282 +28003,20 @@ "in": "path", "required": true, "schema": { - "type": "string" + "type": "string", + "description": "may contain slashes" } } ] } }, - "/api/endpoints/apppackage/{appversionid}": { + "/api/warranty/report": { "get": { "tags": [ - "endpoints-delivery" + "plugin-warranty" ], - "summary": "Agent-facing installer download for a pinned app version; serves the exact bytes the update_app descriptor pinned by...", - "description": "Agent-facing installer download for a pinned app version; serves the exact bytes the update_app descriptor pinned by SHA-256\n\n**Auth:** collector token (X-API-Key), endpoints scope\n\n**Params:** path: appversionid (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/apppackage/12 -H 'X-API-Key: $COLLECTOR_KEY' -o setup.exe\n```", - "security": [ - { - "apiKeyAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "appversionid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/endpoints/apppackage/app/{app_id}": { - "get": { - "tags": [ - "endpoints-delivery" - ], - "summary": "Agent-facing installer download resolved from the application rather than a specific version", - "description": "Agent-facing installer download resolved from the application rather than a specific version\n\n**Auth:** collector token (X-API-Key), endpoints scope\n\n**Params:** path: app_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/apppackage/app/4 -H 'X-API-Key: $COLLECTOR_KEY' -o setup.exe\n```", - "security": [ - { - "apiKeyAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "app_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/endpoints/apppackage/app/{app_id}/{filename}": { - "get": { - "tags": [ - "endpoints-delivery" - ], - "summary": "As above, naming the stored file explicitly so the agent writes it back under the expected name", - "description": "As above, naming the stored file explicitly so the agent writes it back under the expected name\n\n**Auth:** collector token (X-API-Key), endpoints scope\n\n**Params:** path: app_id (int), filename\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/apppackage/app/4/setup.exe -H 'X-API-Key: $COLLECTOR_KEY' -o setup.exe\n```", - "security": [ - { - "apiKeyAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "app_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "filename", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/endpoints/scriptbody/{script_id}": { - "get": { - "tags": [ - "endpoints-delivery" - ], - "summary": "Agent-facing script download for a run_script delivery, on the same key as the rest of the delivery path", - "description": "Agent-facing script download for a run_script delivery, on the same key as the rest of the delivery path\n\n**Auth:** collector token (X-API-Key), endpoints scope\n\n**Params:** path: script_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/scriptbody/3 -H 'X-API-Key: $COLLECTOR_KEY' -o run.ps1\n```", - "security": [ - { - "apiKeyAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "script_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/endpoints/asset/{asset_id}/applications": { - "get": { - "tags": [ - "endpoints-delivery" - ], - "summary": "Installed-software snapshot the agent last reported for this endpoint, for the asset detail panel; a per-report...", - "description": "Installed-software snapshot the agent last reported for this endpoint, for the asset detail panel; a per-report snapshot rather than history\n\n**Auth:** jwt + require_permission endpoints.view\n\n**Params:** path: asset_id (int)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/asset/313/applications -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "asset_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/endpoints/siteagent": { - "get": { - "tags": [ - "endpoints-siteagent" - ], - "summary": "Report whether a prebuilt agent binary is staged and can therefore be packaged", - "description": "Report whether a prebuilt agent binary is staged and can therefore be packaged\n\n**Auth:** jwt + require_role admin\n\n**Params:** none\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/siteagent -H \"Authorization: Bearer $TOK\"\n```", + "summary": "Counts + lists bucketed by derived status.", + "description": "Counts + lists bucketed by derived status.\n\n**Auth:** permission:warranty.view\n\n**Params:** not documented\n\n**Example:**\n```\ncurl http://localhost:5001/api/warranty/report -H \"Authorization: Bearer $TOK\"\n```", "security": [ { "bearerAuth": [] @@ -24443,13 +28048,13 @@ } } }, - "/api/endpoints/siteagent/package": { + "/api/warranty/sync/dell": { "post": { "tags": [ - "endpoints-siteagent" + "plugin-warranty" ], - "summary": "Bundle the agent as a downloadable ZIP with a generated install.ps1 that provisions credentials, so a bay install needs...", - "description": "Bundle the agent as a downloadable ZIP with a generated install.ps1 that provisions credentials, so a bay install needs no hand editing\n\n**Auth:** jwt + require_role admin\n\n**Params:** body: baseurl, collectorkey, cadenceseconds, and the site TLS cert when one is readable\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/siteagent/package -H \"Authorization: Bearer $TOK\" -o shopdb-agent.zip\n```", + "summary": "Look up every asset serial (= Dell service tag) that has no warranty yet and create warranties for the ones Dell...", + "description": "Look up every asset serial (= Dell service tag) that has no warranty yet and create warranties for the ones Dell recognizes.\n\n**Auth:** permission:warranty.edit\n\n**Params:** body: JSON (fields not yet documented)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/warranty/sync/dell -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{}'\n```", "security": [ { "bearerAuth": [] @@ -24486,770 +28091,7 @@ "schema": { "type": "object", "additionalProperties": true, - "description": "body: baseurl, collectorkey, cadenceseconds, and the site TLS cert when one is readable" - } - } - } - } - } - }, - "/api/endpoints/adoptiontool/package": { - "post": { - "tags": [ - "endpoints-siteagent" - ], - "summary": "Bundle the agentless adoption tool, for taking an inventory of a machine that will not run the service", - "description": "Bundle the agentless adoption tool, for taking an inventory of a machine that will not run the service\n\n**Auth:** jwt + require_role admin\n\n**Params:** body: as for the site agent package\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/adoptiontool/package -H \"Authorization: Bearer $TOK\" -o shopdb-adoption.zip\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: as for the site agent package" - } - } - } - } - } - }, - "/api/endpoints/siteagent/binaries/{binkey}": { - "get": { - "tags": [ - "endpoints-siteagent" - ], - "summary": "List the staged versions of one agent binary, newest first, with the current one marked", - "description": "List the staged versions of one agent binary, newest first, with the current one marked\n\n**Auth:** jwt + require_permission endpoints.agentpackage\n\n**Params:** path: binkey (agent|tray)\n\n**Example:**\n```\ncurl http://localhost:5001/api/endpoints/siteagent/binaries/agent -H \"Authorization: Bearer $TOK\"\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "binkey", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - }, - "post": { - "tags": [ - "endpoints-siteagent" - ], - "summary": "Stage an uploaded binary as a new version. This supplies the executable every future package installs and runs as...", - "description": "Stage an uploaded binary as a new version. This supplies the executable every future package installs and runs as SYSTEM on the fleet, so it is audit-logged with the stored file's sha256\n\n**Auth:** jwt + require_permission endpoints.agentpackage\n\n**Params:** path: binkey (agent|tray); multipart/form-data: file (a Windows PE, 64 MB cap), version (strict dotted-numeric), makecurrent (default true)\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/siteagent/binaries/agent -H \"Authorization: Bearer $TOK\" -F file=@shopdbagent.exe -F version=1.6.3\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "binkey", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "path: binkey (agent|tray); multipart/form-data: file (a Windows PE, 64 MB cap), version (strict dotted-numeric), makecurrent (default true)" - } - } - } - } - } - }, - "/api/endpoints/siteagent/binaries/{binkey}/detect": { - "post": { - "tags": [ - "endpoints-siteagent" - ], - "summary": "Read the version out of an uploaded PE's resource table so the upload form can prefill it. Stores nothing", - "description": "Read the version out of an uploaded PE's resource table so the upload form can prefill it. Stores nothing\n\n**Auth:** jwt + require_permission endpoints.agentpackage\n\n**Params:** path: binkey; multipart/form-data: file\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/siteagent/binaries/agent/detect -H \"Authorization: Bearer $TOK\" -F file=@shopdbagent.exe\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "binkey", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "path: binkey; multipart/form-data: file" - } - } - } - } - } - }, - "/api/endpoints/siteagent/binaries/{binkey}/adopt": { - "post": { - "tags": [ - "endpoints-siteagent" - ], - "summary": "Adopt a binary already staged on disk into the version manifest, for a build that predates the store", - "description": "Adopt a binary already staged on disk into the version manifest, for a build that predates the store\n\n**Auth:** jwt + require_permission endpoints.agentpackage\n\n**Params:** path: binkey; body: version\n\n**Example:**\n```\ncurl -X POST http://localhost:5001/api/endpoints/siteagent/binaries/agent/adopt -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"version\":\"1.6.2\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "binkey", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "path: binkey; body: version" - } - } - } - } - } - }, - "/api/endpoints/siteagent/binaries/{binkey}/current": { - "put": { - "tags": [ - "endpoints-siteagent" - ], - "summary": "Point the current staged build at another version, so a package rolls back to it. The retention sweep never prunes the...", - "description": "Point the current staged build at another version, so a package rolls back to it. The retention sweep never prunes the current one\n\n**Auth:** jwt + require_permission endpoints.agentpackage\n\n**Params:** path: binkey; body: version (must already be staged)\n\n**Example:**\n```\ncurl -X PUT http://localhost:5001/api/endpoints/siteagent/binaries/agent/current -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"version\":\"1.6.2\"}'\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "binkey", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "path: binkey; body: version (must already be staged)" - } - } - } - } - } - }, - "/api/shopdbbackups": { - "get": { - "tags": [ - "core-shopdbbackups" - ], - "summary": "List every retained backup set, newest first, each with a compatibility flag saying whether it restores onto this build", - "description": "List every retained backup set, newest first, each with a compatibility flag saying whether it restores onto this build\n\n**Auth:** admin\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/shopdbbackups\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - }, - "post": { - "tags": [ - "core-shopdbbackups" - ], - "summary": "Kick off an on-demand backup run. Requires the backup overlay: without a configured backups root there is nothing...", - "description": "Kick off an on-demand backup run. Requires the backup overlay: without a configured backups root there is nothing behind it\n\n**Auth:** admin\n\n**Params:** none\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/shopdbbackups\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "none" - } - } - } - } - } - }, - "/api/shopdbbackups/{snapshotid}": { - "get": { - "tags": [ - "core-shopdbbackups" - ], - "summary": "Return one set's full manifest plus a resolved compatibility verdict", - "description": "Return one set's full manifest plus a resolved compatibility verdict\n\n**Auth:** admin\n\n**Params:** path: snapshotid\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/shopdbbackups/2026-09-06T120000Z\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "snapshotid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ] - } - }, - "/api/shopdbbackups/{snapshotid}/restore": { - "post": { - "tags": [ - "core-shopdbbackups" - ], - "summary": "Queue a restore of a set, gated on schema compatibility. DESTRUCTIVE: the load drops and replaces every table", - "description": "Queue a restore of a set, gated on schema compatibility. DESTRUCTIVE: the load drops and replaces every table\n\n**Auth:** admin\n\n**Params:** path: snapshotid\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/shopdbbackups/2026-09-06T120000Z/restore\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - }, - "404": { - "description": "No such record." - } - }, - "parameters": [ - { - "name": "snapshotid", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "path: snapshotid" - } - } - } - } - } - }, - "/api/shopdbbackups/finalize-restore": { - "post": { - "tags": [ - "core-shopdbbackups" - ], - "summary": "Queue the post-restore schema upgrade for the in-container finalizer, and retry it if a previous attempt failed. Does...", - "description": "Queue the post-restore schema upgrade for the in-container finalizer, and retry it if a previous attempt failed. Does NOT run the upgrade in the request: that watcher runs it out of band, so a restore completes whether or not the browser that started it is still open, and Alembic cannot be killed by the worker timeout mid-chain\n\n**Auth:** admin\n\n**Params:** none\n\n**Example:**\n```\ncurl -X POST -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/shopdbbackups/finalize-restore\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "none" - } - } - } - } - } - }, - "/api/shopdbbackups/status": { - "get": { - "tags": [ - "core-shopdbbackups" - ], - "summary": "Live status of the most recent backup and restore runs, or idle when neither is moving", - "description": "Live status of the most recent backup and restore runs, or idle when neither is moving\n\n**Auth:** admin\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/shopdbbackups/status\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - } - }, - "/api/shopdbbackups/history": { - "get": { - "tags": [ - "core-shopdbbackups" - ], - "summary": "Recorded backup and restore run history, newest first", - "description": "Recorded backup and restore run history, newest first\n\n**Auth:** admin\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/shopdbbackups/history\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - } - }, - "/api/shopdbbackups/settings": { - "get": { - "tags": [ - "core-shopdbbackups" - ], - "summary": "Read the backup settings, with secret values masked", - "description": "Read the backup settings, with secret values masked\n\n**Auth:** admin\n\n**Params:** none\n\n**Example:**\n```\ncurl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/shopdbbackups/settings\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - } - }, - "put": { - "tags": [ - "core-shopdbbackups" - ], - "summary": "Update the backup settings. A masked secret submitted unchanged leaves the stored value alone", - "description": "Update the backup settings. A masked secret submitted unchanged leaves the stored value alone\n\n**Auth:** admin\n\n**Params:** body: backup settings keys\n\n**Example:**\n```\ncurl -X PUT -H \"Authorization: Bearer $TOKEN\" -H 'Content-Type: application/json' -d '{\"backup_schedule\":\"0 2 * * *\"}' http://localhost:5001/api/shopdbbackups/settings\n```", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Success. Body is the success_response envelope: {status, data, meta}.", - "content": { - "application/json": { - "$ref": "#/components/schemas/SuccessEnvelope" - } - } - }, - "default": { - "description": "Error. Body is the error envelope; the code and message are nested under data.error.", - "content": { - "application/json": { - "$ref": "#/components/schemas/ErrorEnvelope" - } - } - }, - "401": { - "description": "Missing or invalid credentials." - }, - "403": { - "description": "Authenticated, but not permitted." - } - }, - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true, - "description": "body: backup settings keys" + "description": "body: JSON (fields not yet documented)" } } } diff --git a/docs/wiki-drafts/ADR-019-Plugin-Dynamic-Settings-Routes.md b/docs/wiki-drafts/ADR-019-Plugin-Dynamic-Settings-Routes.md index daf2c74a..82b5c148 100644 --- a/docs/wiki-drafts/ADR-019-Plugin-Dynamic-Settings-Routes.md +++ b/docs/wiki-drafts/ADR-019-Plugin-Dynamic-Settings-Routes.md @@ -2,7 +2,12 @@ **Status:** Accepted (2026-09-11) **Context:** Plugin UI extensions, lean builds -**Supersedes:** Hardcoded plugin routes in core router +**Supersedes:** Hardcoded plugin routes in core router +**Last checked against code:** 2026-09-18, branch gh-wj at 35d4fcd41 + +This draft lives only in `docs/wiki-drafts/`. There is no `docs/adr/` directory +on this branch, so this is the sole ADR-shaped record here and nothing enforces +it except the tests named below. See ADR-Conformance-2026-09-18.md. ## Problem @@ -63,27 +68,44 @@ class ComputersPlugin(BasePlugin): ### Backend: API Endpoint -Added to `shopdb/core/api/plugins.py`: +In `shopdb/core/api/plugins.py:46-95`, as it stands on gh-wj at 35d4fcd41: ```python @plugins_bp.route('/settings-routes', methods=['GET']) -@jwt_required(optional=True) +@require_permission('settings.view') def get_plugin_settings_routes(): """Return settings routes contributed by all enabled plugins.""" pm = current_app.extensions.get('plugin_manager') if not pm: return success_response({'routes': []}) - + routes = [] - for plugin in pm.get_enabled_plugin_instances(): - if hasattr(plugin, 'get_settings_routes'): + for plugin_name, plugin in pm.get_all_plugins().items(): + # Only include routes from enabled plugins + if not pm.registry.is_enabled(plugin_name): + continue + if not hasattr(plugin, 'get_settings_routes'): + continue + # One broken plugin must not take every settings hub down with it. + try: plugin_routes = plugin.get_settings_routes() - if plugin_routes: - routes.extend(plugin_routes) - + except Exception: + current_app.logger.exception( + 'get_settings_routes failed for plugin %s; skipping it', + plugin_name) + continue + routes.extend(renderable_settings_routes(plugin_name, plugin_routes, + current_app.logger)) + return success_response({'routes': routes}) ``` +Earlier revisions of this draft showed a `@jwt_required(optional=True)` handler +iterating `pm.get_enabled_plugin_instances()`. Neither is in the code: +`grep -rn get_enabled_plugin_instances shopdb/ plugins/` returns nothing, so +that block would not have run. It is replaced above rather than kept, because a +copy-paste block that reopens the anonymous read is worse than no block. + **Auth:** the endpoint is gated on `settings.view`. It was written anonymous, on the reasoning that unauthenticated pages (kiosks, TV displays) register plugin tabs before login. Nothing does: its only caller is `ModelCatalogHub.vue`, which sits behind `requiresAuth` and `requiresAdmin`. Open, it handed an anonymous caller the enabled plugin list, the settings hub layout and the internal Vue component paths, so it was closed. ### Frontend: Dynamic Route Registration @@ -323,7 +345,7 @@ const pluginComponents = import.meta.glob('../.plugins-staged/*/views/*.vue') **Nested routes:** A plugin could return routes with `children: [...]` for nested settings pages. Router supports this via `addRoute(parentName, childRoute)`. -**Permissions:** The `tab` could include a `permission` field (e.g., `"computers.manage_os"`), and the frontend filters out tabs the user can't access. The backend endpoint is anonymous, so permission checks would be client-side only (acceptable for UI gating; the API endpoint enforces permission anyway). +**Permissions:** The `tab` could include a `permission` field (e.g., `"computers.manage_os"`), and the frontend would filter out tabs the user cannot reach. This would be UI gating only, and the page behind the tab must still enforce its own permission; the `settings.view` gate on `/api/plugins/settings-routes` says who may read the tab list, not who may use a given tab. **Icons:** Add an `icon` field to the route metadata (e.g., `"mdi-microsoft-windows"`), and the tab renders the icon next to the label. diff --git a/docs/wiki-drafts/ADR-Conformance-2026-09-18.md b/docs/wiki-drafts/ADR-Conformance-2026-09-18.md new file mode 100644 index 00000000..36df7f02 --- /dev/null +++ b/docs/wiki-drafts/ADR-Conformance-2026-09-18.md @@ -0,0 +1,556 @@ +# ADR Conformance Review - September 18, 2026 + +**Branch:** gh-wj at 35d4fcd41 +**Range reviewed:** 2ac2e6d33..gh-wj, 20 commits, 149 files, +22055 lines +**Method:** read-only. Every statement below cites a file and line that was read. +No test suite was run except `tests/test_plugins/test_kiosk_network_allowlist.py` +(20 passed) and `bash scripts/check-naming-and-style.sh` (exit 0). + +## Read this first: the ADRs are not in this repository + +`ls docs/adr` fails on gh-wj. There is no ADR directory on this branch. The +whole set, ADR-001 through ADR-018 plus a README, lives on the other lineage at +`docs/adr/ in the product repo` and was read there for this review. +`docs/` on gh-wj holds only `ShopDB-Planner-Tasks-v2.csv`, `deployment/`, +`notes/`, `openapi.json` and `wiki-drafts/`. + +Three consequences, and they are the reason this page exists: + +1. **Nothing on this branch states the rules.** A person working here has no + ADR to read, so a decision can be contradicted without anyone noticing that + a decision existed. That is exactly what happened to ADR-016 (see below). +2. **Nothing on this branch enforces them except one script.** + `scripts/check-naming-and-style.sh` is the only ADR-aware gate in the repo, + and it names two ADRs: ADR-017 at line 343 and ADR-015 at line 395. Its other + eight checks enforce CONTRIBUTING.md house rules, not ADRs. A handful of + tests cover ADR ground without being described that way: + `tests/test_plugin_migrations.py` (ADR-008 chain shape and ownership), + `tests/test_core/test_plugin_settings_hubs.py` (ADR-019), + `tests/test_plugin_contract.py` (manifest fields), + `tests/test_core/test_public_read_surface.py` (the read gate). + `tests/test_api_inventory_parity.py` skips itself entirely on this branch: + `docs/api-inventory.json` is not tracked here, and its own skip reason says + "docs/ is excluded from publication". +3. **Some violations cannot be fixed from here.** ADR-018 has no + implementation-status note; ADR-016 is still marked "decided, NOT yet + implemented". Both corrections belong in files that are not in this + repository. + +The one ADR-shaped file on gh-wj is the ADR-019 draft in this folder, and it is +a draft. + +## Result by ADR + +| ADR | Subject | Result | +|-----|---------|--------| +| ADR-001 | Asset as platform contract | Two exceptions, both below: the unbumped Asset cert columns (2) and raw SQL over core tables (6) | +| ADR-002 | Plugin versioning | Holds for the bumps. Two stale manifests, and one unbumped core-model change (2) | +| ADR-003 | Plugin distribution | Holds | +| ADR-004 | Deployment topology | Holds | +| ADR-005 | Equipment vs measuringtools | Superseded by ADR-018 | +| ADR-006 | Collector contract | Holds on shape. One gap in a new boundary | +| ADR-007 | Product versioning and releases | VIOLATED (CHANGELOG) | +| ADR-008 | Plugin migration ownership | VIOLATED (plugin chain, core table) | +| ADR-009 | Frontend plugin gating | Holds | +| ADR-010 | Frontend plugin hooks | Holds | +| ADR-011 | Machines rename | Holds | +| ADR-012 | GE-Enforce manifest ownership | Holds | +| ADR-013 | Plugin catalog and lean builds | Holds | +| ADR-014 | Schema-lean per site | Holds | +| ADR-015 | Site-specific configuration | Holds in scope. Scope has a hole | +| ADR-016 | Credential delivery | Unbuilt by its own status, and MISDESCRIBED in four files here | +| ADR-017 | Buildings and levels | Holds | +| ADR-018 | Metrology instruments are machines | Accepted, not implemented | +| ADR-019 | Plugin dynamic settings routes (draft) | Implementation matches the draft | + +## What holds, and the evidence + +**ADR-001, the import surface.** +`grep -rn "from shopdb\.\(core\|utils\|extensions\|services\)" plugins/ +--include=*.py` returns zero hits. The only `shopdb.*` import in `plugins/` +outside `shopdb.api` is `shopdb.plugins.alembic_template` from migration +`env.py` files, which is the shared Alembic runner ADR-008 sanctions. The three +names added to the contract this range - `BusinessUnit`, `ip_in_cidrs`, +`request_ip_allowlisted` - are both defined and listed in `__all__` +(`shopdb/api/__init__.py:190-194`, `:471-486`). The new endpoints tables key off +`assets.assetid` by foreign key rather than introducing a parallel identity. +This covers the names added and kept. It does NOT cover a name added to the +Asset model and then withdrawn again without a bump, which happened in this +range; see finding 2. + +**ADR-002, the bumps.** `__contract_version__` moved 0.32.0 to 0.35.0 with a +per-bump rationale naming what was added and why it is additive +(`shopdb/__init__.py:124-152`). Every name those comments claim is genuinely +exported. Every new `BasePlugin` hook is optional with a safe default +(`shopdb/plugins/base.py:313-332`), so minor is the right classification. The +loader still checks `core_version` against `__contract_version__` +(`shopdb/plugins/loader.py:255-258`). + +**ADR-004 and ADR-014, per-site instances.** No `siteid` column anywhere in the +new schema. utf8mb4 still pinned in `.env.example`. The two-command deploy +(`flask db upgrade` then `flask plugin upgrade-all`) is unchanged. All four new +endpoints tables are registered in `PLUGIN_TABLE_OWNERS` +(`shopdb/plugins/alembic_template.py:57-63`) in the same change that creates +them, so `flask plugin prune-schema` still leaves a lean site lean. + +**ADR-006, the collector shape.** `/api/collector/` is still the +single dynamic dispatch route (`shopdb/core/api/collector.py:229`), the response +envelope is byte-for-byte the ADR's `{status, action, assetid, identityvalue, +warnings}` (`:324-331`), every submission still writes an AuditLog row +(`:313-320`), `/api/collector/_schemas` is still JWT-protected (`:337-341`), and +per-plugin `COLLECTOR_API_KEY_` with shared fallback resolves as the ADR +specifies (`:194-203`). Identity integrity under the new credential mode is +enforced in all three schema-declaring plugins - a per-PC credential reports +only for its own asset (`plugins/computers/plugin.py:729-733`, +`plugins/printers/plugin.py:556-559`, `plugins/endpoints/plugin.py:1422-1427`) - +and the dispatcher maps the `PermissionError` to 403 ASSET_MISMATCH +(`collector.py:294-299`). + +**ADR-007, the version lock-step.** `__version__ = '0.12.0'` +(`shopdb/__init__.py:158`) matches `frontend/package.json:3`, and `__version__` +is still absent from `shopdb.api`. + +**ADR-008 in the forward direction.** +`git diff --stat 2ac2e6d33..gh-wj -- migrations/` is empty. Not one line of +plugin schema went into the core chain. The endpoints chain is 24 revisions with +exactly one root and one head, no dangling `down_revision`, longest revision id +29 characters against the 32-character `alembic_version` cap. + +**ADR-009, route gating.** Every plugin-owned route added or touched this range +carries `meta.plugin`, including the two new endpoints settings pages, the USB +kiosk and the printed-parts kiosk. `GET /api/plugins/enabled` stayed +`jwt_required(optional=True)` (`shopdb/core/api/plugins.py:31-42`) through the +read-permission sweep that gated 165 other GETs, so the unauthenticated kiosk +gate still works. + +**ADR-010 and ADR-013, frontend boundaries.** The new +`frontend/src/components/NetworkAllowlistPicker.vue` is a CORE component +consumed by plugin views through the `@/` alias +(`plugins/usb/frontend/views/USBSettings.vue:108`, +`plugins/printedparts/frontend/views/PrintedPartsSettings.vue:164`), which is +the established pattern; 97 plugin frontend files already import that way. No +core file gained a hardcoded plugin import. + +**ADR-012, GE-Enforce ownership.** The per-PC rollout keeps ShopDB as author of +record: the runbook's JSON snippets are entries to author and publish through +the plugin, not hand edits to the share, and `Set-ShopDBServerPins.ps1` is +staged as a normal PS1 entry with `TargetHostnames`/`TargetMachineNumbers` +gates. The engine is still referenced, not vendored. + +**ADR-015 within its enforced scope.** `scripts/check-naming-and-style.sh` exits +0 and the pipeline is not vacuous: a control pattern over the same six +directories returns 1995 lines. The new kiosk network admission is a setting +with a blank default and a documented-neutral example +(`plugins/usb/plugin.py:153-168`), blank means off +(`shopdb/utils/ipallowlist.py:44-46`), and `frontend/vite.config.js:5-16` +replaced a hardcoded dev-proxy address with `VITE_DEV_API_TARGET`. + +**ADR-017.** The per-occurrence `mapx`/`levelid` gate and the +`LocationMapTooltip :levelid` gate both pass. No payload in this range emits a +position without its level. + +**ADR-019 against the implementation.** `SETTINGS_HUBS_WITH_PLUGIN_TABS = +('model-catalog',)` at `shopdb/plugins/base.py:29`; the loud drop at `:32-54`; +the `settings.view` gate at `shopdb/core/api/plugins.py:47`; the still +unrewritable `` import(`../../${tab.component}`) `` at +`frontend/src/views/settings/ModelCatalogHub.vue:84` with no `vite.config.js` +change to fix it; and a non-vacuous test suite in +`tests/test_core/test_plugin_settings_hubs.py`. The draft in this folder was +corrected on 2026-09-18: its code block still showed +`@jwt_required(optional=True)` and its Future Extensions section still called +the endpoint anonymous, both contradicting its own Auth paragraph and the +shipped code. + +## Violations + +### 1. ADR-007: no release record for the largest change in the range + +`CHANGELOG.md:11` + +`git diff --stat 2ac2e6d33..gh-wj -- CHANGELOG.md` returns nothing at all. The +file is byte-identical across all 20 commits, which include per-PC agent +credentials, enrollment, bootstrap tokens, server pinning, kiosk network +admission and three contract bumps. Grepping the whole file for "per-PC", +"X-Endpoint-Credential", "Endpoint credential" and "enrol" finds nothing +relevant. ADR-007 decision point 3 makes `CHANGELOG.md` the canonical +human-readable record and says work in flight accumulates under +`## [Unreleased]`; that block still describes only the pre-range share-link and +read-gate work. + +Low severity: no release has been cut in the range, so the obligation point +(ADR-007 release procedure step 3) has not been reached and the debt is fully +repairable before the tag. Operator-facing content does exist, at +`plugins/endpoints/docs/PER-PC-CREDENTIALS-ROLLOUT.md`, but that is plugin-local +and not where ADR-007 says an adopter looks. + +### 2. ADR-008: a plugin migration chain does DDL on the core `assets` table + +`plugins/endpoints/migrations/versions/0023_endpoint_credentials.py:73` and `:76` + +0023 declares `plugin_name = 'endpoints'` (line 33) and then runs +`op.drop_index(index['name'], table_name='assets')` and +`op.drop_column('assets', column)`. It is cleaning up after itself: +`git show 5cd533239:plugins/endpoints/migrations/versions/0021_client_certificates.py` +shows the shipped 0021 adding three columns to `assets` plus +`idx_assets_cert_fingerprint` from the same plugin chain. `assets` is not, and +cannot be, in `PLUGIN_TABLE_OWNERS['endpoints']` +(`shopdb/plugins/alembic_template.py:57-63`), so the metadata ownership filter +never saw it, and `tests/test_plugin_migrations.py` has no test over which +tables a chain touches. + +Low severity: every step is guarded on the live schema (0023:64-76), 0021 and +0022 are now no-ops, the forward direction is clean, and +`tests/test_plugins/test_endpoints_credentials_migration.py:96-140` runs the +chain twice from both a legacy and a fresh schema. The lane crossing is real; +the fleet blast radius is essentially zero. + +**The ADR-001/ADR-002 half of this was written up wrong, and that conclusion is +withdrawn here (2026-09-18).** The original text read: "`git show 5cd533239 +--stat` touched no file under `shopdb/`, and `grep -rn cert +shopdb/core/models/asset.py` is empty: the Asset *model* never carried those +columns. Only the database did. No contract bump was owed." Both halves of the +evidence are real and the conclusion drawn from them is still false, because +5cd533239 is not the only commit in the range and `grep` only sees the tree as +it stands now. `git show 18e5fdc00:shopdb/core/models/asset.py` puts +`client_cert_fingerprint` (declared `index=True`), `cert_issued_at`, +`cert_expires_at`, `cert_revoked_at` and `cert_revocation_reason` on the CORE +`Asset` model. 18e5fdc00 (2026-09-14, "Fix critical security vulnerabilities in +client certificate authentication") is an ancestor of `gh-wj`; 7480e4fe3 +(2026-09-15) deleted all 28 lines again. The grep comes back empty because it +runs after the retraction, not because the contract was never touched. + +The ADR-001/ADR-002 finding therefore holds, in a worse form than first stated: +the core Asset contract surface was extended to carry a plugin's feature and +then retracted, with no contract bump in either direction, and for the length of +that window five underscore-spelled columns sat on a core model in a repo whose +column convention forbids underscores. Severity stays low - the window is inside +an unreleased branch, and nothing outside the endpoints plugin ever read the +columns - but the finding is a real ADR-001/ADR-002 breach and not, as written, +a non-finding. + +**Now guarded, 2026-09-18.** The finding above ended with "no test over which +tables a chain touches". There is one: +`tests/test_plugin_migrations.py::test_no_plugin_migration_touches_another_lanes_table`. +It AST-scans every `plugins/*/migrations/versions/*.py` for `create_table`, +`drop_table`, `add_column`, `drop_column`, `alter_column`, `create_index`, +`drop_index`, `rename_table`, the five constraint ops (`create_foreign_key`, +`drop_constraint`, `create_unique_constraint`, `create_check_constraint`, +`create_primary_key`) and the `batch_alter_table` forms, resolves each target +table through module-level constants and literal `for` sequences, and fails when +the table belongs to `core` or to a different plugin. Ownership comes from +`PLUGIN_TABLE_OWNERS` and a static scan of `shopdb/core/models/` (including the +three bare `db.Table` association tables), so the map stays the single source of +truth rather than a table list copied into a test. + +Raw SQL is scanned too, because `op.execute('ALTER TABLE assets DROP COLUMN +assetnumber')` is DDL that no `op.*` scan can see. `op.execute` and +`exec_driver_sql` have their statement text flattened - f-string holes and +unresolvable names become a sentinel character rather than being dropped - and +`ALTER TABLE x`, `DROP TABLE x`, `CREATE TABLE x`, `RENAME TABLE x TO y`, +`CREATE/DROP INDEX ... ON x` and `TRUNCATE x` name the op's target table. +`UPDATE`, `DELETE`, `INSERT` and `SELECT` are data, not schema, and are not +flagged: three shipped revisions already run cross-checked DML +(`plugins/backups/.../0002_backups_lastseenat.py:37`, +`plugins/backups/.../0003_backups_clear_backfilled_lastseen.py:58`, +`plugins/computers/.../0006_policyactionhold_indefinite.py:78`) and all three +stay green. Keeping the hole in place is what separates the two cases that +matter: `machines 0002` interpolates a CONSTRAINT name into +`f"ALTER TABLE equipment DROP FOREIGN KEY {name}"`, where the table is literal +text and resolves; `f'ALTER TABLE {t} DROP COLUMN x'` puts the hole inside the +table name, resolves to nothing, and fails as UNRESOLVED, which is the obvious +dodge closed. + +Four supporting tests keep it honest: an op whose table name cannot be resolved +statically is a failure, not a pass, so a computed name does not slip through; a +non-vacuity test asserts the scan still finds more than 100 ops and still +detects the 0023 crossing; a second non-vacuity test pins two specific raw-SQL +ops out of `machines 0002` and asserts the three DML statements are absent; and +a dead-entry test fails if an allowlist line stops matching anything, so an +excuse cannot outlive what it excused. + +The allowlist keys on `(plugin, filename, op, table, column)`, not on the first +four. Keyed on the first four alone, the one shipped `drop_column on assets` +entry would also excuse a brand new `op.drop_column('assets', 'assetnumber')` +appended to that same shipped file - the exact breach the guard exists to stop - +so the six entries name their columns: one `drop_index` (column `None`, the +index name is computed) and one `drop_column` per certificate column. 0021 and +0022 are not listed, because they are stamp-only no-ops and present no DDL to +scan. + +Verified non-vacuous against the pre-fix version of the same test module. Three +violations were planted in a scratch copy of `plugins/` (the shipped 0023 is +applied on production and was not edited in place): the five constraint ops on +`assets` from a plugin chain, `op.execute` with both a literal `ALTER TABLE +assets` and an f-string table name, and a new `op.drop_column('assets', +'assetnumber')` inside 0023. The pre-fix guard passed all four of its tests on +all three plants. The current guard fails, naming all seven ops with file, line, +op, table, column and owner, and goes green again once the plants are removed. + +**The 0023 index-drop loop was examined and needs no repair.** 0023:71-73 drops +any index on `assets` whose column set intersects the certificate columns and +recreates nothing, which would matter if such an index could carry columns that +still exist. It cannot. The complete set of things that create an index on +`assets` is: + +* the core baseline `68b3947ae14f`: `idx_asset_active`, `idx_asset_location`, + `idx_asset_status`, `idx_asset_type_bu`, `ix_assets_assetnumber`, + `ix_assets_serialnumber` +* core `7b02`: `ix_assets_gaugelabreference`, `ix_assets_maintenancereference` +* core `7d33`: `idx_assets_levelid` +* the shipped endpoints `0021`: `idx_assets_cert_fingerprint` over + `client_cert_fingerprint` alone (the shipped `0022` added two columns and no + index) +* `db.create_all` off `shopdb/core/models/asset.py`. The earlier version of this + inventory said that builds "that same set" as the core baseline, on the + strength of the Asset model never carrying a cert column. As corrected above, + between 18e5fdc00 and 7480e4fe3 it did, with `client_cert_fingerprint` + declared `index=True`, so `create_all` in that window ALSO built + `ix_assets_client_cert_fingerprint`. That index is missing from every earlier + telling and is the one real correction to this list. + +Re-checked rather than assumed, the conclusion is unchanged. +`ix_assets_client_cert_fingerprint` is single-column over +`client_cert_fingerprint`, which `_drop_certificate_columns` removes in the +statement immediately after the loop, so the index carries no column that +outlives it and there is nothing to recreate. The filter is also unreachable by +accident: every name in `_CERT_COLUMNS` is underscore-spelled, which the column +convention forbids, so no real `assets` column can collide with it. The only two +indexes the loop can ever drop are `idx_assets_cert_fingerprint` and +`ix_assets_client_cert_fingerprint`, both dead by construction. No index was +lost on any database at the reference site and no repair migration is owed; the reasoning is +recorded in the test file beside the allowlist entries so it is not +re-litigated. + +### 3. ADR-016: four files here describe a decision ADR-016 never took + +`docs/wiki-drafts/README.md:316-323` (corrected 2026-09-18), `CLAUDE.md:119`, +`the assistant quick-reference file, lines 135 and 14`, +`docs/wiki-drafts/Security-Audit-2026-09-11.md:392` + +All four attribute "device tokens" to ADR-016. `grep -rni device +docs/adr/ in the product repo*.md` returns no hit for device tokens +in ANY ADR. ADR-016's Decision is a fleet credential store with Fernet at rest, +a targeting and precedence resolver reusing `plugins/geenforce/filters.py`, an +explain endpoint, `credentials.manage`, and `GET /api/collector/credentials` +behind a dedicated `credentials.fetch` scope. + +The `docs/wiki-drafts/README.md` copy went further and, under a heading +"ADR-016: Credential Delivery", listed registry keys among the places +credentials must **never** live. ADR-016 says the opposite: its +"Alternatives rejected" section calls registry-only provisioning "a worthwhile +FIRST STEP" and "Recommended as tier one regardless". The ban also contradicted +shipped code on this branch: +`plugins/computers/client/Report-AssetToShopDB.ps1:367-374` reads `CollectorKey` +out of `HKLM:\SOFTWARE\GE\ShopDB`, and +`plugins/endpoints/client/Set-ShopDBServerPins.ps1:4-20` writes the server trust +pins into the same key by design, with +`plugins/endpoints/docs/PER-PC-CREDENTIALS-ROLLOUT.md:106-111` ("The registry +wins") making that the intended precedence. That section was rewritten on +2026-09-18; the three files outside `docs/wiki-drafts/` still carry the +misattribution and are out of scope for this edit. + +Two qualifications, so nobody overcorrects: `PER-PC-CREDENTIALS-ROLLOUT.md:351-361` +deliberately plans to clear `CollectorKey` from both registry views at cutover, +so the shared key in HKLM is being retired anyway; and `ServerPins` are trust +anchors, not credentials. + +### 4. ADR-016: the per-PC programme redefines "credential" with no design record + +`plugins/endpoints/models/credential.py:1` + +ADR-016's mechanism is unbuilt. Grepping `shopdb/` and `plugins/` for +`credentials.fetch`, `collector/credentials` and `Fernet` returns zero hits, and +`plugins/endpoints/plugin.py:154-265` declares only `endpoints.*` permissions - +no `credentials.manage`, no `credentials.fetch`. That matches ADR-016's own +"Implementation status, 2026-08-14". What the 20-commit programme built is a +different subject under the same word: `EndpointCredential` / +`EndpointEnrollRequest` / `EndpointMigrationWave` as the agent's own identity +secret (`models/credential.py:1-22`), `X-Endpoint-Credential` as the agent auth +header (`api/agentauth.py:1-18`), and an unauthenticated `/enroll` +(`api/enrollment.py:289`). + +The design itself is sound and consistent with ADR-016's intent: the rollout +removes the cleartext key from the share, and rotation is a settings change plus +a pickup report, which is what ADR-016 wanted. The gap is that nobody wrote it +down as a decision. `grep -ril 'enroll|endpoint credential|X-Endpoint-Credential'` +over the ADR directory on the other lineage matches only ADR-012, which is +unrelated. Low severity and not fixable from here: the missing record belongs on +the lineage that carries `docs/adr/`. + +### 5. ADR-018: accepted, unimplemented, with nothing saying so + +`shopdb/plugins/alembic_template.py:74` + +ADR-018 (ACCEPTED, 2026-09-04) decides that the `measuringtools` plugin is +retired and the `measuring_tool` asset type removed, that machines gains four +calibration columns and machinetypes gains `ismetrology`. None of it exists +here: `plugins/measuringtools/` still ships with its manifest, +`alembic_template.py:74` still registers +`'measuringtools': ('measuringtooltypes', 'measuringtools')`, +`shopdb/core/api/dashboard.py:19`, `:55` and `:93` still count `measuring_tool` +into the asset totals, `shopdb/cli/__init__.py:1021` still seeds it, +`deploy/site-profile-universal.json:12` still lists it in the universal profile, +and `grep -rn ismetrology shopdb/ plugins/` and +`grep -n calibration plugins/machines/models/*.py` both return nothing. +`git log -3 -- plugins/measuringtools/` shows no retirement work. + +Unlike ADR-016, ADR-018 carries no implementation-status paragraph, so its text +reads as done. Low severity: the table holds zero rows, so nothing is broken +today, and ADR-018's own argument is that the window is time-boxed - the +migration is free while the 134 staged metrology instruments have not been +entered. gh-wj is the branch that deploys to the reference site, so entering them here is what +closes the window. + +### 6. ADR-001 / ADR-002: raw SQL over core tables from a plugin + +`plugins/computers/plugin.py:2080` + +`db.session.execute(db.text(...))` joining core `assets`, `assetrelationships` +and `assettypes`, inside the `retype` CLI command. `Asset`, `AssetType`, +`AssetRelationship` and `RelationshipType` are all exported and in `__all__` +(`shopdb/api/__init__.py:47-48`, `:424`, `:438-439`), so unlike the +`BusinessUnit` case that the 0.34.0 bump note closed, there is no missing export +to blame, and `plugins/endpoints/api/routes.py:3981-3986` is the same pattern +already corrected. The 0.34.0 note names the reason in the code: +"a raw query is hidden coupling to the core schema that no contract bump can +catch, and it is the kind of thing that gets copied" +(`shopdb/__init__.py:135-142`). + +Low severity and arguably generous: ADR-001's stated rule is an import rule, +which raw SQL does not literally breach, and the blast radius is one admin CLI +command with no serving route affected. Predates this range (331d27e07). The +other plugin raw query, `plugins/computers/plugin.py:2128`, touches only +plugin-owned tables and is fine. + +### 7. ADR-015: the site-literal scan does not cover the C# agent + +`scripts/check-naming-and-style.sh:440` + +Lines 436-440 scope the grep to `plugins/ shopdb/ scripts/ deploy/ +frontend/src/ tools/` with `--include` on +`.py/.vue/.js/.ps1/.psm1/.sh/.iss/.json/.html`. `agent/` is absent and `.cs` is +not an included extension. This range added roughly 2,500 lines of C# under +`agent/`, all of which ships to a sister site. Re-running the assembled site +patterns by hand over `agent/`, excluding bin and obj, finds one hit: prose in +`agent/ShopDbEndpointAgent/ShopDbEndpointAgent.csproj:108`, which ADR-015 +explicitly permits ("Comments and documentation may name a site where it +explains WHY"). So the rule holds today; the gate would not notice if it stopped +holding. + +**This is a coverage gap in the rule, not a breach of it.** ADR-015's 2026-08-14 +amendment names those six directories as the scan's scope, so the script matches +the ADR precisely. The amendment widened the scan on exactly this reasoning - +the original scope missed every literal that reached a second site, because +those were in the client scripts and the installer - and the C# agent is now +that client channel. + +### 8. ADR-002: two manifests declare a floor below the contract name they import + +`plugins/usb/manifest.json:8` and `plugins/printedparts/manifest.json:8` + +`da91a2915` added `shopdb/utils/ipallowlist.py`, bumped `__contract_version__` +to 0.35.0 with `ip_in_cidrs` and `request_ip_allowlisted` named as the new +surface (`shopdb/__init__.py:143-152`), and made +`plugins/usb/api/routes.py:42` and `plugins/printedparts/api/routes.py:521` +import `request_ip_allowlisted`. Neither manifest was touched: usb still says +`">=0.1.0,<1.0.0"` and printedparts `">=0.16.0,<1.0.0"`. The team does maintain +this field - `066a19052` raised `plugins/endpoints/manifest.json:7` to +`">=0.34.0,<1.0.0"` one commit earlier. + +On a 0.34.0-or-older core both plugins would pass the loader's version gate +(`shopdb/plugins/loader.py:141-166`, called at `:258`) and then die with an +ImportError at blueprint registration. Impact at the reference site is nil, because core and +plugins always ship from this one tree. + +The ADR-002 framing is weak and should not be quoted as a clean breach: ADR-002 +itself says pre-1.0 plugins should declare `">=0.1.0,<1.0.0"`, which is +literally what `plugins/usb/manifest.json:8` declares, while its Neutral section +says bundled plugins' `core_version` is "always the current +`__contract_version__`". The ADR contradicts itself here. This is staleness +against team practice. + +## Checked and not a violation + +These were examined and did NOT hold up as ADR breaches. They are listed so the +next review does not re-open them. + +- **`get_settings_routes` narrowed to a one-hub allowlist without a contract + bump.** `shopdb/core/api/plugins.py:92` now filters through + `renderable_settings_routes`. Nothing observable changed: the only frontend + caller, `frontend/src/views/settings/ModelCatalogHub.vue:73`, already filtered + to its own hub at `:76`, and `frontend/src/router/routes/core.js:62-63` states + in-tree that no other hub page fetches or registers plugin routes. A route + naming another hub was drawn nowhere before and is drawn nowhere now. No + `BasePlugin` ABC signature or `PluginMeta` schema changed, so under ADR-002 + this is patch-level and no bump was owed. It is also not silent: + `shopdb/plugins/base.py:46-52` logs an error naming the plugin, the hub and + the supported set, and the same commit added the "Constraints as implemented" + section to the ADR-019 draft plus + `tests/test_core/test_plugin_settings_hubs.py`. +- **`computers` and `printers` manifests declaring `>=0.20.0` against the + contract-0.33.0 key `acceptsagentcredential`.** ADR-002 requires no such + tracking, nothing in `loader.py` implements it, and the claimed fail-open + cannot occur: on a core pinned at 0.32.x the endpoints plugin is refused at + load by its own `>=0.34.0` floor, so no credential is ever issued and there is + no enrolled PC whose shared key an older dispatcher could wrongly accept. + +## Drift that no ADR covers + +Recorded because it is real and someone will look for it here. + +**`docs/openapi.json` is a release behind the code.** +`shopdb/core/api/docs.py:18` pins `_SPEC` to that file and serves it through a +vendored Redoc at `/api/docs`, specifically for the air-gapped production box. +The tracked spec holds 377 paths and `info.version` is still `0.12.0`. It +contains zero paths matching `enroll` or `bootstrap`, while +`git diff 2ac2e6d33..gh-wj -- 'plugins/*/api/*.py' 'shopdb/core/api/*.py'` adds +24 route decorators, including the whole enrollment surface +(`plugins/endpoints/api/enrollment.py:289`, `:580`, `:657`, `:752`, `:773`, +`:827`, `:853`, `:1095`, `:1135`, `:1233`, `:1253`) and the bootstrap-token +surface (`plugins/endpoints/api/bootstrap.py:419`, `:474`, `:485`, `:506`, +`:530`). Where the spec does describe a route that changed, it describes the old +behaviour: `/api/plugins/settings-routes` is documented as +"**Auth:** none (jwt-optional decorator, deliberately anonymous...)" with +`"security": [{}, {"bearerAuth": []}]`, while the shipped handler is +`@require_permission('settings.view')`. + +Neither ADR-001 nor ADR-002 mentions `openapi.json` or `api-inventory.json`, so +no stated rule is broken. The drift also cannot be detected or repaired from +this branch: `docs/api-inventory.json` is not tracked here, so +`tests/test_api_inventory_parity.py` skips and `scripts/gen_openapi.py` has no +input. + +**`CLAUDE.md` in this repo contradicts the repo.** Line 3 and line 9 describe +the database as "PostgreSQL 15+", while `docker-compose.yml:20` runs +`mysql:8.4` and `.env.example:29` is a `mysql+pymysql://` URL. Line 31 states +"**Columns**: snake_case (e.g., `serial_number`, `last_seen`)", which is the +opposite of `CONTRIBUTING.md:17` ("Column names: lowercase, concatenated, +singular. No dashes, no underscores") and of what the gate enforces at +`scripts/check-naming-and-style.sh:236`. Line 119 carries the ADR-016 device +token misattribution described above. `CLAUDE.md` is outside this folder and was +not edited. + +## Scope note + +This page records ADR conformance only. The same review produced security +findings against the per-PC credential surface, the kiosk network admission and +the one-line bootstrap; those are not ADR matters and are recorded in +`Security-Audit-2026-09-11.md`, refreshed on the same date and the same commit. +Violation 3 on this page and finding 31 on that one are the same ADR-016 +misattribution seen from two angles. The highest of the security findings, for +cross-reference, is that `install.ps1` writes the fleet shared key to +`HKLM\SOFTWARE\GE\ShopDB` with no ACL +(`plugins/endpoints/api/siteagent.py:328`, helper generated at `:394-411`, no +`SetAccessControl` or `RegistrySecurity` anywhere in the generated installer), +where any logged-on user can read it - a consequence the repo states itself at +`.github/GE-ENFORCE-COLLECTOR-FIX.html:377-380`. + +## References + +- ADR set: `docs/adr/ in the product repo` (NOT on this branch) +- `scripts/check-naming-and-style.sh` - the only ADR-aware gate in this repo +- `plugins/endpoints/docs/PER-PC-CREDENTIALS-ROLLOUT.md` - the rollout runbook +- ADR-019-Plugin-Dynamic-Settings-Routes.md - the one ADR-shaped file here +- Security-Audit-2026-09-11.md - the security half of the same review +- Architectural-Findings-2026-09-11.md - re-checked 2026-09-18 +- Maintenance-Footprint-Baseline.md - re-checked 2026-09-18 diff --git a/docs/wiki-drafts/Architectural-Findings-2026-09-11.md b/docs/wiki-drafts/Architectural-Findings-2026-09-11.md index 0b611847..b01af514 100644 --- a/docs/wiki-drafts/Architectural-Findings-2026-09-11.md +++ b/docs/wiki-drafts/Architectural-Findings-2026-09-11.md @@ -1,443 +1,405 @@ # Architectural Findings - September 11, 2026 -**Scope:** Full codebase analysis via Gortex semantic code intelligence -**Severity:** 22 findings (6 critical, 6 high, 10 medium) -**Status:** Documented for remediation planning +**Scope:** Full codebase analysis via Gortex semantic code intelligence +**Original severity claim:** 22 findings (6 critical, 6 high, 10 medium) +**Status:** RE-CHECKED against branch gh-wj at 35d4fcd41 on 2026-09-18. Most of +the original findings do not survive a read of the code. This page keeps them, +with the verdict and the evidence, so nobody acts on a claim that was never +true. -## Executive Summary +## How to read this page (added 2026-09-18) -Gortex semantic analysis identified architectural issues in plugin boundaries, dependency management, and data schema consistency. Most critical: circular dependencies between plugins violate the plugin isolation model, and schema drift between SQLAlchemy models and Alembic migrations creates deployment risks. +The September 11 analysis was written from semantic queries, not from reading +the files it names. Eight of its claims cite code that does not exist on this +branch: a `machines` plugin that imports `computers`, a migration that spells a +column `last_reported_date`, a ServiceNow plugin, `MACHINE_TYPES = ['Lathe', +'Mill', 'CNC']`, a hardcoded `['Bldg 300', 'Bldg 400']`, Vuex, an absent CSP +header, an unhandled `requests.post`. Each of those was grepped for on +2026-09-18 and returns nothing. -**Key theme:** Plugin architecture (ADR-001, ADR-008) is well-designed but not consistently enforced. Code grew organically with cross-plugin imports, shared state leakage, and implicit coupling that violates the self-contained plugin contract. +Every finding below now carries one of four verdicts: + +- **REFUTED** - the cited code does not exist, or the opposite is in the tree. +- **STALE** - it was true once; the tree has since fixed it. +- **HOLDS (narrowed)** - something real is there, but smaller or different than + the original text claims. +- **UNVERIFIED** - cannot be settled from this repository. + +Effort and priority numbers in the original text were estimates with no stated +basis. They are left where they sit for findings that still hold and struck +where the finding does not. ## Critical Findings (P0) ### 1. Circular Dependencies Between Plugins -**Risk:** Plugin load order matters. Enabling/disabling plugins can break others. +**Verdict: REFUTED as stated. HOLDS (narrowed) as a deferred-import coupling.** -**Evidence:** -- `computers` plugin imports from `machines` plugin: `from plugins.machines.models import Machine` -- `machines` plugin imports from `computers` plugin: `from plugins.computers.models import Computer` -- `printers` plugin imports from `computers` plugin -- `endpoints` plugin imports from `computers`, `printers`, and `usb` plugins +**Original claim:** `computers` imports `machines` and `machines` imports +`computers`; import errors at runtime if plugins load in the wrong order; 25+ +violations. -**Impact:** -- Violates plugin isolation (ADR-001: plugins are independent extensions) -- Disabling `computers` breaks `machines`, `printers`, `endpoints` -- Plugin dependency chain not declared in plugin metadata -- Import errors at runtime if plugins loaded in wrong order +**What the code shows (2026-09-18):** -**Root cause:** Plugins use each other's models for relationships (e.g., Computer controls Machine). +- `grep -rn "^from plugins\." plugins/ --include=*.py` returns **nothing**. + There is not one module-level cross-plugin import in the tree, so there is no + load-order failure mode and no import cycle at import time. +- `grep -rn "from plugins.computers" plugins/machines/` returns **nothing**. + The `machines` plugin does not reference `computers` at all, so the cycle the + finding is built on does not exist. +- 52 cross-plugin imports do exist, and every one of them is inside a function + body, resolved on call. `plugins/computers/plugin.py:414` and `:1166` reach + `plugins.machines.models` that way; `plugins/endpoints/api/routes.py:235` and + fourteen more reach `plugins.computers.models`. -**Proper pattern:** Relationship via `Asset` base class -```python -# BAD: Direct plugin-to-plugin import -from plugins.machines.models import Machine +**What is actually there:** a mutual reference pair between `computers` and +`endpoints`, both deferred: -# GOOD: Relationship via Asset ID, resolve at runtime -machine_asset_id = db.Column(db.Integer, db.ForeignKey('asset.id')) -machine = relationship('Asset', foreign_keys=[machine_asset_id]) +- `plugins/computers/api/policy_routes.py:44` imports + `plugins.endpoints.commands.elevated_permission_for`, unguarded. +- `plugins/endpoints/commands.py:477` imports + `plugins.computers.services.policyresolver.effective_policy_for`, wrapped in + `try/except` that falls back to the base allowlist (`commands.py:475-481`). -# In plugin manifest, declare dependency -dependencies = ['machines'] # Explicit, not implicit -``` +So one side of that pair degrades when its partner is absent and the other +raises. The same asymmetry appears at +`plugins/endpoints/api/agentauth.py:118`, where `resolve_hostname` imports +`plugins.computers.models.Computer` with no guard: an endpoints-without- +computers site fails there rather than returning "unknown hostname". -**Remediation:** -1. Audit all `from plugins.` imports (25+ violations found) -2. Replace with Asset-based relationships or declared dependencies -3. Enforce via linter: plugins can import from `shopdb.api`, not `plugins.*` - -**Effort:** 60 hours (high impact, requires refactoring multiple plugins) -**Priority:** Q1 2027 (blocks plugin catalog ADR-013 lean builds) +**What to do instead of the original remediation:** nothing about load order. +The open question is narrower and worth one pass: decide whether a deferred +cross-plugin import must be guarded, and guard the unguarded ones. A linter +rule banning `plugins.*` imports outright would break the working policy and +enrollment paths listed above, which are the product's actual design. ### 2. Undeclared Import Dependencies -**Risk:** Plugins import from `shopdb.core` internals instead of stable `shopdb.api` surface. +**Verdict: REFUTED.** -**Evidence:** -- 18 plugins import from `shopdb.core.models` directly (not via `shopdb.api`) -- 12 plugins import from `shopdb.core.utils` (internal helper modules) -- 7 plugins import from `shopdb.core.services` (not part of plugin API) +**Original claim:** 18 plugins import `shopdb.core.models`, 12 import +`shopdb.core.utils`, 7 import `shopdb.core.services`. -**Impact:** -- Plugins break when core refactors internals -- No API versioning or stability guarantee -- Circular imports between core and plugins +**What the code shows:** +`grep -rn "from shopdb\.\(core\|utils\|extensions\|services\)" plugins/ +--include=*.py` returns **0 hits**. Not one plugin deep-imports core. The only +`shopdb.*` import in `plugins/` outside `shopdb.api` is +`shopdb.plugins.alembic_template` from migration `env.py` files, which is the +shared Alembic runner ADR-008 sanctions. -**Plugin API contract (ADR-001):** Plugins MUST import from `shopdb.api` only -```python -# GOOD: Stable plugin API -from shopdb.api import db, Asset, BasePlugin, success_response +ADR-001's rule that `shopdb.api` is the only core surface a plugin may import is +being kept. The thing that is NOT enforced is raw SQL against core tables: see +finding 3a below. -# BAD: Internal implementation details -from shopdb.core.models.asset import Asset # Internal path -from shopdb.core.services.search import SearchService # Not exported -``` +### 2a. Raw SQL against core tables from a plugin (added 2026-09-18) -**Remediation:** -1. Audit imports: `grep -r "from shopdb.core" plugins/` -2. Export needed classes via `shopdb.api.__init__.py` -3. Update plugin imports to use `shopdb.api` -4. Add linter rule: plugins cannot import `shopdb.core.*` +**Verdict: HOLDS (new, narrow).** -**Effort:** 40 hours -**Priority:** Q4 2026 (enables core refactoring without breaking plugins) +`plugins/computers/plugin.py:2080-2091` runs +`db.session.execute(db.text(...))` joining core `assets`, `assetrelationships` +and `assettypes` inside the `flask computers retype` CLI command. `Asset`, +`AssetType`, `AssetRelationship` and `RelationshipType` are all exported from +`shopdb.api` (`shopdb/api/__init__.py:47-48`, `:424`, `:438-439`), so no +missing export excuses it, and `plugins/endpoints/api/routes.py:3981-3986` is +the same pattern already corrected for `BusinessUnit`. The contract note at +`shopdb/__init__.py:135-142` names the reason: a raw query is coupling to the +core schema that no contract bump can catch. + +Blast radius: one admin CLI command, no serving route. Import rule not +literally broken. Worth fixing because it is the kind of thing that gets copied. ### 3. Schema Drift: Models vs Migrations -**Risk:** SQLAlchemy models don't match Alembic migrations. Deployed schema differs from code. +**Verdict: REFUTED as stated. HOLDS (narrowed) to one column.** -**Evidence (examples):** -- `Computer.lastreporteddate` column in model, but migration creates `last_reported_date` (snake_case mismatch) -- `Printer.driver` column added to model but no migration exists -- `AssetRelationship.label` column in migration but not in model -- 14 tables with mismatched column names or missing columns +**Original claim:** `Computer.lastreporteddate` in the model but +`last_reported_date` in the migration; `Printer.driver` with no migration; +`AssetRelationship.label` in a migration but not the model; 14 tables affected. -**Impact:** -- Fresh install (runs migrations) has different schema than ORM expects -- Queries fail: `Column 'lastreporteddate' does not exist in table` -- Adding data via ORM writes to wrong column -- Impossible to tell what production schema actually is (migrations are source of truth, but models are what dev sees) +**What the code shows:** -**Root cause:** -- Naming convention changed (ADR: lowercase concatenated, no underscores) -- Manual migrations written with old convention -- No validation that migrations match models +- `grep -rn "last_reported_date" --include=*.py .` returns **nothing**. The + model spells it `lastreporteddate` (`plugins/computers/models/computer.py:85`) + and so does every migration. +- `grep -rPn "sa\.Column\(\s*'[a-z]+_[a-z_]+'" plugins/*/migrations/versions/*.py + migrations/versions/*.py` returns **nothing**. There is no snake_case column + name in any migration in the tree. +- There is no `shopdb/models/` directory to hold the claimed + `AssetRelationship.label`; core models live under `shopdb/core/`. -**Remediation:** -1. Generate fresh migration from current models: `flask db migrate` -2. Compare to existing migrations, find diffs -3. Write fixup migration to align schema with models -4. Add test: `test_migrations_match_models()` validates ORM vs DB schema -5. Enforce naming convention in migration template +**The one real instance:** `plugins/notifications/models/notification.py:158` +declares `alertscope = db.Column(db.JSON, nullable=True)` while the migration +that creates it, `plugins/notifications/migrations/versions/0006_add_fleet_alerts.py:45`, +creates `sa.Column('alertscope', sa.Text, nullable=True)` and its own docstring +at line 8 calls it "the alertscope TEXT field (JSON string)". Commit f484be8a3 +fixed the two neighbouring columns to agree with 0006 and left this one. Nothing +breaks on MySQL 8 - the SQLAlchemy JSON type serialises into TEXT and reads back +- and consumers already normalise both shapes +(`plugins/endpoints/api/routes.py:4011-4020`). The cost is that the model is not +a true description of the deployed column, which is what autogenerate reads. -**Effort:** 80 hours (high risk, requires careful schema analysis) -**Priority:** Q4 2026 (CRITICAL - blocks confidence in production schema) +**Related gap that is real:** the naming gate cannot see migrations at all. +`scripts/check-naming-and-style.sh:236` greps +`^\s+[a-z]+_[a-z_]+\s*=\s*db\.Column`, which only matches a model attribute +assignment; migrations write `sa.Column('name', ...)` with the name as a +positional string. That is how the shipped build of +`plugins/endpoints/migrations/versions/0021_client_certificates.py` created +`client_cert_fingerprint`, `cert_issued_at` and `cert_expires_at` and passed the +gate (`git show 5cd533239:plugins/endpoints/migrations/versions/0021_client_certificates.py`, +lines 20-28). The tree is conformant today, because 0021 and 0022 are now +no-ops and 0023 renamed the survivor; the gate simply would not notice the next +one. ### 4. Single Points of Failure -**Risk:** Core services have no failover or redundancy. +**Verdict: UNVERIFIED from this repository, and partly STALE.** -**Evidence:** -- Single database instance (no replication) -- Single Flask app instance (gunicorn workers on one server) -- Single Docker host (no orchestration) -- No load balancing +Replication, load balancing and orchestration are properties of the reference site +deployment, not of this tree, so nothing here can confirm or deny them. Two +parts of the original text are wrong about what the repo ships: -**Impact:** -- Database outage = full system outage -- App server failure = downtime until manual restart -- Hardware failure = data loss without recent backup +- Restart policy is configured, not absent: `docker-compose.yml:24`, `:104` and + `:120` set `restart: unless-stopped`, and three services carry `healthcheck:` + blocks (`:38`, `:112`, `:219`). +- Backups are no longer "manual mysqldump weekly, stored on the same server". + See finding 4a. -**Mitigation (current):** -- Weekly manual backups (mysqldump) -- VM snapshots before deploys -- Docker restart on failure (docker-compose restart policy) +The underlying point - one database, one app host, air gap limits the options - +is a statement about the site, and belongs in a deployment document rather than +a code findings page. The 200+ hour estimate has no basis in anything measured. -**Proper solution:** High availability architecture -- Database: MySQL master-replica replication -- App: Multiple gunicorn instances behind nginx load balancer -- Orchestration: Docker Swarm or Kubernetes for auto-restart -- Monitoring: Automated health checks + alerting +### 4a. Backups (added 2026-09-18, correcting the original text) -**Effort:** 200+ hours (infrastructure redesign) -**Priority:** Q2 2027 (production risk, but air-gapped environment has limited HA options) +**Verdict: STALE. The repo ships a backup system.** -**Note:** Air-gap deployment model limits cloud HA solutions. On-premise HA requires hardware investment. +`scripts/backup/` contains `backup.sh`, `restore.sh`, `status.sh`, `watch.sh`, +`simulate-restore.sh` and `test-restore-datarev.sh`. Retention is enforced by +restic (`scripts/backup/backup.sh:134-141`: `--keep-daily 5 --keep-weekly 4 +--keep-monthly 6 --prune`), destinations include SMB and S3 as well as local +(`scripts/backup/README.md`), each set carries `manifest.json` with schema +revisions for compatibility checking, and restore runs from the Settings > +Backups UI with a schema compatibility check. A restore-simulation harness is in +the tree. ### 5. Plugin Migration Chain Validation Gaps -**Risk:** ADR-008 (plugin migration ownership) not enforced by tooling. +**Verdict: STALE as written, and the credited fix is the wrong artifact.** -**Evidence:** -- Manual inspection required to verify linear chains -- Plugin migrations can create branches (multiple head revisions) -- `PLUGIN_TABLE_OWNERS` order not validated against actual dependencies +Validation is in the test suite, in this repository, and has been for longer +than the `/migration-check` skill: `tests/test_plugin_migrations.py` holds nine +tests covering owners-versus-models, chain shape, no-op anchors and idempotency, +and pins each chain's head in `EXPECTED_HEAD_REVISION` +(`tests/test_plugin_migrations.py:125`). `tests/test_plugins/test_endpoints_credentials_migration.py` +runs the 0020-0023 chain twice from both a legacy and a fresh schema, plus +downgrade and re-upgrade. The endpoints chain today is 24 revisions with one +root and one head and no dangling `down_revision`. -**Example violation:** -``` -plugins/computers/migrations/versions/ - 001_initial.py (down_revision=None) - 002_add_access.py (down_revision=001) - 003_add_policies.py (down_revision=001) # BRANCH! Two revisions point to 001 -``` +The branch example in the original text (`003_add_policies.py` with +`down_revision=001`) is invented; no such file exists under +`plugins/computers/migrations/versions/`, which holds 6 revisions. -**Impact:** -- Alembic upgrade fails with "Multiple head revisions" error -- Plugin can't be enabled until migration chain fixed -- Hard to diagnose (requires reading migration files manually) - -**Proper validation:** `/migration-check` skill (created 2026-09-11) -- Checks: Each plugin has linear chain (no branches) -- Checks: `down_revision` references exist -- Checks: `PLUGIN_TABLE_OWNERS` order matches declared dependencies -- Status: Implemented, run via pre-commit hook - -**Effort:** DONE (skill created) -**Priority:** Resolved 2026-09-11 +**The real gap, which the original text missed:** no test asserts that a plugin +chain touches only the tables it owns. `plugins/endpoints/migrations/versions/0023_endpoint_credentials.py` +declares `plugin_name = 'endpoints'` (line 33) and then runs +`op.drop_index(..., table_name='assets')` at line 73 and `op.drop_column('assets', +column)` at line 76 against the core `assets` table, cleaning up after the +shipped 0021 which had added those columns from the same plugin chain. `assets` +is not, and cannot be, in `PLUGIN_TABLE_OWNERS['endpoints']` +(`shopdb/plugins/alembic_template.py:57-63`), so the metadata ownership filter +never saw it. Every step is guarded on the live schema and the forward direction +is clean, so nothing is broken; nothing stops the next one either. ### 6. Implicit Database Connection Sharing -**Risk:** Plugins share `db.session` without transaction isolation. +**Verdict: REFUTED as a blanket claim.** -**Evidence:** -- Multiple plugins write to database in same request -- No explicit transaction boundaries -- Rollback in one plugin affects others +Savepoints are in use where concurrency actually bites: +`plugins/endpoints/plugin.py:1318`, `:1387` and `:1692` all wrap work in +`with db.session.begin_nested():`, and the application-delta path reconciles a +unique-constraint collision inside that savepoint rather than sinking the +check-in. The original text's example - a printers rollback silently discarding +a computers write in the same request - is not backed by any cited call site, +and none was found. -**Example:** -```python -# computers plugin writes Computer -computer = Computer(hostname='PC1') -db.session.add(computer) - -# printers plugin writes Printer (same session!) -printer = Printer(name='Printer1') -db.session.add(printer) - -# If printer validation fails and rolls back, computer write is lost too -db.session.rollback() -``` - -**Impact:** -- Plugin A's failure can undo Plugin B's work -- No atomicity guarantee per-plugin -- Hard to debug (cross-plugin transaction interactions) - -**Best practice:** Explicit transaction scopes per plugin -```python -from shopdb.api import db - -with db.session.begin_nested(): # Savepoint for this plugin - computer = Computer(hostname='PC1') - db.session.add(computer) - db.session.flush() -# If exception here, only this savepoint rolls back -``` - -**Remediation:** -1. Audit plugin DB writes: find implicit `db.session.add()` without savepoint -2. Wrap in `begin_nested()` savepoints or explicit `commit()` -3. Document transaction patterns in PLUGIN-DEVELOPMENT.md - -**Effort:** 40 hours -**Priority:** Q1 2027 (data integrity risk, but low frequency in practice) +A per-plugin transaction policy is still not written down anywhere. That is a +documentation item for PLUGIN-DEVELOPMENT.md, not a critical data-integrity +finding. ## High Priority Findings (P1) ### 7. Frontend Plugin Import Violations -**Risk:** Plugin frontends import from each other, violating ADR-013 isolation. +**Verdict: STALE. Enforced and passing.** -**Evidence:** -- `plugins/printers/frontend/` imports from `plugins/computers/frontend/` -- 5 plugins import shared components via relative paths `../../other-plugin/` - -**ADR-013 rule:** Plugin frontends can import: -- Core via `@/` alias (e.g., `@/components/Shared.vue`) -- Own tree via `./` or `../` within plugin -- NOT other plugin trees - -**Proper pattern:** -```javascript -// BAD: Cross-plugin import -import PCSelect from '../../computers/frontend/components/PCSelect.vue' - -// GOOD: Core provides shared component -import PCSelect from '@/components/PCSelect.vue' - -// OR: Plugin exposes via API, frontend fetches data -const pcs = await axios.get('/api/computers/') -``` - -**Enforcement:** `check-naming-and-style.sh` now checks for escaping imports -```bash -grep -rPn "(import|from)\s+['\"]([^'\"]*\.\./\.\./|[^'\"]*/plugins/)" \ - plugins/*/frontend/ -``` - -**Remediation:** -1. Move shared components to `frontend/src/components/` -2. Update plugin imports to use `@/` alias -3. Run pre-commit check to catch violations - -**Effort:** 20 hours -**Priority:** Q4 2026 (blocks lean builds) -**Status:** Linter rule added 2026-09-11 (commit 4943e182c3) +`scripts/check-naming-and-style.sh` runs "Checking for cross-plugin / escaping +imports in plugin frontends" and the whole script exits 0 on this branch. The +established pattern is a core component consumed through the `@/` alias: 97 +plugin frontend files already import that way, and the kiosk allowlist editor +added in this range follows it +(`frontend/src/components/NetworkAllowlistPicker.vue`, consumed at +`plugins/usb/frontend/views/USBSettings.vue:108` and +`plugins/printedparts/frontend/views/PrintedPartsSettings.vue:164`). ### 8. Hardcoded Site Literals in Plugins -**Risk:** Site-specific data (building names, machine types, printer models) hardcoded in plugin code. +**Verdict: REFUTED as stated. HOLDS (narrowed) as a gate coverage gap.** -**Evidence (violations of ADR-015):** -- `plugins/machines/models.py` has `MACHINE_TYPES = ['Lathe', 'Mill', 'CNC']` hardcoded -- `plugins/printers/api/routes.py` validates `building in ['Bldg 300', 'Bldg 400']` -- 12 plugins have site-specific literals +`grep -rn "MACHINE_TYPES" . --include=*.py` and +`grep -rn "Bldg 300\|Bldg 400" . --include=*.py` both return **nothing**. The +ADR-015 block in `scripts/check-naming-and-style.sh:392-462` runs and passes, +and a control pattern over the same directories returns 1995 lines, so the +pipeline is not vacuous. -**ADR-015 rule:** Site data MUST live in: -- `shopdb/core/settings/` - Settings table defaults -- `shopdb/core/seed/` - Initial data SQL -- `scripts/site_imports/` - Import tools for site-specific data - -**Proper pattern:** -```python -# BAD: Hardcoded site data -MACHINE_TYPES = ['Lathe', 'Mill', 'CNC'] - -# GOOD: Setting with neutral default -machine_types = Setting.get('machines_allowed_types', default='[]') -MACHINE_TYPES = json.loads(machine_types) -``` - -**Enforcement:** `check-naming-and-style.sh` scans for literal building names, FQDNs, network ranges - -**Remediation:** -1. Extract literals to Settings table -2. Update plugins to read from Settings -3. Seed data with site-specific values during setup - -**Effort:** 30 hours -**Priority:** Q1 2027 (blocks multi-site deployments) +**The narrowed finding:** the ADR-015 scan is scoped at +`scripts/check-naming-and-style.sh:436-440` to `plugins/ shopdb/ scripts/ +deploy/ frontend/src/ tools/` with `--include` on +`.py/.vue/.js/.ps1/.psm1/.sh/.iss/.json/.html`. `agent/` is not in that list and +`.cs` is not an included extension, so roughly 2,500 lines of C# added in this +range - `AgentCredentials.cs`, `AgentHttpFactory.cs`, `EnrollmentService.cs`, +`AgentPipeServer.cs`, `ProtectedFileSystem.cs`, `StrictModePreamble.cs`, plus +`agent/appsettings.json` - ship to a sister site unscanned. Re-running the +assembled site patterns by hand over `agent/` (excluding bin/obj) finds one hit, +prose in `agent/ShopDbEndpointAgent/ShopDbEndpointAgent.csproj:108`, which +ADR-015 permits. The tree is clean; the gate would not notice if it stopped +being clean. Note this is a gap in ADR-015's own stated scope, not a breach of +it: the 2026-08-14 amendment names those six directories. ### 9. Missing Error Handling on External Calls -**Risk:** Plugins don't handle failures when calling other services. +**Verdict: REFUTED.** -**Evidence:** -- ServiceNow plugin: no timeout on HTTP requests, no retry logic -- LDAP sync: no handling of connection failures -- Email plugin: assumes SMTP always succeeds +The plugins the finding names do not exist. `ls plugins/` gives: applications, +backups, computers, employees, endpoints, geenforce, guides, knowledgebase, +machines, measuringtools, network, notifications, printedparts, printers, +slides, tools, usb, warranty. There is no ServiceNow plugin, no LDAP sync +plugin and no email plugin; mail is core (`shopdb/utils/mailer.py`). -**Impact:** -- Hung requests block worker threads -- Transient failures cause permanent data loss -- No user feedback when external service down +Every outbound HTTP call in the tree carries a timeout and an exception handler: -**Best practice:** Timeouts, retries, circuit breakers -```python -# BAD: No timeout -response = requests.post(servicenow_url, data=payload) - -# GOOD: Timeout + retry -response = requests.post(servicenow_url, data=payload, timeout=10) -if response.status_code >= 500: - # Retry with exponential backoff - time.sleep(2 ** attempt) -``` - -**Remediation:** -1. Audit external calls: `grep -r "requests\.(get|post)" plugins/` -2. Add timeouts (default 10s) -3. Add retry logic for transient failures (5xx errors) -4. Log failures for monitoring - -**Effort:** 30 hours -**Priority:** Q4 2026 (production stability) +- `plugins/warranty/services/providers.py:129` and `:157` - `timeout=HTTP_TIMEOUT`, + `except requests.RequestException` raising `WarrantyLookupError`. +- `plugins/printers/services/zabbix_service.py:95` and `:126` - + `timeout=self.REACHABLE_TIMEOUT` / `self.API_TIMEOUT`, caught. +- `shopdb/utils/mailer.py:238` - `timeout=10`, caught, returns `(False, error)` + and never raises. ### 10-12. Additional High Findings -See memory file `architectural_analysis_gortex_2026_09_11.md` for: -- Inconsistent permission checking across plugins -- Plugin state leakage via module-level globals -- Missing database connection pool tuning +The original text deferred these to a memory file rather than citing code: +"inconsistent permission checking across plugins", "plugin state leakage via +module-level globals", "missing database connection pool tuning". + +The third is REFUTED. The pool is sized on purpose and env-driven: +`shopdb/config.py:20-22` (`DB_POOL_SIZE` 10, `DB_MAX_OVERFLOW`, `DB_POOL_TIMEOUT` +5) and `:48-72`, with the reasoning for the non-default `pool_timeout` written +in place, and `docker-compose.yml:172-193` documents the +`workers x (pool_size + max_overflow)` arithmetic against MySQL's +`max_connections`. + +The other two were not re-checked, because neither cites a file. They should be +restated with a file:line or dropped. ## Medium Priority Findings (P2) ### 13. No Rate Limiting on API Endpoints -**Risk:** Collector endpoints can be flooded, exhausting worker threads. +**Verdict: HOLDS (narrowed). Login is limited; two kiosk surfaces are not.** -**Mitigation:** See Security-Audit-2026-09-11.md (login rate limiting planned Q1 2027) +Login rate limiting exists and is not "planned Q1 2027": `shopdb/core/api/auth.py:35` +`_login_ratelimited()`, keyed per host at `:52`, applied at `:80`, with +`tests/test_core/test_auth_ratelimit.py` over it. Enrollment has its own limiter +(`plugins/endpoints/api/ratelimit.py`, 300 per 60 seconds per IP applied at +`plugins/endpoints/api/enrollment.py:306`). + +What has none: the USB kiosk routes. `grep` for `limiter` or `ratelimit` across +`plugins/usb/` returns nothing, while `plugins/usb/api/selfhosted.py:346-371` +resolves a posted badge to an employee name and returns it. That is an +unauthenticated, unthrottled badge-to-name lookup for any caller inside the +configured kiosk CIDRs. + +Both limiter counters live in per-process `SimpleCache` +(`plugins/endpoints/api/ratelimit.py:10-11`), so at the shipped four gunicorn +workers the effective budget is four times the configured maximum. ### 14. Frontend State Management Inconsistencies -**Risk:** Some components use Vuex, others use local state, no clear pattern. +**Verdict: REFUTED.** -**Impact:** Hard to track state flow, data duplication, synchronization bugs - -**Recommendation:** Standardize on Pinia (Vue 3 state management) or document when to use local vs global state +`grep -rn "vuex" frontend/src/` returns **nothing**, and `frontend/package.json:28` +declares `"pinia": "^2.1.0"`. There is no Vuex-versus-Pinia split to +standardise. ### 15. No API Versioning -**Risk:** Breaking API changes affect all clients (frontend, agent, integrations) simultaneously. +**Verdict: HOLDS.** -**Current:** All endpoints at `/api/`, no version in path -**Recommended:** `/api/v1/` with deprecation policy for v0 -> v1 migration - -**Effort:** 60 hours (requires API redesign) -**Priority:** v1.0 release +`grep -rn "api/v1" shopdb/ --include=*.py` returns nothing; every route is +mounted under `/api/`. This is a v1.0 decision, not a defect, and ADR-002 already +carries the versioning story for the plugin contract. The 60-hour estimate has +no stated basis. ### 16-22. Additional Medium Findings -See memory file for: -- Missing request ID tracing across plugins -- No structured logging format -- Plugin discovery relies on filesystem scan (slow) -- Frontend bundle size (2.1MB) larger than ideal -- No content security policy headers -- Database connection pool too small for peak load -- Missing health check endpoints for monitoring +Re-checked on 2026-09-18: -## Remediation Priority +- **No content security policy headers: REFUTED as stated.** `shopdb/__init__.py:340` + sets `Content-Security-Policy: frame-ancestors 'self'` on every response, plus + `X-Content-Type-Options`, `X-Frame-Options` and `Referrer-Policy` at `:334-337`; + uploads carry a stricter `default-src 'none'; sandbox` + (`shopdb/utils/uploads.py:13`), pinned by `tests/test_core/test_security_headers.py`. + The accurate, narrower statement is that there is no `script-src` policy, and + the code says so in place and says why. +- **Missing health check endpoints: REFUTED.** `shopdb/core/api/dashboard.py:318` + is `GET /api/dashboard/health`. +- **Database connection pool too small: REFUTED.** See 10-12 above. +- **No request ID tracing: HOLDS.** No request-id plumbing was found in + `shopdb/__init__.py`. +- **No structured logging format: not re-checked.** +- **Plugin discovery relies on a filesystem scan: HOLDS, by design.** ADR-003 + decides filesystem discovery under `plugins/`. "Slow" was never measured. +- **Frontend bundle size 2.1MB: UNVERIFIED.** No build was run for this page. -| Finding | Severity | Effort (hrs) | Priority | Status | -|---------|----------|--------------|----------|--------| -| Schema drift (models vs migrations) | CRITICAL | 80 | Q4 2026 | Planned | -| Circular plugin dependencies | CRITICAL | 60 | Q1 2027 | Planned | -| Undeclared import dependencies | CRITICAL | 40 | Q4 2026 | Planned | -| Migration chain validation | CRITICAL | 0 | DONE | /migration-check skill | -| Frontend plugin imports | HIGH | 20 | Q4 2026 | Linter added | -| Site literals in plugins | HIGH | 30 | Q1 2027 | Planned | -| Missing error handling | HIGH | 30 | Q4 2026 | Planned | -| Implicit DB session sharing | HIGH | 40 | Q1 2027 | Planned | -| Single points of failure | CRITICAL | 200+ | Q2 2027 | Infrastructure | -| API versioning | MEDIUM | 60 | v1.0 | Planned | +## Remediation Priority (revised 2026-09-18) -## Testing Performed +| Finding | Verdict | What is actually left to do | +|---------|---------|------------------------------| +| Circular plugin dependencies | REFUTED / narrowed | Decide whether deferred cross-plugin imports must be guarded; guard `agentauth.py:118` and `policy_routes.py:44` | +| Undeclared core imports | REFUTED | Nothing | +| Schema drift | REFUTED / narrowed | Align `notification.py:158` with migration 0006; teach the naming gate to read `sa.Column('...')` | +| Plugin chain touches core tables | HOLDS (new) | Add a test that a plugin chain writes only owned tables | +| Migration chain validation | STALE | Nothing; `tests/test_plugin_migrations.py` is the gate | +| Implicit DB session sharing | REFUTED | Document the transaction pattern in PLUGIN-DEVELOPMENT.md | +| Frontend plugin imports | STALE | Nothing; gate passes | +| Site literals in plugins | REFUTED / narrowed | Extend the ADR-015 scan to `agent/` and `.cs` | +| Missing external-call error handling | REFUTED | Nothing | +| Rate limiting | HOLDS (narrowed) | Throttle the USB kiosk badge lookup; decide whether per-process counters are acceptable at 4 workers | +| Vuex/Pinia split | REFUTED | Nothing | +| API versioning | HOLDS | v1.0 decision | +| Single points of failure | UNVERIFIED | Belongs in a deployment document | + +## Testing Performed (2026-09-18 re-check) -**Gortex semantic analysis:** ```bash -gortex explore "find circular dependencies between plugins" -gortex explore "find undeclared imports from shopdb.core" -gortex explore "find schema mismatches between models and migrations" +# Every claim above was checked with a grep or a read on gh-wj at 35d4fcd41. +grep -rn "^from plugins\." plugins/ --include=*.py # 0 hits +grep -rn "from shopdb\.\(core\|utils\|extensions\|services\)" \ + plugins/ --include=*.py # 0 hits +grep -rPn "sa\.Column\(\s*'[a-z]+_[a-z_]+'" \ + plugins/*/migrations/versions/*.py migrations/versions/*.py # 0 hits +grep -rn "MACHINE_TYPES\|Bldg 300\|Bldg 400" . --include=*.py # 0 hits +grep -rn "vuex" frontend/src/ # 0 hits +grep -rn "api/v1" shopdb/ --include=*.py # 0 hits +bash scripts/check-naming-and-style.sh # exit 0 ``` -**Migration chain validation:** -```bash -/migration-check # Runs pytest tests/test_plugin_migrations.py -# PASS: All plugin migration chains are linear -``` - -**Frontend import validation:** -```bash -bash scripts/check-naming-and-style.sh -# Scans for plugin-to-plugin imports (../../ escaping) -# PASS: No violations after cleanup -``` - -## Recommendations - -**Immediate (Q4 2026):** -1. Fix schema drift (models vs migrations) - CRITICAL -2. Enforce plugin API imports (no shopdb.core) - enables refactoring -3. Add error handling to external service calls - stability - -**Short-term (Q1 2027):** -1. Break circular plugin dependencies - enables lean builds -2. Remove site literals from plugins - enables multi-site -3. Fix implicit DB session sharing - data integrity - -**Long-term (Q2-Q4 2027):** -1. High availability architecture (if feasible in air-gap) -2. API versioning for v1.0 release -3. Frontend optimization (bundle size, state management) +No test suite was run for this re-check; every verdict above is static. ## References -- Gortex MCP Server: Semantic code intelligence for architectural analysis - ADR-001: Asset as Platform Contract (plugin isolation model) +- ADR-003: Plugin Distribution (filesystem discovery) - ADR-008: Plugin Migration Ownership (linear chains, PLUGIN_TABLE_OWNERS) - ADR-013: Plugin Catalog and Lean Builds (frontend isolation) - ADR-015: Site-Specific Configuration (no literals in code) -- Memory file: `.claude/memory/architectural_analysis_gortex_2026_09_11.md` +- ADR-Conformance-2026-09-18.md: the ADR-by-ADR result of the 2026-09-18 review, + including the fact that the ADR set is not in this repository diff --git a/docs/wiki-drafts/Maintenance-Footprint-Baseline.md b/docs/wiki-drafts/Maintenance-Footprint-Baseline.md index b39324c1..e7980424 100644 --- a/docs/wiki-drafts/Maintenance-Footprint-Baseline.md +++ b/docs/wiki-drafts/Maintenance-Footprint-Baseline.md @@ -1,8 +1,37 @@ # Maintenance Footprint Baseline - 2026-09-11 -**Total annual burden:** 936 hours/year -**Risk level:** 8 critical + 10 high operational risks +**Total annual burden:** 936 hours/year (estimate, see the note below) +**Risk level:** originally stated as 8 critical + 10 high operational risks **Optimization potential:** 147-205 hours/year savings with automation +**Last checked against code:** 2026-09-18, branch gh-wj at 35d4fcd41 + +## About the numbers (added 2026-09-18) + +Every hour figure on this page is an estimate with no recorded measurement +behind it. No time log, ticket export or deploy log is cited anywhere, and none +was found in the repository. Treat the hours as an ordering of where effort +goes, not as data. The same applies to the row counts and growth rates in the +Database Management section: no query output is cited and none could be taken +from a repository checkout. + +What CAN be checked from the repository is which of the recommendations have +since been built. Several have. The sections below are annotated +accordingly, dated 2026-09-18, and the stale risk-matrix rows are corrected. + +## What the tree actually contains (2026-09-18) + +| Thing | Count | How counted | +|-------|-------|-------------| +| Plugins | 18 directories, 17 with a `manifest.json` | `ls -d plugins/*/`; `applications` carries no manifest | +| Plugin migration revisions | 70 across 17 chains | `ls plugins/*/migrations/versions/*.py` | +| Largest single chain | `endpoints`, 24 revisions | one root, one head, no branch | +| Core migration revisions | 58 | `ls migrations/versions/*.py` | +| Test files | 223 | `find tests -name "test_*.py"` | +| Top-level scripts | 29 (.sh/.py/.ps1/.mjs), plus `scripts/backup`, `scripts/dev`, `scripts/migration`, `scripts/loadtest`, `scripts/site_imports` | `ls scripts/` | +| Automation skills | 13 | `ls /skills/*.md` minus README | + +The "13 automation skills" claim later on this page is the one headline figure +that checks out exactly. ## Executive Summary @@ -29,6 +58,19 @@ Baseline assessment of ShopDB operational burden across air-gapped production en - Savings: 52 hrs/year (from 52 hrs to ~0 hrs, fully automated) - Status: PLANNED for Q4 2026 +**2026-09-18 update: half built, and the half that is built is manual.** +Retention exists as a service and a CLI, not as a cron job and not as a stored +procedure. `shopdb/core/services/auditretention.py` provides `configureddays()`, +`stats()` and `purge()`; `flask audit stats` (`shopdb/cli/__init__.py:1891`) +reports size against the configured window and `flask audit purge` +(`:1911`) deletes, with a dry-run preview first and `--days` to override for one +run. The window is the `audit_retentiondays` setting +(`shopdb/core/api/settings.py:1049`), and its default is **`'0'`, which means +keep everything forever**: nothing is deleted until an operator sets a value. +There is no scheduler in the application and no cron entry in the repository, so +the purge runs when somebody runs it. The remaining work is a value in Settings +> Site & Facility and a host cron line, not code. + **Endpoint events cleanup:** 26 hrs/year (HIGH RISK) - Current: 450K rows, 127MB, no retention - Growth: Process events every 30 seconds, app launches, USB connects @@ -38,10 +80,32 @@ Baseline assessment of ShopDB operational burden across air-gapped production en - Implementation: Similar cron job as audit log - Savings: 26 hrs/year (fully automated) +**2026-09-18 update: partly built, per table rather than globally.** Operator +reports carry a retention window defaulting to 7 days +(`plugins/endpoints/api/routes.py:3619-3620`, +`endpoints.operatorreport.retentiondays`, 0 or less meaning keep forever), and +endpoint health transitions are trimmed to a high-water mark on write, keeping +every unresolved row regardless of age +(`plugins/endpoints/services/health.py:326`). The row counts and the 127MB +figure above were never sourced and are not verifiable from a checkout. + **Database backups:** 78 hrs/year (MEDIUM RISK) -- Current: Manual mysqldump weekly, stored on same server +- Current (as stated 2026-09-11): Manual mysqldump weekly, stored on same server - Risk: No offsite copies, no restore testing, backup growth unbounded -- Backup size: 2.1GB compressed, growing 15% per quarter +- Backup size: 2.1GB compressed, growing 15% per quarter (unsourced) + +**2026-09-18 update: STALE. This is built, and it is what the recommendation +below asks for.** `scripts/backup/` holds `backup.sh`, `restore.sh`, `status.sh`, +`watch.sh`, `simulate-restore.sh` and `test-restore-datarev.sh`, plus a +`Dockerfile.backup` and a compose file. Retention is enforced by restic: +`scripts/backup/backup.sh:134-141` runs `restic forget --keep-daily 5 +--keep-weekly 4 --keep-monthly 6 --prune`. Destinations are configured in +Settings > Backups and include local, SMB and AWS S3, so offsite is a setting +rather than a manual copy. Each backup set carries `db.sql.gz`, the `instance/` +tree and a `manifest.json` of schema revisions, and restore validates schema +compatibility before it runs. A restore-simulation harness is in the tree. +Backup credential redaction landed in the same commit as audit retention +(`scripts/redact_backup_secrets.py`). **Recommended:** Automated backup with rotation - Daily incremental, weekly full, monthly archive @@ -69,7 +133,7 @@ Baseline assessment of ShopDB operational burden across air-gapped production en **Air-gap coordination:** 312 hrs/year (CRITICAL BURDEN) - Current: Manual bundle creation, USB transfer, deploy in 3 environments -- Environments: dev PC (gitea) <-> work PC (GitHub) <-> VM (prod) +- Environments: dev PC (the internal git server) <-> work PC (GitHub) <-> VM (prod) - Steps per deploy: 14 manual steps, 45-90 minutes each - Frequency: 2-3 deploys/week = 104-156 deploys/year @@ -111,6 +175,13 @@ logging: ``` - Savings: 52 hrs/year (fully automated, from manual cleanup) +**2026-09-18 update: STILL OPEN, and still the cheapest item on this page.** +`grep -c logging docker-compose.yml` returns 0: there is no `logging:` block on +any service. Three services do carry `restart: unless-stopped` +(`docker-compose.yml:24`, `:104`, `:120`) and three carry `healthcheck:` blocks +(`:38`, `:112`, `:219`), so the container lifecycle is managed; log size is not. +The 500MB/week growth figure was never sourced. + **Image rebuilds:** 52 hrs/year - Current: Manual rebuild when dependencies change - Frequency: ~20 rebuilds/year (Python deps, base image updates) @@ -197,9 +268,57 @@ logging: | Docker log disk fill | HIGH | High (3-6 mo) | Container crashes | Log rotation Q4 2026 | | Deployment coordination errors | HIGH | Medium (2-3/year) | Failed deploys, downtime | Automation scripts implemented | | Migration conflicts (ADR-008) | HIGH | Low | Plugin blocked | Migration-check skill | -| Agent certificate validation disabled | CRITICAL | Low | MITM attack | Per-endpoint cert pinning Q4 2026 | -| No login rate limiting | HIGH | Medium | Brute force attack | Rate limiting Q1 2027 | -| Unauthenticated collector endpoints | CRITICAL | N/A | FIXED 2026-09-11 | Device token auth | +| Agent certificate validation disabled | CORRECTED 2026-09-18 | n/a | n/a | Built. See note below | +| No login rate limiting | CORRECTED 2026-09-18 | n/a | n/a | Built. See note below | +| Unauthenticated collector endpoints | CORRECTED 2026-09-18 | n/a | n/a | Per-PC endpoint credentials, not device tokens. See note below | +| Docker log disk fill | HIGH | unmeasured | Container crashes | STILL OPEN, no `logging:` block in docker-compose.yml | + +### Risk matrix corrections (2026-09-18) + +**Agent certificate validation.** Not disabled. `AgentHttpFactory` is the only +`HttpClient` construction site in the agent, every channel runs +`ServerPinSet.Validate`, and `Validate` has no path returning true for an +unmatched pin (`agent/ShopDbEndpointAgent.Core/AgentHttpFactory.cs:291-331`). A +failed registry re-read returns null and keeps the existing pins rather than +silently unpinning (`:352-386`). The three SYSTEM PowerShell reporters refuse to +send at all when `ServerPins` is set and the check cannot be enabled +(`plugins/computers/client/Report-AssetToShopDB.ps1:228-247`, and the same block +in `Report-PrintersToShopDB.ps1` and `upgrade-shopdbagent.ps1:436-447`). The one +`ServerCertificateValidationCallback = {$true}` left in the tree is +`test-vm-connectivity.ps1:44`, a developer connectivity probe. + +**Login rate limiting.** Built, and not "Q1 2027": +`shopdb/core/api/auth.py:35` `_login_ratelimited()`, keyed per host at `:52`, +applied at `:80`, with `tests/test_core/test_auth_ratelimit.py` over it. Note +the counter lives in per-process `SimpleCache`, so at four gunicorn workers the +effective budget is four times the configured maximum. + +**Collector authentication.** The mechanism is a per-PC endpoint credential, +not a device token and not ADR-016. An enrolled PC sends +`X-Endpoint-Credential` (`plugins/endpoints/api/agentauth.py:1-18`); the server +stores only a sha256 hash plus a 12-character display prefix +(`plugins/endpoints/models/credential.py`); once a PC holds an active +credential the shared key is refused for it +(`plugins/endpoints/api/agentauth.py:129-134`). ADR-016's own mechanism - +`credentials.fetch`, `GET /api/collector/credentials`, Fernet at rest - is +**not built**: grepping `shopdb/` and `plugins/` for `credentials.fetch`, +`collector/credentials` and `Fernet` returns zero hits, matching ADR-016's own +"Implementation status, 2026-08-14". The rollout runbook is +`plugins/endpoints/docs/PER-PC-CREDENTIALS-ROLLOUT.md`. + +One boundary the runbook does not state: turning the shared key off does not +close the four legacy core collector routes. `/pc` +(`shopdb/core/api/collector.py:349`), `/apps` (`:387`), `/heartbeat` (`:459`) +and `/bulk` (`:494`) are decorated `@require_api_key`, which at `:154-164` +checks only `COLLECTOR_API_KEY` or a `collector.ingest`-scoped token and never +consults `agent_shared_key_refused`. Anything still holding the env shared key +can rewrite logged-in user, last boot time, serial number and installed-app +inventory for any hostname. Regenerating the key in the UI +(`plugins/endpoints/api/routes.py:1864-1884`) mints a new managed token and +clears the env fallback flag but does not change `COLLECTOR_API_KEY`, and +`_env_fallback_disabled(None)` returns False (`collector.py:60-74`), so the +legacy routes keep honouring the env key. Rotating `COLLECTOR_API_KEY` in +`.env` is the step that closes it. ## Automation Savings Summary @@ -313,11 +432,23 @@ rsync -av $BACKUP_DIR/ /mnt/offsite-backups/shopdb/ ### 5. check-naming-and-style.sh Optimization (DONE) -**Effort:** 2 hours -**Savings:** Script runtime 5x faster (10s -> 2s) -**Status:** Committed 2026-09-11 (commit 4943e182c3) +**Status:** In the tree and on by default. `USE_GIT_LS_FILES` defaults to `1` +(`scripts/check-naming-and-style.sh:24`) and the script takes the +`git ls-files -z` path whenever it is inside a work tree (`:26`, `:159`, `:183`, +`:218`), filtering out `migrations/versions/`, `frontend/dist/`, `staticdocs/` +and `.plugins-staged/`. Setting `USE_GIT_LS_FILES=0` falls back to recursive +grep with `--exclude-dir` for non-git checkouts. The whole script exits 0 on +gh-wj at 35d4fcd41. -**Impact:** 10-15 minutes saved per commit = ~26 hrs/year for team +**The 5x / 10s-to-2s claim is unsourced** and no timing was recorded for this +page. The savings line below it follows from it and is equally unsourced. + +**Coverage note added 2026-09-18:** the root-anchored `^migrations/versions/` +filter does not exclude `plugins/*/migrations/versions/`, so plugin migrations +ARE scanned. They are not caught by the column rule, though: that rule greps +`^\s+[a-z]+_[a-z_]+\s*=\s*db\.Column` (`:236`), which only matches a model +attribute assignment, while migrations write `sa.Column('name', ...)`. The +column name is created in the one file the naming gate cannot read. ### 6. Pre-commit Hook Integration @@ -349,17 +480,36 @@ pytest tests/ -x || exit 1 ## Long-Term Improvements (Planned) -### 1. Agent Delta Updates (Q1 2027) +### 1. Agent Delta Updates - BUILT (2026-09-18 update) -**Current:** Agent POSTs full inventory (11KB) every 30 seconds -**Proposed:** POST only deltas (added/removed apps, IP changes) = ~500 bytes +**Original text:** "Agent POSTs full inventory (11KB) every 30 seconds. +Proposed: POST only deltas." Both halves are now wrong. -**Savings:** -- 99% reduction in network traffic (17GB/day -> 170MB/day for 500 endpoints) -- 99% reduction in database writes (only changed data) -- Performance: Collector API load reduced from 1.44M requests/day to ~14K writes/day +**The cadence was never 30 seconds.** The agent default is 900 seconds with up +to 60 seconds of jitter: `agent/ShopDbEndpointAgent/appsettings.json` +(`"cadenceseconds": 900`, `"jitterseconds": 60`) and +`agent/ShopDbEndpointAgent.Core/AgentConfig.cs:14` and `:31`. The 17GB/day and +1.44M-requests/day figures were derived from the wrong interval and should not +be quoted. -**Effort:** 40 hours (agent changes + server changes + testing) +**The delta is implemented, on the server side.** The agent still reports the +full installed-software list each cycle, but each entry carries a SHA-256 +fingerprint of `displayname|displayversion|publisher`, and +`_replace_applications` (`plugins/endpoints/plugin.py:1122-1140`) compares +fingerprint sets instead of delete-all-insert-all: adds for new fingerprints, +deletes for departed ones, unchanged rows untouched (`:1277-1289`). The +docstring states the effect as roughly 100 database operations per check-in +down to about 2. It is guarded: fingerprint format validation, a delete-count +check that warns rather than aborting, a memory cap above 5000 apps, a legacy +fall-back for agents that send no fingerprints, and collision reconciliation +inside a savepoint (`:1318`, `:1387`). The unique `(assetid, appfingerprint)` +index was added NULL-filled by migration +`plugins/endpoints/migrations/versions/0020_*`, so index creation cannot fail on +a populated production table. + +Remaining network-side work, if anyone wants it, is making the agent send only +the changed entries rather than the full list. The database win is already +taken. ### 2. Automated Staging Tests (Q4 2026) @@ -369,14 +519,17 @@ pytest tests/ -x || exit 1 **Savings:** 50-100 hrs/year (from deployment coordination) **Effort:** 20 hours (test suite + CI integration) -### 3. Per-Endpoint Certificate Pinning (Q4 2026) +### 3. Per-Endpoint Certificate Pinning - BUILT (2026-09-18 update) -**Current:** Certificate validation disabled (security risk) -**Proposed:** Each endpoint validates server cert against pinned thumbprint - -**Savings:** Eliminates CRITICAL security risk -**Effort:** 40 hours (agent changes + deployment script + mass agent update) -**Status:** Planned (see Security-Audit-2026-09-11.md) +Pins are SPKI SHA-256, validated as 64 lowercase hex on the way in, enforced on +every agent channel, and owned by the registry rather than by `appsettings.json` +(`plugins/endpoints/client/Set-ShopDBServerPins.ps1` writes +`HKLM:\SOFTWARE\GE\ShopDB` `ServerPins`; the precedence is stated in +`plugins/endpoints/docs/PER-PC-CREDENTIALS-ROLLOUT.md`, section "The registry +wins"). A CA-flagged certificate is pinned but deliberately NOT imported into +`LocalMachine\Root`, and the build warns the admin through an +`X-ShopDB-Build-Warning` header. See the risk-matrix correction above for the +enforcement call sites. ## Monitoring & Alerting @@ -399,8 +552,11 @@ pytest tests/ -x || exit 1 ## References - Security Audit 2026-09-11: 4 critical + 6 high vulnerabilities, some fixed, some planned -- Architectural Findings 2026-09-11: 22 findings (6 critical, 6 high) from Gortex analysis -- Automation Skills: 13 skills created in `.claude/skills/` directory -- Deployment Documentation: `docs/deployment/` and `.claude/setup/` guides +- Architectural Findings 2026-09-11: re-checked 2026-09-18; most of the 22 + findings are refuted or stale, see that page for the verdict on each +- ADR-Conformance-2026-09-18: which ADRs were checked in the 2026-09-18 review + and which hold +- Automation Skills: 13 skills created in the assistant skills directory +- Deployment Documentation: `docs/deployment/` and the assistant setup guides - ADR-004: Deployment Topology (air-gapped architecture) - ADR-008: Plugin Migration Ownership (linear chain requirement) diff --git a/docs/wiki-drafts/README.md b/docs/wiki-drafts/README.md index 98dfba5e..4e71d6da 100644 --- a/docs/wiki-drafts/README.md +++ b/docs/wiki-drafts/README.md @@ -1,6 +1,18 @@ -# Wiki Page Drafts - 2026-09-11 +# Wiki Page Drafts - 2026-09-11, revised 2026-09-18 -This directory contains drafted wiki pages based on work completed Q3 2026. Copy these to the GitHub wiki at https://github.com/ge-aero/shopdb-flask/wiki. +This directory contains drafted wiki pages based on work completed Q3 2026. Copy +these to the GitHub wiki at https://github.com/ge-aero/shopdb-flask/wiki. + +**Revised 2026-09-18, branch gh-wj at 35d4fcd41.** A full review re-checked every +page here against the code. `Architectural-Findings-2026-09-11.md` was +substantially wrong and now carries a verdict and evidence per finding. +`Maintenance-Footprint-Baseline.md` is annotated where the tree has moved on. +`Security-Audit-2026-09-11.md` was refreshed with a verdict per finding and +thirteen new ones. `ADR-019-...md` had two internal contradictions corrected. A +new page, `ADR-Conformance-2026-09-18.md`, was added. The SECURITY.md draft below +had its ADR-016 section replaced: it described a decision ADR-016 never took and +banned the registry storage the reference site fleet depends on. Nothing here is ready to +paste without reading the 2026-09-18 notes inside each file first. ## New Pages to Create @@ -19,6 +31,15 @@ This directory contains drafted wiki pages based on work completed Q3 2026. Copy **Related commits:** fe09d2745e, d59d0c3194 +**2026-09-18:** corrected against the shipped code. The "Backend: API Endpoint" +code block showed `@jwt_required(optional=True)` iterating +`get_enabled_plugin_instances()`; neither exists. It now shows +`shopdb/core/api/plugins.py:46-95` as it stands, gated on `settings.view`. The +Future Extensions section no longer calls the endpoint anonymous. Everything +else in the draft checked out, including the three constraints. Note +`docs/openapi.json` still documents this endpoint as anonymous, so publishing +the ADR without regenerating the spec leaves the two disagreeing. + --- ### 2. Security Audit - September 11, 2026 @@ -54,12 +75,23 @@ This directory contains drafted wiki pages based on work completed Q3 2026. Copy - Security/compliance: 78 hrs/year (scanning + updates) **Automation:** -- 13 Claude Code skills created +- 13 assistant automation skills created (this figure checks out: `ls /skills/*.md`) - 147-205 hrs/year saved (from 936 baseline) - 7 quick wins identified (4 hours - 6 hours effort each) **Risk matrix:** 8 critical + 10 high operational risks +**2026-09-18:** annotated. Every hour figure and row count on that page is an +unsourced estimate and the page now says so. Four recommendations have since +been built and the page records what and where: audit-log retention (service, +CLI and setting, but default 0 and no scheduler), backups (restic with +5/4/6 retention, SMB and S3 destinations, restore simulation), agent delta +updates, and per-endpoint certificate pinning. Three risk-matrix rows were +stale and are corrected: certificate validation is not disabled, login rate +limiting exists, and the collector fix is per-PC endpoint credentials rather +than "device token auth". Docker log rotation is still genuinely missing: +`grep -c logging docker-compose.yml` returns 0. + --- ### 4. Architectural Findings - September 11, 2026 @@ -83,6 +115,41 @@ This directory contains drafted wiki pages based on work completed Q3 2026. Copy **Remediation timeline:** Q4 2026 - Q4 2027 +**2026-09-18: do not publish this page as it stood.** Re-checking it against +gh-wj refuted most of it. There is no module-level cross-plugin import anywhere +(`grep -rn "^from plugins\." plugins/ --include=*.py` = 0 hits) and `machines` +does not import `computers` at all, so finding 1's cycle does not exist. No +plugin deep-imports `shopdb.core` (0 hits), so finding 2 is refuted outright. No +migration in the tree spells a column in snake_case, so finding 3's examples are +refuted. `MACHINE_TYPES`, `Bldg 300`, Vuex, a ServiceNow plugin and an +unhandled `requests.post` do not exist. The page has been rewritten to carry a +verdict and the evidence per finding, keeping the four that survive in narrowed +form. + +--- + +### 5. ADR Conformance - September 18, 2026 +**File:** `ADR-Conformance-2026-09-18.md` +**Wiki path:** `ADRs/ADR-Conformance-2026-09-18` +**Summary:** ADR-by-ADR result of the 2026-09-18 review of gh-wj at 35d4fcd41. +Fourteen ADRs hold with cited evidence; eight items are recorded as violations +or gaps, each with a file and line. + +**Reads first, before the table:** the ADR set does not exist on this branch. +`ls docs/adr` fails on gh-wj; ADR-001 through ADR-018 live only on the other +lineage. Nothing here states the rules and nothing here enforces them except +`scripts/check-naming-and-style.sh`, which names exactly two ADRs (ADR-017 at +line 343, ADR-015 at line 395). A handful of tests cover ADR ground without +saying so, and `tests/test_api_inventory_parity.py` skips itself entirely +because `docs/api-inventory.json` is not tracked here. + +**Violations recorded:** ADR-007 (CHANGELOG untouched across 20 commits), +ADR-008 (endpoints migration 0023 does DDL on core `assets`), ADR-016 +(misdescribed in four files, and the per-PC programme has no design record), +ADR-018 (accepted, unimplemented, no status note), ADR-001/002 (raw SQL over +core tables in the computers CLI), ADR-015 (the scan does not cover `agent/`), +ADR-002 (two stale manifest floors). + --- ## Pages to Update @@ -189,7 +256,7 @@ Returns settings routes contributed by all enabled plugins via `get_settings_rou ShopDB deploys across three air-gapped environments: ``` -Dev PC (gitea) <--USB--> Work PC (GitHub) <--USB--> Production VM +Dev PC (the internal git server) <--USB--> Work PC (GitHub) <--USB--> Production VM ``` ### Bundle Creation @@ -255,18 +322,28 @@ USE_GIT_LS_FILES=0 bash scripts/check-naming-and-style.sh Gate tests validate generated documentation and API consistency: - `tests/test_openapi_spec.py` - OpenAPI spec completeness -- `tests/test_public_read_surface.py` - Public endpoint security -- `tests/test_authz.py` - Authorization decorators -- `tests/test_plugin_migrations.py` - Migration chain validation (ADR-008) +- `tests/test_core/test_public_read_surface.py` - public endpoint security +- `tests/test_core/test_authz.py` - authorization decorators +- `tests/test_plugin_migrations.py` - migration chain validation (ADR-008) +- `tests/test_core/test_plugin_settings_hubs.py` - plugin settings hubs (ADR-019) -**Important:** When you add/modify API endpoints, regenerate docs: +(Paths corrected 2026-09-18: the first draft put three of these at the top of +`tests/` rather than in `tests/test_core/`.) + +**Important:** When you add or modify API endpoints, regenerate docs: ```bash # Update docs/openapi.json to reflect new endpoints -# (Manual until docs/api-inventory.json exists) git add docs/openapi.json ``` +**Caveat for the wj branch (2026-09-18):** the parity gate cannot run here. +`tests/test_api_inventory_parity.py` skips itself when `docs/api-inventory.json` +is absent, and that file is not tracked on gh-wj, so `scripts/gen_openapi.py` +has no input either. `docs/openapi.json` on this branch is 377 paths at +`info.version` 0.12.0 and documents none of the 24 routes added since +2ac2e6d33. Regenerating it needs the inventory file from the other lineage. + **Enforcement:** `scripts/check-gates-ran.sh` verifies gates actually ran (not skipped) **CI failure:** "Gate tests FAILED" means docs are stale or missing @@ -286,9 +363,9 @@ git add docs/openapi.json ```bash # These MUST be in .gitignore -.claude/settings.json -.claude/settings.local.json -.claude/mcp-servers.json +the assistant settings file +the assistant local-settings file +the assistant tool-config file ``` **Why:** Personal config files may contain: @@ -297,7 +374,7 @@ git add docs/openapi.json - API keys and collector tokens - Personal authentication tokens -**Incident:** 2026-09-11 - Passwords exposed in `.claude/mcp-servers.json` committed to GitHub +**Incident:** 2026-09-11 - Passwords exposed in the assistant tool-config file committed to GitHub - Fixed: Removed from git tracking, added to .gitignore - Impact: Dev environment credentials only (not production) @@ -313,14 +390,56 @@ Pre-commit hook scans for these patterns: **If blocked:** Replace hardcoded credentials with environment variables or config files -## ADR-016: Credential Delivery +## How fleet credentials actually work (corrected 2026-09-18) -Production credentials delivered via: -- Device tokens (for endpoints/agents) -- Personal Access Tokens (for users) -- Environment variables (for services) +The paragraph that stood here attributed a decision to ADR-016 that ADR-016 +never took. It is replaced with what the code does. Do not reinstate it. -**Never:** Hardcoded in source, registry keys, git history +**What was wrong.** It said ADR-016 delivers credentials by "device tokens" and +that credentials must **never** live in registry keys. ADR-016 says neither. +Grepping every ADR for device tokens returns nothing. ADR-016's Decision is a +fleet credential store with Fernet at rest, a targeting and precedence resolver, +and `GET /api/collector/credentials` behind a dedicated `credentials.fetch` +scope - and its "Alternatives rejected" section calls registry-only provisioning +"a worthwhile FIRST STEP" and "Recommended as tier one regardless". The ban also +contradicted shipped code: `plugins/computers/client/Report-AssetToShopDB.ps1:367-374` +reads the collector key from `HKLM:\SOFTWARE\GE\ShopDB`, and +`plugins/endpoints/client/Set-ShopDBServerPins.ps1:4-20` writes the server trust +pins to the same key by design. Publishing the old text would have told the next +person to delete values the fleet depends on. + +**What is actually built, as of gh-wj 35d4fcd41:** + +- **Per-PC endpoint credentials.** An enrolled PC authenticates with + `X-Endpoint-Credential` (`plugins/endpoints/api/agentauth.py`). The server + stores only a sha256 hash plus a 12-character display prefix + (`plugins/endpoints/models/credential.py`); the secret exists once, in the + 201 response to `/enroll`. On the endpoint it is a DPAPI LocalMachine blob + under `%ProgramData%\ShopDB\EndpointAgent\credential`, in a directory + ACL'd to SYSTEM and Administrators only. +- **Bootstrap tokens** for the one-line install: hashed at rest, expiry + mandatory and bounded to 1-168 hours, uses bounded to 1-500, revocable. The + package they serve never carries the collector key. +- **The fleet shared key** is still live authority for any PC that is not yet + enrolled, and turning it off is a fleet-wide switch, not a per-PC one. The + rollout sequence is `plugins/endpoints/docs/PER-PC-CREDENTIALS-ROLLOUT.md`. +- **ADR-016's own mechanism is NOT built.** `credentials.fetch`, + `collector/credentials` and `Fernet` return zero hits across `shopdb/` and + `plugins/`, matching ADR-016's own "Implementation status, 2026-08-14". + +**Two things that are genuinely never acceptable**, and this part of the +original text was right: a credential hardcoded in source, and a credential in +git history. + +**Registry storage is not itself the problem; an unprotected key is.** +`install.ps1` writes the shared collector key to `HKLM\SOFTWARE\GE\ShopDB` +with no ACL (`plugins/endpoints/api/siteagent.py:328`, helper at `:394-411`), so +it inherits read for BUILTIN\Users and any logged-on user can read it. The repo +states that consequence itself at +`.github/GE-ENFORCE-COLLECTOR-FIX.html:377-380`. The rollout plans to clear that +value at cutover. + +**See also:** ADR-Conformance-2026-09-18.md, violation 3. ``` --- @@ -332,7 +451,7 @@ Production credentials delivered via: ```markdown # Automation Skills -Claude Code skills created Q3 2026 for common workflows. Save 147-205 hrs/year from 936 hrs baseline. +Assistant automation skills created Q3 2026 for common workflows. Save 147-205 hrs/year from 936 hrs baseline. ## Available Skills @@ -414,11 +533,15 @@ Note: Realistic savings are 147-205 hrs/year due to task overlap. ## Summary -**4 new wiki pages drafted:** -1. ADR-019: Plugin Dynamic Settings Routes (architectural decision) -2. Security Audit 2026-09-11 (4 critical + 6 high findings) -3. Maintenance Footprint Baseline (936 hrs/year burden, automation opportunities) -4. Architectural Findings 2026-09-11 (22 findings from Gortex analysis) +**5 new wiki pages drafted:** +1. ADR-019: Plugin Dynamic Settings Routes (architectural decision) - corrected 2026-09-18 +2. Security Audit 2026-09-11 (4 critical + 6 high findings) - refreshed 2026-09-18 + against the same branch; every 2026-09-11 finding now carries a verdict with a + file and line, and findings 19 and up were added on 2026-09-18 +3. Maintenance Footprint Baseline (936 hrs/year burden, automation opportunities) - annotated 2026-09-18 +4. Architectural Findings 2026-09-11 - rewritten 2026-09-18 with a verdict per finding +5. ADR Conformance 2026-09-18 (new) - which ADRs hold, which are violated, and the fact + that the ADR set is not in this repository **7 existing pages need updates:** 1. PLUGIN-DEVELOPMENT.md (add get_settings_routes() docs) @@ -426,26 +549,44 @@ Note: Realistic savings are 147-205 hrs/year due to task overlap. 3. DEPLOYMENT.md (add air-gap workflow) 4. CONTRIBUTING.md (add optimized pre-commit checks) 5. TESTING.md (add gate tests explanation) -6. SECURITY.md (create new page for credential security) +6. SECURITY.md (create new page for credential security) - its ADR-016 section was + replaced on 2026-09-18; publish the replacement, not what was there 7. Automation-Skills.md (create new page for skill catalog) -**Total effort to publish:** ~2 hours (copy-paste to wiki, adjust formatting, add navigation links) +**Total effort to publish:** the "~2 hours, copy-paste" estimate no longer +holds. Three of the five pages needed correcting against the code before they +were fit to publish, and the Security Audit page has not been through that pass +yet. --- ## Next Steps -1. Review drafts for accuracy (have I missed anything?) +1. Read the 2026-09-18 notes inside each page before pasting anything. All five + pages in this folder have now been checked against gh-wj at 35d4fcd41. 2. Copy to GitHub wiki at https://github.com/ge-aero/shopdb-flask/wiki -3. Update wiki navigation/sidebar to include new pages -4. Link related pages (ADRs reference each other, operations pages link to security audit, etc.) -5. Notify team of new documentation +3. Update wiki navigation/sidebar to include the new pages +4. Link related pages. In particular, `Security-Audit-2026-09-11.md` finding 31 + and `ADR-Conformance-2026-09-18.md` violation 3 are the same ADR-016 + misattribution seen from two angles, and the SECURITY.md draft above is the + copy that was fixed. +5. Notify the team + +**Three corrections that live outside this folder** and were therefore NOT made: + +- `CLAUDE.md:3` and `:9` say PostgreSQL 15+; `docker-compose.yml:20` runs + `mysql:8.4` and `.env.example:29` is a `mysql+pymysql://` URL. +- `CLAUDE.md:31` says columns are snake_case; `CONTRIBUTING.md:17` says + lowercase concatenated with no underscores, and that is what + `scripts/check-naming-and-style.sh:236` enforces. +- `CLAUDE.md:119` and the assistant quick-reference file, lines 14 and 135 repeat the + ADR-016 device-token misattribution. --- ## Related Memory Files -These wiki pages are based on memory files in `.claude/memory/`: +These wiki pages are based on memory files in the assistant memory directory: - `security_audit_2026_09_11.md` - `maintenance_footprint_baseline.md` - `architectural_analysis_gortex_2026_09_11.md` diff --git a/docs/wiki-drafts/Security-Audit-2026-09-11.md b/docs/wiki-drafts/Security-Audit-2026-09-11.md index 87c8413c..983d40b1 100644 --- a/docs/wiki-drafts/Security-Audit-2026-09-11.md +++ b/docs/wiki-drafts/Security-Audit-2026-09-11.md @@ -1,393 +1,406 @@ -# Security Audit - September 11, 2026 +# Security Audit - September 11, 2026, refreshed September 18, 2026 -**Scope:** Endpoints plugin, C# agent, system tray application -**Severity:** 4 critical, 6 high, 8 medium findings -**Status:** 3 critical fixed, 1 critical planned, 6 high in progress +**Scope:** Endpoints plugin, C# agent, system tray application, and (new on 2026-09-18) the USB and printed-parts kiosks +**Branch reviewed:** `gh-wj` at `35d4fcd41` (2026-09-16). Range re-read for this refresh: `2ac2e6d33..gh-wj`, 38 commits, 2026-09-11 to 2026-09-16 +**Severity, 2026-09-11 set:** 4 critical, 6 high, 8 medium +**Severity, added 2026-09-18:** 1 high, 4 medium, 9 low +**Status:** see the remediation summary. Every 2026-09-11 finding below now carries a verdict read off the code on this branch, with a file and line. + +This page covers two dates. The findings numbered 1 to 18 were raised on 2026-09-11 and their status lines were rewritten on 2026-09-18. The findings numbered 19 and up were raised on 2026-09-18 and did not exist on the earlier page. + +## What changed since 2026-09-11 + +Two programmes landed in this range and both move the security boundary. + +**Per-PC agent credentials.** The agent no longer has to authenticate with one fleet-wide shared key. Each PC can hold its own secret: `plugins/endpoints/models/credential.py:36` stores only a sha256 hash and a 12-character display prefix, `plugins/endpoints/api/agentauth.py` makes `X-Endpoint-Credential` the agent auth header, and `plugins/endpoints/api/enrollment.py:289` adds an unauthenticated `/enroll` that a PC uses to ask for one. On the endpoint the secret is a DPAPI LocalMachine blob written through a SYSTEM and Administrators only DACL (`agent/ShopDbEndpointAgent.Core/AgentCredentials.cs:203`, `agent/ShopDbEndpointAgent.Core/ProtectedFileSystem.cs:219`). Once a PC holds an active credential the server refuses the shared key for it (`plugins/endpoints/api/agentauth.py:129`). The operator sequence for the cutover is `plugins/endpoints/docs/PER-PC-CREDENTIALS-ROLLOUT.md`. + +**Kiosk network admission.** Commit `da91a2915` added a second way for an unauthenticated kiosk caller to be admitted: a source address inside a configured CIDR list, alongside the existing per-device token. Both the USB kiosk (`plugins/usb/api/routes.py:798`) and the printed-parts kiosk (`plugins/printedparts/api/routes.py:551`) call `request_ip_allowlisted`, backed by the new `shopdb/utils/ipallowlist.py`. Both lists default to empty, which matches nothing (`shopdb/utils/ipallowlist.py:44`), and neither can override the kiosk's own enabled flag. This widens an unauthenticated write surface from a handful of provisioned screens to every PC on the named networks, so the correctness of the client IP the server sees now matters more than it did. + +Also in range: the agent's server certificate pinning was completed, the tray stopped holding secrets, a one-line bootstrap install was added, and the 2026-09-12 audit reports were deliberately removed from the product tree (`8eee6f19d`) because they described weaknesses in detail on the branch that deploys to production. This page keeps that spirit: findings are located precisely enough to fix, and no exploit recipe is written out. ## Executive Summary -Comprehensive security review of the endpoint management subsystem (plugin + C# agent + tray app) identified critical authentication gaps, credential exposure risks, and performance bottlenecks that expose the system to unauthorized access and operational failure. +The headline of the 2026-09-11 page, unauthenticated inventory writes, is closed on the ingest routes that actually exist on this branch: `POST /api/collector/` requires either a per-PC credential or a collector key, and neither branch of `shopdb/core/api/collector.py:251` reaches the plugin without one. -**Most critical:** Unauthenticated inventory updates allowed any network client to modify asset data. **Fixed** in commit `XXXXX` by enforcing device token authentication on all collector endpoints. +The 2026-09-11 page's second headline, the collector key sitting unprotected in the registry, was still open when this review ran and was **fixed the same day**: the generated installer now restricts `HKLM\SOFTWARE\GE\ShopDB` to SYSTEM and Administrators in both views (finding 19). The key itself stays on purpose until every consumer is on a per-PC credential, and bays installed before this change keep the old DACL until they are reinstalled or upgraded. The per-PC credential work in this range is the exit from the shared key entirely, but only for PCs that have enrolled. -**Highest impact:** Certificate validation disabled in agent allows man-in-the-middle attacks on collector traffic. **Planned** for Q4 2026 with per-endpoint certificate authentication. +The third headline, no certificate validation in the agent, is **fixed** and was fixed properly, not worked around. ## Critical Findings (P0) -### 1. Unauthenticated Inventory Updates [FIXED] +### 1. Unauthenticated Inventory Updates [CLOSED, ON THE ROUTES THAT EXIST] -**Risk:** Any client on the network could POST to `/api/endpoints/inventory` and modify asset records without authentication. +**2026-09-18 verdict:** closed, but not by the mechanism the 2026-09-11 page described, and the evidence it published cannot be reproduced. -**Impact:** -- Attacker could inject false inventory data -- Legitimate collector reports could be overwritten -- Audit trail shows "unknown" as modifier -- Data integrity compromised +None of the four paths named in the original fix (`/api/endpoints/inventory`, `/api/endpoints/heartbeat`, `/api/endpoints/{id}/apps`, `/api/endpoints/{id}/snapshots`) exists on this branch. Endpoint ingest is the ADR-006 generic collector: `@collector_bp.route('/', methods=['POST'])` at `shopdb/core/api/collector.py:229`. That route admits a caller in exactly two ways and neither is anonymous: -**Root cause:** Missing `@requires_device_token` decorator on inventory endpoints. +- a per-PC secret in `X-Endpoint-Credential`, resolved to an asset id or rejected 401 `CREDENTIAL_INVALID` (`shopdb/core/api/collector.py:255-264`) +- otherwise `_check_collector_auth`, which takes a per-plugin or shared collector key, or a `collector.ingest` scoped managed token, from the header only (`shopdb/core/api/collector.py:154`) -**Fix:** Commit `XXXXX` added device token authentication to: -- `/api/endpoints/inventory` (POST) -- `/api/endpoints/heartbeat` (POST) -- `/api/endpoints/{id}/apps` (POST) -- `/api/endpoints/{id}/snapshots` (POST) +A credential caller also cannot write for another PC: the three schema-declaring plugins raise `PermissionError` on an asset mismatch and the dispatcher maps it to 403 `ASSET_MISMATCH` (`shopdb/core/api/collector.py:294`, `plugins/computers/plugin.py:729`, `plugins/printers/plugin.py:556`, `plugins/endpoints/plugin.py:1422`). -**Verification:** -```bash -# Before: Succeeds without auth -curl -X POST http://localhost:5001/api/endpoints/inventory -d '{...}' +**What is still open:** four legacy collector routes take the shared key alone and are not covered by the new refusal. See finding 20. -# After: Returns 401 Unauthorized -curl -X POST http://localhost:5001/api/endpoints/inventory -d '{...}' -# {"status":"error","data":{"error":{"code":"UNAUTHORIZED","message":"Device token required"}}} +**Residual risk, restated:** the original text said device tokens sit in agent config files protected by Windows ACLs. On this branch the per-PC credential is not in a config file at all; it is a DPAPI LocalMachine blob in `%ProgramData%\ShopDB\EndpointAgent\credential` written through a protected DACL (`agent/ShopDbEndpointAgent.Core/AgentCredentials.cs:207-215`). The fleet shared key is the one that is still readable, and that is finding 19. -# With token: Succeeds -curl -X POST http://localhost:5001/api/endpoints/inventory \ - -H "X-API-Key: shopdb_device_abc123..." -d '{...}' -``` +### 2. Deployment Credential Exposure in Registry [STILL OPEN] -**Residual risk:** Device tokens stored in agent config files on endpoints. An attacker with file access can read and reuse the token. Mitigation: Windows ACLs restrict config file access to LOCAL_SYSTEM and Administrators. +**2026-09-18 verdict:** still open, and the described fix did not ship. Promoted to the highest open severity and restated in full as finding 19. -### 2. Deployment Credential Exposure in Registry [MITIGATED] +The 2026-09-11 page said the deployment script would store the token in an ACL'd config file, remove the registry key, and have the agent read the file rather than the registry. On this branch: -**Risk:** Deployment scripts wrote collector API key to HKLM registry in plaintext, readable by any user via `reg query`. +- the generated installer still writes the key to the registry with no ACL: `plugins/endpoints/api/siteagent.py:328`, through a helper that does only `CreateSubKey` plus `SetValue` in both views (`plugins/endpoints/api/siteagent.py:394-411`). There is no `SetAccessControl` or `RegistrySecurity` anywhere in that file. +- the collection script still reads the key back out of the registry: `plugins/computers/client/Report-AssetToShopDB.ps1:367-375`, both the native and the WOW6432Node view. +- the registry path in the original write-up (`HKLM\SOFTWARE\ShopDB\Agent`, value `CollectorApiKey`) never matched this product. The real location is `HKLM\SOFTWARE\GE\ShopDB`, value `CollectorKey`. -**Impact:** -- Any logged-in user (including non-admin) could read the API key -- Key valid for all collector operations (inventory, heartbeat, app updates) -- No expiration or rotation mechanism +**What did change, and it matters:** there is now a keyless build. When the package is built without the shared key the installer emits no `-CollectorKey` parameter and no key write at all (`plugins/endpoints/api/siteagent.py:331-334`), and the PC enrolls for its own credential instead. The rollout runbook plans to clear `CollectorKey` from both registry views at cutover (`plugins/endpoints/docs/PER-PC-CREDENTIALS-ROLLOUT.md:351-361`). So the exposure ends when the fleet finishes enrolling, not before. -**Evidence:** -```powershell -PS> reg query "HKLM\SOFTWARE\ShopDB\Agent" /v CollectorApiKey -CollectorApiKey REG_SZ shopdb_pat_b4a3fe2a01abc6bc1cb685280e65a8c35d52ec43 -``` +**Note on the registry generally:** the same key is also where the agent's server trust pins live, by design (`plugins/endpoints/client/Set-ShopDBServerPins.ps1:4-20`, precedence stated at `plugins/endpoints/docs/PER-PC-CREDENTIALS-ROLLOUT.md:106-111`). Pins are trust anchors, not secrets. Do not read "get the key out of the registry" as "the registry must hold nothing". -**Fix:** Deployment script updated to: -1. Store token in config file with restrictive ACL (LOCAL_SYSTEM + Administrators only) -2. Remove registry key after migration -3. Agent reads from config file, not registry +### 3. No Certificate Validation in Agent [FIXED] -**Status:** Mitigated on new deployments. Legacy endpoints still have registry key. Remediation plan: deployment script detects and removes registry key on upgrade. +**2026-09-18 verdict:** fixed, and fixed by pinning rather than by re-enabling default validation and hoping. -**Residual risk:** Existing endpoints need redeployment to remove registry key. Scheduled for Q4 2026 mass agent update. +`agent/ShopDbEndpointAgent.Core/AgentHttpFactory.cs` is the only place the agent constructs an HTTP client, and every channel goes through `pins.Validate(cert, errors, channel)` (`AgentHttpFactory.cs:52`). `Validate` (`AgentHttpFactory.cs:291`) has no path that returns true for an unmatched pin: unpinned falls back to `SslPolicyErrors.None` (`AgentHttpFactory.cs:302`), pinned requires an SPKI sha256 or legacy thumbprint match (`AgentHttpFactory.cs:314`). A failed registry re-read returns null and keeps the existing pins rather than silently unpinning. -### 3. No Certificate Validation in Agent [PLANNED] +The three SYSTEM PowerShell reporters refuse to send at all when pins are configured but the check cannot be enabled, including under pwsh (`plugins/computers/client/Report-AssetToShopDB.ps1:228-247`, and the same block in `plugins/printers/client/Report-PrintersToShopDB.ps1:125` and `plugins/endpoints/api/templates/upgrade-shopdbagent.ps1:110`). -**Risk:** Agent's HTTP client disables SSL certificate validation: +The blanket accept-any-certificate callback the original finding quoted survives in exactly one place, a developer connectivity probe that is not shipped to endpoints: `test-vm-connectivity.ps1:44`. -```csharp -ServicePointManager.ServerCertificateValidationCallback = - (sender, cert, chain, sslPolicyErrors) => true; // ACCEPTS ANY CERT -``` +### 4. Hardcoded Admin Credentials [PARTLY FIXED] -**Impact:** -- Man-in-the-middle attack possible on collector traffic -- Attacker can intercept inventory data (hostnames, IPs, installed apps, usernames) -- Attacker can inject false commands or updates +**2026-09-18 verdict:** partly fixed. The production path is blocked; the credential is still in the source tree. -**Why it exists:** Corporate proxy with self-signed certificate breaks standard validation. Quick fix was to disable validation entirely. +`flask seed test-user` still creates `admin` / `admin123` (`shopdb/cli/__init__.py:636`), and the string appears four times in that file. What changed is that the command now refuses to run outside DEBUG or TESTING and says why (`shopdb/cli/__init__.py:619-623`), pointing the operator at `flask seed admin`, which generates a password. -**Proper fix (PLANNED):** Per-endpoint certificate pinning -1. Deployment script embeds server's certificate thumbprint in agent config -2. Agent validates that presented cert matches pinned thumbprint -3. Pinned cert stored in protected config file (LOCAL_SYSTEM ACL) -4. Rotation: new deployment updates pinned thumbprint +The file the original finding quoted, `tests/fixtures/users.py`, does not exist on this branch, so the described fix to it cannot be confirmed. -**Implementation plan:** ADR-XXX Per-Endpoint Certificate Authentication (see Security-Fix-Implementation.md) - -**Timeline:** Q4 2026 (requires mass agent redeployment) - -**Temporary mitigation:** Collector API is internal only (not exposed to internet). Attacker must be on corporate network with proxy access. Still unacceptable for long-term. - -### 4. Hardcoded Admin Credentials [REMOVED] - -**Risk:** Test fixtures and seed scripts contained hardcoded passwords: - -```python -# tests/fixtures/users.py -admin_user = User( - username='admin', - password='admin123', # HARDCODED - role='admin' -) -``` - -**Impact:** -- Default credentials usable if seed script runs in production -- Password visible in source code and git history -- No password complexity or rotation - -**Fix:** Commit `XXXXX` removed hardcoded passwords: -1. Test fixtures use `os.environ.get('TEST_PASSWORD', generate_random())` -2. Seed scripts prompt for admin password on first run -3. Password stored hashed (bcrypt) in database, not in code -4. Deployment docs updated to mandate password change on first login - -**Verification:** Grep for hardcoded passwords returns zero hits: -```bash -grep -r "password.*=.*['\"][^'\"]\{8,\}" --include="*.py" shopdb/ plugins/ | wc -l -# 0 -``` - -**Residual risk:** Git history still contains hardcoded passwords. Requires git-filter-repo to fully purge. Scheduled for next major release (v1.0). +**Residual risk:** unchanged. A dev-only well-known credential still ships in the tree, and git history still holds the older forms. ## High Priority Findings (P1) -### 5. Collector Key Exposure in Committed Files +### 5. Collector Key Exposure in Committed Files [FIXED, ONE CLAIM CORRECTED] -**Risk:** Collector API key (`shopdb_pat_...`) committed to `.claude/skills/*.md` files and pushed to GitHub. +**2026-09-18 verdict:** the redaction holds. `git ls-files | xargs grep -l "shopdb_pat_"` returns nothing on this branch, and the assistant settings file, the assistant local-settings file and the assistant tool-config file are ignored (`.gitignore:174-176`). -**Impact:** -- Key visible in public/private GitHub repository -- Valid until manually rotated -- Grants full collector access (inventory, heartbeat, app updates) +**Correction:** the original page's prevention line is not true of this branch. The committed pre-commit hook runs the naming and style gate only (`.githooks/pre-commit:5`), and `scripts/check-naming-and-style.sh` has no credential pattern check at all. Its eleven checks are non-ASCII, banned shorthand, three snake_case rules, frontend API params, plugin frontend imports, the shared code-library module, ADR-017 map levels and ADR-015 site literals (`scripts/check-naming-and-style.sh:151-395`). Nothing blocks a commit that reintroduces a key. -**Evidence:** -```bash -git log -S "shopdb_pat_b4a3fe2a" --oneline -ce27cb5eef Add 9 comprehensive automation skills -``` +### 6. Password Exposure in MCP Config [FIXED AS FAR AS THIS BRANCH GOES] -**Fix:** Commit `1e8c6a513a` redacted key and added to .gitignore: -1. Replaced key in files with placeholder `shopdb_pat_YOUR_COLLECTOR_KEY_HERE` -2. Added `.claude/settings*.json` and `.claude/mcp-servers.json` to .gitignore -3. Removed files from git tracking (`git rm --cached`) +**2026-09-18 verdict:** the assistant tool-config file is ignored (`.gitignore:176`) and is not in `git ls-files`. The accepted risk recorded in 2026-09-11, that the exposed dev passwords were not rotated, is unchanged and still accepted. -**Key rotation:** User indicated passwords/keys do NOT need rotation despite exposure ("won't need to"). Accepted risk: exposed key remains valid. +### 7. Missing Rate Limiting on Login [FIXED] -**Prevention:** Pre-commit hook scans for `shopdb_pat_` pattern and blocks commit if found. +**2026-09-18 verdict:** fixed, in two layers. -### 6. Password Exposure in MCP Config +- Per-IP fixed-window limiter: `_login_ratelimited` at `shopdb/core/api/auth.py:35`, applied at `shopdb/core/api/auth.py:80`, defaults 30 hits per 300 seconds. +- Per-account lockout: after `MAX_FAILED_LOGINS` consecutive bad passwords the account is locked (`shopdb/core/api/auth.py:113-118`), and a locked account is rejected before the password is checked so a valid password cannot reset the clock mid-lockout (`shopdb/core/api/auth.py:100`). -**Risk:** `.claude/mcp-servers.json` contained plaintext MySQL password and VM SSH passwords, committed to GitHub. +**Known limit, stated in the code:** under the default SimpleCache the limiter's counter is per-process, so with N gunicorn workers the effective budget is N times the configured maximum (`shopdb/core/api/auth.py:39-42`). The per-account lockout is the control that does not have that weakness. Both are keyed on the client address, which is why the client-IP plumbing in finding 23 matters. -**Impact:** -- MySQL dev database password: `shopdb-dev-password` -- VM SSH password for SupportUser: `vos313` -- VM SSH password for shopfloor account: `vos313` -- Passwords visible in git history +### 8. SQL Injection in Asset Search [STILL A FALSE POSITIVE] -**Fix:** Commit `1e8c6a513a` removed from git tracking and added to .gitignore. +**2026-09-18 verdict:** re-checked, still no injection. -**Key rotation:** User indicated "passwords rotated" then corrected "my bad, not rotated yet. but won't need to". Accepted risk: exposed passwords remain valid. +The raw SQL that exists is parameterised. The USB external-database path builds a WHERE clause out of fixed fragments and passes every value as a parameter (`plugins/usb/api/routes.py:277-299`); the only interpolated text is the fragment list itself. `plugins/employees/api/routes.py:113` binds through `%s`. -**Scope:** Dev environment only. Production uses different credentials not stored in repo. +**One related note that is not injection:** `plugins/computers/plugin.py:2080` runs a raw `db.text` query joining the core `assets`, `assetrelationships` and `assettypes` tables inside the `flask computers retype` CLI. No user input reaches it, so it is not a security finding. It is hidden coupling to the core schema, which is an architecture item, not one for this page. -### 7. Missing Rate Limiting on Login +### 9. Session Token Storage in LocalStorage [UNCHANGED] -**Risk:** No rate limiting or account lockout on failed login attempts. +**2026-09-18 verdict:** unchanged. The JWT and the refresh token are still written to and read from localStorage (`frontend/src/stores/auth.js:6-7`, `frontend/src/stores/auth.js:43-44`). No HttpOnly cookie path was added in this range. -**Impact:** -- Brute force attack on login endpoint -- No delay or throttling after failed attempts -- Audit log fills with failed attempts but no automated response +Partial compensating control added since: the app now sets `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy` and a `frame-ancestors 'self'` CSP on every response (`shopdb/__init__.py:334-341`). That is not a script-src policy and the code says so deliberately. -**Evidence:** -```bash -# Can attempt 1000 logins/second with no throttling -for i in {1..1000}; do - curl -X POST http://localhost:5001/api/auth/login \ - -d '{"username":"admin","password":"wrong"}' & -done -``` +### 10. Audit Log Unbounded Growth [MECHANISM BUILT, OFF BY DEFAULT] -**Mitigation options:** -1. Flask-Limiter: `@limiter.limit("5 per minute")` on login endpoint -2. Account lockout: Lock account after 5 failed attempts for 15 minutes -3. CAPTCHA after 3 failed attempts +**2026-09-18 verdict:** the tooling now exists; it does nothing unless an operator turns it on and runs it. -**Status:** Planned for Q1 2027. Not blocking for air-gapped production (internal network only). +- Service: `shopdb/core/services/auditretention.py` +- Admin API: `GET /api/auditlogs/retention` (`shopdb/core/api/auditlogs.py:195`) and the purge route at `shopdb/core/api/auditlogs.py:211` +- CLI: `shopdb/cli/__init__.py:1894` and `shopdb/cli/__init__.py:1918` -### 8. SQL Injection in Asset Search (False Positive) - -**Initial concern:** Asset search endpoint uses user input in query. - -**Investigation:** All queries use SQLAlchemy ORM or parameterized queries. String concatenation not found. - -**Verification:** -```bash -grep -rn "execute.*+\|execute.*format\|execute.*%" \ - --include="*.py" shopdb/ plugins/ | wc -l -# 0 -``` - -**Conclusion:** No SQL injection vulnerabilities found. Marked false positive. - -### 9. Session Token Storage in LocalStorage - -**Risk:** JWT tokens stored in browser localStorage, vulnerable to XSS. - -**Impact:** -- XSS attack can read token from localStorage -- Token persists across browser restarts (no automatic expiry on client) -- Token visible in browser DevTools - -**Best practice:** HttpOnly cookies prevent XSS access to tokens. - -**Mitigation:** Current implementation requires localStorage for kiosk/TV displays that can't handle cookie flow. Acceptable for internal network deployment. - -**Enhancement:** Add `httpOnly` cookie option for standard user sessions while keeping localStorage for kiosk mode. - -**Status:** Planned for v1.0 (requires auth refactor). - -### 10. Audit Log Unbounded Growth - -**Risk:** Audit log table (`auditlog`) has no retention policy. Grows unbounded. - -**Impact:** -- Database disk space exhausted over time -- Query performance degrades as table grows -- Backup size increases indefinitely - -**Current state:** -- 2.3M rows after 18 months (WJ site) -- Growing at ~4200 rows/day -- 89MB table size, 156MB with indexes - -**Recommended:** Retention policy of 90 days for non-critical actions, 2 years for admin/modification actions. - -**Status:** Planned for Q4 2026. See MAINTENANCE-DEPLOYMENT-CHECKLIST.md for implementation. +`configureddays()` returns 0 when `audit_retentiondays` is unset, and 0 means keep forever (`shopdb/core/services/auditretention.py:123-133`). There is no scheduler in that module, so nothing purges on its own. The row and size figures quoted in 2026-09-11 come from the reference site database and cannot be confirmed from the repository. ## Medium Priority Findings (P2) -### 11. Endpoint Event Accumulation +### 11. Endpoint Event Accumulation [STILL OPEN] -**Risk:** `endpointevents` table accumulates every app launch, process change, USB connect. No retention. +**2026-09-18 verdict:** still open. `EndpointEvent` is `endpointevents` (`plugins/endpoints/models/endpoint.py:333`) and there is no retention, purge or prune code anywhere under `plugins/endpoints/`. The audit-log retention service added in this range covers `auditlog` only. The row and size figures quoted in 2026-09-11 are site measurements and are not verifiable here. -**Impact:** -- 450K rows after 18 months -- 127MB table size -- High-churn data (process events every 30 seconds) +### 12. Missing HTTPS Enforcement in Dev [CHANGED; THE STATED MITIGATION IS GONE] -**Recommended:** Retention of 30 days for events, 90 days for snapshots. +**2026-09-18 verdict:** the specific mitigation the page described no longer exists, and that is an improvement. `NODE_TLS_REJECT_UNAUTHORIZED` appears nowhere in the tree. The Vite dev proxy target is now an environment variable defaulting to `http://localhost:5001`, and TLS verification for an https target is off unless `VITE_DEV_SECURE=true` (`frontend/vite.config.js:5-18`). Dev only; production still terminates TLS in front of the app. -### 12. Missing HTTPS Enforcement in Dev +### 13. Docker Log Rotation Not Configured [STILL OPEN] -**Risk:** Vite dev server runs HTTP, not HTTPS. Cookies sent without Secure flag. +**2026-09-18 verdict:** still open. There is no `logging:` block anywhere in `docker-compose.yml`. Both hops log to stdout (`nginx.conf:36`, `docker-compose.yml:213-214`), so the host's Docker log driver defaults apply unchanged. This now has a second consequence: see finding 25. -**Impact:** Dev environment only. Production uses HTTPS via gunicorn + nginx. +### 14-18. Additional Medium Findings [PARTLY ADDRESSED, SOURCE NOW MISSING] -**Mitigation:** `start-dev.cmd` sets `NODE_TLS_REJECT_UNAUTHORIZED=0` for Vite proxy. +**2026-09-18 verdict:** the file this section pointed at is gone. the assistant memory directory does not exist on this branch; commit `8eee6f19d` moved the 2026-09-12 audit reports and scan output out of the product tree on purpose. The detail behind findings 14 to 18 is therefore not readable from here. -### 13. Docker Log Rotation Not Configured +Of the five items summarised in the original bullet list, two can be answered from the code: -**Risk:** Docker container logs grow unbounded. Can fill host disk. +- **No content security policy headers:** partly addressed. `shopdb/__init__.py:334-341` sets a security header block including `Content-Security-Policy: frame-ancestors 'self'`, and uploads carry a much stricter `default-src 'none'; sandbox` (`shopdb/utils/uploads.py:13`, applied at `shopdb/utils/uploads.py:35`). A real script-src policy is still not set, deliberately. +- **Directory listing enabled in nginx:** no `autoindex` directive appears in `nginx.conf`, and nginx defaults to off. -**Mitigation:** Configure log rotation in docker-compose.yml: -```yaml -logging: - driver: "json-file" - options: - max-size: "10m" - max-file: "3" -``` +The remaining three, input validation gaps, world-readable backup files and old TLS versions, cannot be confirmed or refuted without the removed source document. Treat them as unverified rather than as fixed. -**Status:** Planned for Q4 2026. +## New findings, 2026-09-18 -### 14-18. Additional Medium Findings +### High -See full audit report in `.claude/memory/security_audit_2026_09_11.md` for: -- Missing input validation on some endpoints -- No content security policy headers -- Directory listing enabled in nginx (dev only) -- Backup files world-readable (dev only) -- Old TLS versions enabled (corporate proxy requirement) +### 19. The fleet shared key is written to the registry with no ACL (FIXED 2026-09-18; the key itself is transitional by design) + +**Risk:** for a shared-key build, the generated `install.ps1` writes the collector key to `HKLM\SOFTWARE\GE\ShopDB\CollectorKey` (`plugins/endpoints/api/siteagent.py:328`) through a helper that does only `CreateSubKey` plus `SetValue` in both registry views (`plugins/endpoints/api/siteagent.py:394-411`). No `RegistrySecurity`, no `SetAccessControl`. The key inherits read for BUILTIN\Users. The repository states this outcome itself, in a file added in this same range: `.github/GE-ENFORCE-COLLECTOR-FIX.html:377-380`. + +**Why it is still authority:** `authenticate_agent` accepts `X-API-Key` whenever `shared_key_allowed()` (`plugins/endpoints/api/agentauth.py:184-192`, default true at `plugins/endpoints/api/agentauth.py:67`), and `shared_key_refused_for` refuses it only for a hostname whose asset holds an **active** credential (`plugins/endpoints/api/agentauth.py:129-134`). So for any not-yet-enrolled, revoked or re-enroll-blocked PC the shared key still claims queued commands (`plugins/endpoints/api/routes.py:3352`) and downloads script bodies and installer packages (`plugins/endpoints/api/routes.py:3511`, `:3550`, `:3589`). + +**Impact:** any operator logged on to a bay can read the fleet key and act as any un-enrolled endpoint: claim and answer its queued commands, write false compliance records, and fetch hosted installers and script bodies. It also means revoke plus re-enroll-block leaves a compromised PC holding a working credential of the older kind. + +**Contrast:** the per-PC credential got the opposite treatment in the same range. DPAPI LocalMachine inside a SYSTEM and Administrators only DACL (`agent/ShopDbEndpointAgent.Core/AgentCredentials.cs:203-217`, `agent/ShopDbEndpointAgent.Core/ProtectedFileSystem.cs:219-232`), and `appsettings.json` is re-ACLed at runtime (`agent/ShopDbEndpointAgent/AgentPipeServer.cs:494-522`). The registry key alone was left open. + +**Status, 2026-09-18: fixed.** The generated installer now calls `Protect-HklmShopdbKey` after provisioning, which breaks inheritance, drops the inherited entries rather than copying them, and states SYSTEM (S-1-5-18) and Administrators (S-1-5-32-544) by SID in both registry views (`plugins/endpoints/api/siteagent.py`). It runs for the key-less package too, because `ServerPins` live in the same key and the registry is authoritative for pinning. A bay that cannot take the ACL still installs, and says plainly that the key stays readable there. Four tests cover it in `tests/test_plugins/test_endpoints_siteagent_build.py`, including the write-then-lock ordering. + +**The key's continued existence is a decision, not an oversight.** The shared key stays until `upgrade-shopdbagent.ps1`, `Report-AssetToShopDB.ps1` and `Report-PrintersToShopDB.ps1` are off it, because rollback dies without it; the site is mid-migration to per-bay credentials and the key-less package (`includesharedkey=False`) is already the path for new installs. What this finding was about was the DACL, and that is now closed. The remaining work is the migration itself, and finally clearing `CollectorKey` from both views as the runbook plans (`plugins/endpoints/docs/PER-PC-CREDENTIALS-ROLLOUT.md:351-361`). + +**Not closed by this:** bays that already carry the key keep their inherited DACL until they are reinstalled or upgraded through a package built after this change. + +### Medium + +### 20. Turning the shared key off does not close the four legacy collector routes + +`generic_collect` enforces the new rule: 401 `SHARED_KEY_REFUSED` when `agent_shared_key_refused(identityvalue)` is true (`shopdb/core/api/collector.py:279-284`), which `plugins/endpoints/api/agentauth.py:129-134` makes true fleet-wide once `plugin.endpoints.sharedkeyallowed` is off. + +Four legacy routes never consult it. `/pc` (`shopdb/core/api/collector.py:349`), `/apps` (`:387`), `/heartbeat` (`:459`) and `/bulk` (`:494`) are decorated `@require_api_key`, which checks only `COLLECTOR_API_KEY` or a `collector.ingest` scoped token (`shopdb/core/api/collector.py:154-164`). `/pc` then writes `lastreporteddate`, `loggedinuser`, `lastboottime` and the asset serial number for any hostname it can resolve (`shopdb/core/api/collector.py:367-376`); `/apps` and `/bulk` rewrite installed-app inventory. + +Two details that make this worse than the runbook implies. `_env_fallback_disabled(pluginname)` returns False when `pluginname` is None (`shopdb/core/api/collector.py:60-74`), so these routes keep honouring the `.env` `COLLECTOR_API_KEY` even after `plugin.endpoints.envkeyfallback` is switched off. And "Regenerate key" mints a new `collector.ingest.endpoints` token and clears the env fallback but never changes `COLLECTOR_API_KEY` (`plugins/endpoints/api/routes.py:1864-1884`), so runbook Phase 6 step 4 does not close this either. + +**Bounded by:** the UI-managed agent key is scoped `collector.ingest.endpoints` (`plugins/endpoints/api/routes.py:1834`) and `_collector_scopes_for(None)` allows only the generic `collector.ingest` (`shopdb/core/api/collector.py:46-57`), so a PC holding only that token is already refused on all four routes. The exposure needs a copy of the env shared key, and the reachable damage is inventory-field integrity. No command issuance and no credential minting. + +ADR-006 says the legacy `/pc` endpoint is removed before v1.0. At `__version__` 0.12.0 keeping it is not yet a violation, but it is a hole in a boundary this branch advertises. + +### 21. Rejecting an enrollment request is not durable + +`enroll_endpoint` is unauthenticated by design. When a row for (hostname, serialnumber) already exists, it returns `rejected` only when the presented nonce matches the stored one (`plugins/endpoints/api/enrollment.py:383`). A REJECTED row presented with a **different** nonce falls into the non-pending branch at `plugins/endpoints/api/enrollment.py:390` and is rewritten into a fresh cycle: new nonce, new `requestedat`, `decidedat` and `decidedby` cleared (`:393-396`). Execution then falls through to the match block, and with the fleet shared key present it is auto-issued a credential outright (`plugins/endpoints/api/enrollment.py:426-427`). Both gates default on (`plugins/endpoints/api/agentauth.py:67`, `:71`). Without the key the request simply lands back in the admin approval queue, indistinguishable from a first-time request. + +There is no second lever: `reject_enroll_request` sets no durable flag (`plugins/endpoints/api/enrollment.py:753-770`), and `reenrollblocked` lives on `EndpointCredential` (`plugins/endpoints/models/credential.py:56`), which `revoke_endpoint_credential` 404s on when no credential row exists (`plugins/endpoints/api/enrollment.py:801-805`). So a PC that never enrolled cannot be blocked. + +**Bounded by:** auto-issue additionally requires a Computer record whose hostname matches and whose Asset serial matches case-insensitively (`plugins/endpoints/api/enrollment.py:405-421`), so this does not grant a credential for an arbitrary asset. The existing test covers only the same-nonce case (`tests/test_plugins/test_endpoints_credentials.py:250`). + +### 22. The one-line bootstrap stages its script in a world-writable path and runs it elevated + +`COMMAND_SCRIPT_PATH` is a literal `C:\Windows\Temp\shopdb-bootstrap.ps1` (`plugins/endpoints/api/bootstrap.py:68`), and the emitted command downloads to that path and then executes it, with a `Remove-Item -Force` against the same path in the finally block (`plugins/endpoints/api/bootstrap.py:164-169`). The module docstring and the script header both say the line is pasted into an elevated PowerShell (`plugins/endpoints/api/bootstrap.py:3-5`, `:209-213`). The same literal is published in `plugins/endpoints/AGENT-CONTRACT.md:794` and `plugins/endpoints/docs/PER-PC-CREDENTIALS-ROLLOUT.md:296`. + +`C:\Windows\Temp` is not admin-only: its default DACL grants BUILTIN\Users create-file plus CREATOR OWNER full control on what they create, and overwriting a file's contents does not reset its owner or DACL. Nothing verifies the script after download; the sha256 checks in the script body cover the downloaded ZIP's binaries, not the script itself. + +**Bounded by:** this needs an attacker who can already log on interactively to that specific bay and a tech who later pastes the one-liner on that same PC. The bootstrap path is documented as the one-off route (`plugins/endpoints/api/bootstrap.py:7-11`), not how managed bays get the agent. + +**Fix direction:** stage under a path only Administrators and SYSTEM can write, or verify the script against a server-stamped hash before executing it. + +### 23. Kiosk network admission rests on a header the client can send, and proxy mode verifies nothing + +`request_ip_allowlisted` reads `client_ip()`, which is `strip_port(request.remote_addr)` (`shopdb/utils/clientip.py:53`). On the Windows deployment `remote_addr` is not the TCP peer: `deploy/windows/web.config:31` launches waitress with `--trusted-proxy=127.0.0.1 --trusted-proxy-headers=x-forwarded-for --trusted-proxy-count=1`, and HttpPlatformHandler always connects from loopback, so waitress always parses `X-Forwarded-For` and promotes the selected hop into `REMOTE_ADDR`. + +The control that makes that hop honest is the IIS rewrite rule that overwrites the header with `{REMOTE_ADDR}`, and it ships commented out (`deploy/windows/web.config:90-102`). The installer enables and then verifies it only under `-ClientIpSource direct`: the whole "CLIENT-IP RULE IS NOT IN EFFECT" check sits inside `if ($ClientIpSource -eq 'direct')` at `deploy/windows/installer/shopdb-install.ps1:2509-2523`. In proxy mode the rule is deliberately left off (`:2041-2046`) and **nothing checks that a proxy is actually in front**. The module docstring says the same thing about the failure mode: it does not fail closed (`shopdb/utils/ipallowlist.py:17-26`). + +**Bounded by:** `-ClientIpSource` defaults to `direct` (`deploy/windows/installer/shopdb-install.ps1:129`), which installs URL Rewrite, uncomments the block and verifies it live, so a default install is covered. With a real proxy in proxy mode the attack also fails, because waitress takes the last hop and ARR appends the true peer after any client-supplied value. Both kiosk allowlists default to empty and both kiosks are additionally gated on their enabled flag (`plugins/usb/api/routes.py:798-801`, `plugins/printedparts/api/routes.py:551-557`). + +**The real gap:** proxy mode performs no verification at all. A server set to proxy mode with no proxy really there, or a hand-maintained `web.config` where the rewrite block was never uncommented, admits an attacker-chosen address. The same header also satisfies the GE-Enforce IP path (`plugins/geenforce/api/routes.py:82-108`), which is pre-existing and not introduced by `da91a2915`, and it re-keys the login rate limit and account lockout on a value the caller chose. + +### Low + +### 24. Revoke does not stop a live agent; it falls back to the shared key + +`plugins/endpoints/models/credential.py:7-10` says `reenrollblocked` keeps a revoked PC from enrolling again, listing "a compromised PC" among the uses. The containment does not exist. On a 401 the agent deletes its credential and clears it (`agent/ShopDbEndpointAgent.Core/AgentCredentials.cs:107-135`), then every later request takes the shared-key branch (`agent/ShopDbEndpointAgent.Core/AgentHttpFactory.cs:119-120`). Server side, `shared_key_refused_for` refuses the shared key only when an active credential exists (`plugins/endpoints/api/agentauth.py:129-134`), and revoke has just cleared that (`plugins/endpoints/api/enrollment.py:773-825`). + +This is documented intended behaviour, not an undetected gap: `plugins/endpoints/api/agentauth.py:11-14` states it, and `plugins/endpoints/docs/PER-PC-CREDENTIALS-ROLLOUT.md:379-380` says outright that the server admits the shared key again. What needs fixing is the model comment, which implies a containment the design never provides. There is no per-PC deny until the shared key is off fleet-wide. + +### 25. The bootstrap token travels in the query string and is written to both access logs + +`_resolve_token` takes the whole authorization from the URL (`plugins/endpoints/api/bootstrap.py:182`) for both token routes (`plugins/endpoints/api/bootstrap.py:60-61`). Both hops log the full request line: `nginx.conf:36` is `access_log /dev/stdout combined`, and the gunicorn access format uses `%(r)s` (`docker-compose.yml:213-214`). The IIS deployment logs `cs-uri-query` in its W3C log by default too. + +The same range deliberately stopped logging the setup token for exactly this reason (`shopdb/__init__.py`, setup-token block), and the audit rows this module writes carry only `tokenprefix` (`plugins/endpoints/api/bootstrap.py:192-195`). + +**Bounded by:** what a replayed token buys is bounded and carries no secret. `download_bootstrap_package` is explicitly keyless (`plugins/endpoints/api/bootstrap.py:532-535`) and the script it serves contains only the base URL and pins. The reader also needs container or IIS log access, which is already privileged. + +### 26. The unauthenticated enroll queue can be filled to its cap + +`enroll_endpoint` takes no auth and writes a row for every unseen (hostname, serialnumber) pair (`plugins/endpoints/api/enrollment.py:350-358`). The bounds are a per-IP fixed window of 300 per 60 seconds (`plugins/endpoints/api/enrollment.py:65-66`) and a pending cap of 500 (`plugins/endpoints/api/enrollment.py:255`). Junk rows count toward the cap: an unknown hostname returns `_pending(row, REASON_UNKNOWN_HOSTNAME)` at `plugins/endpoints/api/enrollment.py:411`, which leaves the row PENDING. There is no bulk-reject, prune or stale-row expiry in that module. The limiter's counter is per-process (`plugins/endpoints/api/ratelimit.py:10-11`), so the real budget is the configured maximum times the worker count. + +**Bounded by:** while `sharedkeyallowed` is on (the default), a PC that cannot enroll is not offline; it keeps reporting on the fleet key. This only becomes an outage for new bays once the shared key is turned off at the end of the rollout. + +### 27. The kiosk confirms an employee name for any badge it is handed + +`kiosk_session` normalises the posted badge and returns the resolved employee name in the response body (`plugins/usb/api/selfhosted.py:346-371`, resolver at `:34-57`); the external mode does the same (`plugins/usb/api/routes.py:904-960`). The route carries no auth decorator (`plugins/usb/api/routes.py:905-906`) and there is no rate limiter anywhere in either file. `kiosk_others` additionally returns the holder name for every checked-out drive to the same caller, gated only on `usb_kiosk_allow_return_for_others`, which defaults true (`plugins/usb/plugin.py:169-172`). + +Before `da91a2915` this needed the per-device token, so it was confined to screens an administrator had provisioned. It is now reachable by anything inside `usb_kiosk_allowed_cidrs`. + +**Bounded by:** the kiosk must be enabled and the caller must be inside a deliberately configured CIDR. The data is badge-to-name, which is printed on the badges themselves and visible to any signed-in user, and the surface is read-only. An unrecognised badge answers 422 rather than silently. + +### 28. The kiosk allowlist settings accept a world-open range with no server-side validation + +`usb_kiosk_allowed_cidrs` (`plugins/usb/plugin.py:154-168`) and `printedparts_kiosk_allowed_cidrs` (`plugins/printedparts/plugin.py:176-188`) are plain string settings with no validator, and the settings PUT path applies none. `ip_in_cidrs` parses each entry with `ipaddress.ip_network(part, strict=False)` (`shopdb/utils/ipallowlist.py:55`), so `0.0.0.0/0` and `::/0` parse and match everything. The only shape check is client-side and cosmetic (`frontend/src/components/NetworkAllowlistPicker.vue:84-87`), and `0.0.0.0/0` satisfies its regex. + +The realistic route in is an administrator who cannot get the allowlist to match, most likely because the client-IP rule is missing and every caller reads as loopback, and who widens the range to make it work. + +### 29. The kiosk provisioning link hands out the token in a URL + +`kiosk_provisioning_link` returns `/usb-kiosk?token=` (`plugins/usb/api/routes.py:849`) and the printed-parts twin returns `/parts-kiosk?token=` (`plugins/printedparts/api/routes.py:578`). Both are gated on `settings.edit` and both write an audit row first (`plugins/usb/api/routes.py:846-848`, `plugins/printedparts/api/routes.py:574-576`). The token then lands in the IIS or nginx access log, browser history and any clipboard history on the admin's PC, and nothing rotates it after a reveal. + +The same file elsewhere states the contrasting reasoning: `kiosk_session` is a POST specifically so the badge does not end up in a proxy access log the way a query string would (`plugins/usb/api/routes.py:914-916`). + +**Qualification:** the query-string model is not introduced here. It is the mechanism the token setting itself prescribes (`plugins/usb/plugin.py:146-151`), so the new route only surfaces a URL the product already documented. This is hardening, not a regression. + +### 30. Every deployment warning about the client-IP rule still names only GE-Enforce + +Three operator-facing places enumerate what depends on the `X-Forwarded-For` rewrite rule, and none was updated when the kiosk allowlists were added: `deploy/windows/installer/shopdb-install.ps1:2520-2522`, `deploy/windows/installer/shopdb-preflight.ps1:322-325` and `deploy/windows/web.config:51-58`. None names `usb_kiosk_allowed_cidrs` or `printedparts_kiosk_allowed_cidrs`, which are now the only allowlists in the product gating unauthenticated writes. The dependency is stated only in the settings descriptions (`plugins/usb/plugin.py:161-168`, `plugins/printedparts/plugin.py:183-187`), which the person building the server does not read. + +**One claim not to carry forward:** the remediation line offered at `deploy/windows/installer/shopdb-install.ps1:2522`, "or use token authentication", is correct for the kiosks. The per-device token is a genuine alternative to the allowlist on both. + +### 31. This page, and three other documents, attributed a credential policy to ADR-016 that ADR-016 does not contain + +The References section of the 2026-09-11 version of this page described ADR-016 as "Credential Delivery (device token authentication)". That is wrong, and the same misattribution is repeated at `docs/wiki-drafts/README.md:316-323`, `CLAUDE.md:119` and the assistant quick-reference file line 135. Grepping the ADR set for device tokens returns nothing in any ADR. + +The worst form is in the SECURITY.md page that `docs/wiki-drafts/README.md:429` queues for publication. Under "ADR-016: Credential Delivery" it lists device tokens, personal access tokens and environment variables as the delivery mechanisms and then bans, at `docs/wiki-drafts/README.md:323`, "Hardcoded in source, registry keys, git history". ADR-016 decides none of that. Its Decision is a fleet credential store with Fernet at rest, a targeting and precedence resolver reusing `plugins/geenforce/filters.py`, an explain endpoint and `GET /api/collector/credentials` behind a dedicated `credentials.fetch` scope. Its own alternatives section recommends registry-only provisioning as a worthwhile first step. + +The blanket ban on registry keys also contradicts shipped code on this branch: `plugins/computers/client/Report-AssetToShopDB.ps1:367-374` reads the collector key from `HKLM:\SOFTWARE\GE\ShopDB`, and `plugins/endpoints/client/Set-ShopDBServerPins.ps1:4-20` writes the server trust pins there by design. + +**Status:** the References section of this page is corrected below. The other three are not, and the SECURITY.md draft should not be published as written. Note that `gh-wj` carries no `docs/adr` directory at all, so there is no ADR text on this branch to correct a reader who follows the wiki. + +**Related, and unverifiable from this branch:** ADR-016's own mechanism is still unbuilt. Grepping `shopdb/` and `plugins/` for `credentials.fetch`, `collector/credentials` and `Fernet` returns nothing, and `plugins/endpoints/plugin.py:163-253` declares only `endpoints.*` permissions. The per-PC credential programme in this range is a different subject under the same word: the agent's own identity secret, not a store that delivers third-party credentials to a PC. The design is sound and consistent with ADR-016's intent, but nothing on this branch records it as a decision. That record belongs on the lineage that holds `docs/adr`. + +### 32. Kiosk check-in records no wipe or scan state, so the new CMMC audit columns stay blank + +`35d4fcd41` added `sanitized` and `scanned_viruses` to the USB audit report on the stated grounds that custody without the state of the media on return is half the story. Both values are written only on the authenticated return path (`plugins/usb/api/selfhosted.py:275-280`, `plugins/usb/api/routes.py:562-571`). The kiosk returns write neither: `kiosk_checkin` updates only check-in time, SSO and name (`plugins/usb/api/selfhosted.py:527-531`), and the external kiosk insert lists only badge, device, action, timestamp and locker (`plugins/usb/api/routes.py:1157-1162`). + +This is an honest blank, not a false record: `auditflag` renders the NULL as an empty cell rather than "no" (`plugins/usb/api/selfhosted.py:547-559`), and the tri-state is deliberate. But `da91a2915` has just widened the kiosk from a few provisioned screens to every PC on the allowed networks, so it is about to become the path most returns take. + +**Correction to note:** the signed-in path does not answer the scan question either. `USBDetail.vue:226-230` and `USBList.vue:267-271` send only `sanitized`, never `scanned_viruses`, so `plugins/usb/api/routes.py:564` always receives None and that column is blank for every return through any first-party UI. + +## Verified as sound + +Read on 2026-09-18 and found correct. Recorded so the next review does not re-litigate them. + +- **Credential storage on the server.** Only a sha256 hash is persisted, plus a 12-character display prefix. The secret exists once, in the 201 enroll response (`plugins/endpoints/api/enrollment.py:212-222`). No admin route re-reads or re-displays it; `issue_credential` has exactly two callers, both inside `enroll_endpoint`. +- **Cross-asset binding.** A credential caller cannot act for another PC. `bind_hostname` returns `ASSET_MISMATCH` for a hostname resolving elsewhere (`plugins/endpoints/api/agentauth.py:206-231`), and every agent-facing route goes through it. +- **Admin surface authorization.** Approve, reject, revoke, allow-reenroll, the settings PUT, the migration wave and all three bootstrap-token routes are `@require_role('admin')` (`plugins/endpoints/api/enrollment.py:642`, `:755`, `:773`, `:827`, `:852`, `:1134`; `plugins/endpoints/api/bootstrap.py:420`, `:475`, `:486`). +- **Tray holds no secret.** The tray reaches the SYSTEM agent over a named pipe with a SYSTEM and Administrators full, INTERACTIVE read-write ACL and FirstPipeInstance anti-squat (`agent/ShopDbEndpointAgent/AgentPipeServer.cs:126-146`); no pipe operation returns the credential (`:246-256`). +- **Bootstrap token scope and lifetime.** Hashed at rest, expiry mandatory and bounded to 1 to 168 hours, uses bounded to 1 to 500, revocable, and unknown, malformed, expired, revoked and exhausted all answer an identical 404 so the URL confirms nothing (`plugins/endpoints/models/bootstrap.py:197-212`, `plugins/endpoints/api/bootstrap.py:109-111`, `:176-189`). The package it serves never carries the collector key. +- **GE-Enforce payload scoping.** `/payload/` enforces the token's resource scope before the ETag shortcut, so a bound token cannot even confirm another scope's hash exists (`plugins/geenforce/api/routes.py:252-283`). +- **`GET /api/plugins/settings-routes` is no longer anonymous.** It moved to `@require_permission('settings.view')` (`shopdb/core/api/plugins.py:47`) and was removed from the public-read allowlist with the reasoning recorded (`tests/test_core/test_public_read_surface.py:29-34`). +- **The kiosk admission gate fails closed in every direction it claims to.** Empty setting matches nothing, an unparseable caller address matches nothing, a malformed CIDR entry is skipped rather than widening the match (`shopdb/utils/ipallowlist.py:44-57`), and the allowlist cannot override the enabled flag. Device-token comparison uses `hmac.compare_digest` in both kiosks (`plugins/usb/api/routes.py:762`, `plugins/printedparts/api/routes.py:536`). An unadmitted caller gets 404, not 403. +- **No secrets committed in the range.** Every `sdbe_` and `sdbb_` hit in the diff is a prefix constant, a regex, a doc example or a test fixture. `certs/` remains ignored except its README, openssl.cnf and the two regeneration scripts. ## Performance Findings -### Agent Polling Inefficiency (99.9% improvement opportunity) +### Agent Polling Inefficiency [LARGELY ADDRESSED] -**Current behavior:** Agent polls `/api/endpoints/inventory` every 30 seconds to POST full inventory (hostname, IP, apps, drives, processes). +**2026-09-18 verdict:** the behaviour the 2026-09-11 page measured no longer matches the code. -**Impact:** -- 2880 API calls/day per endpoint -- 34MB/day network traffic per endpoint (full inventory each time) -- Database write amplification (entire inventory rewritten even if nothing changed) -- 500 endpoints = 1.44M API calls/day, 17GB/day traffic +- The cadence is not 30 seconds. `agent/ShopDbEndpointAgent/appsettings.json:5` ships `cadenceseconds: 900` with 60 seconds of jitter and 30 seconds of startup jitter, so roughly 96 check-ins per day per endpoint rather than 2880. +- Application inventory is a delta, not a full rewrite. `plugins/endpoints/plugin.py:1187` onward builds an incoming fingerprint map, validates each fingerprint as 64 lowercase hex, and applies only the difference; a genuine unique-key collision is caught inside a savepoint and reconciled row by row (`plugins/endpoints/plugin.py:1301-1336`, `:1359-1410`) rather than sinking the check-in. +- Two hotfixes in this range removed row-level locks and hot-path logging from the collector check-in path (`affe455c5`, `1ed0c4d21`). -**Optimization:** Delta updates -1. Agent calculates inventory delta locally (added/removed apps, changed IP, etc.) -2. POST only changes: `{"added_apps": [...], "removed_apps": [...], "changed_ip": "10.1.2.3"}` -3. Server applies delta to existing record -4. Fallback to full inventory on agent startup or if delta rejected - -**Expected improvement:** -- 99% fewer writes (only ~1% of polls have changes) -- 99.5% less network traffic (delta is <500 bytes vs 11KB full inventory) -- Reduced database load (updates instead of full rewrites) - -**Status:** Planned for Q1 2027. Requires agent + server changes. +The traffic and write-volume figures in the 2026-09-11 text were site measurements against the old cadence and should not be carried forward. ## Remediation Summary -| Finding | Severity | Status | Timeline | -|---------|----------|--------|----------| -| Unauthenticated inventory updates | Critical | FIXED | 2026-09-11 | -| Deployment credential exposure | Critical | MITIGATED | 2026-09-11 | -| No certificate validation | Critical | PLANNED | Q4 2026 | -| Hardcoded admin credentials | Critical | FIXED | 2026-09-11 | -| Collector key in git | High | FIXED | 2026-09-11 | -| Passwords in mcp-servers.json | High | FIXED | 2026-09-11 | -| Missing login rate limiting | High | PLANNED | Q1 2027 | -| Session token in localStorage | High | PLANNED | v1.0 | -| Audit log retention | High | PLANNED | Q4 2026 | -| Endpoint events retention | High | PLANNED | Q4 2026 | -| Docker log rotation | Medium | PLANNED | Q4 2026 | -| Agent delta updates | Performance | PLANNED | Q1 2027 | +| Finding | Severity | Status as of 2026-09-18 | Evidence | +|---------|----------|-------------------------|----------| +| 1. Unauthenticated inventory updates | Critical | CLOSED on the routes that exist | `shopdb/core/api/collector.py:251` | +| 2. Collector key unprotected in registry | Critical | STILL OPEN, see finding 19 | `plugins/endpoints/api/siteagent.py:328` | +| 3. No certificate validation in agent | Critical | FIXED by SPKI pinning | `agent/ShopDbEndpointAgent.Core/AgentHttpFactory.cs:291` | +| 4. Hardcoded admin credentials | Critical | PARTLY FIXED, dev-only guard added | `shopdb/cli/__init__.py:619` | +| 5. Collector key in git | High | FIXED; no pre-commit secret scan exists | `.gitignore:174`, `.githooks/pre-commit:5` | +| 6. Passwords in mcp-servers.json | High | FIXED on this branch; keys not rotated | `.gitignore:176` | +| 7. Missing login rate limiting | High | FIXED, limiter plus account lockout | `shopdb/core/api/auth.py:35`, `:113` | +| 8. SQL injection in asset search | High | FALSE POSITIVE, re-confirmed | `plugins/usb/api/routes.py:277` | +| 9. Session token in localStorage | High | UNCHANGED | `frontend/src/stores/auth.js:6` | +| 10. Audit log retention | High | BUILT, off by default, no scheduler | `shopdb/core/services/auditretention.py:123` | +| 11. Endpoint events retention | Medium | STILL OPEN, no purge code exists | `plugins/endpoints/models/endpoint.py:333` | +| 12. HTTPS enforcement in dev | Medium | CHANGED, TLS bypass removed | `frontend/vite.config.js:5` | +| 13. Docker log rotation | Medium | STILL OPEN | `docker-compose.yml` (no logging block) | +| 14-18. Additional medium findings | Medium | PARTLY ANSWERED, source document removed | `shopdb/__init__.py:334`, `8eee6f19d` | +| 19. Shared key in registry with no ACL | High | FIXED 2026-09-18 (installed bays until reinstall) | `plugins/endpoints/api/siteagent.py`, `Protect-HklmShopdbKey` | +| 20. Legacy collector routes ignore the refusal | Medium | OPEN | `shopdb/core/api/collector.py:349` | +| 21. Enroll rejection is not durable | Medium | OPEN | `plugins/endpoints/api/enrollment.py:390` | +| 22. Bootstrap stages elevated script in Windows\Temp | Medium | OPEN | `plugins/endpoints/api/bootstrap.py:68` | +| 23. Kiosk admission spoofable in proxy mode | Medium | OPEN | `deploy/windows/installer/shopdb-install.ps1:2509` | +| 24. Revoke falls back to the shared key | Low | BY DESIGN, model comment wrong | `plugins/endpoints/models/credential.py:8` | +| 25. Bootstrap token in query string | Low | OPEN | `plugins/endpoints/api/bootstrap.py:182` | +| 26. Enroll queue can be filled to cap | Low | OPEN | `plugins/endpoints/api/enrollment.py:350` | +| 27. Badge to name oracle on the kiosk | Low | OPEN | `plugins/usb/api/selfhosted.py:346` | +| 28. Allowlist accepts 0.0.0.0/0 | Low | OPEN | `plugins/usb/plugin.py:154` | +| 29. Provisioning link carries the token in a URL | Low | OPEN, pre-existing mechanism | `plugins/usb/api/routes.py:849` | +| 30. Client-IP warnings name only GE-Enforce | Low | OPEN | `deploy/windows/installer/shopdb-install.ps1:2520` | +| 31. ADR-016 misattributed in four documents | Low | ONE FIXED HERE, three open | `docs/wiki-drafts/README.md:316` | +| 32. Kiosk check-in records no wipe or scan | Low | OPEN | `plugins/usb/api/selfhosted.py:463` | +| Agent polling inefficiency | Performance | LARGELY ADDRESSED | `agent/ShopDbEndpointAgent/appsettings.json:5` | ## Testing Performed -**Authentication enforcement:** -```bash -# Inventory endpoint now requires device token -curl -X POST http://localhost:5001/api/endpoints/inventory -# 401 Unauthorized +This refresh was a read of the code at `35d4fcd41`, not a live exercise against a server. What was run: -curl -X POST http://localhost:5001/api/endpoints/inventory \ - -H "X-API-Key: shopdb_device_test123" -d '{...}' -# 200 OK +**The naming and style gate:** +```bash +bash scripts/check-naming-and-style.sh +# exit 0, all checks pass +``` +Its ADR-015 site-literal grep is not vacuous: a control pattern over the same six directories returns 1995 lines. + +**Kiosk network allowlist suite:** +```bash +pytest tests/test_plugins/test_kiosk_network_allowlist.py +# 20 passed ``` -**Credential scanning:** +**Tracked-secret scan:** ```bash -# No hardcoded passwords in source -bash scripts/check-naming-and-style.sh -# PASS (includes credential pattern checks) - -# No collector keys in tracked files git ls-files | xargs grep -l "shopdb_pat_" | wc -l # 0 ``` -**Registry key cleanup:** -```powershell -# Verify registry key removed -Get-ItemProperty "HKLM:\SOFTWARE\ShopDB\Agent" -Name CollectorApiKey -ErrorAction SilentlyContinue -# (none found) -``` +### Gate coverage gaps found while testing + +- **The ADR-015 site-literal scan does not look at the C# agent.** The grep is scoped to `plugins/ shopdb/ scripts/ deploy/ frontend/src/ tools/` with `--include` on .py, .vue, .js, .ps1, .psm1, .sh, .iss, .json and .html (`scripts/check-naming-and-style.sh:434-440`). `agent/` is absent and `.cs` is not an included extension. Roughly 2500 lines of C# were added under `agent/` in this range, all of which ships to a sister site. Re-running the assembled site patterns over `agent/` by hand returns one hit, prose in `agent/ShopDbEndpointAgent/ShopDbEndpointAgent.csproj:108`, which ADR-015 explicitly permits. The tree is clean; the gate simply would not notice if it stopped being clean. ADR-015's own 2026-08-14 amendment widened the scan on exactly this reasoning, because the literals that reached a second site were in the client scripts and the installer. The C# agent is now that client channel. +- **The gate has no credential pattern check.** See finding 5. The 2026-09-11 page's claim that it includes one is not true of this branch. ## Recommendations -**Immediate (P0):** -1. Deploy per-endpoint certificate pinning (Q4 2026) -2. Mass agent update to remove registry keys (Q4 2026) -3. Rotate exposed collector keys (user accepted risk, not rotating) +**Do first:** +1. ACL the collector key registry value in the generated installer, or finish enrollment and clear it (finding 19). +2. Make the shared-key refusal cover `/pc`, `/apps`, `/heartbeat` and `/bulk`, or retire them (finding 20). +3. Make an admin rejection durable so a new nonce cannot restart the cycle (finding 21). +4. Stage the bootstrap script somewhere a standard user cannot pre-create, or verify it against a server-stamped hash (finding 22). +5. Verify in proxy mode that a proxy is really in front, the way direct mode already verifies the rewrite rule (finding 23). -**Short-term (P1):** -1. Implement audit log retention (Q4 2026) -2. Configure Docker log rotation (Q4 2026) -3. Add login rate limiting (Q1 2027) +**Then:** +1. Do not publish the SECURITY.md wiki page as drafted; it attributes a policy to ADR-016 that ADR-016 does not contain and bans the registry storage the fleet depends on (finding 31). +2. Name the two kiosk allowlists in the installer, preflight and web.config warnings (finding 30). +3. Add server-side validation that rejects a world-open range on both kiosk allowlist settings (finding 28). +4. Move the bootstrap token out of the query string, or accept it and document the log exposure (finding 25). +5. Bound or expire pending enrollment rows so the queue cannot be held at its cap (finding 26). +6. Correct the `reenrollblocked` comment so it does not claim containment of a compromised PC (finding 24). -**Long-term (P2):** -1. Implement agent delta updates (Q1 2027) -2. Migrate sessions to HttpOnly cookies (v1.0) -3. Add CSP headers (v1.0) -4. Purge credentials from git history (v1.0) +**Carried over from 2026-09-11 and still open:** +1. Retention for `endpointevents` (finding 11). +2. Docker log rotation (finding 13). +3. A retention value and a schedule for the audit log, now that the tooling exists (finding 10). +4. HttpOnly cookie sessions for non-kiosk users (finding 9). +5. Recover or re-derive the detail behind findings 14 to 18, which left the tree with `8eee6f19d`. + +**Extend the gates:** +1. Add `agent/` and `.cs` to the ADR-015 site-literal scan. +2. Add a credential pattern check to the gate so the pre-commit hook actually blocks a committed key. ## References -- Security Fix Implementation Guide: `.claude/memory/security_fix_implementation.md` -- Maintenance Baseline: 936 hrs/year operational burden, includes security tasks -- ADR-016: Credential Delivery (device token authentication) -- ADR-XXX: Per-Endpoint Certificate Authentication (planned) +- Per-PC credential rollout runbook: `plugins/endpoints/docs/PER-PC-CREDENTIALS-ROLLOUT.md` +- Agent contract: `plugins/endpoints/AGENT-CONTRACT.md` +- ADR-006: Collector API, the single dynamic dispatch route and its response contract +- ADR-015: Site-specific configuration, and the 2026-08-14 amendment that set the scan's scope +- ADR-016: Credential Delivery. A fleet credential store with Fernet at rest and a `credentials.fetch` scope. It is **not** about device tokens, and it does not ban registry storage. Still unbuilt on this branch. See finding 31. +- The ADR set is not in this repository. It lives on the product lineage; `gh-wj` has no `docs/adr` directory. +- The 2026-09-12 detailed audit reports were removed from the product tree by `8eee6f19d` and are kept with the site's working records. the assistant memory directory does not exist here, so the 2026-09-11 page's pointers into it are dead. diff --git a/frontend/src/components/NetworkAllowlistPicker.vue b/frontend/src/components/NetworkAllowlistPicker.vue new file mode 100644 index 00000000..52b5ed48 --- /dev/null +++ b/frontend/src/components/NetworkAllowlistPicker.vue @@ -0,0 +1,129 @@ + + + + + diff --git a/frontend/src/views/settings/AuthenticationSettings.vue b/frontend/src/views/settings/AuthenticationSettings.vue index 4dfa58ee..5e6fdd96 100644 --- a/frontend/src/views/settings/AuthenticationSettings.vue +++ b/frontend/src/views/settings/AuthenticationSettings.vue @@ -133,6 +133,59 @@ + +
+

Collector API

+

+ The four legacy collector routes (/api/collector/pc, + /apps, /heartbeat, /bulk) name + the PC they write in the request body, so the shared collector key is + an any-host write key on them: whoever holds it can rewrite the + logged-in user, last boot time, serial number and installed apps of + any hostname. A collector-scoped managed token is unaffected. +

+ +
+ + + Off by default, which is what every site does today. Turning it on + stops Report-AssetToShopDB.ps1 and + Report-PrintersToShopDB.ps1 reporting through those + routes until they are moved to per-PC credentials, along with + anything else still calling them with the shared key. Every + shared-key call is logged as "Legacy collector route used with the + shared key" whether this is on or off - read those warnings for a + while first, or turning it on is a guess. On the shipped IIS deploy + that line names the route but not the caller: the X-Forwarded-For + rewrite rule in web.config ships disabled, so every request reaches + the app from 127.0.0.1. Count the routes, not the addresses, unless + that rule is on. The two GE-Enforce + reporters that post to /api/collector/computers and + /api/collector/printers are not affected. This should + follow the move to per-PC credentials, not lead it. + +
+
+ {{ legacySharedKeyError }} +
+
+

User Management

@@ -151,14 +204,66 @@ diff --git a/plugins/endpoints/AGENT-CONTRACT.md b/plugins/endpoints/AGENT-CONTRACT.md index b7813459..cf1a6b2e 100644 --- a/plugins/endpoints/AGENT-CONTRACT.md +++ b/plugins/endpoints/AGENT-CONTRACT.md @@ -791,27 +791,62 @@ One-line install and hands a technician one command, which is run in an ELEVATED PowerShell on a PC with no agent: ``` -powershell -ExecutionPolicy Bypass -c "try { Invoke-WebRequest -UseBasicParsing '/api/endpoints/siteagent/bootstrap.ps1?t=' -OutFile 'C:\Windows\Temp\shopdb-bootstrap.ps1'; & 'C:\Windows\Temp\shopdb-bootstrap.ps1' } finally { Remove-Item 'C:\Windows\Temp\shopdb-bootstrap.ps1' -Force -ErrorAction SilentlyContinue }" +powershell -ExecutionPolicy Bypass -c "try { New-Item -ItemType Directory -Force -Path 'C:\Program Files\ShopDB\bootstrap' | Out-Null; icacls 'C:\Program Files\ShopDB\bootstrap' /inheritance:r /grant '*S-1-5-32-544:(OI)(CI)F' '*S-1-5-18:(OI)(CI)F' | Out-Null; if ((Get-Variable LASTEXITCODE -ValueOnly) -ne 0) { Write-Host 'Refusing to run: could not restrict C:\Program Files\ShopDB\bootstrap to administrators (icacls failed). Fix the directory or delete it, then run the command again.' -ForegroundColor Red; exit 6 }; Remove-Item 'C:\Program Files\ShopDB\bootstrap\shopdb-bootstrap.ps1' -Force -ErrorAction SilentlyContinue; Invoke-WebRequest -UseBasicParsing '/api/endpoints/siteagent/bootstrap.ps1' -Headers @{'X-Endpoint-Bootstrap-Token'=''} -OutFile 'C:\Program Files\ShopDB\bootstrap\shopdb-bootstrap.ps1'; if ((Get-Acl 'C:\Program Files\ShopDB\bootstrap\shopdb-bootstrap.ps1').GetOwner([Security.Principal.SecurityIdentifier]).Value -notin @('S-1-5-32-544','S-1-5-18',[Security.Principal.WindowsIdentity]::GetCurrent().User.Value)) { Write-Host 'Refusing to run: the staged bootstrap script is not owned by an administrator.' -ForegroundColor Red; exit 5 }; & 'C:\Program Files\ShopDB\bootstrap\shopdb-bootstrap.ps1' } finally { Remove-Item 'C:\Program Files\ShopDB\bootstrap\shopdb-bootstrap.ps1' -Force -ErrorAction SilentlyContinue }" ``` Download then run, never `irm ... | iex`: Microsoft Defender scores the piped-to-iex shape as `Trojan:Win32/Commando.A!ml` and kills the process before a line of it runs (reproduced 5 of 5 on a default install, no ASR rules), and -every bay runs Defender. The shape of the line is load-bearing too: +every bay runs Defender. The rest of the shape is load-bearing too: -- a literal temp path and no `$` anywhere, because pasting into an - already-running PowerShell would otherwise expand `$p` and `$env:TEMP` in the - PARENT and hand the child a broken line - as written it means the same in - cmd.exe and in PowerShell; +- the token rides in the `X-Endpoint-Bootstrap-Token` HEADER. A query string + lands verbatim in the IIS and waitress access logs, and a live install token + has no business sitting in a log file; +- staging is `C:\Program Files\ShopDB\bootstrap`, **not** + `C:\Windows\Temp`. That directory grants Users create-file and gives CREATOR + OWNER full control of what they create, so any standard user on the bay can + pre-plant `C:\Windows\Temp\shopdb-bootstrap.ps1` and wait for a tech to + paste the command, which then runs the planted content elevated; +- the line re-asserts an admin-only DACL on the directory it creates + (`icacls /inheritance:r` granting only the well-known SIDs `S-1-5-32-544` and + `S-1-5-18`, never group names, which a rename or a localized install would + break), and CHECKS that icacls succeeded: it is a native command, so it + reports failure only in an exit code and `$ErrorActionPreference` never + applies to it. Exit 6 with a red message, rather than downloading into a + directory that still inherits whatever the parent grants; +- it refuses (exit 5) to run a staged file owned by anyone but an + administrator, `SYSTEM`, or the elevated caller, which is what a pre-planted + file looks like; +- a literal staging path and no `$` anywhere - including the exit-code read, + which is `Get-Variable LASTEXITCODE -ValueOnly`. Pasting into an + already-running PowerShell would otherwise expand every `$` in the PARENT and + hand the child a broken line. As written it means the same in cmd.exe and in + PowerShell; - single quotes for every inner literal, so the outer `-c "..."` is never closed early; - `try`/`finally`, so the dropped file is removed even when the script exits non-zero or the download itself fails. The wrapper's own exit code is the removal's, so read the script's printed result rather than `%ERRORLEVEL%`. -The same URL is offered in the UI as a **Download .ps1** link, for a tech who -would rather save the file and run it. Fetching it does not spend a use either -way. +The same script is offered in the UI as a **Download .ps1** link, for a tech +who would rather save the file and run it. That link is the one caller that +cannot send a header, so it uses `?t=&via=download`. Fetching the script +does not spend a use either way. + +The `?t=` form without `via=download` is accepted ONLY so a bay provisioned +before this change - one still running a command pasted when the query form was +the published shape - keeps working. It is deprecated: every such fetch logs a +warning naming the caller's IP, and that warning is how the rollout finds bays +whose command needs re-issuing. Do not publish it, do not paste it into a new +bay, and do not build it by hand. A site that has re-issued all of its live +commands turns the query form off with +`plugin.endpoints.bootstrapquerytoken` = false, which also disables the +Download .ps1 link. + +The key is declared in the endpoints plugin's `get_settings_defaults()`, so it +seeds as a boolean row, and it is flipped from Settings > Endpoints > Build +site agent > One-line install ("Accept the token in the URL"). It is read +through the core settings API, not a route of this plugin's own. The token is only an authorization to FETCH the installer. The package it serves carries no shared key, whatever `plugin.endpoints.sharedkeyallowed` @@ -822,26 +857,41 @@ Admin routes (admin role, audited): - `POST /api/endpoints/siteagent/bootstrap/tokens` `{"hours": 24, "maxuses": 25}` (`hours` 1-168, `maxuses` 1-500). Returns - `{"secret", "command", "scripturl", "token"}`; `scripturl` is the same token - URL the command downloads, for the UI's Download .ps1 link. - The secret is in the response exactly once; only its sha256 and a 12-character - `tokenprefix` are stored. + `{"secret", "command", "scripturl", "token"}`. `command` carries the token in + the header; `scripturl` is the same route for the UI's Download .ps1 link, in + the `?t=&via=download` form a browser link can actually use. The + `via=download` marker is what keeps an admin's own click out of the + un-migrated-bay warning count - it is a hint, not authorization, and nothing + is granted by it; what matters is that a command generated before this change + cannot carry it. The secret is in the response exactly once; only its sha256 + and a 12-character `tokenprefix` are stored. - `GET /api/endpoints/siteagent/bootstrap/tokens` - prefix, created, expires, `usecount` / `maxuses`, `revokedat`, `lastusedat`, `lastusedip` and a `state` of `active`, `expired`, `exhausted` or `revoked`. Never a secret. - `POST /api/endpoints/siteagent/bootstrap/tokens//revoke` -Token-authenticated routes. No login; the token is the whole authorization and -travels in the query string as `t`, because a PC with no agent has neither a -credential nor a user session to put in a header: +Token-authenticated routes. No login; the token is the whole authorization, +because a PC with no agent has neither a credential nor a user session. It +travels in the `X-Endpoint-Bootstrap-Token` header: | Route | Spends a use | Returns | |---|---|---| -| `GET /api/endpoints/siteagent/bootstrap.ps1?t=...` | no | `text/plain` PowerShell, generated per token | -| `GET /api/endpoints/siteagent/bootstrap/package?t=...` | yes | the keyless agent package ZIP | +| `GET /api/endpoints/siteagent/bootstrap.ps1` | no | `text/plain` PowerShell, generated per token | +| `GET /api/endpoints/siteagent/bootstrap/package` | yes | the keyless agent package ZIP | + +Both also accept `?t=` while +`plugin.endpoints.bootstrapquerytoken` is true (the default), for bays +provisioned before the header form and for the UI Download .ps1 link. A query +token without `via=download` logs a deprecation warning against the caller's +IP; the Download link's own fetches log at info instead and are audited as +`(query-string token, admin download link)`, so the warning count stays a list +of bays to re-issue. With the setting false, a query token answers **400** +naming the header. Both answer **404** for a token that is unknown, malformed, expired, revoked or -exhausted, so the URL confirms nothing about which tokens exist. Both are +exhausted, and for a request that presents no token at all - so the URL +confirms nothing about which tokens exist, and a bare probe cannot learn +whether this site still accepts the query form. Both are rate-limited per IP (fixed window, `ENDPOINTS_BOOTSTRAP_RATELIMIT_MAX` in `ENDPOINTS_BOOTSTRAP_RATELIMIT_WINDOW_SECONDS`, 429 `RATE_LIMITED` over budget) and both write an audit row against `EndpointBootstrapToken` carrying @@ -851,7 +901,9 @@ retried paste must not burn the download budget. The generated script: - refuses to run unelevated (exit 1) and says to reopen PowerShell as - administrator; + administrator (the wrapper line has already refused with exit 6 if the + staging directory could not be locked down, or exit 5 if the staged file was + owned by someone else); - refuses when the agent service already exists (exit 2) and says a managed bay should get the agent from GE-Enforce, unless `SHOPDB_BOOTSTRAP_FORCE` is set to anything but `0` in the same session; diff --git a/plugins/endpoints/api/agentauth.py b/plugins/endpoints/api/agentauth.py index 6f23b118..52dcdf23 100644 --- a/plugins/endpoints/api/agentauth.py +++ b/plugins/endpoints/api/agentauth.py @@ -9,9 +9,15 @@ and, when the route names a PC, binds the caller through bind_hostname(). X-API-Key The shared fleet key (or a collector-scoped managed token). Accepted only while plugin.endpoints.sharedkeyallowed is on, and never for a - PC that holds an active credential. A PC blocked - from re-enrolling has no active credential, so an - older agent on it keeps working on the shared key. + PC that holds an active credential. A PC whose + credential was REVOKED falls back to the shared key, + which is how a rollback to an older agent works and how + the un-migrated fleet keeps reporting; a site that has + finished migrating turns + plugin.endpoints.revokeddeniessharedkey on and a revoked + PC is then refused until it enrolls again. A PC that + never enrolled has no credential row at all and is + never touched by either rule. Only the sha256 of a credential is stored. The full secret exists once, in the enroll response. @@ -37,6 +43,12 @@ DISPLAY_PREFIX_LENGTH = 12 SHARED_KEY_SETTING = 'plugin.endpoints.sharedkeyallowed' AUTO_ENROLL_SETTING = 'plugin.endpoints.autoenroll' +# Off by default, which is what this site does today: revoking a credential +# drops the PC back to the shared key. Turning it on makes a revoke a real +# eviction - the PC reports nothing until it enrolls again - so it is only safe +# once the fleet is off the shared key, and it also closes the rollback path +# that revoke + blockreenroll exists for. +REVOKED_DENIES_SHARED_KEY_SETTING = 'plugin.endpoints.revokeddeniessharedkey' CODE_CREDENTIAL_INVALID = 'CREDENTIAL_INVALID' CODE_SHARED_KEY_REFUSED = 'SHARED_KEY_REFUSED' @@ -70,6 +82,12 @@ def auto_enroll_enabled(): return Setting.get(AUTO_ENROLL_SETTING, default=True) is not False +def revoked_denies_shared_key(): + """True when a revoked credential also bars the PC from the shared key.""" + return Setting.get(REVOKED_DENIES_SHARED_KEY_SETTING, + default=False) is True + + def active_credential_for(assetid): from ..models import EndpointCredential @@ -81,6 +99,33 @@ def active_credential_for(assetid): .first()) +def shared_key_denied_for(assetid): + """True when this asset may not report on the shared key. + + An active credential always denies it: the PC has its own secret and a + shared-key call for it is either an older agent or somebody else. + + A revoked credential denies it only while revokeddeniessharedkey is on. + Today it does not, on purpose: revoke is also how an admin rotates a + credential or rolls a PC back to an agent that only knows the shared key + (revoke + blockreenroll), and denying by default would silence that PC + instead. A PC that never enrolled has no credential row and is never denied, + which is what keeps the un-migrated fleet reporting. + """ + from ..models import EndpointCredential + + if assetid is None: + return False + row = (EndpointCredential.query + .filter(EndpointCredential.assetid == assetid) + .first()) + if row is None: + return False + if row.revokedat is None: + return True + return revoked_denies_shared_key() + + def reenroll_blocked(assetid): """True when an admin blocked this asset from enrolling again.""" from ..models import EndpointCredential @@ -130,8 +175,7 @@ def shared_key_refused_for(hostname): """True when the shared key may not report for this hostname.""" if not shared_key_allowed(): return True - assetid = resolve_hostname(hostname) - return assetid is not None and active_credential_for(assetid) is not None + return shared_key_denied_for(resolve_hostname(hostname)) def credential_invalid_response(): @@ -148,6 +192,20 @@ def shared_key_refused_response(): http_code=401) +def shared_key_revoked_response(): + """Same code and status; the operator needs to know which refusal it is. + + The PC holds no credential and cannot use the shared key either, so an + admin has to let it enroll again. Nothing changes for the agent, which + keeps retrying either way. + """ + return error_response( + CODE_SHARED_KEY_REFUSED, + 'This endpoint had its credential revoked, so the shared key is not ' + 'accepted for it; let it enroll again', + http_code=401) + + def asset_mismatch_response(): return error_response( CODE_ASSET_MISMATCH, @@ -210,7 +268,8 @@ def bind_hostname(hostname): ASSET_MISMATCH, a hostname on no computer record at all is refused with the same code but its own message, and a missing hostname is fine. Shared key: the hostname's asset (None when absent or unknown, for the - route to answer); an enrolled PC is refused. + route to answer); an enrolled PC is refused, and so is one whose credential + was revoked while revokeddeniessharedkey is on. """ hostname = (hostname or '').strip() if is_credential_caller(): @@ -225,7 +284,9 @@ def bind_hostname(hostname): if not hostname: return None, None assetid = resolve_hostname(hostname) - if assetid is not None and active_credential_for(assetid) is not None: + if shared_key_denied_for(assetid): + if active_credential_for(assetid) is None: + return None, shared_key_revoked_response() return None, shared_key_refused_response() g.agentassetid = assetid return assetid, None diff --git a/plugins/endpoints/api/bootstrap.py b/plugins/endpoints/api/bootstrap.py index b8ea3b8e..e602a52b 100644 --- a/plugins/endpoints/api/bootstrap.py +++ b/plugins/endpoints/api/bootstrap.py @@ -14,14 +14,25 @@ Routes POST /siteagent/bootstrap/tokens admin, audited; secret once GET /siteagent/bootstrap/tokens admin; never a secret POST /siteagent/bootstrap/tokens//revoke admin, audited - GET /siteagent/bootstrap.ps1?t=... token only; not a use - GET /siteagent/bootstrap/package?t=... token only; one use + GET /siteagent/bootstrap.ps1 token only; not a use + GET /siteagent/bootstrap/package token only; one use -The two token-authenticated routes answer 404 for anything not currently -usable - unknown, expired, revoked or exhausted - so the URL confirms nothing -about which tokens exist. Both are per-IP rate limited with the same fixed -window shape the login and payload-download limiters use, and both write an -audit row carrying the caller's IP. +The two token-authenticated routes take the token in the +X-Endpoint-Bootstrap-Token header, and still accept the deprecated ?t= form so +a command already pasted on a bay keeps working. They answer 404 for anything +not currently usable - unknown, malformed, expired, revoked, exhausted, or no +token presented at all - so the URL confirms nothing about which tokens exist, +nor about whether the query form is still accepted here. The one 400 is a real +query token offered at a site that has turned the query form off. + +The admin UI's Download .ps1 link has to use the query form (a browser link +cannot send a header), so its URL carries a marker that tells it apart from a +bay still running a command pasted before this change. Only the bay logs the +deprecation warning, which is what the rollout counts un-migrated bays with. + +Both routes are per-IP rate limited with the same fixed window shape the login +and payload-download limiters use, and both write an audit row carrying the +caller's IP. """ import hashlib @@ -60,12 +71,60 @@ LIST_LIMIT = 100 SCRIPT_PATH = '/api/endpoints/siteagent/bootstrap.ps1' PACKAGE_PATH = '/api/endpoints/siteagent/bootstrap/package' -# Where the one-line command drops the script before running it. A literal, not -# $env:TEMP: the line has to survive being pasted into an already-running +# Where the one-line command stages the script before running it. A literal, +# not $env:TEMP: the line has to survive being pasted into an already-running # PowerShell, which expands every $ inside the outer double quotes before the -# child process sees them. The install needs elevation anyway, so this path is -# writable by definition. -COMMAND_SCRIPT_PATH = r'C:\Windows\Temp\shopdb-bootstrap.ps1' +# child process sees them. +# +# NOT C:\Windows\Temp. That directory grants Users create-file and gives +# CREATOR OWNER full control of what they create, so any standard user on the +# bay can pre-plant this exact path and wait for a tech to paste the command, +# which then runs the planted content elevated. Program Files is admin-only on +# a stock install, the command re-asserts an admin-only DACL on the directory +# it creates, and it refuses to run a staged file owned by anyone else. +COMMAND_STAGE_DIR = r'C:\Program Files\ShopDB\bootstrap' +COMMAND_SCRIPT_PATH = COMMAND_STAGE_DIR + r'\shopdb-bootstrap.ps1' + +# Well-known SIDs, not names: the bays are domain-joined and a renamed or +# localized Administrators group would quietly break both the grant and the +# ownership test. +ADMINISTRATORS_SID = 'S-1-5-32-544' +SYSTEM_SID = 'S-1-5-18' + +# The token the two public routes accept. A header, not ?t=: a query string +# lands verbatim in the reverse proxy and gunicorn access logs, and a live +# install token has no business sitting in a log file. +BOOTSTRAP_TOKEN_HEADER = 'X-Endpoint-Bootstrap-Token' +QUERY_TOKEN_ARG = 't' + +# Where the token on this request came from. Kept apart because the rollout +# counts un-migrated bays by the deprecation warning SOURCE_QUERY logs: an +# admin clicking Download .ps1 in the browser must not land in that count, and +# a request carrying no token at all must not answer differently from one +# carrying a token nobody issued. +SOURCE_HEADER = 'header' +SOURCE_QUERY = 'query' +SOURCE_UI_LINK = 'uilink' +SOURCE_NONE = 'none' + +# The marker on the admin Download .ps1 link. A hint, NOT authentication: a +# caller can add it by hand, and nothing is granted by it. What matters is the +# other direction - a one-line command generated before this change cannot +# carry it, so a request without it really is a bay to go and re-issue. +UI_LINK_ARG = 'via' +UI_LINK_VALUE = 'download' + +# DEPRECATED, kept on purpose: every command already pasted on a bay, and the +# UI's "Download .ps1" link (a browser link cannot send a header), carry the +# token in the query string. Accepting it stays the default so a mid-rollout +# bay keeps working; a site that has re-issued its live commands turns it off. +# Turning it off also disables the download link. +# +# Declared in the endpoints plugin's get_settings_defaults() (so the row seeds +# boolean, not string) and flipped from Settings > Endpoints > Build site agent +# > One-line install, "Accept the token in the URL". A switch with no seed and +# no control would be inert on an air-gapped box. +QUERY_TOKEN_SETTING = 'plugin.endpoints.bootstrapquerytoken' # Fixed-window per-IP limiter, same shape and cache extension as the login # limiter in shopdb.core.api.auth and the GE-Enforce payload limiter. A tech @@ -86,6 +145,22 @@ def _naive_utcnow(): def _caller_ip(): + """The caller address for the audit rows and the deprecation warning. + + Read the value before trusting it to identify a bay. client_ip() returns + remote_addr and nothing else. On the shipped Windows deploy IIS launches + waitress on loopback through HttpPlatformHandler, and deploy/windows/ + web.config ships the X-Forwarded-For rewrite rule COMMENTED OUT - IIS sets + no such header on its own, so waitress's --trusted-proxy=127.0.0.1 + --trusted-proxy-count=1 has nothing to rewrite REMOTE_ADDR from and leaves + it as the loopback peer. TRUSTED_PROXY_COUNT also defaults to 0, so + ProxyFix is not installed either. Every row and every warning then reads + 127.0.0.1, and the un-migrated-bay list identifies no bay. + + It is a real address only where the site enabled that rewrite rule (the + installer's -ClientIpSource direct), or set TRUSTED_PROXY_COUNT for a + front proxy that sets the header itself. + """ return client_ip(request) or 'unknown' @@ -142,11 +217,31 @@ def bootstrap_base_url(): return 'https://{0}{1}'.format(host, root) +def _base(baseurl=None): + resolved = baseurl if baseurl is not None else bootstrap_base_url() + return resolved.rstrip('/') + + def bootstrap_script_url(secret, baseurl=None): - """The token URL that serves the script: downloaded by the command, and - offered in the UI as a plain file a tech can save and run instead.""" - base = (baseurl if baseurl is not None else bootstrap_base_url()).rstrip('/') - return '{0}{1}?t={2}'.format(base, SCRIPT_PATH, secret) + """The UI's "Download .ps1" link: a plain file a tech can save and run. + + Still the query-string form, because an cannot set a header. + The one-line command no longer uses this shape - see bootstrap_command. + + It carries UI_LINK_ARG so the server can tell this click apart from a bay + running a pre-change command. Without that the admin's own Download link + fires the deprecation warning the rollout uses to FIND un-migrated bays, + and the count becomes noise the moment an admin mints a token. + """ + return '{0}{1}?{2}={3}&{4}={5}'.format(_base(baseurl), SCRIPT_PATH, + QUERY_TOKEN_ARG, secret, + UI_LINK_ARG, UI_LINK_VALUE) + + +def bootstrap_fetch_url(path, baseurl=None): + """A token-free URL for the machine-to-machine fetches. The token rides in + BOOTSTRAP_TOKEN_HEADER so it never reaches an access log.""" + return '{0}{1}'.format(_base(baseurl), path) def bootstrap_command(secret, baseurl=None): @@ -158,28 +253,145 @@ def bootstrap_command(secret, baseurl=None): Single quotes inside and no $ anywhere, so the one line means the same whether it is pasted into cmd.exe or into a PowerShell prompt. The download - and the run are wrapped in try/finally so the dropped file goes away even + and the run are wrapped in try/finally so the staged file goes away even when the script exits non-zero or the download itself fails. + + Staging is the part worth being careful about, so the line creates + COMMAND_STAGE_DIR with an admin-only DACL, clears any leftover file, and + refuses (exit 5) to run a staged script that some other principal owns - + which is what a pre-planted file looks like. Well-known SIDs throughout, + and the owner may also be the elevated caller, since a file created by an + administrator is owned by that account rather than by the group. + + The hardening is also CHECKED (exit 6). icacls is a native command, so it + reports failure only in an exit code: no exception is raised, and + $ErrorActionPreference never applies to it. Unchecked, an icacls that + cannot write the DACL leaves the directory inheriting whatever the parent + grants and the line downloads into it anyway - the silent version of the + hole this staging move was made to close. The same check also covers the + New-Item before it, because icacls on a directory that was never created + exits non-zero too. + + Read as `Get-Variable LASTEXITCODE -ValueOnly`, not `$LASTEXITCODE`: the + whole line must stay free of `$`, or a paste into an already-running + PowerShell expands it in the PARENT and the child is handed `-ne 0` with + the parent's last exit code baked in. """ - return ('powershell -ExecutionPolicy Bypass -c "' - "try {{ Invoke-WebRequest -UseBasicParsing '{0}' -OutFile '{1}'; " - "& '{1}' }} finally {{ Remove-Item '{1}' -Force " - '-ErrorAction SilentlyContinue }}"' - ).format(bootstrap_script_url(secret, baseurl), - COMMAND_SCRIPT_PATH) + return ( + 'powershell -ExecutionPolicy Bypass -c "' + "try {{ New-Item -ItemType Directory -Force -Path '{stagedir}' " + '| Out-Null; ' + "icacls '{stagedir}' /inheritance:r /grant '*{admin}:(OI)(CI)F' " + "'*{system}:(OI)(CI)F' | Out-Null; " + 'if ((Get-Variable LASTEXITCODE -ValueOnly) -ne 0) ' + "{{ Write-Host 'Refusing to run: could not restrict " + "{stagedir} to administrators (icacls failed). Fix the directory " + "or delete it, then run the command again.' -ForegroundColor Red; " + 'exit 6 }}; ' + "Remove-Item '{path}' -Force -ErrorAction SilentlyContinue; " + "Invoke-WebRequest -UseBasicParsing '{url}' " + "-Headers @{{'{header}'='{secret}'}} -OutFile '{path}'; " + "if ((Get-Acl '{path}').GetOwner(" + '[Security.Principal.SecurityIdentifier]).Value -notin ' + "@('{admin}','{system}'," + '[Security.Principal.WindowsIdentity]::GetCurrent().User.Value)) ' + "{{ Write-Host 'Refusing to run: the staged bootstrap script is not " + "owned by an administrator.' -ForegroundColor Red; exit 5 }}; " + "& '{path}' }} finally {{ Remove-Item '{path}' -Force " + '-ErrorAction SilentlyContinue }}"' + ).format(stagedir=COMMAND_STAGE_DIR, + path=COMMAND_SCRIPT_PATH, + url=bootstrap_fetch_url(SCRIPT_PATH, baseurl), + header=BOOTSTRAP_TOKEN_HEADER, + secret=secret, + admin=ADMINISTRATORS_SID, + system=SYSTEM_SID) def _hash_token(secret): return hashlib.sha256(secret.encode('utf-8')).hexdigest() -def _resolve_token(): - """The usable token named by ?t=, else None. +def _presented_token(): + """(secret, source) for the token this request carries. + + Header first, query string second. The query form is deprecated but still + the only one a browser download link can use. + + SOURCE_NONE when nothing was presented, which is NOT the same as an empty + query token: reporting a bare GET as 'query' made it answer 400 naming the + header, telling an anonymous caller whether this site had turned the query + form off, and contradicting the 404-for-everything-unusable rule at the top + of this module. + """ + header = (request.headers.get(BOOTSTRAP_TOKEN_HEADER) or '').strip() + if header: + return header, SOURCE_HEADER + query = (request.args.get(QUERY_TOKEN_ARG) or '').strip() + if not query: + return '', SOURCE_NONE + if request.args.get(UI_LINK_ARG) == UI_LINK_VALUE: + return query, SOURCE_UI_LINK + return query, SOURCE_QUERY + + +def _is_query_source(source): + """Both query-string forms, the bay's and the admin download link's. The + setting is about a token in an access log, and the admin link puts one + there just as surely as a pasted command does.""" + return source in (SOURCE_QUERY, SOURCE_UI_LINK) + + +def _query_token_allowed(): + """Defaults to True: today's pasted commands and the UI download link both + use the query form, and turning that off mid-rollout strands a bay.""" + return bool(Setting.get(QUERY_TOKEN_SETTING, True)) + + +def _query_token_refused(): + """Answered before any lookup, so it confirms nothing about any token.""" + return error_response( + ErrorCodes.VALIDATION_ERROR, + 'Send the bootstrap token in the {0} header.'.format( + BOOTSTRAP_TOKEN_HEADER), + http_code=400) + + +def _note_token_source(source): + """Audit suffix for a token that really resolved, and, for a query-string + token, one log line naming the caller and WHICH query path it was. + + Only the bay warns. That warning is the rollout's un-migrated-bay counter + (see PER-PC-CREDENTIALS-ROLLOUT.md), so the admin's own Download .ps1 link + logs at info instead - otherwise every mint-and-test by an admin adds a + bay that does not exist. + + Never the token itself, in either line: keeping it out of the logs is the + whole point of the header form. + """ + if source == SOURCE_UI_LINK: + current_app.logger.info( + 'bootstrap script fetched by %s from the admin Download .ps1 ' + 'link; query-string token, but the admin browser path, NOT an ' + 'un-migrated bay', _caller_ip()) + return ' (query-string token, admin download link)' + if source == SOURCE_QUERY: + current_app.logger.warning( + 'bootstrap token presented in the query string by %s with no ' + 'admin download marker, so this is an un-migrated bay running a ' + 'command pasted before the header form; that form is deprecated ' + 'and slated for removal, re-issue the one-line install command', + _caller_ip()) + return ' (query-string token, deprecated)' + return '' + + +def _resolve_token(secret): + """The usable token with this secret, else None. Unknown, malformed, expired, revoked and exhausted all resolve to None so the caller can answer 404 for every one of them. """ - secret = (request.args.get('t') or '').strip() if not _TOKEN_PATTERN.match(secret): return None row = EndpointBootstrapToken.query.filter_by( @@ -218,6 +430,7 @@ _SCRIPT_TEMPLATE = '''# ShopDB endpoint agent bootstrap $ErrorActionPreference = "Stop" $BootstrapPackageUrl = {packageurl} +$BootstrapPackageHeaders = @{{ {tokenheader} = {token} }} $BootstrapServiceName = "ShopDbEndpointAgent" $BootstrapInstallDir = "C:\\Program Files\\ShopDB\\EndpointAgent" $BootstrapAgentSha256 = {agentsha} @@ -308,7 +521,7 @@ try {{ New-Item -ItemType Directory -Force -Path $bootstrapTempDir | Out-Null Write-Host "[1/4] Downloading the agent package ..." - Invoke-WebRequest -Uri $BootstrapPackageUrl -OutFile $bootstrapZipPath -UseBasicParsing + Invoke-WebRequest -Uri $BootstrapPackageUrl -Headers $BootstrapPackageHeaders -OutFile $bootstrapZipPath -UseBasicParsing Write-Host "[2/4] Expanding and verifying ..." Expand-Archive -Path $bootstrapZipPath -DestinationPath $bootstrapExtractDir -Force @@ -394,11 +607,12 @@ exit 0 def build_bootstrap_script(secret, baseurl=None): """The PowerShell text served for one token, with the URL and hashes baked in.""" - base = (baseurl if baseurl is not None else bootstrap_base_url()).rstrip('/') + base = _base(baseurl) hashes = siteagent.staged_binary_hashes() return _SCRIPT_TEMPLATE.format( - packageurl=json.dumps('{0}{1}?t={2}'.format(base, PACKAGE_PATH, - secret)), + packageurl=json.dumps(bootstrap_fetch_url(PACKAGE_PATH, base)), + tokenheader=json.dumps(BOOTSTRAP_TOKEN_HEADER), + token=json.dumps(secret), agentsha=json.dumps(hashes.get('agent', '')), traysha=json.dumps(hashes.get('tray', '')), forcevariable=FORCE_VARIABLE, @@ -500,7 +714,8 @@ def revoke_bootstrap_token(tokenid): # --------------------------------------------------------------------------- -# Token-authenticated: no login, the token in ?t= is the whole authorization +# Token-authenticated: no login, the token is the whole authorization. It +# rides in the X-Endpoint-Bootstrap-Token header; ?t= is still accepted. # --------------------------------------------------------------------------- @endpoints_bp.route('/siteagent/bootstrap.ps1', methods=['GET']) @@ -516,12 +731,15 @@ def serve_bootstrap_script(): """ if _ratelimited(): return _rate_limit_response() - row = _resolve_token() + secret, source = _presented_token() + if _is_query_source(source) and not _query_token_allowed(): + return _query_token_refused() + row = _resolve_token(secret) if row is None: return _not_found() - secret = (request.args.get('t') or '').strip() - _audit('requested', row, 'bootstrap script served') + _audit('requested', row, + 'bootstrap script served' + _note_token_source(source)) db.session.commit() return Response(build_bootstrap_script(secret), mimetype='text/plain') @@ -536,7 +754,10 @@ def download_bootstrap_package(): """ if _ratelimited(): return _rate_limit_response() - row = _resolve_token() + secret, source = _presented_token() + if _is_query_source(source) and not _query_token_allowed(): + return _query_token_refused() + row = _resolve_token(secret) if row is None: return _not_found() @@ -544,8 +765,8 @@ def download_bootstrap_package(): row.usecount = (row.usecount or 0) + 1 row.lastusedat = now row.lastusedip = _caller_ip()[:45] - _audit('downloaded', row, 'bootstrap package, use {0} of {1}'.format( - row.usecount, row.maxuses)) + _audit('downloaded', row, 'bootstrap package, use {0} of {1}{2}'.format( + row.usecount, row.maxuses, _note_token_source(source))) db.session.commit() buffer, warning = siteagent.build_keyless_package(bootstrap_base_url()) diff --git a/plugins/endpoints/api/enrollment.py b/plugins/endpoints/api/enrollment.py index 972d6ebd..cf4e3ee9 100644 --- a/plugins/endpoints/api/enrollment.py +++ b/plugins/endpoints/api/enrollment.py @@ -78,6 +78,14 @@ ENROLL_RATELIMIT_WINDOW_SECONDS = 60 PENDING_CAP_SETTING = 'plugin.endpoints.maxpendingenrollments' DEFAULT_PENDING_CAP = 500 +# How long a pending request may go untouched before the cap treats it as +# abandoned. An agent waiting for approval re-asks about once a minute, so a row +# nothing has touched for a week is a PC that was re-imaged, renamed or scrapped, +# or a junk row somebody wrote at the open door. At the cap those are dropped to +# make room before a PC nobody has seen before is refused, so a queue that filled +# with abandoned rows drains itself instead of blocking every new PC on site. +PENDING_STALE_AFTER = timedelta(days=7) + _NONCE_PATTERN = re.compile(r'^[A-Za-z0-9_-]{32,128}$') REASON_UNKNOWN_HOSTNAME = 'unknown hostname' @@ -265,6 +273,38 @@ def _pending_queue_is_full(): return pending >= cap +def _drop_stale_pending(): + """Delete pending requests nothing has touched in PENDING_STALE_AFTER. + + Only PENDING rows: an admin decision (approved, rejected, issued) is never + aged out, so a rejection keeps standing however long the PC argues with it. + Returns how many rows went, for the caller to decide whether the cap is + still reached. + """ + cutoff = _naive_utcnow() - PENDING_STALE_AFTER + stale = (EndpointEnrollRequest.query + .filter(EndpointEnrollRequest.status + == EndpointEnrollRequest.STATUS_PENDING, + EndpointEnrollRequest.lastseenat < cutoff) + .order_by(EndpointEnrollRequest.lastseenat.asc()) + .all()) + if not stale: + return 0 + for row in stale: + db.session.delete(row) + current_app.logger.info( + 'endpoint enroll: aged out %d pending requests untouched for %d days', + len(stale), PENDING_STALE_AFTER.days) + AuditLog.log( + 'deleted', 'EndpointEnrollRequest', + entityname=stale[0].hostname, + details='aged out {0} pending enroll requests untouched for {1} ' + 'days, to make room under the pending cap'.format( + len(stale), PENDING_STALE_AFTER.days)) + db.session.commit() + return len(stale) + + def _pending_queue_full_response(hostname, serialnumber): """Refuse a new row, say so plainly in the log and leave an audit trail.""" cap = pending_cap() @@ -349,15 +389,42 @@ def enroll_endpoint(): if row is None: # First contact for this hostname and serial. This is the only branch - # that grows the table, so it is the only one the cap guards. + # that grows the table, so it is the only one the cap guards. Every + # other contact reuses the one row this identity already owns, which is + # what keeps a single PC from holding a queue's worth of them. if _pending_queue_is_full(): - return _pending_queue_full_response(hostname, serialnumber) + # Abandoned rows come out before a real PC is turned away. + _drop_stale_pending() + if _pending_queue_is_full(): + return _pending_queue_full_response(hostname, serialnumber) row = EndpointEnrollRequest( hostname=hostname, serialnumber=serialnumber, enrollnonce=nonce, status=EndpointEnrollRequest.STATUS_PENDING, requestedat=now) db.session.add(row) else: samenonce = hmac.compare_digest(row.enrollnonce, nonce) + if row.status == EndpointEnrollRequest.STATUS_REJECTED: + # A rejection binds to the IDENTITY - this hostname and serial - not + # to the nonce it was rejected with. A new installation on the same + # PC brings a new nonce, and re-arming the row on it would put the + # request back in the queue and, with the shared key and auto-enroll + # on, hand the retry the credential an admin had just refused. + # Nothing the PC sends clears a rejection; an admin approving the + # request does. + # + # The nonce is still recorded, because a rejected row hands nothing + # out and an admin who does approve it later has to bind that + # approval to the installation asking NOW, not to the one that was + # refused weeks ago. + row.enrollnonce = nonce + row.lastseenat = now + row.sourceip = sourceip + if agentversion: + row.agentversion = agentversion + db.session.commit() + return success_response( + {'status': 'pending', 'requestid': row.requestid, + 'reason': REASON_REJECTED}, http_code=202) if row.status == EndpointEnrollRequest.STATUS_APPROVED: row.lastseenat = now row.sourceip = sourceip @@ -380,16 +447,10 @@ def enroll_endpoint(): row.lastseenat = now row.sourceip = sourceip return _pending(row, REASON_NONCE_CHANGED, row.matchedassetid) - if row.status == EndpointEnrollRequest.STATUS_REJECTED and samenonce: - row.lastseenat = now - row.sourceip = sourceip - db.session.commit() - return success_response( - {'status': 'pending', 'requestid': row.requestid, - 'reason': REASON_REJECTED}, http_code=202) if row.status != EndpointEnrollRequest.STATUS_PENDING: - # Issued earlier (credential since revoked) or rejected for another - # installation: a new enrollment cycle bound to this nonce. + # Issued earlier and the credential has since been revoked: a new + # enrollment cycle bound to this nonce. Rejected rows never reach + # here - they returned above, whatever nonce they carried. row.enrollnonce = nonce row.requestedat = now row.decidedat = None @@ -574,6 +635,7 @@ def _page(rows): def _settings_payload(): return {'sharedkeyallowed': agentauth.shared_key_allowed(), 'autoenroll': agentauth.auto_enroll_enabled(), + 'revokeddeniessharedkey': agentauth.revoked_denies_shared_key(), 'maxpendingenrollments': pending_cap()} @@ -667,6 +729,11 @@ def approve_enroll_request(requestid): The pending reason for a one-off PC is by definition `unknown hostname`, so this is the normal case, not the exception. + Approving is also the only way out of a rejection: a rejected request keeps + refusing its PC whatever nonce it sends, and this is the deliberate admin + step that lifts it. The approval binds to the nonce of that PC's most recent + contact, so an agent reinstalled while it was refused still collects it. + Body: {"assetid": 123, "sethostname": false}. A computer with no hostname is stamped with the request's. A computer carrying a DIFFERENT hostname is refused with 409 naming both, until the caller repeats the approval with @@ -734,6 +801,7 @@ def approve_enroll_request(requestid): details='hostname {0} from enroll request {1}'.format( hostnameresult, row.requestid)) + wasrejected = row.status == EndpointEnrollRequest.STATUS_REJECTED row.status = EndpointEnrollRequest.STATUS_APPROVED row.matchedassetid = assetid row.reason = None @@ -741,8 +809,9 @@ def approve_enroll_request(requestid): row.decidedby = _username() AuditLog.log('updated', 'EndpointEnrollRequest', entityid=row.requestid, entityname=row.hostname, - details='enroll request approved for asset {0}, hostname {1}' - .format(assetid, hostnameresult)) + details='enroll request {0} for asset {1}, hostname {2}' + .format('approved out of a rejection' if wasrejected + else 'approved', assetid, hostnameresult)) db.session.commit() approved = row.to_dict() approved['hostnameresult'] = hostnameresult @@ -780,6 +849,11 @@ def revoke_endpoint_credential(assetid): before rolling a PC back to an agent that only knows the shared key: without it the current agent re-enrolls within seconds and the older agent is then refused. A PC whose credential is already revoked can still be blocked. + + Whether a revoked PC may still report on the shared key is the site setting + plugin.endpoints.revokeddeniessharedkey. It is off here, so today it can - + which is what makes the rollback above work, and what keeps a rotation from + silencing the PC while it waits for approval. """ payload = request.get_json(silent=True) or {} if not isinstance(payload, dict): @@ -853,12 +927,24 @@ def allow_endpoint_reenroll(assetid): @endpoints_bp.route('/enrollment/settings', methods=['PUT']) @require_role('admin') def update_enrollment_settings(): + """Change the enrollment settings. + + revokeddeniessharedkey is off by default, which is what the fleet does + today: a PC whose credential was revoked falls back to the shared key, so a + credential rotation, and the rollback to an older agent that revoke + + blockreenroll exists for, both keep reporting. Turning it on makes a revoke + an eviction - that PC reports nothing until it is allowed to enroll again - + and is only safe once every PC here holds its own credential. + """ from shopdb.api import Setting payload = request.get_json(silent=True) or {} changes = {} - for key, settingkey in (('sharedkeyallowed', agentauth.SHARED_KEY_SETTING), - ('autoenroll', agentauth.AUTO_ENROLL_SETTING)): + for key, settingkey in ( + ('sharedkeyallowed', agentauth.SHARED_KEY_SETTING), + ('autoenroll', agentauth.AUTO_ENROLL_SETTING), + ('revokeddeniessharedkey', + agentauth.REVOKED_DENIES_SHARED_KEY_SETTING)): if key not in payload: continue if not isinstance(payload[key], bool): @@ -878,7 +964,8 @@ def update_enrollment_settings(): if not changes: return error_response( ErrorCodes.VALIDATION_ERROR, - 'Send sharedkeyallowed, autoenroll and/or maxpendingenrollments') + 'Send sharedkeyallowed, autoenroll, revokeddeniessharedkey ' + 'and/or maxpendingenrollments') before = _settings_payload() for key, (settingkey, value, valuetype) in changes.items(): diff --git a/plugins/endpoints/api/siteagent.py b/plugins/endpoints/api/siteagent.py index 6c6b4527..897573f3 100644 --- a/plugins/endpoints/api/siteagent.py +++ b/plugins/endpoints/api/siteagent.py @@ -410,6 +410,57 @@ function Set-HklmValueBothViews {{ }} }} +# Lock SOFTWARE\\GE\\ShopDB to SYSTEM and Administrators, in both views. +# +# WHY. The shared fleet key is written here and stays until every consumer is +# on a per-PC credential, and the DACL this key inherits by default grants +# BUILTIN\\Users read - so any logged-on user on any bay could read the fleet +# key, which is the exact exposure the per-PC credential migration exists to +# close. Everything that legitimately reads this key runs as SYSTEM (the agent +# service, the GE-Enforce reporters) or as an admin (the adoption tool, +# Set-ShopDBServerPins.ps1). The tray reads no registry at all - it talks to the +# service over the named pipe - so nothing loses access here. +# +# It also protects ServerPins from a standard user, which is what makes the +# registry safe to treat as authoritative for pinning. +function Protect-HklmShopdbKey {{ + param([Parameter(Mandatory)] [string] $SubKey) + $rights = [System.Security.AccessControl.RegistryRights] + foreach ($view in @([Microsoft.Win32.RegistryView]::Registry64, + [Microsoft.Win32.RegistryView]::Registry32)) {{ + $base = [Microsoft.Win32.RegistryKey]::OpenBaseKey( + [Microsoft.Win32.RegistryHive]::LocalMachine, $view) + try {{ + $key = $base.OpenSubKey( + $SubKey, + [Microsoft.Win32.RegistryKeyPermissionCheck]::ReadWriteSubTree, + $rights::ChangePermissions -bor $rights::ReadPermissions) + if (-not $key) {{ continue }} + try {{ + $acl = $key.GetAccessControl( + [System.Security.AccessControl.AccessControlSections]::Access) + # Break inheritance and DROP the inherited entries rather than + # copying them in: copying is what silently preserves the + # BUILTIN\\Users read this function exists to remove. + $acl.SetAccessRuleProtection($true, $false) + foreach ($rule in @($acl.Access)) {{ $null = $acl.RemoveAccessRule($rule) }} + foreach ($sid in @("S-1-5-18", "S-1-5-32-544")) {{ + $who = New-Object System.Security.Principal.SecurityIdentifier($sid) + $acl.AddAccessRule((New-Object System.Security.AccessControl.RegistryAccessRule( + $who, + $rights::FullControl, + [System.Security.AccessControl.InheritanceFlags]::ContainerInherit, + [System.Security.AccessControl.PropagationFlags]::None, + [System.Security.AccessControl.AccessControlType]::Allow))) + }} + $key.SetAccessControl($acl) + }} + finally {{ $key.Dispose() }} + }} + finally {{ $base.Dispose() }} + }} +}} + # REG_MULTI_SZ in both views, compare-before-write so a reinstall with the same # pins leaves the value (and its last-write time) alone. function Set-HklmMultiStringBothViews {{ @@ -492,6 +543,17 @@ else {{ Write-Warning "no server pin in this package; ServerPins left as is." }} +# Harden the key AFTER the writes: the DACL is replaced, not merged, so writing +# first and locking second keeps this correct whether or not the key pre-existed. +try {{ + Protect-HklmShopdbKey -SubKey "SOFTWARE\\GE\\ShopDB" + Write-Host " HKLM\\SOFTWARE\\GE\\ShopDB restricted to SYSTEM and Administrators." +}} +catch {{ + Write-Warning "could not restrict HKLM\\SOFTWARE\\GE\\ShopDB: $($_.Exception.Message)" + Write-Warning "the shared key on this PC stays readable by any logged-on user until that is fixed." +}} + # 1a. Trust the bundled server certificate, if one shipped. The agent's outbound # TLS to a self-signed or internal-CA server fails closed until the machine # trusts the cert. Importing into LocalMachine\\Root covers the SYSTEM service. diff --git a/plugins/endpoints/docs/PER-PC-CREDENTIALS-ROLLOUT.md b/plugins/endpoints/docs/PER-PC-CREDENTIALS-ROLLOUT.md index 259ec22d..3998487e 100644 --- a/plugins/endpoints/docs/PER-PC-CREDENTIALS-ROLLOUT.md +++ b/plugins/endpoints/docs/PER-PC-CREDENTIALS-ROLLOUT.md @@ -292,17 +292,63 @@ says so when it finds an agent already installed. a new one if it is lost. 3. On the target PC, open PowerShell **as administrator** and paste it: +**Copy the command the UI gives you.** Do not retype it from here and do not +reuse one you saved from an older round - see the warning under this block. It +looks like this: + ``` -powershell -ExecutionPolicy Bypass -c "try { Invoke-WebRequest -UseBasicParsing 'https:///api/endpoints/siteagent/bootstrap.ps1?t=' -OutFile 'C:\Windows\Temp\shopdb-bootstrap.ps1'; & 'C:\Windows\Temp\shopdb-bootstrap.ps1' } finally { Remove-Item 'C:\Windows\Temp\shopdb-bootstrap.ps1' -Force -ErrorAction SilentlyContinue }" +powershell -ExecutionPolicy Bypass -c "try { New-Item -ItemType Directory -Force -Path 'C:\Program Files\ShopDB\bootstrap' | Out-Null; icacls 'C:\Program Files\ShopDB\bootstrap' /inheritance:r /grant '*S-1-5-32-544:(OI)(CI)F' '*S-1-5-18:(OI)(CI)F' | Out-Null; if ((Get-Variable LASTEXITCODE -ValueOnly) -ne 0) { Write-Host 'Refusing to run: could not restrict C:\Program Files\ShopDB\bootstrap to administrators (icacls failed). Fix the directory or delete it, then run the command again.' -ForegroundColor Red; exit 6 }; Remove-Item 'C:\Program Files\ShopDB\bootstrap\shopdb-bootstrap.ps1' -Force -ErrorAction SilentlyContinue; Invoke-WebRequest -UseBasicParsing 'https:///api/endpoints/siteagent/bootstrap.ps1' -Headers @{'X-Endpoint-Bootstrap-Token'=''} -OutFile 'C:\Program Files\ShopDB\bootstrap\shopdb-bootstrap.ps1'; if ((Get-Acl 'C:\Program Files\ShopDB\bootstrap\shopdb-bootstrap.ps1').GetOwner([Security.Principal.SecurityIdentifier]).Value -notin @('S-1-5-32-544','S-1-5-18',[Security.Principal.WindowsIdentity]::GetCurrent().User.Value)) { Write-Host 'Refusing to run: the staged bootstrap script is not owned by an administrator.' -ForegroundColor Red; exit 5 }; & 'C:\Program Files\ShopDB\bootstrap\shopdb-bootstrap.ps1' } finally { Remove-Item 'C:\Program Files\ShopDB\bootstrap\shopdb-bootstrap.ps1' -Force -ErrorAction SilentlyContinue }" ``` -It downloads the script, runs it and deletes it again. Do not shorten it back -to `irm ... | iex`: Defender scores that shape as `Trojan:Win32/Commando.A!ml` -and kills the process before anything runs, on a default install with no ASR -rules. Paste it as one line into an elevated **cmd** or **PowerShell** window; -it means the same in both. **Download .ps1** in the same block saves the script -instead, for a PC where pasting a long line is awkward - it carries the same -token, so treat the file the way you treat the command. +It creates an admin-only staging directory, downloads the script into it, runs +it and deletes it again. Do not shorten it back to `irm ... | iex`: Defender +scores that shape as `Trojan:Win32/Commando.A!ml` and kills the process before +anything runs, on a default install with no ASR rules. Paste it as one line +into an elevated **cmd** or **PowerShell** window; it means the same in both. +**Download .ps1** in the same block saves the script instead, for a PC where +pasting a long line is awkward - it carries the same token, so treat the file +the way you treat the command. + +> **Do not use the old `?t=` one-liner that staged into +> `C:\Windows\Temp`.** Any standard user on the bay can create that exact path +> first and wait for a tech to paste the command, which then runs the planted +> file elevated; the token also lands in the web server's access log. The +> command above stages under `C:\Program Files`, locks the directory to +> Administrators and SYSTEM and stops if that fails, refuses to run a staged +> file owned by anyone else, and sends the token in a header. +> +> The `?t=` form is still ACCEPTED, but only so a bay provisioned before this +> change keeps working until its command is re-issued. Every such fetch logs a +> server warning and writes an audit row against the token: +> +> ``` +> bootstrap token presented in the query string by with no admin +> download marker, so this is an un-migrated bay running a command pasted +> before the header form; ... re-issue the one-line install command +> ``` +> +> **`` is 127.0.0.1 on a stock Windows install.** IIS runs the app +> through HttpPlatformHandler over loopback and sets no `X-Forwarded-For` of +> its own; `deploy/windows/web.config` ships the rewrite rule that would add +> one commented out, so waitress has nothing to rewrite `REMOTE_ADDR` from. +> Check before you rely on the address: if these lines all say 127.0.0.1, the +> warning still tells you un-migrated bays EXIST and how many fetches they +> made, but not which bays. Get real addresses by enabling that rule (the +> Windows installer's `-ClientIpSource direct`, which installs URL Rewrite and +> allows `HTTP_X_FORWARDED_FOR` at server level); behind a further reverse +> proxy leave it off and set `TRUSTED_PROXY_COUNT` instead. Without that, the +> usable identifier is the token: each warning has a matching audit row on +> `EndpointBootstrapToken` carrying the 12-character `tokenprefix`, so you can +> at least tell which install round the stale command came from. +> +> Work that list down: mint a new token and hand out the new command. Clicking +> **Download .ps1** yourself does NOT add to it - that link is marked +> `&via=download` and logs at info, precisely so the warnings stay a list of +> bays. Once the list is empty, clear **Accept the token in the URL** in +> Settings -> Endpoints -> Build site agent -> One-line install +> (`plugin.endpoints.bootstrapquerytoken`) to refuse the query form outright. +> That also disables the Download .ps1 link and kills every command still in a +> technician's hands, so do it deliberately and re-issue first. The token is short-lived and only fetches the installer. It carries no shared key and provisions nothing by itself: the package it downloads is built with @@ -324,6 +370,15 @@ window to override). After a successful install it waits up to two minutes for the credential to land and prints `approved and enrolled` if an admin approves it in that window; still pending is a normal outcome, not a failure. +If it stops before any of that with a red **Refusing to run**, nothing was +installed and nothing was downloaded. Two cases, both meaning look at the bay +rather than retry: + +| Exit | Message | What to do | +|---|---|---| +| 6 | could not restrict `C:\Program Files\ShopDB\bootstrap` to administrators | the ACL could not be set - usually the directory already exists with an owner or DACL you did not put there. Inspect it (`icacls`), delete it, run the command again. | +| 5 | the staged bootstrap script is not owned by an administrator | something else wrote the staged file. Treat the bay as suspect, do not force past it. | + Revoke a token from the same block when the install round is done. Tokens list with their prefix, expiry and uses; they are also spent automatically (`usecount` reaching `maxuses`) and expire on their own. diff --git a/plugins/endpoints/frontend/views/AgentKeySettings.vue b/plugins/endpoints/frontend/views/AgentKeySettings.vue index e0ef8500..7ff5785d 100644 --- a/plugins/endpoints/frontend/views/AgentKeySettings.vue +++ b/plugins/endpoints/frontend/views/AgentKeySettings.vue @@ -482,6 +482,31 @@ Settings > Endpoints > Enrollment.

+ + +

{{ querytokenerror }}

+
@@ -764,6 +789,15 @@ const bootstraptokens = ref([]) const bootstrapmessage = ref('') const bootstraperror = ref('') +// plugin.endpoints.bootstrapquerytoken, read and written through the core +// settings API: the endpoints plugin has no settings route of its own for it, +// and the key is declared in get_settings_defaults so the upsert on PUT gets +// the boolean valuetype and the description from that declaration. +const QUERY_TOKEN_SETTING = 'plugin.endpoints.bootstrapquerytoken' +const querytokenallowed = ref(true) +const querytokenbusy = ref(false) +const querytokenerror = ref('') + function agentnextpininvalid() { const pin = agentnextserverpin.value return !!pin && !/^[0-9a-f]{64}$/.test(pin) @@ -863,6 +897,7 @@ onMounted(async () => { } loadEnrollment() loadBootstrapTokens() + loadQueryTokenSetting() try { const response = await api.get('/endpoints/siteagent') const status = response.data.data || {} @@ -878,6 +913,53 @@ onMounted(async () => { } }) +async function loadQueryTokenSetting() { + try { + const response = await api.get(`/settings/${QUERY_TOKEN_SETTING}`) + // The backend reads it as bool(Setting.get(key, True)), so only an + // explicit false is off. A row seeded as boolean comes back as a real + // false; an older row that was created as a string comes back as 'false'. + const value = response.data.data?.value + querytokenallowed.value = !(value === false || value === 'false') + } catch (loadError) { + // 404 = the row was never seeded (plugin installed before the key was + // declared). The backend default is on, so show on. + if (loadError.response?.status !== 404) console.error(loadError) + querytokenallowed.value = true + } +} + +async function onQueryTokenToggle(event) { + const turningOn = event.target.checked + if (!turningOn) { + const answer = prompt( + 'Stop accepting the token in the URL?\n\n' + + 'Every install command already handed out uses ?t= and stops ' + + 'working at once, including the Download .ps1 links. Re-issue them ' + + 'before turning this off.\n\n' + + 'Type OFF to confirm.') + if (!answer || answer.trim().toUpperCase() !== 'OFF') { + event.target.checked = true + return + } + } + querytokenbusy.value = true + querytokenerror.value = '' + try { + // PUT upserts, so this works on a site whose row was never seeded. + await api.put(`/settings/${QUERY_TOKEN_SETTING}`, { value: turningOn }) + querytokenallowed.value = turningOn + } catch (saveError) { + querytokenerror.value = saveError.response?.status === 403 + ? 'Only an administrator can change this setting' + : 'Could not save the setting' + event.target.checked = querytokenallowed.value + console.error(saveError) + } finally { + querytokenbusy.value = false + } +} + async function loadBootstrapTokens() { try { const response = await api.get('/endpoints/siteagent/bootstrap/tokens') diff --git a/plugins/endpoints/frontend/views/EnrollmentSettings.vue b/plugins/endpoints/frontend/views/EnrollmentSettings.vue index d0da4eca..0915b6c0 100644 --- a/plugins/endpoints/frontend/views/EnrollmentSettings.vue +++ b/plugins/endpoints/frontend/views/EnrollmentSettings.vue @@ -229,6 +229,27 @@ on record matches. Anything else waits below for an admin. +