fix(cropper): make load failures visible; loud config errors
'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
This commit is contained in:
@@ -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
|
* 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.
|
* 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) {
|
export function buildCropperClients(env = process.env) {
|
||||||
const {
|
const {
|
||||||
MINIO_ACCESS_KEY,
|
MINIO_ACCESS_KEY,
|
||||||
|
|||||||
@@ -37,10 +37,9 @@
|
|||||||
|
|
||||||
<!-- notifications -->
|
<!-- notifications -->
|
||||||
<div class="toast-viewport" aria-live="polite">
|
<div class="toast-viewport" aria-live="polite">
|
||||||
<cds-toast>
|
<cds-inline-notification v-for="t in toasts" v-bind:key="t.id" v-bind:kind="t.kind"
|
||||||
<cds-toast-item v-for="t in toasts" v-bind:key="t.id" v-bind:kind="t.kind"
|
v-bind:title="t.title" v-bind:subtitle="t.caption" low-contrast
|
||||||
v-bind:title="t.title" v-bind:caption="t.caption" timeout="6000"></cds-toast-item>
|
v-on:cds-notification-closed="removeToast(t.id)"></cds-inline-notification>
|
||||||
</cds-toast>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 1 · load -->
|
<!-- 1 · load -->
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ body.app {
|
|||||||
/* prevent FOUC on web components */
|
/* prevent FOUC on web components */
|
||||||
cds-button:not(:defined), cds-tag:not(:defined), cds-tile:not(:defined),
|
cds-button:not(:defined), cds-tag:not(:defined), cds-tile:not(:defined),
|
||||||
cds-clickable-tile:not(:defined), cds-inline-notification: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;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
/* Carbon styles the shadow anchor inside tiles, not the host — give the host
|
/* 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; }
|
.review-options .form-check { margin-bottom: 0.35rem; }
|
||||||
|
|
||||||
/* ── notifications viewport (cropper toasts) ────────────────────────────── */
|
/* ── 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 ─────────────────────────────────────────────────────────────── */
|
||||||
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); }
|
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); }
|
||||||
|
|||||||
+15
-1
@@ -12,7 +12,7 @@ import fs from 'node:fs';
|
|||||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||||
import { loadYrkaSession, extractPbCookieValue } from './yrka-auth.js';
|
import { loadYrkaSession, extractPbCookieValue } from './yrka-auth.js';
|
||||||
import { permittedDomains, stripPhonePrefix, CACHE_FRESH, DOMAIN_TTL_MS, cropPermission } from './permit.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 ──────────────────────────────────────────────────
|
// ── Hardcoded configuration ──────────────────────────────────────────────────
|
||||||
export const PORT = 4173;
|
export const PORT = 4173;
|
||||||
@@ -471,6 +471,14 @@ export function createApp(overrides = {}) {
|
|||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
app.get('/cropper', requirePage, pageCropGate, (req, res, 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(
|
renderPage(
|
||||||
res,
|
res,
|
||||||
'cropper/index.html',
|
'cropper/index.html',
|
||||||
@@ -479,6 +487,12 @@ export function createApp(overrides = {}) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
const cropperClients = cfg.cropperClients !== undefined ? cfg.cropperClients : buildCropperClients();
|
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) {
|
if (cropperClients) {
|
||||||
app.use(
|
app.use(
|
||||||
'/cropper',
|
'/cropper',
|
||||||
|
|||||||
@@ -217,6 +217,10 @@ test('unconfigured deployment: cropper answers 503, other tools unaffected', asy
|
|||||||
const cookies = await login(base);
|
const cookies = await login(base);
|
||||||
const res = await fetch(`${base}/cropper/api/baseUrl`, { headers: { cookie: cookies } });
|
const res = await fetch(`${base}/cropper/api/baseUrl`, { headers: { cookie: cookies } });
|
||||||
assert.equal(res.status, 503);
|
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}/`);
|
const landing = await fetch(`${base}/`);
|
||||||
assert.equal(landing.status, 200);
|
assert.equal(landing.status, 200);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -233,6 +237,11 @@ test('crop gate: permitted operator loads the page; denied operator does not', a
|
|||||||
const html = await page.text();
|
const html = await page.text();
|
||||||
assert.match(html, /Smart Photo Editor|Image Cropper/);
|
assert.match(html, /Smart Photo Editor|Image Cropper/);
|
||||||
assert.match(html, /window\.__SESSION__ = \{/, 'session injected server-side');
|
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 {
|
} finally {
|
||||||
await ok.stop();
|
await ok.stop();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -316,11 +316,11 @@ test('the active tool is marked on both navigation surfaces', async () => {
|
|||||||
const { base, stop } = await bootApp();
|
const { base, stop } = await bootApp();
|
||||||
try {
|
try {
|
||||||
const cookies = await login(base);
|
const cookies = await login(base);
|
||||||
const cropper = await (await fetch(`${base}/cropper`, { headers: { cookie: cookies } })).text();
|
const wizard = await (await fetch(`${base}/fingerprint`, { headers: { cookie: cookies } })).text();
|
||||||
assert.match(cropper, /cds-header-nav-item href="\/cropper" aria-current="page"/);
|
assert.match(wizard, /cds-header-nav-item href="\/fingerprint" aria-current="page"/);
|
||||||
assert.match(cropper, /cds-side-nav-link href="\/cropper" aria-current="page"/);
|
assert.match(wizard, /cds-side-nav-link href="\/fingerprint" aria-current="page"/);
|
||||||
assert.ok(
|
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'
|
'the other tool is not marked current'
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user