diff --git a/webui/app/main.py b/webui/app/main.py index 9aefd13..acc9335 100644 --- a/webui/app/main.py +++ b/webui/app/main.py @@ -1,8 +1,11 @@ """WebUI FastAPI application.""" +import asyncio import html import os +import uuid +from datetime import datetime, timezone from pathlib import Path -from typing import List, Optional +from typing import Any, Dict, List, Optional from fastapi import FastAPI, File, Form, Request, UploadFile from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse @@ -23,6 +26,8 @@ templates.env.globals["escapeHtml"] = lambda value: html.escape(str(value or "") app.mount("/static", StaticFiles(directory=os.path.join(os.path.dirname(__file__), "static")), name="static") _client: Optional[DocsAPIClient] = None +_sync_jobs: Dict[str, Dict[str, Any]] = {} +_sync_tasks: set[asyncio.Task] = set() def get_client() -> DocsAPIClient: @@ -35,6 +40,25 @@ def get_client() -> DocsAPIClient: return _client +def utc_now() -> str: + return datetime.now(timezone.utc).isoformat() + + +async def run_sync_job(job_id: str, override: bool) -> None: + job = _sync_jobs[job_id] + job["status"] = "running" + job["started_at"] = utc_now() + try: + result = await get_client().post("/sources/sync", json={"override": override}) + job["result"] = result + job["status"] = "succeeded" if result.get("success") else "failed" + except Exception as exc: + job["status"] = "failed" + job["error"] = str(exc) + finally: + job["finished_at"] = utc_now() + + @app.on_event("shutdown") async def shutdown() -> None: if _client is not None: @@ -300,10 +324,33 @@ async def add_source( @app.post("/sources/sync") async def sync_sources(override: bool = Form(False)): - client = get_client() - try: - result = await client.post("/sources/sync", json={"override": override}) - body = f"
{html.escape(str(result))}Back"
- except Exception as e:
- body = f"{html.escape(str(e))}Back"
- return page("Git Sync", body)
+ job_id = uuid.uuid4().hex
+ _sync_jobs[job_id] = {
+ "id": job_id,
+ "status": "queued",
+ "created_at": utc_now(),
+ "started_at": None,
+ "finished_at": None,
+ "result": None,
+ "error": None,
+ }
+ task = asyncio.create_task(run_sync_job(job_id, override))
+ _sync_tasks.add(task)
+ task.add_done_callback(_sync_tasks.discard)
+ return RedirectResponse(url=f"/sources/jobs/{job_id}", status_code=303)
+
+
+@app.get("/sources/jobs/{job_id}")
+async def sync_job_page(request: Request, job_id: str):
+ job = _sync_jobs.get(job_id)
+ if job is None:
+ return page("Git Sync Not Found", "Cloning, reading, embedding, and indexing documents. Large repositories can take several minutes.
+This page updates automatically. You can leave it open or return later using the same URL.
+{% elif job.error %} +{{ job.error }}
+{% elif job.result %}
+