Fix arc reactor capabilities showing blank — fall back to handlers field

This commit is contained in:
2026-06-12 01:07:57 +00:00
parent b19e8e1b25
commit 48b912574d
+50 -15
View File
@@ -995,6 +995,17 @@ if ($action) {
$raw = curl_exec($ch); curl_close($ch); $raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw, true) ?: ['error'=>'Arc Reactor unreachable']); j(json_decode($raw, true) ?: ['error'=>'Arc Reactor unreachable']);
case 'vision_analyze':
$id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing id');
$ch = curl_init('http://127.0.0.1:7474/job');
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_POST=>true, CURLOPT_TIMEOUT=>5,
CURLOPT_POSTFIELDS=>json_encode(['type'=>'vision','payload'=>['screenshot_id'=>$id,'provider'=>'claude'],'priority'=>8,'created_by'=>'admin']),
CURLOPT_HTTPHEADER=>['Content-Type: application/json']]);
$raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw, true) ?: ['error'=>'Arc Reactor unreachable']);
break;
case 'vision_purge': case 'vision_purge':
$ch = curl_init('http://127.0.0.1:7474/screenshots/purge'); $ch = curl_init('http://127.0.0.1:7474/screenshots/purge');
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5, CURLOPT_CUSTOMREQUEST=>'DELETE']); curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5, CURLOPT_CUSTOMREQUEST=>'DELETE']);
@@ -3120,23 +3131,32 @@ async function loadVision() {
gallery.innerHTML = shots.map(s => { gallery.innerHTML = shots.map(s => {
const shotTs = ts(s.created_at); const shotTs = ts(s.created_at);
const has = s.file_size > 0; const hasImg = (s.file_size || 0) > 0;
const meth = (s.method || 'unknown').toUpperCase(); const meth = (s.method || 'unknown').toUpperCase();
const dim = s.width && s.height ? `${s.width}×${s.height}` : ''; const rawAnalysis = s.vision_analysis || '';
const analysis = (s.vision_analysis || '').substring(0, 180); const isFailed = rawAnalysis.startsWith('Vision analysis unavailable');
const analysis = isFailed ? '' : rawAnalysis.substring(0, 200);
const hasAnalysis = !isFailed && rawAnalysis.length > 0;
return `<div style="background:rgba(0,212,255,0.03);border:1px solid var(--border);border-radius:4px;overflow:hidden"> return `<div style="background:rgba(0,212,255,0.03);border:1px solid var(--border);border-radius:4px;overflow:hidden">
<div style="background:rgba(0,212,255,0.06);padding:8px 10px;display:flex;align-items:center;gap:8px"> <div style="background:rgba(0,212,255,0.06);padding:8px 10px;display:flex;align-items:center;gap:6px">
<span style="font-family:var(--mono);font-size:0.65rem;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${esc(s.hostname||'unknown')}</span> <span class="dot ${s.hostname?'dot-green':'dot-dim'}" style="flex-shrink:0"></span>
<span style="font-family:var(--mono);font-size:0.55rem;color:var(--dim)">${meth}</span> <span style="font-family:var(--mono);font-size:0.65rem;font-weight:600;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${esc(s.hostname||'unknown')}</span>
<span style="font-family:var(--mono);font-size:0.5rem;color:var(--text-dim)">${meth}</span>
<button class="btn btn-xs" onclick="visionViewScreenshot(${s.id})" style="border-color:var(--cyan);color:var(--cyan)">VIEW</button> <button class="btn btn-xs" onclick="visionViewScreenshot(${s.id})" style="border-color:var(--cyan);color:var(--cyan)">VIEW</button>
<button class="btn btn-xs" onclick="visionDeleteShot(${s.id})"></button> <button class="btn btn-xs" onclick="visionReanalyze(${s.id})" title="Re-run Claude vision analysis" style="border-color:var(--yellow);color:var(--yellow)"></button>
<button class="btn btn-xs" onclick="visionDeleteShot(${s.id})" style="border-color:var(--red);color:var(--red)"></button>
</div> </div>
<div style="padding:8px 10px"> <div style="padding:8px 10px">
${has ? `<div style="background:#060a0e;border:1px solid var(--border);border-radius:3px;padding:5px;margin-bottom:8px;cursor:pointer;font-family:var(--mono);font-size:0.6rem;color:var(--text-dim);text-align:center" onclick="visionViewScreenshot(${s.id})"> ${hasImg
${dim ? dim + ' · ' : ''}${Math.round((s.file_size||0)/1024)}KB IMAGE ? `<div style="background:#060a0e;border:1px solid var(--border);border-radius:3px;padding:6px;margin-bottom:8px;cursor:pointer;display:flex;align-items:center;gap:8px" onclick="visionViewScreenshot(${s.id})">
</div>` : '<div style="font-size:0.6rem;color:var(--dim);margin-bottom:6px;font-family:var(--mono)">TEXT SNAPSHOT ONLY</div>'} <span style="font-size:1.2rem">🖥</span>
${analysis ? `<div style="font-size:0.62rem;line-height:1.5;color:var(--text-dim)">${esc(analysis)}${s.vision_analysis?.length>180?'…':''}</div>` : ''} <span style="font-family:var(--mono);font-size:0.6rem;color:var(--text-dim)">${Math.round((s.file_size||0)/1024)}KB &nbsp;·&nbsp; click to view</span>
<div style="font-family:var(--mono);font-size:0.55rem;color:var(--border2);margin-top:6px">${shotTs}</div> </div>`
: '<div style="font-size:0.6rem;color:var(--text-dim);margin-bottom:6px;font-family:var(--mono)">TEXT SNAPSHOT ONLY</div>'}
${hasAnalysis
? `<div style="font-size:0.62rem;line-height:1.6;color:var(--text);">${esc(analysis)}${rawAnalysis.length>200?'…':''}</div>`
: `<div style="font-size:0.6rem;color:var(--text-dim);font-family:var(--mono);font-style:italic">${isFailed?'Analysis failed — credits needed':'No analysis yet — click ◈ to analyze'}</div>`}
<div style="font-family:var(--mono);font-size:0.55rem;color:var(--text-dim);opacity:0.5;margin-top:6px">${shotTs}</div>
</div> </div>
</div>`; </div>`;
}).join(''); }).join('');
@@ -3155,12 +3175,26 @@ async function visionViewScreenshot(id) {
METHOD: ${esc(d.method||'')} · ${d.width&&d.height?d.width+'×'+d.height+' · ':''} ${Math.round((d.file_size||0)/1024)}KB · ${ts(d.created_at)} METHOD: ${esc(d.method||'')} · ${d.width&&d.height?d.width+'×'+d.height+' · ':''} ${Math.round((d.file_size||0)/1024)}KB · ${ts(d.created_at)}
</div> </div>
${imgHtml} ${imgHtml}
${d.vision_analysis ? `<div style="font-size:0.55rem;letter-spacing:2px;color:var(--dim);margin-bottom:6px">VISION ANALYSIS</div> ${d.vision_analysis && !d.vision_analysis.startsWith('Vision analysis unavailable') ? `
<pre style="white-space:pre-wrap;font-size:0.65rem;line-height:1.6;color:var(--text);background:rgba(0,212,255,0.04);border:1px solid var(--border);padding:10px;border-radius:3px;max-height:300px;overflow-y:auto">${esc(d.vision_analysis)}</pre>` : ''} <div style="font-size:0.55rem;letter-spacing:2px;color:var(--text-dim);margin-bottom:6px"> VISION ANALYSIS</div>
<pre style="white-space:pre-wrap;font-size:0.65rem;line-height:1.6;color:var(--text);background:rgba(0,212,255,0.04);border:1px solid var(--border);padding:10px;border-radius:3px;max-height:300px;overflow-y:auto">${esc(d.vision_analysis)}</pre>` :
`<div style="font-size:0.6rem;color:var(--text-dim);font-family:var(--mono);padding:8px 0">No analysis — click ◈ in gallery to analyze when credits are available.</div>`}
`, null, null); `, null, null);
document.getElementById('modalSave').style.display = 'none'; document.getElementById('modalSave').style.display = 'none';
} }
async function visionReanalyze(id) {
toast('Submitting vision analysis job...', 'ok');
const d = await api('vision_analyze', {id});
if (d && d.job_id) {
toast('Analysis job #' + d.job_id + ' queued', 'ok');
setTimeout(() => loadVision(), 8000);
} else {
toast((d && d.error) || 'Failed — Arc offline or no credits', 'err');
}
}
async function visionRunScreenshot() { async function visionRunScreenshot() {
let agents = _visionAgents.length ? _visionAgents : null; let agents = _visionAgents.length ? _visionAgents : null;
if (!agents) { if (!agents) {
@@ -4548,7 +4582,8 @@ async function loadArc() {
document.getElementById('arc-done-val').textContent = s?.jobs_done ?? s?.stats?.done ?? '—'; document.getElementById('arc-done-val').textContent = s?.jobs_done ?? s?.stats?.done ?? '—';
document.getElementById('arc-fail-val').textContent = s?.jobs_failed ?? s?.stats?.failed ?? '—'; document.getElementById('arc-fail-val').textContent = s?.jobs_failed ?? s?.stats?.failed ?? '—';
document.getElementById('arc-hb-val').textContent = s?.last_heartbeat ? ts(s.last_heartbeat) : (online ? 'ALIVE' : '—'); document.getElementById('arc-hb-val').textContent = s?.last_heartbeat ? ts(s.last_heartbeat) : (online ? 'ALIVE' : '—');
document.getElementById('arc-caps-val').textContent = Array.isArray(s?.capabilities) ? s.capabilities.join(' · ') : (s?.capabilities || '—'); const caps = s?.capabilities || s?.handlers;
document.getElementById('arc-caps-val').textContent = Array.isArray(caps) ? caps.join(' · ') : (caps || '—');
const list = Array.isArray(jobs) ? jobs : (jobs?.jobs || []); const list = Array.isArray(jobs) ? jobs : (jobs?.jobs || []);
if (!list.length) { if (!list.length) {