Initial commit: Media Server Dashboard

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Housemann
2026-07-11 05:40:27 +02:00
co-authored by Claude Sonnet 4.6
commit 02dec08748
8 changed files with 1239 additions and 0 deletions
+335
View File
@@ -0,0 +1,335 @@
# Media Server Dashboard
Statisches Homelab-Dashboard mit Live-Speicheranzeige für einen gemounteten Pfad (`/downloads`). Die Weboberfläche listet Media-Server-Dienste als Kacheln und zeigt den aktuellen Speicherplatz des Download-Laufwerks an.
## Übersicht
| Komponente | Aufgabe |
|---|---|
| **nginx** | Liefert die statische Website (`index.html`) und die Datei `disk.json` aus |
| **diskinfo** | Liest per `df` den Speicher des gemounteten Pfads und schreibt alle 30 Sekunden `disk.json` |
| **Browser** | Lädt `disk.json` per Javalescript und aktualisiert die Speicheranzeige |
```
┌─────────────┐ GET /disk.json ┌─────────────┐
│ Browser │ ◄────────────────────── │ nginx │
└─────────────┘ │ (Port 80) │
└──────▲──────┘
│ liest
/opt/nginx/html/disk.json
│ schreibt alle 30s
┌──────┴──────┐
│ diskinfo │
│ (alpine) │
└──────┬──────┘
│ df -h
/mnt/ssd → /downloads
```
## Projektstruktur
```
.
├── docker-compose.yml # Container-Definition (nginx + diskinfo)
├── html/
│ ├── index.html # Dashboard (Apps + Speicheranzeige)
│ ├── disk.json # Wird vom diskinfo-Container geschrieben
│ ├── filebot-media-browser.svg
│ ├── redirect.html
│ └── bk_index.htm # Backup der alten index.html
└── scripts/
└── update-disk.sh # Speicher auslesen + Endlosschleife
```
## Voraussetzungen
- Docker und Docker Compose auf dem Host
- Gemounteter Speicherpfad auf dem Host (Standard: `/mnt/ssd`)
- Port 80 frei
## Deployment
### 1. Dateien auf den Server kopieren
Auf dem Server liegt das Projekt typischerweise unter `/opt/nginx`:
```bash
/opt/nginx/
├── docker-compose.yml
├── html/
└── scripts/
└── update-disk.sh
```
Dateien aus diesem Repository dorthin kopieren, z. B. per `git clone`, `rsync` oder `scp`.
Alternativ kann `update-disk.sh` direkt auf dem Server angelegt werden:
```bash
cat > /opt/nginx/scripts/update-disk.sh << 'EOF'
#!/bin/sh
MOUNT="/downloads"
OUTPUT="/html/disk.json"
update_disk() {
LINE=$(df -h "$MOUNT" 2>/dev/null | awk 'NR==2')
if [ -z "$LINE" ]; then
echo "ERROR: df failed for $MOUNT" >&2
return 1
fi
TOTAL=$(echo "$LINE" | awk '{print $2}')
USED=$(echo "$LINE" | awk '{print $3}')
AVAILABLE=$(echo "$LINE" | awk '{print $4}')
USE_PERCENT=$(echo "$LINE" | awk '{print $5}')
cat > "$OUTPUT" <<JSON
{
"total": "$TOTAL",
"used": "$USED",
"available": "$AVAILABLE",
"use_percent": "$USE_PERCENT",
"updated_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
JSON
echo "disk.json updated: $USE_PERCENT used ($USED / $TOTAL)"
}
while true; do
update_disk || true
sleep 30
done
EOF
```
Vorher sicherstellen, dass das Verzeichnis existiert:
```bash
mkdir -p /opt/nginx/scripts
```
### 2. Pfade in `docker-compose.yml` anpassen
Im Repository sind relative Pfade (`./html`, `./scripts`) gesetzt. Auf dem Server können absolute Pfade verwendet werden:
```yaml
services:
nginx:
volumes:
- /opt/nginx/html:/usr/share/nginx/html:ro
- /mnt/ssd:/downloads:ro
diskinfo:
volumes:
- /mnt/ssd:/downloads:ro
- /opt/nginx/html:/html
- /opt/nginx/scripts/update-disk.sh:/update-disk.sh:ro
```
**Wichtig:** Der Host-Pfad `/mnt/ssd` muss auf das Laufwerk zeigen, dessen Speicher angezeigt werden soll. Beide Container mounten ihn als `/downloads` (nur lesend).
### 3. Container starten
```bash
cd /opt/nginx
docker compose up -d
```
### 4. Funktion prüfen
```bash
# Container-Status
docker compose ps
# diskinfo-Logs (sollte alle ~30s eine Zeile ausgeben)
docker compose logs -f diskinfo
# Generierte JSON-Datei
cat /opt/nginx/html/disk.json
```
Erwartete Log-Ausgabe:
```
diskinfo | disk.json updated: 48% used (213.7G / 468.2G)
```
Erwarteter Inhalt von `disk.json`:
```json
{
"total": "468.2G",
"used": "213.7G",
"available": "254.5G",
"use_percent": "48%",
"updated_at": "2026-06-05T12:08:16Z"
}
```
Das Feld `updated_at` dient zur Kontrolle, ob die Datei regelmäßig aktualisiert wird.
## Wie die Speicheranzeige funktioniert
### Backend (`scripts/update-disk.sh`)
1. `df -h /downloads` liefert Belegung des gemounteten Pfads
2. Werte werden als JSON nach `/html/disk.json` geschrieben
3. Eine `while true`-Schleife wiederholt das alle 30 Sekunden
Die Schleife liegt **bewusst im Shell-Skript** und nicht in der `docker-compose.yml`. So werden YAML-Parsing-Probleme (z. B. in Portainer) vermieden.
### Frontend (`html/index.html`)
- Beim Laden der Seite: `fetch('/disk.json')`
- Alle 30 Sekunden: erneuter Abruf per `setInterval`
- Anzeige: Prozent, Belegt, Frei, Gesamt sowie Fortschrittsbalken
## Docker Compose Referenz
```yaml
services:
nginx:
image: nginx:latest
container_name: nginx
restart: unless-stopped
ports:
- 80:80
volumes:
- ./html:/usr/share/nginx/html:ro
- /mnt/ssd:/downloads:ro
diskinfo:
image: alpine:latest
container_name: diskinfo
restart: unless-stopped
volumes:
- /mnt/ssd:/downloads:ro
- ./html:/html
- ./scripts/update-disk.sh:/update-disk.sh:ro
command: ["/bin/sh", "/update-disk.sh"]
```
**Hinweise:**
- Beim `diskinfo`-Service **kein** `entrypoint` setzen
- `command` als Array: `["/bin/sh", "/update-disk.sh"]`
- Die `while`-Schleife **nicht** in `command` der Compose-Datei definieren
### Was nicht funktioniert
Diese Variante führt zu Syntaxfehlern, weil YAML/Docker den Befehl an Semikolons zerlegt:
```yaml
# ❌ Nicht verwenden
entrypoint: ["/bin/sh", "-c"]
command: while true; do sh /update-disk.sh; sleep 30; done
```
Fehler im Log:
```
diskinfo | true: line 0: syntax error: unexpected end of file (expecting "do")
```
## Apps anpassen
Die Dienste-Kacheln werden in `html/index.html` im `apps`-Array konfiguriert:
```javascript
const apps = [
{ name: "Dockage", port: 5001, icon: "...", color: "#3498db" },
{ name: "SabNZBd", port: 8080, icon: "...", color: "#3498db" },
// ...
];
```
| Feld | Beschreibung |
|---|---|
| `name` | Anzeigename |
| `port` | Port auf dem Host (IP = `window.location.hostname`) |
| `icon` | URL oder Pfad zum Icon |
| `color` | Kachel-Hintergrundfarbe |
| `protocol` | Optional: `http` (Standard) oder `https` |
| `vnc` | Optional: VNC-Badge und Hinweis-Toast |
| `extra` | Optional: URL-Suffix (z. B. für Krusader VNC) |
Nach Änderungen an `index.html` reicht ein Browser-Reload; kein Container-Neustart nötig.
## Fehlerbehebung
### `diskinfo exited with code 0` (ständig neu startend)
**Ursache:** Alte Version von `update-disk.sh` ohne `while true`-Schleife. Das Skript läuft einmal durch und beendet sich.
**Prüfen:**
```bash
tail -5 /opt/nginx/scripts/update-disk.sh
```
Am Ende muss stehen:
```sh
while true; do
update_disk || true
sleep 30
done
```
**Lösung:** Aktuelles Skript aus diesem Repository nach `/opt/nginx/scripts/update-disk.sh` kopieren (siehe [Deployment](#deployment)) oder per `cat > ... << 'EOF'` direkt auf dem Server anlegen, dann Container neu erstellen:
```bash
docker compose up -d --force-recreate diskinfo
```
### `disk.json` bleibt bei 93 Bytes / alte Werte
- Altes JSON-Format ohne `updated_at` → Skript auf dem Server ist veraltet
- Neues Format ist ca. 130+ Bytes groß
### Speicheranzeige zeigt „Fehler“
```bash
# Mount im Container prüfen
docker compose exec diskinfo df -h /downloads
# Schreibrechte prüfen
docker compose exec diskinfo sh -c 'touch /html/test && rm /html/test && echo OK'
```
### `diskinfo` zeigt `ERROR: df failed for /downloads`
- Host-Pfad `/mnt/ssd` existiert nicht oder ist nicht gemountet
- Volume-Mapping in `docker-compose.yml` prüfen
### Nginx liefert `disk.json`, Werte ändern sich aber nicht
1. `docker compose ps diskinfo` → Status muss `Up` sein (nicht `Restarting`)
2. `docker compose logs diskinfo` → regelmäßige `disk.json updated`-Zeilen
3. `watch -n 5 cat /opt/nginx/html/disk.json``updated_at` muss sich ändern
## Wartung
```bash
# Container neu starten
docker compose restart
# Nur diskinfo neu erstellen (nach Skript-Update)
docker compose up -d --force-recreate diskinfo
# Logs anzeigen
docker compose logs -f
# Container stoppen
docker compose down
```
## Bekannte Einschränkungen
- Die Speicheranzeige bezieht sich auf das **Filesystem des Mounts**, nicht auf den Inhalt eines Unterordners
- nginx mountet `/downloads` nur lesend; Schreibzugriff erfolgt ausschließlich über `diskinfo` auf `/html`
- Service-Status (grüner Punkt) nutzt `fetch` mit `no-cors` und kann je nach Browser/Dienst ungenau sein
- Apple Touch Icons (`apple-touch-icon.png` etc.) sind nicht vorhanden → 404 in den nginx-Logs (harmlos)