phase: 132_image_upload_feedback
Build and Push Containers / build-and-push-app (push) Successful in 2m25s
Build and Push Containers / build-and-push-db (push) Successful in 17s

All verification complete. Final report:

**Phase 132 (image upload feedback) — final verification pass: 100% green**

- **Verified** all 3 tasks' implementations in the working tree: `index.html` status part (hidden-by-default, arc + "Uploading image…", pinned order), `app.js` `enterUploadState`/`exitUploadState` (entered after `attachUpload` guard pre-await, exited on BOTH resolution paths, `setUiState` untouched, idle "Send" restore byte-identical), `styles.css` `.is-uploading` rules (house `spin` keyframes, reduced-motion block, existing `:disabled` Send language), unit pins, E2E suite with route-delayed upload window.
- **Completion criteria** — (1) mid-flight spinner + "Uploading image…", remove + Send disabled with "Uploading…": ✅ E2E-asserted; (2) second Send press = no second upload (request count exactly 1): ✅; (3) success clears + turn proceeds (bubble carries image); failure clears + banner + text/attachment remain: ✅; (4) `uv run pytest tests/e2e/test_image_upload_feedback.py -v --no-cov`: ✅ 3 passed (18.5 s, in isolation); (5) no behavior change in completed phases: ✅ (full suite green, A5 path-never-bytes re-pinned in E2E).
- **Full suite**: `uv run pytest --cov=app --cov-report=term-missing` → **3108 passed**, `app/` coverage **99%** (>90% gate); `uv run ruff check .` → clean; `uv run pyright` → 0 errors.
- **Documentation**: README.md fully reviewed (setup, pages, all `.env.example` vars + every default vs `app/config.py`, tools incl. phase-131 `read(offset,limit)`, image/PDF/sync/deploy sections) and AGENTS.md fully reviewed (rules, quick-reference commands, gitignore/gpg claims) — **verified consistent; no changes needed**. Phase-132 README bullet already in the working tree; CSS `:root` contrast-note corrections (15.3:1 / 8.2:1) re-verified mathematically correct (comments only).
- **Decisions**: none — all task work was already implemented and correct; no fixes required.
- **Next pending phase**: `133_pdf_screenshot_fallback`.
This commit is contained in:
2026-09-28 13:45:31 -04:00
parent 101156bada
commit 4cb85e5baa
22 changed files with 1696 additions and 2 deletions
+59
View File
@@ -314,6 +314,10 @@ const attachBtn = document.querySelector("#attach-btn");
const attachFile = document.querySelector("#attach-file");
const attachPreview = document.querySelector("#attach-preview");
const attachRemove = document.querySelector("#attach-remove");
// Phase 132 (task 01, TODO L8): the strip's uploading status part —
// ships hidden; enterUploadState / exitUploadState (below) are its
// sole `hidden` writers (shown iff the image upload is in flight).
const attachStatus = document.querySelector(".attach-preview-status");
const banner = document.querySelector("#kb-banner");
const bannerText = document.querySelector("#kb-banner-text");
const versionEl = document.querySelector("#app-version");
@@ -2757,6 +2761,49 @@ function clearAttachedImage() {
if (attachPreview) attachPreview.hidden = true;
}
/* Phase 132 (task 01, TODO L8; A5, owner-confirmed 2026-09-27): the
* PRE-turn uploading state — the visible proof that the attached
* image is moving while POST /api/chat-images is in flight. The
* affordance is BOTH halves at once: the preview strip shows the
* spinner + "Uploading image…" (the status part unhidden, the
* .is-uploading class for task 02's CSS) with its remove X disabled
* (the attachment is mid-flight — it can't be removed out from under
* the upload), AND the Send button is DISABLED with the "Uploading…"
* label — the user-facing half of the attachUpload double-fire guard
* (a second press does nothing: the top-of-handleSend
* `sendBtn.disabled` guard AND the `attachUpload` guard both no-op).
* exitUploadState is the mirror image: the strip returns to its
* normal preview (thumbnail + filename stay visible), the remove X
* re-enables, and the button restores EXACTLY the idle look —
* enabled + "Send" (byte-identical to setUiState's idle label),
* restored BEFORE the success path calls runTurn (whose
* setUiState("thinking") then takes the button over as usual) and
* before the failure path returns (banner shown, input text intact,
* the attachment still attached — the user can retry or remove).
* setUiState / uiState / the turn loader are deliberately UNTOUCHED:
* the upload is PRE-turn (uiState stays idle; the turn's
* thinking/streaming feedback starts only when runTurn does).
* Both are idempotent and null-guarded on the attach family (the
* house pattern — a New chat mid-upload hides the strip; the class
* on a hidden element is harmless and still cleared at resolution);
* the send refs are unconditional (the composer is required for the
* chat view to exist at all). */
function enterUploadState() {
if (attachPreview) attachPreview.classList.add("is-uploading");
if (attachStatus) attachStatus.hidden = false;
if (attachRemove) attachRemove.disabled = true;
sendBtn.disabled = true;
sendLabel.textContent = "Uploading…";
}
function exitUploadState() {
if (attachPreview) attachPreview.classList.remove("is-uploading");
if (attachStatus) attachStatus.hidden = true;
if (attachRemove) attachRemove.disabled = false;
sendBtn.disabled = false;
sendLabel.textContent = "Send";
}
async function handleSend(e) {
e.preventDefault();
// Phase 48: while a turn is in flight the Send button IS the Stop
@@ -2787,8 +2834,20 @@ async function handleSend(e) {
if (attachedImage) {
if (attachUpload) return; // a second submit mid-upload: the first owns the send (never two uploads, never two turns)
attachUpload = true;
// Phase 132 (task 01, TODO L8): the uploading state is entered
// exactly ONCE per upload — after the guard, before the await —
// so the gap between the press and the resolution is never
// frozen-looking (the strip's spinner + "Uploading image…", the
// disabled "Uploading…" Send).
enterUploadState();
const path = await uploadAttachedImage(attachedImage.file);
attachUpload = false; // the helper never throws (every failure path is a banner + null)
// Phase 132: exited on BOTH resolution paths — the helper never
// throws, so this line is reached on success AND failure. Before
// the success path continues to runTurn (whose setUiState takes
// the button over) and before the failure path returns below
// (banner + editable composer + the attachment for the retry).
exitUploadState();
if (path === null) return; // A8: the send is blocked — the question stays
image = {
path, // the record + the /api/chat body (A5: the path, never base64)