Skip to content

Changelog — v1 → v3 (Backend & Frontend Upgrades)

This document records all changes made to the Threat Detection System codebase between the initial v1 baseline and the current v3 state. Changes are grouped by component.


1. Backend — backend/app/

1.1 Configuration (config.py)

Change v1 v3
Camera MQTT topic (not defined) MQTT_TOPIC_CAMERA = "cluster/camera/#"
DB connect timeout (not defined, caused AttributeError on startup) DATABASE_CONNECT_TIMEOUT_SECONDS = 30
DB command timeout (not defined) DATABASE_COMMAND_TIMEOUT_SECONDS = 30

1.2 Application Startup (main.py)

Added auto-migration — Alembic upgrade head now runs automatically at startup before any service initialises.
This fixes the relation "sensor_nodes" does not exist error on a fresh database.

# v3 lifespan — runs before MinIO / MQTT setup
from alembic.config import Config as AlembicConfig
from alembic import command as alembic_command
alembic_cfg = AlembicConfig("alembic.ini")
alembic_command.upgrade(alembic_cfg, "head")

1.3 Detection Service (services/detection_service.py)

Issue v1 v3
metadata column not saved "metadata": data.metadata silently wrote to SQLAlchemy's MetaData class object Fixed to "metadata_": data.metadata (correct Python attribute name)
No logger detection_service had no logging import Added import logging + logger = logging.getLogger(__name__)
Image errors invisible except Exception: pass swallowed all MinIO errors Changed to logger.warning("Image upload skipped for event %s: %s", ...)
Presigned URLs inaccessible from browser Returned http://minio:9000/... (K8s internal DNS, unreachable) Returns /api/v1/images/{id}/download — routes through FastAPI Ingress, works everywhere

1.4 Detection Schema (schemas/detection_event.py)

Change v1 v3
event_type field type str = Field(..., max_length=50) — no validation, anything accepted EventType enum — Swagger shows dropdown: person / theft / fire / vandalism / weapon / unknown
metadata response field Field(None, alias="metadata_") — read SQLAlchemy MetaData class object instead of column value Field(None, validation_alias=AliasChoices("metadata_", "metadata")) — reads metadata_ column correctly

1.5 Detections Router (api/v1/detections.py)

Change v1 v3
event_type query param str \| None — free text, no hint in Swagger EventType \| None — Swagger shows dropdown for filtering

1.6 Detection Event Repository (repositories/detection_event_repo.py)

Method v1 sort key v3 sort key Reason
get_recent detected_at DESC received_at DESC detected_at is the edge node's clock (can be wrong); received_at is always the server's UTC time
get_filtered detected_at DESC received_at DESC Same

1.7 MQTT Service (services/mqtt_service.py)

_handle_camera_event — all Pi4 payload fields now used:

Field v1 (hardcoded) v3 (from payload)
severity always "medium" payload.get("severity") — e.g. "high" for weapon, "critical" for fire
confidence always 1.0 payload.get("confidence") — actual YOLO score e.g. 0.87
raw_detections {"objects": N, "label": "person"} Full YOLO output wrapped: {"detections": [...], "count": N}
metadata {"mqtt_topic":..., "source":...} only Merged Pi4 edge data (camera_backend, frame_width, frame_height, yolo_class) + backend tracking fields

_handle_detection (sensor topics) — input normalisation added:

# v3 — prevents Pydantic ValidationError from unknown YOLO class names
raw_type = payload.get("event_type", "unknown")
event_type = raw_type if raw_type in _valid_types else "unknown"

raw_severity = payload.get("severity", "medium")
severity = raw_severity if raw_severity in _valid_severities else "medium"

_LABEL_MAP expanded with YOLO COCO class → EventType mappings:

"knife"        → "weapon"
"scissors"     → "weapon"
"baseball bat" → "weapon"
"gun"          → "weapon"
"pistol"       → "weapon"
"rifle"        → "weapon"
"smoke"        → "fire"

Duplicate MQTT subscription removed:
cluster/camera/stream was subscribed twice (once explicitly + once via cluster/camera/#). Explicit subscription removed.

EventType added to imports — used for normalising event types in both MQTT handlers.


2. Frontend — frontend/src/

2.1 Authentication (App.tsx)

Added full login gate — the dashboard is replaced by a login screen when no JWT is stored.

Feature Detail
Login screen Username + password → POST /api/v1/auth/login → JWT stored in localStorage
Register tab Username, email, password, role (viewer / operator / admin)
Auto token injection On login the token is saved and injected into every API call automatically
Sign out button Top-right of header — clears JWT and returns to login screen
Persistent session Survives page refresh via localStorage

2.2 Image URL Resolution (App.tsx)

Added buildImageUrl helper — converts the relative download path from the API into an absolute URL using settings.apiBaseUrl:

// Works in K3s:  http://192.168.1.50/api/v1/images/{id}/download
// Works locally: http://localhost:8001/api/v1/images/{id}/download
function buildImageUrl(previewUrl, apiBaseUrl) { ... }

Image loading simplified — the frontend no longer calls /images/{id}/url (which returned an inaccessible MinIO presigned URL). All image loads now use /api/v1/images/{id}/download directly.

2.3 Other Fixes (App.tsx)

Change v1 v3
Manual Bearer token field Shown in Settings panel, user had to paste JWT manually Removed — token is managed automatically by the login flow
saveConnectionSettings Overwrote the auth token when saving API URL Fixed to preserve the token
Swagger link Not present Added "Open Swagger API" button in the hero header
Detection list event URL Fetched /images/{id}/url → unusable MinIO URL Always uses download endpoint; buildImageUrl converts relative → absolute

3. Edge Node — edge_node/edge_camera_publisher.py

Setting v1 v3
Frame resolution 320 × 240 640 × 480 (4× pixel count)
JPEG quality 70% 85%
rpicam-still warmup -t 1000 (1 s per frame → ~1 real FPS) -t 200 (200 ms → ~4–5 real FPS)

Added YOLO → EventType mapping at the edge (so the database never receives raw COCO class names):

_YOLO_TO_EVENT_TYPE = {
    "person": "person",
    "knife": "weapon",   "scissors": "weapon",   "baseball bat": "weapon",
    "gun":   "weapon",   "pistol":   "weapon",   "rifle":        "weapon",
    "fire":  "fire",     "smoke":    "fire",
    # everything else → "unknown"
}
_EVENT_TYPE_SEVERITY = {
    "person": "low", "weapon": "high", "fire": "critical",
    "theft": "high", "vandalism": "medium", "unknown": "medium",
}

Detection event payload now includes: - event_type — mapped EventType value (not raw YOLO class) - label — same mapped value - severity — from _EVENT_TYPE_SEVERITY lookup - confidence — YOLO's actual score - raw_detections — full list of all detected objects with bboxes - image_base64 — annotated frame JPEG - metadata.yolo_class — original YOLO class (for debugging)


4. Kubernetes Manifests — k8s/

File Change Reason
backend.yaml Added MQTT_TOPIC_CAMERA: "cluster/camera/#" env var Config was missing; backend couldn't subscribe to camera events
frontend.yaml imagePullPolicy: NeverIfNotPresent Never caused CreateContainerError when the image wasn't pre-loaded into every worker

5. Camera Stream HTML — camera_stream.html

Change v1 v3
Broker IP 10.42.0.43 (K8s pod IP — changes every restart) 192.168.1.50 (stable Pi5 master IP)
Image fit object-fit: contain (black bars around image) object-fit: cover (fills the frame edge-to-edge)

6. Environment / .env.example

Variables added that were missing and caused startup errors:

# Was missing — caused AttributeError in database.py
DATABASE_CONNECT_TIMEOUT_SECONDS=30
DATABASE_COMMAND_TIMEOUT_SECONDS=30

# Was missing — backend couldn't subscribe to Pi4 camera topic
MQTT_TOPIC_CAMERA=cluster/camera/#

# Was empty — JWT auth failed silently on local dev
JWT_SECRET_KEY=dev-secret-change-in-production

7. Summary: Root Causes Fixed

# Symptom Root Cause Fix
1 "sensor_nodes" does not exist on startup Alembic migrations never ran automatically Auto-run alembic upgrade head in lifespan
2 metadata always null in API response Wrong dict key "metadata" instead of "metadata_" in event_repo.create() Fixed key; added AliasChoices in response schema
3 Images not showing in browser Presigned URL used K8s-internal minio:9000 DNS name Return /api/v1/images/{id}/download path; proxy through FastAPI
4 Only "person" type detections saved All non-EventType YOLO class names failed Pydantic validation silently Normalise at edge before publishing; expand _LABEL_MAP in backend
5 Latest events not at top of list Sorted by detected_at (edge node clock, can be wrong) Sort by received_at (server clock, always correct)
6 Camera preview not loading HTML had hardcoded pod IP 10.42.0.43 Changed to stable Pi5 IP 192.168.1.50
7 severity always "medium", confidence always 1.0 _handle_camera_event hardcoded both instead of reading from payload Read payload.get("severity") and payload.get("confidence")
8 Full YOLO detection list lost raw_detections in schema was dict | None; YOLO sends a list Wrap list in {"detections": [...], "count": N} before validation
9 No login protection No auth gate on the frontend Added LoginPage component with JWT login/register
10 AttributeError: settings has no attribute DATABASE_CONNECT_TIMEOUT_SECONDS Settings were missing from config.py Added all missing settings with sensible defaults