diff --git a/CHANGELOG.md b/CHANGELOG.md index e879fba..1995ef3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/API-REFERENCE.md b/docs/API-REFERENCE.md index f445f12..327c2ae 100644 --- a/docs/API-REFERENCE.md +++ b/docs/API-REFERENCE.md @@ -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/` | `applications.view` | Licensed vendor installers. An anonymous URL is a redistribution channel. | +| `GET /api/warranty/proof/` | `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/` | Serve site branding assets (logo, etc.). | | `GET /api/settings` and `GET /api/settings/` | 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/` | Serve a model image. | +| `GET /api/applications/image/` | 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). | diff --git a/docs/api-inventory.json b/docs/api-inventory.json index d9716f6..0456c24 100644 --- a/docs/api-inventory.json +++ b/docs/api-inventory.json @@ -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//image", + "auth": "jwt + applications.edit", + "params": "multipart/form-data: file= (png/jpg/gif/webp/svg/ico)", + "purpose": "Upload or replace an application's image; sets application.image to the served URL", + "example": "curl -X POST -H 'Authorization: Bearer ' -F file=@logo.png http://localhost:5001/api/applications/5/image" + }, + { + "method": "GET", + "path": "/api/applications/image/", + "auth": "none", + "params": "none", + "purpose": "Serve an uploaded application image. Public: application tiles render before login", + "example": "curl http://localhost:5001/api/applications/image/application-5.png" + }, + { + "method": "DELETE", + "path": "/api/applications//image", + "auth": "jwt + applications.edit", + "params": "none", + "purpose": "Remove an application's image and clear the field", + "example": "curl -X DELETE -H 'Authorization: Bearer ' http://localhost:5001/api/applications/5/image" + }, + { + "method": "POST", + "path": "/api/applications//package", + "auth": "jwt + applications.edit", + "params": "multipart/form-data: file= (exe/msi/msp/zip/7z/cab/iso/appx/msix/ps1/bat/txt/pdf), max 500MB", + "purpose": "Upload or replace the installer; sets installpath to the download URL", + "example": "curl -X POST -H 'Authorization: Bearer ' -F file=@setup.msi http://localhost:5001/api/applications/5/package" + }, + { + "method": "GET", + "path": "/api/applications/package/", + "auth": "jwt + applications.view", + "params": "none", + "purpose": "Download an uploaded installer as an attachment. Authenticated: licensed vendor software", + "example": "curl -OJ -H 'Authorization: Bearer ' http://localhost:5001/api/applications/package/application-5.msi" + }, + { + "method": "DELETE", + "path": "/api/applications//package", + "auth": "jwt + applications.edit", + "params": "none", + "purpose": "Remove an uploaded installer; clears installpath only when it pointed at the upload", + "example": "curl -X DELETE -H 'Authorization: Bearer ' http://localhost:5001/api/applications/5/package" } ] }, @@ -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//proof", + "auth": "jwt + warranty.edit", + "params": "multipart/form-data: file= (pdf/png/jpg/gif/webp/tif/msg/eml/doc/docx/xls/xlsx), max 25MB", + "purpose": "Upload or replace the proof-of-cover document (invoice, certificate)", + "example": "curl -X POST -H 'Authorization: Bearer ' -F file=@invoice.pdf http://localhost:5001/api/warranty/12/proof" + }, + { + "method": "GET", + "path": "/api/warranty/proof/", + "auth": "jwt + warranty.view", + "params": "none", + "purpose": "Download a proof document under the vendor's original filename. Authenticated: carries pricing and a service tag", + "example": "curl -OJ -H 'Authorization: Bearer ' http://localhost:5001/api/warranty/proof/warranty-12.pdf" + }, + { + "method": "DELETE", + "path": "/api/warranty//proof", + "auth": "jwt + warranty.edit", + "params": "none", + "purpose": "Remove a proof document and clear both columns", + "example": "curl -X DELETE -H 'Authorization: Bearer ' http://localhost:5001/api/warranty/12/proof" } ] } diff --git a/docs/openapi.json b/docs/openapi.json index 5bf29b0..cd4f381 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -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= (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 (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 ' 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= (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 (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 ' 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 ' 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= (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 (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 ' 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 ' http://localhost:5001/api/warranty/proof/warranty-12.pdf\n```", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Success (success_response envelope)" + } + } + } } } } \ No newline at end of file