Release: v3.1.0
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
+287
-64
@@ -4,6 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Stream-Recorder</title>
|
||||
<link rel="icon" type="image/png" href="/favicon.png">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0f1117;
|
||||
@@ -54,7 +55,15 @@
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
header h1 .rec-dot {
|
||||
header h1 .header-logo {
|
||||
width: 28px; height: 28px;
|
||||
border-radius: 50%;
|
||||
transition: filter 0.3s;
|
||||
}
|
||||
|
||||
|
||||
.rec-dot {
|
||||
display: inline-block;
|
||||
width: 10px; height: 10px;
|
||||
background: var(--recording);
|
||||
border-radius: 50%;
|
||||
@@ -307,13 +316,49 @@
|
||||
.remaining.low { color: var(--warning); }
|
||||
.remaining.critical { color: var(--error); }
|
||||
|
||||
/* Burger menu */
|
||||
.burger {
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text);
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
transition: background 0.15s;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.burger:hover { background: var(--bg-card); }
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 640px) {
|
||||
.app { padding: 10px; }
|
||||
.form-grid { grid-template-columns: 1fr; }
|
||||
.tabs { flex-wrap: wrap; }
|
||||
.tab { padding: 6px 12px; font-size: 0.8rem; }
|
||||
header { flex-direction: column; gap: 12px; align-items: flex-start; }
|
||||
.burger { display: block; }
|
||||
.tabs {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0 0 var(--radius) var(--radius);
|
||||
padding: 8px;
|
||||
z-index: 90;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
|
||||
}
|
||||
.tabs.open { display: flex; }
|
||||
.tab { padding: 10px 16px; font-size: 0.9rem; text-align: left; border-radius: 6px; }
|
||||
header {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
}
|
||||
header h1 { font-size: 1.1rem; }
|
||||
.card { padding: 12px; }
|
||||
.card-header { flex-direction: column; align-items: flex-start; gap: 8px; }
|
||||
@@ -340,6 +385,29 @@
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.card-recent { cursor: pointer; }
|
||||
.card-recent:hover { border-color: var(--accent); }
|
||||
.recent-meta { display: flex; gap: 0; font-size: 0.8rem; color: var(--text-dim); font-variant-numeric: tabular-nums; }
|
||||
.recent-meta .rm-dur { min-width: 100px; text-align: right; }
|
||||
.recent-meta .rm-size { min-width: 90px; text-align: right; }
|
||||
.recent-meta .rm-date { min-width: 150px; text-align: right; }
|
||||
@media (max-width: 640px) {
|
||||
.recent-meta { flex-wrap: wrap; gap: 4px; }
|
||||
.recent-meta .rm-dur, .recent-meta .rm-size, .recent-meta .rm-date { min-width: auto; text-align: left; }
|
||||
}
|
||||
|
||||
.rec-detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px 24px; }
|
||||
.rec-detail-item { display: flex; flex-direction: column; gap: 2px; }
|
||||
.rec-detail-item.full { grid-column: 1 / -1; }
|
||||
.rec-detail-label { font-size: 0.7rem; font-weight: 600; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.04em; }
|
||||
.rec-detail-value { font-size: 0.9rem; color: var(--text); word-break: break-all; }
|
||||
.rec-detail-value a { color: var(--accent); text-decoration: none; }
|
||||
.rec-detail-value a:hover { text-decoration: underline; }
|
||||
.rec-plik-link { display: inline-flex; align-items: center; gap: 6px; padding: 8px 14px; background: rgba(108,123,255,0.1); border: 1px solid var(--accent); border-radius: 6px; color: var(--accent); font-weight: 500; font-size: 0.85rem; text-decoration: none; transition: background 0.15s; }
|
||||
.rec-plik-link:hover { background: rgba(108,123,255,0.2); text-decoration: none; }
|
||||
.rec-detail-divider { grid-column: 1 / -1; border: none; border-top: 1px solid var(--border); margin: 4px 0; }
|
||||
@media (max-width: 640px) { .rec-detail-grid { grid-template-columns: 1fr; } }
|
||||
|
||||
.site-footer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
@@ -366,8 +434,9 @@
|
||||
<body>
|
||||
<div class="app">
|
||||
<header>
|
||||
<h1><span class="rec-dot" id="recDot"></span> Stream-Recorder</h1>
|
||||
<div class="tabs">
|
||||
<h1><img src="/logo.png" alt="Logo" class="header-logo" id="headerLogo"> Stream-Recorder</h1>
|
||||
<button class="burger" id="burgerBtn" onclick="toggleBurger()" aria-label="Menü">☰</button>
|
||||
<div class="tabs" id="navTabs">
|
||||
<button class="tab active" onclick="switchTab('dashboard')">Dashboard</button>
|
||||
<button class="tab" onclick="switchTab('jobs')">Jobs</button>
|
||||
<button class="tab" onclick="switchTab('stations')">Sender</button>
|
||||
@@ -487,7 +556,7 @@
|
||||
<option value="">URL manuell eingeben</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group full">
|
||||
<div class="form-group full" id="jobUrlGroup">
|
||||
<label>Stream-URL *</label>
|
||||
<input type="text" id="job-url" placeholder="rtmp://... oder https://...m3u8">
|
||||
</div>
|
||||
@@ -567,6 +636,23 @@
|
||||
<input type="time" id="job-schedule-stop" oninput="updateDurationField()">
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top:8px; padding:8px 12px; background:var(--bg-input); border:1px solid var(--border); border-radius:6px">
|
||||
<div style="font-size:0.8rem; color:var(--text-dim); margin-bottom:6px">Zeitpuffer</div>
|
||||
<div style="display:flex; gap:16px; flex-wrap:wrap">
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox" id="job-pre-buffer-enabled" onchange="togglePreBuffer()">
|
||||
<label for="job-pre-buffer-enabled" style="margin:0">Vorlaufzeit</label>
|
||||
<input type="number" id="job-pre-buffer" min="1" max="120" style="width:60px; display:none" oninput="updateDurationField()">
|
||||
<span id="job-pre-buffer-unit" style="font-size:0.8rem; color:var(--text-dim); display:none">Min.</span>
|
||||
</div>
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox" id="job-post-buffer-enabled" onchange="togglePostBuffer()">
|
||||
<label for="job-post-buffer-enabled" style="margin:0">Nachlaufzeit</label>
|
||||
<input type="number" id="job-post-buffer" min="1" max="120" style="width:60px; display:none" oninput="updateDurationField()">
|
||||
<span id="job-post-buffer-unit" style="font-size:0.8rem; color:var(--text-dim); display:none">Min.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -586,28 +672,7 @@
|
||||
<div id="plikFields" style="display:none; margin-top:8px; margin-left:24px">
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox" id="job-delete-after">
|
||||
<label for="job-delete-after" style="margin:0">Nach Upload löschen</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="job-section" style="margin-top:12px; border-top:1px solid var(--border); padding-top:12px">
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox" id="job-metadata-enabled" onchange="toggleMetadata()">
|
||||
<label for="job-metadata-enabled" style="margin:0">Euer-Radio Metadata-Monitor</label>
|
||||
</div>
|
||||
<div id="metadataFields" style="display:none; margin-top:8px" class="form-grid">
|
||||
<div class="form-group full">
|
||||
<label>Show-Pattern (z.B. Sendungsname)</label>
|
||||
<input type="text" id="job-metadata-pattern" placeholder="Donnerwolke">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Karenzzeit (Min.)</label>
|
||||
<input type="number" id="job-metadata-grace" value="5" min="1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Poll-Intervall (Sek.)</label>
|
||||
<input type="number" id="job-metadata-poll" value="30" min="10">
|
||||
<label for="job-delete-after" style="margin:0">File nach Upload lokal löschen</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -683,6 +748,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recording Detail Modal -->
|
||||
<div class="modal-overlay" id="recDetailModal">
|
||||
<div class="modal" style="max-width:560px">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">
|
||||
<div class="modal-title" id="recDetailTitle" style="margin:0"></div>
|
||||
<div id="recDetailBadges"></div>
|
||||
</div>
|
||||
<div class="rec-detail-grid" id="recDetailBody"></div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn" onclick="closeModal('recDetailModal')">Schließen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API = '';
|
||||
let stationsCache = [];
|
||||
@@ -699,8 +778,14 @@ async function api(path, opts = {}) {
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// Burger menu
|
||||
function toggleBurger() {
|
||||
document.getElementById('navTabs').classList.toggle('open');
|
||||
}
|
||||
|
||||
// Tabs
|
||||
function switchTab(name) {
|
||||
document.getElementById('navTabs').classList.remove('open');
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
||||
document.querySelector(`#panel-${name}`).classList.add('active');
|
||||
@@ -727,10 +812,6 @@ async function loadDashboard() {
|
||||
const active = await api('/api/recordings?active_only=true');
|
||||
const recent = await api('/api/recordings?limit=20');
|
||||
|
||||
const dot = document.getElementById('recDot');
|
||||
dot.style.background = active.length > 0 ? 'var(--recording)' : 'var(--text-dim)';
|
||||
dot.style.animation = active.length > 0 ? 'pulse 2s ease-in-out infinite' : 'none';
|
||||
|
||||
renderActive(active);
|
||||
renderRecent(recent.filter(r => r.status !== 'running' && r.status !== 'starting'));
|
||||
} catch (e) {
|
||||
@@ -752,7 +833,7 @@ function renderActive(recs) {
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">
|
||||
<span style="color:var(--recording)">●</span>
|
||||
<span class="rec-dot"></span>
|
||||
${esc(r.job_name)}
|
||||
<span class="badge badge-${r.status}">${r.status}</span>
|
||||
${r.is_scheduled ? '<span class="badge badge-scheduled">geplant</span>' : ''}
|
||||
@@ -781,21 +862,98 @@ function renderRecent(recs) {
|
||||
el.innerHTML = '<div class="empty" style="padding:24px"><span style="color:var(--text-dim)">Noch keine abgeschlossenen Aufnahmen</span></div>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = recs.slice(0, 10).map(r => `
|
||||
<div class="card" style="padding:12px">
|
||||
el.innerHTML = recs.slice(0, 10).map(r => {
|
||||
const started = r.started_at ? new Date(r.started_at).toLocaleString('de-DE') : '';
|
||||
return `
|
||||
<div class="card card-recent" style="padding:12px" onclick="openRecDetail(${r.id})">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between">
|
||||
<div>
|
||||
<span style="font-weight:500">${esc(r.job_name)}</span>
|
||||
<span class="badge badge-${r.status}" style="margin-left:8px">${r.status}</span>
|
||||
${r.plik_url ? '<span class="badge badge-completed" style="margin-left:4px">📤 Plik</span>' : ''}
|
||||
</div>
|
||||
<div class="meta">
|
||||
<span class="meta-item">🕓 ${r.elapsed_display}</span>
|
||||
<span class="meta-item">💾 ${r.total_size_display}</span>
|
||||
<span class="meta-item">${r.started_at ? new Date(r.started_at).toLocaleString('de-DE') : ''}</span>
|
||||
<div class="recent-meta">
|
||||
<span class="rm-dur">🕓 ${r.elapsed_display}</span>
|
||||
<span class="rm-size">💾 ${r.total_size_display}</span>
|
||||
<span class="rm-date">${started}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function openRecDetail(recId) {
|
||||
try {
|
||||
const r = await api('/api/recordings/' + recId);
|
||||
const started = r.started_at ? new Date(r.started_at).toLocaleString('de-DE') : '—';
|
||||
const stopped = r.stopped_at ? new Date(r.stopped_at).toLocaleString('de-DE') : '—';
|
||||
const fileName = r.output_file ? r.output_file.split('/').pop() : null;
|
||||
|
||||
document.getElementById('recDetailTitle').textContent = r.job_name;
|
||||
document.getElementById('recDetailBadges').innerHTML =
|
||||
`<span class="badge badge-${r.status}">${r.status}</span>` +
|
||||
(r.plik_url ? ' <span class="badge badge-completed">📤 Plik</span>' : '');
|
||||
|
||||
let html = '';
|
||||
|
||||
if (r.plik_url) {
|
||||
html += `<div class="rec-detail-item full">
|
||||
<span class="rec-detail-label">Plik-Download</span>
|
||||
<span class="rec-detail-value"><a class="rec-plik-link" href="${esc(r.plik_url)}" target="_blank" rel="noopener">🔗 Im Browser öffnen</a></span>
|
||||
</div>
|
||||
<hr class="rec-detail-divider">`;
|
||||
}
|
||||
|
||||
html += `
|
||||
<div class="rec-detail-item">
|
||||
<span class="rec-detail-label">Gestartet</span>
|
||||
<span class="rec-detail-value">${started}</span>
|
||||
</div>
|
||||
<div class="rec-detail-item">
|
||||
<span class="rec-detail-label">Beendet</span>
|
||||
<span class="rec-detail-value">${stopped}</span>
|
||||
</div>
|
||||
<div class="rec-detail-item">
|
||||
<span class="rec-detail-label">Dauer</span>
|
||||
<span class="rec-detail-value">${r.elapsed_display}</span>
|
||||
</div>
|
||||
<div class="rec-detail-item">
|
||||
<span class="rec-detail-label">Dateigröße</span>
|
||||
<span class="rec-detail-value">${r.total_size_display}</span>
|
||||
</div>`;
|
||||
|
||||
if (r.error_message) {
|
||||
html += `<div class="rec-detail-item full">
|
||||
<span class="rec-detail-label">Fehler</span>
|
||||
<span class="rec-detail-value" style="color:var(--error)">${esc(r.error_message)}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
html += '<hr class="rec-detail-divider">';
|
||||
|
||||
if (fileName) {
|
||||
html += `<div class="rec-detail-item full">
|
||||
<span class="rec-detail-label">Datei</span>
|
||||
<span class="rec-detail-value">${esc(fileName)}</span>
|
||||
</div>`;
|
||||
}
|
||||
if (r.output_dir) {
|
||||
html += `<div class="rec-detail-item full">
|
||||
<span class="rec-detail-label">Verzeichnis</span>
|
||||
<span class="rec-detail-value">${esc(r.output_dir)}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
html += `<div class="rec-detail-item full">
|
||||
<span class="rec-detail-label">Stream-URL</span>
|
||||
<span class="rec-detail-value">${esc(r.stream_url)}</span>
|
||||
</div>`;
|
||||
|
||||
document.getElementById('recDetailBody').innerHTML = html;
|
||||
openModal('recDetailModal');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Stations
|
||||
@@ -908,6 +1066,7 @@ function applyStationToQuickRecord() {
|
||||
function applyStationToJob() {
|
||||
const id = document.getElementById('job-station').value;
|
||||
const station = stationsCache.find(s => String(s.id) === id);
|
||||
document.getElementById('jobUrlGroup').style.display = station ? 'none' : '';
|
||||
if (!station) return;
|
||||
document.getElementById('job-url').value = station.stream_url;
|
||||
document.getElementById('job-type').value = station.stream_type || 'auto';
|
||||
@@ -945,13 +1104,12 @@ async function loadJobs() {
|
||||
${j.description ? `<div class="meta" style="margin-bottom:4px"><span class="meta-item" style="color:var(--text)">${esc(j.description)}</span></div>` : ''}
|
||||
<div class="meta">
|
||||
<span class="meta-item">${esc(j.stream_type || 'auto')}</span>
|
||||
<span class="meta-item">${esc(j.stream_url)}</span>
|
||||
<span class="meta-item">${j.station_name ? '📻 ' + esc(j.station_name) : esc(j.stream_url)}</span>
|
||||
${j.max_duration ? `<span class="meta-item">🕓 ${Math.round(j.max_duration/60)} Min</span>` : ''}
|
||||
${j.segment_duration ? `<span class="meta-item">📄 alle ${Math.round(j.segment_duration/60)} Min</span>` : ''}
|
||||
${j.schedule_enabled ? `<span class="meta-item">📅 ${j.schedule_once ? j.schedule_date : j.schedule_days} ${j.schedule_start}${j.schedule_stop ? ' - '+j.schedule_stop : ''}</span>` : ''}
|
||||
${j.schedule_enabled ? `<span class="meta-item">📅 ${j.schedule_once ? j.schedule_date : j.schedule_days} ${j.schedule_start}${j.schedule_stop ? ' - '+j.schedule_stop : ''}${j.pre_buffer ? ' (−'+j.pre_buffer+'min)' : ''}${j.post_buffer ? ' (+'+j.post_buffer+'min)' : ''}</span>` : ''}
|
||||
${j.ntfy_enabled ? `<span class="meta-item">🔔 NTFY</span>` : ''}
|
||||
${j.plik_enabled ? `<span class="meta-item">📤 Plik${j.delete_after_upload ? ' (löschen)' : ''}</span>` : ''}
|
||||
${j.metadata_monitor_enabled ? `<span class="meta-item">📡 ${esc(j.metadata_pattern)}</span>` : ''}
|
||||
${j.plik_enabled ? `<span class="meta-item">📤 Plik${j.delete_after_upload ? ' (danach löschen)' : ''}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
@@ -964,7 +1122,8 @@ function openJobModal(job = null) {
|
||||
loadStationOptions();
|
||||
document.getElementById('job-edit-id').value = job ? job.id : '';
|
||||
document.getElementById('jobModalTitle').textContent = job ? 'Job bearbeiten' : 'Neuer Job';
|
||||
document.getElementById('job-station').value = '';
|
||||
document.getElementById('job-station').value = job && job.station_id ? job.station_id : '';
|
||||
document.getElementById('jobUrlGroup').style.display = (job && job.station_id) ? 'none' : '';
|
||||
document.getElementById('job-url').value = job ? job.stream_url : '';
|
||||
document.getElementById('job-name').value = job ? job.name : '';
|
||||
document.getElementById('job-type').value = job ? job.stream_type : 'auto';
|
||||
@@ -983,14 +1142,15 @@ function openJobModal(job = null) {
|
||||
document.getElementById('job-ntfy-enabled').checked = job ? job.ntfy_enabled : false;
|
||||
document.getElementById('job-plik-enabled').checked = job ? job.plik_enabled : false;
|
||||
document.getElementById('job-delete-after').checked = job ? job.delete_after_upload : false;
|
||||
document.getElementById('job-metadata-enabled').checked = job ? job.metadata_monitor_enabled : false;
|
||||
document.getElementById('job-metadata-pattern').value = job ? job.metadata_pattern || '' : '';
|
||||
document.getElementById('job-metadata-grace').value = job ? Math.round((job.metadata_grace_period || 300) / 60) : 5;
|
||||
document.getElementById('job-metadata-poll').value = job ? job.metadata_poll_interval || 30 : 30;
|
||||
document.getElementById('job-pre-buffer-enabled').checked = job ? (job.pre_buffer || 0) > 0 : false;
|
||||
document.getElementById('job-pre-buffer').value = job && job.pre_buffer ? job.pre_buffer : '';
|
||||
document.getElementById('job-post-buffer-enabled').checked = job ? (job.post_buffer || 0) > 0 : false;
|
||||
document.getElementById('job-post-buffer').value = job && job.post_buffer ? job.post_buffer : '';
|
||||
toggleSchedule();
|
||||
toggleScheduleOnce();
|
||||
togglePreBuffer();
|
||||
togglePostBuffer();
|
||||
togglePlik();
|
||||
toggleMetadata();
|
||||
openModal('jobModal');
|
||||
}
|
||||
|
||||
@@ -1018,14 +1178,13 @@ async function saveJob() {
|
||||
schedule_start: document.getElementById('job-schedule-start').value,
|
||||
schedule_stop: document.getElementById('job-schedule-stop').value,
|
||||
schedule_end_date: document.getElementById('job-schedule-end-date').value,
|
||||
pre_buffer: document.getElementById('job-pre-buffer-enabled').checked ? parseInt(document.getElementById('job-pre-buffer').value || '0') : 0,
|
||||
post_buffer: document.getElementById('job-post-buffer-enabled').checked ? parseInt(document.getElementById('job-post-buffer').value || '0') : 0,
|
||||
description: document.getElementById('job-description').value,
|
||||
ntfy_enabled: document.getElementById('job-ntfy-enabled').checked,
|
||||
plik_enabled: document.getElementById('job-plik-enabled').checked,
|
||||
delete_after_upload: document.getElementById('job-delete-after').checked,
|
||||
metadata_monitor_enabled: document.getElementById('job-metadata-enabled').checked,
|
||||
metadata_pattern: document.getElementById('job-metadata-pattern').value,
|
||||
metadata_grace_period: parseInt(document.getElementById('job-metadata-grace').value || '5') * 60,
|
||||
metadata_poll_interval: parseInt(document.getElementById('job-metadata-poll').value || '30'),
|
||||
station_id: document.getElementById('job-station').value ? parseInt(document.getElementById('job-station').value) : null,
|
||||
};
|
||||
|
||||
if (!data.name || !data.stream_url) {
|
||||
@@ -1081,11 +1240,14 @@ function updateDurationField() {
|
||||
document.getElementById('durationAutoGroup').style.display = hasScheduleStop ? '' : 'none';
|
||||
|
||||
if (hasScheduleStop) {
|
||||
const preBuf = document.getElementById('job-pre-buffer-enabled').checked ? parseInt(document.getElementById('job-pre-buffer').value || '0') : 0;
|
||||
const postBuf = document.getElementById('job-post-buffer-enabled').checked ? parseInt(document.getElementById('job-post-buffer').value || '0') : 0;
|
||||
const bufTotal = preBuf + postBuf;
|
||||
let text;
|
||||
if (once && startDate && endDate && endDate !== startDate) {
|
||||
const startDt = new Date(`${startDate}T${start}`);
|
||||
const endDt = new Date(`${endDate}T${stop}`);
|
||||
let diffMin = Math.round((endDt - startDt) / 60000);
|
||||
let diffMin = Math.round((endDt - startDt) / 60000) + bufTotal;
|
||||
if (diffMin <= 0) { text = 'Enddatum muss nach Startdatum liegen'; }
|
||||
else {
|
||||
const d = Math.floor(diffMin / 1440);
|
||||
@@ -1096,15 +1258,18 @@ function updateDurationField() {
|
||||
if (h > 0) parts.push(`${h} Std.`);
|
||||
if (m > 0) parts.push(`${m} Min.`);
|
||||
text = `${parts.join(' ')} (mehrtägig, aus Zeitplan)`;
|
||||
if (bufTotal > 0) text += ` inkl. ${bufTotal} Min. Puffer`;
|
||||
}
|
||||
} else {
|
||||
const [sh, sm] = start.split(':').map(Number);
|
||||
const [eh, em] = stop.split(':').map(Number);
|
||||
let diff = (eh * 60 + em) - (sh * 60 + sm);
|
||||
if (diff <= 0) diff += 1440;
|
||||
diff += bufTotal;
|
||||
const h = Math.floor(diff / 60);
|
||||
const m = diff % 60;
|
||||
text = (h > 0 ? `${h} Std. ${m > 0 ? m + ' Min.' : ''}` : `${m} Min.`) + ' (automatisch aus Zeitplan)';
|
||||
text = (h > 0 ? `${h} Std. ${m > 0 ? m + ' Min.' : ''}` : `${m} Min.`) + ' (aus Zeitplan)';
|
||||
if (bufTotal > 0) text += ` inkl. ${bufTotal} Min. Puffer`;
|
||||
}
|
||||
document.getElementById('durationAutoText').textContent = text;
|
||||
}
|
||||
@@ -1118,16 +1283,31 @@ function toggleScheduleOnce() {
|
||||
updateDurationField();
|
||||
}
|
||||
|
||||
function togglePreBuffer() {
|
||||
const on = document.getElementById('job-pre-buffer-enabled').checked;
|
||||
document.getElementById('job-pre-buffer').style.display = on ? '' : 'none';
|
||||
document.getElementById('job-pre-buffer-unit').style.display = on ? '' : 'none';
|
||||
if (on && !document.getElementById('job-pre-buffer').value) {
|
||||
document.getElementById('job-pre-buffer').value = window._defaultPreBuffer || 5;
|
||||
}
|
||||
updateDurationField();
|
||||
}
|
||||
|
||||
function togglePostBuffer() {
|
||||
const on = document.getElementById('job-post-buffer-enabled').checked;
|
||||
document.getElementById('job-post-buffer').style.display = on ? '' : 'none';
|
||||
document.getElementById('job-post-buffer-unit').style.display = on ? '' : 'none';
|
||||
if (on && !document.getElementById('job-post-buffer').value) {
|
||||
document.getElementById('job-post-buffer').value = window._defaultPostBuffer || 5;
|
||||
}
|
||||
updateDurationField();
|
||||
}
|
||||
|
||||
function togglePlik() {
|
||||
document.getElementById('plikFields').style.display =
|
||||
document.getElementById('job-plik-enabled').checked ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function toggleMetadata() {
|
||||
document.getElementById('metadataFields').style.display =
|
||||
document.getElementById('job-metadata-enabled').checked ? 'grid' : 'none';
|
||||
}
|
||||
|
||||
// Quick Record
|
||||
function openQuickRecord() {
|
||||
loadStationOptions();
|
||||
@@ -1250,6 +1430,19 @@ async function loadSettings() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-section">
|
||||
<h3>Zeitplan-Puffer (Standardwerte)</h3>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label>Standard-Vorlaufzeit (Min.)</label>
|
||||
<input type="number" id="cfg-pre-buffer" min="1" max="120" value="${cfg.schedule?.default_pre_buffer ?? 5}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Standard-Nachlaufzeit (Min.)</label>
|
||||
<input type="number" id="cfg-post-buffer" min="1" max="120" value="${cfg.schedule?.default_post_buffer ?? 5}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-section">
|
||||
<h3>NTFY-Benachrichtigungen</h3>
|
||||
<div class="form-grid">
|
||||
@@ -1298,12 +1491,19 @@ async function saveSettings() {
|
||||
};
|
||||
if (passVal && passVal !== '***') authData.password = passVal;
|
||||
|
||||
window._defaultPreBuffer = parseInt(document.getElementById('cfg-pre-buffer').value) || 5;
|
||||
window._defaultPostBuffer = parseInt(document.getElementById('cfg-post-buffer').value) || 5;
|
||||
|
||||
const data = {
|
||||
recording: {
|
||||
download_path: document.getElementById('cfg-download-path').value,
|
||||
max_retries: parseInt(document.getElementById('cfg-max-retries').value) || 5,
|
||||
retry_delay: parseInt(document.getElementById('cfg-retry-delay').value) || 5,
|
||||
},
|
||||
schedule: {
|
||||
default_pre_buffer: parseInt(document.getElementById('cfg-pre-buffer').value) || 5,
|
||||
default_post_buffer: parseInt(document.getElementById('cfg-post-buffer').value) || 5,
|
||||
},
|
||||
auth: authData,
|
||||
ntfy: {
|
||||
url: document.getElementById('cfg-ntfy-url').value,
|
||||
@@ -1337,13 +1537,36 @@ document.querySelectorAll('.modal-overlay').forEach(overlay => {
|
||||
});
|
||||
});
|
||||
|
||||
// Close burger menu on outside click
|
||||
document.addEventListener('click', e => {
|
||||
const tabs = document.getElementById('navTabs');
|
||||
const burger = document.getElementById('burgerBtn');
|
||||
if (tabs.classList.contains('open') && !tabs.contains(e.target) && e.target !== burger) {
|
||||
tabs.classList.remove('open');
|
||||
}
|
||||
});
|
||||
|
||||
// Load schedule defaults from config
|
||||
async function loadDefaults() {
|
||||
try {
|
||||
const cfg = await api('/api/config');
|
||||
window._defaultPreBuffer = cfg.schedule?.default_pre_buffer ?? 5;
|
||||
window._defaultPostBuffer = cfg.schedule?.default_post_buffer ?? 5;
|
||||
} catch (e) { /* defaults stay at 5 */ }
|
||||
}
|
||||
|
||||
// Auto-refresh
|
||||
loadStationOptions();
|
||||
loadDefaults();
|
||||
loadDashboard();
|
||||
setInterval(loadDashboard, 5000);
|
||||
|
||||
api('/api/health').then(h => {
|
||||
document.getElementById('footerVersion').textContent = 'Stream-Recorder v' + h.version;
|
||||
}).catch(() => {});
|
||||
</script>
|
||||
<footer class="site-footer">
|
||||
stream-recorder © <a href="https://www.patrick-asmus.de" target="_blank" rel="noopener" style="color:var(--text-dim)">Patrick Asmus</a> · <a href="https://git.techniverse.net/scriptos/stream-recorder" target="_blank" rel="noopener" style="color:var(--accent)">Source</a>
|
||||
<span id="footerVersion">Stream-Recorder</span> © <a href="https://www.patrick-asmus.de" target="_blank" rel="noopener" style="color:var(--text-dim)">Patrick Asmus</a> · <a href="https://git.techniverse.net/scriptos/stream-recorder" target="_blank" rel="noopener" style="color:var(--accent)">Source</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 135 KiB |
Reference in New Issue
Block a user