Skip to main content

Web App Overview

The Apollon web app is a Next.js 16 application that provides a real-time GPS tracking dashboard for logistics and cargo operations. It connects to the Rust API via REST proxy routes (GraphQL under the hood) and WebSocket for real-time data.

Tech Stack

TechnologyPurpose
Next.js 16React framework with App Router + Turbopack
React 19UI rendering
TypeScriptType-safe application code
Tailwind CSS 4Utility-first styling
shadcn/uiAccessible UI components (Button, Card, Input, Select, Badge, etc.)
MapLibre GLInteractive maps with multiple tile providers (Carto, OpenFreeMap)
ZustandClient-side state management
MotionSpring-based animations (speed overlay)
@elcto/apollon-apiShared GraphQL client, WebSocket client, types
@elcto/apollon-uiShared components (Map, MapMarker, MapRoute, Spinner, ThemeToggle, AIS Vessel Markers)
@elcto/apollon-loggerStructured logger with Sentry integration
@sentry/nextjsError tracking (client + server + edge)

Data Flow

Browser → fetch('/api/*') → Next.js API Route → gql() → Rust API /v1/gql
Browser → WebSocket → Rust API /v1/ws (direct, no proxy)
  • REST Proxy: Next.js API routes call gql() from @elcto/apollon-api, transforming GraphQL responses to REST
  • WebSocket: Browser connects directly to Rust API for real-time GPS, speed, trip, AIS vessel, and watchlist alert data via channels
  • No GraphQL client in browser — frontend uses only fetch() against /api/* proxy routes

Project Structure

platform/web/
├── src/
│ ├── app/
│ │ ├── globals.css # Tailwind + shadcn theme
│ │ ├── layout.tsx # Root layout (Roboto font)
│ │ ├── (app)/ # Main app with navbar
│ │ │ ├── layout.tsx # Navbar (Trips, Clients, GPS, Features ▾) + ThemeToggle
│ │ │ ├── page.tsx # Dashboard
│ │ │ ├── gps/
│ │ │ │ ├── page.tsx # Server: reads config, renders client
│ │ │ │ └── gps-client.tsx # Client: GPS tracker with WS
│ │ │ ├── trips/
│ │ │ │ ├── page.tsx # Trip list (filterable)
│ │ │ │ ├── new/page.tsx # Create trip
│ │ │ │ ├── [id]/page.tsx # Trip detail
│ │ │ │ ├── [id]/edit/page.tsx # Edit trip
│ │ │ │ └── categories/page.tsx
│ │ │ ├── clients/
│ │ │ │ ├── page.tsx # Client list
│ │ │ │ ├── new/page.tsx # Create client
│ │ │ │ └── [id]/page.tsx # Client detail
│ │ │ └── feature/
│ │ │ ├── watchlist/
│ │ │ │ ├── page.tsx # Watchlist entries list
│ │ │ │ ├── new/page.tsx # Create entry
│ │ │ │ ├── [id]/page.tsx # Entry detail/edit
│ │ │ │ └── settings/page.tsx # Global settings + categories
│ │ │ └── vessels/
│ │ │ ├── page.tsx # Vessels registry (Registry + History Log tabs)
│ │ │ └── [mmsi]/page.tsx # Per-vessel detail
│ │ ├── (overlay)/ # Overlay layout (no nav)
│ │ │ └── overlay/
│ │ │ ├── speed/
│ │ │ │ ├── page.tsx # Server wrapper
│ │ │ │ └── speed-client.tsx # Client: speed display
│ │ │ ├── map/
│ │ │ │ ├── page.tsx # Server wrapper
│ │ │ │ └── map-client.tsx # Client: live map + AIS
│ │ │ ├── compass/
│ │ │ │ ├── page.tsx # Server wrapper
│ │ │ │ └── compass-client.tsx # Client: compass display
│ │ │ ├── data/
│ │ │ │ ├── page.tsx # Server wrapper
│ │ │ │ └── data-client.tsx # Client: compass + speed bar
│ │ │ ├── pegel/
│ │ │ │ ├── page.tsx # Server wrapper
│ │ │ │ └── pegel-client.tsx # Client: water level display
│ │ │ └── watchlist/
│ │ │ ├── page.tsx # Server wrapper
│ │ │ └── watchlist-client.tsx # Client: alert stars/icon
│ │ └── api/ # REST proxy routes
│ │ ├── gps/ # GPS endpoints
│ │ ├── trips/ # Trip CRUD
│ │ ├── clients/ # Client CRUD
│ │ ├── categories/ # Category CRUD
│ │ └── watchlist/ # Watchlist CRUD + sounds + settings
│ ├── components/
│ │ ├── ui/ # shadcn/ui components
│ │ ├── pegel-widget.tsx # Shared water level widget
│ │ └── watchlist-stars.tsx # Watchlist alert stars/icon overlay
│ ├── hooks/
│ │ ├── use-spring.ts # Spring animation hook
│ │ ├── use-interpolated-position.ts # Dead reckoning GPS interpolation
│ │ ├── use-pegel.ts # Pegelonline water level polling
│ │ └── use-watchlist.ts # Watchlist WebSocket alert state
│ └── lib/
│ ├── api.ts # Client-side fetch wrapper
│ └── utils/ # Formatting helpers
├── components.json # shadcn configuration
├── next.config.ts
├── postcss.config.mjs
└── tsconfig.json

Route Groups

The app uses Next.js route groups to provide different layouts:

  • (app) — Main application with navigation bar and standard layout. All CRUD pages live here, including the AIS Vessels registry page (Registry + History Log tabs, with a per-vessel detail view backed by the vessels registry queries/endpoints).
  • (overlay) — Minimal layout without navigation. Used for embeddable overlays: speed display, live map, compass, pegel, and watchlist alerts (OBS browser source).

Development

just dev-web # Start dev server (port 5001)
just watch-web # Same as dev-web
just build-web # Production build
just typecheck-web # TypeScript check
just lint-web # ESLint

Configuration

Runtime environment variables (server-side, Docker-friendly):

VariableDefaultDescription
APOLLON_API_URLhttp://localhost:3000Rust API base URL
APOLLON_WS_URL(derived from API URL)WebSocket URL
APOLLON_WS_TOKEN(empty)WebSocket auth token
APOLLON_API_KEY(empty)Server-only API key for GraphQL
SENTRY_DSN(empty)Sentry DSN (server-side)
NEXT_PUBLIC_SENTRY_DSN(empty)Sentry DSN (client-side)
NODE_ENVdevelopmentNode.js environment
PORT5001Dev server port

Legacy NEXT_PUBLIC_* variables are supported as fallbacks but APOLLON_* variables take precedence and are read at runtime (not embedded at build time).

Runtime Config Pattern

Server-Component page.tsx reads env vars via @/lib/config and passes them as props to Client-Components (same pattern as Lumio):

// page.tsx (Server Component)
import { getWsUrl, getWsToken } from '@/lib/config';
import { MapOverlayClient } from './map-client';

export default function MapOverlayPage() {
return <MapOverlayClient wsUrl={getWsUrl()} wsToken={getWsToken()} />;
}

WebSocket Client

The createWebSocket() function from @elcto/apollon-api provides:

  • Auto-reconnect with exponential backoff (3s base, max 30s)
  • Token authentication via ?token= query parameter
  • JSON message parsing with type routing
  • Clean disconnect with intentional close detection
import { createWebSocket } from '@elcto/apollon-api/clients';

const ws = createWebSocket('/v1/ws', {
wsUrl,
token: wsToken,
channels: ['speed', 'gps', 'watchlist'],
autoReconnect: true,
maxReconnectAttempts: 10,
});

ws.onMessage((data) => {
const msg = data as { type: string; data: unknown };
if (msg.type === 'speed') {
console.log('Speed:', msg.data);
}
if (msg.type === 'gps') {
console.log('GPS:', msg.data);
}
if (msg.type === 'vessels') {
console.log('AIS Vessels:', msg.data);
}
});