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>
This commit is contained in:
2026-03-26 11:47:33 +01:00
commit 946c0a5377
57 changed files with 3450 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
export default function ZoneCard({ zone, onVolume, onMute, onSource }) {
const { id, name, active, volume, muted, source } = zone
return (
<div style={{
...styles.card,
opacity: active ? 1 : 0.45,
borderColor: active && !muted ? 'var(--accent)' : 'var(--border)',
}}>
<div style={styles.header}>
<span style={styles.name}>{name}</span>
<div style={styles.badges}>
<span style={{ ...styles.badge, background: active ? '#34d39922' : 'var(--border)', color: active ? 'var(--success)' : 'var(--muted)' }}>
{active ? 'ON' : 'OFF'}
</span>
</div>
</div>
<div style={styles.source}>{source}</div>
<div style={styles.volumeRow}>
<button style={styles.muteBtn} onClick={() => onMute(id, !muted)}>
{muted ? '🔇' : '🔊'}
</button>
<input
type="range" min={0} max={100} value={muted ? 0 : volume}
onChange={e => onVolume(id, Number(e.target.value))}
style={{ flex: 1 }}
/>
<span style={styles.volVal}>{muted ? '' : volume}</span>
</div>
</div>
)
}
const styles = {
card: {
padding: 14,
background: 'var(--surface)',
borderRadius: 'var(--radius)',
border: '1px solid var(--border)',
display: 'flex', flexDirection: 'column', gap: 10,
transition: 'border-color 0.2s, opacity 0.2s',
},
header: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' },
name: { fontWeight: 600, fontSize: 14 },
badges: { display: 'flex', gap: 4 },
badge: { fontSize: 10, padding: '2px 6px', borderRadius: 4, fontWeight: 700 },
source: { fontSize: 11, color: 'var(--muted)' },
volumeRow: { display: 'flex', alignItems: 'center', gap: 10 },
muteBtn: { fontSize: 18, minWidth: 44, minHeight: 44 },
volVal: { fontFamily: 'var(--font-mono)', fontSize: 13, minWidth: 26, textAlign: 'right', color: 'var(--muted)' },
}