Record worker browser sessions

This commit is contained in:
2026-08-02 23:23:23 +03:30
parent 601b8ec69e
commit 66c8c19e0c
11 changed files with 384 additions and 14 deletions
+75
View File
@@ -348,6 +348,7 @@
<div class="tab active" data-tab="dashboard">Dashboard</div>
<div class="tab" data-tab="flows">Flow Configs</div>
<div class="tab" data-tab="batch">Batch Run</div>
<div class="tab" data-tab="recordings">Recordings</div>
<div class="tab" data-tab="logs">Logs</div>
</div>
@@ -589,6 +590,29 @@
</main>
</div>
<!-- ── Recordings Tab ──────────────────────────────────────────── -->
<div class="tab-panel" id="tab-recordings">
<main>
<div class="panel" style="max-width:1100px">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:14px">
<div>
<div class="section-title" style="margin:0">Browser Session Recordings</div>
<div style="color:var(--muted);font-size:12px;margin-top:4px">The newest three completed worker sessions are retained at 12 FPS.</div>
</div>
<button class="btn" onclick="loadRecordings()" style="margin-left:auto;padding:5px 14px;font-size:12px">↻ Refresh</button>
</div>
<div style="overflow-x:auto">
<table>
<thead><tr><th>Recorded</th><th>Session</th><th>Size</th><th>Actions</th></tr></thead>
<tbody id="recordings-tbody">
<tr><td colspan="4" class="empty">No recordings yet.</td></tr>
</tbody>
</table>
</div>
</div>
</main>
</div>
<!-- ── Logs Tab ───────────────────────────────────────────────── -->
<div class="tab-panel" id="tab-logs">
<main>
@@ -712,6 +736,7 @@ document.querySelectorAll('.tab').forEach(tab => {
document.getElementById(`tab-${tab.dataset.tab}`).classList.add('active');
if (tab.dataset.tab === 'flows') loadConfigs();
if (tab.dataset.tab === 'batch') { loadConfigs(); pollBatchStatus(); loadBatchHistory(); }
if (tab.dataset.tab === 'recordings') loadRecordings();
if (tab.dataset.tab === 'logs') { loadLogs(); _startLogPoll(); }
else _stopLogPoll();
});
@@ -1421,6 +1446,56 @@ async function loadBatchHistory() {
renderBatchHistory(await res.json());
}
/* ─────────────────────────── Recordings ─────────────────────── */
function fmtBytes(bytes) {
if (!bytes) return '0 B';
const units = ['B', 'KB', 'MB', 'GB'];
const index = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
return `${(bytes / Math.pow(1024, index)).toFixed(index ? 1 : 0)} ${units[index]}`;
}
async function loadRecordings() {
const res = await apiFetch('/api/recordings');
if (!res || !res.ok) return;
const rows = await res.json();
const tbody = document.getElementById('recordings-tbody');
if (!rows.length) {
tbody.innerHTML = '<tr><td colspan="4" class="empty">No recordings yet.</td></tr>';
return;
}
tbody.innerHTML = rows.map(row => {
const encodedName = encodeURIComponent(row.name);
return `<tr>
<td style="white-space:nowrap;color:var(--muted)">${fmtTime(row.created_at)}</td>
<td style="font-family:monospace;font-size:12px">${esc(row.name)}</td>
<td style="white-space:nowrap;color:var(--muted)">${fmtBytes(row.size_bytes)}</td>
<td style="white-space:nowrap">
<button class="btn" onclick="downloadRecording('${encodedName}')" style="padding:4px 10px;font-size:12px">↓ Download</button>
<button class="btn btn-danger btn-sm" onclick="deleteRecording('${encodedName}')">Delete</button>
</td>
</tr>`;
}).join('');
}
async function downloadRecording(encodedName) {
const res = await apiFetch(`/api/recordings/${encodedName}`);
if (!res || !res.ok) return;
const blobUrl = URL.createObjectURL(await res.blob());
const link = document.createElement('a');
link.href = blobUrl;
link.download = decodeURIComponent(encodedName);
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(blobUrl);
}
async function deleteRecording(encodedName) {
if (!confirm(`Delete recording ${decodeURIComponent(encodedName)}?`)) return;
const res = await apiFetch(`/api/recordings/${encodedName}`, { method: 'DELETE' });
if (res && res.ok) await loadRecordings();
}
/* ─────────────────────────── Logs ──────────────────────────── */
let _logPollTimer = null;