A real-time monitoring service for a fleet of ~50 autonomous industrial vehicles operating in a warehouse. Each vehicle emits a JSON telemetry event at 1 Hz. The system ingests those events, detects anomalies, tracks per-zone entry counts, and surfaces fleet state through a REST API and an operator dashboard.
- Architecture Overview
- Tech Stack
- Project Structure
- Prerequisites
- Getting Started
- API Reference
- Anomaly Detection
- Warehouse Zones
- Development Commands
- Running Tests
- CI/CD
- Configuration
- Design Decisions
┌──────────────────────────────────────────────────┐
│ Vehicle Fleet (50 vehicles, 1 Hz telemetry) │
└────────────────────────┬─────────────────────────┘
│ POST /api/v1/telemetry/events
▼
┌──────────────────────────────────────────────────┐
│ Django + Ninja REST API (port 8000) │
│ ├── fleet app (Vehicle, Mission, Zone) │
│ ├── telemetry app (TelemetryEvent, ingest) │
│ └── anomalies app (9 detectors) │
└───────────┬──────────────────────┬───────────────┘
│ │
┌───────▼──────┐ ┌─────────▼──────────┐
│ PostgreSQL │ │ Redis │
│ (port 5433) │ │ (zone counters, │
└──────────────┘ │ Celery broker) │
└────────────────────┘
│
┌─────────────▼──────────┐
│ React Dashboard │
│ (port 5173) │
└────────────────────────┘
The backend is split into three Django apps with a one-way dependency graph: fleet ← telemetry ← anomalies.
| Layer | Technology |
|---|---|
| Backend language | Python ≥ 3.13 |
| Web framework | Django 6, Django Ninja 1.x |
| Schema validation | Pydantic v2 |
| Database | PostgreSQL 17 |
| Cache / broker | Redis 7 |
| Task queue | Celery 5 |
| Package manager | uv |
| Frontend language | TypeScript (strict) |
| Frontend framework | React 19 |
| Build tool | Vite 5 |
| State management | Redux Toolkit + RTK Query |
| Styling | Tailwind CSS v3 |
| Backend testing | pytest, factory-boy |
| Frontend testing | Vitest, Playwright |
| Containerisation | Docker (multi-stage) |
| Task runner | GNU Make |
fleet-telemetry/
├── backend/ # Django + Ninja API
│ ├── apps/
│ │ ├── fleet/ # Vehicle, Mission, MaintenanceRecord, Zone
│ │ ├── telemetry/ # TelemetryEvent + ingest pipeline
│ │ └── anomalies/ # Anomaly model + 9 detectors
│ ├── fleet_telemetry/ # Django project (settings, urls, asgi)
│ ├── Dockerfile
│ ├── pyproject.toml
│ └── .env.example
├── frontend/ # React + Vite operator dashboard
│ ├── src/
│ │ ├── features/
│ │ │ ├── fleet/ # Fleet state & vehicle list
│ │ │ ├── anomalies/ # Anomaly feed
│ │ │ ├── zones/ # Zone entry counters
│ │ │ ├── dashboard/ # Combined operator view
│ │ │ └── health/ # API health indicator
│ │ └── app/ # Redux store, RTK Query base
│ ├── Dockerfile
│ └── package.json
├── docs/
│ ├── ADR.md # Architecture Decision Record
│ └── AI_INTERACTION_LOG.md # AI Interaction Log
├── .github/workflows/ci.yml # GitHub Actions CI
├── docker-compose.yml # Local dev stack
├── Makefile # Single entry point — `make help`
└── CLAUDE.md # AI-assistant project guide
| Tool | Version |
|---|---|
| Docker & Docker Compose | latest |
| Python | ≥ 3.13 |
| uv | latest |
| Node.js | 22 LTS |
| pnpm | 10 |
| GNU Make | any |
git clone https://github.com/lfbos/fleet-telemetry.git
cd fleet-telemetry
cp backend/.env.example backend/.env # default credentials work locallymake docker-upPostgres is exposed on host port 5433. Redis is on 6379.
make install # backend (uv sync) + frontend (pnpm install)make be-migrate # creates 20 zones and 50 vehiclesOpen two terminals (or use separate processes):
# Terminal 1 — Django API on :8000
make be-dev
# Terminal 2 — Vite dev server on :5173
make fe-devBrowse the interactive OpenAPI docs at http://localhost:8000/api/v1/docs.
The dashboard is at http://localhost:5173.
To run everything in containers (including hot-reload for both backend and frontend):
docker-compose upTo enable the async Celery worker (moves anomaly detection off the request thread):
docker-compose --profile async upAll endpoints are under /api/v1. Full, interactive schema is available at /api/v1/docs.
| Method | Path | Description |
|---|---|---|
POST |
/telemetry/events |
Ingest a single telemetry event |
Event payload:
{
"vehicle_id": "V-001",
"timestamp": "2026-05-15T21:00:00Z",
"lat": 37.7749,
"lon": -122.4194,
"battery_pct": 82.5,
"speed_mps": 1.2,
"status": "moving",
"error_codes": [],
"zone_entered": "aisle_a",
"idempotency_key": "optional-unique-key"
}| Method | Path | Description |
|---|---|---|
GET |
/fleet/state |
Per-status vehicle counts + total |
GET |
/vehicles |
List all vehicles with current state |
POST |
/vehicles/{vehicle_id}/fault |
Operator override: transition vehicle to fault (cancels active mission, opens maintenance record) |
| Method | Path | Description |
|---|---|---|
GET |
/zones/counts |
Per-zone entry counts |
| Method | Path | Description |
|---|---|---|
GET |
/anomalies |
List anomalies; filter by vehicle_id, from, to, limit (1–500, default 50) |
| Method | Path | Description |
|---|---|---|
GET |
/health |
Liveness probe |
Anomaly detection runs on every ingested event (per-event detectors) and on a schedule driven by Celery beat (see settings.CELERY_BEAT_SCHEDULE).
| Code | Trigger |
|---|---|
FAULT_REPORTED |
Vehicle status is fault |
LOW_BATTERY |
Battery ≤ 15 % (critical alert at ≤ 5 %) |
OVERSPEED |
Speed exceeds warehouse limit |
BATTERY_DROP_RATE |
Battery draining faster than expected |
ACCELERATION_SPIKE |
Sudden speed change (throttle or brake) |
BATTERY_SENSOR_STUCK |
Battery reading unchanged despite movement |
| Code | Trigger |
|---|---|
TELEMETRY_GAP |
Vehicle stopped sending data |
STUCK |
Vehicle reports moving but speed stays near zero |
CHRONIC_FAULT_RATE |
Vehicle accumulated more than N faults over 30 days |
The warehouse is partitioned into 20 named zones:
inbound_dock_a, inbound_dock_b, receiving_staging, aisle_a, aisle_b, aisle_c, high_bay_1, high_bay_2, bulk_storage, pick_zone_1, pick_zone_2, pack_station, sort_belt, outbound_dock_a, outbound_dock_b, shipping_staging, charging_bay_1, charging_bay_2, charging_bay_3, maintenance_bay
Zone entry counts are buffered in Redis (INCR) on ingest and flushed to Postgres in batches by a Celery beat task every few seconds (see settings.CELERY_BEAT_SCHEDULE).
Run make help for a full list. Key targets:
# Install
make install # all deps (backend + frontend)
# Backend
make be-dev # Django dev server on :8000
make be-migrate # apply migrations + seed
make be-makemigrations # generate new migrations
make be-lint # ruff check
make be-format # ruff format
make be-typecheck # mypy
# Frontend
make fe-dev # Vite dev server on :5173
make fe-lint # ESLint
make fe-typecheck # tsc --noEmit
make fe-build # production build
# Docker
make docker-up # start Postgres + Redis
make docker-down # stop containers
make docker-down-v # stop + delete volumes (destructive)
make docker-logs # tail all container logs
# Full CI suite
make ci # equivalent to what GitHub Actions runs# All tests
make test
# Backend only (pytest)
make be-test
# Backend with HTML coverage report
make be-test-cov # opens backend/htmlcov/index.html
# Frontend only (Vitest)
make fe-test
# Frontend with coverage
make fe-test-covCoverage gate: 80 % — the build fails if coverage drops below this threshold.
GitHub Actions runs on every push to main and on all pull requests (.github/workflows/ci.yml):
Backend job — ruff lint → mypy type-check → pytest with coverage
Frontend job — ESLint → tsc type-check → Vitest with coverage → Vite production build
Both jobs upload coverage artifacts. CI is cancelled automatically if a newer commit arrives on the same branch.
| Variable | Default | Description |
|---|---|---|
DJANGO_SECRET_KEY |
— | Django secret key (required) |
DJANGO_DEBUG |
True |
Debug mode |
DJANGO_ALLOWED_HOSTS |
localhost,127.0.0.1,backend |
Allowed hosts |
DB_NAME |
fleet_telemetry |
Postgres database name |
DB_USER |
postgres |
Postgres user |
DB_PASSWORD |
postgres |
Postgres password |
DB_HOST |
localhost |
Postgres host |
DB_PORT |
5433 |
Postgres port (host-side) |
REDIS_URL |
redis://localhost:6379/0 |
Redis URL (required) — zone counter buffer and Celery broker |
CELERY_TASK_ALWAYS_EAGER |
true |
Run Celery tasks inline (disable to use a separate worker) |
| Variable | Default | Description |
|---|---|---|
VITE_API_URL |
(unset) | Backend API origin; leave unset in local dev to use the Vite proxy |
docs/ADR.md— Architecture Decision Record.docs/AI_INTERACTION_LOG.md— AI Interaction Log.