feat: on-demand auto-triage from web UI

Adds an "Auto-process queue" button to /admin/triage that lets the
user kick off a full LLM triage pass without SSH. Bridges the gap
between web UI (in container) and claude CLI (host-only).

Architecture:
- UI button POSTs to /admin/triage/request-drain
- Endpoint writes atocore/config/auto_triage_requested_at flag
- Host-side watcher cron (every 2 min) checks for the flag
- When found: clears flag, acquires lock, runs auto_triage.py,
  records progress via atocore/status/* entries
- UI polls /admin/triage/drain-status every 10s to show progress,
  auto-reloads when done

Safety:
- Lock file prevents concurrent runs on host
- Flag cleared before run so duplicate clicks queue at most one re-run
- Fail-open: watcher errors just log, don't break anything
- Status endpoint stays read-only

Installation on host (one-time):
  */2 * * * * /srv/storage/atocore/app/deploy/dalidou/auto-triage-watcher.sh \
    >> /home/papa/atocore-logs/auto-triage-watcher.log 2>&1

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-16 21:05:30 -04:00
parent d8b370fd0a
commit 271ee25d99
3 changed files with 220 additions and 1 deletions

View File

@@ -130,6 +130,57 @@ def admin_triage(limit: int = 100) -> HTMLResponse:
return HTMLResponse(content=render_triage_page(limit=limit))
@router.post("/admin/triage/request-drain")
def admin_triage_request_drain() -> dict:
"""Request a host-side auto-triage run.
Writes a flag in project state. A host cron watcher picks it up
within ~2min and runs auto_triage.py, then clears the flag.
This is the bridge between "user clicked button in web UI" and
"claude CLI (on host, not in container) runs".
"""
from datetime import datetime as _dt, timezone as _tz
from atocore.context.project_state import set_state
now = _dt.now(_tz.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
set_state(
project_name="atocore",
category="config",
key="auto_triage_requested_at",
value=now,
source="admin ui",
)
return {"requested_at": now, "note": "Host watcher will pick this up within 2 minutes."}
@router.get("/admin/triage/drain-status")
def admin_triage_drain_status() -> dict:
"""Current state of the auto-triage pipeline (for UI polling)."""
from atocore.context.project_state import get_state
out = {
"requested_at": None,
"last_started_at": None,
"last_finished_at": None,
"last_result": None,
"is_running": False,
}
try:
for e in get_state("atocore"):
if e.category == "config" and e.key == "auto_triage_requested_at":
out["requested_at"] = e.value
elif e.category == "status" and e.key == "auto_triage_last_started_at":
out["last_started_at"] = e.value
elif e.category == "status" and e.key == "auto_triage_last_finished_at":
out["last_finished_at"] = e.value
elif e.category == "status" and e.key == "auto_triage_last_result":
out["last_result"] = e.value
elif e.category == "status" and e.key == "auto_triage_running":
out["is_running"] = (e.value == "1")
except Exception:
pass
return out
# --- Request/Response models ---