Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 1 addition & 10 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,11 @@ import GoogleDocViewer from '../components/GoogleDocViewer'
import HrmTiles from '../components/HrmTiles'
import SpotifyDisplay from '../components/SpotifyDisplay'
import TimerDisplay from '../components/TimerDisplay'
import { useWebSocket } from '@/context/WebSocketContext'

const DOC_URL =
'https://docs.google.com/document/d/e/2PACX-1vTev5AMiHYi2Jkg9x6zRQoiJ_o2X_wZMqAXVpwgjlSqzlcXelxSc7psjE8n3N-ghzXMFtnv51nc2fJZ/pub?embedded=true'

const Dashboard = () => {
const { timerData } = useWebSocket()
const [docIsManuallyShrunk, setDocIsManuallyShrunk] = useState(false)

// Signal when page is ready for testing
Expand All @@ -45,14 +43,7 @@ const Dashboard = () => {

{/* 1. TABATA TIMER - Componentized */}
<Grid item xs={12} lg={6}>
<TimerDisplay
phase={timerData.currentPhase}
timeRemaining={timerData.timeRemaining}
timeElapsed={timerData.timeElapsed}
mode={timerData.mode}
workDuration={timerData.workDuration}
restDuration={timerData.restDuration}
/>
<TimerDisplay />
</Grid>

<ErrorBoundary fallback={<ErrorFallback />}>
Expand Down
4 changes: 2 additions & 2 deletions components/HrmTiles.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
// File: app/components/dashboard/HrmTiles.tsx
'use client'
import HrTile from '@/components/HrTile'
import { useWebSocket } from '@/context/WebSocketContext'
import { useHrmData } from '@/hooks/useHrmData'
import { MAX_HR_DEFAULT } from '@/utils/constants'
import { getHrZoneProps } from '@/utils/visualization'
import Grid from '@mui/material/Grid'
import Skeleton from '@mui/material/Skeleton'

const HrmTiles = () => {
const { hrmData } = useWebSocket()
const hrmData = useHrmData()

if (hrmData.length > 0) {
return (
Expand Down
8 changes: 5 additions & 3 deletions components/SpotifyDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import useSpotifyWebPlayback from '@/hooks/useSpotifyWebPlayback'
import useVolumePreference, { clampVolume } from '@/hooks/useVolumePreference'
import { useWebSocket } from '@/context/WebSocketContext'
import { useSpotifyData } from '@/hooks/useSpotifyData'
import { SpotifyCommandMessage } from '@/types/websocket'
import VolumeUp from '@mui/icons-material/VolumeUp'
import PauseIcon from '@mui/icons-material/Pause'
Expand All @@ -18,7 +19,7 @@ import MenuItem from '@mui/material/MenuItem'
import Slider from '@mui/material/Slider'
import Typography from '@mui/material/Typography'
import { signIn, signOut, useSession } from 'next-auth/react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { memo, useCallback, useEffect, useRef, useState } from 'react'

interface SpotifyDevice {
id: string
Expand All @@ -31,7 +32,8 @@ interface SpotifyDevice {
}

const SpotifyDisplay = () => {
const { spotifyData, sendData, connectionStatus } = useWebSocket()
const { sendData, connectionStatus } = useWebSocket()
const spotifyData = useSpotifyData()
const { data: session } = useSession()
console.log('spotifyData.trackName:', spotifyData.trackName)
const { volume, setVolume } = useVolumePreference()
Expand Down Expand Up @@ -405,4 +407,4 @@ const SpotifyDisplay = () => {
return null
}

export default SpotifyDisplay
export default memo(SpotifyDisplay)
28 changes: 10 additions & 18 deletions components/TimerDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,19 @@ import Card from '@mui/material/Card'
import CardContent from '@mui/material/CardContent'
import Typography from '@mui/material/Typography'
import { memo } from 'react'
import { TimerMode, TimerPhase } from '../types/websocket'

export interface TimerDisplayProps {
phase: TimerPhase
timeRemaining: number // seconds (for countdown)
timeElapsed: number // seconds (for stopwatch)
mode: TimerMode
workDuration?: number
restDuration?: number
}
import { useTimerData } from '@/hooks/useTimerData'

const pad = (n: number) => String(n).padStart(2, '0')

const TimerDisplay = ({
phase,
timeRemaining,
timeElapsed,
mode,
workDuration = 20,
restDuration = 10,
}: TimerDisplayProps) => {
const TimerDisplay = () => {
const {
phase,
timeRemaining,
timeElapsed,
mode,
workDuration = 20,
restDuration = 10,
} = useTimerData()
// Determine what to display based on mode and phase
let displayTime: string
let phaseColor: string
Expand Down
3 changes: 1 addition & 2 deletions ecosystem.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ module.exports = {
name: 'hrm-server',
script: './start-production.sh',
interpreter: 'bash',
instances: 'max',
exec_mode: 'cluster',
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '1G',
Expand Down
23 changes: 23 additions & 0 deletions hooks/useHrmData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// File: hooks/useHrmData.ts
'use client'
import { useWebSocket } from '@/context/WebSocketContext'
import { HrmData } from '@/types/websocket'
import { useMemo } from 'react'

/**
* @description A hook to extract a memoized HRM data array from the WebSocket context.
* This hook ensures that consumers only re-render when the content of the hrmData array changes.
* @returns {HrmData[]} The heart rate monitor data array.
*/
export const useHrmData = (): HrmData[] => {
const { hrmData } = useWebSocket()

// Memoize the hrmData array based on its content.
// By using JSON.stringify, we create a stable dependency that only changes
// when the actual data inside the array changes.
const hrmDataString = JSON.stringify(hrmData)
// eslint-disable-next-line react-hooks/exhaustive-deps
const memoizedHrmData = useMemo(() => hrmData, [hrmDataString])

return memoizedHrmData
}
33 changes: 33 additions & 0 deletions hooks/useSpotifyData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// File: hooks/useSpotifyData.ts
'use client'
import { useWebSocket } from '@/context/WebSocketContext'
import { useMemo } from 'react'

/**
* @description A hook to extract memoized Spotify data from the WebSocket context.
* This hook ensures that consumers only re-render when the specific spotifyData values change.
* @returns {object} An object containing the Spotify data.
*/
export const useSpotifyData = () => {
const { spotifyData } = useWebSocket()

const memoizedSpotifyData = useMemo(() => {
return {
trackName: spotifyData.trackName,
artist: spotifyData.artist,
isPlaying: spotifyData.isPlaying,
albumArtUrl: spotifyData.albumArtUrl,
durationMs: spotifyData.durationMs,
progressMs: spotifyData.progressMs,
}
}, [
spotifyData.trackName,
spotifyData.artist,
spotifyData.isPlaying,
spotifyData.albumArtUrl,
spotifyData.durationMs,
spotifyData.progressMs,
])

return memoizedSpotifyData
}
33 changes: 33 additions & 0 deletions hooks/useTimerData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// File: hooks/useTimerData.ts
'use client'
import { useWebSocket } from '@/context/WebSocketContext'
import { useMemo } from 'react'

/**
* @description A hook to extract memoized Timer data from the WebSocket context.
* This hook ensures that consumers only re-render when the specific timerData values change.
* @returns {object} An object containing the timer data.
*/
export const useTimerData = () => {
const { timerData } = useWebSocket()

const memoizedTimerData = useMemo(() => {
return {
phase: timerData.phase,
timeRemaining: timerData.timeRemaining,
timeElapsed: timerData.timeElapsed,
mode: timerData.mode,
workDuration: timerData.workDuration,
restDuration: timerData.restDuration,
}
}, [
timerData.phase,
timerData.timeRemaining,
timerData.timeElapsed,
timerData.mode,
timerData.workDuration,
timerData.restDuration,
])

return memoizedTimerData
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"test:unit": "jest",
"test:unit:watch": "jest --watch",
"test:unit:coverage": "jest --coverage",
"test:json": "pnpm exec cross-env TESTING=true bash start-production.sh > /tmp/hrm-server.log 2>&1 & echo $! > /tmp/hrm-server.pid && npx wait-on http://127.0.0.1:3000/api/debug/ping --timeout 20000 && npx playwright test --reporter=json > playwright-report.json; kill $(cat /tmp/hrm-server.pid) 2>/dev/null || true",
"test:json": "pnpm exec cross-env TESTING=true bash start-production.sh > /tmp/hrm-server.log 2>&1 & echo $! > /tmp/hrm-server.pid && npx wait-on http://127.0.0.1:$npm_package_config_port/api/debug/ping --timeout 20000 && npx playwright test --reporter=json > playwright-report.json; kill $(cat /tmp/hrm-server.pid) 2>/dev/null || true",
"test:visual": "pnpm exec cross-env TESTING=true bash start-production.sh > /tmp/hrm-server.log 2>&1 & echo $! > /tmp/hrm-server.pid && npx wait-on http://127.0.0.1:3000/api/debug/ping --timeout 20000 && playwright test; kill $(cat /tmp/hrm-server.pid) 2>/dev/null || true",
"test:all": "pnpm run test:visual && pnpm run test:unit",
"test:quick": "pnpm run build && pnpm exec cross-env TESTING=true bash start-production.sh > /tmp/hrm-server.log 2>&1 & echo $! > /tmp/hrm-server.pid && npx wait-on http://127.0.0.1:3000/api/debug/ping --timeout 20000 && playwright test --reporter=dot; kill $(cat /tmp/hrm-server.pid) 2>/dev/null || true",
Expand Down
9 changes: 0 additions & 9 deletions server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,6 @@ const hostname =
: process.env.HOST || '127.0.0.1' // Bind to all interfaces in production

const dev = process.env.NODE_ENV !== 'production'

// === QUICK WIN 1: CRITICAL SECURITY CHECK ===
if (!dev && !process.env.NEXTAUTH_SECRET) {
console.error('FATAL: NEXTAUTH_SECRET environment variable is missing.')
console.error('This is mandatory for production security. Shutting down.')
process.exit(1)
}
// ===========================================

const app = next({ dev, hostname, port })

logger.info(`Starting server in ${dev ? 'development' : 'production'} mode`)
Expand Down
13 changes: 6 additions & 7 deletions services/spotifyPolling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,14 @@ export class SpotifyPolling {
// --- Polling Logic ---

// Expose start/stop polling publicly (used by server to control lifecycle)
public startPolling() {
public startPolling(intervalMs: number = 3000) {
if (this.pollInterval) return

const intervalMs = process.env.SPOTIFY_POLLING_INTERVAL_MS
? parseInt(process.env.SPOTIFY_POLLING_INTERVAL_MS, 10)
: 3000
// Poll every `intervalMs` for low-latency updates
this.pollInterval = setInterval(() => this.getCurrentlyPlaying(), intervalMs)
logger.debug(`Spotify polling started with interval: ${intervalMs}ms.`)
this.pollInterval = setInterval(
() => this.getCurrentlyPlaying(),
intervalMs
)
logger.debug('Spotify polling started.')
}

public stopPolling() {
Expand Down
5 changes: 2 additions & 3 deletions tests/unit/spotifyPolling.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ describe('SpotifyPolling Service', () => {
// Mock environment variables
process.env.SPOTIFY_CLIENT_ID = 'test_client_id'
process.env.SPOTIFY_CLIENT_SECRET = 'test_client_secret'
process.env.SPOTIFY_POLLING_INTERVAL_MS = '100' // Use a short interval for testing
process.env.SPOTIFY_DEBUG = 'false' // Disable debug logging in tests

// Initialize the service and await its creation, which includes SDK setup
Expand Down Expand Up @@ -290,7 +289,7 @@ describe('SpotifyPolling Service', () => {
Promise.resolve(mockPlayback)
)

spotifyService.startPolling()
spotifyService.startPolling(100)
jest.advanceTimersByTime(150)
await Promise.resolve()
await Promise.resolve()
Expand All @@ -306,7 +305,7 @@ describe('SpotifyPolling Service', () => {
Promise.resolve(null)
)

spotifyService.startPolling()
spotifyService.startPolling(100)
jest.advanceTimersByTime(150)
await Promise.resolve()
await Promise.resolve()
Expand Down
Loading