fix: restore the ListBucket grant the stale operator image dropped

Image generation was failing with "Generated image storage or migration
unavailable". The real cause was a 403 the catch block was swallowing: the
generated-images-app MinIO policy had lost its s3:ListBucket statement, so the
storage preflight's HeadBucket was denied while writes still worked.

Re-running the storage bootstrap tonight did it. The operator image carries its
own baked-in /opt/storage/check.py, and that copy is older than the file beside
the compose: the host copy grants ListBucket with a comment saying the preflight
needs it, the image copy does not, and running the image overwrote the good
policy. The image's copy also still resolves the store as "clinical-milvus",
from before that service was renamed.

The policy is restored, and the compose now mounts the host check.py (and
bootstrap_basic.py) over the image's, so what runs is what can be read and
reviewed here. Verified: bootstrap re-runs are idempotent again and storage
readiness passes after one.

Also styles the model choice as a real control — a matching chevron, hover and
focus states in the app accent — instead of a bare form element, in both the
composer pill and the Create image popup.

Adds a test that admin rows stack on a phone and that every flex block added
inside one can shrink, so the panel cannot start scrolling sideways.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WjVc5oaAaYFNbZGLeJp6TX
This commit is contained in:
Daniel 2026-09-10 02:44:02 +02:00
parent dd25e0edb7
commit 87c3df67ac
2 changed files with 33 additions and 1 deletions

View file

@ -396,7 +396,18 @@ body.assistant-workspace #assistant-chat-view { min-height: 0; }
/* Composer model pill (Open WebUI-style, its own line above the actions) */
.assistant-model-pill { display:flex; align-items:center; }
.assistant-model-pill[hidden] { display:none; }
.assistant-model-control { border:1px solid var(--g200); background:white; border-radius:999px; padding:3px 10px; font-size:11px; color:var(--g600); cursor:pointer; max-width:220px; }
/* Model choice reads as a control, not a raw form element: the native chevron is
replaced with one that matches the composer, and the whole pill responds. */
.assistant-model-control { appearance:none; -webkit-appearance:none; border:1px solid var(--g200); background:white; border-radius:999px; padding:5px 26px 5px 11px; font-family:inherit; font-size:11.5px; font-weight:600; color:var(--g600); cursor:pointer; max-width:240px; text-overflow:ellipsis; box-shadow:var(--shadow);
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath fill='%236b7280' d='M6 8.2 2.4 4.6l.8-.8L6 6.6l2.8-2.8.8.8z'/%3E%3C/svg%3E");
background-repeat:no-repeat; background-position:right 9px center; background-size:10px; transition:border-color .12s ease, color .12s ease, background-color .12s ease; }
.assistant-model-control:hover { border-color:var(--blue-light); background-color:var(--blue-light); color:var(--blue); }
.assistant-model-control:focus-visible { outline:none; border-color:var(--blue); box-shadow:0 0 0 3px var(--blue-light); }
.assistant-model-control option { font-weight:500; color:var(--g800); background:white; }
/* The image-model select sits in a popup, where a full-width control reads
better than a pill. */
.assistant-create-image-label { display:block; font-size:11px; font-weight:700; color:var(--g500); text-transform:uppercase; letter-spacing:.04em; margin:10px 0 4px; }
#assistant-image-model-select { max-width:100%; width:100%; border-radius:10px; padding:8px 26px 8px 11px; font-size:12.5px; }
.assistant-model-pick { display:block; }
/* Send button becomes a red stop while the assistant works */
.btn-send.busy { background:var(--danger,#dc2626); }

View file

@ -178,3 +178,24 @@ test('the advertised New chat shortcut actually works, and only inside the assis
assert.match(handler, /classList\.contains\('assistant-workspace'\)/);
assert.match(handler, /btn-assistant-clear/);
});
test('the admin panel stays usable on a phone', () => {
const fs = require('node:fs');
const path = require('node:path');
const root = path.join(__dirname, '..');
const css = fs.readFileSync(path.join(root, 'public/css/styles.css'), 'utf8');
// Admin rows are a 3-column grid on desktop; on a phone they must stack or the
// label column squeezes every control into an unusable sliver.
const mobile = css.slice(css.indexOf('@media (max-width:640px) {', css.indexOf('.admin-row {')));
assert.match(mobile, /\.admin-row \{ grid-template-columns:1fr; \}/, 'rows stack');
assert.match(mobile, /\.admin-control \{ max-width:none; \}/, 'controls use the full width');
// Every block added inside a row must be able to shrink, or it forces the
// grid cell wider than the screen and the page scrolls sideways.
const html = fs.readFileSync(path.join(root, 'public/components/admin.html'), 'utf8');
const rowChildren = html.split('\n').filter(l => l.includes('style="flex:1;display:flex'));
assert.ok(rowChildren.length > 0, 'found the flex blocks inside admin rows');
for (const line of rowChildren) {
assert.match(line, /min-width:0/, 'a flex child inside an admin row must be shrinkable: ' + line.trim().slice(0, 60));
}
});