Components
shadcn/ui Components
The app uses shadcn/ui for standard UI components. These are installed directly into src/components/ui/ and can be customized:
| Component | Usage |
|---|---|
| Button | Primary actions, form submissions, destructive actions |
| Card | Content containers for dashboard cards, detail sections |
| Input | Text inputs with label support |
| Select | Dropdown selects for filters, form fields |
| Badge | Trip status badges (ACTIVE, COMPLETED, CANCELLED) |
| Checkbox | Toggle switches (GPS logging, auto-refresh) |
| Label | Form field labels |
| Textarea | Multi-line text input (descriptions, notes) |
| Separator | Visual dividers |
| DropdownMenu | Navigation dropdowns (Features menu in header) |
Theme
shadcn/ui uses CSS variables for theming with automatic dark mode support via the dark class on <html>. The theme is configured in globals.css with oklch colors.
Shared Components (@elcto/apollon-ui)
Custom components that are shared across apps via the @elcto/apollon-ui workspace package.
Map
File: shared/ui/src/components/map.tsx
Interactive MapLibre GL map with composable children components. Supports multiple tile providers with automatic dark/light theme switching.
Map Props
| Prop | Type | Default | Description |
|---|---|---|---|
latitude | number | — | Center latitude |
longitude | number | — | Center longitude |
center | [lng, lat] | — | Center as GeoJSON array (alternative to lat/lng) |
zoom | number | 13 | Initial zoom level |
tiles | MapTileProvider | 'carto' | Tile provider |
themeOverride | 'light' | 'dark' | — | Override auto theme detection |
children | ReactNode | — | Map children (markers, routes, etc.) |
Tile Providers
| Provider | Light | Dark |
|---|---|---|
carto | Carto Voyager | Carto Dark Matter |
openfreemap | OpenFreeMap Bright | Carto Dark Matter |
openfreemap3d | OpenFreeMap Liberty (60° pitch) | Carto Dark Matter |
positron | OpenFreeMap Positron | Carto Dark Matter |
liberty | OpenFreeMap Liberty (flat) | Carto Dark Matter |
fiord | OpenFreeMap Fiord | Carto Dark Matter |
Child Components
| Component | Description |
|---|---|
MapMarker | Position marker with React children (dot, arrow, badge) |
StaticMarker | Simple colored dot marker |
MapRoute | GeoJSON line layer for routes |
FitBounds | Auto-fit map to given points |
Current Position Marker
The GPS page uses MapMarker with a CurrentPositionMarker child that renders:
- Dot —
#14F5BFwith beacon pulse animation - Heading arrow — SVG chevron pointing in direction of travel (only when speed > 2 km/h)
- Speed badge — Spring-animated speed display with
useSpring, positioned opposite to heading arrow using 8-point anchoring - Dark mode — Adapts border/stroke colors for dark map tiles
Usage
import { Map, MapMarker, StaticMarker, MapRoute, FitBounds } from '@elcto/apollon-ui';
<Map latitude={52.52} longitude={13.40} zoom={14} tiles="carto">
<MapMarker longitude={13.40} latitude={52.52}>
<CurrentPositionMarker speed={12.5} heading={90} />
</MapMarker>
<StaticMarker latitude={52.51} longitude={13.39} color="#22c55e" />
<MapRoute points={gpsHistory} />
<FitBounds points={allPoints} />
</Map>
Spinner
Loading indicator with configurable size (sm, md, lg).
import { Spinner } from '@elcto/apollon-ui';
<Spinner size="md" />
ThemeToggle
Dark/light mode toggle with system preference detection. Persists to localStorage.
import { ThemeToggle } from '@elcto/apollon-ui';
<ThemeToggle />
Speed Overlay
Files: src/app/(overlay)/overlay/speed/page.tsx (Server) + speed-client.tsx (Client)
A minimal, embeddable speed display designed for OBS Studio browser source.
- Real-time WebSocket — Subscribes to the
speedchannel - Spring animation — Smooth transitions via
useSpringhook - Multi-Unit Cycling — Kommagetrennte Units mit Fade-Animation (
?unit=kmh,knots) - OBS compatible — Transparent background, dark card design (
#14F5BFaccent) - Float mode —
?float=truefür freistehende Darstellung ohne Border
For URL parameters, see Overlays.
Data Overlay
Files: src/app/(overlay)/overlay/data/page.tsx (Server) + data-client.tsx (Client)
Kombiniert Compass und Speed-Widget in einer kompakten Leiste — identisch zum Widget-Bar im Map-Overlay, als Standalone ohne Karte.
- Real-time WebSocket — Subscribes to
gps+speedchannels - Compass + Speed — Divider-getrennt, einzeln ein-/ausblendbar
- Multi-Unit Cycling — Kommagetrennte Units mit Fade-Animation
- Float mode —
?float=truefür freistehende Darstellung ohne Border
For URL parameters, see Overlays.
Map Overlay
Files: src/app/(overlay)/overlay/map/page.tsx (Server) + map-client.tsx (Client)
Fullpage live GPS map for OBS Studio browser source with heading arrow, speed badge, compass widget, and AIS vessel tracking.
For the full list of URL parameters, see Overlays.
Compass Overlay
Files: src/app/(overlay)/overlay/compass/page.tsx (Server) + compass-client.tsx (Client)
Standalone compass widget for OBS. Shows heading needle, degree value, and 16-point cardinal direction.
For URL parameters, see Overlays.
Pegel Overlay
Files: src/app/(overlay)/overlay/pegel/page.tsx (Server) + pegel-client.tsx (Client)
Standalone Wasserstand-Widget für OBS. Zeigt aktuellen Pegel der nächsten WSV-Station mit Gewässername und Trend-Farbkodierung (blau=niedrig, grün=normal, rot=hoch, grau=unbekannt).
For URL parameters, see Overlays.
AIS Vessel Markers
The map overlay displays AIS (Automatic Identification System) vessel data received via the vessels WebSocket channel. The Rust API supports two AIS sources, each toggled independently: AISstream.io ([ais.aisstream], wss://stream.aisstream.io, via tokio-tungstenite) and/or a local NMEA/AIS receiver ([ais.local], e.g. Quark-elec A027+, raw NMEA 0183 over TCP, decoded by the hand-rolled apollon-ais-nmea crate). Enabling both runs dual mode: the local receiver takes priority for vessels in antenna range while aisstream fills in everything beyond it (see Configuration). Both sources feed the same vessels channel, so the map works unchanged regardless of which are active. Each vessel carries a source field ("local" or "aisstream") marking its effective tracking source, so the frontend can distinguish or style them.
Vessel Display
| Vessel State | Marker | Arrow | Badge |
|---|---|---|---|
| Moving (Unterwegs) | Orange dot | Orange heading arrow | Speed, name, info rotation |
| Stationary (Vor Anker, Festgemacht, Auf Grund) | Gray dot | None | Name only, no speed |
Badge Info Rotation
Moving vessels rotate through available info badges every 10 seconds with a fade animation:
- Speed in km/h (only shown when > 0)
- Length (meters, from ShipStaticData: A+B, if available)
- Beam (meters, from ShipStaticData: C+D, if available)
- Ship type (if not "Unknown")
When only one info item is available, it displays statically without fade. Ships without name show only the info badge. Ships without any data (0 km/h, no type/length/beam) show no badge.
URL Parameters
| Parameter | Default | Description |
|---|---|---|
ais | true | Show AIS vessels. false / 0 to disable |
aisnames | true | Show vessel name badges. false / 0 to disable |
aisinfo | true | Show info badges (speed, length, beam, type). false / 0 to disable |
aisbadgekm | 10 | Max distance (km) for name/info badges |
aisbadgescale | 1 | Scale factor for AIS badges (e.g. 1.5, 2) |
Distance-Based Opacity
Badge colors and blur scale with distance from own ship (no container opacity, so backdrop-blur remains visible):
- ≤1km: full visibility
- 1–2km: 95%
- 2–3km: 85%
- 3–4km: 70%
- 4–5km: 50%
- 5–8km: 35%
- ≥8km: 15% minimum
z-index Layering
| Layer | z-index |
|---|---|
| Own ship marker | 100 |
| Moving vessels | 10 |
| Stationary vessels | 1 |
Nav Status (German Labels)
Unterwegs, Vor Anker, Festgemacht, Nicht unter Kommando, Tiefgangbeschränkt, Auf Grund, Fischend, Unter Segel, Manövrierunfähig.
Configuration
AIS is configured in the Rust API via the [ais] TOML section:
| Key | Description |
|---|---|
enabled | Master switch for AIS tracking |
local_priority_ttl_secs | Dual mode: seconds an MMSI stays local-tracked after its last local message before aisstream resumes it (default 120) |
ignore_mmsi | Array of MMSI numbers to blacklist — applies to both sources |
The AISstream.io source is configured in [ais.aisstream]:
| Key | Description |
|---|---|
enabled | Enable the AISstream.io source |
api_key | AISstream.io API key (required when this source is enabled) |
radius_km | Radius around GPS position for the aisstream vessel subscription |
The local receiver is configured in [ais.local]:
| Key | Description |
|---|---|
enabled | Enable the local NMEA/AIS receiver source |
host | Local receiver host, e.g. 192.168.1.69 |
port | Local receiver TCP port, e.g. 2000 |
reception_range_km | Optional. Only affects the map's bbox circle — a physical VHF receiver has no fixed radius, so received vessels are never filtered by this value. Unset = no bbox shown. |
Enabling both [ais.aisstream] and [ais.local] runs dual mode: local takes priority for in-range vessels, aisstream is secondary. A vessel heard live via local is not double-tracked from aisstream; once it goes silent on local for longer than local_priority_ttl_secs (out of antenna range), aisstream resumes tracking it (handoff).
WatchlistStars
File: src/components/watchlist-stars.tsx
Overlay component that displays the current watchlist alert status. Used by the map overlay and watchlist overlay pages.
Props
| Prop | Type | Description |
|---|---|---|
alert | WatchlistAlert | null | The current top alert to display. null hides the component. |
Display Modes
- Stars mode (
displayType: "stars") -- Shows 5 stars filled right-to-left based onalert.starscount. The boundary star (first filled) pulses with astar-pulseanimation. Stars usealert.starColorfor fill/stroke. - Custom icon mode (
displayType: "custom") -- Renders a Lucide icon specified byalert.overlayIcon(e.g."ship","anchor"). The icon pulses with anicon-pulseanimation and usesalert.starColorfor fill/stroke. - Hidden -- Returns
nullwhenalertisnulloralert.overlayEnabledisfalse.
Behavior
- Appears in the top-right corner with a fade-in + slide-in animation
- Dark backdrop blur container for OBS compatibility
- Lucide icon names are resolved dynamically and cached for performance
import { WatchlistStars } from '@/components/watchlist-stars';
<WatchlistStars alert={topAlert} />
Watchlist Overlay
Files: src/app/(overlay)/overlay/watchlist/page.tsx (Server) + watchlist-client.tsx (Client)
Standalone watchlist alert overlay for OBS Studio browser source. Subscribes to the watchlist WebSocket channel and displays the WatchlistStars component with the top alert.
GPS Tracker Page
Files: src/app/(app)/gps/page.tsx (Server) + gps-client.tsx (Client)
Displays real-time GPS position via WebSocket with interactive map.
- Subscribes to
speedandgpsWebSocket channels for live updates - Dead reckoning interpolation for smooth marker movement between GPS updates
- Heading arrow + spring-animated speed badge on current position marker
- Beacon pulse animation on dot
- Tile provider selector (Carto / OpenStreetMap) with auto dark/light mode
- Map auto-centers on position via
LiveTracker - Manual refresh button as REST fallback
Vessels Registry
Files: src/app/(app)/feature/vessels/page.tsx (Registry + History Log tabs) + [mmsi]/page.tsx (detail) + [mmsi]/edit/page.tsx (edit)
Vessels, die bereits in der Watchlist hinterlegt sind, tragen in der Registry-Tabelle ein Auge-Icon direkt neben der MMSI. Ein Klick öffnet die Watchlist vorgefiltert auf diese MMSI (/feature/watchlist?q=<mmsi>). Über den Filter „Nur Watchlist" lässt sich die Tabelle auf beobachtete Vessels einschränken. Auf der Detail-Page ([mmsi]) erscheint bei beobachteten Vessels zusätzlich ein „Watchlist"-Button neben „Edit" mit demselben Deep-Link.
Umgekehrt verlinkt die Watchlist-Tabelle (/feature/watchlist) jede MMSI auf die zugehörige Vessel-Detail-Page (/feature/vessels/<mmsi>).
Der Watchlist-Match ist rein client-seitig (kein API-Feld): die Vessels-Seiten laden watchlist/entries mit und matchen per MMSI über den Helper feature/vessels/watchlist-match.ts (buildWatchedSet / isWatched, normalisiert number→string). Die ?q=-Vorfilterung wird auf der Watchlist-Seite über useSearchParams() (in einer Suspense-Boundary) als initialer Suchwert geseedet.
Trip Management
Full CRUD with filtering by status, client, category, date range, GPS tracking, and search.
Pages
| Page | Path | Description |
|---|---|---|
| Trip List | /trips | Filterable list with status badges |
| New Trip | /trips/new | Create with sender/receiver, cargo, GPS toggle |
| Trip Detail | /trips/[id] | Map with route, complete/delete actions |
| Edit Trip | /trips/[id]/edit | Update all trip fields |
| Categories | /trips/categories | Inline CRUD for cargo categories |
Client Management
Client directory with address management, contact persons, and trip associations.
Pages
| Page | Path | Description |
|---|---|---|
| Client List | /clients | Names, emails, trip counts |
| New Client | /clients/new | Company info, address, dynamic contacts |
| Client Detail | /clients/[id] | Full info with trips as sender/receiver |
Watchlist Management
Vessel proximity watchlist with categories, custom sounds, and configurable alert thresholds.
Pages
| Page | Path | Description |
|---|---|---|
| Entry List | /feature/watchlist | All tracked vessels with MMSI/name search |
| New Entry | /feature/watchlist/new | Add vessel by MMSI |
| Entry Detail | /feature/watchlist/[id] | View/edit entry, category, sounds, thresholds |
| Settings | /feature/watchlist/settings | Global defaults, categories, sound management |
API Proxy Routes
| Route | Description |
|---|---|
/api/watchlist/entries | Entry CRUD |
/api/watchlist/categories | Category CRUD |
/api/watchlist/settings | Settings read/update |
/api/watchlist/sounds | Sound list |
/api/watchlist/sounds/upload | Sound upload (multipart) |
/api/watchlist/test-alert | Trigger test alert |