feat(sources): real-time file progress for sync and upload — background upload with success toast

This commit is contained in:
2026-09-01 23:51:43 -04:00
parent cddc84c7db
commit 4677d86f49
103 changed files with 5914 additions and 456 deletions
+42 -2
View File
@@ -51,7 +51,12 @@ decisions):
run aborts in the ``failed`` state before this step.
Status is in memory: a restart mid-sync loses the running state
(accepted — the next click re-syncs idempotently).
(accepted — the next click re-syncs idempotently). The status also
carries the phase-64 per-file progress — ``current_file`` (the
``source/relative/path`` the import is processing right now) plus
``files_done`` / ``files_total`` — null/0/0 before the import starts
(clone/pull reports no file yet) and in terminal states, which clear
``current_file`` but keep the run's final counts.
"""
from __future__ import annotations
@@ -107,6 +112,13 @@ class SyncStatus:
``running``, ``success``, ``failed``. Terminal states carry the run's
``detail`` (success) or ``error`` (failure) so the UI can render the
last result after a page reload (task 02's re-attach behavior).
Phase 64 (task 02) progress fields: ``current_file`` is the
``source/relative/path`` the import is processing right now (null
outside the import phase — clone/pull first, terminal states
after); ``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).
"""
state: Literal["idle", "running", "success", "failed"] = "idle"
@@ -114,6 +126,11 @@ class SyncStatus:
finished_at: datetime | None = None
detail: dict[str, Any] = field(default_factory=dict)
error: str | None = None
# Phase 64 (task 02): per-file progress — the file the import is
# processing right now and the hook's done/total position.
current_file: str | None = None
files_done: int = 0
files_total: int = 0
_status = SyncStatus()
@@ -125,6 +142,10 @@ def sync_status() -> dict[str, Any]:
"""Current sync state (the UI polls this every 2 s — task 02).
``started_at`` / ``finished_at`` are ISO-8601 strings or null.
``current_file`` (phase 64) is the ``source/relative/path`` the
import is processing right now — null during the clone/pull phase
and in terminal states; ``files_done`` / ``files_total`` carry the
hook's position (0/0 idle).
"""
return {
"state": _status.state,
@@ -132,6 +153,9 @@ def sync_status() -> dict[str, Any]:
"finished_at": _status.finished_at.isoformat() if _status.finished_at else None,
"detail": _status.detail,
"error": _status.error,
"current_file": _status.current_file,
"files_done": _status.files_done,
"files_total": _status.files_total,
}
@@ -165,6 +189,11 @@ async def _run_sync() -> None:
_status.finished_at = None
_status.detail = {}
_status.error = None
# Phase 64 (task 02): the progress fields reset with the run — no
# current file until the import starts (the clone/pull phase).
_status.current_file = None
_status.files_done = 0
_status.files_total = 0
try:
settings = get_settings()
# Step 1 (phase 41): fail fast — verify both models the sync
@@ -208,7 +237,16 @@ async def _run_sync() -> None:
if not path.is_dir():
raise GitSyncError(f"local source missing: {path}")
sources.append(path)
summary: ImportSummary = await import_sources(sources, llm, prune=True)
# Phase 64 (task 02): the per-file progress hook — the status
# endpoint reports the file being processed right now. The
# closure captures the module ``_status`` exactly like the state
# assignments above.
def _hook(source: str, rel: str, done: int, total: int) -> None:
_status.current_file = f"{source}/{rel}"
_status.files_done = done
_status.files_total = total
summary: ImportSummary = await import_sources(sources, llm, prune=True, progress=_hook)
overview = False
if summary.added + summary.updated > 0:
overview = await regenerate_overview(llm)
@@ -234,6 +272,7 @@ async def _run_sync() -> None:
db.close()
_status.state = "success"
_status.finished_at = datetime.now(UTC)
_status.current_file = None # phase 64: keep the final counts
_status.detail = {
"files": summary.files,
"added": summary.added,
@@ -253,3 +292,4 @@ async def _run_sync() -> None:
_status.state = "failed"
_status.finished_at = datetime.now(UTC)
_status.error = _sanitize_error(str(e))
_status.current_file = None # phase 64: keep the final counts