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>
483 lines
16 KiB
Markdown
483 lines
16 KiB
Markdown
# 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: 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.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.
|