Files
boWave/plan.md
denshooter 946c0a5377 Initial implementation: Bordanlage boat onboard system
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>
2026-03-26 11:47:33 +01:00

483 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
1. **Docker-Stack** alle Backend-Dienste (Audio, Navigation, Management)
2. **React-Dashboard** ein modernes, touch-optimiertes Web-UI für Touchscreen-Display
3. **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: host` auf 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/dri` auskommentiert 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:latest` Container als snapclient
- `--host snapserver --hostID <zonename>`
- Auf dem Boot: `--soundcard hw:N,0` und `/dev/snd` device
- 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
```env
# 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
```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: 47 Knoten, sanfte Schwankungen
- COG/Heading: 200230°, leichte Drift
- Tiefe: 820m, langsame Änderung
- Windgeschwindigkeit: 1018 Knoten, böig
- Windwinkel: 3060° (am Wind), variabel
- Motorendrehzahl: 16002000 RPM
- Batterien: Starter 12.412.8V, Bord 24.825.4V
- Wassertemperatur: 1719°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.speedOverGround`
- `navigation.courseOverGroundTrue`
- `navigation.headingTrue`
- `environment.depth.belowKeel`
- `environment.wind.speedApparent`
- `environment.wind.angleApparent`
- `propulsion.*.revolutions`
- `electrical.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:
```js
{
sog, cog, heading, depth, windSpeed, windAngle, windDirection,
lat, lon, rpm, battery1, battery2, waterTemp, airTemp, rudder, fuel
}
```
**`useZones()`:**
Verbindet sich mit Snapcast.
Gibt zurück:
```js
{
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:
```js
{
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
```ini
[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
```conf
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
1. Projektstruktur und alle Konfigurationsdateien anlegen
2. `docker-compose.yml` + `docker-compose.dev.yml` + `.env` + `Makefile`
3. Mopidy Custom-Dockerfile mit Plugins
4. Dashboard Grundgerüst (Vite + React Setup)
5. Mock-System vollständig implementieren
6. Echte API-Clients implementieren
7. Hooks implementieren (useNMEA, useZones, usePlayer)
8. UI-Komponenten (Instrumente, Audio, Navigation)
9. Pages (Overview, Navigation, Audio, Systems)
10. Dashboard Dockerfile + Nginx-Config
11. 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 57621` an 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: host` auf Mac funktionieren wenn
`--host` Netzwerk erlaubt ist. Auf Windows WSL2 muss avahi im Container laufen.
- **Named Pipes** müssen VOR dem Start existieren. Das `init-pipes.sh` Skript
legt sie an. In Docker: via `entrypoint` sicherstellen 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.