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
| Technology | Purpose |
|---|---|
| Next.js 16 | React framework with App Router + Turbopack |
| React 19 | UI rendering |
| TypeScript | Type-safe application code |
| Tailwind CSS 4 | Utility-first styling |
| shadcn/ui | Accessible UI components (Button, Card, Input, Select, Badge, etc.) |
| MapLibre GL | Interactive maps with multiple tile providers (Carto, OpenFreeMap) |
| Zustand | Client-side state management |
| Motion | Spring-based animations (speed overlay) |
| @elcto/api | Shared GraphQL client, WebSocket client, types |
| @elcto/ui | Shared components (Map, MapMarker, MapRoute, Spinner, ThemeToggle, AIS Vessel Markers) |
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/api, transforming GraphQL responses to REST - WebSocket: Browser connects directly to Rust API for real-time GPS, speed, trip, and AIS vessel 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 + ThemeToggle
│ │ │ ├── page.tsx # Dashboard
│ │ │ ├── gps/page.tsx # GPS tracker
│ │ │ ├── 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
│ │ ├── (overlay)/ # Overlay layout (no nav)
│ │ │ └── overlay/
│ │ │ ├── speed/page.tsx # Speed overlay (OBS)
│ │ │ ├── map/page.tsx # Map overlay (OBS, URL params)
│ │ │ └── compass/page.tsx # Compass overlay (OBS)
│ │ └── api/ # REST proxy routes
│ │ ├── gps/ # GPS endpoints
│ │ ├── trips/ # Trip CRUD
│ │ ├── clients/ # Client CRUD
│ │ └── categories/ # Category CRUD
│ ├── components/ui/ # shadcn/ui components
│ ├── hooks/
│ │ ├── use-spring.ts # Spring animation hook
│ │ └── use-interpolated-position.ts # Dead reckoning GPS interpolation
│ └── 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.(overlay)— Minimal layout without navigation. Used for embeddable overlays: speed display, live map, and compass (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
| Variable | Default | Description |
|---|---|---|
NEXT_PUBLIC_API_URL | http://localhost:3000 | Rust API base URL |
NEXT_PUBLIC_WS_URL | ws://localhost:3000 | WebSocket URL (optional, derived from API URL) |
NEXT_PUBLIC_WS_TOKEN | (empty) | WebSocket auth token (?token= param) |
API_KEY | (empty) | Server-only API key for GraphQL (X-API-Key header) |
SENTRY_DSN | (empty) | Sentry error tracking DSN |
NODE_ENV | development | Node.js environment |
PORT | 5001 | Dev server port |
WebSocket Client
The createWebSocket() function from @elcto/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/api/clients';
const ws = createWebSocket('/v1/ws', {
token: process.env.NEXT_PUBLIC_WS_TOKEN,
channels: ['speed', 'gps'],
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);
}
});