Document the upload, proof and forecast endpoints
Some checks failed
CI / backend (push) Failing after 9s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 10s
CI / migrations-mysql (push) Failing after 7s

Ten routes shipped over the last few commits without reaching the inventory,
which is the file everything else is generated from - openapi.json, the /api/docs
browser, and the per-site build. An endpoint absent from it is invisible to
anyone integrating against the API even though it answers requests.

The auth notes matter more than the paths. API-REFERENCE tells a deployment
reviewer that essentially every GET is optional-auth; two of these are not, and
an exception buried in a table of 372 operations is one nobody reads. The
installer download and the warranty proof are called out with why: one is
licensed vendor software, the other carries pricing and a service tag. The
application image is listed with the fully public endpoints, since tiles render
before login.
This commit is contained in:
cproudlock
2026-08-12 12:06:25 -04:00
parent ea35a134fe
commit a64796f060
4 changed files with 302 additions and 1 deletions

View File

@@ -12,6 +12,34 @@ ADR-007 and ADR-002.
### Added
- Applications accept an uploaded image and an uploaded installer, rather than
only a path typed into a text box. The path field kept working only while
someone maintained a share by hand, and a link to a file nobody could still
find was indistinguishable from a link to one that worked. The installer
download is gated on `applications.view` because it is licensed vendor
software; the image is public, since tiles render before login.
- Warranty records accept an uploaded proof document - the invoice or
certificate that actually settles a claim. The documentation path stays as it
was: it points at a vendor portal or a share, which an upload cannot replace.
Downloads require `warranty.view`, since a proof carries pricing and a service
tag, and it comes back under the vendor's own filename so it is recognisable
in a support ticket.
- Toner forecast report: days until each cartridge runs out, soonest first,
from Zabbix level history. Ordering by days left rather than by level is the
point - a cartridge at 60% draining fast needs ordering before one sitting at
8% that has not moved in a month. It also counts how many cartridges each
printer has been through in the window, which is the number a purchasing
conversation asks for. Printers with no honest estimate are listed
separately with the reason (too little history, gauge has not moved, replaced
recently) instead of being sorted in as zero or as safe, and an unreachable
Zabbix says so rather than rendering an empty table that reads as "nothing is
due".
- Fiscal week under the site name in the sidebar and on the shop-floor board,
as the classic ASP site showed it. People quote a week number across both
sites, so the arithmetic is a port of the old one (ISO 8601: week 1 contains
4 January, the week's Thursday decides the year) rather than a fresh
interpretation.
- Backup history distinguishes CAPTURED from LAST VERIFIED. Captured is when a
configuration was written; verified is when a PC last posted it and the hash
still matched. They differ by design, because an unchanged config writes no

View File

@@ -96,6 +96,18 @@ Two auth patterns dominate the reads:
Every mutating endpoint (POST / PUT / PATCH / DELETE) requires a JWT and is
gated by `require_role` or `require_permission`; none are public.
Two GETs deliberately break the optional-auth convention and require a
permission, because what they return is not asset metadata but a file that
carries value or liability of its own:
| Endpoint | Requires | Why |
|---|---|---|
| `GET /api/applications/package/<filename>` | `applications.view` | Licensed vendor installers. An anonymous URL is a redistribution channel. |
| `GET /api/warranty/proof/<filename>` | `warranty.view` | Invoices and certificates carry pricing and a service tag. |
Their sibling upload/delete routes are `applications.edit` / `warranty.edit`
like any other mutation.
### Fully public endpoints (auth = none)
| Endpoint | Purpose |
@@ -107,6 +119,7 @@ gated by `require_role` or `require_permission`; none are public.
| `GET /api/settings/branding/<filename>` | Serve site branding assets (logo, etc.). |
| `GET /api/settings` and `GET /api/settings/<key>` | Read-only, and only the public allowlist: the `branding` and `map` categories, a few named site keys, plus any key a plugin declares `public` in `get_settings_defaults` (e.g. `printedparts_label_prefix`, which the logged-out parts kiosk renders). Every other key answers 404 to an anonymous caller. |
| `GET /api/models/image/<filename>` | Serve a model image. |
| `GET /api/applications/image/<filename>` | Serve an application image (tiles render before login). |
| `GET /api/dashboard/navigation` | Public navigation tree. |
| `GET /api/dashboard/health` | Liveness / health probe. |
| `GET /api/plugins/enabled` | List enabled plugins (no claims used). |

View File

@@ -1437,6 +1437,54 @@
"auth": "permission:applications.edit",
"params": "body: appversionid",
"example": "curl -X PUT http://localhost:5001/api/applications/machines/8/15 -H \"Authorization: Bearer $TOK\" -H 'Content-Type: application/json' -d '{\"appversionid\":3}'"
},
{
"method": "POST",
"path": "/api/applications/<app_id>/image",
"auth": "jwt + applications.edit",
"params": "multipart/form-data: file=<image> (png/jpg/gif/webp/svg/ico)",
"purpose": "Upload or replace an application's image; sets application.image to the served URL",
"example": "curl -X POST -H 'Authorization: Bearer <token>' -F file=@logo.png http://localhost:5001/api/applications/5/image"
},
{
"method": "GET",
"path": "/api/applications/image/<filename>",
"auth": "none",
"params": "none",
"purpose": "Serve an uploaded application image. Public: application tiles render before login",
"example": "curl http://localhost:5001/api/applications/image/application-5.png"
},
{
"method": "DELETE",
"path": "/api/applications/<app_id>/image",
"auth": "jwt + applications.edit",
"params": "none",
"purpose": "Remove an application's image and clear the field",
"example": "curl -X DELETE -H 'Authorization: Bearer <token>' http://localhost:5001/api/applications/5/image"
},
{
"method": "POST",
"path": "/api/applications/<app_id>/package",
"auth": "jwt + applications.edit",
"params": "multipart/form-data: file=<installer> (exe/msi/msp/zip/7z/cab/iso/appx/msix/ps1/bat/txt/pdf), max 500MB",
"purpose": "Upload or replace the installer; sets installpath to the download URL",
"example": "curl -X POST -H 'Authorization: Bearer <token>' -F file=@setup.msi http://localhost:5001/api/applications/5/package"
},
{
"method": "GET",
"path": "/api/applications/package/<filename>",
"auth": "jwt + applications.view",
"params": "none",
"purpose": "Download an uploaded installer as an attachment. Authenticated: licensed vendor software",
"example": "curl -OJ -H 'Authorization: Bearer <token>' http://localhost:5001/api/applications/package/application-5.msi"
},
{
"method": "DELETE",
"path": "/api/applications/<app_id>/package",
"auth": "jwt + applications.edit",
"params": "none",
"purpose": "Remove an uploaded installer; clears installpath only when it pointed at the upload",
"example": "curl -X DELETE -H 'Authorization: Bearer <token>' http://localhost:5001/api/applications/5/package"
}
]
},
@@ -2895,6 +2943,14 @@
"params": "path: modelsupplyid",
"purpose": "Hard-delete a model supply mapping.",
"example": "curl -X DELETE -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/printers/supplies/44"
},
{
"method": "GET",
"path": "/api/printers/supplies/forecast",
"auth": "jwt-optional",
"params": "days (1-365, default 90)",
"purpose": "Days-to-empty per printer from Zabbix history, soonest first, with cartridge replacement counts; printers with no honest estimate are returned separately with the reason. available=false when Zabbix is unreachable",
"example": "curl 'http://localhost:5001/api/printers/supplies/forecast?days=90'"
}
]
},
@@ -2972,6 +3028,30 @@
"auth": "jwt-optional",
"params": "none",
"example": "curl -H \"Authorization: Bearer $TOKEN\" http://localhost:5001/api/warranty/report"
},
{
"method": "POST",
"path": "/api/warranty/<warrantyid>/proof",
"auth": "jwt + warranty.edit",
"params": "multipart/form-data: file=<document> (pdf/png/jpg/gif/webp/tif/msg/eml/doc/docx/xls/xlsx), max 25MB",
"purpose": "Upload or replace the proof-of-cover document (invoice, certificate)",
"example": "curl -X POST -H 'Authorization: Bearer <token>' -F file=@invoice.pdf http://localhost:5001/api/warranty/12/proof"
},
{
"method": "GET",
"path": "/api/warranty/proof/<filename>",
"auth": "jwt + warranty.view",
"params": "none",
"purpose": "Download a proof document under the vendor's original filename. Authenticated: carries pricing and a service tag",
"example": "curl -OJ -H 'Authorization: Bearer <token>' http://localhost:5001/api/warranty/proof/warranty-12.pdf"
},
{
"method": "DELETE",
"path": "/api/warranty/<warrantyid>/proof",
"auth": "jwt + warranty.edit",
"params": "none",
"purpose": "Remove a proof document and clear both columns",
"example": "curl -X DELETE -H 'Authorization: Bearer <token>' http://localhost:5001/api/warranty/12/proof"
}
]
}

View File

@@ -2,7 +2,7 @@
"openapi": "3.1.0",
"info": {
"title": "ShopDB Flask API",
"version": "0.8.1",
"version": "0.9.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."
},
"servers": [
@@ -3271,6 +3271,112 @@
}
}
},
"/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=<image> (png/jpg/gif/webp/svg/ico)\n\n**Example:**\n```\ncurl -X POST -H 'Authorization: Bearer <token>' -F file=@logo.png http://localhost:5001/api/applications/5/image\n```",
"security": [
{
"bearerAuth": []
}
],
"responses": {
"200": {
"description": "Success (success_response envelope)"
}
}
},
"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 <token>' http://localhost:5001/api/applications/5/image\n```",
"security": [
{
"bearerAuth": []
}
],
"responses": {
"200": {
"description": "Success (success_response envelope)"
}
}
}
},
"/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 (success_response envelope)"
}
}
}
},
"/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=<installer> (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 <token>' -F file=@setup.msi http://localhost:5001/api/applications/5/package\n```",
"security": [
{
"bearerAuth": []
}
],
"responses": {
"200": {
"description": "Success (success_response envelope)"
}
}
},
"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 <token>' http://localhost:5001/api/applications/5/package\n```",
"security": [
{
"bearerAuth": []
}
],
"responses": {
"200": {
"description": "Success (success_response envelope)"
}
}
}
},
"/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 <token>' http://localhost:5001/api/applications/package/application-5.msi\n```",
"security": [
{
"bearerAuth": []
}
],
"responses": {
"200": {
"description": "Success (success_response envelope)"
}
}
}
},
"/api/computers/types": {
"get": {
"tags": [
@@ -6451,6 +6557,25 @@
}
}
},
"/api/printers/supplies/forecast": {
"get": {
"tags": [
"plugin-printers"
],
"summary": "Days-to-empty per printer from Zabbix history, soonest first, with cartridge replacement counts; printers with no honest",
"description": "Days-to-empty per printer from Zabbix history, soonest first, with cartridge replacement counts; printers with no honest estimate are returned 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 (success_response envelope)"
}
}
}
},
"/api/warranty": {
"get": {
"tags": [
@@ -6615,6 +6740,61 @@
}
}
}
},
"/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=<document> (pdf/png/jpg/gif/webp/tif/msg/eml/doc/docx/xls/xlsx), max 25MB\n\n**Example:**\n```\ncurl -X POST -H 'Authorization: Bearer <token>' -F file=@invoice.pdf http://localhost:5001/api/warranty/12/proof\n```",
"security": [
{
"bearerAuth": []
}
],
"responses": {
"200": {
"description": "Success (success_response envelope)"
}
}
},
"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 <token>' http://localhost:5001/api/warranty/12/proof\n```",
"security": [
{
"bearerAuth": []
}
],
"responses": {
"200": {
"description": "Success (success_response envelope)"
}
}
}
},
"/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 <token>' http://localhost:5001/api/warranty/proof/warranty-12.pdf\n```",
"security": [
{
"bearerAuth": []
}
],
"responses": {
"200": {
"description": "Success (success_response envelope)"
}
}
}
}
}
}