feat(recorders): add Edit Recorder panel with PATCH support
- Edit (pencil) button appears on idle recorder cards; hidden while recording - openEditPanel() pre-populates all form fields from existing recorder state - openPanel() resets editingId and restores "New recorder" defaults - closePanel() clears editingId and removes any stale probe result - handleSaveRecorder() dispatches PATCH /recorders/:id in edit mode, POST otherwise - Fix field name bugs in create path: codec→recording_codec, resolution→recording_resolution, proxy_config object→proxy_enabled/proxy_codec/proxy_resolution flat fields - Badge in card now reads rec.recording_codec (correct DB field) instead of rec.codec - Bump api.js cache-buster to v=6
This commit is contained in:
parent
79d44826fe
commit
508cf8d41b
1 changed files with 122 additions and 26 deletions
|
|
@ -218,7 +218,7 @@
|
|||
<div class="slide-overlay" id="panelOverlay"></div>
|
||||
<div class="slide-panel" id="recorderPanel">
|
||||
<div class="slide-panel-header">
|
||||
<span class="slide-panel-title">New recorder</span>
|
||||
<span class="slide-panel-title" id="panelTitle">New recorder</span>
|
||||
<button class="btn btn-ghost btn-sm" id="closePanelBtn" style="padding:0;width:28px;height:28px;">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 3l10 10M13 3L3 13"/></svg>
|
||||
</button>
|
||||
|
|
@ -325,10 +325,10 @@
|
|||
|
||||
<div class="toast-container" id="toastContainer" aria-live="polite"></div>
|
||||
|
||||
<script src="js/api.js?v=5"></script>
|
||||
<script src="js/api.js?v=6"></script>
|
||||
<script src="js/topbar-strip.js?v=1"></script>
|
||||
<script>
|
||||
const pState = { recorders: [], timers: {}, sourceType: 'srt', mode: 'caller', projects: [], signals: {} };
|
||||
const pState = { recorders: [], timers: {}, sourceType: 'srt', mode: 'caller', projects: [], signals: {}, editingId: null };
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
await Promise.all([loadRecorders(), loadProjects()]);
|
||||
|
|
@ -434,10 +434,13 @@
|
|||
<div class="recorder-name">${esc(rec.name)}</div>
|
||||
<div class="recorder-badges">
|
||||
<span class="badge ${badgeClass}">${sourceTypeKey.toUpperCase()}</span>
|
||||
${rec.codec ? `<span class="badge badge-idle">${esc(rec.codec)}</span>` : ''}
|
||||
${rec.recording_codec ? `<span class="badge badge-idle">${esc(rec.recording_codec)}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="recorder-actions">
|
||||
${!isRecording ? `<button class="btn btn-ghost btn-sm" onclick="openEditPanel('${rec.id}')" title="Edit recorder" style="padding:0;width:28px;height:28px;">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 13l1.5-3.5L11 2l3 3-7.5 7.5L3 14zM10 3l3 3"/></svg>
|
||||
</button>` : ''}
|
||||
<button class="btn btn-ghost btn-sm" onclick="handleDeleteRecorder('${rec.id}')" title="Delete recorder" style="padding:0;width:28px;height:28px;">
|
||||
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 4h10M6 4V2h4v2M5 4v9h6V4"/></svg>
|
||||
</button>
|
||||
|
|
@ -593,14 +596,100 @@
|
|||
|
||||
// ── Panel ─────────────────────────────────
|
||||
function openPanel() {
|
||||
pState.editingId = null;
|
||||
document.getElementById('panelTitle').textContent = 'New recorder';
|
||||
document.getElementById('saveRecorderBtn').textContent = 'Create recorder';
|
||||
document.getElementById('probeBtn').style.display = '';
|
||||
// Reset form to defaults
|
||||
document.getElementById('recName').value = '';
|
||||
document.getElementById('recCodec').value = 'prores_hq';
|
||||
document.getElementById('recResolution').value = 'native';
|
||||
document.getElementById('proxyToggle').checked = false;
|
||||
document.getElementById('proxyFields').style.display = 'none';
|
||||
document.getElementById('recProject').value = '';
|
||||
document.getElementById('recBin').innerHTML = '<option value="">Project root</option>';
|
||||
const pr = document.getElementById('probeResult');
|
||||
if (pr) pr.remove();
|
||||
pState.sourceType = 'srt';
|
||||
document.querySelectorAll('.source-type-btn').forEach(b => b.classList.toggle('active', b.dataset.type === 'srt'));
|
||||
document.getElementById('recorderPanel').classList.add('open');
|
||||
document.getElementById('panelOverlay').classList.add('open');
|
||||
updateSourceFields();
|
||||
}
|
||||
|
||||
function openEditPanel(recId) {
|
||||
const rec = pState.recorders.find(r => r.id === recId);
|
||||
if (!rec) return;
|
||||
if (rec.status === 'recording') {
|
||||
toast('Cannot edit while recording', 'Stop the recorder first', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
pState.editingId = recId;
|
||||
document.getElementById('panelTitle').textContent = 'Edit recorder';
|
||||
document.getElementById('saveRecorderBtn').textContent = 'Save changes';
|
||||
document.getElementById('probeBtn').style.display = '';
|
||||
const pr = document.getElementById('probeResult');
|
||||
if (pr) pr.remove();
|
||||
|
||||
// Basic fields
|
||||
document.getElementById('recName').value = rec.name || '';
|
||||
document.getElementById('recCodec').value = rec.recording_codec || 'prores_hq';
|
||||
document.getElementById('recResolution').value = rec.recording_resolution || 'native';
|
||||
|
||||
// Proxy
|
||||
const proxyEnabled = !!rec.proxy_enabled;
|
||||
document.getElementById('proxyToggle').checked = proxyEnabled;
|
||||
document.getElementById('proxyFields').style.display = proxyEnabled ? 'grid' : 'none';
|
||||
if (proxyEnabled) {
|
||||
const pc = document.getElementById('proxyCodec');
|
||||
if (pc) pc.value = rec.proxy_codec || 'h264';
|
||||
const pb = document.getElementById('proxyBitrate');
|
||||
// proxy_resolution stores a value like "4000k" (set from proxy bitrate select on create)
|
||||
if (pb) pb.value = rec.proxy_resolution || '4000k';
|
||||
}
|
||||
|
||||
// Source type
|
||||
const srcType = (rec.source_type || 'srt').toLowerCase();
|
||||
pState.sourceType = srcType;
|
||||
document.querySelectorAll('.source-type-btn').forEach(b => b.classList.toggle('active', b.dataset.type === srcType));
|
||||
updateSourceFields();
|
||||
|
||||
// Populate source-specific fields after updateSourceFields injects DOM nodes
|
||||
setTimeout(() => {
|
||||
const cfg = rec.source_config || {};
|
||||
if (srcType === 'sdi') {
|
||||
const d = document.getElementById('sdiDevice');
|
||||
if (d) d.value = String(cfg.device ?? 0);
|
||||
} else if (srcType === 'srt') {
|
||||
const u = document.getElementById('srtUrl');
|
||||
if (u) u.value = cfg.url || '';
|
||||
} else if (srcType === 'rtmp') {
|
||||
const u = document.getElementById('rtmpUrl');
|
||||
if (u) u.value = cfg.url || '';
|
||||
}
|
||||
}, 0);
|
||||
|
||||
// Project / bin
|
||||
const projSel = document.getElementById('recProject');
|
||||
projSel.value = rec.project_id || '';
|
||||
document.getElementById('recBin').innerHTML = '<option value="">Project root</option>';
|
||||
if (rec.project_id) {
|
||||
handleProjectChange().then(() => {
|
||||
if (rec.bin_id) document.getElementById('recBin').value = rec.bin_id;
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('recorderPanel').classList.add('open');
|
||||
document.getElementById('panelOverlay').classList.add('open');
|
||||
}
|
||||
|
||||
function closePanel() {
|
||||
pState.editingId = null;
|
||||
document.getElementById('recorderPanel').classList.remove('open');
|
||||
document.getElementById('panelOverlay').classList.remove('open');
|
||||
const pr = document.getElementById('probeResult');
|
||||
if (pr) pr.remove();
|
||||
}
|
||||
|
||||
// ── Source type ───────────────────────────
|
||||
|
|
@ -692,7 +781,7 @@
|
|||
if (r.success) r.data.forEach(b => binSel.innerHTML += `<option value="${b.id}">${esc(b.name)}</option>`);
|
||||
}
|
||||
|
||||
// ── Save recorder ─────────────────────────
|
||||
// ── Save recorder (create or update) ──────
|
||||
|
||||
async function handleProbe() {
|
||||
const btn = document.getElementById('probeBtn');
|
||||
|
|
@ -756,16 +845,14 @@
|
|||
host.innerHTML = html;
|
||||
}
|
||||
|
||||
async function handleSaveRecorder() {
|
||||
async function handleSaveRecorder() {
|
||||
const name = document.getElementById('recName').value.trim();
|
||||
if (!name) { toast('Enter a recorder name', '', 'warning'); return; }
|
||||
|
||||
const type = pState.sourceType;
|
||||
const mode = pState.mode;
|
||||
const codec = document.getElementById('recCodec').value;
|
||||
const resolution = document.getElementById('recResolution').value;
|
||||
const recording_codec = document.getElementById('recCodec').value;
|
||||
const recording_resolution = document.getElementById('recResolution').value;
|
||||
const projectId = document.getElementById('recProject').value || null;
|
||||
const binId = document.getElementById('recBin').value || null;
|
||||
|
||||
let sourceConfig = {};
|
||||
if (type === 'sdi') {
|
||||
|
|
@ -778,25 +865,34 @@
|
|||
sourceConfig.url = document.getElementById('rtmpUrl')?.value;
|
||||
}
|
||||
|
||||
const proxy = document.getElementById('proxyToggle').checked;
|
||||
const proxyConfig = proxy ? {
|
||||
codec: document.getElementById('proxyCodec').value,
|
||||
bitrate: document.getElementById('proxyBitrate').value,
|
||||
} : null;
|
||||
|
||||
const proxy_enabled = document.getElementById('proxyToggle').checked;
|
||||
const payload = {
|
||||
name, source_type: type, source_config: sourceConfig,
|
||||
codec, resolution, project_id: projectId, bin_id: binId,
|
||||
proxy_config: proxyConfig,
|
||||
name,
|
||||
source_type: type,
|
||||
source_config: sourceConfig,
|
||||
recording_codec,
|
||||
recording_resolution,
|
||||
project_id: projectId,
|
||||
proxy_enabled,
|
||||
proxy_codec: proxy_enabled ? document.getElementById('proxyCodec').value : undefined,
|
||||
proxy_resolution: proxy_enabled ? document.getElementById('proxyBitrate').value : undefined,
|
||||
};
|
||||
|
||||
const r = await createRecorder(payload);
|
||||
if (r.success) {
|
||||
toast('Recorder created', name, 'success');
|
||||
closePanel();
|
||||
document.getElementById('recName').value = '';
|
||||
await loadRecorders();
|
||||
} else toast('Failed to create recorder', r.error, 'error');
|
||||
if (pState.editingId) {
|
||||
const r = await patchRecorder(pState.editingId, payload);
|
||||
if (r.success) {
|
||||
toast('Recorder updated', name, 'success');
|
||||
closePanel();
|
||||
await loadRecorders();
|
||||
} else toast('Failed to update recorder', r.error, 'error');
|
||||
} else {
|
||||
const r = await createRecorder(payload);
|
||||
if (r.success) {
|
||||
toast('Recorder created', name, 'success');
|
||||
closePanel();
|
||||
await loadRecorders();
|
||||
} else toast('Failed to create recorder', r.error, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function toast(title, msg, type = 'info') {
|
||||
|
|
|
|||
Loading…
Reference in a new issue