Files
brain-of-reese/.agent/phases/complete/32_admin_sync_button/00_phase.md
T
ducoterra 02c76ad328 chore(agent): phase roadmap from TODO.md — 8 phases (40–47), 24 tasks
Converts the 9 TODO items into an executable phase roadmap (Protocol B,
appended after phase 39):

- 40 tuning toggle anonymous flash (TODO L3)
- 41 sync fail-fast + modal when a model is down (TODO L4)
- 42 no reply autoscroll (TODO L5)
- 43 thinking scroll back — user scroll + gated autoscroll (TODO L7)
- 44 markdown tables (TODO L6)
- 45 agent unlimited tool calls behind BOR_AGENT_MAX_ROUNDS (TODO L8)
- 46 mobile hamburger nav (TODO L9)
- 47 quadlet + jinja import formats, A9 revision (TODO L10–L11)

Each phase carries a user story, a dedicated Playwright E2E suite plan,
and owner-locked decisions (R1 A9 format extension, R2 phase-37 budget
revision, A1–A5 scope decisions) confirmed 2026-08-27.

Also records the completed phases 30–39 todo/ -> complete/ moves that
were pending in the working tree. TODO.md is cleared (items now live in
.agent/phases/todo/).
2026-08-27 18:25:53 -04:00

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.chat for 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_admin for the new endpoints; the header.js whoami gate for the button.
  • 19_shared_header / 29_tuning_nav_link (complete) — the Sources page header actions area where the button lives.

Tasks

  1. 01_sync_api.md — in-process sync runner: POST /api/sync (admin, 409 when running) + GET /api/sync/status (admin).
  2. 02_ui_button.md — the admin-only button on Sources with §7.4 feedback states (polling, last-result, error banner) + frontend unit assertions.
  3. 03_e2e_and_docs.md — tests/e2e/test_sync_button.py (real file:// git fixture), README, story file, commit.

Testing & Quality

  • Integration: sync API — anonymous 403s, admin idle/running/success/failed transitions, 409 double-trigger, GitSyncError → failed with the repo named (git + import + overview mocked, as test_import_docs_git.py does).
  • Unit (frontend-assertion style, cf. tests/unit/test_shared_header.py): button markup hidden-by-default + labeled; header.js reveal; sources.js polling/terminal-state logic.
  • Coverage: >90% on app/ (app/api/sync.py fully covered); app/ TOTAL ≥ pre-change.
  • E2E (mandatory, A16): tests/e2e/test_sync_button.py — one story, run in isolation; uses a real local file:// 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-live result 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/status every 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 the kb_overview row is fresh (phase-31 trigger).
  • uv run pytest green; uv run pytest --cov=app --cov-report=term-missing TOTAL ≥ pre-change number (app/ >90%).
  • uv run pytest tests/e2e/test_sync_button.py -v --no-cov green in isolation; test_admin_auth.py, test_shared_header.py, test_import_documents.py stay green.
  • uv run ruff check . && uv run pyright clean.
  • .agent/user_stories/admin-sync-button.md exists; README documents the button (behavior, states, prerequisites).
  • One --no-gpg-sign commit 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 existing require_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 asyncio background task + a module-level status object in app/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_SOURCES only (manual --source dirs have no repo to clone; an unset/empty BOR_GIT_SOURCES → the sync fails loudly with "no git sources configured"); the import runs with prune=True so 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's regenerate_overview runs after the import when docs changed.
  • Concurrency — one sync at a time: POST /api/sync while 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-sign commit.