EdgeGuard โ a React single-page application with 7 pages, dark/light mode, interactive benchmark charts, and live cluster monitoring. Deployed as 2 highly-available k3s replicas.
The EdgeGuard frontend is a React Single Page Application (SPA) โ a web app that loads once and handles all page navigation client-side without full page reloads. It communicates exclusively with the Flask backend REST API and never touches the database or MinIO directly.
The application has 7 pages covering every aspect of the EdgeGuard system โ live camera feed, security alerts, cluster health, auto-scaling, pod distribution, benchmark results, and user management. All pages support both dark and light mode with a toggle persisted in localStorage.
The built React app is served by nginx running inside a Docker container, deployed as 2 k3s replicas with pod anti-affinity โ same high-availability approach as the backend. Users access the app via a k3s NodePort service.
A 7-page application with live data, interactive charts, pagination, and real-time updates would be extremely complex to maintain in plain HTML/JS. React's component model lets each page be built independently and reused โ the node health card component, for example, is written once and used in both the Monitoring page and the Dashboard overview. State management and automatic re-rendering when data changes are built in.
Every page in EdgeGuard needs data from the backend. The pattern is always the same: React component mounts โ calls backend API with JWT token โ receives JSON โ renders data. Live pages (Camera, Monitoring) poll the API every few seconds to keep data fresh.
// Standard API call pattern โ used on every page const fetchData = async () => { const token = localStorage.getItem('token'); const response = await fetch(`${API_BASE}/api/alerts?page=${page}`, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); if (response.status === 401) { // Token expired โ redirect to login localStorage.removeItem('token'); navigate('/login'); return; } const data = await response.json(); setAlerts(data.alerts); setTotalPages(data.total_pages); }; // Auto-refresh every 10 seconds for live pages useEffect(() => { fetchData(); const interval = setInterval(fetchData, 10000); return () => clearInterval(interval); // cleanup on unmount }, [page]);
Displays a real-time video feed from the Raspberry Pi 4 AI Camera with AI detection overlays. Each detected person has a bounding box showing their classification (face_visible in green, face_covered in red) and the confidence score percentage.
The backend proxies the ZMQ video stream from Pi4 as an MJPEG HTTP stream at
/api/stream.
The React frontend renders this as a standard HTML <img> tag
pointing to the stream endpoint โ the browser handles MJPEG rendering natively.
Detection bounding boxes are drawn on a transparent canvas overlay positioned
exactly on top of the video element. The frontend polls
/api/latest-detections
every 100ms to get current bounding box coordinates and labels.
Paginated history of all suspicious events detected by the AI. Each alert shows the event type, confidence score, timestamp, camera ID, and the snapshot image fetched from MinIO via a presigned URL. Alerts are numbered with a per-alert ID for easy reference during incident review.
The frontend calls /api/alerts?page=<n>&limit=10
on load and on every page change. The backend returns a paginated JSON response
including presigned MinIO image URLs. The frontend renders each alert as a card
with the snapshot image, confidence badge, and timestamp.
Navigation uses First / Previous / Next / Last buttons
with the current page and total shown. Individual alerts can be deleted โ
this calls DELETE /api/alerts/<id> which removes both
the database record and the MinIO snapshot.
Live health grid showing all 10 cluster nodes with real-time CPU usage, RAM usage, uptime, and online/offline status. Service health panel shows whether the Detection Service, Backend API, Prometheus, and Grafana are running. Direct link to the Grafana dashboard for deeper time-series inspection.
Polls /api/system/status
every 10 seconds. The backend queries Prometheus for each node's current CPU and RAM,
checks service pod status via the Kubernetes API, and returns a unified JSON response.
Each node is rendered as a card with a color indicator โ green (healthy),
yellow (high load), red (offline). CPU and RAM are shown as percentage bars.
If a node does not respond to Prometheus scrapes, it shows as offline immediately.
Shows the current cluster CPU average and the auto-scaling status. When cluster CPU exceeds a configurable threshold, the Pi4 sensor-node automatically joins the k3s cluster as an emergency compute node, adding 4 more cores. When CPU drops below the threshold, it gracefully leaves.
The backend reads cluster-wide CPU average from Prometheus every 10 seconds.
When the threshold is exceeded, it executes a kubectl command
to add pi4 as a k3s worker node. The frontend polls
/api/autoscaler/status
and shows the current CPU gauge, threshold setting (adjustable from the UI),
and the Pi4 join/leave history log. The threshold can be changed live
from the frontend โ the new value is sent to the backend via a POST request
and takes effect on the next polling cycle.
Visual overview of how Kubernetes pods are distributed across all cluster nodes. Shows which pods are running on which Pi3 nodes โ useful for verifying that anti-affinity rules are working correctly (e.g. confirming backend Replica 1 and Replica 2 are on different nodes).
Calls /api/pods
which queries the Kubernetes API server for all running pods and their node assignments.
The frontend renders a grid โ each column is a node, each row is a pod.
Pods are color-coded by namespace or application type.
This page makes it visually obvious when a node is overloaded with pods
or when the anti-affinity rules have placed replicas correctly on separate nodes.
Interactive benchmark results for all 4 tasks โ Monte Carlo Pi, Matrix Multiplication, HPL, and Task Distributor. Features speedup charts, runtime bars, hover tooltips with exact values, and an MPI Based / Non-MPI toggle to filter between MPI benchmarks (Tasks 2 and 3) and the Task Distributor (Task 4).
Calls /api/benchmarks
on load to fetch all benchmark data from PostgreSQL. Data is processed client-side
to compute speedup (time at 1 core / time at N cores), efficiency, and min/max/mean.
Charts are built with custom SVG โ bar charts for runtime, line charts for speedup
vs ideal. Each bar has an interactive tooltip on hover showing exact values.
The MPI/Non-MPI toggle filters which benchmark cards are shown โ
Monte Carlo Pi and Matrix Multiply appear under MPI,
Task Distributor appears under Non-MPI. HPL (GFLOPS) is shown separately
as it measures a different metric.
โ Color coding: green (>85% efficiency), yellow (50โ85%), red (<50% / parallelization limit reached)
โ Ideal speedup line overlaid on actual speedup bars
โ Problem size selector tabs (0.1M, 1M, 10M, 100M, 1B, 10B for Monte Carlo)
โ N size selector for Matrix Multiply (N=1000 to N=4000)
โ Min/Max error bars showing run variance
โ Values displayed on each bar (Prof. Baun's format)
Admin page for managing EdgeGuard user accounts. Lists all users with their username, role (admin/viewer), and creation date. Admins can create new users with a password and role, and delete existing users. Regular viewers cannot access this page โ it is protected by role-based access control.
Calls /api/users
to list users. Create form sends a POST with username, password, and role.
Delete button sends DELETE to /api/users/<id>.
The backend validates that the requesting user has admin role before allowing
any user management operation. Non-admin users who navigate to this URL
are shown a "Not authorized" message โ the backend also enforces this
server-side regardless of what the frontend shows.
All 7 pages support both dark and light mode. The toggle button is in the
navigation bar โ clicking it switches all colors instantly without a page reload.
The chosen theme is saved in localStorage
so it persists across browser sessions.
// Theme toggle โ stored in localStorage const [isDark, setIsDark] = useState( localStorage.getItem('theme') === 'dark' ); const toggleTheme = () => { const newTheme = !isDark; setIsDark(newTheme); localStorage.setItem('theme', newTheme ? 'dark' : 'light'); // Apply to root element โ all CSS variables update instantly document.documentElement.setAttribute('data-theme', newTheme ? 'dark' : 'light'); };
/* Light mode (default) */ :root { --bg: #f0f4f8; --surface: #ffffff; --text: #0f172a; --border: #e2e8f0; } /* Dark mode โ overrides same variables */ [data-theme="dark"] { --bg: #0b0f1a; --surface: #111827; --text: #e2e8f0; --border: #1e2d45; } /* All components use variables โ theme switch is instant */ .card { background: var(--surface); /* automatically changes with theme */ color: var(--text); border: 1px solid var(--border); }
React Router handles client-side navigation โ when a user goes to
/alerts
or /monitoring,
the browser requests that URL from the server. Without special nginx config,
the server would return 404 because those files don't exist on disk.
The fix is to make nginx always return index.html for any URL โ
React Router then reads the URL and shows the correct page.
server { listen 80; root /usr/share/nginx/html; index index.html; # Serve static files (JS, CSS, images) directly location ~* \.(js|css|png|jpg|svg|ico)$ { expires 1y; add_header Cache-Control "public, immutable"; } # All other URLs โ return index.html so React Router handles them location / { try_files $uri $uri/ /index.html; } }
The frontend is built with npm run build
which produces a static dist/ folder of HTML, CSS, and JavaScript.
This is copied into an nginx Docker image and deployed as a k3s Deployment
with 2 replicas and pod anti-affinity โ same pattern as the backend.
# Stage 1: Build React app FROM node:18-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build # Stage 2: Serve with nginx FROM nginx:alpine COPY --from=builder /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80
# Both frontend pods should show Running on different nodes kubectl get pods -A -o wide | grep frontend # Check service and NodePort kubectl get svc -A | grep frontend # View frontend logs kubectl logs -l app=edgeguard-frontend --all-containers
When updating the frontend (new build), k3s performs a rolling update โ it starts a new pod with the updated image, waits for it to become healthy, then terminates the old pod. With 2 replicas, one is always serving traffic during the update. Users never see a downtime or blank page during deployments.
Every page fetches real data from the Flask API with JWT authentication. No mock data or hardcoded values โ what you see in the demo is the actual live system state.
Runtime bars (top), speedup bars (bottom), values on bars, color-coded by efficiency, ideal line overlay โ exactly matching the format from lecture slides. The MPI/Non-MPI toggle and problem size selector make it easy to navigate all 4 benchmarks interactively during the presentation.
The React app has the backend URL set to http://10.10.10.1:30800.
This only works when the browser is on the same network as the cluster.
Outside the cluster LAN (e.g. from a phone on mobile data), the app loads
but API calls fail. For the presentation, the laptop must be connected
to the cluster hotspot.
A traditional web app would reload the full page on every navigation โ fetching HTML from the server each time. A React SPA loads once and handles all navigation client-side. This gives instant page transitions (no network round-trip), persistent state across pages (JWT token, theme choice), and a native app-like feel. The tradeoff is initial load time โ the entire React bundle downloads on first visit.