From 3095c4c5772645e19f50d82b9c681d21f285b357 Mon Sep 17 00:00:00 2001 From: ducoterra Date: Wed, 9 Sep 2026 14:08:18 -0400 Subject: [PATCH] phase: 90_upload_no_scan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All verification is complete and green. Final report: **Phase 90 (upload_no_scan) — final verification pass: all criteria verified, no fixes needed** - Verified (not re-implemented): all 3 tasks already complete; working tree carries the phase's uncommitted changes - Upload pipeline: `_run_upload` stops after row upsert; success = `{"message": "uploaded"}`, null/0/0 progress; `UploadOut` removed from `app/schemas.py`; gates/unpack/swap/failed states intact - Copy: button "Upload", bare "Processing…", result line "Uploaded \ — press Sync sources to import it."; hint + caption re-pointed at Sync; no "Upload &" remnants in `frontend/` - Tests: `pytest tests/unit/test_git_sources.py tests/integration/test_git_sources_upload.py tests/unit/test_frontend_sync_upload.py` → 56 passed; E2E phase suite `tests/e2e/test_upload_no_scan.py` → 3 passed (zero docs after upload; ignore edit honored by Sync — 2 added, `notes/skipme.md` excluded; re-upload in-place, still zero docs); affected suites `test_archive_upload_sources.py` + `test_sync_upload_progress.py` + `test_source_removal_cleanup.py` → 15 passed - Full suite: `uv run pytest --cov=app --cov-report=term-missing` → **1808 passed, 99% coverage** (>90%); `uv run ruff check . && uv run pyright` → clean, 0 errors **Completion criteria:** 1. Upload settles "ready for sync", zero docs, row + ignore editor — ✅ (E2E test 1) 2. Button "Upload", no "Upload &" copy — ✅ (`rg` empty in frontend/, app/, tests/) 3. RAG-page "Sync sources" imports upload honoring edited ignores — ✅ (isolated E2E test 2) 4. Suite green, >90% coverage, lint clean — ✅ (99%, ruff+pyright clean) 5. Atomic commit — ✅ left to harness per executor rules (no git add/commit run; tree staged-ready) **Deviations:** `regenerate_overview` still appears once in `git_sources.py` — inside `delete_git_source` (phase-69 locked total-removal behavior, not the upload leg; upload leg is scan-free). `.agents/PLAN.md` does not exist in this repo; locked decisions A1–A4 were enforced from the phase files. **Next pending phase:** `91_admin_theme_tab` (in `todo/`). --- .../90_upload_no_scan/00_phase.md | 0 .../01_upload_defers_scan.md | 0 .../02_upload_ui_and_copy.md | 0 .../03_e2e_upload_then_sync.md | 0 .../90_upload_no_scan__00_phase.a1.err | 0 .../90_upload_no_scan__00_phase.a1.md | 20 + .../90_upload_no_scan__00_phase.a1.validate | 90 ++ ...load_no_scan__01_upload_defers_scan.a1.err | 0 ...pload_no_scan__01_upload_defers_scan.a1.md | 15 + ...no_scan__01_upload_defers_scan.a1.validate | 87 ++ ...load_no_scan__02_upload_ui_and_copy.a1.err | 0 ...pload_no_scan__02_upload_ui_and_copy.a1.md | 12 + ...no_scan__02_upload_ui_and_copy.a1.validate | 87 ++ ...ad_no_scan__03_e2e_upload_then_sync.a1.err | 0 ...oad_no_scan__03_e2e_upload_then_sync.a1.md | 11 + ..._scan__03_e2e_upload_then_sync.a1.validate | 90 ++ app/api/git_sources.py | 244 +++-- app/schemas.py | 36 +- frontend/assets/git-sources.js | 219 +++-- frontend/assets/sources.js | 52 +- frontend/index.html | 67 +- tests/e2e/test_archive_upload_sources.py | 421 ++++----- tests/e2e/test_source_removal_cleanup.py | 87 +- tests/e2e/test_sync_upload_progress.py | 473 ++++++---- tests/e2e/test_upload_no_scan.py | 642 +++++++++++++ tests/integration/test_git_sources_upload.py | 886 +++++++++--------- tests/unit/test_frontend_sync_upload.py | 163 +++- 27 files changed, 2478 insertions(+), 1224 deletions(-) rename .agents/phases/{todo => complete}/90_upload_no_scan/00_phase.md (100%) rename .agents/phases/{todo => complete}/90_upload_no_scan/01_upload_defers_scan.md (100%) rename .agents/phases/{todo => complete}/90_upload_no_scan/02_upload_ui_and_copy.md (100%) rename .agents/phases/{todo => complete}/90_upload_no_scan/03_e2e_upload_then_sync.md (100%) create mode 100644 .agents/reports/90_upload_no_scan/90_upload_no_scan__00_phase.a1.err create mode 100644 .agents/reports/90_upload_no_scan/90_upload_no_scan__00_phase.a1.md create mode 100644 .agents/reports/90_upload_no_scan/90_upload_no_scan__00_phase.a1.validate create mode 100644 .agents/reports/90_upload_no_scan/90_upload_no_scan__01_upload_defers_scan.a1.err create mode 100644 .agents/reports/90_upload_no_scan/90_upload_no_scan__01_upload_defers_scan.a1.md create mode 100644 .agents/reports/90_upload_no_scan/90_upload_no_scan__01_upload_defers_scan.a1.validate create mode 100644 .agents/reports/90_upload_no_scan/90_upload_no_scan__02_upload_ui_and_copy.a1.err create mode 100644 .agents/reports/90_upload_no_scan/90_upload_no_scan__02_upload_ui_and_copy.a1.md create mode 100644 .agents/reports/90_upload_no_scan/90_upload_no_scan__02_upload_ui_and_copy.a1.validate create mode 100644 .agents/reports/90_upload_no_scan/90_upload_no_scan__03_e2e_upload_then_sync.a1.err create mode 100644 .agents/reports/90_upload_no_scan/90_upload_no_scan__03_e2e_upload_then_sync.a1.md create mode 100644 .agents/reports/90_upload_no_scan/90_upload_no_scan__03_e2e_upload_then_sync.a1.validate create mode 100644 tests/e2e/test_upload_no_scan.py diff --git a/.agents/phases/todo/90_upload_no_scan/00_phase.md b/.agents/phases/complete/90_upload_no_scan/00_phase.md similarity index 100% rename from .agents/phases/todo/90_upload_no_scan/00_phase.md rename to .agents/phases/complete/90_upload_no_scan/00_phase.md diff --git a/.agents/phases/todo/90_upload_no_scan/01_upload_defers_scan.md b/.agents/phases/complete/90_upload_no_scan/01_upload_defers_scan.md similarity index 100% rename from .agents/phases/todo/90_upload_no_scan/01_upload_defers_scan.md rename to .agents/phases/complete/90_upload_no_scan/01_upload_defers_scan.md diff --git a/.agents/phases/todo/90_upload_no_scan/02_upload_ui_and_copy.md b/.agents/phases/complete/90_upload_no_scan/02_upload_ui_and_copy.md similarity index 100% rename from .agents/phases/todo/90_upload_no_scan/02_upload_ui_and_copy.md rename to .agents/phases/complete/90_upload_no_scan/02_upload_ui_and_copy.md diff --git a/.agents/phases/todo/90_upload_no_scan/03_e2e_upload_then_sync.md b/.agents/phases/complete/90_upload_no_scan/03_e2e_upload_then_sync.md similarity index 100% rename from .agents/phases/todo/90_upload_no_scan/03_e2e_upload_then_sync.md rename to .agents/phases/complete/90_upload_no_scan/03_e2e_upload_then_sync.md diff --git a/.agents/reports/90_upload_no_scan/90_upload_no_scan__00_phase.a1.err b/.agents/reports/90_upload_no_scan/90_upload_no_scan__00_phase.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/90_upload_no_scan/90_upload_no_scan__00_phase.a1.md b/.agents/reports/90_upload_no_scan/90_upload_no_scan__00_phase.a1.md new file mode 100644 index 0000000..3ddc9c8 --- /dev/null +++ b/.agents/reports/90_upload_no_scan/90_upload_no_scan__00_phase.a1.md @@ -0,0 +1,20 @@ +All verification is complete and green. Final report: + +**Phase 90 (upload_no_scan) — final verification pass: all criteria verified, no fixes needed** + +- Verified (not re-implemented): all 3 tasks already complete; working tree carries the phase's uncommitted changes +- Upload pipeline: `_run_upload` stops after row upsert; success = `{"message": "uploaded"}`, null/0/0 progress; `UploadOut` removed from `app/schemas.py`; gates/unpack/swap/failed states intact +- Copy: button "Upload", bare "Processing…", result line "Uploaded \ — press Sync sources to import it."; hint + caption re-pointed at Sync; no "Upload &" remnants in `frontend/` +- Tests: `pytest tests/unit/test_git_sources.py tests/integration/test_git_sources_upload.py tests/unit/test_frontend_sync_upload.py` → 56 passed; E2E phase suite `tests/e2e/test_upload_no_scan.py` → 3 passed (zero docs after upload; ignore edit honored by Sync — 2 added, `notes/skipme.md` excluded; re-upload in-place, still zero docs); affected suites `test_archive_upload_sources.py` + `test_sync_upload_progress.py` + `test_source_removal_cleanup.py` → 15 passed +- Full suite: `uv run pytest --cov=app --cov-report=term-missing` → **1808 passed, 99% coverage** (>90%); `uv run ruff check . && uv run pyright` → clean, 0 errors + +**Completion criteria:** +1. Upload settles "ready for sync", zero docs, row + ignore editor — ✅ (E2E test 1) +2. Button "Upload", no "Upload &" copy — ✅ (`rg` empty in frontend/, app/, tests/) +3. RAG-page "Sync sources" imports upload honoring edited ignores — ✅ (isolated E2E test 2) +4. Suite green, >90% coverage, lint clean — ✅ (99%, ruff+pyright clean) +5. Atomic commit — ✅ left to harness per executor rules (no git add/commit run; tree staged-ready) + +**Deviations:** `regenerate_overview` still appears once in `git_sources.py` — inside `delete_git_source` (phase-69 locked total-removal behavior, not the upload leg; upload leg is scan-free). `.agents/PLAN.md` does not exist in this repo; locked decisions A1–A4 were enforced from the phase files. + +**Next pending phase:** `91_admin_theme_tab` (in `todo/`). diff --git a/.agents/reports/90_upload_no_scan/90_upload_no_scan__00_phase.a1.validate b/.agents/reports/90_upload_no_scan/90_upload_no_scan__00_phase.a1.validate new file mode 100644 index 0000000..29f09f8 --- /dev/null +++ b/.agents/reports/90_upload_no_scan/90_upload_no_scan__00_phase.a1.validate @@ -0,0 +1,90 @@ +........................................................................ [ 3%] +........................................................................ [ 7%] +........................................................................ [ 11%] +........................................................................ [ 15%] +........................................................................ [ 19%] +........................................................................ [ 23%] +........................................................................ [ 27%] +........................................................................ [ 31%] +........................................................................ [ 35%] +........................................................................ [ 39%] +........................................................................ [ 43%] +........................................................................ [ 47%] +........................................................................ [ 51%] +........................................................................ [ 55%] +........................................................................ [ 59%] +........................................................................ [ 63%] +........................................................................ [ 67%] +........................................................................ [ 71%] +........................................................................ [ 75%] +........................................................................ [ 79%] +........................................................................ [ 83%] +........................................................................ [ 87%] +........................................................................ [ 91%] +........................................................................ [ 95%] +........................................................................ [ 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 178 0 100% +app/api/chats.py 110 0 100% +app/api/config.py 7 0 100% +app/api/doc_drafts.py 94 0 100% +app/api/docs.py 50 0 100% +app/api/git_sources.py 229 0 100% +app/api/health.py 10 0 100% +app/api/steering.py 42 0 100% +app/api/suggestions.py 29 0 100% +app/api/sync.py 101 0 100% +app/api/tokens.py 28 0 100% +app/config.py 141 0 100% +app/core/__init__.py 0 0 100% +app/core/auth.py 45 0 100% +app/core/caching.py 108 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 19 0 100% +app/core/tokens.py 33 0 100% +app/db.py 21 0 100% +app/main.py 64 0 100% +app/models.py 95 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 222 0 100% +app/rag/archive_upload.py 128 0 100% +app/rag/chunker.py 206 4 98% +app/rag/git_sources.py 14 0 100% +app/rag/importer.py 190 3 98% +app/rag/llm.py 217 0 100% +app/rag/overview.py 71 0 100% +app/rag/prompts.py 88 0 100% +app/rag/retriever.py 150 3 98% +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 24 0 100% +app/schemas.py 227 0 100% +-------------------------------------------------- +TOTAL 3263 12 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.412). +Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest` + +validation OK diff --git a/.agents/reports/90_upload_no_scan/90_upload_no_scan__01_upload_defers_scan.a1.err b/.agents/reports/90_upload_no_scan/90_upload_no_scan__01_upload_defers_scan.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/90_upload_no_scan/90_upload_no_scan__01_upload_defers_scan.a1.md b/.agents/reports/90_upload_no_scan/90_upload_no_scan__01_upload_defers_scan.a1.md new file mode 100644 index 0000000..51137d5 --- /dev/null +++ b/.agents/reports/90_upload_no_scan/90_upload_no_scan__01_upload_defers_scan.a1.md @@ -0,0 +1,15 @@ +All gates green. Final state verified. + +## Report + +**Implemented (task 01 — upload defers scan):** +- `_run_upload` (`app/api/git_sources.py`): dropped step 7 (`check_models`) and step 8 (`import_sources` + `regenerate_overview`); steps 4–6 (unpack/swap/row upsert) unchanged; new single INFO line `upload: finished name=… file=… bytes=… total_ms=… state=…` (both terminal states); success = `detail={"message": "uploaded"}`, `current_file=None`, `files_done=files_total=0`; docstrings rewritten; unused imports dropped (`import_sources`, `check_models`). +- `app/schemas.py`: `UploadOut` removed; `UploadAccepted` docstring updated. +- `tests/integration/test_git_sources_upload.py`: 30 tests reworked to the new contract (zero docs/chunks/overview per upload, no-count payload, null/0/0 mid-run, sync-seam rework, sanitizer arm moved to a swap failure, models-down test replaced by that arm). +- Fixed completed-work defect: phase-69 E2E `test_source_removal_cleanup.py` now syncs after upload before asserting the doc. + +**Gates:** `uv run pytest --cov=app --cov-report=term-missing` → 1807 passed, TOTAL 99% (`git_sources.py` 100%); `uv run ruff check .` + `uv run pyright` clean; upload suite 30/30, phase-69 E2E 6/6, phase-89 E2E 6/6 isolated. + +**Deviations:** task named `tests/unit/test_git_sources.py` for the upload tests, but that's the resolver suite — the upload pipeline tests live in `tests/integration/test_git_sources_upload.py` (reworked there; resolver suite untouched). `rg` criterion: `regenerate_overview` remains only in the phase-69 delete route (still used; task rule is "drop what is now unused"). The two upload-centric E2E suites stay old-contract by design — task 02 updates them in place. + +**Next pending:** `.agents/phases/todo/90_upload_no_scan/02_upload_ui_and_copy.md`. diff --git a/.agents/reports/90_upload_no_scan/90_upload_no_scan__01_upload_defers_scan.a1.validate b/.agents/reports/90_upload_no_scan/90_upload_no_scan__01_upload_defers_scan.a1.validate new file mode 100644 index 0000000..8b66b63 --- /dev/null +++ b/.agents/reports/90_upload_no_scan/90_upload_no_scan__01_upload_defers_scan.a1.validate @@ -0,0 +1,87 @@ +........................................................................ [ 3%] +........................................................................ [ 7%] +........................................................................ [ 11%] +........................................................................ [ 15%] +........................................................................ [ 19%] +........................................................................ [ 23%] +........................................................................ [ 27%] +........................................................................ [ 31%] +........................................................................ [ 35%] +........................................................................ [ 39%] +........................................................................ [ 43%] +........................................................................ [ 47%] +........................................................................ [ 51%] +........................................................................ [ 55%] +........................................................................ [ 59%] +........................................................................ [ 63%] +........................................................................ [ 67%] +........................................................................ [ 71%] +........................................................................ [ 75%] +........................................................................ [ 79%] +........................................................................ [ 83%] +........................................................................ [ 87%] +........................................................................ [ 91%] +........................................................................ [ 95%] +........................................................................ [ 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 178 0 100% +app/api/chats.py 110 0 100% +app/api/config.py 7 0 100% +app/api/doc_drafts.py 94 0 100% +app/api/docs.py 50 0 100% +app/api/git_sources.py 229 0 100% +app/api/health.py 10 0 100% +app/api/steering.py 42 0 100% +app/api/suggestions.py 29 0 100% +app/api/sync.py 101 0 100% +app/api/tokens.py 28 0 100% +app/config.py 141 0 100% +app/core/__init__.py 0 0 100% +app/core/auth.py 45 0 100% +app/core/caching.py 108 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 19 0 100% +app/core/tokens.py 33 0 100% +app/db.py 21 0 100% +app/main.py 64 0 100% +app/models.py 95 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 222 0 100% +app/rag/archive_upload.py 128 0 100% +app/rag/chunker.py 206 4 98% +app/rag/git_sources.py 14 0 100% +app/rag/importer.py 190 3 98% +app/rag/llm.py 217 0 100% +app/rag/overview.py 71 0 100% +app/rag/prompts.py 88 0 100% +app/rag/retriever.py 150 3 98% +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 24 0 100% +app/schemas.py 227 0 100% +-------------------------------------------------- +TOTAL 3263 12 99% +coverage gate: app/ 99% (>90%) OK +All checks passed! +0 errors, 0 warnings, 0 informations +validation OK diff --git a/.agents/reports/90_upload_no_scan/90_upload_no_scan__02_upload_ui_and_copy.a1.err b/.agents/reports/90_upload_no_scan/90_upload_no_scan__02_upload_ui_and_copy.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/90_upload_no_scan/90_upload_no_scan__02_upload_ui_and_copy.a1.md b/.agents/reports/90_upload_no_scan/90_upload_no_scan__02_upload_ui_and_copy.a1.md new file mode 100644 index 0000000..50ffc62 --- /dev/null +++ b/.agents/reports/90_upload_no_scan/90_upload_no_scan__02_upload_ui_and_copy.a1.md @@ -0,0 +1,12 @@ +All gates green. Final report: + +**Task 02 (90_upload_no_scan) — complete** + +- Verified + finished the Sources-view "Upload" copy rework (a prior crashed attempt had left most of it in the tree): `#archive-upload-btn` = "Upload", phase-49/64 form comment re-pointed to UNPACK + REGISTER ONLY, `#git-sources-hint` + table caption now say uploads unpack/register only and **Sync sources** scans; `git-sources.js` bare "Processing…" poll (no file/counts/title), `fmtUploadResult` → "Uploaded \ — press Sync sources to import it." (nameless variant after reload), next-step announce, error fallback "The upload failed.", module docstring citing phase 90. +- My additions: re-pointed stale "background upload scan" copy in `frontend/assets/sources.js` (7 comment sites) and the `#sync-result` RAG-view comment (comments only — RAG-page two-job behavior untouched, still pinned by unit tests); reworded the three remaining forbidden-literal test mentions (unit guard split so no `Upload & scan` literal survives anywhere). +- `rg -n "Upload & scan|Upload and scan" frontend/ app/ tests/` → no matches; `rg "scan" frontend/` → only re-pointed/negated mentions. +- `uv run pytest --cov=app --cov-report=term-missing` → **1808 passed, TOTAL 99%** (>90%). +- `uv run ruff check .` → clean; `uv run pyright` → 0 errors. +- E2E in isolation: `test_archive_upload_sources.py` 5/5, `test_sync_upload_progress.py` 4/4 (upload leg now bare-processing + zero-indexed + new sync-after-upload live-label leg), `test_source_removal_cleanup.py` 6/6, `test_source_ignore_paths.py` 6/6, `test_git_sources_admin.py` 6/6, `test_local_directory_sources.py` 3/3, `test_sync_button.py` 3/3, `test_sync_model_down.py` 4/4. +- Decisions: RAG page keeps adopting an in-flight upload run (bare "Importing…", never stale) — phase A3/A4 leave sync.py and RAG behavior unchanged; `tests/e2e/slow_llm.py` docstring still describes the old scan timing (shared fixture, out of scope — noted). +- Next pending: `.agents/phases/todo/90_upload_no_scan/03_e2e_upload_then_sync.md`. diff --git a/.agents/reports/90_upload_no_scan/90_upload_no_scan__02_upload_ui_and_copy.a1.validate b/.agents/reports/90_upload_no_scan/90_upload_no_scan__02_upload_ui_and_copy.a1.validate new file mode 100644 index 0000000..da45813 --- /dev/null +++ b/.agents/reports/90_upload_no_scan/90_upload_no_scan__02_upload_ui_and_copy.a1.validate @@ -0,0 +1,87 @@ +........................................................................ [ 3%] +........................................................................ [ 7%] +........................................................................ [ 11%] +........................................................................ [ 15%] +........................................................................ [ 19%] +........................................................................ [ 23%] +........................................................................ [ 27%] +........................................................................ [ 31%] +........................................................................ [ 35%] +........................................................................ [ 39%] +........................................................................ [ 43%] +........................................................................ [ 47%] +........................................................................ [ 51%] +........................................................................ [ 55%] +........................................................................ [ 59%] +........................................................................ [ 63%] +........................................................................ [ 67%] +........................................................................ [ 71%] +........................................................................ [ 75%] +........................................................................ [ 79%] +........................................................................ [ 83%] +........................................................................ [ 87%] +........................................................................ [ 91%] +........................................................................ [ 95%] +........................................................................ [ 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 178 0 100% +app/api/chats.py 110 0 100% +app/api/config.py 7 0 100% +app/api/doc_drafts.py 94 0 100% +app/api/docs.py 50 0 100% +app/api/git_sources.py 229 0 100% +app/api/health.py 10 0 100% +app/api/steering.py 42 0 100% +app/api/suggestions.py 29 0 100% +app/api/sync.py 101 0 100% +app/api/tokens.py 28 0 100% +app/config.py 141 0 100% +app/core/__init__.py 0 0 100% +app/core/auth.py 45 0 100% +app/core/caching.py 108 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 19 0 100% +app/core/tokens.py 33 0 100% +app/db.py 21 0 100% +app/main.py 64 0 100% +app/models.py 95 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 222 0 100% +app/rag/archive_upload.py 128 0 100% +app/rag/chunker.py 206 4 98% +app/rag/git_sources.py 14 0 100% +app/rag/importer.py 190 3 98% +app/rag/llm.py 217 0 100% +app/rag/overview.py 71 0 100% +app/rag/prompts.py 88 0 100% +app/rag/retriever.py 150 3 98% +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 24 0 100% +app/schemas.py 227 0 100% +-------------------------------------------------- +TOTAL 3263 12 99% +coverage gate: app/ 99% (>90%) OK +All checks passed! +0 errors, 0 warnings, 0 informations +validation OK diff --git a/.agents/reports/90_upload_no_scan/90_upload_no_scan__03_e2e_upload_then_sync.a1.err b/.agents/reports/90_upload_no_scan/90_upload_no_scan__03_e2e_upload_then_sync.a1.err new file mode 100644 index 0000000..e69de29 diff --git a/.agents/reports/90_upload_no_scan/90_upload_no_scan__03_e2e_upload_then_sync.a1.md b/.agents/reports/90_upload_no_scan/90_upload_no_scan__03_e2e_upload_then_sync.a1.md new file mode 100644 index 0000000..dc16fa4 --- /dev/null +++ b/.agents/reports/90_upload_no_scan/90_upload_no_scan__03_e2e_upload_then_sync.a1.md @@ -0,0 +1,11 @@ +**Task 03 complete — Phase 90 E2E: upload → (no scan) → edit ignores → Sync sources** + +- Implemented `tests/e2e/test_upload_no_scan.py` (new, 3 tests, house-style module header; per-module app env: scratch `BOR_UPLOAD_DIR`, empty `BOR_GIT_SOURCES`, mock LLM, no `slow_llm` proxy; mirrored local helpers from `test_archive_upload_sources.py`, no cross-suite imports) + - `test_upload_does_not_scan`: "Upload" button → 202 toast → "Uploaded … — press Sync sources to import it." + no-count status payload; row + `Ignore paths` control; all 3 files on host; zero docs (`/api/docs` + RAG empty state) + - `test_ignore_list_then_sync_scans`: phase-89 editor → `notes` → "1 ignored" tag + stored `ignore_paths` → RAG page `#sync-btn` → "Synced HH:MM" / "2 added" / 2-of-2 files; catalog has alpha+beta, not `notes/skipme.md` + - `test_reupload_replaces_without_scan`: v1→v2 same basename → one row, folder = only v2 files, KB empty throughout +- `uv run pytest tests/e2e/test_upload_no_scan.py -v --no-cov` → 3 passed (16.21s, isolated, DB up) +- `uv run pytest --cov=app --cov-report=term-missing` → 1808 passed, app/ coverage **99%** (gate >90% — task adds no app/ code) +- `uv run ruff check .` → clean; `uv run pyright` → 0 errors +- Notable: A4 assumption held — `app/api/sync.py` needed no change; settled-state polling (not live-label racing) used for the short sync leg, per task note. Only the new test file was added; no existing work touched. +- Next pending task: none in `90_upload_no_scan` — the phase's final task is done (harness commits/moves files). diff --git a/.agents/reports/90_upload_no_scan/90_upload_no_scan__03_e2e_upload_then_sync.a1.validate b/.agents/reports/90_upload_no_scan/90_upload_no_scan__03_e2e_upload_then_sync.a1.validate new file mode 100644 index 0000000..29f09f8 --- /dev/null +++ b/.agents/reports/90_upload_no_scan/90_upload_no_scan__03_e2e_upload_then_sync.a1.validate @@ -0,0 +1,90 @@ +........................................................................ [ 3%] +........................................................................ [ 7%] +........................................................................ [ 11%] +........................................................................ [ 15%] +........................................................................ [ 19%] +........................................................................ [ 23%] +........................................................................ [ 27%] +........................................................................ [ 31%] +........................................................................ [ 35%] +........................................................................ [ 39%] +........................................................................ [ 43%] +........................................................................ [ 47%] +........................................................................ [ 51%] +........................................................................ [ 55%] +........................................................................ [ 59%] +........................................................................ [ 63%] +........................................................................ [ 67%] +........................................................................ [ 71%] +........................................................................ [ 75%] +........................................................................ [ 79%] +........................................................................ [ 83%] +........................................................................ [ 87%] +........................................................................ [ 91%] +........................................................................ [ 95%] +........................................................................ [ 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 178 0 100% +app/api/chats.py 110 0 100% +app/api/config.py 7 0 100% +app/api/doc_drafts.py 94 0 100% +app/api/docs.py 50 0 100% +app/api/git_sources.py 229 0 100% +app/api/health.py 10 0 100% +app/api/steering.py 42 0 100% +app/api/suggestions.py 29 0 100% +app/api/sync.py 101 0 100% +app/api/tokens.py 28 0 100% +app/config.py 141 0 100% +app/core/__init__.py 0 0 100% +app/core/auth.py 45 0 100% +app/core/caching.py 108 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 19 0 100% +app/core/tokens.py 33 0 100% +app/db.py 21 0 100% +app/main.py 64 0 100% +app/models.py 95 0 100% +app/rag/__init__.py 0 0 100% +app/rag/agent.py 222 0 100% +app/rag/archive_upload.py 128 0 100% +app/rag/chunker.py 206 4 98% +app/rag/git_sources.py 14 0 100% +app/rag/importer.py 190 3 98% +app/rag/llm.py 217 0 100% +app/rag/overview.py 71 0 100% +app/rag/prompts.py 88 0 100% +app/rag/retriever.py 150 3 98% +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 24 0 100% +app/schemas.py 227 0 100% +-------------------------------------------------- +TOTAL 3263 12 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.412). +Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest` + +validation OK diff --git a/app/api/git_sources.py b/app/api/git_sources.py index 92394af..6dfea63 100644 --- a/app/api/git_sources.py +++ b/app/api/git_sources.py @@ -26,16 +26,19 @@ list: 404 unknown id, the required body list is normalized + A4- validated with fixed-detail 422s and REPLACES the row's list wholesale — an empty list clears all; 200 → the ``GitSourceOut`` shape), ``POST /upload`` (phase 49, backgrounded in phase -64 task 03 — admin archive upload: the ``.tar``/``.tar.gz``/``.tgz``/ -``.zip`` name/format gate + the 1 MiB-chunk receive with the -``upload_max_mb`` cap run **inline** and answered 202 the moment the -archive is safely on disk; unpack → swap → row upsert → model check → -single-source scan → change-gated overview then run in a **background -task** — see :func:`upload_archive` and :func:`_run_upload`), +64 task 03, scan deferred in phase 90 — admin archive upload: the +``.tar``/``.tar.gz``/``.tgz``/``.zip`` name/format gate + the 1 MiB- +chunk receive with the ``upload_max_mb`` cap run **inline** and +answered 202 the moment the archive is safely on disk; unpack → swap +→ row upsert then run in a **background task** — and nothing else: +no model check, no import, no overview refresh (phase 90, A1 — the +scan is the RAG page's "Sync sources" button's job) — see +:func:`upload_archive` and :func:`_run_upload`), ``GET /upload/status`` (the phase-32 ``SyncStatus``-shaped in-memory -state of that run — incl. the phase-64 ``current_file`` / -``files_done`` / ``files_total`` progress fields; navigating away from -the page mid-scan no longer aborts anything), ``DELETE /{source_id}`` +state of that run — the phase-64 ``current_file`` / ``files_done`` / +``files_total`` keys stay in the set but null/0/0 for the whole run: +uploads have no file-level progress, phase 90 A2; navigating away from +the page mid-upload no longer aborts anything), ``DELETE /{source_id}`` (204 — total removal, phase 69: row + the source's documents (chunks + embeddings) committed first, then the app-managed on-disk dir). The whole router sits behind :func:`app.core.auth.require_admin` — @@ -61,12 +64,16 @@ sibling row sharing the source name keeps the shared documents + files (only the row goes), and a pruned KB bumps ``sources_version`` exactly once (the phase-53 saved-chat invalidation) with a best-effort overview refresh. The upload route is the other exception -(phase 64, task 03): after the 202 receive -answer, its background task unpacks the archive, swaps it in, upserts -the row, probes the models, scans the single source -(``import_sources`` with ``prune=True`` + the change-gated overview -refresh), and lands the sync-style counts (the ``UploadOut`` fields) -in the status ``detail``. +(phase 64, task 03; phase 90): after the 202 receive +answer, its background task unpacks the archive, swaps it in, and +upserts the row — and **stops there**: no model probe, no import, no +overview refresh. The scan is the RAG page's Sync button's job +(phase 90, A1 — it gives the owner time to edit the new source's +ignore list first; the sync already imports ``kind='local'`` rows +with prune + each row's ignore list, A4). The terminal ``success`` +carries the no-count payload ``{"message": "uploaded"}`` in the status +``detail`` (phase 90, A2 — the key set is unchanged; the UI composes +the user copy). """ from __future__ import annotations @@ -98,8 +105,8 @@ from app.rag.archive_upload import ( swap_in, unpack_archive, ) -from app.rag.importer import import_sources, normalize_ignore_path -from app.rag.llm import LLMClient, check_models +from app.rag.importer import normalize_ignore_path +from app.rag.llm import LLMClient from app.rag.overview import regenerate_overview from app.rag.source_removal import ( has_sibling, @@ -151,16 +158,17 @@ class UploadStatus: Mirrors :class:`app.api.sync.SyncStatus` (the phase-32 pattern, phase 64 task 03): ``state`` is the same four-state machine (``idle`` / ``running`` / ``success`` / ``failed``); terminal states - carry the run's ``detail`` (success — the ``UploadOut`` fields) or - ``error`` (failure — sanitized) so the UI can render the last result + carry the run's ``detail`` (success — the no-count + ``{"message": "uploaded"}`` payload, phase 90 A2) or ``error`` + (failure — sanitized) so the UI can render the last result after a page reload (the re-attach behavior, task 05). - Phase 64 (task 03) progress fields: ``current_file`` is the - ``source/relative/path`` the scan is processing right now (null - outside the import phase — unpack/swap/row/model-check first — and - in terminal states); ``files_done`` / ``files_total`` carry the - hook's done/total position and survive a terminal state (the run's - last position is useful context next to the error). + Phase 64 (task 03) progress keys: ``current_file`` / + ``files_done`` / ``files_total`` stay null/0/0 for the **whole** + run (phase 90, A2 — the key set is unchanged, but uploads have no + file-level progress: unpack has no per-file hook and the scan — + the only thing that had one — moved to the sync, which keeps its + live file label). """ state: Literal["idle", "running", "success", "failed"] = "idle" @@ -390,13 +398,21 @@ def patch_git_source( async def upload_archive( file: UploadFile = File(...), # noqa: B008 ) -> UploadAccepted: - """Receive a source archive; scan it in the background (phase 49, - backgrounded in phase 64 task 03 — owner-locked A1/A2). + """Receive a source archive; unpack it and register the source row + in the background — and nothing else (phase 49, backgrounded in + phase 64 task 03, scan deferred in phase 90 — owner-locked A1/A2). + + The upload's job ends with the source row registered and the folder + on disk: no model check, no import, no overview refresh (phase 90, + A1 — the scan is the RAG page's "Sync sources" button's job, which + gives the owner time to edit the new source's ignore list first; + the sync already imports ``kind='local'`` rows with prune + the + row's ignore list, A4). The **inline (request) work is exactly three gates** — steps 1–3 — everything else runs in a background task behind ``GET /upload/status`` (the phase-32 ``SyncStatus`` pattern), so - navigating away mid-scan no longer aborts anything: + navigating away mid-upload no longer aborts anything: 1. name/format gate — only ``.tar``/``.tar.gz``/``.tgz``/``.zip`` (422 naming the accepted set) and a safe source name @@ -414,30 +430,26 @@ async def upload_archive( 4. unpack to a temp sibling (traversal/symlink/device/corrupt/ over-cap → ``failed`` with the task-01 user-safe message, temps deleted); a zero-entry archive is ``failed`` ``the archive - contains no files`` — an archive with only non-A9 files is a - VALID replacement (the scan indexes nothing, prune removes the - source's docs); + contains no files`` — an archive with only non-importable files + is a VALID replacement (the folder lands and the row registers; + what the KB indexes with it is the sync's call); 5. atomic swap-in — a same-name re-upload replaces the previous folder in place; a failure leaves the previous folder/row/KB untouched; 6. upsert the row by ``path`` (``kind='local'``; an existing row is - left as-is — ``added_at`` preserved — and the unique index is - the backstop: a concurrent insert lands ``failed`` with - ``a local source with this path already exists: ``); the - row's saved ``ignore_paths`` are captured for the scan (phase - 89: a re-upload of an existing source honors the list the owner - already saved); - 7. fail-fast ``check_models`` — ``ModelUnavailableError`` → - ``failed`` with the sanitized message (the phase-49 503 becomes - a status state, A5); the folder/row are already committed, so - the next sync/re-upload retries idempotently; - 8. ``import_sources([folder], llm, prune=True, progress=, - ignore_by_root={folder: row's list})`` (phase 89) + the - change-gated ``regenerate_overview`` — the hook feeds the status - ``current_file`` / ``files_done`` / ``files_total``; - 9. one INFO log line (PLAN §9 / AGENTS.md rule 10 — ``total_ms`` is - the background run's duration); - 10. ``success`` — ``detail`` = the ``UploadOut`` fields. + left as-is — ``added_at`` and ``ignore_paths`` preserved — and + the unique index is the backstop: a concurrent insert lands + ``failed`` with ``a local source with this path already exists: + ``); the scan the sync later performs reads the row's + ignore list straight off it (phase 89); + 7. one INFO log line (PLAN §9 / AGENTS.md rule 10 — + ``upload: finished name=… file=… bytes=… total_ms=… state=…``; + unpack+register only, no file counts — the state is ``success`` + or ``failed``, one line per run); + 8. ``success`` — ``detail = {"message": "uploaded"}`` (no count + fields, phase 90 A2), ``current_file = None``, + ``files_done = files_total = 0`` (the key set is unchanged — + the UI composes the user copy). """ # 1. Name/format gate — the accepted formats first (the 422 names # them), then the task-01 safe-name derivation. A BARE suffix @@ -488,7 +500,8 @@ async def upload_archive( ) out.write(chunk) # The archive is safely on disk — 202 is the "successfully - # uploaded" moment (A2). Steps 4–10 run in the background: + # uploaded" moment (phase 64 A2). Steps 4–8 (unpack → swap → + # row upsert — no scan, phase 90) run in the background: asyncio.create_task( _run_upload(name, filename, total, upload_root, temp_upload, temp_unpack) ) @@ -509,11 +522,12 @@ def upload_status() -> dict[str, Any]: ``GET /api/sync/status`` contract, identical key set). ``started_at`` / ``finished_at`` are ISO-8601 strings or null. - ``current_file`` (phase 64) is the ``source/relative/path`` the - scan is processing right now — null during the unpack/swap/row/ - model phases and in terminal states; ``files_done`` / ``files_total`` - carry the hook's position (0/0 idle). The router dependency makes - it admin-only like every other route here. + ``current_file`` / ``files_done`` / ``files_total`` stay null/0/0 + for the whole run (phase 90, A2 — the key set is unchanged, but + uploads have no file-level progress: the scan the progress + belonged to moved to the sync button, which keeps its live file + label). The router dependency makes it admin-only like every other + route here. """ return { "state": _upload_status.state, @@ -540,16 +554,19 @@ async def _run_upload( temp_unpack: Path, ) -> None: """The post-202 upload pipeline, one in-process background task - (the phase-32 ``_run_sync`` shape — A1). + (the phase-32 ``_run_sync`` shape — phase 64 A1). - Every failure mode (unpack, zero entries, swap, row, models, - import, anything else) lands in the ``failed`` state with a + Unpack → swap → row upsert — and nothing else (phase 90, A1: the + model check, the import, and the overview refresh are the sync's + job, not the upload's). Every failure mode (unpack, zero entries, + swap, row, anything else) lands in the ``failed`` state with a sanitized ``error`` string — a background task must die in state, - never as an unobserved exception (A5: post-202 failures are status - states, never HTTP errors). ``CancelledError`` is deliberately *not* - caught: app shutdown cancels the task, and swallowing that would - mask a real stop. The ``finally`` cleans both temps (defensive — - each step already cleans its own) and clears ``_upload_in_progress``. + never as an unobserved exception (phase 64 A5: post-202 failures + are status states, never HTTP errors). ``CancelledError`` is + deliberately *not* caught: app shutdown cancels the task, and + swallowing that would mask a real stop. The ``finally`` cleans both + temps (defensive — each step already cleans its own) and clears + ``_upload_in_progress``. """ global _upload_in_progress started = time.monotonic() @@ -561,6 +578,21 @@ async def _run_upload( _upload_status.files_total = 0 _upload_status.detail = {} _upload_status.error = None + + def _log_finished(state: str) -> None: + # Per-upload log line (PLAN §9 / AGENTS.md rule 10) — unpack+ + # register only, no file counts (the scan's counts belong to + # the sync, phase 90). One line per run, in BOTH terminal + # states; ``total_ms`` is the background run's duration. + logger.info( + "upload: finished name=%s file=%s bytes=%d total_ms=%d state=%s", + name, + filename, + total_bytes, + round((time.monotonic() - started) * 1000), + state, + ) + try: settings = get_settings() max_bytes = settings.upload_max_mb * 1024 * 1024 @@ -570,8 +602,9 @@ async def _run_upload( unpack_archive(temp_upload, temp_unpack, max_bytes) temp_upload.unlink(missing_ok=True) if not any(temp_unpack.iterdir()): - # Zero entries = a user error. (Only non-A9 files is NOT an - # error — it still has entries and is a valid replacement.) + # Zero entries = a user error. (Only non-importable files + # is NOT an error — it still has entries and is a valid + # replacement.) raise ArchiveUploadError("the archive contains no files") # Step 5 — swap in — a same-name re-upload replaces the # previous folder atomically; a failure leaves it, the row, @@ -585,8 +618,10 @@ async def _run_upload( # background task has no request session to leak locks from # (the old inline ``db.close()`` discipline, now structural). # No duplicates: an existing row is left exactly as it is - # (``added_at`` preserved); the unique index is the backstop - # for a concurrent insert the pre-check missed. + # (``added_at`` and ``ignore_paths`` preserved — the scan the + # sync performs later reads the list straight off the row, + # phase 89); the unique index is the backstop for a concurrent + # insert the pre-check missed. path = str(final_dir) db = SessionLocal() try: @@ -601,78 +636,29 @@ async def _run_upload( raise ValueError( f"a local source with this path already exists: {path}" ) from None - # Phase 89: the row's saved ignore list, copied to plain - # values while the row is still usable in this session — a - # re-upload of an existing source honors the list the owner - # already saved; a fresh row has no list yet. - ignore_paths = list(row.ignore_paths or []) finally: db.close() - # Step 7 — fail-fast models (phase 41): ``ModelUnavailableError`` - # lands in the ``failed`` state sanitized (the phase-49 503 - # becomes a status state, A5). Nothing is rolled back — the - # folder/row are committed and the next sync/re-upload retries - # idempotently. - llm = LLMClient() - await check_models(llm) - # Step 8 — scan — single source, prune (dropped files leave - # the KB), with the phase-64 progress hook feeding the status, - # then the change-gated overview refresh (phases 31/32). The - # closure captures the module ``_upload_status`` exactly like - # the state assignments above. - def _hook(source: str, rel: str, done: int, total: int) -> None: - _upload_status.current_file = f"{source}/{rel}" - _upload_status.files_done = done - _upload_status.files_total = total - - summary = await import_sources( - [final_dir], llm, prune=True, progress=_hook, - ignore_by_root={str(final_dir): ignore_paths}, - ) - overview = False - if summary.added + summary.updated > 0: - overview = await regenerate_overview(llm) - # Step 9 — per-upload log line (PLAN §9 / AGENTS.md rule 10) - # — moved with the scan: ``total_ms`` is the background run's - # duration. - logger.info( - "upload: name=%s file=%s bytes_in=%d files=%d added=%d updated=%d " - "unchanged=%d pruned=%d errors=%d overview=%s total_ms=%d", - name, - filename, - total_bytes, - summary.files, - summary.added, - summary.updated, - summary.unchanged, - summary.pruned, - summary.errors, - overview, - round((time.monotonic() - started) * 1000), - ) - # Step 10 — success: the ``UploadOut`` fields ride in the - # status ``detail`` (the UI renders the same result line from - # the status that the sync button renders from its own). + # Step 7 — the INFO line (``_log_finished`` — PLAN §9 / + # AGENTS.md rule 10) lands together with the terminal state. + # Step 8 — success: the no-count "uploaded" payload rides in + # the status ``detail`` (phase 90 A2 — the key set is + # unchanged; the scan's counts land in the SYNC's status when + # the owner presses the button, and the UI composes the + # user-facing result line from this payload). _upload_status.state = "success" _upload_status.finished_at = datetime.now(UTC) - _upload_status.current_file = None # phase 64: keep the final counts - _upload_status.detail = { - "source": name, - "files": summary.files, - "added": summary.added, - "updated": summary.updated, - "unchanged": summary.unchanged, - "pruned": summary.pruned, - "errors": summary.errors, - "chunks": summary.chunks, - "overview": overview, - } + _upload_status.current_file = None + _upload_status.files_done = 0 + _upload_status.files_total = 0 + _upload_status.detail = {"message": "uploaded"} + _log_finished(_upload_status.state) except Exception as e: # noqa: BLE001 — a background task dies in state, see above logger.exception("upload: failed") _upload_status.state = "failed" _upload_status.finished_at = datetime.now(UTC) _upload_status.error = _sanitize_error(str(e)) - _upload_status.current_file = None # phase 64: keep the final counts + _upload_status.current_file = None + _log_finished(_upload_status.state) finally: _upload_in_progress = False # No temp may survive any failure path (defensive — each step diff --git a/app/schemas.py b/app/schemas.py index 67aa946..3a63337 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -421,40 +421,18 @@ class GitSourceList(BaseModel): from_env: bool -class UploadOut(BaseModel): - """The upload run's result fields (phase 49, task 02; phase 64, task 03). - - Phase 64 (task 03): ``POST /api/git-sources/upload`` answers 202 the - moment the archive is on disk; these fields become the shape of - ``GET /api/git-sources/upload/status`` ``detail`` on ``success`` — - the uploaded source's name (filename minus the archive suffix) plus - the SAME count keys as the admin sync's success ``detail`` - (``files``, ``added``, ``updated``, ``unchanged``, ``pruned``, - ``errors``, ``chunks`` — ``app.api.sync._run_sync``) and the - ``overview`` flag: the Sources page renders the same - "N added · N pruned" result line for both. - """ - - source: str - files: int - added: int - updated: int - unchanged: int - pruned: int - errors: int - chunks: int - overview: bool - - class UploadAccepted(BaseModel): """``POST /api/git-sources/upload`` 202 response (phase 64, task 03). The archive is **safely on disk** — this is the "successfully - uploaded" moment the Sources page toasts on (owner-locked A2). The - scan itself (unpack → swap → row upsert → model check → import → - overview) runs in a background task behind + uploaded" moment the Sources page toasts on (phase 64 A2). The rest + (unpack → swap → row upsert — and nothing else: no model check, no + import, no overview refresh, phase 90 A1 — the scan is the RAG + page's "Sync sources" button's job) runs in a background task behind ``GET /api/git-sources/upload/status``, whose ``success`` ``detail`` - carries the :class:`UploadOut` fields. + carries the no-count ``{"message": "uploaded"}`` payload (phase 90 + A2 — the status key set is unchanged; the UI composes the user + copy). """ detail: str = "upload received" diff --git a/frontend/assets/git-sources.js b/frontend/assets/git-sources.js index 212c3fd..4157e7f 100644 --- a/frontend/assets/git-sources.js +++ b/frontend/assets/git-sources.js @@ -20,7 +20,7 @@ * shell — scoped lookups keep the module honest and testable). * The router mounts a view ONCE (mount-once, hide-forever), so * the bindings and the upload-progress state machine survive - * every switch: the scan poller is a self-chaining setTimeout + * every switch: the upload poller is a self-chaining setTimeout * started when an upload begins (never at boot), so progress * continues while the user is on another view, and nothing * refetches on re-show. The single toast node/timer stay module @@ -58,37 +58,43 @@ * instruction survives. 409/422 details are fixed generic strings * (credential safety — the URL is never echoed). * • upload — #archive-upload-form submit (phase 49, reworked to the - * phase-64 202 contract in task 05 — the phase-49 synchronous - * 200 paragraph is superseded): POST + * phase-64 202 contract in task 05, unpack-only in phase 90 — + * the phase-49 synchronous 200 paragraph is superseded): POST * /api/git-sources/upload with a FormData file (NO manual * Content-Type — the browser sets the multipart boundary). The * §7.4 never-stale lifecycle keeps its shape — the button - * disables + relabels "Uploading…" while the request is out — - * but the transfer is now short: the 202 arrives the moment the - * archive is safely on disk (A1). 202 → the page-local + * ("Upload") disables + relabels "Uploading…" while the request + * is out — but the transfer is short: the 202 arrives the moment + * the archive is safely on disk (A1). 202 → the page-local * "Successfully uploaded — " toast fires (showUploadToast, * the phase-55 share-toast pattern; A2: safe to navigate away), - * the file input clears, and the button hands over to the scan — - * the processing state ("Processing…", disabled, title cleared) - * plus startUploadPolling(): a 2 s poll of - * GET /api/git-sources/upload/status renders the live - * "Processing… (n/m)" label (A4 — bare during unpack; the - * full path rides the button title) and settles it: success → - * the sync-style count line (fmtUploadResult, the role=status - * result line) + the "Archive uploaded: …" announce + - * loadSources (the new/updated row lands with the Local badge; - * a re-upload refreshes the row — no duplicate; NO second toast - * — A2); failure → the sanitized server error in the role=alert - * banner + loadSources, the file selection KEPT for a one-click + * the file input clears, and the button hands over to the + * background run — the processing state (bare "Processing…", + * disabled, title cleared) plus startUploadPolling(): a 2 s poll + * of GET /api/git-sources/upload/status that renders the bare + * "Processing…" label for the WHOLE run (phase 90, A2 — the run + * is unpack + register only: no file, no "(n/m)" counts, no + * title) and settles it: success → the ready-for-sync line + * ("Uploaded — press Sync sources to import it.", + * fmtUploadResult off the status's {"message": "uploaded"} + * detail — the role=status result line) + the "Archive uploaded + * — press Sync sources to import it." announce + loadSources + * (the new/updated row lands with the Local badge; a re-upload + * refreshes the row — no duplicate; NO second toast — A2); + * failure → the sanitized server error in the role=alert banner + * + loadSources, the file selection KEPT for a one-click * re-upload. 409 (an upload is already in progress) raises NO * error banner — it re-attaches to the in-flight run (processing * state + poll, never stale). Other non-2xx (422 format/name, * 413 cap, 5xx) keep the phase-49 error banner + the kept file * selection. The submit finally restores the button ONLY when no * poll is active (§7.4). Boot re-attach (initUploadStatus, admin - * branch): a running scan re-enters the processing state + poll - * (a reload mid-scan re-attaches — no second upload), a terminal - * run re-renders its result line / error banner. + * branch): a running run re-enters the processing state + poll + * (a reload mid-run re-attaches — no second upload), a terminal + * run re-renders its result line / error banner (the safe name + * was page-local — lastUploadName is null after a reload — so the + * re-rendered line is the nameless "Uploaded — press Sync sources + * to import it."). * • remove — a row's Remove button opens the page-local * confirmation modal (#remove-confirm-dialog, a real * role="alertdialog" — the native confirm() retired, phase 69): @@ -182,9 +188,12 @@ * page's hint box matches. The Sync button still mirrors the * remaining sources (upstream file churn is pruned on that run). * The phase-49 upload is the other in-place exception: it unpacks - * and scans the single source in place (the phase-64 background task - * — 202 + status endpoint), and its counts render as the result - * line. + * and registers the source in place (the phase-64 background task — + * 202 + status endpoint) and STOPS THERE — no model check, no + * import, no overview refresh (phase 90, A1): the scan is the RAG + * page's "Sync sources" button's job (it imports the uploaded + * kind=local row with prune + the row's ignore list), and the result + * line points at that button. * * The shared header module loads through this script's own relative * import ("./header.js") — a hoisted import evaluated before this body @@ -209,7 +218,8 @@ export async function mount(root) { const addError = root.querySelector("#git-source-error"); /* Phase 49: the archive upload form (replaces the phase-38 local directory form — same card, a file input instead of a path input). - The response counts render in the role=status result line. */ + The no-count result line (phase 90) renders in the role=status + result line. */ const uploadFormEl = root.querySelector("#archive-upload-form"); const uploadFileInput = root.querySelector("#archive-upload-file"); const uploadBtn = root.querySelector("#archive-upload-btn"); @@ -821,53 +831,57 @@ export async function mount(root) { idleLabel: "Add source", }); - /* ---------- upload (POST /api/git-sources/upload) — phase 64 (task 05) ------- + /* ---------- upload (POST /api/git-sources/upload) — phase 64 (task 05), unpack-only (phase 90) ------- * The archive upload form follows the phase-64 202 contract (A1): * the file input's selection is posted as FormData (the browser sets * the multipart boundary — no manual Content-Type), and the 202 * answers the moment the archive is safely on disk — the "Uploading…" * label covers only that short receive. Then the button HANDS OVER to - * the scan: 202 → the page-local "Successfully uploaded — " - * toast (showUploadToast — A2, safe to navigate away), the file input - * clears, and the processing state ("Processing…", disabled, title - * cleared) + startUploadPolling() own it — a 2 s poll of - * GET /api/git-sources/upload/status renders the live "Processing… - * (n/m)" label (A4 — bare during unpack; the full path rides - * the button title) and settles it: success → the sync-style count - * line (fmtUploadResult) in the role=status result line + the - * "Archive uploaded: …" announce + loadSources (NO second toast — it - * already fired at the 202, A2); failure → the sanitized server - * error in the role=alert banner + loadSources, the file selection - * KEPT for a one-click re-upload. 409 (an upload is already in - * progress) raises NO error banner — it re-attaches to the in-flight - * run (processing state + poll, never stale); the phase-49 "server + * the background run (phase 90: UNPACK + REGISTER only — no scan): + * 202 → the page-local "Successfully uploaded — " toast + * (showUploadToast — A2, safe to navigate away), the file input + * clears, and the processing state (bare "Processing…", disabled, + * title cleared) + startUploadPolling() own it — a 2 s poll of + * GET /api/git-sources/upload/status that renders the bare + * "Processing…" label for the WHOLE run (phase 90, A2 — the unpack + * has no file-level progress: no file, no "(n/m)" counts, no title) + * and settles it: success → the ready-for-sync line ("Uploaded + * — press Sync sources to import it.", fmtUploadResult off + * the status's {"message": "uploaded"} detail) in the role=status + * result line + the "Archive uploaded — press Sync sources to + * import it." announce + loadSources (NO second toast — it already + * fired at the 202, A2); failure → the sanitized server error in + * the role=alert banner + loadSources, the file selection KEPT for + * a one-click re-upload. 409 (an upload is already in progress) + * raises NO error banner — it re-attaches to the in-flight run + * (processing state + poll, never stale); the phase-49 "server * detail inline for 409" branch is superseded. Other non-2xx (422 * format/name, 413 cap, 5xx) keep the phase-49 error banner + the * kept file selection; a network failure keeps the fixed line. The * submit finally restores the button ONLY when no poll is active * (PLAN §7.4 — while startUploadPolling owns the button it stays * disabled / "Processing…"). Boot re-attach (initUploadStatus, the - * admin branch): a running scan re-enters the processing state + poll + * admin branch): a running run re-enters the processing state + poll * (no second upload, no error); a terminal run re-renders its result * line (success) or error banner (failed); idle does nothing. - * (The phase-49 synchronous 200 paragraph is superseded by phase 64.) */ + * (The phase-49 synchronous 200 paragraph is superseded by phase 64; + * the phase-64 scan counts are superseded by phase 90.) */ - /* The success line's text — the sync-result shape (sources.js's - fmtSyncResult convention): "N added" always leads, then updated / - unchanged / pruned — zero parts omitted (unchanged is shown - when nothing was added or updated). Reads exactly the keys the - upload status's detail carries (task 03's UploadOut-shaped dict). */ - function fmtUploadResult(detail) { - const d = detail || {}; - const added = d.added || 0; - const updated = d.updated || 0; - const parts = [`${added} added`]; - if (updated > 0) parts.push(`${updated} updated`); - if ((d.unchanged || 0) > 0 || (added === 0 && updated === 0)) { - parts.push(`${d.unchanged || 0} unchanged`); + /* The result line's text (phase 90, A2 — the no-count contract): the + status success detail is exactly {"message": "uploaded"} — the + sync-style counts the phase-64 line rendered are gone (the scan — + and its counts — belong to the Sync button, which renders them on + the RAG page). `name` is the accepted 202's safe source name + (lastUploadName) when the run started on this page; it is null + after a reload or on the 409 re-attach (the line still points at + the next step, only without the name). */ + function fmtUploadResult(detail, name) { + if (detail && detail.message === "uploaded") { + return name + ? `Uploaded ${name} — press Sync sources to import it.` + : "Uploaded — press Sync sources to import it."; } - if ((d.pruned || 0) > 0) parts.push(`${d.pruned} pruned`); - return parts.join(" · "); + return "The upload finished."; } /* Upload-success toast (phase 64 task 05, A2 — owner-locked): the @@ -903,14 +917,15 @@ export async function mount(root) { }, UPLOAD_TOAST_MS); } - /* The scan poll (phase 64 task 05): a 2 s cadence — the SYNC_POLL_MS - * house value. Single timer, one loop at a time (the guard makes a - * double-start a no-op, and the submit finally reads this same - * variable to know whether the poll OWNS the button). Each tick - * fetches GET /api/git-sources/upload/status: running → the live - * "Processing… (n/m)" label (A4 — bare "Processing…" during - * the unpack phase, before any file is indexed; the full untruncated - * path rides the button title) + reschedule; success → stop + the + /* The background-run poll (phase 64 task 05, unpack-only in phase + * 90): a 2 s cadence — the SYNC_POLL_MS house value. Single timer, + * one loop at a time (the guard makes a double-start a no-op, and + * the submit finally reads this same variable to know whether the + * poll OWNS the button). Each tick fetches + * GET /api/git-sources/upload/status: running → the bare + * "Processing…" label for the whole run (phase 90, A2 — the unpack + * has no file-level progress: no file, no "(n/m)" counts, the title + * stays clear) + reschedule; success → stop + the ready-for-sync * result line + the announcement + the row reload (NO toast — it * fired at the 202, A2); failed → stop + the sanitized server error * banner + the row reload (a post-swap failure keeps the row — the @@ -920,6 +935,7 @@ export async function mount(root) { * retries next tick. */ const UPLOAD_POLL_MS = 2000; // the SYNC_POLL_MS house value let uploadPollTimer = null; // null = no poll active (the finally's guard) + let lastUploadName = null; // phase 90: the accepted 202's safe source name — the result line's (null after a reload / on the 409 re-attach) function stopUploadPolling() { if (uploadPollTimer !== null) { @@ -929,8 +945,9 @@ export async function mount(root) { } /* The button's processing entry (the 202 + the 409 re-attach): from - * here the poll OWNS it — disabled, "Processing…", title cleared (a - * live file lands on it at the first tick). */ + * here the poll OWNS it — disabled, bare "Processing…", title + * cleared (a live file never lands on it — phase 90: the run is + * unpack + register only, so the label stays bare). */ function enterUploadProcessingState() { uploadBtn.disabled = true; uploadBtn.textContent = "Processing…"; @@ -941,7 +958,7 @@ export async function mount(root) { * finally, which calls this ONLY when no poll is active — PLAN §7.4). */ function restoreUploadButton() { uploadBtn.disabled = false; // never stale — success OR failure - uploadBtn.textContent = "Upload & scan"; + uploadBtn.textContent = "Upload"; uploadBtn.removeAttribute("title"); } @@ -957,28 +974,27 @@ export async function mount(root) { uploadPollTimer = setTimeout(tick, UPLOAD_POLL_MS); return; } - // running: the live file label (A4 — bare "Processing…" during - // the unpack phase, before any file is indexed). + // running: the bare label for the whole background run (phase + // 90, A2 — the unpack has no file-level progress, so no file, + // no counts, and the title stays clear). if (status.state === "running") { - uploadBtn.textContent = - "Processing…" + - (status.current_file ? ` ${status.current_file}` : "") + - (status.files_total > 0 ? ` (${status.files_done}/${status.files_total})` : ""); - uploadBtn.title = status.current_file || ""; // full path on hover + uploadBtn.textContent = "Processing…"; + uploadBtn.title = ""; uploadPollTimer = setTimeout(tick, UPLOAD_POLL_MS); return; } stopUploadPolling(); if (status.state === "success") { - // The scan finished: the result line (the existing helper reads - // exactly these keys), the announcement, the row lands. NO toast - // here — it already fired at the 202 (A2). + // The run finished (unpack + register only — phase 90): the + // ready-for-sync line (fmtUploadResult reads the no-count + // detail), the announcement, the row lands. NO toast here — it + // already fired at the 202 (A2). const detail = status.detail || {}; if (uploadResult) { - uploadResult.textContent = fmtUploadResult(detail); + uploadResult.textContent = fmtUploadResult(detail, lastUploadName); uploadResult.hidden = false; } - announce(`Archive uploaded: ${detail.source}.`); + announce("Archive uploaded — press Sync sources to import it."); uploadFileInput.value = ""; restoreUploadButton(); loadSources(); // the row lands / refreshes @@ -990,7 +1006,7 @@ export async function mount(root) { // one-click re-upload, and the list reloads (a post-swap failure // keeps the row — the list state may have changed). if (uploadError) { - uploadError.textContent = status.error || "The upload scan failed."; + uploadError.textContent = status.error || "The upload failed."; uploadError.hidden = false; } restoreUploadButton(); @@ -1005,12 +1021,15 @@ export async function mount(root) { } /* Boot re-attach (phase 64 task 05, the admin branch only): fetch the - * upload status ONCE — a running scan re-enters the processing state - * + the poll (a reload mid-scan re-attaches instead of dead-ending — + * upload status ONCE — a running run re-enters the processing state + * + the poll (a reload mid-run re-attaches instead of dead-ending — * no second upload, no error); a finished run re-renders its result - * line ONLY (no announce, no toast — the toast fired at the 202, A2); - * a failed run re-renders its error banner; idle does nothing (and a - * blip is a no-op — the page boots honest either way). */ + * line ONLY (no announce, no toast — the toast fired at the 202, A2; + * the name is unknown after a reload — lastUploadName is null — so + * the line is the nameless "Uploaded — press Sync sources to import + * it.", phase 90); a failed run re-renders its error banner; idle + * does nothing (and a blip is a no-op — the page boots honest + * either way). */ async function initUploadStatus() { if (!uploadBtn) return; let status; @@ -1029,15 +1048,17 @@ export async function mount(root) { } if (status.state === "success") { // The last run's result line only — no announce, no toast (A2). + // The safe name was page-local (lastUploadName is null after a + // reload) — the line still points at the next step (phase 90). if (uploadResult) { - uploadResult.textContent = fmtUploadResult(status.detail); + uploadResult.textContent = fmtUploadResult(status.detail, lastUploadName); uploadResult.hidden = false; } return; } if (status.state === "failed") { if (uploadError) { - uploadError.textContent = status.error || "The upload scan failed."; + uploadError.textContent = status.error || "The upload failed."; uploadError.hidden = false; } } @@ -1070,11 +1091,13 @@ export async function mount(root) { body: new FormData(uploadFormEl), }); if (r.status === 202) { - // The archive is safely on disk (A1) — the "successfully - // uploaded" moment: the toast fires NOW (A2), the file input - // clears, and the scan's poll takes over the button. The 202 - // body (UploadAccepted) carries the safe source name; a body - // parse failure degrades to the picked file's name. + // The archive is safely on disk (phase 64 A1) — the + // "successfully uploaded" moment: the toast fires NOW (A2), + // the file input clears, and the background run's poll takes + // over the button. The 202 body (UploadAccepted) carries the + // safe source name (the settled result line's , phase + // 90 — recorded page-locally); a body parse failure degrades + // to the picked file's name. let name = file.name; try { const data = await r.json(); @@ -1082,6 +1105,7 @@ export async function mount(root) { } catch { /* body parse failure — the picked file's name degrades fine */ } + lastUploadName = name; showUploadToast(`Successfully uploaded — ${name}`); uploadFileInput.value = ""; // 202: the archive is on the server enterUploadProcessingState(); @@ -1111,7 +1135,7 @@ export async function mount(root) { } } finally { // Never stale (PLAN §7.4) — but ONLY when no poll owns the - // button: while startUploadPolling tracks the scan (202 / 409) + // button: while startUploadPolling tracks the run (202 / 409) // it stays disabled / "Processing…", so a finally restore here // would race the poll. No poll → the button is ours to restore. if (uploadPollTimer === null) restoreUploadButton(); @@ -1159,8 +1183,9 @@ export async function mount(root) { fetch /api/git-sources (the Sources-page gate pattern). */ root.addEventListener("bor:view-refresh", () => loadSources()); await loadSources(); - // Phase 64 (task 05): re-attach a running scan (a reload mid-scan - // resumes the Processing state) or re-render a terminal run's - // result line / error banner. + // Phase 64 (task 05): re-attach a running run (a reload mid-run + // resumes the bare Processing state) or re-render a terminal + // run's result line / error banner (phase 90: the unpack-only, + // ready-for-sync line). await initUploadStatus(); } diff --git a/frontend/assets/sources.js b/frontend/assets/sources.js index 5177cb8..7d69220 100644 --- a/frontend/assets/sources.js +++ b/frontend/assets/sources.js @@ -79,15 +79,18 @@ export async function mount(root) { * tree, in order (startSyncPolling): * 1. sync running → "Syncing… (n/m)" — bare "Syncing…" until * the import's first file (clone/pull, A4); - * 2. upload running → "Importing (n/m)" — the background - * archive scan (the "clicked upload, then opened + * 2. upload running → BARE "Importing…" — the background upload + * RUN (phase 90: unpack + register only, no + * scan — its status never carries a file or + * counts; the "clicked upload, then opened * sources" contract, A3); * 3. sync success → the phase-32 settle (counts + catalog refresh); * 4. sync failed → the phase-32 failure (banner + modal); * 5. upload success → settle "Sync sources" + catalog refresh - * (loadDocs — the new documents must appear); the - * upload's counts live on the Sources page, never - * in #sync-result (A3); + * (loadDocs — phase 90: an upload no longer + * changes the KB, the re-read is a no-op safety + * net); the upload's result line lives on the + * Sources page, never in #sync-result (A3); * 6. upload failed → settle "Sync sources" — the failure is the * Sources page's error banner, never this page's (A3); * 7. both idle → retry-ready idle. @@ -134,14 +137,16 @@ export async function mount(root) { } /* Phase 64 (task 04): the live-file label. `kind` picks the prefix — - * "sync" → "Syncing…", "upload" → "Importing" (the background scan's + * "sync" → "Syncing…", "upload" → "Importing" (the background run's * word, A3). The current file — the status endpoint's full * source/relative/path (A4) — is appended while one is being processed; - * the BARE prefix shows during the clone/pull (sync) or unpack (upload) - * phase, before any file is indexed. The counts appear only once the - * import has started (total > 0). CSS ellipsizes the button label; the - * same untruncated text goes to the button title + #sync-result (the - * aria-live announcer). */ + * the BARE prefix shows during the clone/pull (sync), before any file + * is indexed. Phase 90: the upload run is unpack + register only (no + * scan), so its status never carries a file or counts — the + * "Importing" label is always the bare one. The counts appear only + * once the import has started (total > 0). CSS ellipsizes the button + * label; the same untruncated text goes to the button title + + * #sync-result (the aria-live announcer). */ function fmtSyncLabel(kind, currentFile, done, total) { const prefix = kind === "upload" ? "Importing" : "Syncing…"; let label = currentFile ? `${prefix} ${currentFile}` : prefix; @@ -283,11 +288,12 @@ export async function mount(root) { } /* The 2 s poll (phase 64 task 04): each tick fetches BOTH jobs — the - * sync AND the background upload scan — and applies the two-job - * decision tree in order (see the section header). The 403 on the SYNC - * fetch hides the button (the whoami backstop); a 403 on the UPLOAD - * fetch is simply "no upload" (never a hide), and a network blip on - * either fetch retries next tick. */ + * sync AND the background upload run (phase 90: unpack + register, + * no scan) — and applies the two-job decision tree in order (see the + * section header). The 403 on the SYNC fetch hides the button (the + * whoami backstop); a 403 on the UPLOAD fetch is simply "no upload" + * (never a hide), and a network blip on either fetch retries next + * tick. */ function startSyncPolling() { if (syncPollTimer !== null) return; const tick = async () => { @@ -305,7 +311,8 @@ export async function mount(root) { applySyncIdle(); return; } - // The SECOND job: the background upload scan (admin-only surface). + // The SECOND job: the background upload run (phase 90: unpack + + // register only — no scan; admin-only surface). try { const ur = await fetch("/api/git-sources/upload/status"); if (ur.ok) uploadStatus = await ur.json(); @@ -406,9 +413,10 @@ export async function mount(root) { /* Load-time re-attach (ADMIN ONLY): a running run re-enters running * state, a terminal run renders its last result. Phase 64 (A3): with - * the sync IDLE, an in-flight background upload scan adopts the button - * the same way — the "user clicked upload, then opened sources" case; - * a terminal upload is a no-op (the boot-time loadDocs() already shows + * the sync IDLE, an in-flight background upload RUN (phase 90: unpack + * + register — the bare "Importing…" label) adopts the button the + * same way — the "user clicked upload, then opened sources" case; a + * terminal upload is a no-op (the boot-time loadDocs() already shows * the current catalog). */ async function initSyncButton() { if (!syncBtn) return; @@ -436,7 +444,9 @@ export async function mount(root) { applySyncFailure(status); return; } - // Sync idle: check the SECOND job — an in-flight upload scan re-attaches. + // Sync idle: check the SECOND job — an in-flight upload run + // re-attaches (the bare "Importing…" label — phase 90: unpack + + // register only). let upload; try { const ur = await fetch("/api/git-sources/upload/status"); diff --git a/frontend/index.html b/frontend/index.html index 804f1dd..68f3aff 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -372,12 +372,15 @@ + the LIVE file label while a sync runs ("Syncing… + (n/m)"), UNTRUNCATED (the button's label span ellipsizes; + screen readers hear the full source/relative path, which + also rides the button title). Phase 90: an in-flight upload + run adopts the button with the BARE "Importing…" label (the + run is unpack + register only — no scan, so its status + never carries a file or counts). After an upload settles it + stays empty — the upload's result line lives on the Sources + page (A3). -->