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:
@@ -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
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user