Record worker browser sessions
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user