Task 8

Frontend

frontend/ running on the physical cluster

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 /api paths) — no CORS configuration needed, Traefik routes /api to 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

DocumentWhat 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