Ship War Room UX, CEO inbox, mobile chat, and Agents NAS workspace.

Make A2A togglable, keep reply context while reading, route agent data to //10.4.7.11/share/Agents, and polish themes plus export-intel sync cancel.
This commit is contained in:
Aissa
2026-08-16 22:40:32 +00:00
parent 910ba465d7
commit c5ea8cbb63
26 changed files with 2891 additions and 407 deletions
+14
View File
@@ -1372,7 +1372,9 @@ def api_sync_places(payload: SyncRequestIn) -> dict[str, Any]:
job_id = start_job("places", f"Google Places · {len(countries)} landen", total=len(countries))
results: dict[str, Any] = {}
try:
from app.export_intel_sync_progress import SyncCancelled, raise_if_cancelled
for i, c in enumerate(countries, start=1):
raise_if_cancelled()
job_step(job_id, message=f"Google Places {i}/{len(countries)}: {c}", country=c, current=i, total=len(countries))
results[c] = sync_places_country(c)
time.sleep(1)
@@ -1385,6 +1387,18 @@ def api_sync_places(payload: SyncRequestIn) -> dict[str, Any]:
return {"status": "completed", "phase": "B", "result": results}
@router.post("/sync/cancel")
def api_sync_cancel() -> dict[str, Any]:
"""Request cancellation of the currently running sync job(s)."""
from app.export_intel_sync_progress import request_cancel, get_monitor_snapshot
out = request_cancel("user")
snap = get_monitor_snapshot()
out["any_running"] = snap.get("any_running")
out["active_jobs"] = [{"id": j.get("id"), "label": j.get("label"), "status": j.get("status")} for j in snap.get("active_jobs", [])]
return out
@router.get("/sync/status")
def sync_status() -> dict[str, Any]:
from app.export_intel_sync_progress import get_monitor_snapshot
+13
View File
@@ -26,6 +26,8 @@ TED_RSS_URLS = [
from app.export_territory_seeds import BBOX_ONLY_ISO2
from app.export_intel_sync_progress import (
SyncCancelled,
raise_if_cancelled,
finish_job,
get_active_job,
job_step,
@@ -593,6 +595,7 @@ def sync_all_priority(max_priority: int = 2, region_code: Optional[str] = None)
cust: dict[str, Any] = {}
contacts: dict[str, Any] = {}
for i, c in enumerate(iso_list, start=1):
raise_if_cancelled()
job_step(job_id, message=f"Land {i}/{len(iso_list)}: {c}", country=c, step="osm", current=i, total=len(iso_list))
try:
materialize_caterer_presence(c)
@@ -610,6 +613,10 @@ def sync_all_priority(max_priority: int = 2, region_code: Optional[str] = None)
report["steps"]["google_places"] = {"skipped": True, "reason": "gratis-modus: geen Google Places"}
finish_job(job_id, result={"country_count": len(iso_list), "region": region_code})
except SyncCancelled as exc:
finish_job(job_id, error=str(exc))
report["cancelled"] = True
return report
except Exception as exc:
finish_job(job_id, error=str(exc))
raise
@@ -644,6 +651,7 @@ def sync_missing_countries(max_priority: int = 3) -> dict[str, Any]:
contacts: dict[str, Any] = {}
try:
for i, c in enumerate(iso_list, start=1):
raise_if_cancelled()
job_step(job_id, message=f"Leeg land {i}/{len(iso_list)}: {c}", country=c, current=i, total=len(iso_list))
try:
materialize_caterer_presence(c)
@@ -661,6 +669,10 @@ def sync_missing_countries(max_priority: int = 3) -> dict[str, Any]:
"contacts": contacts,
}
finish_job(job_id, result={"country_count": len(iso_list)})
except SyncCancelled as exc:
finish_job(job_id, error=str(exc))
report["cancelled"] = True
return report
except Exception as exc:
finish_job(job_id, error=str(exc))
raise
@@ -743,6 +755,7 @@ def sync_customers(country_iso2: Optional[str] = None, region_code: Optional[str
results: dict[str, Any] = {}
try:
for i, c in enumerate(countries, start=1):
raise_if_cancelled()
job_step(job_id, message=f"Horeca {i}/{len(countries)}: {c}", country=c, current=i, total=len(countries))
try:
results[c] = sync_osm_country(c, kinds=["customers"])
+41 -3
View File
@@ -11,6 +11,39 @@ STATUS_PATH = Path("/tmp/export-intel-sync-status.json")
LOG_PATH = Path("/tmp/export-intel-sync-progress.jsonl")
LOCK_PATH = Path("/tmp/export-intel-sync.lock")
CANCEL_PATH = Path("/tmp/export-intel-sync-cancel.flag")
class SyncCancelled(Exception):
"""Raised when a running sync job is cancelled by the user."""
def request_cancel(reason: str = "user") -> dict[str, Any]:
CANCEL_PATH.write_text(json.dumps({"ts": _now(), "reason": reason}))
log_event("cancel_requested", reason=reason)
# Mark running jobs as cancelling
status = _load_status()
for job in status.get("jobs", {}).values():
if job.get("status") == "running":
job["status"] = "cancelling"
job["message"] = "Annuleren…"
job["updated_at"] = _now()
_save_status(status)
return {"ok": True, "cancel_requested": True}
def clear_cancel() -> None:
CANCEL_PATH.unlink(missing_ok=True)
def is_cancel_requested() -> bool:
return CANCEL_PATH.exists()
def raise_if_cancelled() -> None:
if is_cancel_requested():
raise SyncCancelled("Sync geannuleerd door gebruiker")
_active_job_id: Optional[str] = None
@@ -68,6 +101,7 @@ def _clear_lock() -> None:
def start_job(job_type: str, label: str, total: int = 0, meta: Optional[dict[str, Any]] = None) -> str:
clear_cancel()
job_id = f"{job_type}-{_now():.0f}"
status = _load_status()
job = {
@@ -138,7 +172,10 @@ def finish_job(job_id: str, result: Optional[dict[str, Any]] = None, error: Opti
job = status["jobs"].get(job_id)
if not job:
return
job["status"] = "error" if error else "completed"
if error and "geannuleerd" in str(error).lower():
job["status"] = "cancelled"
else:
job["status"] = "error" if error else "completed"
job["updated_at"] = _now()
job["finished_at"] = _now()
job["percent"] = 100 if not error else job.get("percent", 0)
@@ -151,9 +188,10 @@ def finish_job(job_id: str, result: Optional[dict[str, Any]] = None, error: Opti
log_event("job_finish", job_id=job_id, status=job["status"], error=error)
if get_active_job() == job_id:
set_active_job(None)
running = [j for j in status["jobs"].values() if j.get("status") == "running"]
running = [j for j in status["jobs"].values() if j.get("status") in ("running", "cancelling")]
if not running:
_clear_lock()
clear_cancel()
def tail_log(limit: int = 40) -> list[dict[str, Any]]:
@@ -172,7 +210,7 @@ def tail_log(limit: int = 40) -> list[dict[str, Any]]:
def get_monitor_snapshot(db_counts: Optional[dict[str, Any]] = None) -> dict[str, Any]:
status = _load_status()
jobs = status.get("jobs", {})
active = [j for j in jobs.values() if j.get("status") == "running"]
active = [j for j in jobs.values() if j.get("status") in ("running", "cancelling")]
recent = [jobs[jid] for jid in status.get("history", [])[:8] if jid in jobs and jobs[jid].get("status") != "running"]
lock = _read_json(LOCK_PATH, None) if LOCK_PATH.exists() else None
return {