6.2 KiB
Phase 32 — Admin Sync Button (one-click doc import sync)
Source: TODO.md L5 — "Need a button that only the admin can see that triggers a doc import sync by cloning the relevant repos and then running import doc script"
Story: .agent/user_stories/admin-sync-button.md
Context: Phase 28 (scripts/git_sync.py::clone_or_pull — shallow clone / --ff-only pull; BOR_GIT_SOURCES + BOR_SOURCES_DIR; repo_name in scripts/import_docs.py), phase 31 (regenerate_overview — the sync refreshes the KB outline), phase 16 (require_admin dependency + the header.js fetchIsAdmin() reveal gate for admin-only UI like #nav-sources / #nav-tuning), PLAN §7.4 "never stale" feedback contract (the UI can never sit on a stale button state).
Objective
Give the admin a "Sync sources" button (Sources page, visible to the admin only) that triggers the full document sync in-process — clone/pull every BOR_GIT_SOURCES repo, re-import (with prune) so the KB mirrors the repos, and refresh the KB overview — with live, non-stale UI feedback driven by a polled sync-status endpoint.
Dependencies
31_kb_overview_prompt(todo) —regenerate_overview(llm)is the sync's final step;LLMClient.chatfor it.28_git_based_sources(complete) —clone_or_pull/GitSyncError/BOR_GIT_SOURCES/repo_name(the sync reuses them, does not re-implement git).16_admin_auth(complete) —require_adminfor the new endpoints; theheader.jswhoami gate for the button.19_shared_header/29_tuning_nav_link(complete) — the Sources page header actions area where the button lives.
Tasks
01_sync_api.md— in-process sync runner:POST /api/sync(admin, 409 when running) +GET /api/sync/status(admin).02_ui_button.md— the admin-only button on Sources with §7.4 feedback states (polling, last-result, error banner) + frontend unit assertions.03_e2e_and_docs.md—tests/e2e/test_sync_button.py(realfile://git fixture), README, story file, commit.
Testing & Quality
- Integration: sync API — anonymous 403s, admin idle/running/success/failed transitions, 409 double-trigger, GitSyncError →
failedwith the repo named (git + import + overview mocked, astest_import_docs_git.pydoes). - Unit (frontend-assertion style, cf.
tests/unit/test_shared_header.py): button markup hidden-by-default + labeled;header.jsreveal;sources.jspolling/terminal-state logic. - Coverage: >90% on
app/(app/api/sync.pyfully covered);app/TOTAL ≥ pre-change. - E2E (mandatory, A16):
tests/e2e/test_sync_button.py— one story, run in isolation; uses a real localfile://git repo fixture (deterministic, no network) with the mock LLM for embeddings. - UI Structure Check (AGENTS.md rule 5): labeled button, focus-visible, contrast ≥4.5:1,
aria-liveresult region, no CDN.
Completion Criteria
- Anonymous: the button is not revealed (stays
hidden) and both endpoints return 403. - Admin: clicking "Sync sources" starts the sync (202), the button goes disabled with "Syncing…" while polling
GET /api/sync/statusevery 2 s, and on completion shows the last result (Synced HH:MM+N added · M updated); a failed sync re-enables the button with an error banner (role="alert") naming the failure. - A double trigger while running returns 409 and the UI never starts a second poll loop.
- After a successful sync against the
file://fixture repo, the newly committed fixture doc appears in the Sources table and thekb_overviewrow is fresh (phase-31 trigger). uv run pytestgreen;uv run pytest --cov=app --cov-report=term-missingTOTAL ≥ pre-change number (app/ >90%).uv run pytest tests/e2e/test_sync_button.py -v --no-covgreen in isolation;test_admin_auth.py,test_shared_header.py,test_import_documents.pystay green.uv run ruff check . && uv run pyrightclean..agent/user_stories/admin-sync-button.mdexists; README documents the button (behavior, states, prerequisites).- One
--no-gpg-signcommit staging only this phase's files (e.g.feat(admin): one-click sources sync — admin-only button triggers git clone/pull + re-import + KB overview refresh with polled live status);.agent/phases/todo/32_admin_sync_button/moved to.agent/phases/complete/.
Locked decisions
- A10 extended (recorded, not a revision) — two new admin-only endpoints (
POST /api/sync,GET /api/sync/status) behind the existingrequire_admin; the public API surface stays stateless, the signed cookie remains the only session state (same pattern as/api/steering). - A12 untouched — the sync runs in-process (one
asynciobackground task + a module-level status object inapp/api/sync.py). The app is a single instance on the homelab; no Valkey/queue. Status is in memory — a restart mid-sync loses the running state (accepted: the next click re-syncs idempotently). - Sync semantics — the button targets
BOR_GIT_SOURCESonly (manual--sourcedirs have no repo to clone; an unset/emptyBOR_GIT_SOURCES→ the sync fails loudly with "no git sources configured"); the import runs withprune=Trueso files deleted upstream leave the index (the button is the canonical "mirror the repos" action — the CLI default of no-prune is unchanged); phase-31'sregenerate_overviewruns after the import when docs changed. - Concurrency — one sync at a time:
POST /api/syncwhile running →409 {"detail": "a sync is already running"}; the UI reflects the in-flight run (re-attaches on page load while a sync is running). - §7.4 adaptation (recorded) — the 120 s client guard applies to LLM turns; a sync can legitimately run for minutes (clone + embed), so the button has no client-side hard timeout — the 2 s status poll is the feedback loop and the server state is authoritative. The button is disabled until the run reaches a terminal state, so it can never be stale or stuck: a failed run re-enables it, a running run always shows "Syncing…".
- A16 / A17 honoured — one dedicated story E2E suite (real
file://git fixture — git is a documented environment prerequisite, as in phase 28); one atomic--no-gpg-signcommit.