feat: add NVIDIA Graphics tab with GPU monitoring via nvidia-smi and configurable binary path
Add graphics_snapshot() function to query GPU details using nvidia-smi with 5s timeout. Parse GPU metrics including model, driver, UUID, serial, temperature, utilization, VRAM, power, clocks, and compute processes. Add NVIDIA_SMI_BIN environment variable with default "nvidia-smi". Add /api/graphics endpoint returning GPU data with error handling. Add Graphics navigation tab with purple expansion-card icon
This commit is contained in:
1 parent
6aa4032134
commit
7d1987c4f4
3 files changed
+74
-5
No files matched your search
@@ -60,10 +60,10 @@ if ('serviceWorker' in navigator) {
|
||||
.settings-modal .setting-help{display:block;margin:-3px 0 12px;color:var(--muted);font-size:11px}.settings-modal .setting-select{width:100%;height:38px}.pwa-install{display:none}
|
||||
@media(max-width:700px){body{font-size:12px;padding-bottom:env(safe-area-inset-bottom)}header{height:auto;min-height:62px;padding:9px 12px;gap:9px;align-items:center}.brand-mark,.brand-mark svg{width:30px;height:30px}.brand-name{font-size:14px}.head-title{font-size:12px}.head-sub{font-size:10px}.actions{gap:4px}.actions button{height:34px;padding:0 8px}.settings-button{font-size:15px}.actions #rescan{font-size:0}.actions #rescan::after{content:'↻';font-size:16px}main{padding:10px 10px 24px}.nav{margin-bottom:10px;padding:4px;overflow-x:auto;scrollbar-width:none}.nav::-webkit-scrollbar{display:none}.nav button{flex:0 0 auto;white-space:nowrap;padding:8px 10px;font-size:11px}.toolbar{grid-template-columns:1fr;gap:7px;padding:8px}.toolbar input,.toolbar select{width:100%;height:38px}.stats{gap:6px}.stat{padding:6px 8px}.tablewrap{overflow-x:auto;border-radius:8px}table{min-width:860px}.dashboard-hero{margin-bottom:10px;padding:18px;border-radius:11px}.dashboard-hero h1{font-size:21px}.dashboard-hero p{font-size:11px;line-height:1.4}.overview-grid{grid-template-columns:1fr 1fr;gap:7px}.overview-card{min-height:84px;padding:12px 10px;gap:8px}.overview-icon{width:32px;height:32px;font-size:18px}.overview-card strong{font-size:18px}.overview-card span:not(.overview-icon){font-size:9px}.overview-card small{font-size:9px}.dashboard-columns{gap:8px}.system-panel,.service-panel{padding:14px;border-radius:10px}.system-meters{gap:12px}.panel-heading{margin-bottom:13px}.panel-heading h2{font-size:14px}.panel-heading p{font-size:10px}.disk-row{grid-template-columns:82px 1fr auto;gap:7px}.disk-values{font-size:10px}.download-hero{padding:18px;margin-bottom:10px}.download-hero h2{font-size:19px}.download-hero p{font-size:11px}.download-stats{grid-template-columns:1fr 1fr;gap:7px}.download-card{min-height:78px;padding:10px;gap:8px}.card-icon{width:30px;height:30px;font-size:17px}.card-label{font-size:9px}.download-card strong{font-size:17px}.download-card small{font-size:9px}.settings-modal{max-height:calc(100dvh - 24px);overflow:auto;padding:18px}.settings-head{margin-bottom:15px}.settings-modal label{margin-top:13px}}
|
||||
@media(max-width:390px){.brand-name{display:none}.head-sub{display:none}.overview-grid{grid-template-columns:1fr}.dashboard-hero{padding:15px}.download-stats{grid-template-columns:1fr}}
|
||||
</style><style>.scan-control{display:flex;align-items:center;gap:5px}.scan-control select{height:36px;padding:0 8px;font-size:11px}.scan-control button{white-space:nowrap}.scan-running{position:relative;overflow:hidden;background:linear-gradient(90deg,rgba(244,196,48,.14) var(--scan-progress,0%),rgba(244,196,48,.04) var(--scan-progress,0%))!important}.scan-running::after{content:'';position:absolute;left:0;bottom:0;width:var(--scan-progress,0%);height:2px;background:#f4c430;transition:width .35s ease}@media(max-width:700px){.scan-control select{max-width:108px;font-size:10px}.scan-control button{font-size:0}.scan-control button::after{content:'↻';font-size:16px}}</style></head><body>
|
||||
</style><style>.scan-control{display:flex;align-items:center;gap:5px}.scan-control select{height:36px;padding:0 8px;font-size:11px}.scan-control button{white-space:nowrap}.scan-running{position:relative;overflow:hidden;background:linear-gradient(90deg,rgba(244,196,48,.14) var(--scan-progress,0%),rgba(244,196,48,.04) var(--scan-progress,0%))!important}.scan-running::after{content:'';position:absolute;left:0;bottom:0;width:var(--scan-progress,0%);height:2px;background:#f4c430;transition:width .35s ease}@media(max-width:700px){.scan-control select{max-width:108px;font-size:10px}.scan-control button{font-size:0}.scan-control button::after{content:'↻';font-size:16px}}</style><style>.graphics-hero{display:flex;justify-content:space-between;align-items:center;gap:18px;margin-bottom:14px;padding:22px 26px;border:1px solid rgba(167,139,250,.24);border-radius:14px;background:radial-gradient(circle at 88% 20%,rgba(167,139,250,.16),transparent 32%),linear-gradient(135deg,rgba(45,40,65,.96),rgba(27,30,36,.96));box-shadow:0 14px 34px rgba(0,0,0,.2)}.graphics-hero h2{margin:7px 0 4px;font-size:22px}.graphics-hero p{margin:0;color:#9da5b2}.graphics-live{padding:8px 12px;border:1px solid rgba(167,139,250,.28);border-radius:999px;color:#dfd2ff;background:rgba(125,91,190,.15);font-size:11px;white-space:nowrap}.graphics-summary{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:12px}.graphics-summary-card{display:flex;align-items:center;gap:11px;min-height:82px;padding:14px;border:1px solid rgba(255,255,255,.08);border-radius:11px;background:linear-gradient(145deg,rgba(43,47,55,.92),rgba(29,31,36,.92));box-shadow:0 8px 22px rgba(0,0,0,.16)}.graphics-icon{display:grid;place-items:center;width:35px;height:35px;border-radius:10px;background:rgba(167,139,250,.18);color:#d7c4ff;font-weight:800}.graphics-summary-card small{display:block;color:#9da5b2;font-size:10px;text-transform:uppercase;letter-spacing:.06em}.graphics-summary-card strong{display:block;margin-top:3px;color:#f1f5fa;font-size:18px}.graphics-cards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin-bottom:12px}.graphics-card,.graphics-panel{padding:18px;border:1px solid rgba(255,255,255,.08);border-radius:12px;background:rgba(31,33,38,.84);box-shadow:0 12px 30px rgba(0,0,0,.18)}.graphics-card-head,.graphics-card-line,.graphics-panel-head{display:flex;align-items:flex-start;justify-content:space-between;gap:14px}.graphics-card-head h3,.graphics-panel-head h3{margin:3px 0 0;font-size:16px}.graphics-gpu-index{color:#a78bfa;font-size:10px;text-transform:uppercase;letter-spacing:.07em}.graphics-meta-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin:16px 0}.graphics-meta-grid div{padding:9px;border:1px solid rgba(255,255,255,.07);border-radius:8px;background:rgba(10,14,20,.2)}.graphics-meta-grid small{display:block;color:#7f8996;font-size:10px}.graphics-meta-grid strong{display:block;margin-top:4px;color:#e8eef5;font-size:13px}.graphics-card-line{font-size:11px;color:#9da5b2}.graphics-card-line strong{color:#e8eef5}.graphics-track{height:9px;margin-top:7px;border-radius:99px;overflow:hidden;background:#363b44}.graphics-track span{display:block;height:100%;border-radius:99px;background:linear-gradient(90deg,#60a9e6,#a78bfa);transition:width .5s ease}.graphics-details{display:flex;flex-wrap:wrap;gap:6px 14px;margin-top:14px;color:#7f8996;font-size:10px}.graphics-empty{padding:20px;text-align:center;color:#7f8996;font-size:11px}.graphics-panel-head{margin-bottom:12px}.graphics-panel-head h3{margin:0}.graphics-panel-head p{margin:4px 0 0;color:#7f8996;font-size:11px}.graphics-processes{display:grid;gap:6px}.graphics-process{display:grid;grid-template-columns:1fr auto auto;gap:14px;padding:9px 10px;border:1px solid rgba(255,255,255,.07);border-radius:8px;background:rgba(40,43,49,.65);font-size:11px}.graphics-process span{color:#9da5b2}@media(max-width:900px){.graphics-summary{grid-template-columns:repeat(2,1fr)}.graphics-cards{grid-template-columns:1fr}}@media(max-width:600px){.graphics-hero{align-items:flex-start;flex-direction:column;padding:18px}.graphics-live{align-self:stretch;text-align:center}.graphics-summary{gap:7px}.graphics-summary-card{min-height:72px;padding:10px}.graphics-summary-card strong{font-size:16px}.graphics-card,.graphics-panel{padding:14px}.graphics-meta-grid{grid-template-columns:repeat(2,1fr)}.graphics-process{grid-template-columns:1fr;gap:3px}}</style></head><body>
|
||||
<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><option value="missing">Nur Ohne Deutsch</option></select><button id="rescan" data-de="↻ Neu scannen" data-en="↻ Rescan">↻ Neu scannen</button></div></div></header>
|
||||
<main>
|
||||
<nav class="nav"><button class="active" data-view="dashboard"><img class="nav-tab-icon" src="https://api.iconify.design/mdi:view-dashboard-outline.svg?color=%238bc9f2" alt="" aria-hidden="true">Dashboard</button><button data-view="radarr"><img class="nav-tab-icon" src="https://api.iconify.design/mdi:movie-open-outline.svg?color=%23cc7b19" alt="" aria-hidden="true">RADARR · Filme</button>{% if sonarr_enabled %}<button data-view="sonarr"><img class="nav-tab-icon" src="https://api.iconify.design/mdi:television-classic.svg?color=%2335c5f4" alt="" aria-hidden="true">SONARR · Serien</button>{% endif %}<button data-view="missing"><img class="nav-tab-icon" src="https://api.iconify.design/mdi:alert-outline.svg?color=%23ff9e9e" alt="" aria-hidden="true">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"><img class="nav-tab-icon" src="https://api.iconify.design/mdi:download-box-outline.svg?color=%23f4c430" alt="" aria-hidden="true">SABNZBD · Downloads <span class="nav-count download-nav-count hidden" id="downloadNavCount">0</span></button>{% endif %}<button data-view="datastore"><img class="nav-tab-icon" src="https://api.iconify.design/mdi:harddisk.svg?color=%2360a9e6" alt="" aria-hidden="true">Datastore</button></nav>
|
||||
<nav class="nav"><button class="active" data-view="dashboard"><img class="nav-tab-icon" src="https://api.iconify.design/mdi:view-dashboard-outline.svg?color=%238bc9f2" alt="" aria-hidden="true">Dashboard</button><button data-view="radarr"><img class="nav-tab-icon" src="https://api.iconify.design/mdi:movie-open-outline.svg?color=%23cc7b19" alt="" aria-hidden="true">RADARR · Filme</button>{% if sonarr_enabled %}<button data-view="sonarr"><img class="nav-tab-icon" src="https://api.iconify.design/mdi:television-classic.svg?color=%2335c5f4" alt="" aria-hidden="true">SONARR · Serien</button>{% endif %}<button data-view="missing"><img class="nav-tab-icon" src="https://api.iconify.design/mdi:alert-outline.svg?color=%23ff9e9e" alt="" aria-hidden="true">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"><img class="nav-tab-icon" src="https://api.iconify.design/mdi:download-box-outline.svg?color=%23f4c430" alt="" aria-hidden="true">SABNZBD · Downloads <span class="nav-count download-nav-count hidden" id="downloadNavCount">0</span></button>{% endif %}<button data-view="datastore"><img class="nav-tab-icon" src="https://api.iconify.design/mdi:harddisk.svg?color=%2360a9e6" alt="" aria-hidden="true">Datastore</button><button data-view="graphics"><img class="nav-tab-icon" src="https://api.iconify.design/mdi:expansion-card.svg?color=%23a78bfa" alt="" aria-hidden="true">Graphics</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>
|
||||
@@ -79,7 +79,7 @@ if ('serviceWorker' in navigator) {
|
||||
{% for group in missing_german_series %}<details class="series missing-series"><summary>{{ group.title }} <span class="muted">({{ group.year or '—' }}) · {{ group.episodes|length }} Episoden ohne Deutsch</span></summary><table><thead><tr><th class="col-title">Episode</th><th class="col-quality">Qualität</th><th class="col-state">Status</th><th class="col-audio">Audio</th><th class="col-subs">Untertitel</th><th class="col-video">Video</th><th class="col-size">Größe</th></tr></thead><tbody>{% for r in group.episodes %}<tr><td><span class="title">{{ r.title }}</span></td><td><span class="badge {% if not r.missing %}quality{% endif %}">{{ r.quality }}</span></td><td><span class="state bad">Ohne Deutsch</span></td><td><div class="badges">{% for l in r.audio_languages %}<span class="badge">{{ l.flag }} {{ l.name }}</span>{% else %}<span class="muted">—</span>{% endfor %}</div></td><td><div class="badges">{% for l in r.subtitle_languages %}<span class="badge">{{ l.flag }} {{ l.name }}</span>{% else %}<span class="muted">—</span>{% endfor %}</div></td><td>{{ r.video_codec }}</td><td>{{ r.size }}</td></tr>{% endfor %}</tbody></table></details>{% endfor %}
|
||||
{% if not missing_german_movies and not missing_german_series %}<div class="empty-state">Alle vorhandenen Filme und Episoden haben deutsches Audio oder deutsche Untertitel.</div>{% endif %}</div></section>
|
||||
{% if sab_enabled %}<section id="downloads" class="view" data-source="downloads"><div class="download-hero"><div><div class="eyebrow">SABNZBD · DOWNLOAD CENTER</div><h2>Download-Übersicht</h2><p>Queue, Fortschritt und verbleibender Speicherplatz auf einen Blick.</p></div><div class="queue-pill" id="sabQueueCard"><span class="queue-dot"></span><span>Verbinde mit SABnzbd …</span></div></div><div class="download-stats"><div class="download-card remaining-card"><span class="card-icon">↓</span><div><span class="card-label">Noch zu laden</span><strong id="sabRemaining">—</strong><small id="sabRemainingHint">Wird berechnet …</small></div></div><div class="download-card"><span class="card-icon blue">◉</span><div><span class="card-label">Aktive Downloads</span><strong id="sabActive">—</strong><small id="sabHistory">Verlauf: —</small></div></div><div class="download-card"><span class="card-icon purple">↯</span><div><span class="card-label">Geschwindigkeit</span><strong id="sabSpeedCard">—</strong><small>Aktuelle Queue</small></div></div><div class="download-card"><span class="card-icon orange">✓</span><div><span class="card-label">Status</span><strong id="sabStatusCard">—</strong><small id="downloadCount">—</small></div></div></div><div id="downloadError" class="banner hidden"></div><div class="tablewrap download-table"><table id="downloadsTable"><thead><tr><th class="col-title">Download</th><th class="col-state">Status</th><th class="col-progress">Fortschritt</th><th class="col-size">Größe / Rest</th><th class="col-time">Restzeit</th><th class="col-source">Quelle</th><th class="col-language">Sprache</th></tr></thead><tbody id="downloadsBody"><tr><td colspan="7" class="muted">Downloads werden geladen …</td></tr></tbody></table></div></section>{% endif %}
|
||||
<section id="datastore" class="view" data-source="datastore"><div class="datastore-hero"><div><div class="eyebrow">MEDIA MAX · DATASTORE</div><h2>Datastore</h2><p>Speicherbelegung, Medienkategorien und RAID-Status von <code>/nesflix</code>.</p></div><div id="datastoreUpdated" class="datastore-live">Wird geladen …</div></div><div class="datastore-storage"><div class="storage-card"><span class="storage-icon">▣</span><div><small>Gesamtspeicher</small><strong id="datastoreTotal">—</strong></div></div><div class="storage-card"><span class="storage-icon used">◒</span><div><small>Belegt</small><strong id="datastoreUsed">—</strong></div></div><div class="storage-card"><span class="storage-icon free">◌</span><div><small>Frei</small><strong id="datastoreFree">—</strong></div></div><div class="storage-card"><span class="storage-icon percent">%</span><div><small>Auslastung</small><strong id="datastorePercent">—</strong></div></div></div><section class="datastore-panel datastore-capacity"><div class="datastore-panel-head"><div><h3>Gesamtauslastung</h3><p id="datastorePath">/nesflix</p></div><strong id="datastoreCapacityLabel">—</strong></div><div class="datastore-progress"><span id="datastoreCapacityBar"></span></div></section><div class="datastore-grid"><section class="datastore-panel"><div class="datastore-panel-head"><div><h3>Speicher nach Kategorie</h3><p>Anhand der tatsächlichen Verzeichnisse</p></div></div><div id="datastoreCategories" class="datastore-categories"><div class="datastore-empty">Wird geladen …</div></div></section><section class="datastore-panel raid-panel"><div class="datastore-panel-head"><div><h3>RAID-Status</h3><p id="raidDevice">/dev/md127</p></div><span id="raidStatusBadge" class="raid-status unknown">Wird geladen …</span></div><div id="raidSummary" class="raid-summary"><span>—</span><span>—</span><span>—</span></div><div id="raidRecovery" class="raid-recovery hidden"><div class="recovery-head"><strong>RAID Rebuild</strong><span id="raidRecoveryPercent">—</span></div><div class="datastore-progress recovery"><span id="raidRecoveryBar"></span></div><div class="recovery-meta"><span>Geschwindigkeit <strong id="raidRecoverySpeed">—</strong></span><span>Verbleibend <strong id="raidRecoveryFinish">—</strong></span></div></div><div id="raidDisks" class="raid-disks"><div class="datastore-empty">Wird geladen …</div></div></section></div></section>
|
||||
<section id="graphics" class="view" data-source="graphics"><div class="graphics-hero"><div><div class="eyebrow">MEDIA MAX · GRAPHICS</div><h2>NVIDIA Graphics</h2><p>GPU-Auslastung, Speicher, Temperatur, Leistung und laufende Prozesse.</p></div><div id="graphicsStatus" class="graphics-live">Wird geladen …</div></div><div id="graphicsSummary" class="graphics-summary"><div class="graphics-summary-card"><span class="graphics-icon">▣</span><div><small>GPUs</small><strong>—</strong></div></div><div class="graphics-summary-card"><span class="graphics-icon">◒</span><div><small>GPU-Auslastung</small><strong>—</strong></div></div><div class="graphics-summary-card"><span class="graphics-icon">▤</span><div><small>VRAM</small><strong>—</strong></div></div><div class="graphics-summary-card"><span class="graphics-icon">°</span><div><small>Temperatur</small><strong>—</strong></div></div></div><div id="graphicsCards" class="graphics-cards"><div class="graphics-empty">GPU-Daten werden geladen …</div></div><section class="graphics-panel"><div class="graphics-panel-head"><div><h3>GPU-Prozesse</h3><p>Aktuelle Compute-Prozesse laut NVIDIA SMI</p></div></div><div id="graphicsProcesses" class="graphics-processes"><div class="graphics-empty">Keine Prozesse erkannt</div></div></section></section><section id="datastore" class="view" data-source="datastore"><div class="datastore-hero"><div><div class="eyebrow">MEDIA MAX · DATASTORE</div><h2>Datastore</h2><p>Speicherbelegung, Medienkategorien und RAID-Status von <code>/nesflix</code>.</p></div><div id="datastoreUpdated" class="datastore-live">Wird geladen …</div></div><div class="datastore-storage"><div class="storage-card"><span class="storage-icon">▣</span><div><small>Gesamtspeicher</small><strong id="datastoreTotal">—</strong></div></div><div class="storage-card"><span class="storage-icon used">◒</span><div><small>Belegt</small><strong id="datastoreUsed">—</strong></div></div><div class="storage-card"><span class="storage-icon free">◌</span><div><small>Frei</small><strong id="datastoreFree">—</strong></div></div><div class="storage-card"><span class="storage-icon percent">%</span><div><small>Auslastung</small><strong id="datastorePercent">—</strong></div></div></div><section class="datastore-panel datastore-capacity"><div class="datastore-panel-head"><div><h3>Gesamtauslastung</h3><p id="datastorePath">/nesflix</p></div><strong id="datastoreCapacityLabel">—</strong></div><div class="datastore-progress"><span id="datastoreCapacityBar"></span></div></section><div class="datastore-grid"><section class="datastore-panel"><div class="datastore-panel-head"><div><h3>Speicher nach Kategorie</h3><p>Anhand der tatsächlichen Verzeichnisse</p></div></div><div id="datastoreCategories" class="datastore-categories"><div class="datastore-empty">Wird geladen …</div></div></section><section class="datastore-panel raid-panel"><div class="datastore-panel-head"><div><h3>RAID-Status</h3><p id="raidDevice">/dev/md127</p></div><span id="raidStatusBadge" class="raid-status unknown">Wird geladen …</span></div><div id="raidSummary" class="raid-summary"><span>—</span><span>—</span><span>—</span></div><div id="raidRecovery" class="raid-recovery hidden"><div class="recovery-head"><strong>RAID Rebuild</strong><span id="raidRecoveryPercent">—</span></div><div class="datastore-progress recovery"><span id="raidRecoveryBar"></span></div><div class="recovery-meta"><span>Geschwindigkeit <strong id="raidRecoverySpeed">—</strong></span><span>Verbleibend <strong id="raidRecoveryFinish">—</strong></span></div></div><div id="raidDisks" class="raid-disks"><div class="datastore-empty">Wird geladen …</div></div></section></div></section>
|
||||
</main>
|
||||
<div class="settings-backdrop hidden" id="settingsBackdrop"><section class="settings-modal" role="dialog" aria-modal="true" aria-labelledby="settingsTitle"><div class="settings-head"><div><h2 id="settingsTitle" data-de="Einstellungen" data-en="Settings">Einstellungen</h2><p data-de="Deine Anzeige- und Sprachpräferenzen" data-en="Your display and language preferences">Deine Anzeige- und Sprachpräferenzen</p></div><button class="settings-close" id="settingsClose" aria-label="Schließen">×</button></div><label for="preferredAudio" data-de="Bevorzugte Audio-Sprache" data-en="Preferred audio language">Bevorzugte Audio-Sprache</label><select id="preferredAudio"><option value="" data-de="Keine Präferenz" data-en="No preference">Keine Präferenz</option><option value="Deutsch" data-de="Deutsch" data-en="German">Deutsch</option><option value="Englisch" data-de="Englisch" data-en="English">Englisch</option><option value="Japanisch" data-de="Japanisch" data-en="Japanese">Japanisch</option><option value="Koreanisch" data-de="Koreanisch" data-en="Korean">Koreanisch</option></select><label for="preferredSubs" data-de="Bevorzugte Untertitel-Sprache" data-en="Preferred subtitle language">Bevorzugte Untertitel-Sprache</label><select id="preferredSubs"><option value="" data-de="Keine Präferenz" data-en="No preference">Keine Präferenz</option><option value="Deutsch" data-de="Deutsch" data-en="German">Deutsch</option><option value="Englisch" data-de="Englisch" data-en="English">Englisch</option><option value="Japanisch" data-de="Japanisch" data-en="Japanese">Japanisch</option><option value="Koreanisch" data-de="Koreanisch" data-en="Korean">Koreanisch</option></select><div class="settings-actions"><button id="settingsCancel" data-de="Abbrechen" data-en="Cancel">Abbrechen</button><button class="settings-save" id="settingsSave" data-de="Speichern" data-en="Save">Speichern</button></div></section></div>
|
||||
<script>
|
||||
@@ -152,7 +152,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||'—')} · Seriennummer: ${html(disk.serial||'—')}</small><small class="raid-smart">SMART: ${html(disk.smart||'Nicht aktiviert')}</small><small class="raid-temperature">Temperatur: ${html(disk.temperature||'Nicht verfügbar')}</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 graphicsView=document.querySelector('#graphics');function graphicsValue(value,unit){return value===null||value===undefined||value===''?'—':String(value)+(unit||'')}function renderGraphics(data){if(!graphicsView)return;const gpus=data.gpus||[],status=document.querySelector('#graphicsStatus');if(status)status.textContent=data.available?gpus.length+' GPU'+(gpus.length===1?'':'s')+' · Aktualisiert':'Nicht verfügbar';const avg=function(key){const values=gpus.map(function(g){return Number(g[key])}).filter(Number.isFinite);return values.length?Math.round(values.reduce(function(a,b){return a+b},0)/values.length):null};const memUsed=gpus.reduce(function(sum,g){return sum+(Number(g['memory.used'])||0)},0),memTotal=gpus.reduce(function(sum,g){return sum+(Number(g['memory.total'])||0)},0),summary=document.querySelector('#graphicsSummary');if(summary)summary.innerHTML='<div class="graphics-summary-card"><span class="graphics-icon">▣</span><div><small>GPUs</small><strong>'+(gpus.length||'—')+'</strong></div></div><div class="graphics-summary-card"><span class="graphics-icon">◒</span><div><small>GPU-Auslastung</small><strong>'+graphicsValue(avg('utilization.gpu'),'%')+'</strong></div></div><div class="graphics-summary-card"><span class="graphics-icon">▤</span><div><small>VRAM</small><strong>'+(memTotal?(memUsed/1024).toFixed(1)+' / '+(memTotal/1024).toFixed(1)+' GB':'—')+'</strong></div></div><div class="graphics-summary-card"><span class="graphics-icon">°</span><div><small>Temperatur</small><strong>'+graphicsValue(avg('temperature.gpu'),' °C')+'</strong></div></div>';const cards=document.querySelector('#graphicsCards');if(cards)cards.innerHTML=gpus.length?gpus.map(function(g){const used=Number(g['memory.used'])||0,total=Number(g['memory.total'])||0,pct=total?Math.round(used/total*100):0;return '<article class="graphics-card"><div class="graphics-card-head"><div><span class="graphics-gpu-index">GPU '+html(g.index)+'</span><h3>'+html(g.name||'NVIDIA GPU')+'</h3></div><span class="graphics-live">'+graphicsValue(g['temperature.gpu'],' °C')+'</span></div><div class="graphics-meta-grid"><div><small>GPU-Last</small><strong>'+graphicsValue(g['utilization.gpu'],'%')+'</strong></div><div><small>Memory-Last</small><strong>'+graphicsValue(g['utilization.memory'],'%')+'</strong></div><div><small>Leistung</small><strong>'+graphicsValue(g['power.draw'],' W')+(g['power.limit']?' / '+g['power.limit']+' W':'')+'</strong></div><div><small>P-State</small><strong>'+html(g.pstate||'—')+'</strong></div></div><div class="graphics-memory"><div class="graphics-card-line"><span>VRAM</span><strong>'+(total?(used/1024).toFixed(1)+' / '+(total/1024).toFixed(1)+' GB · '+pct+'%':'—')+'</strong></div><div class="graphics-track"><span style="width:'+pct+'%"></span></div></div><div class="graphics-details"><span>Treiber '+html(g.driver_version||'—')+'</span><span>Bus '+html(g['pci.bus_id']||'—')+'</span><span>UUID '+html(g.uuid||'—')+'</span><span>Serial '+html(g.serial||'—')+'</span><span>Clock '+graphicsValue(g['clocks.current.graphics'],' MHz')+'</span></div></article>'}).join(''):'<div class="graphics-empty">'+html(data.error||'Keine NVIDIA-GPU erkannt')+'</div>';const processes=document.querySelector('#graphicsProcesses');if(processes)processes.innerHTML=(data.processes||[]).length?data.processes.map(function(p){return '<div class="graphics-process"><strong>'+html(p.name)+'</strong><span>PID '+html(p.pid)+'</span><span>'+html(p.memory)+'</span></div>'}).join(''):'<div class="graphics-empty">Keine GPU-Prozesse erkannt</div>'}async function fetchGraphics(){if(!graphicsView)return;try{renderGraphics(await (await fetch('/api/graphics',{cache:'no-store'})).json())}catch(error){renderGraphics({available:false,error:'GPU-Status momentan nicht verfügbar'})}}if(graphicsView){fetchGraphics();setInterval(fetchGraphics,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.innerHLine truncated
|
||||
</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}
|
||||
@@ -223,7 +223,7 @@ function translateLayout(language){
|
||||
currentUiLanguage=language;localStorage.setItem('dashboard-language',language);document.documentElement.lang=language;const t=uiTranslations[language];
|
||||
document.querySelectorAll('[data-de][data-en]').forEach(node=>{node.textContent=node.dataset[language]});
|
||||
document.querySelector('#languageToggle').textContent=language==='de'?'EN':'DE';
|
||||
const navLabels={dashboard:['▦ Dashboard','▦ Dashboard'],radarr:['RADARR · Filme','RADARR · Movies'],sonarr:['SONARR · Serien','SONARR · Series'],missing:['⚠ Ohne Deutsch','⚠ Without German'],downloads:['SABNZBD · Downloads','SABNZBD · Downloads']};document.querySelectorAll('.nav button').forEach(button=>{const labels=navLabels[button.dataset.view];if(labels)button.childNodes[0].textContent=labels[language==='de'?0:1]});
|
||||
const navLabels={dashboard:['▦ Dashboard','▦ Dashboard'],radarr:['RADARR · Filme','RADARR · Movies'],sonarr:['SONARR · Serien','SONARR · Series'],missing:['⚠ Ohne Deutsch','⚠ Without German'],downloads:['SABNZBD · Downloads','SABNZBD · Downloads'],datastore:['Datastore','Datastore'],graphics:['Graphics','Graphics']};document.querySelectorAll('.nav button').forEach(button=>{const labels=navLabels[button.dataset.view];if(labels)button.childNodes[0].textContent=labels[language==='de'?0:1]});
|
||||
const searchInput=document.querySelector('#search');if(searchInput)searchInput.placeholder=t.searchMovie;const seriesSearchInput=document.querySelector('#seriesSearch');if(seriesSearchInput)seriesSearchInput.placeholder=t.searchSeries;
|
||||
const filterOptions=[['#audioFilter',t.allAudio],['#subFilter',t.allSubs],['#stateFilter',t.allStatus]];filterOptions.forEach(([selector,label])=>{const first=document.querySelector(`${selector} option:first-child`);if(first)first.textContent=label});
|
||||
document.querySelectorAll('#movies thead th').forEach((th,i)=>{th.textContent=[t.film,t.quality,t.status,t.audio,t.subs,t.video,t.size][i]||th.textContent});
|
||||
|
||||
Reference in new issue
Block a user