Skip to content

Task 8 - Frontend Dashboard on k3s

What We Built

A React single-page application that serves as the central monitoring dashboard. It shows live threat events with evidence images, cluster health metrics, node topology, performance benchmarks, and a live camera feed. Deployed as a Docker container (nginx) on the k3s cluster.

Why These Technology Choices?

Why React (and not Vue.js, Svelte, or server-rendered templates)?

  • Component model - A monitoring dashboard is made of independent widgets (event list, cluster map, metric charts). React's component architecture maps directly to this
  • Recharts library - Mature charting library for React that supports stacked bars, line charts, and custom tooltips. The professor-style runtime breakdown charts and speedup comparisons use Recharts
  • Team experience - The team had prior React experience, reducing ramp-up time
  • Ecosystem - Libraries for maps (react-leaflet), animations (framer-motion), and accessible UI components (Radix UI) are all React-first

Why Vite (and not Create React App or Webpack)?

  • Sub-second hot reload - During development, changes appear in the browser almost instantly
  • Fast production builds - Full build completes in under 1 second, producing optimized bundles with content hashing for cache busting
  • Modern defaults - ES modules, TypeScript support, and tree shaking out of the box

Why Tailwind CSS (and not plain CSS, Bootstrap, or Material UI)?

  • Dark/light mode - Tailwind's class-based dark: variant makes theme switching trivial. CSS variables define all colors, and a single class toggle on <html> switches everything
  • Consistent spacing - Utility classes prevent inconsistent margins and padding across pages
  • Small bundle - Only the classes actually used are included in production

Why Grafana integration (embedded via the React dashboard)?

  • Pre-built dashboards - Grafana has hundreds of community dashboards for node_exporter metrics. We customized the "Cluster Overview" dashboard instead of building metric charts from scratch
  • PromQL support - Grafana natively queries Prometheus using PromQL. Building our own query engine in React would have been redundant
  • Easy to embed - Grafana panels are embeddable via iframes. The Monitoring page loads Grafana panels directly, with anonymous access enabled
  • Still needed React for everything else - Grafana cannot display detection events, evidence images, cluster topology, or benchmark results. React handles all non-metric pages

Architecture

graph LR
    browser["Browser"] --> traefik["Traefik Ingress\n192.168.137.10:80"]
    traefik -->|"/"| frontend["React SPA\nnginx"]
    traefik -->|"/api"| backend["Flask API"]
    traefik -->|"/evidence"| minio["MinIO S3"]
    traefik -->|"/prom"| prom["Prometheus"]
    traefik -->|"/cluster"| topo["Cluster Server"]
    traefik -->|"/camera"| sensor["Pi 4 MJPEG"]

Single origin design: All traffic flows through http://192.168.137.10/. Traefik routes by path prefix. No CORS configuration needed.

Dashboard Pages

Page Route What it shows Data source
Dashboard / Latest events, threat breakdown, cluster health, camera preview Backend API
Events /events Paginated event list, filter by threat level and status Backend API
Event Detail /events/:id Evidence image from MinIO, bounding boxes, triage buttons Backend + MinIO
Monitoring /nodes CPU, memory, disk, network charts via embedded Grafana Prometheus
Cluster /cluster k3s node topology, readiness status, workload health Cluster Server
Performance /benchmark HPL LINPACK GFLOPS results Bench Server
MPI Clustering /mpi OSU micro-benchmarks, latency/bandwidth analysis Static + API
Non MPI Scaling /scaling Amdahl/Gustafson charts, stacked runtime, raw runs Task 4 API
Logs /logs System event log viewer Backend API

How the Frontend Communicates with the Backend

Polling Model (not WebSockets)

The dashboard uses a custom usePolledAsync hook that fetches data every 5-10 seconds:

  • Why polling? The event rate is low (a few per minute). Polling is simpler than WebSockets and works through any reverse proxy without special configuration
  • How? Each page component calls its data-fetching function on mount and then every N seconds. When data changes, React re-renders only the affected components

API Endpoints Used by the Frontend

What the user does API call What happens
Opens the Events page GET /api/events?limit=50 Fetches the 50 most recent events
Clicks an event GET /api/events/<id> Loads full detail + evidence image URL
Clicks "Acknowledge" POST /api/events/<id>/status Changes status to acknowledged
Clicks "Delete" DELETE /api/events/<id> Removes event from PostgreSQL + image from MinIO
Toggles detection on/off POST /api/detection Sets cluster-wide flag in PostgreSQL
What the user sees API call Data source
Store health badges GET /api/stores Backend checks PostgreSQL + MinIO connectivity
CPU/memory charts GET /prom/api/v1/query_range Prometheus (via Traefik)
Node topology GET /cluster/topology Cluster Server (kubectl get nodes)
Live camera <img src="/camera/stream.mjpg"> Sensor Pi 4 (via Traefik)

How Evidence Images Are Served

  1. Backend stores image in MinIO with key like 2026/07/13/143022_cam01.jpg
  2. Backend stores the key in PostgreSQL alongside event metadata
  3. Frontend reads the event and constructs URL: /evidence/2026/07/13/143022_cam01.jpg
  4. Browser requests this URL from Traefik
  5. Traefik routes /evidence/* to MinIO
  6. MinIO reconstructs the image from erasure-coded blocks across workers
  7. Image is displayed in the Event Detail page

Theming (Dark/Light Mode)

  • How it works: A ThemeProvider context wraps the entire app. Clicking the toggle adds/removes the dark class on <html>. Tailwind's dark: variant handles all color changes
  • Persistence: Theme preference is saved in localStorage so it survives page refreshes
  • System detection: On first visit, the app checks prefers-color-scheme media query

Deployment

Build and Deploy

# 1. Build the production bundle (under 1 second)
cd frontend && npm run build

# 2. Copy to master node
scp -r dist/* master@192.168.137.10:~/frontend-deploy/dist/

# 3. Build Docker image and deploy
ssh master@192.168.137.10 '
  cd ~/frontend-deploy
  docker build --no-cache -t 192.168.137.10:5000/ss2026-frontend:latest .
  docker push 192.168.137.10:5000/ss2026-frontend:latest
  KUBECONFIG=~/.kube/config kubectl -n sentinel rollout restart deployment/frontend
'

Why nginx (and not Node.js or direct Vite)?

  • Static file serving - The React build produces static HTML/JS/CSS files. nginx serves them at near-zero CPU cost on the Pi 5
  • SPA fallback - nginx is configured to serve index.html for any route that does not match a file, which is required for client-side routing
  • Cache headers - Hashed asset filenames get 30-day cache headers. index.html is never cached, so updates are immediate after a deploy