feat: add human-readable RAID recovery formatting with German time units and improved member detection from mdstat

Add format_transfer_rate() to convert mdstat rates (e.g., 133628K/sec) into readable values like "130.5 MB/s" using 1024-based units. Add format_remaining_time() to convert mdstat durations (e.g., 450.1min) into compact German format like "7 Std. 30 Min." with day/hour/minute components. Update raid_status() to parse member devices directly from array_line in /proc/mdstat to avoid cross
This commit is contained in:
nessi committed 2026-08-16 12:10:17 +02:00
1 parent 5ddf506f20
commit 0c922d4f5d
3 files changed
+67 -9

No files matched your search

+6 -4
View File
@@ -1,4 +1,5 @@
<!doctype html>
<script>document.addEventListener('DOMContentLoaded',()=>{const icons={total:'mdi:harddisk',used:'mdi:database',free:'mdi:database-outline',percent:'mdi:chart-donut'};document.querySelectorAll('.storage-card .storage-icon').forEach((node,index)=>{const key=['total','used','free','percent'][index];if(!key)return;const image=document.createElement('img');image.src=`https://api.iconify.design/${icons[key]}.svg?color=${key==='used'?'%23f4d267':key==='free'?'%236ae29a':key==='percent'?'%23c59cff':'%2360a9e6'}`;image.alt='';image.setAttribute('aria-hidden','true');node.textContent='';node.appendChild(image)})});</script>
<script>document.addEventListener('DOMContentLoaded',()=>{if(!document.querySelector('link[rel="shortcut icon"]')){const favicon=document.createElement('link');favicon.rel='shortcut icon';favicon.type='image/svg+xml';favicon.href="{{ url_for('static', filename='media-mark.svg') }}?v=media-max";document.head.appendChild(favicon)}});</script>
<html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"><meta name="theme-color" content="#1f252d"><meta name="mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-capable" content="yes"><link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}"><link rel="apple-touch-icon" href="{{ url_for('static', filename='media-mark.svg') }}"><link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='media-mark.svg') }}"><title>Media Max</title>
<style>
@@ -14,7 +15,7 @@
<header><div class="brand"><div class="brand-mark" aria-label="Media Max"><svg viewBox="0 0 48 48" role="img"><path d="M10 8h28a4 4 0 0 1 4 4v24a4 4 0 0 1-4 4H10a4 4 0 0 1-4-4V12a4 4 0 0 1 4-4Z" fill="#20252d" stroke="#60a9e6" stroke-width="2"/><path d="M14 17h20M14 24h13M14 31h20" stroke="#f4c430" stroke-width="3" stroke-linecap="round"/><circle cx="34" cy="24" r="5" fill="#159447" stroke="#c9ffe0" stroke-width="1.5"/></svg></div><div class="brand-name">MEDIA</div></div><div><div class="head-title" data-de="Media Max" data-en="Media Max">Media Max</div><div class="head-sub" data-de="Audio- und Untertitelspuren aus den Mediendateien" data-en="Audio and subtitle tracks from media files">Audio- und Untertitelspuren aus den Mediendateien</div></div><div class="actions"><button class="lang-switch" id="languageToggle" title="Sprache / Language">DE</button><button class="settings-button" id="settingsOpen" title="Einstellungen">⚙</button><div class="scan-control"><select id="scanSource" aria-label="Scan-Quelle"><option value="all">Alles scannen</option><option value="radarr">Nur Radarr</option><option value="sonarr">Nur Sonarr</option></select><button id="rescan" data-de="↻ Neu scannen" data-en="↻ Rescan">↻ Neu scannen</button></div></div></header>
<main>
{% if error %}<div class="banner"><strong>Fehler:</strong> {{ error }}</div>{% endif %}
<nav class="nav"><button class="active" data-view="dashboard">▦ Dashboard</button><button data-view="radarr">RADARR · Filme</button>{% if sonarr_enabled %}<button data-view="sonarr">SONARR · Serien</button>{% endif %}<button data-view="missing">⚠ Ohne Deutsch <span class="nav-count" id="missingNavCount">{{ missing_german_movies|length + (missing_german_series|map(attribute='episodes')|map('length')|sum) }}</span></button>{% if sab_enabled %}<button data-view="downloads">SABNZBD · Downloads <span class="nav-count download-nav-count hidden" id="downloadNavCount">0</span></button>{% endif %}</nav>
<nav class="nav"><button class="active" data-view="dashboard">▦ Dashboard</button><button data-view="radarr">RADARR · Filme</button>{% if sonarr_enabled %}<button data-view="sonarr">SONARR · Serien</button>{% endif %}<button data-view="missing">⚠ Ohne Deutsch <span class="nav-count" id="missingNavCount">{{ missing_german_movies|length + (missing_german_series|map(attribute='episodes')|map('length')|sum) }}</span></button>{% if sab_enabled %}<button data-view="downloads">SABNZBD · Downloads <span class="nav-count download-nav-count hidden" id="downloadNavCount">0</span></button>{% endif %}<button data-view="datastore"><img class="nav-icon" src="https://api.iconify.design/mdi:harddisk.svg?color=%2360a9e6" alt="" aria-hidden="true"><span data-de="Datastore" data-en="Datastore">Datastore</span></button></nav>
<section id="dashboard" class="view active" data-source="dashboard"><div class="dashboard-hero"><div><div class="eyebrow">MEDIA MAX</div><h1>Dashboard</h1><p>Deine gesamte Medienbibliothek und Systemauslastung auf einen Blick.</p></div><div class="live-pill"><span class="live-dot"></span> Live</div></div><div class="overview-grid"><div class="overview-card"><span class="overview-icon movie">▣</span><div><span>Filme</span><strong>{{ rows|length }}</strong><small>Radarr Library</small></div></div><div class="overview-card"><span class="overview-icon series-icon">▤</span><div><span>Serien</span><strong>{{ series|length }}</strong><small>Sonarr Shows</small></div></div><div class="overview-card"><span class="overview-icon warning">⚠</span><div><span>Ohne Deutsch</span><strong id="dashboardMissing">{{ missing_german_movies|length + (missing_german_series|map(attribute='episodes')|map('length')|sum) }}</strong><small>Audio oder Untertitel prüfen</small></div></div><div class="overview-card"><span class="overview-icon download-icon">↓</span><div><span>Downloads</span><strong id="dashboardDownloads">—</strong><small id="dashboardRemaining">SABnzbd wird geladen …</small></div></div></div><div class="dashboard-columns"><section class="system-panel"><div class="panel-heading"><div><h2>Systemauslastung</h2><p>Aktuelle Werte aus dem Dashboard-Container</p></div><span id="systemUpdated" class="muted">Wird geladen …</span></div><div class="system-meters"><div class="meter"><div class="meter-top"><span>CPU</span><strong id="cpuValue">—</strong></div><div class="meter-track"><span id="cpuBar"></span></div></div><div class="meter"><div class="meter-top"><span>RAM</span><strong id="ramValue">—</strong></div><div class="meter-track"><span id="ramBar"></span></div></div></div><div id="diskList" class="disk-list"><div class="disk-row muted">Laufwerke werden geladen …</div></div></section><section class="service-panel"><div class="panel-heading"><div><h2>Dienste</h2><p>Verbindung und Bibliotheksstatus</p></div></div><div class="service-row"><span class="service-logo radarr-dot">R</span><div><strong>Radarr</strong><small>Filme & Qualität</small></div><span class="service-status online">Aktiv</span></div><div class="service-row"><span class="service-logo sonarr-dot">S</span><div><strong>Sonarr</strong><small>Serien & Episoden</small></div><span class="service-status {% if sonarr_enabled %}online{% else %}offline{% endif %}">{% if sonarr_enabled %}Aktiv{% else %}Nicht konfiguriert{% endif %}</span></div><div class="service-row"><span class="service-logo sab-dot">↓</span><div><strong>SABnzbd</strong><small>Downloads & Queue</small></div><span id="dashboardSabStatus" class="service-status {% if sab_enabled %}online{% else %}offline{% endif %}">{% if sab_enabled %}Wird geprüft …{% else %}Nicht konfiguriert{% endif %}</span></div></section></div></section>
<section id="radarr" class="view" data-source="radarr">
<div class="toolbar"><input id="search" type="search" placeholder="Film suchen …"><select id="audioFilter"><option value="">Alle Audio-Sprachen</option><option>Deutsch</option><option>Englisch</option><option>Japanisch</option><option>Koreanisch</option></select><select id="subFilter"><option value="">Alle Untertitel</option><option>Deutsch</option><option>Englisch</option><option>Japanisch</option><option>Koreanisch</option></select><select id="stateFilter"><option value="">Alle Status</option><option>DE + EN</option><option>Deutsch</option><option>English only</option></select></div>
@@ -36,7 +37,7 @@
<script>
const dashboardView=document.querySelector('#dashboard');if(dashboardView){dashboardView.classList.add('dashboard-view');const stats={{ dashboard_stats|tojson }};const insights=document.createElement('div');insights.className='dashboard-insights';insights.innerHTML=`<div class="insight-card"><span class="insight-icon">▣</span><div><strong>${stats.movie_files} von ${stats.movie_total} Filmen vorhanden</strong><small>${stats.movie_missing} fehlen · ${stats.movie_downloading} werden geladen</small></div></div><div class="insight-card"><span class="insight-icon">▤</span><div><strong>${stats.episode_files} von ${stats.episode_total} Episoden vorhanden</strong><small>${stats.episode_missing} fehlen in ${stats.series_total} Serien</small></div></div><div class="insight-card"><span class="insight-icon">◌</span><div><strong>Medienbestand geprüft</strong><small>Audio, Untertitel und Qualität werden überwacht</small></div></div><div class="insight-card"><span class="insight-icon">✓</span><div><strong>Live-Überwachung aktiv</strong><small>Systemwerte und Downloads aktualisieren sich automatisch</small></div></div>`;const columns=dashboardView.querySelector('.dashboard-columns');if(columns)dashboardView.insertBefore(insights,columns)}
const servicePanel=document.querySelector('.service-panel');let serviceList=null;const serviceSummary=document.createElement('span');serviceSummary.className='service-summary';if(servicePanel){servicePanel.querySelectorAll('.service-row').forEach(row=>row.remove());serviceList=document.createElement('div');serviceList.className='service-list';servicePanel.appendChild(serviceList);const heading=servicePanel.querySelector('.panel-heading');if(heading)heading.appendChild(serviceSummary)}function renderServices(data){if(!serviceList)return;const values=data.services||[];serviceSummary.textContent=`${data.online||0} / ${data.total||values.length} online`;serviceList.innerHTML=values.map(service=>`<div class="service-row service-${service.status}"><span class="service-logo service-icon-${service.key}">${service.key==='dashboard'?'⌂':service.name.slice(0,1)}</span><div><strong>${html(service.name)}</strong><small>${html(service.detail||'Keine Antwort')}</small></div><span class="service-status ${service.status}">${html(service.label)}</span></div>`).join('')}async function fetchServices(){try{renderServices(await (await fetch('/api/services')).json())}catch(error){serviceSummary.textContent='Status nicht verfügbar'}}if(servicePanel){fetchServices();setInterval(fetchServices,30000)}
const navigation=document.querySelector('.nav');if(navigation&&!navigation.querySelector('[data-view="datastore"]')){const datastoreButton=document.createElement('button');datastoreButton.dataset.view='datastore';datastoreButton.dataset.de='▣ Datastore';datastoreButton.dataset.en='▣ Datastore';datastoreButton.textContent='▣ Datastore';navigation.appendChild(datastoreButton)}const rows=[...document.querySelectorAll('#movies tbody tr')], search=document.querySelector('#search'), audio=document.querySelector('#audioFilter'), subs=document.querySelector('#subFilter'), state=document.querySelector('#stateFilter'), count=document.querySelector('#visibleCount');
const navigation=document.querySelector('.nav');if(navigation&&!navigation.querySelector('[data-view="datastore"]')){const datastoreButton=document.createElement('button');datastoreButton.dataset.view='datastore';datastoreButton.innerHTML='<img class="nav-icon" src="https://api.iconify.design/mdi:harddisk.svg?color=%2360a9e6" alt="" aria-hidden="true"><span data-de="Datastore" data-en="Datastore">Datastore</span>';navigation.appendChild(datastoreButton)}const rows=[...document.querySelectorAll('#movies tbody tr')], search=document.querySelector('#search'), audio=document.querySelector('#audioFilter'), subs=document.querySelector('#subFilter'), state=document.querySelector('#stateFilter'), count=document.querySelector('#visibleCount');
function apply(){const q=search.value.trim().toLowerCase();let n=0;rows.forEach(r=>{const ok=(!q||r.dataset.title.includes(q))&&(!audio.value||r.dataset.audio.includes(audio.value))&&(!subs.value||r.dataset.subs.includes(subs.value))&&(!state.value||r.dataset.state===state.value);r.classList.toggle('hidden',!ok);if(ok)n++});count.textContent=`${n} Filme`};[search,audio,subs,state].forEach(x=>x.addEventListener(x===search?'input':'change',apply));apply();
document.querySelectorAll('.nav button').forEach(b=>b.onclick=()=>{document.querySelectorAll('.nav button').forEach(x=>x.classList.remove('active'));document.querySelectorAll('.view').forEach(x=>x.classList.remove('active'));b.classList.add('active');document.querySelector('#'+b.dataset.view).classList.add('active');const rescan=document.querySelector('#rescan'),sourceSelect=document.querySelector('#scanSource'),canRescan=['radarr','sonarr'].includes(b.dataset.view);if(canRescan)sourceSelect.value=b.dataset.view;rescan.disabled=false;rescan.title='Bibliothek neu scannen';sessionStorage.setItem('dashboard-active-view',b.dataset.view)});const rememberedView=sessionStorage.getItem('dashboard-active-view');if(rememberedView)document.querySelector(`.nav button[data-view="${rememberedView}"]`)?.click();
function statusText(s){if(s.state==='running'){const pct=s.total?Math.round(s.done/s.total*100):0;return `Scan läuft · ${s.done}/${s.total||'?'} Dateien · ${pct}%`}if(s.state==='done')return 'Scan abgeschlossen ✓';if(s.state==='error')return 'Scanfehler: '+s.error;return 'Scan wird vorbereitet …'}
@@ -50,7 +51,7 @@ let autoScanTimer=null;function configureAutoScan(){if(autoScanTimer)clearInterv
const seasonZeroLabel=document.createElement('label');seasonZeroLabel.className='setting-toggle';seasonZeroLabel.htmlFor='showSeasonZero';seasonZeroLabel.innerHTML='<input type="checkbox" id="showSeasonZero"><span>Staffel 0 anzeigen</span>';const seasonZeroHelp=document.createElement('span');seasonZeroHelp.className='setting-help';seasonZeroHelp.textContent='Gilt für Sonarr und die Liste Ohne Deutsch.';if(settingsActions){settingsActions.before(seasonZeroLabel,seasonZeroHelp)}
function applySeasonZeroPreference(){const show=localStorage.getItem('show-season-zero')==='true';const rows=[...document.querySelectorAll('#sonarr tbody tr:not(.season-divider),#missing .missing-series tbody tr:not(.season-divider)')];rows.forEach(row=>{const title=row.querySelector('.title')?.textContent.trim()||'';row.classList.toggle('season-zero-hidden',!show&&/^S00E/i.test(title))});document.querySelectorAll('#sonarr details.series,#missing details.missing-series').forEach(detail=>{const episodeRows=[...detail.querySelectorAll('tbody tr:not(.season-divider)')];detail.classList.toggle('season-zero-empty',episodeRows.length>0&&episodeRows.every(row=>row.classList.contains('season-zero-hidden')))});const missingCount=document.querySelector('#missingCount'),missingNavCount=document.querySelector('#missingNavCount');if(missingCount||missingNavCount){const movies=document.querySelectorAll('#missingMovies tbody tr:not(.hidden)').length,episodes=document.querySelectorAll('#missing .missing-series tbody tr:not(.season-divider):not(.season-zero-hidden)').length,total=movies+episodes;if(missingCount)missingCount.textContent=`${total} Einträge ohne Deutsch`;if(missingNavCount)missingNavCount.textContent=total}const hint=document.querySelector('#seasonZeroHint');if(hint)hint.textContent=show?'Vorhanden oder wird gerade geladen · Staffel 0 eingeblendet':'Vorhanden oder wird gerade geladen · Staffel 0 ausgeblendet'}const seasonZeroToggle=document.querySelector('#showSeasonZero');if(seasonZeroToggle){seasonZeroToggle.checked=localStorage.getItem('show-season-zero')==='true';seasonZeroToggle.addEventListener('change',()=>{localStorage.setItem('show-season-zero',seasonZeroToggle.checked?'true':'false');applySeasonZeroPreference()})}applySeasonZeroPreference();
function syncDashboardMissingCounter(){const visibleMissing=document.querySelectorAll('#missingMovies tbody tr:not(.hidden),#missing .missing-series tbody tr:not(.season-zero-hidden):not(.season-divider)').length;const dashboardMissing=document.querySelector('#dashboardMissing');if(dashboardMissing)dashboardMissing.textContent=visibleMissing}syncDashboardMissingCounter();document.querySelector('#showSeasonZero')?.addEventListener('change',()=>setTimeout(syncDashboardMissingCounter,0));
if('serviceWorker' in navigator)window.addEventListener('load',()=>navigator.serviceWorker.register('/static/sw.js').catch(()=>{}));
if('serviceWorker' in navigator)window.addEventListener('load',()=>navigator.serviceWorker.register('/static/sw.js?v=2').catch(()=>{}));
function updateMissingHeading(){const heading=document.querySelector('#missingMovies thead th:first-child');if(heading)heading.textContent=document.documentElement.lang==='en'?'Media':'Medien'}updateMissingHeading();document.querySelector('#languageToggle')?.addEventListener('click',()=>setTimeout(updateMissingHeading,0));
function addMissingSections(){const view=document.querySelector('#missing'),movieTable=document.querySelector('#missingMovies'),seriesItems=[...document.querySelectorAll('#missing .missing-series')];if(!view)return;const makeHeading=(type,de,en)=>{const heading=document.createElement('div');heading.className=`missing-section-heading ${type}`;heading.dataset.de=de;heading.dataset.en=en;heading.textContent=document.documentElement.lang==='en'?en:de;return heading};if(movieTable&&movieTable.querySelector('tbody tr'))movieTable.before(makeHeading('movies','Filme ohne Deutsch','Movies without German'));if(seriesItems.length)seriesItems[0].before(makeHeading('series','Serien ohne Deutsch','Series without German'))}addMissingSections();
</script>
@@ -104,7 +105,7 @@ const diskList=document.querySelector('#diskList');
function renderSystem(data){const cpu=Number(data.cpu_percent)||0,ram=data.memory||{};const cpuValue=document.querySelector('#cpuValue'),ramValue=document.querySelector('#ramValue'),cpuBar=document.querySelector('#cpuBar'),ramBar=document.querySelector('#ramBar'),updated=document.querySelector('#systemUpdated');if(cpuValue)cpuValue.textContent=`${cpu.toFixed(1)}%`;if(ramValue)ramValue.textContent=`${ram.used||'—'} / ${ram.total||'—'} (${Number(ram.percent||0).toFixed(1)}%)`;if(cpuBar)cpuBar.style.width=`${Math.min(100,cpu)}%`;if(ramBar)ramBar.style.width=`${Math.min(100,Number(ram.percent)||0)}%`;if(updated)updated.textContent='Gerade aktualisiert';if(!diskList)return;diskList.innerHTML=(data.disks||[]).map(d=>`<div class="disk-row"><div class="disk-name">${html(d.label)}<span class="disk-path">${html(d.path)}</span></div><div class="disk-bar"><span style="width:${Math.min(100,Number(d.percent)||0)}%"></span></div><div class="disk-values">${html(d.used)} / ${html(d.total)}<br><span class="muted">${Number(d.percent||0).toFixed(1)}% belegt</span></div></div>`).join('')||'<div class="disk-row muted">Keine Laufwerke gefunden</div>'}
async function fetchSystem(){try{const response=await fetch('/api/system');renderSystem(await response.json())}catch(error){const updated=document.querySelector('#systemUpdated');if(updated)updated.textContent='Nicht verfügbar'}}
if(document.querySelector('#dashboard')){fetchSystem();setInterval(fetchSystem,5000)}
const datastoreView=document.querySelector('#datastore');function renderDatastore(data){const storage=data.storage||{},raid=data.raid||{};const set=(id,value)=>{const node=document.querySelector(id);if(node)node.textContent=value};set('#datastoreTotal',storage.total||'—');set('#datastoreUsed',storage.used||'—');set('#datastoreFree',storage.free||'—');set('#datastorePercent',storage.percent!=null?`${storage.percent}%`:'—');set('#datastorePath',storage.path||'/nesflix');set('#datastoreCapacityLabel',storage.percent!=null?`${storage.used||'—'} / ${storage.total||'—'} · ${storage.percent}%`:storage.error||'Nicht verfügbar');set('#datastoreUpdated',storage.scanned_at?'Gerade aktualisiert':'Nicht verfügbar');const capacityBar=document.querySelector('#datastoreCapacityBar');if(capacityBar)capacityBar.style.width=`${Math.min(100,Number(storage.percent)||0)}%`;const categories=document.querySelector('#datastoreCategories');if(categories){const values=(storage.categories||[]).filter(category=>category.bytes>0||['movies','series','anime','kdrama','downloads'].includes(category.key));categories.innerHTML=values.length?values.map(category=>`<div class="category-row"><div class="category-row-head"><span>${html(category.label)}</span><strong>${html(category.size||'0 B')}</strong></div><div class="category-track"><span style="width:${Math.min(100,Number(category.percent)||0)}%"></span></div><small>${Number(category.percent||0).toFixed(1)}% vom belegten Speicher · ${Number(category.files||0).toLocaleString('de-DE')} Dateien</small></div>`).join(''):'<div class="datastore-empty">Keine Kategorien gefunden</div>'}const badge=document.querySelector('#raidStatusBadge');if(badge){badge.textContent=raid.label||'UNKNOWN';badge.className=`raid-status ${raid.status||'unknown'}`}set('#raidDevice',raid.device||'/dev/md127');const summary=document.querySelector('#raidSummary');if(summary)summary.innerHTML=`<span><small>Level</small><strong>${html(raid.level||'—')}</strong></span><span><small>Aktiv</small><strong>${html(raid.active_devices??'—')} / ${html(raid.raid_devices??'—')}</strong></span><span><small>Fehlend</small><strong>${html(raid.failed_devices??0)}</strong></span>`;const recovery=raid.recovery||{},recoveryBox=document.querySelector('#raidRecovery');if(recoveryBox){recoveryBox.classList.toggle('hidden',!recovery.active);set('#raidRecoveryPercent',`${Number(recovery.percent||0).toFixed(1)}%`);set('#raidRecoverySpeed',recovery.speed||'—');set('#raidRecoveryFinish',recovery.finish||'—');const recoveryBar=document.querySelector('#raidRecoveryBar');if(recoveryBar)recoveryBar.style.width=`${Math.min(100,Number(recovery.percent)||0)}%`}const disks=document.querySelector('#raidDisks');if(disks){disks.innerHTML=(raid.devices||[]).length?(raid.devices||[]).map(disk=>`<div class="raid-disk"><span class="disk-status ${html(disk.status||'unknown')}"></span><div><strong>${html(disk.device)}</strong><small>Slot ${html(disk.slot||'—')} · ${html(disk.model||'—')}</small><small>${html(disk.size||'—')} · Serial: ${html(disk.serial||'—')}</small></div><b>${html(disk.status||'unknown')}</b></div>`).join(''):'<div class="datastore-empty">Keine RAID-Mitglieder erkannt</div>'}if(raid.error){const node=document.querySelector('#raidDisks .datastore-empty');if(node)node.textContent=raid.error}}async function fetchDatastore(){if(!datastoreView)return;try{renderDatastore(await (await fetch('/api/datastore')).json())}catch(error){renderDatastore({storage:{error:'Datastore nicht erreichbar'},raid:{status:'failed',label:'FAILED',error:'RAID-Status nicht verfügbar'}})}}if(datastoreView){fetchDatastore();setInterval(fetchDatastore,5000)}
const datastoreView=document.querySelector('#datastore');function renderDatastore(data){const storage=data.storage||{},raid=data.raid||{};const set=(id,value)=>{const node=document.querySelector(id);if(node)node.textContent=value};set('#datastoreTotal',storage.total||'—');set('#datastoreUsed',storage.used||'—');set('#datastoreFree',storage.free||'—');set('#datastorePercent',storage.percent!=null?`${storage.percent}%`:'—');set('#datastorePath',storage.path||'/nesflix');set('#datastoreCapacityLabel',storage.percent!=null?`${storage.used||'—'} / ${storage.total||'—'} · ${storage.percent}%`:storage.error||'Nicht verfügbar');set('#datastoreUpdated',storage.scanned_at?'Gerade aktualisiert':'Nicht verfügbar');const capacityBar=document.querySelector('#datastoreCapacityBar');if(capacityBar)capacityBar.style.width=`${Math.min(100,Number(storage.percent)||0)}%`;const categories=document.querySelector('#datastoreCategories');if(categories){const values=(storage.categories||[]).filter(category=>category.bytes>0||['movies','series','anime','kdrama','downloads'].includes(category.key));categories.innerHTML=values.length?values.map(category=>`<div class="category-row"><div class="category-row-head"><span>${html(category.label)}</span><strong>${html(category.size||'0 B')}</strong></div><div class="category-track"><span style="width:${Math.min(100,Number(category.percent)||0)}%"></span></div><small>${Number(category.percent||0).toFixed(1)}% vom belegten Speicher · ${Number(category.files||0).toLocaleString('de-DE')} Dateien</small></div>`).join(''):'<div class="datastore-empty">Keine Kategorien gefunden</div>'}const badge=document.querySelector('#raidStatusBadge');if(badge){badge.textContent=raid.label||'UNKNOWN';badge.className=`raid-status ${raid.status||'unknown'}`}set('#raidDevice',raid.device||'/dev/md127');const summary=document.querySelector('#raidSummary');if(summary)summary.innerHTML=`<span><small>Level</small><strong>${html(raid.level||'—')}</strong></span><span><small>Aktiv</small><strong>${html(raid.active_devices??'—')} / ${html(raid.raid_devices??'—')}</strong></span><span><small>Fehlend</small><strong>${html(raid.failed_devices??0)}</strong></span>`;const recovery=raid.recovery||{},recoveryBox=document.querySelector('#raidRecovery');if(recoveryBox){recoveryBox.classList.toggle('hidden',!recovery.active);set('#raidRecoveryPercent',`${Number(recovery.percent||0).toFixed(1)}%`);set('#raidRecoverySpeed',recovery.speed||'—');set('#raidRecoveryFinish',recovery.finish||'—');const recoveryBar=document.querySelector('#raidRecoveryBar');if(recoveryBar)recoveryBar.style.width=`${Math.min(100,Number(recovery.percent)||0)}%`}const disks=document.querySelector('#raidDisks');if(disks){disks.innerHTML=(raid.devices||[]).length?(raid.devices||[]).map(disk=>`<div class="raid-disk"><span class="disk-status ${html(disk.status||'unknown')}"></span><div><strong>${html(disk.device)}</strong><small>Slot ${html(disk.slot||'—')} · ${html(disk.model||'—')}</small><small>${html(disk.size||'—')} · Seriennummer: ${html(disk.serial||'—')}</small><small class="raid-smart">SMART: ${html(disk.smart||'Nicht aktiviert')}</small></div><b>${html(disk.status||'unknown')}</b></div>`).join(''):'<div class="datastore-empty">Keine RAID-Mitglieder erkannt</div>'}if(raid.error){const node=document.querySelector('#raidDisks .datastore-empty');if(node)node.textContent=raid.error}}async function fetchDatastore(){if(!datastoreView)return;try{renderDatastore(await (await fetch('/api/datastore')).json())}catch(error){renderDatastore({storage:{error:'Datastore nicht erreichbar'},raid:{status:'failed',label:'FAILED',error:'RAID-Status nicht verfügbar'}})}}if(datastoreView){fetchDatastore();setInterval(fetchDatastore,5000)}
</script>
<style>
.season-divider td{padding:6px 10px;background:#1b1b1b;border-top:1px solid #444;border-bottom:1px solid #303030;color:#aaa;font-size:11px;font-weight:700;letter-spacing:.3px}
@@ -194,3 +195,4 @@ document.querySelectorAll('.service-logo img').forEach(img=>{img.style.width='20
setInterval(()=>{document.querySelectorAll('.service-logo svg').forEach(svg=>{svg.style.width='19px';svg.style.height='19px';svg.style.fill='none';svg.style.stroke='currentColor';svg.style.strokeWidth='1.8';svg.style.strokeLinecap='round';svg.style.strokeLinejoin='round'});document.querySelectorAll('.service-logo img').forEach(img=>{img.style.width='20px';img.style.height='20px';img.style.objectFit='contain'})},500);
const settingsBackdrop=document.querySelector('#settingsBackdrop');const closeSettings=()=>settingsBackdrop.classList.add('hidden');document.querySelector('#settingsOpen').addEventListener('click',()=>{loadPreferences();settingsBackdrop.classList.remove('hidden')});document.querySelector('#settingsClose').addEventListener('click',closeSettings);document.querySelector('#settingsCancel').addEventListener('click',closeSettings);settingsBackdrop.addEventListener('click',event=>{if(event.target===settingsBackdrop)closeSettings()});document.querySelector('#settingsSave').addEventListener('click',()=>{localStorage.setItem('preferred-audio',document.querySelector('#preferredAudio').value);localStorage.setItem('preferred-subs',document.querySelector('#preferredSubs').value);loadPreferences();closeSettings()});
</script>
<style>.nav-icon{width:15px;height:15px;margin-right:5px;vertical-align:-3px;object-fit:contain}.storage-icon img{width:20px;height:20px;object-fit:contain}</style>