A real-time heart rate monitoring dashboard built with Next.js, Material-UI, WebSockets, and Spotify integration. Features a custom Express server for stateful WebSocket connections, Tabata timer with audio feedback, and live HR zone visualization.
server.ts) that runs Next.js, a persistent WebSocket server, and background services. The server is stateful and cannot be deployed on serverless platforms like Vercel.
- HRM (Heart Rate Monitor) Dashboard
- Real-time Heart Rate Monitoring - WebSocket streaming from Bluetooth HRM devices or mock client
- Dual-Mode Timer - Tabata (work/rest intervals) and Stopwatch (count-up) modes with 5-second prepare countdown
- Audio Feedback - Original HRM beep sounds for countdown (3-2-1) and phase transitions
- HR Zone Visualization - Large percentage tiles with color-coded zones and user names/ages
- Spotify Integration - Full playback control, device selection, volume control, and now-playing display
- Mock HRM Client - Test interface with zone buttons, device ID, and noise simulation (available at
/client/mock) - Control Panel - Mobile-optimized UI with timer controls, Spotify controls, and configuration steppers (available at
/client/control) - Bluetooth HRM Support - Real heart rate monitor connection via Web Bluetooth API (available at
/client/connect) - Visual Regression Tests - Playwright screenshot-based testing
Data Usage Disclaimer: This application is a local data generator.
- Live Dashboard: The heart rate tiles and group displays shown in screenshots utilize direct Bluetooth sensor data. They do not display data fetched from Strava, complying with Strava API Brand Guidelines regarding user privacy.
- AI Features: Any AI/LLM integration (e.g., Gemini) analyzes strictly local project metadata or locally recorded Bluetooth logs. No data fetched from the Strava API is ever sent to AI models.
- Strava Integration: The Strava API is used strictly for uploading completed activities (Write-Only).
This repository is configured with a VS Code DevContainer, which provides a fully automated, "one-click" setup.
-
Prerequisites:
- Docker Desktop installed and running.
- Visual Studio Code with the Dev Containers extension.
-
Launch:
- Open the repository in VS Code.
- Click the "Reopen in Container" button when prompted.
That's it. The container will build, install all dependencies (pnpm install and Playwright), and create a .env.local file for you. Once the container is ready, you can start the development server:
pnpm run devIf you are not using the DevContainer, you can set up the project manually:
Before setting up the project locally, ensure you have the following installed on your system:
-
Node.js (version 18.x or higher recommended)
- Download from nodejs.org
- Verify installation:
node --version
-
pnpm (Package Manager)
- Install globally:
npm install -g pnpm - Verify installation:
pnpm --version
- Install globally:
-
Git
- Download from git-scm.com
- Verify installation:
git --version
1. Clone the Repository
# Clone the repository to your local machine
git clone https://github.com/arii/hrm.git
# Navigate to the project directory
cd hrm2. Install pnpm (if not already installed)
# Install pnpm globally using npm
npm install -g pnpm
# Verify pnpm installation
pnpm --version3. Install Project Dependencies
# Install all required dependencies using pnpm
pnpm install --frozen-lockfileThis will install all the Node.js packages required by the project and also set up pre-commit hooks using Husky to automatically lint and format your code when you commit.
4. Install Playwright Browser Dependencies
# Install Playwright browsers for testing
pnpm exec playwright install --with-deps5. Set Up Environment Variables
# Create your local environment file from the example
cp .env.example .env.localThen open .env.local and fill in the required values:
# Spotify OAuth credentials (get these from developer.spotify.com/dashboard)
SPOTIFY_CLIENT_ID=your_client_id_here
SPOTIFY_CLIENT_SECRET=your_client_secret_here
# NextAuth.js configuration
NEXTAUTH_URL=http://127.0.0.1:3000
NEXTAUTH_SECRET=your_random_secret_hereTo generate a secure NEXTAUTH_SECRET:
openssl rand -base64 326. Start the Development Server
# Start the custom server (Next.js + WebSocket + background services)
pnpm run devThe server will start and you should see output indicating:
- Next.js app running on http://127.0.0.1:3000
- WebSocket server on ws://127.0.0.1:3000/ws
- Spotify polling service initialized
- Tabata timer service initialized
7. Verify the Setup
Open your browser and navigate to:
- Dashboard: http://127.0.0.1:3000
- Mock HRM Client: http://127.0.0.1:3000/mock
- Phone Controls: http://127.0.0.1:3000/phone
- Bluetooth Connection: http://127.0.0.1:3000/connect
You should see the HRM dashboard interface load successfully.
8. (Optional) Verify Spotify Integration
# Run the Spotify verification script
pnpm run verify:spotifyThis will test your Spotify credentials and connection.
If you encounter issues during setup:
- Port 3000 already in use: Kill any process using port 3000, or use
pnpm run pm2:stopif PM2 is running - Module not found errors: Ensure all dependencies are installed with
pnpm install --frozen-lockfile - Playwright errors: Run
pnpm exec playwright install --with-depsagain - Environment variable issues: Double-check that
.env.localexists and contains valid values
For more detailed troubleshooting, see the Troubleshooting section below.
β οΈ Package Manager Change: This project now uses pnpm instead of npm. Allnpmcommands are blocked to preventpackage-lock.jsoncreation.
# 1. Create your environment file from the example
cp .env.example .env.local
# 2. Fill in the required values in .env.local (see "Environment Variables" section)
# 3. Install pnpm if not already installed
npm install -g pnpm
# 4. Install dependencies using the lockfile
pnpm install --frozen-lockfile
# 5. Install Playwright's browser dependencies
pnpm exec playwright install --with-deps
# 6. Start the development server
pnpm run devMigration from npm: If you accidentally run
npm install, the project will block it and show a helpful message. Use the wrapper script./scripts/npm-to-pnpm.shto automatically convert npm commands to pnpm equivalents.
The server will start with:
- Next.js app on http://127.0.0.1:3000
- WebSocket server on ws://127.0.0.1:3000/ws
- Spotify polling service
- Tabata timer service
# Build TypeScript server and Next.js app (optional; pnpm run start auto-builds if needed)
pnpm run build
# Start with PM2 (requires .env.production)
pnpm run start
# View logs
pnpm run pm2:logspnpm run start now checks for .env.production and verifies build artifacts. When .next/ or
dist/server.mjs are missing it runs pnpm run build before launching PM2, so manual builds are
only required when you want to inspect the output ahead of time.
This project follows a standard Next.js application structure with a few key additions to support its real-time, stateful nature. For a detailed guide to the project's architecture, file organization, and component inventory, please see the documentation in the docs/ directory.
- File Structure Guide: A comprehensive overview of the directories and their purposes.
- Component Inventory: A map of the major UI components and their locations.
/app: Core Next.js pages, layouts, and API routes./components: Reusable React components./context: Global state management with React Context./hooks: Custom React hooks for shared logic./services: Backend business logic (e.g., Tabata timer, Spotify polling)./utils: Helper functions and utilities.
pnpm run dev # Start dev server (Next.js + WebSocket + services)
pnpm run build # Build for production
pnpm run start # Start production server with PM2
pnpm run prepare # Install git hooks with Husky
pnpm run lint # Run ESLint
pnpm run lint:fix # Auto-fix lint issues
pnpm run format # Format codebase with Prettier
pnpm run format:check # Verify formatting without writing
pnpm run test:core # Canonical Playwright suite (chromium baseline screenshots)
pnpm run test:quick # Fast smoke run (Playwright, dot reporter)
pnpm run test:visual:update # Regenerate baseline screenshots after intentional UI changes
pnpm run test:visual:headed # Run Playwright in headed mode for debugging
pnpm run test:visual:ui # Launch Playwright interactive UI
pnpm run test:visual:report # View the latest Playwright HTML report
pnpm run test:clean # Kill stray processes, boot dev server, run baseline tests
pnpm run test:clean:update # Clean start + regenerate baseline screenshots
pnpm run verify:spotify # Automated Spotify integration health check
pnpm run kill-all # Force-stop lingering Node/Chrome processes
pnpm run mcp:chrome-devtools # Start Chrome DevTools MCP (isolated profile)For production, create .env.production alongside .env.local. The start script refuses to run
without it so sensitive secrets are always loaded before PM2 boots.
The app includes URL redirects for easier navigation:
/phoneβ/client/control(Phone Controls)/connectβ/client/connect(Stream HR)/mockβ/client/mock(Mock HRM)
- Baseline visual regression:
pnpm run test:core - Snapshot updates (intentional UI changes):
pnpm run test:visual:update - Headed debugging:
pnpm run test:visual:headed - Interactive runner:
pnpm run test:visual:ui - Fast smoke (under a minute):
pnpm run test:quick - Clean environment runs:
pnpm run test:clean/pnpm run test:clean:update - Reports:
pnpm run test:visual:report - Spotify integration health check:
pnpm run verify:spotify
Playwright tests live in tests/playwright/core-functionality.spec.ts; baseline screenshots are stored in tests/playwright/core-functionality.spec.ts-snapshots/.
Best practice: Run pnpm run test:core before pushing UI changes and regenerate snapshots only after manual review.
You can interact with the project's automation workflows by using the following commands in pull request or issue comments.
| Command | Description | Example |
|---|---|---|
@pr-squash |
Squashes all commits in a pull request into a single commit. | @pr-squash |
@pr-squash-rebase |
Squashes all commits and rebases the pull request on top of the base branch. | @pr-squash-rebase |
/create-issues |
Creates follow-up issues based on the content of a pull request review. | /create-issues |
@gemini-bot |
Invokes the Gemini AI to perform tasks like code review or code generation. | @gemini-bot review |
@jules fix |
(Legacy) Invokes the Jules AI to perform a code review. | @jules fix |
@gemini-update-pr |
Triggers a workflow to update the pull request with the latest changes from the base branch. | @gemini-update-pr |
This workspace is pre-configured for a seamless development experience with VS Code.
.vscode/settings.json: Enables format-on-save (Prettier) and ESLint auto-fix..vscode/launch.json: Provides launch configurations to start the server and attach the debugger with one click..vscode/tasks.json: Defines helper tasks for running dev scripts and the Chrome DevTools MCP.
Recommended Extensions:
- ESLint (
dbaeumer.vscode-eslint) - Prettier - Code formatter (
esbenp.prettier-vscode) - Playwright Test for VSCode (
ms-playwright.playwright)
Create a .env.local file in the root directory for secrets:
# Spotify OAuth credentials (from developer.spotify.com/dashboard)
SPOTIFY_CLIENT_ID=your_client_id
SPOTIFY_CLIENT_SECRET=your_client_secret
# NextAuth.js configuration
NEXTAUTH_URL=http://127.0.0.1:3000
NEXTAUTH_SECRET=your_random_secret_here- Create an app at the Spotify Developer Dashboard.
- Add
http://127.0.0.1:3000/api/auth/callback/spotifyas a Redirect URI in the app settings. - Copy the Client ID and Client Secret into your
.env.localfile. - Generate a
NEXTAUTH_SECRETwithopenssl rand -base64 32. - Restart the server and run
pnpm run verify:spotifyto test the connection.
The app includes the original HRM audio feedback system:
- Countdown beeps: Short beeps during the last 3 seconds of any countdown phase
- Transition beeps: Long beeps when phases change (prepareβwork, workβrest, restβwork)
- Volume control: Synchronized with Spotify volume controls
- Audio files: Located in
public/assets/(beep-01a.wav, beep-07.wav)
- Design Guidelines β Theme, typography, and component standards.
- Front-End Improvement Plan β Current UI polish backlog and priorities.
- Test Improvement Plan β Roadmap for expanding automated coverage.
- Testing Guide β How to run and interpret the existing suites.
- Automation Plan β Chrome DevTools MCP and automation scripting strategy.
- TypeScript Patterns β Best practices for writing type-safe code and avoiding
any.
This application uses a custom Express server (server.ts) that is stateful. It manages:
- Hosting the Next.js application.
- A persistent WebSocket server on the
/wsendpoint. - Long-running background services like the Tabata Timer and Spotify Polling.
CRITICAL: This architecture is incompatible with serverless deployment platforms like Vercel or Netlify. It must be deployed on a traditional Node.js host (e.g., VPS, Docker container, or a dedicated server).
All real-time state updates are managed by the server and pushed to clients via WebSocket.
- Client β Server: Send commands (e.g.,
TIMER_COMMAND,SPOTIFY_COMMAND) or data (HRM_INPUT). - Server β Clients: Broadcasts
STATE_UPDATEmessages containing the latest HR data, timer status, and Spotify track information to all connected clients.
This ensures a single source of truth for application state, keeping all viewers and control panels perfectly in sync.
graph TD
subgraph "Browser"
A[Next.js Frontend]
B[WebSocket Client]
end
subgraph "Server"
C[Express Server]
D[Next.js Middleware]
E[WebSocket Server]
F[Tabata Timer Service]
G[Spotify Polling Service]
end
subgraph "External Services"
H[Spotify API]
I[Bluetooth HRM Device]
end
A -- HTTP Requests --> C
C -- Forwards to --> D
B -- WebSocket Connection --> E
E -- Broadcasts State Updates --> B
E -- Receives Commands --> B
F -- Updates --> E
G -- Updates --> E
G -- Interacts with --> H
A -- Interacts with --> I
tabataTimer.ts: A state machine managing WORK/REST/IDLE phases. It broadcaststimerDataand emitssoundToPlayevents for audio feedback.spotifyPolling.ts: Polls the Spotify API every 3 seconds for the "Now Playing" status and handles playback commands. It loads tokens fromlogs/spotify_tokens.jsonfor persistence.spotifyTokenManager.ts: Automatically refreshes the Spotify access token every 55 minutes and saves it to the file system, ensuring the server can survive restarts without requiring re-authentication.
- Run the custom server: Always use
pnpm run devfor development to ensure all background services are running. - State Management: All global state is owned by the server. Client-side state should be ephemeral.
- UI Components: Use Material-UI (MUI) for all components.
- Code Quality: Git hooks (Husky) automatically enforce linting and formatting on commit. Use
pnpm run lintonly for full-project checks. - Visual Testing:
- Run
pnpm run test:corebefore committing UI changes. - Use
pnpm run test:visual:updateonly after verifying differences locally. - Reach for
pnpm run test:visual:headedorpnpm run test:visual:uiwhen debugging failures. - For end-to-end validation, follow with
pnpm run test:cleanto exercise the dev server startup path.
- Run
- Audio Testing: Test timer sounds on both dashboard and control panel. Audio only plays on dashboard, not control panel.
- Layout Consistency: Timer always takes 50% width, HR tiles 25% each, Google Doc has fixed 500px height.
For more detailed guidelines, especially for AI agents, see .github/prompts/AGENTS.md.
We welcome contributions to the HRM Dashboard! Please follow these guidelines to ensure a smooth development process.
Automated Code Quality: To ensure consistency and prevent common errors, this project uses a pre-commit hook that automatically formats and lints your code. When you commit your changes, lint-staged will:
- Format your code with Prettier.
- Lint your code with ESLint and attempt to fix any auto-fixable issues.
If ESLint finds errors that it cannot fix automatically, the commit will be aborted, and you will need to resolve the issues manually before you can commit again.
- Formatting: This project uses Prettier for code formatting. You can run
pnpm run formatto manually trigger it. - Linting: We use ESLint for static analysis. Run
pnpm run lintto check for any issues manually.
Please follow the Conventional Commits specification for your commit messages.
- Fork the repository and create your branch from
leader. - Make your changes and ensure all tests pass (
pnpm test). - Submit a pull request with a clear description of your changes.
- Spotify token persistence audits β Exercise
pnpm run verify:spotifyafter redeploys and confirmlogs/spotify_tokens.jsonsurvives restarts. - Automated baseline capture β Implement the Playwright-driven screenshot workflow defined in
docs/automation-plan.md(background dev server + Chrome MCP bootstrap). - Server observability β Add structured logs for Tabata timer transitions and WebSocket client lifecycle; surface via PM2 log rotation.
- Disaster recovery runbook β Capture restart, log rotation, and SSL renewal steps under
docs/to unblock production responders.
Track progress by updating the respective markdown plans after each milestone.
- Copied original HRM audio files from product_hrm
- Implemented AudioManager class for centralized sound control
- Added useAudio hook for React components
- Proper sound mapping: shortBeep (countdown) and longBeep (transitions)
- Consistent layout proportions (no dynamic resizing)
- Rotated side labels on timer display for better space utilization
- Volume synchronization between dashboard and control panel
- Improved mobile navigation with proper labels
- Complete deployment scripts and documentation
- PM2 configuration with proper environment handling
- Nginx configuration template
- Pre-deployment validation checks
MIT
- Next.js for the React framework
- Material-UI for components
- NextAuth for Spotify OAuth
- PM2 for process management
- Node.js & pnpm
- PM2 (
pnpm add -g pm2) - Nginx
- A domain name with DDNS
- An SSL certificate (Let's Encrypt is recommended)
- Run Deployment Script: Ensure
.env.productionexists. Then, on the production host, run the automated deployment script:This script automatically syncs the./deploy.sh
leaderbranch, installs dependencies, builds the project, and reloads the application using PM2. - Initial Server Setup: For the first deployment, you will need to:
- Configure Nginx as a reverse proxy (see configuration below).
- Set up SSL certificates (e.g., with Let's Encrypt).
- Configure PM2 to start on system boot with
pm2 startup.
When deploying in production, using Nginx as a reverse proxy is essential for TLS termination (HTTPS) and load balancing. Configuring Nginx for a unified HTTP/WebSocket backend requires specific header settings to upgrade the connection successfully.
Below is the recommended Nginx configuration. This assumes:
- Nginx is listening on port 443 (HTTPS).
- Your HRM application is running internally on
http://127.0.0.1:3000.
server {
# Public HTTPS Listener
listen 443 ssl;
server_name your.hrm.domain.com;
# --- TLS/SSL Configuration (Must be customized) ---
ssl_certificate /etc/letsencrypt/live/your.hrm.domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your.hrm.domain.com/privkey.pem;
# --- Standard Proxy Headers ---
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; # Tells NextAuth/Node if original request was HTTPS
# --- WebSocket Specific Headers (CRITICAL) ---
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Long-lived connections need a generous timeout (default is 60s)
proxy_read_timeout 86400s;
location / {
# Pass all requests (HTTP and WS) to the Next.js/Node backend
proxy_pass http://127.0.0.1:3000;
}
}
# Optional: Redirect HTTP to HTTPS
server {
listen 80;
server_name your.hrm.domain.com;
return 301 https://$host$request_uri;
}The Upgrade and Connection headers are critical for the WebSocket handshake. Nginx, being a hop-by-hop proxy, strips these headers by default. The configuration above ensures they are passed to your Node.js server, allowing the protocol switch to succeed.
- Symptom:
Error: listen EADDRINUSE: address already in use :::3000 - Solution: Find and kill the process using port 3000, or use
pnpm run pm2:stop.
- Symptom:
Error [ERR_MODULE_NOT_FOUND]: Cannot find module ... - Solution: Use
pnpm run devfor development, and ensure yourtsconfig.jsonis configured for CommonJS modules.
- Symptom:
error TS2307: Cannot find module ... - Solution: Verify the file exists, the import path is correct, and the file is included in your
tsconfig.json.
- Cause: Spotify doesn't recognize your credentials.
- Fix: Verify your Client ID and Secret in
.env.localand restart the server.
- Cause: The redirect URI in the Spotify dashboard doesn't match your NextAuth configuration.
- Fix: Add the correct redirect URI to your Spotify app settings.
- Symptom: You have to log in every time the server restarts.
- Fix: Check that the
logs/spotify_tokens.jsonfile exists and is writable.
To ensure the stability of the stateful WebSocket server, all contributions must adhere to the following standards.
We follow Conventional Commits:
feat:New features (e.g.,feat: add tabata countdown audio)fix:Bug fixes (e.g.,fix: sync spotify volume slider)chore:Maintenance (e.g.,chore: update .gitignore)docs:Documentation updates
Deployment is automated via a script that syncs the leader branch and reloads the application.
- Commit Changes: Ensure all your changes are committed and pushed.
- Run Deploy Script: On the production host, run the deployment script:
bash ./deploy.shThis script will handle pulling the latest changes from theleaderbranch, installing dependencies, and gracefully reloading the PM2 process.
See the Nginx Reverse Proxy Configuration section below for the required Nginx setup.