From 4cb85e5baa992bee69f1e263d140d65af0ea7114 Mon Sep 17 00:00:00 2001 From: ducoterra Date: Mon, 28 Sep 2026 13:45:31 -0400 Subject: [PATCH] phase: 132_image_upload_feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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`. --- .../132_image_upload_feedback/00_phase.md | 0 .../01_upload_state.md | 0 .../02_css_upload_state.md | 0 .../03_e2e_upload_feedback.md | 0 ...132_image_upload_feedback__00_phase.a1.err | 0 .../132_image_upload_feedback__00_phase.a1.md | 10 + ...mage_upload_feedback__00_phase.a1.validate | 115 ++++ ...ge_upload_feedback__01_upload_state.a1.err | 0 ...age_upload_feedback__01_upload_state.a1.md | 17 + ...load_feedback__01_upload_state.a1.validate | 115 ++++ ...pload_feedback__02_css_upload_state.a1.err | 0 ...upload_feedback__02_css_upload_state.a1.md | 19 + ..._feedback__02_css_upload_state.a1.validate | 115 ++++ ...ad_feedback__03_e2e_upload_feedback.a1.err | 0 ...oad_feedback__03_e2e_upload_feedback.a1.md | 17 + ...edback__03_e2e_upload_feedback.a1.validate | 115 ++++ README.md | 7 + frontend/assets/app.js | 59 ++ frontend/assets/styles.css | 54 +- frontend/index.html | 16 + tests/e2e/test_image_upload_feedback.py | 602 ++++++++++++++++++ tests/unit/test_chat_image_questions.py | 437 +++++++++++++ 22 files changed, 1696 insertions(+), 2 deletions(-) rename .agents/phases/{todo => complete}/132_image_upload_feedback/00_phase.md (100%) rename .agents/phases/{todo => complete}/132_image_upload_feedback/01_upload_state.md (100%) rename .agents/phases/{todo => complete}/132_image_upload_feedback/02_css_upload_state.md (100%) rename .agents/phases/{todo => complete}/132_image_upload_feedback/03_e2e_upload_feedback.md (100%) create mode 100644 .agents/reports/132_image_upload_feedback/132_image_upload_feedback__00_phase.a1.err create mode 100644 .agents/reports/132_image_upload_feedback/132_image_upload_feedback__00_phase.a1.md create mode 100644 .agents/reports/132_image_upload_feedback/132_image_upload_feedback__00_phase.a1.validate create mode 100644 .agents/reports/132_image_upload_feedback/132_image_upload_feedback__01_upload_state.a1.err create mode 100644 .agents/reports/132_image_upload_feedback/132_image_upload_feedback__01_upload_state.a1.md create mode 100644 .agents/reports/132_image_upload_feedback/132_image_upload_feedback__01_upload_state.a1.validate create mode 100644 .agents/reports/132_image_upload_feedback/132_image_upload_feedback__02_css_upload_state.a1.err create mode 100644 .agents/reports/132_image_upload_feedback/132_image_upload_feedback__02_css_upload_state.a1.md create mode 100644 .agents/reports/132_image_upload_feedback/132_image_upload_feedback__02_css_upload_state.a1.validate create mode 100644 .agents/reports/132_image_upload_feedback/132_image_upload_feedback__03_e2e_upload_feedback.a1.err create mode 100644 .agents/reports/132_image_upload_feedback/132_image_upload_feedback__03_e2e_upload_feedback.a1.md create mode 100644 .agents/reports/132_image_upload_feedback/132_image_upload_feedback__03_e2e_upload_feedback.a1.validate create mode 100644 tests/e2e/test_image_upload_feedback.py diff --git a/.agents/phases/todo/132_image_upload_feedback/00_phase.md b/.agents/phases/complete/132_image_upload_feedback/00_phase.md similarity index 100% rename from .agents/phases/todo/132_image_upload_feedback/00_phase.md rename to .agents/phases/complete/132_image_upload_feedback/00_phase.md diff --git a/.agents/phases/todo/132_image_upload_feedback/01_upload_state.md b/.agents/phases/complete/132_image_upload_feedback/01_upload_state.md similarity index 100% rename from .agents/phases/todo/132_image_upload_feedback/01_upload_state.md rename to .agents/phases/complete/132_image_upload_feedback/01_upload_state.md diff --git a/.agents/phases/todo/132_image_upload_feedback/02_css_upload_state.md b/.agents/phases/complete/132_image_upload_feedback/02_css_upload_state.md similarity index 100% rename from .agents/phases/todo/132_image_upload_feedback/02_css_upload_state.md rename to .agents/phases/complete/132_image_upload_feedback/02_css_upload_state.md diff --git a/.agents/phases/todo/132_image_upload_feedback/03_e2e_upload_feedback.md b/.agents/phases/complete/132_image_upload_feedback/03_e2e_upload_feedback.md similarity index 100% rename from .agents/phases/todo/132_image_upload_feedback/03_e2e_upload_feedback.md rename to .agents/phases/complete/132_image_upload_feedback/03_e2e_upload_feedback.md diff --git a/.agents/reports/132_image_upload_feedback/132_image_upload_feedback__00_phase.a1.err b/.agents/reports/132_image_upload_feedback/132_image_upload_feedback__00_phase.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/132_image_upload_feedback/132_image_upload_feedback__00_phase.a1.md b/.agents/reports/132_image_upload_feedback/132_image_upload_feedback__00_phase.a1.md new file mode 100644 index 0000000..2696fc5 --- /dev/null +++ b/.agents/reports/132_image_upload_feedback/132_image_upload_feedback__00_phase.a1.md @@ -0,0 +1,10 @@ +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`. diff --git a/.agents/reports/132_image_upload_feedback/132_image_upload_feedback__00_phase.a1.validate b/.agents/reports/132_image_upload_feedback/132_image_upload_feedback__00_phase.a1.validate new file mode 100644 index 0000000..5b0bb41 --- /dev/null +++ b/.agents/reports/132_image_upload_feedback/132_image_upload_feedback__00_phase.a1.validate @@ -0,0 +1,115 @@ +........................................................................ [ 2%] +........................................................................ [ 4%] +........................................................................ [ 6%] +........................................................................ [ 9%] +........................................................................ [ 11%] +........................................................................ [ 13%] +........................................................................ [ 16%] +........................................................................ [ 18%] +........................................................................ [ 20%] +........................................................................ [ 23%] +........................................................................ [ 25%] +........................................................................ [ 27%] +........................................................................ [ 30%] +........................................................................ [ 32%] +........................................................................ [ 34%] +........................................................................ [ 37%] +........................................................................ [ 39%] +........................................................................ [ 41%] +........................................................................ [ 44%] +........................................................................ [ 46%] +........................................................................ [ 48%] +........................................................................ [ 50%] +........................................................................ [ 53%] +........................................................................ [ 55%] +........................................................................ [ 57%] +........................................................................ [ 60%] +........................................................................ [ 62%] +........................................................................ [ 64%] +........................................................................ [ 67%] +........................................................................ [ 69%] +........................................................................ [ 71%] +........................................................................ [ 74%] +........................................................................ [ 76%] +........................................................................ [ 78%] +........................................................................ [ 81%] +........................................................................ [ 83%] +........................................................................ [ 85%] +........................................................................ [ 88%] +........................................................................ [ 90%] +........................................................................ [ 92%] +........................................................................ [ 94%] +........................................................................ [ 97%] +........................................................................ [ 99%] +............ [100%] +=============================== warnings summary =============================== +.venv/lib/python3.13/site-packages/fastapi/testclient.py:1 + /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead. + from starlette.testclient import TestClient as TestClient # noqa + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +================================ tests coverage ================================ +_______________ coverage: platform linux, python 3.13.13-final-0 _______________ + +Name Stmts Miss Cover +-------------------------------------------------- +app/__init__.py 1 0 100% +app/api/__init__.py 0 0 100% +app/api/auth.py 52 0 100% +app/api/chat.py 268 1 99% +app/api/chat_images.py 50 0 100% +app/api/chats.py 110 0 100% +app/api/config.py 13 0 100% +app/api/doc_drafts.py 99 0 100% +app/api/docs.py 179 1 99% +app/api/git_sources.py 241 0 100% +app/api/health.py 10 0 100% +app/api/steering.py 42 0 100% +app/api/suggestions.py 33 0 100% +app/api/sync.py 156 0 100% +app/api/tokens.py 40 0 100% +app/api/ui_settings.py 55 0 100% +app/config.py 256 0 100% +app/core/__init__.py 0 0 100% +app/core/auth.py 45 0 100% +app/core/caching.py 124 0 100% +app/core/debugging.py 29 2 93% +app/core/docs_push.py 39 0 100% +app/core/errors.py 5 0 100% +app/core/logging.py 13 0 100% +app/core/rate_limit.py 44 0 100% +app/core/security_headers.py 20 0 100% +app/core/theming.py 38 0 100% +app/core/tokens.py 44 0 100% +app/db.py 22 0 100% +app/main.py 68 0 100% +app/models.py 132 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 412 1 99% +app/rag/archive_upload.py 134 0 100% +app/rag/chunker.py 220 4 98% +app/rag/doc_dates.py 18 0 100% +app/rag/folder_summaries.py 134 0 100% +app/rag/git_sources.py 38 0 100% +app/rag/importer.py 571 9 98% +app/rag/llm.py 246 1 99% +app/rag/md_images.py 63 0 100% +app/rag/overview.py 71 0 100% +app/rag/pdf.py 81 0 100% +app/rag/prompts.py 144 0 100% +app/rag/retriever.py 248 3 99% +app/rag/scaffolding.py 55 0 100% +app/rag/source_removal.py 41 0 100% +app/rag/sources_meta.py 16 0 100% +app/rag/suggestions.py 27 0 100% +app/rag/summarizer.py 49 0 100% +app/schemas.py 410 0 100% +-------------------------------------------------- +TOTAL 5206 22 99% +coverage gate: app/ 99% (>90%) OK +All checks passed! +0 errors, 0 warnings, 0 informations +WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414). +Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest` + +validation OK diff --git a/.agents/reports/132_image_upload_feedback/132_image_upload_feedback__01_upload_state.a1.err b/.agents/reports/132_image_upload_feedback/132_image_upload_feedback__01_upload_state.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/132_image_upload_feedback/132_image_upload_feedback__01_upload_state.a1.md b/.agents/reports/132_image_upload_feedback/132_image_upload_feedback__01_upload_state.a1.md new file mode 100644 index 0000000..0361ac0 --- /dev/null +++ b/.agents/reports/132_image_upload_feedback/132_image_upload_feedback__01_upload_state.a1.md @@ -0,0 +1,17 @@ +All done. Final report: + +**Implemented (phase 132, task 01 — uploading state):** +- `frontend/index.html`: `#attach-preview` gains the hidden status part (house loading arc svg, `aria-hidden` + pinned "Uploading image…" text) between the filename and remove ✕ — ships hidden per the house rule +- `frontend/assets/app.js`: module-scope `attachStatus` ref (null-guard pattern); `enterUploadState()`/`exitUploadState()` helpers beside `showAttachPreview`/`clearAttachedImage` (strip `.is-uploading` + status unhidden + remove disabled + Send disabled "Uploading…" / mirror exit restoring "Send" byte-identical to `setUiState`'s idle label); `handleSend` enters after the `attachUpload` guard before the await, exits at `attachUpload = false` (both success & failure — helper never throws). `setUiState`/`uiState`/turn loader untouched (pre-turn) +- `tests/unit/test_chat_image_questions.py`: +4 tests — markup pin (order, exact arc, hidden), helper affordance pin (order, guards, pre-turn contract, byte-identical "Send"), `handleSend` exact-ordering pin (guard → arm → enter → await → release → exit → block, single enter/exit, no `setUiState(` call, byte-untouched phase-123 contract), node-harness behavioral test (enter / exit-success / exit-failure / re-enter rows) + +**Gates (all green):** +- `uv run pytest --cov=app --cov-report=term -q` → exit 0, **99%** on `app/` (>90% ✓) +- `uv run ruff check .` → clean; `uv run pyright` → exit 0 +- Regression E2E in isolation: `test_chat_image_questions.py` (phase 123 contract on the modified surface) 5 passed; `test_smoke.py`, `test_answer_images.py`, `test_image_anchor_context.py`, `test_image_documents.py` all pass + +**Docs:** no doc impact (no setup/commands/env/API changes) + +**Decisions:** used the task's exact arc snippet (the "check `#turn-loader`" hint is stale — that loader is the brain-wave glyph, not an arc; the arc matches the house `.spinner` language for task 02's spin CSS); removed a stray `✕` glyph my comment introduced (caught by the emoji guard). + +**Next pending task:** `02_css_upload_state.md` diff --git a/.agents/reports/132_image_upload_feedback/132_image_upload_feedback__01_upload_state.a1.validate b/.agents/reports/132_image_upload_feedback/132_image_upload_feedback__01_upload_state.a1.validate new file mode 100644 index 0000000..38b0dc3 --- /dev/null +++ b/.agents/reports/132_image_upload_feedback/132_image_upload_feedback__01_upload_state.a1.validate @@ -0,0 +1,115 @@ +........................................................................ [ 2%] +........................................................................ [ 4%] +........................................................................ [ 6%] +........................................................................ [ 9%] +........................................................................ [ 11%] +........................................................................ [ 13%] +........................................................................ [ 16%] +........................................................................ [ 18%] +........................................................................ [ 20%] +........................................................................ [ 23%] +........................................................................ [ 25%] +........................................................................ [ 27%] +........................................................................ [ 30%] +........................................................................ [ 32%] +........................................................................ [ 34%] +........................................................................ [ 37%] +........................................................................ [ 39%] +........................................................................ [ 41%] +........................................................................ [ 44%] +........................................................................ [ 46%] +........................................................................ [ 48%] +........................................................................ [ 51%] +........................................................................ [ 53%] +........................................................................ [ 55%] +........................................................................ [ 58%] +........................................................................ [ 60%] +........................................................................ [ 62%] +........................................................................ [ 65%] +........................................................................ [ 67%] +........................................................................ [ 69%] +........................................................................ [ 71%] +........................................................................ [ 74%] +........................................................................ [ 76%] +........................................................................ [ 78%] +........................................................................ [ 81%] +........................................................................ [ 83%] +........................................................................ [ 85%] +........................................................................ [ 88%] +........................................................................ [ 90%] +........................................................................ [ 92%] +........................................................................ [ 95%] +........................................................................ [ 97%] +........................................................................ [ 99%] +..... [100%] +=============================== warnings summary =============================== +.venv/lib/python3.13/site-packages/fastapi/testclient.py:1 + /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead. + from starlette.testclient import TestClient as TestClient # noqa + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +================================ tests coverage ================================ +_______________ coverage: platform linux, python 3.13.13-final-0 _______________ + +Name Stmts Miss Cover +-------------------------------------------------- +app/__init__.py 1 0 100% +app/api/__init__.py 0 0 100% +app/api/auth.py 52 0 100% +app/api/chat.py 268 1 99% +app/api/chat_images.py 50 0 100% +app/api/chats.py 110 0 100% +app/api/config.py 13 0 100% +app/api/doc_drafts.py 99 0 100% +app/api/docs.py 179 1 99% +app/api/git_sources.py 241 0 100% +app/api/health.py 10 0 100% +app/api/steering.py 42 0 100% +app/api/suggestions.py 33 0 100% +app/api/sync.py 156 0 100% +app/api/tokens.py 40 0 100% +app/api/ui_settings.py 55 0 100% +app/config.py 256 0 100% +app/core/__init__.py 0 0 100% +app/core/auth.py 45 0 100% +app/core/caching.py 124 0 100% +app/core/debugging.py 29 2 93% +app/core/docs_push.py 39 0 100% +app/core/errors.py 5 0 100% +app/core/logging.py 13 0 100% +app/core/rate_limit.py 44 0 100% +app/core/security_headers.py 20 0 100% +app/core/theming.py 38 0 100% +app/core/tokens.py 44 0 100% +app/db.py 22 0 100% +app/main.py 68 0 100% +app/models.py 132 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 412 1 99% +app/rag/archive_upload.py 134 0 100% +app/rag/chunker.py 220 4 98% +app/rag/doc_dates.py 18 0 100% +app/rag/folder_summaries.py 134 0 100% +app/rag/git_sources.py 38 0 100% +app/rag/importer.py 571 9 98% +app/rag/llm.py 246 1 99% +app/rag/md_images.py 63 0 100% +app/rag/overview.py 71 0 100% +app/rag/pdf.py 81 0 100% +app/rag/prompts.py 144 0 100% +app/rag/retriever.py 248 3 99% +app/rag/scaffolding.py 55 0 100% +app/rag/source_removal.py 41 0 100% +app/rag/sources_meta.py 16 0 100% +app/rag/suggestions.py 27 0 100% +app/rag/summarizer.py 49 0 100% +app/schemas.py 410 0 100% +-------------------------------------------------- +TOTAL 5206 22 99% +coverage gate: app/ 99% (>90%) OK +All checks passed! +0 errors, 0 warnings, 0 informations +WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414). +Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest` + +validation OK diff --git a/.agents/reports/132_image_upload_feedback/132_image_upload_feedback__02_css_upload_state.a1.err b/.agents/reports/132_image_upload_feedback/132_image_upload_feedback__02_css_upload_state.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/132_image_upload_feedback/132_image_upload_feedback__02_css_upload_state.a1.md b/.agents/reports/132_image_upload_feedback/132_image_upload_feedback__02_css_upload_state.a1.md new file mode 100644 index 0000000..b8bfaff --- /dev/null +++ b/.agents/reports/132_image_upload_feedback/132_image_upload_feedback__02_css_upload_state.a1.md @@ -0,0 +1,19 @@ +Task 02 complete. Final report: + +**Implemented** +- `frontend/assets/styles.css`: Phase 132 uploading-state CSS in the `.attach-preview*` family — thumbnail dims to 0.5, name hides via `visibility` (slot kept, no reflow), `.attach-preview-status` row (`--ink-soft`, 0.85rem, `min-width: 0`), 14px arc on the house `spin` keyframes at the sync spinner's 1s pace, `prefers-reduced-motion` stills the arc, remove ✕ dims 0.5 with its disabled attribute +- Verified in real Chromium (320/375/480/768/1440): "Uploading image…" never clips (one line at 320px), strip height stable, arc animates `spin 1s` / `none` under reduced motion +- Disabled Send: existing `.send-btn:disabled` (`--brand-busy` + `not-allowed`) already reads disabled — verified + pinned, no new visual (B4) +- Theme audit: block ships no raw color; `--ink-soft` on `--surface` computed 8.2:1 (≥4.5:1) from `:root` literals +- 7 new unit pins appended to `tests/unit/test_chat_image_questions.py` (Phase 132 task 02 section) + +**Tests / lint** +- `uv run pytest --cov=app`: **3108 passed**, exit 0, `app/` coverage **99%** (>90% gate) +- `uv run ruff check .`: All checks passed · `uv run pyright`: 0 errors, 0 warnings +- Regression: `tests/e2e/test_chat_image_questions.py` (5 passed) + `test_pinned_composer.py` + `test_responsive_polish.py` (11 passed) in isolation + +**Docs:** no README/AGENTS.md impact (visual-state CSS only) + +**Decisions / deviations:** fixed stale `:root` contrast comments (5.1:1→8.2:1, 13.8:1→15.3:1 — palette changed at the phase-08 re-land, comments never updated; both pairs pass AA). ~40 other historical phase comments still cite the old low ratios (all underestimates, AA-safe) — left as out-of-scope for this task. Initial "✕" glyphs in my CSS comment tripped the phase-08 no-emoji guard — replaced with ASCII. + +**Next pending task:** `.agents/phases/todo/132_image_upload_feedback/03_e2e_upload_feedback.md` diff --git a/.agents/reports/132_image_upload_feedback/132_image_upload_feedback__02_css_upload_state.a1.validate b/.agents/reports/132_image_upload_feedback/132_image_upload_feedback__02_css_upload_state.a1.validate new file mode 100644 index 0000000..5b0bb41 --- /dev/null +++ b/.agents/reports/132_image_upload_feedback/132_image_upload_feedback__02_css_upload_state.a1.validate @@ -0,0 +1,115 @@ +........................................................................ [ 2%] +........................................................................ [ 4%] +........................................................................ [ 6%] +........................................................................ [ 9%] +........................................................................ [ 11%] +........................................................................ [ 13%] +........................................................................ [ 16%] +........................................................................ [ 18%] +........................................................................ [ 20%] +........................................................................ [ 23%] +........................................................................ [ 25%] +........................................................................ [ 27%] +........................................................................ [ 30%] +........................................................................ [ 32%] +........................................................................ [ 34%] +........................................................................ [ 37%] +........................................................................ [ 39%] +........................................................................ [ 41%] +........................................................................ [ 44%] +........................................................................ [ 46%] +........................................................................ [ 48%] +........................................................................ [ 50%] +........................................................................ [ 53%] +........................................................................ [ 55%] +........................................................................ [ 57%] +........................................................................ [ 60%] +........................................................................ [ 62%] +........................................................................ [ 64%] +........................................................................ [ 67%] +........................................................................ [ 69%] +........................................................................ [ 71%] +........................................................................ [ 74%] +........................................................................ [ 76%] +........................................................................ [ 78%] +........................................................................ [ 81%] +........................................................................ [ 83%] +........................................................................ [ 85%] +........................................................................ [ 88%] +........................................................................ [ 90%] +........................................................................ [ 92%] +........................................................................ [ 94%] +........................................................................ [ 97%] +........................................................................ [ 99%] +............ [100%] +=============================== warnings summary =============================== +.venv/lib/python3.13/site-packages/fastapi/testclient.py:1 + /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead. + from starlette.testclient import TestClient as TestClient # noqa + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +================================ tests coverage ================================ +_______________ coverage: platform linux, python 3.13.13-final-0 _______________ + +Name Stmts Miss Cover +-------------------------------------------------- +app/__init__.py 1 0 100% +app/api/__init__.py 0 0 100% +app/api/auth.py 52 0 100% +app/api/chat.py 268 1 99% +app/api/chat_images.py 50 0 100% +app/api/chats.py 110 0 100% +app/api/config.py 13 0 100% +app/api/doc_drafts.py 99 0 100% +app/api/docs.py 179 1 99% +app/api/git_sources.py 241 0 100% +app/api/health.py 10 0 100% +app/api/steering.py 42 0 100% +app/api/suggestions.py 33 0 100% +app/api/sync.py 156 0 100% +app/api/tokens.py 40 0 100% +app/api/ui_settings.py 55 0 100% +app/config.py 256 0 100% +app/core/__init__.py 0 0 100% +app/core/auth.py 45 0 100% +app/core/caching.py 124 0 100% +app/core/debugging.py 29 2 93% +app/core/docs_push.py 39 0 100% +app/core/errors.py 5 0 100% +app/core/logging.py 13 0 100% +app/core/rate_limit.py 44 0 100% +app/core/security_headers.py 20 0 100% +app/core/theming.py 38 0 100% +app/core/tokens.py 44 0 100% +app/db.py 22 0 100% +app/main.py 68 0 100% +app/models.py 132 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 412 1 99% +app/rag/archive_upload.py 134 0 100% +app/rag/chunker.py 220 4 98% +app/rag/doc_dates.py 18 0 100% +app/rag/folder_summaries.py 134 0 100% +app/rag/git_sources.py 38 0 100% +app/rag/importer.py 571 9 98% +app/rag/llm.py 246 1 99% +app/rag/md_images.py 63 0 100% +app/rag/overview.py 71 0 100% +app/rag/pdf.py 81 0 100% +app/rag/prompts.py 144 0 100% +app/rag/retriever.py 248 3 99% +app/rag/scaffolding.py 55 0 100% +app/rag/source_removal.py 41 0 100% +app/rag/sources_meta.py 16 0 100% +app/rag/suggestions.py 27 0 100% +app/rag/summarizer.py 49 0 100% +app/schemas.py 410 0 100% +-------------------------------------------------- +TOTAL 5206 22 99% +coverage gate: app/ 99% (>90%) OK +All checks passed! +0 errors, 0 warnings, 0 informations +WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414). +Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest` + +validation OK diff --git a/.agents/reports/132_image_upload_feedback/132_image_upload_feedback__03_e2e_upload_feedback.a1.err b/.agents/reports/132_image_upload_feedback/132_image_upload_feedback__03_e2e_upload_feedback.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/132_image_upload_feedback/132_image_upload_feedback__03_e2e_upload_feedback.a1.md b/.agents/reports/132_image_upload_feedback/132_image_upload_feedback__03_e2e_upload_feedback.a1.md new file mode 100644 index 0000000..081b8b1 --- /dev/null +++ b/.agents/reports/132_image_upload_feedback/132_image_upload_feedback__03_e2e_upload_feedback.a1.md @@ -0,0 +1,17 @@ +All gates green. Final report: + +**Implemented** +- `tests/e2e/test_image_upload_feedback.py` (new, mock-only, run in isolation): module app with `BOR_IMAGES=true` + suite-private image store, one-doc local source seeded through the real in-process sync, house 1×1 PNG constant. +- Three tests per the mapping: (1) mid-flight uploading state + owner-spam (2 dead button presses + Enter) → exactly ONE upload request, ONE turn; (2) success clears state, strip hides, bubble carries the data-URL image, record carries the PATH (A5), grounded turn settles; (3) delayed-`route.abort` network drop → banner copy, draft + attachment intact, no turn. +- In-flight window via `page.route` + async `asyncio.sleep(3.0)` handler, then `route.continue_()` (real server runs, response just late); module counter pins the double-send. + +**Results** +- `uv run pytest tests/e2e/test_image_upload_feedback.py -v --no-cov` → 3 passed (5 consecutive runs, 18 s each; instrumented run confirmed in-flight asserts settle ~10 ms after the click, inside the 3 s hold — not a race) +- `uv run pytest --cov=app --cov-report=term-missing` → 3108 passed, `app/` coverage 99% +- `uv run ruff check .` → all checks passed; `uv run pyright` → 0 errors, 0 warnings + +**Docs:** README "Question images" gained the uploading-state bullet (phase 132 behavior); AGENTS.md — no doc impact (no new rules/commands). + +**Notable decisions:** async Playwright API instead of the sync house pattern — a sync route handler's sleep blocks the test thread for the whole hold, making mid-flight DOM polling impossible (verified empirically; header docstring explains). Admin form login (async twin of `auth_helpers.login`) instead of anonymous: the phase-79 token gate locks the anonymous shell, and phase 123 (the referenced suite) uses the signed-in context. Same-question auto-saves are cleaned by the suite title (house pattern). + +**Next pending task:** `.agents/phases/todo/133_pdf_screenshot_fallback/01_diagnosis_regression_test.md` diff --git a/.agents/reports/132_image_upload_feedback/132_image_upload_feedback__03_e2e_upload_feedback.a1.validate b/.agents/reports/132_image_upload_feedback/132_image_upload_feedback__03_e2e_upload_feedback.a1.validate new file mode 100644 index 0000000..5b0bb41 --- /dev/null +++ b/.agents/reports/132_image_upload_feedback/132_image_upload_feedback__03_e2e_upload_feedback.a1.validate @@ -0,0 +1,115 @@ +........................................................................ [ 2%] +........................................................................ [ 4%] +........................................................................ [ 6%] +........................................................................ [ 9%] +........................................................................ [ 11%] +........................................................................ [ 13%] +........................................................................ [ 16%] +........................................................................ [ 18%] +........................................................................ [ 20%] +........................................................................ [ 23%] +........................................................................ [ 25%] +........................................................................ [ 27%] +........................................................................ [ 30%] +........................................................................ [ 32%] +........................................................................ [ 34%] +........................................................................ [ 37%] +........................................................................ [ 39%] +........................................................................ [ 41%] +........................................................................ [ 44%] +........................................................................ [ 46%] +........................................................................ [ 48%] +........................................................................ [ 50%] +........................................................................ [ 53%] +........................................................................ [ 55%] +........................................................................ [ 57%] +........................................................................ [ 60%] +........................................................................ [ 62%] +........................................................................ [ 64%] +........................................................................ [ 67%] +........................................................................ [ 69%] +........................................................................ [ 71%] +........................................................................ [ 74%] +........................................................................ [ 76%] +........................................................................ [ 78%] +........................................................................ [ 81%] +........................................................................ [ 83%] +........................................................................ [ 85%] +........................................................................ [ 88%] +........................................................................ [ 90%] +........................................................................ [ 92%] +........................................................................ [ 94%] +........................................................................ [ 97%] +........................................................................ [ 99%] +............ [100%] +=============================== warnings summary =============================== +.venv/lib/python3.13/site-packages/fastapi/testclient.py:1 + /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead. + from starlette.testclient import TestClient as TestClient # noqa + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +================================ tests coverage ================================ +_______________ coverage: platform linux, python 3.13.13-final-0 _______________ + +Name Stmts Miss Cover +-------------------------------------------------- +app/__init__.py 1 0 100% +app/api/__init__.py 0 0 100% +app/api/auth.py 52 0 100% +app/api/chat.py 268 1 99% +app/api/chat_images.py 50 0 100% +app/api/chats.py 110 0 100% +app/api/config.py 13 0 100% +app/api/doc_drafts.py 99 0 100% +app/api/docs.py 179 1 99% +app/api/git_sources.py 241 0 100% +app/api/health.py 10 0 100% +app/api/steering.py 42 0 100% +app/api/suggestions.py 33 0 100% +app/api/sync.py 156 0 100% +app/api/tokens.py 40 0 100% +app/api/ui_settings.py 55 0 100% +app/config.py 256 0 100% +app/core/__init__.py 0 0 100% +app/core/auth.py 45 0 100% +app/core/caching.py 124 0 100% +app/core/debugging.py 29 2 93% +app/core/docs_push.py 39 0 100% +app/core/errors.py 5 0 100% +app/core/logging.py 13 0 100% +app/core/rate_limit.py 44 0 100% +app/core/security_headers.py 20 0 100% +app/core/theming.py 38 0 100% +app/core/tokens.py 44 0 100% +app/db.py 22 0 100% +app/main.py 68 0 100% +app/models.py 132 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 412 1 99% +app/rag/archive_upload.py 134 0 100% +app/rag/chunker.py 220 4 98% +app/rag/doc_dates.py 18 0 100% +app/rag/folder_summaries.py 134 0 100% +app/rag/git_sources.py 38 0 100% +app/rag/importer.py 571 9 98% +app/rag/llm.py 246 1 99% +app/rag/md_images.py 63 0 100% +app/rag/overview.py 71 0 100% +app/rag/pdf.py 81 0 100% +app/rag/prompts.py 144 0 100% +app/rag/retriever.py 248 3 99% +app/rag/scaffolding.py 55 0 100% +app/rag/source_removal.py 41 0 100% +app/rag/sources_meta.py 16 0 100% +app/rag/suggestions.py 27 0 100% +app/rag/summarizer.py 49 0 100% +app/schemas.py 410 0 100% +-------------------------------------------------- +TOTAL 5206 22 99% +coverage gate: app/ 99% (>90%) OK +All checks passed! +0 errors, 0 warnings, 0 informations +WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414). +Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest` + +validation OK diff --git a/README.md b/README.md index c2cc828..ed49689 100644 --- a/README.md +++ b/README.md @@ -482,6 +482,13 @@ image: to `POST /api/chat-images` (capped at `BOR_CHAT_IMAGE_MAX_MB`, default 10 MiB — over is a 413) and stored under `BOR_CHAT_IMAGE_DIR`. Saved and shared chats carry the returned *path* — never base64. +- **Visible while uploading.** While the upload is in flight the + preview strip shows a spinner + *"Uploading image…"* (its remove ✕ + disabled) and the Send button is disabled with the *"Uploading…"* + label — a second press is dead, visually and functionally (no second + upload, no second turn). On success the state clears and the turn + proceeds; on failure it clears, the banner shows, and the typed text + + the attachment stay for the retry (phase 132). - **Current turn only.** The model receives a multimodal message for this question; prior turns' images are never replayed into history (the text of a prior turn stands alone). diff --git a/frontend/assets/app.js b/frontend/assets/app.js index 9f40be5..83a2133 100644 --- a/frontend/assets/app.js +++ b/frontend/assets/app.js @@ -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) diff --git a/frontend/assets/styles.css b/frontend/assets/styles.css index 46779e7..c1b4155 100644 --- a/frontend/assets/styles.css +++ b/frontend/assets/styles.css @@ -7,9 +7,9 @@ :root { /* Palette — all text/background pairs meet WCAG 2.1 AA (>= 4.5:1) */ --bg: #0f0a0a; /* page: ink on bg 16.7:1 */ - --surface: #1a0f0f; /* ink on surface 13.8:1 */ + --surface: #1a0f0f; /* ink on surface 15.3:1 */ --ink: #f0e6e6; - --ink-soft: #b8a8a8; /* 5.1:1 on --surface */ + --ink-soft: #b8a8a8; /* 8.2:1 on --surface */ --line: #2d1a1a; /* decorative 1px borders */ --grid-line: #4a2626; /* background grid texture — decorative, no contrast duty (like --line) */ @@ -1745,6 +1745,56 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und } .attach-preview-remove svg { width: 18px; height: 18px; } .attach-preview-remove:hover { background: var(--err-bg); color: var(--err-ink); } +/* Phase 132 (task 02, TODO L8): the strip's UPLOADING look — the + visual half of the owner-confirmed double-spam block (task 01's JS + state proves the image is moving while POST /api/chat-images is in + flight; before this the strip looked frozen and the user spammed + Send). The thumbnail DIMS to 0.5 (it is decorative — no contrast + duty — and 0.5 is the house dim value the disabled remove button + below); the filename HIDES with visibility (the flex: 1 slot keeps + its measured width — the status takes the row's right hand WITHOUT + reflowing the pinned 44px remove button); the house loading arc + (the status part's svg — task 01's markup, currentColor = the + status's --ink-soft) SPINS at the sync spinner's pace (the house + `spin` keyframes at 1s linear — .sync-icon.is-spinning's in-flight + pace; the .spinner's 0.8s is the busy-turn arc's). The status text + is --ink-soft on the strip's --surface — the :root-pinned pair, + 8.2:1 computed against WCAG 2.1 (AA ≥ 4.5:1), at the strip's + small text size (the name's 0.85rem). The remove button (task 01 + disables it mid-flight) dims with its disabled attribute — the + attribute is the state, the 0.5 is its visual half; the Send rides + the same house :disabled language (.send-btn:disabled's + --brand-busy + not-allowed below — which reads as disabled already, + so NO new color or weight was added, B4). prefers-reduced-motion stills the arc (the house block): the + "Uploading image…" TEXT + the dimmed thumbnail carry the state + without motion (B5 — never motion alone). Every value is a theme + variable or a neutral number — the block ships no raw color. */ +.attach-preview.is-uploading .attach-preview-img { + opacity: 0.5; +} +.attach-preview.is-uploading .attach-preview-name { + visibility: hidden; /* keep the flex: 1 slot; the status takes its right hand */ +} +.attach-preview-status { + display: inline-flex; + align-items: center; + gap: 0.35rem; + color: var(--ink-soft); + font-size: 0.85rem; /* the strip's small text — the name's size */ + min-width: 0; /* the row's clamp: at 320 px the text WRAPS (no clip) */ +} +.attach-preview-status-icon { + width: 14px; + height: 14px; + flex: 0 0 auto; + animation: spin 1s linear infinite; /* the house spin keyframes (the sync pace) */ +} +@media (prefers-reduced-motion: reduce) { + .attach-preview-status-icon { animation: none; } +} +.attach-preview.is-uploading .attach-preview-remove { + opacity: 0.5; /* the disabled attribute's visual half (task 01 sets it) */ +} .composer { display: flex; align-items: flex-end; diff --git a/frontend/index.html b/frontend/index.html index 90325ac..2811e1c 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -280,6 +280,22 @@