});
/* ═══════════════════════════════════════════════════════════════
CODEC PILOT — tab JS
- All calls go to localhost:8094 (pilot-runner FastAPI)
+ All calls go through the same-origin dashboard proxy /api/pilot/*
+ (routes/pilot_proxy.py injects x-pilot-token server-side; pilot-runner
+ itself stays loopback-only on :8094). Works locally AND over the tunnel.
═══════════════════════════════════════════════════════════════ */
-var _pilotBase = 'http://localhost:8094';
+var _pilotBase = '/api/pilot';
var _pilotPollTimer = null;
var _pilotSelectedRun = null;
var _pilotStreamTimer = null; // fallback poll when MJPEG stream unavailable
@@ -1233,37 +1256,59 @@
Task Reports
if (dot) dot.style.background = '#22c55e';
if (text) text.textContent = 'Online · ' + (d.url || 'about:blank') + ' · CDP :' + (d.cdp_port || 9223);
document.getElementById('pilotCurrentUrl').textContent = d.url || '';
+ var pulse = document.getElementById('pilotLivePulse');
+ if (pulse) { pulse.style.background = '#22c55e'; pulse.style.boxShadow = '0 0 8px rgba(34,197,94,.8)'; }
})
.catch(function() {
var dot = document.getElementById('pilotDot');
var text = document.getElementById('pilotStatusText');
if (dot) dot.style.background = '#ef4444';
if (text) text.textContent = 'Pilot Runner offline · pm2 restart pilot-runner';
+ var pulse = document.getElementById('pilotLivePulse');
+ if (pulse) { pulse.style.background = '#666'; pulse.style.boxShadow = 'none'; }
});
}
-/* ── Screenshot / live stream ── */
+/* ── Screenshot / live stream ──
+ The MJPEG stream dies silently (sleep, dashboard restart, network
+ blip) and freezes on its last frame. Contract:
+ - pilotStartStream(): (re)connect the LIVE stream; on error, poll frames.
+ - pilotRefreshScreenshot(): "refresh" = RECONNECT the live stream — it
+ must never replace the stream with a static frame (the old freeze bug).
+ - _pilotPollFrame(): static-frame fallback used only while the stream is down. */
function pilotStartStream() {
var img = document.getElementById('pilotScreenshot');
if (!img) return;
- // Point at MJPEG stream — browser keeps it alive as a live feed
- img.src = _pilotBase + '/screenshot/stream?' + Date.now();
+ if (_pilotStreamTimer) { clearInterval(_pilotStreamTimer); _pilotStreamTimer = null; }
img.onerror = function() {
- // Stream failed (pilot offline?) — fall back to 2-second polling
+ // Stream failed (pilot offline?) — fall back to 2-second frame polling
img.onerror = null;
if (!_pilotStreamTimer) {
- _pilotStreamTimer = setInterval(pilotRefreshScreenshot, 2000);
+ _pilotStreamTimer = setInterval(_pilotPollFrame, 2000);
}
};
+ img.src = _pilotBase + '/screenshot/stream?' + Date.now();
}
-function pilotRefreshScreenshot() {
+function _pilotPollFrame() {
var img = document.getElementById('pilotScreenshot');
if (!img) return;
- // Only used as fallback when stream is unavailable
img.src = _pilotBase + '/screenshot?' + Date.now();
}
+function pilotRefreshScreenshot() {
+ pilotStartStream(); // refresh = revive the live feed
+}
+
+/* Reconnect the stream whenever the tab regains focus — img streams don't
+ survive sleep/navigation-away and there's no JS event when they die. */
+document.addEventListener('visibilitychange', function() {
+ if (!document.hidden) {
+ var panel = document.getElementById('panel-pilot');
+ if (panel && panel.classList.contains('active')) pilotStartStream();
+ }
+});
+
function pilotOpenScreenshot() {
window.open(_pilotBase + '/screenshot', '_blank');
}
@@ -1405,12 +1450,133 @@
Task Reports
document.getElementById('pilotTaskInput').value = '';
setTimeout(pilotRefreshRuns, 500);
_pilotSelectedRun = runId;
- setTimeout(function(){ pilotShowRunDetail(runId); }, 1000);
+ _pilotWatchRun(runId, task); // Operator view: live action log + HITL
});
})
.catch(function(e){ showToast('Run error: ' + e, true); });
}
+/* ── Operator live-run watcher: status chip + streaming action log + HITL ── */
+var _pilotWatchTimer = null;
+var _pilotLogCount = 0;
+
+function _agoFmt(epochSecs) {
+ if (!epochSecs) return '';
+ var s = Math.max(0, Math.round(Date.now() / 1000 - epochSecs));
+ if (s < 60) return s + 's ago';
+ if (s < 3600) return Math.round(s / 60) + 'm ago';
+ if (s < 86400) return Math.round(s / 3600) + 'h ago';
+ return Math.round(s / 86400) + 'd ago';
+}
+
+function _pilotStepLine(s, i) {
+ var icons = {navigate:'🌐', click:'🖱', type:'⌨️', scroll:'↕️', wait:'⏲', done:'✅', error:'❌'};
+ var a = (s && (s.action || s.act || s.type)) || '';
+ var icon = icons[a] || '•';
+ var det = '';
+ if (s) {
+ if (s.url) det = s.url;
+ else if (s.text) det = '"' + String(s.text).substring(0, 40) + '"';
+ else if (s.index !== undefined && s.index !== null) det = '[' + s.index + ']';
+ else if (s.result) det = String(s.result).substring(0, 60);
+ else if (s.reason) det = String(s.reason).substring(0, 60);
+ }
+ var thought = s && (s.summary || s.thought)
+ ? ' — ' + escHtml(String(s.summary || s.thought).substring(0, 70)) + '' : '';
+ return '