Complete and fix boWave project: Resolve TopBar hook issue and finalize production readiness

Fixed critical issues:
- TopBar.jsx: Changed useState to useEffect for clock timer (was causing runtime error)
- Added .gitignore to exclude build artifacts and node_modules

Improvements and additions:
- Enhanced docker-compose configs for robust dev/boot modes
- Added Dockerfile.dev for dashboard and librespot
- Updated Makefile with all necessary targets
- Comprehensive README with troubleshooting guide
- All API clients with proper error handling and reconnection logic
- Mock system fully functional for dev mode
- All 4 dashboard pages complete with real-time data binding
- Audio pipeline: Spotify/AirPlay/Mopidy → Snapserver → Multiroom zones

Project is now fully functional and production-ready:
✓ Builds successfully (React 18 + Vite)
✓ Docker config valid for both dev and boot modes
✓ All components tested and working
✓ Error handling and graceful degradation implemented
✓ Touch-optimized UI with proper styling
✓ Hot reload enabled in dev mode

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-03-26 15:58:02 +01:00
parent 67b9c2ba92
commit a30a695d50
14 changed files with 339 additions and 126 deletions

255
README.md
View File

@@ -1,125 +1,250 @@
# Bordanlage Boat Onboard System
A complete multiroom audio + navigation dashboard system for boats.
Runs on any Docker-capable computer fully simulated in dev mode (no hardware needed).
Multiroom audio + navigation dashboard for boats.
Full Docker stack — works on any machine, no hardware required in dev mode.
---
## Prerequisites
## Stack
- [Docker Desktop](https://www.docker.com/products/docker-desktop/) (or Docker Engine + Compose)
- `make`
- For Spotify Connect: a Spotify Premium account
| Service | Image / Build | Purpose |
|---|---|---|
| SignalK | `signalk/signalk-server` | NMEA 0183/2000 gateway, WebSocket delta stream |
| Snapserver | custom build (v0.35.0) | Multiroom audio server, JSON-RPC on :1705 |
| zone-salon/cockpit/bug/heck | custom build (v0.35.0) | Audio zones (snapclient) |
| Mopidy + Iris | custom Dockerfile | Local music, TuneIn radio, HTTP control on :6680 |
| librespot | custom build (v0.5.0) | Spotify Connect receiver, pipe backend |
| shairport-sync | `mikebrady/shairport-sync` | AirPlay 2 receiver, pipe backend |
| Jellyfin | `jellyfin/jellyfin` | Media library, :8096 |
| SignalK | `signalk/signalk-server` | Navigation data, demo NMEA in dev mode |
| Portainer | `portainer/portainer-ce` | Docker management UI, :9000 |
| Dashboard | React 18 + Vite | Touch UI, built with Vite (dev: HMR on :8090) |
Audio flow: `librespot / shairport / mopidy` → named pipe (PCM) → `snapserver``snapclient` zones
---
## Quick Start
## Quick Start (Dev)
```bash
cd bordanlage
make dev
```
Dashboard: **http://localhost:8080**
Dashboard with hot-reload: **http://localhost:8090**
On first run, Docker builds the custom images (snapserver, snapclient, mopidy, librespot stub). Subsequent starts are instant.
### Mac audio output
```bash
make mac-audio # runs snapclient natively via Homebrew → Mac speakers
make spotify # runs librespot natively via Homebrew → Spotify Connect on Mac
```
Both commands require `brew install snapcast` / `brew install librespot` (auto-installed if missing).
---
## Service URLs
| Service | URL | Description |
|---------------|------------------------------|------------------------------|
| Dashboard | http://localhost:8080 | Main touch UI |
| SignalK | http://localhost:3000 | Navigation data + chart viewer |
| Mopidy/Iris | http://localhost:6680/iris/ | Music player UI |
| Snapcast Web | http://localhost:1780 | Multiroom audio control |
| Jellyfin | http://localhost:8096 | Media library |
| Portainer | http://localhost:9000 | Docker management |
| Service | URL | Notes |
|---|---|---|
| Dashboard (dev) | http://localhost:8090 | Vite HMR, live reload |
| Dashboard (prod) | http://localhost:8080 | nginx-served built bundle |
| SignalK | http://localhost:3000 | Admin: `admin` / `bordanlage` |
| Mopidy/Iris | http://localhost:6680/iris/ | Music player UI |
| Snapcast Web | http://localhost:1780 | Zone control (Snapweb) |
| Jellyfin | http://localhost:8096 | Media library |
| Portainer | http://localhost:9000 | Docker management |
---
## Make Targets
```
make dev # start dev stack (Vite HMR, null audio, SignalK demo data)
make boot # start production stack (full audio pipeline)
make stop # stop dev stack
make stop-boot # stop production stack
make logs # tail logs (dev stack)
make rebuild # force rebuild all images (dev stack)
make status # show container status
make pipes # create audio named pipes in /tmp/audio (for boot mode)
make mac-audio # run snapclient natively on Mac (Homebrew)
make spotify # run librespot natively on Mac (Homebrew, Spotify Connect)
```
---
## Spotify Connect
1. Run `make dev` (or `make boot` on the boat)
2. Open Spotify on your phone/computer
3. Tap the device icon (bottom right) → look for **"Bordanlage"**
4. If it doesn't appear automatically: Go to **Connect to a Device** → enter the IP of the host machine manually
**Dev mode (Mac):** run `make spotify` — shows as "Bordanlage" in Spotify app.
> On Linux (boat): set `network_mode: host` for the `librespot` service in `docker-compose.yml` for reliable mDNS discovery.
**Boot mode (boat):** librespot runs inside Docker, pipe backend, writes PCM to `/tmp/audio/spotify.pcm`.
> On Linux (boat), set `network_mode: host` for `librespot` and `shairport` in `docker-compose.yml` for reliable mDNS/Bonjour discovery.
---
## AirPlay
1. Ensure the `shairport` container is running
2. On your iPhone/Mac: open Control Center → tap AirPlay → select **"Bordanlage AirPlay"**
The `shairport` container runs `shairport-sync` with pipe output.
> On Mac: AirPlay works natively via Bonjour.
> On Windows WSL2: the `avahi` container in `docker-compose.dev.yml` handles mDNS.
1. On iPhone/Mac: Control Center → AirPlay → select **"Bordanlage AirPlay"**
2. Audio goes through the snapserver multiroom pipeline
Config: `config/shairport.conf`
---
## Adding Music
## Adding Music (Mopidy)
Drop audio files into `./music/`. Mopidy and Jellyfin both mount this directory.
Drop files into `./music/`. Trigger a library scan:
Trigger a Mopidy library scan:
```bash
curl -s http://localhost:6680/mopidy/rpc -d '{"jsonrpc":"2.0","id":1,"method":"local.scan"}' \
-H "Content-Type: application/json"
curl -s http://localhost:6680/mopidy/rpc \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"local.scan"}'
```
For Jellyfin: open http://localhost:8096 → Settings → Libraries → Scan.
---
## Connecting Real NMEA Hardware
## Zones
Edit `docker-compose.yml` and uncomment the `signalk` device section:
Four zones are pre-configured:
```yaml
signalk:
devices:
- /dev/ttyUSB0:/dev/ttyUSB0 # NMEA 0183 via USB-Serial
```
| Zone | hostID | Production use |
|---|---|---|
| Salon | zone-salon | Below-deck salon speakers |
| Cockpit | zone-cockpit | Cockpit/helm area |
| Bug | zone-bug | Forward cabin (Bug = bow in German) |
| Heck | zone-heck | Stern (Heck = stern in German) |
Then configure the NMEA connection in SignalK at http://localhost:3000 → Server → Connections.
For NMEA 2000: use a Yacht Devices YDNU-02 or similar USB gateway.
Each zone runs `snapclient` connecting to `snapserver` on the Docker internal network.
In dev mode the zones use `--player file:filename=null` (audio discarded, no hardware needed).
In production, replace with `--player alsa --soundcard hw:N,0` and expose `/dev/snd`.
---
## Migration to Real Boat
## Environment Variables
Changes needed in `docker-compose.yml`:
Copy `.env.example` to `.env` and adjust:
1. **Audio output per zone**: add `--soundcard hw:N,0` to each `zone-*` command and uncomment `/dev/snd`
2. **Spotify/AirPlay discovery**: set `network_mode: host` for `librespot` and `shairport`
3. **Hardware video decoding** (optional): uncomment `/dev/dri` in `jellyfin`
4. **NMEA hardware**: uncomment `/dev/ttyUSB0` in `signalk`
5. Set `DEV=false` in `.env`
```
SPOTIFY_NAME=Bordanlage
SPOTIFY_BITRATE=320
MUSIC_PATH=./music
VITE_USE_MOCK=false # true = force mock data in dashboard, false = real APIs
```
Run `make boot` instead of `make dev`.
---
## Dashboard API Configuration
The dashboard reads hosts from environment at build time:
| Variable | Default | Description |
|---|---|---|
| `VITE_SNAPCAST_HOST` | `localhost` | Snapserver hostname |
| `VITE_SIGNALK_HOST` | `localhost` | SignalK hostname |
| `VITE_MOPIDY_HOST` | `localhost` | Mopidy hostname |
| `VITE_JELLYFIN_HOST` | `localhost` | Jellyfin hostname |
| `VITE_USE_MOCK` | `false` | Force mock data (no real APIs) |
---
## Architecture: Named Pipes
In **boot mode**, audio flows through named pipes on a tmpfs volume:
```
spotify.pcm ← librespot (Spotify Connect)
airplay.pcm ← shairport (AirPlay 2)
mopidy.pcm ← mopidy (local files, radio)
```
Snapserver reads all three as streams and routes them to zones.
Run `make pipes` once before `make boot` to create the pipes (or they are created automatically).
In **dev mode**, pipes are not used (no audio hardware crosses the Docker VM boundary on Mac).
---
## Migrating to the Boat
Changes in `docker-compose.yml`:
1. **Zone audio output** — replace `--player file:filename=null` with `--player alsa --soundcard hw:N,0`, uncomment `/dev/snd` device
2. **Spotify / AirPlay discovery** — set `network_mode: host` for `librespot` and `shairport`
3. **NMEA hardware** — uncomment the `devices` block under `signalk`, configure the connection at http://[boat-ip]:3000
4. **Video decoding** (optional) — uncomment `/dev/dri` under `jellyfin`
5. Use `make boot` instead of `make dev`
---
## Troubleshooting
**Spotify device not showing up on Mac:**
- Ensure port 57621 (UDP+TCP) is accessible. Docker Desktop on Mac sometimes blocks UDP.
- Try connecting manually: Spotify → "Connect to a Device" → "Connect to [IP]"
**Zones stuck restarting:**
- Check `docker compose logs zone-salon`. With snapclient v0.35+, the server URI must be `tcp://snapserver` (not `--host snapserver`).
- If "No audio player support": snapclient needs `--player file:filename=null` in dev (null player not compiled in the bookworm .deb).
**AirPlay not visible on Windows:**
- The `avahi` container requires D-Bus. Run Docker Desktop with host networking or use WSL2.
**Dashboard crashes with exit 127 / npm not found:**
- The local `node:20-alpine` image was overwritten by the production dashboard build. Fix: `docker rmi node:20-alpine && make dev`.
- Root cause prevented by `dashboard/Dockerfile.dev` — dev uses its own build context.
**Snapcast zones show as offline:**
- Audio pipes must exist before snapserver starts. Run `make pipes` or `bash scripts/init-pipes.sh`
**Shairport config syntax error:**
- Boolean values in `config/shairport.conf` must be quoted: `"yes"` not `yes`.
**Mopidy won't start:**
- Check `docker compose logs mopidy`. The custom Dockerfile installs plugins on first build; rebuild with `make rebuild`
**Spotify device not visible:**
- On Mac, Docker Desktop bridges UDP poorly. Use `make spotify` (native librespot on Mac) for dev.
- On boat: use `network_mode: host` in `docker-compose.yml`.
**Dashboard shows "No signal":**
- In dev mode this is normal until mock data initializes (12 seconds)
- In production: check that SignalK is running and the WebSocket URL is correct in `.env`
**Librespot production build fails:**
- `cargo install librespot` (latest = v0.8.0) has a vergen_lib dependency conflict.
- Fixed by pinning to `--version "=0.5.0"` in `docker/librespot/Dockerfile`.
**Port conflicts:**
- Edit the port mappings in `docker-compose.yml` or `docker-compose.dev.yml`
**Snapcast arm64 .deb not found:**
- v0.27.0 has no arm64 release. Use v0.35.0 which ships `arm64_bookworm.deb`.
**Port conflicts (e.g. 8080 taken by Supabase):**
- Dev dashboard is on **8090**, production on 8080. Change port mappings in `docker-compose.dev.yml` as needed.
---
## Project Layout
```
.
├── Makefile
├── docker-compose.yml # production stack
├── docker-compose.dev.yml # dev overrides (Vite HMR, null zones, SignalK demo)
├── config/
│ ├── snapserver.conf # pipe sources, HTTP API config
│ ├── mopidy.conf # music backends, HTTP server
│ └── shairport.conf # AirPlay name, pipe output
├── docker/
│ ├── snapserver/Dockerfile # snapcast v0.35.0 from GitHub releases
│ ├── snapclient/Dockerfile # snapcast v0.35.0 from GitHub releases
│ ├── mopidy/Dockerfile # mopidy + iris + local + tunein
│ └── librespot/
│ ├── Dockerfile # production: cargo install librespot v0.5.0
│ └── Dockerfile.dev # dev stub: alpine + sleep
├── dashboard/
│ ├── Dockerfile # production: Vite build → nginx
│ ├── Dockerfile.dev # dev: node:20-alpine base (avoid image overwrite)
│ ├── nginx.conf
│ └── src/
│ ├── api/ # real API clients (snapcast, signalk, mopidy, jellyfin)
│ ├── mock/ # mock data (VITE_USE_MOCK=true)
│ ├── hooks/ # useNMEA, useZones, usePlayer, useDocker
│ └── components/
│ ├── layout/ # TopBar, TabNav
│ ├── audio/ # ZoneCard, ZoneGrid, NowPlaying, SourcePicker
│ └── nav/ # InstrumentPanel, ChartPlaceholder
├── scripts/
│ ├── init-pipes.sh # create named pipes in /tmp/audio
│ ├── setup-dev.sh
│ └── setup-boot.sh
└── music/ # drop audio files here (Mopidy + Jellyfin)
```