Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

⚡ The Pulse — Real-Time Stadium Management Platform

A Cyber-Athletic agentic platform that visualises live crowd density across a stadium, provides dynamic AI-powered wayfinding, and streams real-time IoT sensor data — all running entirely on your local machine. No API keys required.


📸 Overview

The Pulse is a full-stack project built around three core ideas:

Idea Implementation
Live crowd intelligence A simulator writes IoT-style sensor snapshots every 5 s; the backend pushes them to every browser via SSE
Dynamic wayfinding A weighted Dijkstra algorithm re-routes around congested zones in real time
Cyber-Athletic UI Neon-glowing SVG stadium map with density-driven colour transitions (green → yellow → orange → red)

☁️ Cloud Deployment — Google Cloud

Architecture

┌──────────────────────────────────────┐        ┌─────────────────────────────┐
│         Google Cloud Run             │  SSE   │     Firebase Hosting         │
│         (Docker container)           │◄───────│     (Global CDN)             │
│                                      │        │                              │
│  simulator/generator.js              │        │  frontend/dist/              │
│       │ writes every 5s              │        │  (React SPA)                 │
│       ▼                              │        │                              │
│  data/stadium-state.json             │        │  VITE_API_BASE_URL           │
│       │ fs.watch                     │        │  → Cloud Run URL             │
│       ▼                              │        └─────────────────────────────┘
│  backend/server.js  (SSE + REST API) │
└──────────────────────────────────────┘

The simulator and backend run together in one Docker container on Cloud Run — they share the same in-container filesystem.


Prerequisites

Install the tools you need (one-time):

# 1. Google Cloud SDK → https://cloud.google.com/sdk/docs/install
gcloud auth login
gcloud config set project YOUR_PROJECT_ID

# 2. Firebase CLI
npm install -g firebase-tools
firebase login

# 3. Enable required Google Cloud APIs
gcloud services enable run.googleapis.com artifactregistry.googleapis.com cloudbuild.googleapis.com

Create a project at console.cloud.google.com → New Project and note your Project ID.


Step 1 — Push your code to GitHub

git init
git add .
git commit -m "feat: The Pulse — production ready"
git remote add origin https://github.com/YOUR_USERNAME/the-pulse-stadium.git
git push -u origin main

Step 2 — Deploy Backend to Cloud Run

Build the Docker image using Cloud Build (no Docker needed locally) and deploy:

# From the project root
gcloud builds submit --tag gcr.io/YOUR_PROJECT_ID/the-pulse-backend .

gcloud run deploy the-pulse-backend \
  --image gcr.io/YOUR_PROJECT_ID/the-pulse-backend \
  --platform managed \
  --region us-central1 \
  --allow-unauthenticated \
  --port 3001 \
  --min-instances 1 \
  --set-env-vars PORT=3001,ALLOWED_ORIGINS=https://YOUR_PROJECT_ID.web.app

--min-instances 1 keeps the container always alive so the simulator never stops writing data.

Copy the Cloud Run URL printed at the end — e.g. https://the-pulse-backend-abcdef-uc.a.run.app


Step 3 — Deploy Frontend to Firebase Hosting

Update .firebaserc in frontend/ with your real project ID:

{ "projects": { "default": "YOUR_PROJECT_ID" } }

Build the frontend (bakes in the Cloud Run URL at compile time):

cd frontend

# Windows PowerShell
$env:VITE_API_BASE_URL="https://the-pulse-backend-abcdef-uc.a.run.app"

# Mac / Linux
export VITE_API_BASE_URL=https://the-pulse-backend-abcdef-uc.a.run.app

npm run build

Deploy to Firebase:

firebase deploy --only hosting

Your frontend goes live at: https://YOUR_PROJECT_ID.web.app


Step 4 — Update CORS

Allow the Firebase domain in the backend's CORS whitelist:

gcloud run services update the-pulse-backend \
  --region us-central1 \
  --update-env-vars ALLOWED_ORIGINS=https://YOUR_PROJECT_ID.web.app

Step 5 — Verify

Check URL
Backend health https://the-pulse-backend-xyz-uc.a.run.app/health
Stadium state https://the-pulse-backend-xyz-uc.a.run.app/api/state
Frontend https://YOUR_PROJECT_ID.web.app

Environment variable summary

Cloud Run (backend) — set via --set-env-vars or Google Cloud Console

PORT=3001
ALLOWED_ORIGINS=https://YOUR_PROJECT_ID.web.app

Frontend build — set before running npm run build

VITE_API_BASE_URL=https://the-pulse-backend-xyz-uc.a.run.app

⚠️ VITE_ variables are baked in at build time. If the Cloud Run URL changes, rebuild and redeploy the frontend.


🗂 Project Structure

the-pulse-stadium/
├── .gitignore              # Excludes .env, node_modules, data/, dist/
├── Dockerfile              # Cloud Run container — runs simulator + backend together
├── .dockerignore
├── railway.json            # (alternative) Railway deployment config
├── README.md
│
├── backend/
│   ├── .env                # Local config — PORT, ALLOWED_ORIGINS (gitignored)
│   ├── .env.example        # Template — safe to commit
│   ├── server.js           # Express SSE bridge with strict CORS whitelist
│   └── package.json        # Deps: express, cors, dotenv
│
├── frontend/
│   ├── .env                # Local config — VITE_API_BASE_URL (gitignored)
│   ├── .env.example        # Template — safe to commit
│   ├── src/
│   │   ├── components/
│   │   │   ├── StadiumMap.jsx   # Accessible SVG map (ARIA roles, keyboard nav)
│   │   │   ├── Navigator.jsx    # Wayfinding UI (Dijkstra)
│   │   │   ├── ZonePanel.jsx    # Selected zone detail panel
│   │   │   └── Header.jsx       # Top bar with live status + HC toggle
│   │   ├── hooks/
│   │   │   ├── useStadiumData.jsx  # SSE consumer — URLs from env var
│   │   │   └── useNavigator.jsx    # Route calculation hook
│   │   ├── lib/
│   │   │   ├── navigator.js        # Dijkstra + MinHeap implementation
│   │   │   └── navigator.test.js   # 20 Vitest unit tests
│   │   ├── index.css               # Design system + high-contrast mode
│   │   └── data/
│   │       └── stadium-graph.json  # Node/edge graph definition
│   └── package.json        # Includes "test" and "test:watch" scripts
│
├── simulator/
│   └── generator.js        # IoT data simulator (writes every 5 s)
│
└── data/
    └── stadium-state.json  # Live state file (auto-generated, gitignored)

🚀 Quick Start

You need three terminals running simultaneously.

1 — Install dependencies

# Backend
cd backend
npm install

# Frontend
cd ../frontend
npm install

2 — Configure environment

# Backend
cp backend/.env.example backend/.env

# Frontend
cp frontend/.env.example frontend/.env

The defaults work out of the box for local development. Edit only if you change ports.

3 — Start the IoT Simulator

# From the project root
node simulator/generator.js

Generates data/stadium-state.json every 5 seconds with randomised occupancy across 18 stadium zones.

4 — Start the Backend Server

cd backend
node server.js

Runs on http://localhost:3001

Endpoint Description
GET /health Liveness probe — returns client count + timestamp
GET /api/state One-shot JSON snapshot of the latest stadium state
GET /api/stream SSE stream — pushes stadium-update events on every file change

5 — Start the Frontend

cd frontend
npm run dev

Opens at http://localhost:5173


🏟 Stadium Zones

The simulator tracks 18 zones across 5 categories:

Category Zones
Gates North, South, East, West
Seating Section A (Upper), B (Upper), C (Lower), D (Lower), VIP Lounge
Concourses North Concourse, South Concourse
Amenities Food Court, Restrooms NW/SE, Merchandise Store, Medical Bay
Emergency Emergency Exit E1, Emergency Exit E2

Density Levels

Level Fill Ratio Colour
🟢 Low < 30% #00ff88
🟡 Medium 30 – 65% #f5d000
🟠 High 65 – 85% #ff6600
🔴 Critical ≥ 85% #ff1a3a

🧭 Wayfinding — Dijkstra Algorithm

The navigator uses a density-weighted Dijkstra algorithm to find the least-congested path between any two zones.

Weight Formula

weight(edge) = distance × (1 + destinationFillRatio)
  • Empty zone (0%) → multiplier ×1.0 (free flow)
  • Half-full (50%) → multiplier ×1.5 (slowed)
  • Full (100%) → multiplier ×2.0 (worst case)

This steers the algorithm away from congested sections even when they are geographically shorter, naturally routing evacuees and fans via the clearest path.

Key Exports (src/lib/navigator.js)

Function Description
buildAdjacency(graph) Converts edge list → undirected adjacency map
dijkstra(adj, start, end, getDensityFn) Returns PathResult | null
buildNodeMap(graph) O(1) node label/zoneId lookups
formatPath(result, nodeMap) Turn-by-turn human-readable directions
classifyDensity(ratio) Maps 0–1 fill ratio → 'low' | 'medium' | 'high' | 'critical'

🏗 Tech Stack

Frontend

Library Role
React 19 UI framework
Vite 8 Dev server + bundler
Tailwind CSS 4 Utility styling
Framer Motion Animations & transitions
Lucide React Icon library
Vitest Unit test runner

Backend

Library Role
Express 5 HTTP server
cors CORS middleware (strict whitelist)
dotenv Environment variable loader
Node fs.watch File watching (debounced, 100 ms)

Data Flow

simulator/generator.js
        │  writes every 5 s
        ▼
data/stadium-state.json
        │  fs.watch detects change
        ▼
backend/server.js  ──SSE──▶  browser (EventSource)
                                   │
                            useStadiumData hook
                                   │
                    StadiumMap / Navigator / ZonePanel

🔐 Security

Environment Variables

Sensitive configuration is kept out of source code using .env files. Each service has its own .env — copy the .example template to get started.

backend/.env

PORT=3001
ALLOWED_ORIGINS=http://localhost:5173

frontend/.env

VITE_API_BASE_URL=http://localhost:3001

.env files are gitignored. Only .env.example templates are committed.

CORS Hardening

The backend enforces a strict origin whitelist — requests from any origin not listed in ALLOWED_ORIGINS are rejected with a CORS error:

// Only origins in the comma-separated ALLOWED_ORIGINS list are permitted
cors({
  origin(origin, callback) {
    if (!origin || ALLOWED_ORIGINS.includes(origin)) return callback(null, true)
    callback(new Error(`CORS: origin "${origin}" is not allowed.`))
  }
})

The previous Access-Control-Allow-Origin: * wildcard header on the SSE endpoint has been removed; the CORS middleware handles it instead.


♿ Accessibility

Keyboard Navigation

All SVG map zones are fully keyboard-accessible:

  • Tab — cycles focus through every zone on the map
  • Enter / Space — selects the focused zone (same as a click)
  • Focused zones display a visible dashed white focus ring

ARIA Attributes

Every interactive zone exposes:

role="button"
tabIndex="0"
aria-pressed="true|false"         <!-- whether the zone is selected -->
aria-label="North Gate — medium density, 42% full, wait time 3 minutes"

The SVG container itself has role="img" with an aria-label that explains keyboard usage. Decorative elements (grid lines, pitch markings, crosshair) are marked aria-hidden="true".

The live status indicator in the header has aria-live="polite" so screen readers announce connection changes.

High-Contrast Mode

A HC toggle button in the header switches the entire UI to a high-contrast theme:

  • Solid black background — no glassmorphism, no blurred panels
  • Full-opacity zone colours — no semi-transparent overlays or neon glow
  • Yellow focus rings (#ffff00) for maximum keyboard visibility
  • All CSS tokens overridden via [data-high-contrast="1"] on <html>
  • Preference is persisted in localStorage — survives page refresh

🧪 Running Tests

Unit tests for the Dijkstra algorithm live in frontend/src/lib/navigator.test.js and are run with Vitest (no DOM or browser needed — pure JS functions only).

cd frontend
npm test                # run once and exit
npm run test:watch      # re-run on file save

Test Results

✓ buildAdjacency (2 tests)
✓ dijkstra — happy path (3 tests)
✓ dijkstra — start equals end (1 test)
✓ dijkstra — disconnected node (2 tests)
✓ dijkstra — zero-weight edges (2 tests)
✓ dijkstra — non-existent node (2 tests)
✓ dijkstra — congestionNote (2 tests)
✓ formatPath (2 tests)
✓ classifyDensity (4 tests)

Test Files  1 passed (1)
     Tests  20 passed (20)
  Duration  1.78s

Test Coverage

Scenario What is verified
Happy path Finds a valid path between two connected nodes
Start === End Returns zero-cost result with empty segments
Disconnected node Returns null — no infinite loop or crash
Zero-weight edge Resolves correctly — no NaN or negative weights
Non-existent node Throws a descriptive error message
Congestion note warning fires when route density ≥ 0.85
Density avoidance Route actively steers away from critical-density nodes
buildAdjacency Edges are bidirectional in the adjacency map
classifyDensity All four thresholds map to the correct label

📡 SSE Event Reference

The backend emits two named event types on GET /api/stream:

stadium-update

Full stadium state payload on every simulator tick.

{
  "meta": { "version": "1.0.0", "generatedAt": "...", "intervalMs": 5000 },
  "stadium": {
    "name": "The Pulse Arena",
    "totalCapacity": 17210,
    "totalOccupancy": 9344,
    "globalFillRatio": 0.5430,
    "globalDensity": "medium"
  },
  "zones": [
    {
      "id": "gate-north",
      "label": "North Gate",
      "type": "gate",
      "capacity": 800,
      "occupancy": 312,
      "fillRatio": 0.39,
      "density": "medium",
      "waitTime": 4,       // minutes (gates only, null for other zones)
      "alertActive": false
    }
    // … 17 more zones
  ]
}

info

Informational messages from the server (e.g. "Waiting for simulator data…").


🛠 Common Issues

Problem Fix
Map shows no data / grey zones Make sure the simulator is running first — the backend needs data/stadium-state.json to exist
ENOENT error in backend Run the simulator at least once to create the data file
CORS error in browser Ensure the backend is running on port 3001 and ALLOWED_ORIGINS in backend/.env includes your frontend origin
SSE reconnecting in a loop Check the backend terminal — the state file may be missing or malformed
.env changes not picked up Restart the backend (node server.js) after editing backend/.env
High-contrast mode stuck on after refresh Clear localStorage key hc in browser DevTools → Application → Local Storage

📄 License

MIT — built for hackathon purposes. Go build something awesome. ⚡

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages