Complete multiroom audio + navigation dashboard: - Docker stack: SignalK, Snapcast (4 zones), librespot, shairport-sync, Mopidy, Jellyfin, Portainer - React 18 + Vite dashboard with nautical dark theme - Full mock system (SignalK NMEA simulation, Snapcast zones, Mopidy player) - Real API clients for all services with reconnect logic - SVG instruments: Compass, WindRose, Gauge, DepthSounder, SpeedLog - Pages: Overview, Navigation, Audio (zones/radio/library), Systems - Dev mode runs fully without hardware (make dev) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
16 KiB
Bordanlage – Vollständiges Multiroom Audio + Bootsdaten Dashboard
Projektbeschreibung
Baue ein vollständiges, produktionsreifes System namens bordanlage für ein Boot.
Es besteht aus drei Teilen:
- Docker-Stack – alle Backend-Dienste (Audio, Navigation, Management)
- React-Dashboard – ein modernes, touch-optimiertes Web-UI für Touchscreen-Display
- Dev-Modus – vollständig ohne Hardware lauffähig (Mac & Windows), mit simulierten Daten
Technologie-Stack
Backend (Docker)
- Snapcast (snapserver + snapclient) → synchrones Multiroom-Audio, 4 Zonen
- librespot → Spotify Connect (echter Endpunkt, erscheint in Spotify App)
- shairport-sync → AirPlay 2 Empfänger (erscheint auf iPhone/Mac)
- Mopidy + Plugins → Web Radio (HTTP-Streams) + lokale Musikbibliothek
- Jellyfin → Mediathek (Musik, Hörbücher, Videos von Festplatte/USB)
- SignalK → NMEA 2000 / NMEA 0183 Gateway (Bootsdaten)
- Portainer → Docker Management UI
- Nginx → Reverse Proxy, served das Dashboard
Frontend (React + Vite)
- React 18 + Vite
- Keine UI-Bibliothek – eigenes Design-System
- WebSocket-Verbindung zu SignalK für Live-Bootsdaten
- Snapcast JSON-RPC API für Zonen-Steuerung
- Mopidy JSON-RPC API für Musik-Steuerung
- Jellyfin REST API für Mediathek
Projektstruktur
Erstelle folgende Verzeichnisstruktur:
bordanlage/
├── docker-compose.yml # Produktion (Boot)
├── docker-compose.dev.yml # Development Override (Mac/Windows)
├── docker-compose.override.yml # Symlink → dev wenn DEV=true
├── .env.example
├── .env
├── Makefile # make dev / make boot / make stop / make logs
├── README.md
│
├── config/
│ ├── snapserver.conf
│ ├── mopidy.conf
│ ├── shairport.conf
│ └── nginx/
│ └── default.conf
│
├── dashboard/ # React App
│ ├── package.json
│ ├── vite.config.js
│ ├── index.html
│ └── src/
│ ├── main.jsx
│ ├── App.jsx
│ ├── api/
│ │ ├── snapcast.js # Snapcast JSON-RPC Client
│ │ ├── mopidy.js # Mopidy JSON-RPC Client
│ │ ├── signalk.js # SignalK WebSocket Client
│ │ └── jellyfin.js # Jellyfin REST Client
│ ├── mock/
│ │ ├── index.js # Mock-Router: echte API wenn prod, fake wenn dev
│ │ ├── signalk.mock.js # Simulierte NMEA-Daten mit realistischen Werten
│ │ ├── snapcast.mock.js # Simulierte Zonen, Lautstärke, Quellen
│ │ └── mopidy.mock.js # Simulierte Tracks, Radio, Wiedergabe
│ ├── hooks/
│ │ ├── useNMEA.js # Hook: NMEA-Daten (echt oder mock)
│ │ ├── useZones.js # Hook: Snapcast Zonen
│ │ ├── usePlayer.js # Hook: Wiedergabe-Steuerung
│ │ └── useDocker.js # Hook: Container-Status via Portainer API
│ ├── components/
│ │ ├── layout/
│ │ │ ├── TopBar.jsx
│ │ │ └── TabNav.jsx
│ │ ├── instruments/
│ │ │ ├── Gauge.jsx # Analoges Rundinstrument (SVG)
│ │ │ ├── Compass.jsx # Kompassrose (SVG, animiert)
│ │ │ ├── WindRose.jsx # Windrose (SVG, animiert)
│ │ │ ├── DepthSounder.jsx # Tiefenmesser mit Warngrenze
│ │ │ └── SpeedLog.jsx # Fahrtmesser
│ │ ├── audio/
│ │ │ ├── NowPlaying.jsx # Track-Info, Cover, Playback-Controls
│ │ │ ├── ZoneCard.jsx # Eine Snapcast-Zone
│ │ │ ├── ZoneGrid.jsx # Alle Zonen
│ │ │ ├── SourcePicker.jsx # Quelle wählen (Spotify/AirPlay/Radio/Jellyfin)
│ │ │ ├── RadioBrowser.jsx # Senderliste mit Suche
│ │ │ └── LibraryBrowser.jsx # Jellyfin Mediathek
│ │ ├── nav/
│ │ │ ├── ChartPlaceholder.jsx # Seekarten-Iframe (OpenCPN/SignalK)
│ │ │ └── InstrumentPanel.jsx
│ │ └── systems/
│ │ ├── BatteryStatus.jsx
│ │ ├── EngineData.jsx
│ │ └── ServiceHealth.jsx # Docker-Container Status
│ └── pages/
│ ├── Overview.jsx # Tab 1: Instrumente + Now Playing + Zonen
│ ├── Navigation.jsx # Tab 2: Seekarte + alle Navigationsdaten
│ ├── Audio.jsx # Tab 3: Vollständige Audio-Steuerung
│ └── Systems.jsx # Tab 4: Batterien, Motor, Docker-Status
│
└── scripts/
├── init-pipes.sh # Named Pipes anlegen
├── setup-dev.sh # Erstkonfiguration Entwicklung
└── setup-boot.sh # Erstkonfiguration echtes Boot
Detaillierte Anforderungen
1. Docker Compose – Produktion (docker-compose.yml)
Definiere folgende Services:
signalk:
- Image:
signalk/signalk-server:latest - Port: 3000
- Volume: signalk-data
- Kommentar für NMEA-Adapter:
/dev/ttyUSB0(auskommentiert)
librespot (Spotify Connect):
- Image:
ghcr.io/librespot-org/librespot:latest - Umgebungsvariablen aus
.env:SPOTIFY_NAME,SPOTIFY_BITRATE(default: 320) - Backend:
pipe, Output:/tmp/audio/spotify.pcm - Volume: pipes
- WICHTIG: Der Service muss wirklich in der Spotify App als Gerät erscheinen.
Stelle sicher, dass
network_mode: hostauf Linux gesetzt ist (Boot). Auf Mac/Windows alternative Port-Mappings verwenden.
shairport-sync (AirPlay):
- Image:
mikebrady/shairport-sync:latest - Config:
./config/shairport.conf(Output auf/tmp/audio/airplay.pcm) - Avahi/mDNS: Auf Linux
network_mode: host. Auf Mac/Windows eigenen avahi-daemon Container (hauscontribs/avahi) bereitstellen damit AirPlay discoverable ist. - Volume: pipes
mopidy:
- Image:
ghcr.io/mopidy/mopidy:latest - Port: 6680
- Plugins installieren via Custom-Dockerfile in
./docker/mopidy/:mopidy-iris mopidy-local mopidy-stream mopidy-tunein (Web Radio Suche) mopidy-podcast - Audio-Output via GStreamer-Pipeline auf
/tmp/audio/mopidy.pcm - Volumes: pipes, mopidy-data, music
jellyfin:
- Image:
jellyfin/jellyfin:latest - Port: 8096
- Volumes: jellyfin-config, jellyfin-cache, music (read-only)
- Hardware-Decoding:
/dev/driauskommentiert für Boot
snapserver:
- Image:
ghcr.io/badaix/snapcast:latest - Ports: 1704 (Protokoll), 1705 (Control API), 1780 (Snapweb)
- Config:
./config/snapserver.conf - 3 Streams: Spotify, AirPlay, Mopidy
- depends_on: librespot, mopidy
zone-salon, zone-cockpit, zone-bug, zone-heck:
- Je ein
ghcr.io/badaix/snapcast:latestContainer als snapclient --host snapserver --hostID <zonename>- Auf dem Boot:
--soundcard hw:N,0und/dev/snddevice - Im Dev-Modus: Ohne Soundkarte (Audio wird simuliert, kein Fehler)
portainer:
- Image:
portainer/portainer-ce:latest - Port: 9000
- Volume: docker.sock + portainer-data
dashboard:
- Custom Dockerfile in
./dashboard/ - Nginx serviert den gebauten React-Build
- Port: 8080
- Env-Variablen: alle API-URLs
2. Docker Compose Dev Override (docker-compose.dev.yml)
Überschreibt für lokale Entwicklung ohne Hardware:
-
signalk: Zusätzliche Umgebungsvariable
SIGNALK_DEMO=true→ SignalK generiert dann selbst Demo-NMEA-Daten (eingebautes Feature!) -
snapclients: Alle 4 Zonen erhalten
command: snapclient --host snapserver --hostID <name> --player null(Null-Player: kein Audio-Output, kein Fehler) -
dashboard: Statt gebautem Nginx → Vite Dev-Server mit Hot Reload (
node:20-alpine,npm run dev, Port 8080) -
librespot:
LIBRESPOT_DISABLE_DISCOVERY=false→ erscheint trotzdem in Spotify (funktioniert über TCP auch ohne host network, solange Port 57621 gemappt) -
shairport: avahi-container für mDNS-Discovery auf Mac/Windows
3. .env Konfiguration
# Allgemein
COMPOSE_PROJECT_NAME=bordanlage
DEV=true
# Spotify Connect
SPOTIFY_NAME=Bordanlage
SPOTIFY_BITRATE=320
SPOTIFY_CACHE_SIZE=1024
# Boot-Info
BOAT_NAME=Meine Yacht
BOAT_MMSI=123456789
# Pfade (Musik, Logs)
MUSIC_PATH=./music
4. Makefile
dev:
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
boot:
docker compose up -d
stop:
docker compose down
logs:
docker compose logs -f
rebuild:
docker compose build --no-cache
status:
docker compose ps
5. Dashboard – Mock-System
Das Mock-System ist KRITISCH. Es muss folgendes können:
src/mock/index.js:
Exportiert einen createApi()-Factory, der prüft ob import.meta.env.DEV === true.
- Im Dev-Modus: gibt Mock-Implementierungen zurück
- Im Prod-Modus: gibt echte API-Clients zurück
src/mock/signalk.mock.js:
Simuliert einen SignalK WebSocket mit realistischen Bootsdaten.
Werte ändern sich kontinuierlich und realistisch:
- SOG: 4–7 Knoten, sanfte Schwankungen
- COG/Heading: 200–230°, leichte Drift
- Tiefe: 8–20m, langsame Änderung
- Windgeschwindigkeit: 10–18 Knoten, böig
- Windwinkel: 30–60° (am Wind), variabel
- Motorendrehzahl: 1600–2000 RPM
- Batterien: Starter 12.4–12.8V, Bord 24.8–25.4V
- Wassertemperatur: 17–19°C
- GPS: bewegt sich entlang einer realistischen Route (Ostsee-Koordinaten)
Gibt ein EventEmitter-ähnliches Objekt zurück, das on('delta', callback) unterstützt.
Format: echtes SignalK Delta-Format ({"updates": [{"values": [...]}]})
src/mock/snapcast.mock.js:
Simuliert die Snapcast JSON-RPC API:
- 4 Zonen (Salon, Cockpit, Bug, Heck)
- Salon + Cockpit initial aktiv
- Implementiert:
Server.GetStatus,Client.SetVolume,Client.SetMuted,Group.SetStream,Server.GetRPCVersion
src/mock/mopidy.mock.js:
Simuliert die Mopidy WebSocket JSON-RPC API:
- Tracks mit realistischen Metadaten (Titel, Künstler, Album, Dauer)
- Wiedergabestatus: playing/paused/stopped
- Position läuft mit
- Implementiert:
playback.get_current_track,playback.get_state,playback.get_time_position,playback.play,playback.pause,playback.next,playback.previous,tracklist.get_tracks,library.browse,library.search
6. Dashboard – API-Clients
src/api/snapcast.js:
Echter Snapcast JSON-RPC Client über WebSocket (ws://host:1705).
Implementiert alle Methoden des Mock-Clients.
Reconnect-Logik mit exponential backoff.
src/api/signalk.js:
Echter SignalK WebSocket Client (ws://host:3000/signalk/v1/stream).
Abonniert alle relevanten Pfade:
navigation.speedOverGroundnavigation.courseOverGroundTruenavigation.headingTrueenvironment.depth.belowKeelenvironment.wind.speedApparentenvironment.wind.angleApparentpropulsion.*.revolutionselectrical.batteries.*.voltage
src/api/mopidy.js:
Echter Mopidy JSON-RPC Client über WebSocket (ws://host:6680/mopidy/ws).
src/api/jellyfin.js:
Jellyfin REST API Client. Authentifizierung via API-Key aus .env.
Implementiert: Musik browsen, Alben, Künstler, Suche, Stream-URL.
7. Dashboard – Hooks
useNMEA():
Verbindet sich mit SignalK (echt oder mock).
Gibt strukturiertes Objekt zurück:
{
sog, cog, heading, depth, windSpeed, windAngle, windDirection,
lat, lon, rpm, battery1, battery2, waterTemp, airTemp, rudder, fuel
}
useZones():
Verbindet sich mit Snapcast.
Gibt zurück:
{
zones, // Array aller Zonen mit {id, name, active, volume, source, muted}
setVolume, // (zoneId, volume) => void
setMuted, // (zoneId, muted) => void
setSource, // (zoneId, streamId) => void
toggleZone, // (zoneId) => void
}
usePlayer():
Verbindet sich mit Mopidy und Spotify-Status-API.
Gibt zurück:
{
currentTrack, // {title, artist, album, duration, coverUrl}
state, // 'playing' | 'paused' | 'stopped'
position, // Sekunden
activeSource, // 'spotify' | 'airplay' | 'mopidy' | 'jellyfin'
play, pause, next, previous, seek
}
8. Dashboard – Design
Farbschema: Nautisch-dunkel. Hauptfarben:
- Background:
#07111f(tiefes Marineblau) - Surface:
#0a1928 - Border:
#1e2a3a - Text primary:
#e2eaf2 - Text muted:
#4a6080 - Accent:
#38bdf8(Cyan/Himmelblau) - Success:
#34d399 - Warning:
#f59e0b - Danger:
#ef4444 - Spotify:
#1DB954 - AirPlay:
#60a5fa
Schriften: DM Mono für Instrumentenwerte, DM Sans für UI-Text.
Instrumente (SVG-basiert, animiert):
- Rundinstrumente mit Zeiger (Gauge.jsx): SOG, Tiefe, RPM, Wassertemp
- Kompassrose (animiert, dreht sich): Heading
- Windrose: Windrichtung + Geschwindigkeit
- Alle Instrumente reagieren flüssig auf Daten-Updates (CSS transitions)
Touch-Optimierung:
- Alle Buttons mindestens 44×44px
- Volume Slider touch-fähig (pointer events)
- Keine Hover-only Interaktionen
Tabs:
- Übersicht: Alle Instrumente + Now Playing + Zonen-Schnellübersicht
- Navigation: Seekarte (SignalK iframe / Placeholder) + Detaildaten
- Audio: Vollständige Zonen-Steuerung + Quelle + Radio + Bibliothek
- Systeme: Batterien, Motor, Kraftstoff, Container-Status
DEV-Indikator:
Wenn import.meta.env.DEV === true: kleines Badge "DEV · MOCK DATA" in der TopBar.
9. Snapserver Konfiguration
[stream]
source = pipe:///tmp/audio/spotify.pcm?name=Spotify&codec=pcm&sampleformat=44100:16:2&chunk_ms=20
source = pipe:///tmp/audio/airplay.pcm?name=AirPlay&codec=pcm&sampleformat=44100:16:2&chunk_ms=20
source = pipe:///tmp/audio/mopidy.pcm?name=Mopidy&codec=pcm&sampleformat=44100:16:2&chunk_ms=20
[server]
threads = -1
[http]
enabled = true
bind_to_address = 0.0.0.0
port = 1780
[logging]
sink = system
filter = *:info
10. Shairport-sync Konfiguration
general = {
name = "Bordanlage AirPlay";
port = 5000;
interpolation = "auto";
output_backend = "pipe";
};
pipe = {
name = "/tmp/audio/airplay.pcm";
};
11. README.md
Erstelle eine vollständige README mit:
- Voraussetzungen (Docker Desktop, make)
- Schnellstart (
make dev→ http://localhost:8080) - Alle Service-URLs
- Anleitung: Wie erscheint der Spotify Connect Endpunkt?
- Anleitung: Wie erscheint der AirPlay Endpunkt?
- Anleitung: Wie füge ich Musik hinzu? (./music Ordner)
- Anleitung: Wie verbinde ich echte NMEA-Hardware?
- Migration Boot: Was muss geändert werden? (network_mode, soundcards, dri)
- Troubleshooting (häufige Fehler auf Mac/Windows)
Implementierungsreihenfolge
- Projektstruktur und alle Konfigurationsdateien anlegen
docker-compose.yml+docker-compose.dev.yml+.env+Makefile- Mopidy Custom-Dockerfile mit Plugins
- Dashboard Grundgerüst (Vite + React Setup)
- Mock-System vollständig implementieren
- Echte API-Clients implementieren
- Hooks implementieren (useNMEA, useZones, usePlayer)
- UI-Komponenten (Instrumente, Audio, Navigation)
- Pages (Overview, Navigation, Audio, Systems)
- Dashboard Dockerfile + Nginx-Config
- README.md
Wichtige Hinweise für die Implementierung
-
Spotify Connect funktioniert über mDNS. Auf Mac/Windows muss Port 57621 (UDP) gemappt sein und
--zeroconf-port 57621an librespot übergeben werden. Alternativ: Spotify-Nutzer kann den Endpunkt auch manuell über "Gerät verbinden" mit der IP des Computers finden. -
AirPlay braucht Bonjour/mDNS. Auf Mac läuft Bonjour nativ, daher kann shairport-sync mit
network_mode: hostauf Mac funktionieren wenn--hostNetzwerk erlaubt ist. Auf Windows WSL2 muss avahi im Container laufen. -
Named Pipes müssen VOR dem Start existieren. Das
init-pipes.shSkript legt sie an. In Docker: viaentrypointsicherstellen dass die Pipes existieren bevor der eigentliche Prozess startet. -
Multiroom bedeutet: alle aktiven Snapclients spielen synchron denselben Stream. Jede Zone kann aber einen anderen Stream (Quelle) zugewiesen bekommen.
-
Fehlertoleranz: Wenn eine API nicht erreichbar ist (z.B. SignalK offline), soll das Dashboard nicht abstürzen – stattdessen graceful degradation mit "Nicht verbunden" Anzeige.
Starte jetzt mit der Implementierung. Beginne mit der Verzeichnisstruktur und arbeite dich dann systematisch durch alle Komponenten.