336 lines
9.3 KiB
Markdown
336 lines
9.3 KiB
Markdown
# 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)
|