diff --git a/.github/workflows/publish-images.yml b/.github/workflows/publish-images.yml index 95aa54ca..416e5bf1 100644 --- a/.github/workflows/publish-images.yml +++ b/.github/workflows/publish-images.yml @@ -76,18 +76,18 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} -# Connect to a persistent, out-of-cluster BuildKit deployment instead of -# spinning up a builder against a local Docker daemon. The arc-runner-set -# pods have no Docker daemon (no /var/run/docker.sock) and cannot run a -# privileged DinD sidecar under cluster policy, so we point Buildx at a -# remote buildkitd Service. The endpoint is provided as a repo/org secret -# (e.g. tcp://buildkit.buildkit.svc.cluster.local:1234), so it is not -# hardcoded here. See deploy/ci/buildkit-remote.yaml for the deployment. -- name: Set up Buildx (remote BuildKit) - uses: docker/setup-buildx-action@v3 - with: - driver: remote - endpoint: ${{ secrets.BUILDKIT_ENDPOINT }} + # Connect to a persistent, out-of-cluster BuildKit deployment instead of + # spinning up a builder against a local Docker daemon. The arc-runner-set + # pods have no Docker daemon (no /var/run/docker.sock) and cannot run a + # privileged DinD sidecar under cluster policy, so we point Buildx at a + # remote buildkitd Service. The endpoint is provided as a repo/org secret + # (e.g. tcp://buildkit.buildkit.svc.cluster.local:1234), so it is not + # hardcoded here. See deploy/ci/buildkit-remote.yaml for the deployment. + - name: Set up Buildx (remote BuildKit) + uses: docker/setup-buildx-action@v3 + with: + driver: remote + endpoint: ${{ secrets.BUILDKIT_ENDPOINT }} - name: Build and push channel image uses: docker/build-push-action@v6 diff --git a/docs/openapi.json b/docs/openapi.json index 180739c7..b6bc374d 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -1973,6 +1973,194 @@ } } }, + "/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": [ @@ -23905,6 +24093,307 @@ } } } + }, + "/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)" + } + } + } + } + } } } } \ No newline at end of file diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index 304ece15..858814e5 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -11,6 +11,54 @@ const api = axios.create({ }) // Add auth token and normalize pagination params +// Kiosk device tokens. The kiosk routes are the only ones that write without a +// login, so each is additionally gated on a token an administrator provisioned +// into THAT screen's browser - see the kiosk views, which claim it from a +// ?token= URL once and strip it from the address bar. A token served inside the +// page would be readable by anyone who could load the page, so it is per-device +// and never shipped with the bundle. +// +// One entry per kiosk: the two are separate screens in separate rooms and a +// shared token would mean provisioning one grants the other. +export const KIOSK_TOKEN_KEYS = { + '/printedparts/kiosk/': 'printedparts-kiosk-token', + '/usb/kiosk/': 'usb-kiosk-token', +} + +function kioskTokenFor(url) { + const path = url || '' + for (const [prefix, storageKey] of Object.entries(KIOSK_TOKEN_KEYS)) { + if (path.includes(prefix)) { + try { return localStorage.getItem(storageKey) || '' } catch { return '' } + } + } + return '' +} + +// Claim a kiosk's device token, once, from the URL an admin used to set the +// screen up. Kept in THIS browser and stripped from the address bar straight +// away, so it does not sit there for a shoulder to read or travel in a bookmark +// or a screenshot. +export function claimKioskToken(storageKey) { + try { + const url = new URL(window.location.href) + const token = (url.searchParams.get('token') || '').trim() + if (!token) return + localStorage.setItem(storageKey, token) + url.searchParams.delete('token') + window.history.replaceState({}, '', url.pathname + url.search + url.hash) + } catch { + // A browser refusing localStorage cannot be provisioned. Nothing to + // recover; the routes 404 and the operator sees the standard error. + } +} + +api.interceptors.request.use(config => { + const token = kioskTokenFor(config.url) + if (token) config.headers['X-Kiosk-Token'] = token + return config +}) + api.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { @@ -1215,14 +1263,20 @@ export const settingsApi = { headers: { 'Content-Type': 'multipart/form-data' } }) }, - wincCommit(file, acknowledge = []) { + wincCommit(file, acknowledge = [], planhash = '') { // Apply a WINC plan reversibly. acknowledge is the list of needs-create - // group names (e.g. ['businessunit']) the admin confirmed. + // group names (e.g. ['businessunit']) the admin confirmed. planhash is the + // one the PREVIEW returned - the server rejects a commit without it, + // because otherwise "approved" means only that these group names were + // ticked, and the plan applied could be a different one. const form = new FormData() form.append('files', file) if (acknowledge.length) { form.append('acknowledge', acknowledge.join(',')) } + if (planhash) { + form.append('planhash', planhash) + } return api.post('/settings/asset-import/winc/commit', form, { headers: { 'Content-Type': 'multipart/form-data' } }) diff --git a/frontend/src/assets/style.css b/frontend/src/assets/style.css index 18bd2c44..30b8d035 100644 --- a/frontend/src/assets/style.css +++ b/frontend/src/assets/style.css @@ -1323,13 +1323,33 @@ td.actions .btn + .btn { gap: 0.75rem; margin-top: 0.4rem; } +/* A branding preview has to show a logo whose colour we do not control. The + shipped GE mark is fill="white" because it lives on the dark sidebar, so on a + plain white tile it rendered white-on-white and read as a failed load. A + checker ground makes any logo visible - light, dark or transparent - and is + the usual convention for previewing artwork with alpha. */ .map-thumb { height: 40px; + min-width: 40px; border: 1px solid var(--border); border-radius: 4px; - background: #fff; + /* MID-TONE, not white and not near-white. The tile has to show artwork whose + colour we do not control: the shipped GE mark is fill="white" for the dark + sidebar, the monogram is fill="black". A light ground hides the first, a + dark ground hides the second, so the checker sits either side of mid grey + and both read against it. */ + background-color: #9aa1ad; + background-image: + linear-gradient(45deg, #6f7783 25%, transparent 25%, transparent 75%, #6f7783 75%), + linear-gradient(45deg, #6f7783 25%, transparent 25%, transparent 75%, #6f7783 75%); + background-size: 12px 12px; + background-position: 0 0, 6px 6px; + padding: 2px; +} +.map-thumb-dark { + background-color: #222; + background-image: none; } -.map-thumb-dark { background: #222; } .settings-success { margin-top: 1rem; padding: 0.75rem; diff --git a/frontend/src/views/settings/BrandingSettings.vue b/frontend/src/views/settings/BrandingSettings.vue index bf455bc2..dcb6294b 100644 --- a/frontend/src/views/settings/BrandingSettings.vue +++ b/frontend/src/views/settings/BrandingSettings.vue @@ -24,7 +24,7 @@ >