Frontend
A React/Vite web UI presenting a live, MQTT-pushed event log with evidence images — with a REST fallback that makes the live layer optional rather than load-bearing.
What was asked
Develop a frontend to present log information, event messages, and a map of events, using Vue.js or React, deployed as Docker containers on the k3s cluster, communicating via REST or MQTT.
What's built
- React/Vite single-page app, built in a two-stage Docker image (Node build stage → nginx serve stage, so the final image ships no Node.js or source).
- Event list view showing type, source, message, timestamp, detections, and the evidence image for each event.
- Live updates: on load, events are fetched over REST; new events then arrive instantly via MQTT-over-WebSocket, with a visible Live/Polling badge in the header.
- Automatic fallback: if the MQTT socket drops, the UI silently degrades to 5-second REST polling rather than going stale.
- Same-origin API calls (relative
/apipaths) — no CORS configuration needed, Traefik routes/apito the backend and everything else to the SPA.
Communication
The frontend used to poll GET /api/events every 3 seconds. It now loads once
over REST on startup, subscribes to the events/new MQTT topic via Mosquitto
(reached same-origin through the ingress at /mqtt, a Traefik
stripPrefix rule drops the prefix so the broker sees a plain WebSocket
connection), and only falls back to a 5-second REST poll if that socket goes down. REST
stays the source of truth throughout — MQTT is a best-effort speed layer on top of it, not a
replacement.
Why the fallback matters
Mosquitto runs as a single pod — it has no clustering mode, so a second replica would not be a second broker. That is a genuine single point of failure, and the REST fallback is what keeps it from being a user-visible one: if the broker dies, live push stops and the UI degrades to a 5-second poll. Events published during the outage are lost for the live layer, but nothing is lost from the system — the backend has already written them to PostgreSQL and MinIO before publishing, so the next poll picks them up.
The Live / Polling badge in the header exists so this degradation is visible rather than silent. A UI that quietly stops updating looks identical to a system where nothing is happening.
Deploy
On the Pi cluster the image is already built and pre-pulled onto the workers, so deploy without building — the Makefile refuses building targets against a non-local cluster:
cd cluster
make deploy-frontend # frontend Deployment (2 replicas) + ingress
make status # confirm both pods are Ready
The frontend is stateless, so it uses topologySpreadConstraints with
ScheduleAnyway — it prefers to spread across nodes but will never sit
Pending to achieve it.
Deeper reading
| Document | What it covers |
|---|---|
| cluster/docs/step-3-frontend.md | the frontend build step, ingress path precedence and the two-stage image |
| cluster/docs/step-6-mqtt.md | the MQTT broker, the WebSocket route through Traefik, and the fallback behaviour |