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:
@@ -74,10 +74,54 @@ async function apiCall(url, method, body) {
|
||||
opts.body = JSON.stringify(body);
|
||||
}
|
||||
const res = await fetch(url, opts);
|
||||
return { ok: res.ok, status: res.status };
|
||||
return { ok: res.ok, status: res.status, json: res.ok ? await res.json().catch(()=>null) : null };
|
||||
} catch (e) { return { ok: false, status: 0, error: String(e) }; }
|
||||
}
|
||||
|
||||
async function requestAutoTriage() {
|
||||
const btn = document.getElementById('auto-triage-btn');
|
||||
const status = document.getElementById('auto-triage-status');
|
||||
if (!btn) return;
|
||||
btn.disabled = true;
|
||||
btn.textContent = '⏳ Requesting...';
|
||||
const r = await apiCall('/admin/triage/request-drain', 'POST');
|
||||
if (r.ok) {
|
||||
status.textContent = '✓ Requested. Host watcher runs every 2 min. Refresh this page in a minute to check progress.';
|
||||
status.className = 'auto-triage-msg ok';
|
||||
btn.textContent = '✓ Requested';
|
||||
pollDrainStatus();
|
||||
} else {
|
||||
status.textContent = '❌ Request failed: ' + r.status;
|
||||
status.className = 'auto-triage-msg err';
|
||||
btn.disabled = false;
|
||||
btn.textContent = '🤖 Auto-process queue';
|
||||
}
|
||||
}
|
||||
|
||||
async function pollDrainStatus() {
|
||||
const status = document.getElementById('auto-triage-status');
|
||||
const btn = document.getElementById('auto-triage-btn');
|
||||
let polls = 0;
|
||||
const timer = setInterval(async () => {
|
||||
polls++;
|
||||
const r = await apiCall('/admin/triage/drain-status', 'GET');
|
||||
if (!r.ok || !r.json) return;
|
||||
const s = r.json;
|
||||
if (s.is_running) {
|
||||
status.textContent = '⚙️ Auto-triage running on host... (started ' + (s.last_started_at || '?') + ')';
|
||||
status.className = 'auto-triage-msg ok';
|
||||
} else if (s.last_finished_at && !s.requested_at) {
|
||||
status.textContent = '✅ Last run finished: ' + s.last_finished_at + ' → ' + (s.last_result || 'complete');
|
||||
status.className = 'auto-triage-msg ok';
|
||||
if (btn) { btn.disabled = false; btn.textContent = '🤖 Auto-process queue'; }
|
||||
clearInterval(timer);
|
||||
// Reload page to pick up new queue state
|
||||
setTimeout(() => window.location.reload(), 3000);
|
||||
}
|
||||
if (polls > 60) { clearInterval(timer); } // stop after ~10 min of polling
|
||||
}, 10000); // poll every 10s
|
||||
}
|
||||
|
||||
function setStatus(id, msg, ok) {
|
||||
const el = document.getElementById('status-' + id);
|
||||
if (!el) return;
|
||||
@@ -185,6 +229,13 @@ _TRIAGE_CSS = """
|
||||
.cand-status.ok { color:#059669; }
|
||||
.cand-status.err { color:#dc2626; }
|
||||
.empty { text-align:center; padding:3rem; opacity:0.6; }
|
||||
.auto-triage-bar { display:flex; gap:0.8rem; align-items:center; background:var(--card); border:1px solid var(--border); border-radius:6px; padding:0.7rem 1rem; margin-bottom:1.2rem; flex-wrap:wrap; }
|
||||
.auto-triage-bar button { padding:0.55rem 1.1rem; border:1px solid var(--accent); background:var(--accent); color:white; border-radius:4px; cursor:pointer; font-weight:600; font-size:0.95rem; }
|
||||
.auto-triage-bar button:hover:not(:disabled) { opacity:0.9; }
|
||||
.auto-triage-bar button:disabled { opacity:0.5; cursor:not-allowed; }
|
||||
.auto-triage-msg { flex:1; min-width:200px; font-size:0.85rem; opacity:0.75; }
|
||||
.auto-triage-msg.ok { color:var(--accent); opacity:1; font-weight:500; }
|
||||
.auto-triage-msg.err { color:#dc2626; opacity:1; font-weight:500; }
|
||||
</style>
|
||||
"""
|
||||
|
||||
@@ -221,6 +272,15 @@ def render_triage_page(limit: int = 100) -> str:
|
||||
if needed, then promote or reject. Shortcuts: <kbd>Y</kbd> promote · <kbd>N</kbd>
|
||||
reject · <kbd>E</kbd> edit · <kbd>S</kbd> scroll to next.
|
||||
</div>
|
||||
<div class="auto-triage-bar">
|
||||
<button id="auto-triage-btn" onclick="requestAutoTriage()" title="Run auto_triage on Dalidou host">
|
||||
🤖 Auto-process queue
|
||||
</button>
|
||||
<span id="auto-triage-status" class="auto-triage-msg">
|
||||
Sends the full queue through LLM triage on the host. Promotes durable facts,
|
||||
rejects noise, leaves only ambiguous items here for you.
|
||||
</span>
|
||||
</div>
|
||||
{cards_html}
|
||||
""" + _TRIAGE_SCRIPT
|
||||
|
||||
|
||||
Reference in New Issue
Block a user