diff --git a/public_html/admin/index.php b/public_html/admin/index.php
index ce1e171..50f7d62 100644
--- a/public_html/admin/index.php
+++ b/public_html/admin/index.php
@@ -995,6 +995,17 @@ if ($action) {
$raw = curl_exec($ch); curl_close($ch);
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':
$ch = curl_init('http://127.0.0.1:7474/screenshots/purge');
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5, CURLOPT_CUSTOMREQUEST=>'DELETE']);
@@ -3119,24 +3130,33 @@ async function loadVision() {
}
gallery.innerHTML = shots.map(s => {
- const shotTs = ts(s.created_at);
- const has = s.file_size > 0;
- const meth = (s.method || 'unknown').toUpperCase();
- const dim = s.width && s.height ? `${s.width}×${s.height}` : '';
- const analysis = (s.vision_analysis || '').substring(0, 180);
+ const shotTs = ts(s.created_at);
+ const hasImg = (s.file_size || 0) > 0;
+ const meth = (s.method || 'unknown').toUpperCase();
+ const rawAnalysis = s.vision_analysis || '';
+ const isFailed = rawAnalysis.startsWith('Vision analysis unavailable');
+ const analysis = isFailed ? '' : rawAnalysis.substring(0, 200);
+ const hasAnalysis = !isFailed && rawAnalysis.length > 0;
return `
-
-
${esc(s.hostname||'unknown')}
-
${meth}
+
+
+ ${esc(s.hostname||'unknown')}
+ ${meth}
-
+
+
- ${has ? `
- ◈ ${dim ? dim + ' · ' : ''}${Math.round((s.file_size||0)/1024)}KB IMAGE
-
` : '
TEXT SNAPSHOT ONLY
'}
- ${analysis ? `
${esc(analysis)}${s.vision_analysis?.length>180?'…':''}
` : ''}
-
${shotTs}
+ ${hasImg
+ ? `
+ 🖥
+ ${Math.round((s.file_size||0)/1024)}KB · click to view
+
`
+ : '
TEXT SNAPSHOT ONLY
'}
+ ${hasAnalysis
+ ? `
${esc(analysis)}${rawAnalysis.length>200?'…':''}
`
+ : `
${isFailed?'Analysis failed — credits needed':'No analysis yet — click ◈ to analyze'}
`}
+
${shotTs}
`;
}).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)}
${imgHtml}
- ${d.vision_analysis ? `VISION ANALYSIS
- ${esc(d.vision_analysis)}` : ''}
+ ${d.vision_analysis && !d.vision_analysis.startsWith('Vision analysis unavailable') ? `
+ ◈ VISION ANALYSIS
+ ${esc(d.vision_analysis)}` :
+ `No analysis — click ◈ in gallery to analyze when credits are available.
`}
`, null, null);
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() {
let agents = _visionAgents.length ? _visionAgents : null;
if (!agents) {
@@ -4548,7 +4582,8 @@ async function loadArc() {
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-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 || []);
if (!list.length) {