- HTML 58.4%
- JavaScript 21%
- Python 15.3%
- CSS 4.3%
- Shell 0.7%
- Other 0.3%
| backup | ||
| db/init | ||
| graphify-out | ||
| ingestion_worker | ||
| proxy | ||
| web_app | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| AGENTS.md | ||
| docker-compose.yml | ||
| README.md | ||
OIEP LPR Platform — Deployment Guide
Version: 3.0 (UI Redesign & Feature Expansion) Date: June 8, 2026 Target OS: Linux (Ubuntu/Debian recommended)
This guide provides instructions for deploying the Open-Source IoT Event Platform (OIEP) — a real-time license plate recognition monitoring dashboard with analytics, watchlists, and data export.
Overview
The OIEP Platform ingests LPR (License Plate Recognition) events from Frigate via MQTT, stores them in TimescaleDB, and provides a rich web interface for searching, monitoring, and analyzing vehicle detection data.
What's New in v3.0
- Complete UI redesign — dark monitoring theme with collapsible sidebar navigation
- 6 application pages: Search, Dashboard, Live Feed, Watchlist, Plate Details, Config
- 19 API endpoints — full REST API with pagination, filtering, and caching
- Real-time SSE feed — live event stream using Server-Sent Events
- Watchlist with CRUD — persistent watchlist backed by database (survives restarts)
- Interactive charts — confidence distribution, weekly trends, sparklines (Chart.js)
- Data export — CSV and JSON export of search results
- Plate heatmap — 7-day x 24-hour activity grid per plate
- Flask-Caching — response caching for dashboard, search, and live endpoints
- Bootstrap 5.3.3 + Bootstrap Icons — modern responsive frontend
- 7 modular JS files — search, live-feed, plate-detail, watchlist, export, common, charts
Screenshots
| Dashboard | Live Feed | Search |
|---|---|---|
![]() |
![]() |
![]() |
| Watchlist | Plate Details | Config |
|---|---|---|
![]() |
![]() |
![]() |
(Screenshot files are placeholders — add actual screenshots to docs/screenshots/ directory.)
1. Prerequisites
Ensure the deployment server meets the following requirements:
- Operating System: Linux (e.g. Ubuntu 22.04 LTS)
- Docker Engine: v24.0+
- Docker Compose: v2.0+
- Git: Installed
- MQTT Broker: Have the IP + credentials ready (Frigate embedded broker or Mosquitto)
2. Deployment Steps
Step 2.1 — Clone the Repository
cd ~
git clone <YOUR_FORGEJO_REPO_URL> lpr-db-app
cd lpr-db-app
Replace <YOUR_FORGEJO_REPO_URL> with your actual clone URL.
Step 2.2 — Configure Environment Variables
Copy the example environment file and edit it:
cp .env.example .env
nano .env
Update the values in .env:
# Database
DB_POSTGRES_USER=oiep_user
DB_POSTGRES_PASSWORD=CHANGE_ME # <-- CHANGE THIS
DB_POSTGRES_DB=oiep_db
# MQTT Broker
MQTT_BROKER_HOST=10.10.30.253 # <-- CHANGE THIS (Frigate IP)
MQTT_BROKER_PORT=1883
MQTT_USERNAME=frigate_user # <-- CHANGE THIS
MQTT_PASSWORD=CHANGE_ME # <-- CHANGE THIS
# Timezone
TZ=America/Winnipeg
Save & exit: Ctrl+O, Enter, Ctrl+X.
Security Note: The .env file is gitignored and should never be committed to version control.
Step 2.3 — Build and Launch the Stack
docker compose up -d --build
--build→ forces rebuild-d→ detached mode
3. Verification
Step 3.1 — Ensure Containers Are Running
docker ps
Expected:
db→ healthyingestion_worker,web_app,proxy→ Up (healthy)
Step 3.2 — Check Health Status
docker inspect --format='{{.State.Health.Status}}' lpr-db-app-db-1
docker inspect --format='{{.State.Health.Status}}' lpr-db-app-web_app-1
Both should report healthy.
Step 3.3 — Confirm Ingestion Worker Connected to MQTT
docker logs -f lpr-db-app-ingestion_worker-1
Expected output:
2026-06-07T12:00:00+0000 [INFO] Connected to MQTT Broker (Code: 0)
2026-06-07T12:00:00+0000 [INFO] Configuration loaded/updated. Refreshing subscriptions...
Press Ctrl+C to exit.
Step 3.4 — Access the Web Interface
Navigate to:
http://<YOUR_SERVER_IP>/
You should see the OIEP Platform interface with the dark monitoring theme and sidebar navigation.
4. Configuration & Maintenance
Dynamic Configuration (No Restart Required)
Visit:
http://<YOUR_SERVER_IP>/config
You can update:
- MQTT topic
- JSON validation schema
Click Update Configuration — the ingestion worker reloads automatically within ~10 seconds.
Viewing Logs
docker logs -f lpr-db-app-ingestion_worker-1
docker logs -f lpr-db-app-web_app-1
docker logs -f lpr-db-app-db-1
Logs use structured format: YYYY-MM-DDTHH:MM:SS [LEVEL] Message
The web app logs each request: METHOD /path STATUS RESPONSE_TIME
Searching Plates
The search page supports:
- Wildcard patterns: Use
%as a wildcard (e.g.,%885%matches any plate containing "885") - Date range filters: Start and end date pickers
- Camera filter: Filter by specific camera name
- Confidence threshold: Minimum confidence level
- Sorting: By time (asc/desc) or confidence (desc)
- Pagination: Results are paginated (100 per page, max 500) with Previous/Next controls
Live Feed
The Live Feed page connects to /api/live/stream via Server-Sent Events (SSE). It polls the database every 3 seconds for new events and renders them in real time with automatic scrolling.
Watchlist
The Watchlist page provides full CRUD operations:
- Add: Enter a plate string and optional note
- Edit: Click the edit icon to update the note
- Delete: Click the delete icon to remove an entry
- Status: Shows
last_seentimestamp andtotal_hitscount - Persistence: Backed by the
plate_watchlistdatabase table — survives restarts and Gunicorn worker scaling
Plate Details
Click any plate to view its detail page:
- Total detection count and first/last seen timestamps
- Per-camera breakdown
- Average confidence score
- 7-day x 24-hour heatmap grid (day-of-week by hour)
Viewing Ingestion Errors
If messages fail JSON parsing or schema validation, they are logged to the ingestion_errors table. Check via the dashboard or directly:
docker exec lpr-db-app-db-1 psql -U oiep_user -d oiep_db -c "SELECT * FROM ingestion_errors ORDER BY error_time DESC LIMIT 10;"
Database Retention & Compression
The database automatically manages data lifecycle:
- Retention: Data older than 1 year is automatically dropped
- Compression: Data older than 30 days is automatically compressed to save space
These policies are applied during initial setup and require no manual intervention.
Dashboard Performance
The dashboard uses TimescaleDB continuous aggregates (materialized views) for fast data loading:
- Volume cache: Refreshed every 5 minutes
- Trend cache: Refreshed every 5 minutes
- Weekly cache: Refreshed every 1 hour
All frontend API calls are fetched in parallel for instant page loads. Flask-Caching adds in-memory response caching (30-60s TTL) on top of database aggregates.
Stopping the Stack
docker compose down
Persistent data stays in the oiep_pgdata volume.
⚠️ Factory Reset (Deletes ALL Database Data)
docker compose down -v
5. API Reference
The platform exposes a REST API for all data operations. A browsable API documentation page is available at /api/docs.
Quick Reference
| Method | Endpoint | Description |
|---|---|---|
GET |
/health |
Health check |
POST |
/api/config |
Update MQTT/config settings |
GET |
/api/search/plates |
Search plates with filters + pagination |
GET |
/api/stats/plate_frequency |
Daily/weekly/monthly frequency for a plate |
GET |
/api/dashboard/volume |
Today/week/month counts + unique plates |
GET |
/api/dashboard/trend |
Hourly trend (24h) |
GET |
/api/dashboard/top_plates |
Top N plates (7d) |
GET |
/api/dashboard/quality |
Avg confidence, cameras, errors |
GET |
/api/dashboard/confidence_distribution |
Confidence histogram |
GET |
/api/dashboard/weekly_trend |
7-day per-camera trend |
GET |
/api/live/events |
Recent live events |
GET |
/api/live/stream |
SSE real-time event stream |
GET |
/api/plates/<plate> |
Plate summary (count, cameras, confidence) |
GET |
/api/plates/<plate>/heatmap |
7-day x 24h heatmap grid |
GET |
/api/watchlist |
Get all watchlist items |
POST |
/api/watchlist |
Add plate to watchlist |
PATCH |
/api/watchlist/<plate> |
Update watchlist entry |
DELETE |
/api/watchlist/<plate> |
Remove plate from watchlist |
GET |
/api/export/search |
Export search results (CSV or JSON) |
Caching
Response caching is enabled via Flask-Caching (SimpleCache):
- 30s TTL: Search, live events, plate details, heatmap, watchlist
- 60s TTL: Dashboard endpoints (volume, trend, top plates, quality, confidence distribution, weekly trend)
6. Architecture
Data Flow
┌─────────────┐ MQTT ┌──────────────────┐ PostgreSQL ┌─────────────┐
│ Frigate │ ──────────► │ Ingestion │ ────────────────► │ TimescaleDB │
│ (LPR) │ │ Worker │ │ │
└─────────────┘ └──────────────────┘ └──────┬──────┘
│ │
│ Deduplication │ Queries
│ Manitoba Plate Logic │ (connection pooled)
▼ │
alpr/filtered ◄───────────────────────────────┘
Stack Overview
Client ──► Nginx (proxy:80) ──► Flask + Gunicorn (web_app:8000)
│
├── psycopg2 connection pool (4 base + 8 overflow)
├── Flask-Caching (SimpleCache, 30-60s TTL)
├── TimescaleDB continuous aggregates
└── 7 JS modules (search, live-feed, plate-detail, watchlist, export, common, charts)
Client ──► Nginx ──► Ingestion Worker (MQTT consumer)
│
├── paho-mqtt
├── JSON validation
├── Manitoba plate filtering
└── TimescaleDB writes
Services
| Service | Image | Port | Memory Limit | Description |
|---|---|---|---|---|
| db | timescale/timescaledb:2.23.1-pg16 | 5432 (internal) | 1G | TimescaleDB with pg_trgm + continuous aggregates |
| ingestion_worker | Custom | N/A | 512M | MQTT consumer, plate processing, DB writer |
| web_app | Custom (Flask+Gunicorn+Caching) | 8000 (internal) | 256M | REST API + web interface (connection pooled, cached) |
| proxy | nginx:1.27-alpine | 80, 443 | 64M | Reverse proxy |
Dependencies
| Layer | Key Packages |
|---|---|
| Web App | Flask, Flask-Caching, Gunicorn, psycopg2 |
| Ingestion | paho-mqtt, psycopg2 |
| Frontend | Bootstrap 5.3.3, Bootstrap Icons 1.11.3, Chart.js |
| Database | TimescaleDB 2.23.1 (pg16), pg_trgm |
Security Features
- All services run as non-root users in Docker containers
.envfile excluded from version control- Resource limits prevent container resource exhaustion
- XSS protection on all user-facing pages
- Connection pooling limits DB connections
✔️ Deployment Successful
Your OIEP platform v3.0 should now be fully operational. Visit http://<YOUR_SERVER_IP>/ to access the dashboard.





