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¶
- Backend stores image in MinIO with key like
2026/07/13/143022_cam01.jpg - Backend stores the key in PostgreSQL alongside event metadata
- Frontend reads the event and constructs URL:
/evidence/2026/07/13/143022_cam01.jpg - Browser requests this URL from Traefik
- Traefik routes
/evidence/*to MinIO - MinIO reconstructs the image from erasure-coded blocks across workers
- Image is displayed in the Event Detail page
Theming (Dark/Light Mode)¶
- How it works: A
ThemeProvidercontext wraps the entire app. Clicking the toggle adds/removes thedarkclass on<html>. Tailwind'sdark:variant handles all color changes - Persistence: Theme preference is saved in
localStorageso it survives page refreshes - System detection: On first visit, the app checks
prefers-color-schememedia 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.htmlfor 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.htmlis never cached, so updates are immediate after a deploy