From 95200b6abfaea9031ad50e0d26df6046eb1681ac Mon Sep 17 00:00:00 2001 From: "Foluso ADEBISI (DevBox Aux)" Date: Thu, 1 Oct 2026 04:36:31 +0100 Subject: [PATCH] fix(cropper): make load failures visible; loud config errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 'Load next queued' did nothing because the running server had been started without its env file (a restart used '-f .env' from tools/, where no .env exists), so the cropper was unconfigured and every action answered 503 — and none of that was visible, because notifications never rendered either: - notifications used cds-toast/cds-toast-item, which the Carbon bundle does not define; a cds-toast-notification element renders nothing standalone in this build. The cropper now uses cds-inline-notification in the fixed toast viewport (renders, themed, close button) like the wizard - an unconfigured deployment now says so on the page (503 'Cropper unavailable') instead of rendering actions that all fail, and logs a startup warning naming the missing credentials - tests: notifications use a registered element; unconfigured page 503 --- tools/cropper.js | 10 ++++++++++ tools/public/assets/cropper/index.html | 7 +++---- tools/public/assets/style.css | 5 +++-- tools/server.js | 16 +++++++++++++++- tools/test/cropper.test.js | 9 +++++++++ tools/test/server.test.js | 8 ++++---- 6 files changed, 44 insertions(+), 11 deletions(-) diff --git a/tools/cropper.js b/tools/cropper.js index 2fdd46a..884c1a1 100644 --- a/tools/cropper.js +++ b/tools/cropper.js @@ -20,6 +20,16 @@ const REMOVEBG_URL = 'https://removebg.itcass.net/api/removebg'; * credential is missing — the router then answers 503 and nothing else in the * app is affected. URLs are hardcoded here; only credentials come from env. */ +/** Credentials the cropper needs (used for the startup warning). */ +export const CROPPER_CREDENTIALS = [ + 'MINIO_ACCESS_KEY', + 'MINIO_SECRET_KEY', + 'AZURE_STORAGE_CONNECTION_STRING', + 'NODERED_HTTP_USERNAME', + 'NODERED_HTTP_PASSWORD', + 'CARVEKIT_API_KEY', +]; + export function buildCropperClients(env = process.env) { const { MINIO_ACCESS_KEY, diff --git a/tools/public/assets/cropper/index.html b/tools/public/assets/cropper/index.html index 7b730cf..c1635b0 100644 --- a/tools/public/assets/cropper/index.html +++ b/tools/public/assets/cropper/index.html @@ -37,10 +37,9 @@
- - - +
diff --git a/tools/public/assets/style.css b/tools/public/assets/style.css index a0643d6..07a4ce4 100644 --- a/tools/public/assets/style.css +++ b/tools/public/assets/style.css @@ -20,7 +20,7 @@ body.app { /* prevent FOUC on web components */ cds-button:not(:defined), cds-tag:not(:defined), cds-tile:not(:defined), cds-clickable-tile:not(:defined), cds-inline-notification:not(:defined), -cds-toast:not(:defined), cds-toast-item:not(:defined), cds-progress-bar:not(:defined) { +cds-progress-bar:not(:defined) { visibility: hidden; } /* Carbon styles the shadow anchor inside tiles, not the host — give the host @@ -183,7 +183,8 @@ cds-button::part(button) { font-size: 14px; } .review-options .form-check { margin-bottom: 0.35rem; } /* ── notifications viewport (cropper toasts) ────────────────────────────── */ -.toast-viewport { position: fixed; top: 3.5rem; right: 1rem; z-index: 1100; width: min(360px, calc(100vw - 2rem)); } +.toast-viewport { position: fixed; top: 3.5rem; right: 1rem; z-index: 1100; width: min(380px, calc(100vw - 2rem)); display: flex; flex-direction: column; gap: 0.5rem; } +.toast-viewport cds-inline-notification:not(:defined) { visibility: hidden; } /* ── footer ─────────────────────────────────────────────────────────────── */ footer.foot { flex-shrink: 0; padding: 1.1rem 0 1.3rem; color: var(--cds-text-helper, #6f6f6f); font-size: 12px; border-top: 1px solid var(--cds-border-subtle, #c6c6c6); } diff --git a/tools/server.js b/tools/server.js index df8e85d..3272fe5 100644 --- a/tools/server.js +++ b/tools/server.js @@ -12,7 +12,7 @@ import fs from 'node:fs'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { loadYrkaSession, extractPbCookieValue } from './yrka-auth.js'; import { permittedDomains, stripPhonePrefix, CACHE_FRESH, DOMAIN_TTL_MS, cropPermission } from './permit.js'; -import { createCropperRouter, buildCropperClients } from './cropper.js'; +import { createCropperRouter, buildCropperClients, CROPPER_CREDENTIALS } from './cropper.js'; // ── Hardcoded configuration ────────────────────────────────────────────────── export const PORT = 4173; @@ -471,6 +471,14 @@ export function createApp(overrides = {}) { next(); }; app.get('/cropper', requirePage, pageCropGate, (req, res, next) => { + if (!cropperClients) { + return errorPage( + res, + 503, + 'Cropper unavailable', + 'The image cropper is not configured on this deployment. Contact your administrator.' + ); + } renderPage( res, 'cropper/index.html', @@ -479,6 +487,12 @@ export function createApp(overrides = {}) { ); }); const cropperClients = cfg.cropperClients !== undefined ? cfg.cropperClients : buildCropperClients(); + if (!cropperClients) { + const missing = CROPPER_CREDENTIALS.filter((name) => !process.env[name]); + console.warn( + `[cropper] not configured — missing ${missing.join(', ')}; /cropper will answer 503` + ); + } if (cropperClients) { app.use( '/cropper', diff --git a/tools/test/cropper.test.js b/tools/test/cropper.test.js index 87f03c7..aa46422 100644 --- a/tools/test/cropper.test.js +++ b/tools/test/cropper.test.js @@ -217,6 +217,10 @@ test('unconfigured deployment: cropper answers 503, other tools unaffected', asy const cookies = await login(base); const res = await fetch(`${base}/cropper/api/baseUrl`, { headers: { cookie: cookies } }); assert.equal(res.status, 503); + // the page says so too, rather than rendering actions that all fail + const page = await fetch(`${base}/cropper`, { headers: { cookie: cookies } }); + assert.equal(page.status, 503); + assert.match(await page.text(), /not configured/i); const landing = await fetch(`${base}/`); assert.equal(landing.status, 200); } finally { @@ -233,6 +237,11 @@ test('crop gate: permitted operator loads the page; denied operator does not', a const html = await page.text(); assert.match(html, /Smart Photo Editor|Image Cropper/); assert.match(html, /window\.__SESSION__ = \{/, 'session injected server-side'); + assert.match(html, /cds-header-nav-item href="\/cropper" aria-current="page"/); + // notifications must use a component the bundle actually defines + assert.match(html, /cds-inline-notification/); + assert.ok(!/cds-toast-item/.test(html), 'cds-toast-item is not a registered element'); + assert.match(html, /cds-side-nav-link href="\/cropper" aria-current="page"/); } finally { await ok.stop(); } diff --git a/tools/test/server.test.js b/tools/test/server.test.js index 0998d9a..7573efc 100644 --- a/tools/test/server.test.js +++ b/tools/test/server.test.js @@ -316,11 +316,11 @@ test('the active tool is marked on both navigation surfaces', async () => { const { base, stop } = await bootApp(); try { const cookies = await login(base); - const cropper = await (await fetch(`${base}/cropper`, { headers: { cookie: cookies } })).text(); - assert.match(cropper, /cds-header-nav-item href="\/cropper" aria-current="page"/); - assert.match(cropper, /cds-side-nav-link href="\/cropper" aria-current="page"/); + const wizard = await (await fetch(`${base}/fingerprint`, { headers: { cookie: cookies } })).text(); + assert.match(wizard, /cds-header-nav-item href="\/fingerprint" aria-current="page"/); + assert.match(wizard, /cds-side-nav-link href="\/fingerprint" aria-current="page"/); assert.ok( - !/cds-header-nav-item href="\/fingerprint" aria-current/.test(cropper), + !/cds-header-nav-item href="\/cropper" aria-current/.test(wizard), 'the other tool is not marked current' ); } finally {