chore: cleanup unused imports and apply formatting
- Remove unused `and` import from VisionRecorder.ts - Remove unused `IncomingMessage` and `ws` imports from socket-server.ts - Add `muted` attribute to video element in ProblemVideoPlayer - Apply code formatting across vision and practice components - Update documentation formatting in DEPLOYMENT.md and README Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
cecd1e93e2
commit
e703e90875
|
|
@ -52,6 +52,7 @@ curl https://abaci.one/api/health
|
||||||
```
|
```
|
||||||
|
|
||||||
Response:
|
Response:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"status": "healthy",
|
"status": "healthy",
|
||||||
|
|
@ -173,11 +174,13 @@ ssh nas.home.network "cd /volume1/homes/antialias/projects/abaci.one && docker-c
|
||||||
### Health Check Failing
|
### Health Check Failing
|
||||||
|
|
||||||
1. Check container logs:
|
1. Check container logs:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ssh nas.home.network "docker logs abaci-blue"
|
ssh nas.home.network "docker logs abaci-blue"
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Test health endpoint manually:
|
2. Test health endpoint manually:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ssh nas.home.network "docker exec abaci-blue curl -sf http://localhost:3000/api/health"
|
ssh nas.home.network "docker exec abaci-blue curl -sf http://localhost:3000/api/health"
|
||||||
```
|
```
|
||||||
|
|
@ -209,6 +212,7 @@ If upgrading from the old single-container setup:
|
||||||
```
|
```
|
||||||
|
|
||||||
This script will:
|
This script will:
|
||||||
|
|
||||||
1. Stop the old `soroban-abacus-flashcards` container
|
1. Stop the old `soroban-abacus-flashcards` container
|
||||||
2. Stop compose-updater temporarily
|
2. Stop compose-updater temporarily
|
||||||
3. Deploy the new docker-compose.yaml
|
3. Deploy the new docker-compose.yaml
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ curl https://abaci.one/api/health
|
||||||
```
|
```
|
||||||
|
|
||||||
Response:
|
Response:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"status": "healthy",
|
"status": "healthy",
|
||||||
|
|
@ -174,11 +175,13 @@ git rev-parse HEAD
|
||||||
### Health Check Failing
|
### Health Check Failing
|
||||||
|
|
||||||
1. Check container logs:
|
1. Check container logs:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ssh nas.home.network "docker logs abaci-blue"
|
ssh nas.home.network "docker logs abaci-blue"
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Test health endpoint manually:
|
2. Test health endpoint manually:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ssh nas.home.network "docker exec abaci-blue curl -sf http://localhost:3000/api/health"
|
ssh nas.home.network "docker exec abaci-blue curl -sf http://localhost:3000/api/health"
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,9 @@ export async function GET(request: Request, { params }: RouteParams) {
|
||||||
|
|
||||||
if (video.status === 'no_video') {
|
if (video.status === 'no_video') {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'No video was recorded for this problem (camera may have been off)' },
|
{
|
||||||
|
error: 'No video was recorded for this problem (camera may have been off)',
|
||||||
|
},
|
||||||
{ status: 404 }
|
{ status: 404 }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -582,7 +582,10 @@ export function PracticeClient({ studentId, player, initialSession }: PracticeCl
|
||||||
// Handle part transition complete - called when transition screen finishes
|
// Handle part transition complete - called when transition screen finishes
|
||||||
// This is where we trigger game break (after "put away abacus" message is shown)
|
// This is where we trigger game break (after "put away abacus" message is shown)
|
||||||
const handlePartTransitionComplete = useCallback(() => {
|
const handlePartTransitionComplete = useCallback(() => {
|
||||||
console.log('[PracticeClient] handlePartTransitionComplete called, pendingGameBreak:', pendingGameBreak)
|
console.log(
|
||||||
|
'[PracticeClient] handlePartTransitionComplete called, pendingGameBreak:',
|
||||||
|
pendingGameBreak
|
||||||
|
)
|
||||||
// First, broadcast to observers
|
// First, broadcast to observers
|
||||||
sendPartTransitionComplete()
|
sendPartTransitionComplete()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -247,7 +247,13 @@ export function BroadcastDebugPanel({
|
||||||
border: '1px solid rgba(96, 165, 250, 0.3)',
|
border: '1px solid rgba(96, 165, 250, 0.3)',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className={css({ color: '#60a5fa', fontWeight: 'bold', marginBottom: '6px' })}>
|
<div
|
||||||
|
className={css({
|
||||||
|
color: '#60a5fa',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginBottom: '6px',
|
||||||
|
})}
|
||||||
|
>
|
||||||
Current State
|
Current State
|
||||||
</div>
|
</div>
|
||||||
<div className={css({ color: '#d1d5db', fontSize: '10px' })}>
|
<div className={css({ color: '#d1d5db', fontSize: '10px' })}>
|
||||||
|
|
@ -275,7 +281,13 @@ export function BroadcastDebugPanel({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{lastBroadcastTime && (
|
{lastBroadcastTime && (
|
||||||
<div className={css({ color: '#6b7280', fontSize: '10px', marginBottom: '8px' })}>
|
<div
|
||||||
|
className={css({
|
||||||
|
color: '#6b7280',
|
||||||
|
fontSize: '10px',
|
||||||
|
marginBottom: '8px',
|
||||||
|
})}
|
||||||
|
>
|
||||||
Last broadcast: {new Date(lastBroadcastTime).toLocaleTimeString()}
|
Last broadcast: {new Date(lastBroadcastTime).toLocaleTimeString()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -283,7 +283,13 @@ export function ObserverDebugPanel({
|
||||||
border: '1px solid rgba(244, 114, 182, 0.3)',
|
border: '1px solid rgba(244, 114, 182, 0.3)',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className={css({ color: '#f472b6', fontWeight: 'bold', marginBottom: '6px' })}>
|
<div
|
||||||
|
className={css({
|
||||||
|
color: '#f472b6',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginBottom: '6px',
|
||||||
|
})}
|
||||||
|
>
|
||||||
Observed State
|
Observed State
|
||||||
</div>
|
</div>
|
||||||
<div className={css({ color: '#d1d5db', fontSize: '10px' })}>
|
<div className={css({ color: '#d1d5db', fontSize: '10px' })}>
|
||||||
|
|
@ -325,7 +331,13 @@ export function ObserverDebugPanel({
|
||||||
border: '1px solid rgba(96, 165, 250, 0.3)',
|
border: '1px solid rgba(96, 165, 250, 0.3)',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className={css({ color: '#60a5fa', fontWeight: 'bold', marginBottom: '6px' })}>
|
<div
|
||||||
|
className={css({
|
||||||
|
color: '#60a5fa',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginBottom: '6px',
|
||||||
|
})}
|
||||||
|
>
|
||||||
Vision Frame
|
Vision Frame
|
||||||
</div>
|
</div>
|
||||||
<div className={css({ color: '#d1d5db', fontSize: '10px' })}>
|
<div className={css({ color: '#d1d5db', fontSize: '10px' })}>
|
||||||
|
|
@ -349,7 +361,13 @@ export function ObserverDebugPanel({
|
||||||
border: '1px solid rgba(168, 85, 247, 0.3)',
|
border: '1px solid rgba(168, 85, 247, 0.3)',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className={css({ color: '#a855f7', fontWeight: 'bold', marginBottom: '6px' })}>
|
<div
|
||||||
|
className={css({
|
||||||
|
color: '#a855f7',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginBottom: '6px',
|
||||||
|
})}
|
||||||
|
>
|
||||||
DVR Buffer
|
DVR Buffer
|
||||||
</div>
|
</div>
|
||||||
<div className={css({ color: '#d1d5db', fontSize: '10px' })}>
|
<div className={css({ color: '#d1d5db', fontSize: '10px' })}>
|
||||||
|
|
|
||||||
|
|
@ -680,7 +680,10 @@ export function useInteractionPhase(
|
||||||
|
|
||||||
// If no active problem, nothing to do
|
// If no active problem, nothing to do
|
||||||
if (!activeProblem) {
|
if (!activeProblem) {
|
||||||
console.log('[useInteractionPhase] No active problem - cannot load. Current phase:', phase.phase)
|
console.log(
|
||||||
|
'[useInteractionPhase] No active problem - cannot load. Current phase:',
|
||||||
|
phase.phase
|
||||||
|
)
|
||||||
prevActiveProblemKeyRef.current = null
|
prevActiveProblemKeyRef.current = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -718,7 +721,10 @@ export function useInteractionPhase(
|
||||||
|
|
||||||
// Case 2: Key changed - handle redo mode or session advancement
|
// Case 2: Key changed - handle redo mode or session advancement
|
||||||
if (keyChanged) {
|
if (keyChanged) {
|
||||||
console.log('[useInteractionPhase] Key changed:', { prevKey, currentKey })
|
console.log('[useInteractionPhase] Key changed:', {
|
||||||
|
prevKey,
|
||||||
|
currentKey,
|
||||||
|
})
|
||||||
|
|
||||||
// CRITICAL: Don't interrupt normal progression flow
|
// CRITICAL: Don't interrupt normal progression flow
|
||||||
// If we're in showingFeedback, submitting, or transitioning, the normal flow
|
// If we're in showingFeedback, submitting, or transitioning, the normal flow
|
||||||
|
|
|
||||||
|
|
@ -469,6 +469,7 @@ export function ProblemVideoPlayer({
|
||||||
src={videoUrl}
|
src={videoUrl}
|
||||||
controls
|
controls
|
||||||
playsInline
|
playsInline
|
||||||
|
muted
|
||||||
onCanPlay={handleCanPlay}
|
onCanPlay={handleCanPlay}
|
||||||
onTimeUpdate={handleTimeUpdate}
|
onTimeUpdate={handleTimeUpdate}
|
||||||
className={css({
|
className={css({
|
||||||
|
|
@ -839,8 +840,13 @@ export function ProblemVideoPlayer({
|
||||||
Step {noVideoPlaybackIndex + 1} of {answerProgression.length}
|
Step {noVideoPlaybackIndex + 1} of {answerProgression.length}
|
||||||
</span>
|
</span>
|
||||||
{currentNoVideoState?.originalTimestamp !== undefined && (
|
{currentNoVideoState?.originalTimestamp !== undefined && (
|
||||||
<span className={css({ color: isDark ? 'gray.600' : 'gray.300' })}>
|
<span
|
||||||
(actual: {(currentNoVideoState.originalTimestamp / 1000).toFixed(1)}s)
|
className={css({
|
||||||
|
color: isDark ? 'gray.600' : 'gray.300',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
(actual: {(currentNoVideoState.originalTimestamp / 1000).toFixed(1)}
|
||||||
|
s)
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -325,7 +325,9 @@ describe('useUserPlayers hooks', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Should have invalidated with playerKeys.all
|
// Should have invalidated with playerKeys.all
|
||||||
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: playerKeys.all })
|
expect(invalidateSpy).toHaveBeenCalledWith({
|
||||||
|
queryKey: playerKeys.all,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test('invalidates all player queries even on error', async () => {
|
test('invalidates all player queries even on error', async () => {
|
||||||
|
|
@ -349,7 +351,9 @@ describe('useUserPlayers hooks', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// onSettled runs on both success and error
|
// onSettled runs on both success and error
|
||||||
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: playerKeys.all })
|
expect(invalidateSpy).toHaveBeenCalledWith({
|
||||||
|
queryKey: playerKeys.all,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -422,7 +426,10 @@ describe('useUserPlayers hooks', () => {
|
||||||
const { result } = renderHook(() => useUpdatePlayer(), { wrapper })
|
const { result } = renderHook(() => useUpdatePlayer(), { wrapper })
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
result.current.mutate({ id: 'player-1', updates: { name: 'Updated Name' } })
|
result.current.mutate({
|
||||||
|
id: 'player-1',
|
||||||
|
updates: { name: 'Updated Name' },
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Wait for optimistic update
|
// Wait for optimistic update
|
||||||
|
|
@ -461,7 +468,10 @@ describe('useUserPlayers hooks', () => {
|
||||||
const { result } = renderHook(() => useUpdatePlayer(), { wrapper })
|
const { result } = renderHook(() => useUpdatePlayer(), { wrapper })
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
result.current.mutate({ id: 'player-1', updates: { name: 'Updated Name' } })
|
result.current.mutate({
|
||||||
|
id: 'player-1',
|
||||||
|
updates: { name: 'Updated Name' },
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
|
|
||||||
|
|
@ -406,13 +406,12 @@ export function useSessionBroadcast(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only send markers if recording is active
|
// Always send markers - server will capture metadata even without video frames
|
||||||
if (!isRecordingRef.current) {
|
// This enables playback of student answers even when camera wasn't enabled
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
socketRef.current.emit('vision-problem-marker', {
|
socketRef.current.emit('vision-problem-marker', {
|
||||||
sessionId,
|
sessionId,
|
||||||
|
playerId, // Include playerId for auto-starting metadata-only sessions
|
||||||
problemNumber,
|
problemNumber,
|
||||||
partIndex,
|
partIndex,
|
||||||
eventType,
|
eventType,
|
||||||
|
|
@ -430,7 +429,7 @@ export function useSessionBroadcast(
|
||||||
retryContext,
|
retryContext,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[sessionId]
|
[sessionId, playerId]
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ When a student practices with their camera enabled, the system records a separat
|
||||||
Problem markers are socket events that coordinate video recording with problem lifecycle:
|
Problem markers are socket events that coordinate video recording with problem lifecycle:
|
||||||
|
|
||||||
| Marker Event | When Sent | VisionRecorder Action |
|
| Marker Event | When Sent | VisionRecorder Action |
|
||||||
|--------------|-----------|----------------------|
|
| ------------------ | ----------------------------- | ------------------------------------------ |
|
||||||
| `problem-shown` | New problem appears on screen | Start buffering frames for this problem |
|
| `problem-shown` | New problem appears on screen | Start buffering frames for this problem |
|
||||||
| `answer-submitted` | Student submits their answer | Stop buffering, encode video, save to disk |
|
| `answer-submitted` | Student submits their answer | Stop buffering, encode video, save to disk |
|
||||||
| `feedback-shown` | Feedback displayed | (Reserved for future use) |
|
| `feedback-shown` | Feedback displayed | (Reserved for future use) |
|
||||||
|
|
@ -68,27 +68,29 @@ Problem markers are socket events that coordinate video recording with problem l
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface ProblemMarker {
|
interface ProblemMarker {
|
||||||
sessionId: string
|
sessionId: string;
|
||||||
problemNumber: number // 1-indexed problem number
|
problemNumber: number; // 1-indexed problem number
|
||||||
partIndex: number // Which part of the session
|
partIndex: number; // Which part of the session
|
||||||
eventType: 'problem-shown' | 'answer-submitted' | 'feedback-shown'
|
eventType: "problem-shown" | "answer-submitted" | "feedback-shown";
|
||||||
isCorrect?: boolean // Only for answer-submitted
|
isCorrect?: boolean; // Only for answer-submitted
|
||||||
|
|
||||||
// Retry context (for multiple attempts at same problem)
|
// Retry context (for multiple attempts at same problem)
|
||||||
epochNumber: number // 0 = initial pass, 1+ = retry epochs
|
epochNumber: number; // 0 = initial pass, 1+ = retry epochs
|
||||||
attemptNumber: number // Which attempt (1, 2, 3...)
|
attemptNumber: number; // Which attempt (1, 2, 3...)
|
||||||
isRetry: boolean // True if in a retry epoch
|
isRetry: boolean; // True if in a retry epoch
|
||||||
isManualRedo: boolean // True if student clicked dot to redo
|
isManualRedo: boolean; // True if student clicked dot to redo
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Why Retry Context Matters
|
### Why Retry Context Matters
|
||||||
|
|
||||||
Students can attempt the same problem multiple times:
|
Students can attempt the same problem multiple times:
|
||||||
|
|
||||||
- **Epoch retries**: End-of-part retry rounds for missed problems
|
- **Epoch retries**: End-of-part retry rounds for missed problems
|
||||||
- **Manual redos**: Student clicks a completed problem dot to practice again
|
- **Manual redos**: Student clicks a completed problem dot to practice again
|
||||||
|
|
||||||
Each attempt gets its own recording. The retry context determines:
|
Each attempt gets its own recording. The retry context determines:
|
||||||
|
|
||||||
- **Filename**: `problem_001_e0_a1.mp4` (epoch 0, attempt 1) vs `problem_001_e0_a2.mp4` (attempt 2)
|
- **Filename**: `problem_001_e0_a1.mp4` (epoch 0, attempt 1) vs `problem_001_e0_a2.mp4` (attempt 2)
|
||||||
- **Database record**: Separate rows in `vision_problem_videos` with epoch/attempt fields
|
- **Database record**: Separate rows in `vision_problem_videos` with epoch/attempt fields
|
||||||
|
|
||||||
|
|
@ -112,29 +114,29 @@ Each video has a companion `.meta.json` file with time-coded state for synchroni
|
||||||
```typescript
|
```typescript
|
||||||
interface ProblemMetadata {
|
interface ProblemMetadata {
|
||||||
problem: {
|
problem: {
|
||||||
terms: number[] // e.g., [45, -23, 12]
|
terms: number[]; // e.g., [45, -23, 12]
|
||||||
answer: number // e.g., 34
|
answer: number; // e.g., 34
|
||||||
}
|
};
|
||||||
entries: Array<{
|
entries: Array<{
|
||||||
t: number // ms from video start
|
t: number; // ms from video start
|
||||||
detectedValue: number | null // ML-detected abacus value
|
detectedValue: number | null; // ML-detected abacus value
|
||||||
confidence: number // 0-1 detection confidence
|
confidence: number; // 0-1 detection confidence
|
||||||
studentAnswer: string // Current typed answer
|
studentAnswer: string; // Current typed answer
|
||||||
phase: 'problem' | 'feedback'
|
phase: "problem" | "feedback";
|
||||||
isCorrect?: boolean
|
isCorrect?: boolean;
|
||||||
}>
|
}>;
|
||||||
durationMs: number
|
durationMs: number;
|
||||||
frameCount: number
|
frameCount: number;
|
||||||
isCorrect: boolean | null
|
isCorrect: boolean | null;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Key Files
|
## Key Files
|
||||||
|
|
||||||
| File | Purpose |
|
| File | Purpose |
|
||||||
|------|---------|
|
| ------------------------------------------------- | ----------------------------------------------- |
|
||||||
| `src/lib/vision/recording/VisionRecorder.ts` | Server-side recording manager |
|
| `src/lib/vision/recording/VisionRecorder.ts` | Server-side recording manager |
|
||||||
| `src/socket-server.ts` | Socket event routing (lines handling vision-*) |
|
| `src/socket-server.ts` | Socket event routing (lines handling vision-\*) |
|
||||||
| `src/hooks/useSessionBroadcast.ts` | Client socket helpers (`sendProblemMarker`) |
|
| `src/hooks/useSessionBroadcast.ts` | Client socket helpers (`sendProblemMarker`) |
|
||||||
| `src/app/practice/[studentId]/PracticeClient.tsx` | Sends markers on problem state changes |
|
| `src/app/practice/[studentId]/PracticeClient.tsx` | Sends markers on problem state changes |
|
||||||
| `src/components/vision/DockedVisionFeed.tsx` | Captures and sends camera frames |
|
| `src/components/vision/DockedVisionFeed.tsx` | Captures and sends camera frames |
|
||||||
|
|
@ -143,7 +145,7 @@ interface ProblemMetadata {
|
||||||
## API Routes
|
## API Routes
|
||||||
|
|
||||||
| Route | Purpose |
|
| Route | Purpose |
|
||||||
|-------|---------|
|
| ------------------------------------------------------------------ | ------------------------- |
|
||||||
| `GET /api/curriculum/[playerId]/sessions/[sessionId]/videos` | List available recordings |
|
| `GET /api/curriculum/[playerId]/sessions/[sessionId]/videos` | List available recordings |
|
||||||
| `GET /api/.../problems/[problemNumber]/video?epoch=X&attempt=Y` | Stream video file |
|
| `GET /api/.../problems/[problemNumber]/video?epoch=X&attempt=Y` | Stream video file |
|
||||||
| `GET /api/.../problems/[problemNumber]/metadata?epoch=X&attempt=Y` | Get metadata JSON |
|
| `GET /api/.../problems/[problemNumber]/metadata?epoch=X&attempt=Y` | Get metadata JSON |
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { eq, and } from 'drizzle-orm'
|
import { eq } from 'drizzle-orm'
|
||||||
import { createId } from '@paralleldrive/cuid2'
|
import { createId } from '@paralleldrive/cuid2'
|
||||||
import { mkdir, writeFile, rm } from 'fs/promises'
|
import { mkdir, writeFile, rm } from 'fs/promises'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import type { Server as HTTPServer, IncomingMessage } from 'http'
|
import type { Server as HTTPServer } from 'http'
|
||||||
import { Server as SocketIOServer } from 'socket.io'
|
import { Server as SocketIOServer } from 'socket.io'
|
||||||
import type { Server as SocketIOServerType } from 'socket.io'
|
import type { Server as SocketIOServerType } from 'socket.io'
|
||||||
import { WebSocketServer, type WebSocket } from 'ws'
|
|
||||||
import {
|
import {
|
||||||
applyGameMove,
|
applyGameMove,
|
||||||
createArcadeSession,
|
createArcadeSession,
|
||||||
|
|
@ -1174,6 +1173,8 @@ export function initializeSocketServer(httpServer: HTTPServer) {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Vision Recording: Handle problem marker (triggers encoding on problem transitions)
|
// Vision Recording: Handle problem marker (triggers encoding on problem transitions)
|
||||||
|
// NOTE: Markers are always processed - if no recording session exists, one is auto-started
|
||||||
|
// for metadata-only capture (student answers without video)
|
||||||
socket.on(
|
socket.on(
|
||||||
'vision-problem-marker',
|
'vision-problem-marker',
|
||||||
async ({
|
async ({
|
||||||
|
|
@ -1186,6 +1187,7 @@ export function initializeSocketServer(httpServer: HTTPServer) {
|
||||||
attemptNumber,
|
attemptNumber,
|
||||||
isRetry,
|
isRetry,
|
||||||
isManualRedo,
|
isManualRedo,
|
||||||
|
playerId,
|
||||||
}: {
|
}: {
|
||||||
sessionId: string
|
sessionId: string
|
||||||
problemNumber: number
|
problemNumber: number
|
||||||
|
|
@ -1196,8 +1198,19 @@ export function initializeSocketServer(httpServer: HTTPServer) {
|
||||||
attemptNumber?: number
|
attemptNumber?: number
|
||||||
isRetry?: boolean
|
isRetry?: boolean
|
||||||
isManualRedo?: boolean
|
isManualRedo?: boolean
|
||||||
|
playerId?: string
|
||||||
}) => {
|
}) => {
|
||||||
const recorder = VisionRecorder.getInstance()
|
const recorder = VisionRecorder.getInstance()
|
||||||
|
|
||||||
|
// Auto-start a metadata-only session if one doesn't exist
|
||||||
|
// This allows capturing student answers even when camera isn't enabled
|
||||||
|
if (!recorder.isRecording(sessionId) && playerId) {
|
||||||
|
console.log(
|
||||||
|
`📝 Auto-starting metadata-only recording session for ${sessionId} (no camera)`
|
||||||
|
)
|
||||||
|
recorder.startSession(sessionId, playerId)
|
||||||
|
}
|
||||||
|
|
||||||
if (recorder.isRecording(sessionId)) {
|
if (recorder.isRecording(sessionId)) {
|
||||||
// This triggers encoding when 'problem-shown' arrives for the next problem
|
// This triggers encoding when 'problem-shown' arrives for the next problem
|
||||||
await recorder.onProblemMarker(sessionId, {
|
await recorder.onProblemMarker(sessionId, {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,13 @@ services:
|
||||||
networks:
|
networks:
|
||||||
- webgateway
|
- webgateway
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/', r => process.exit(r.statusCode < 400 ? 0 : 1)).on('error', () => process.exit(1))"]
|
test:
|
||||||
|
[
|
||||||
|
"CMD",
|
||||||
|
"node",
|
||||||
|
"-e",
|
||||||
|
"require('http').get('http://localhost:3000/', r => process.exit(r.statusCode < 400 ? 0 : 1)).on('error', () => process.exit(1))",
|
||||||
|
]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,13 @@ services:
|
||||||
networks:
|
networks:
|
||||||
- webgateway
|
- webgateway
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/', r => process.exit(r.statusCode < 400 ? 0 : 1)).on('error', () => process.exit(1))"]
|
test:
|
||||||
|
[
|
||||||
|
"CMD",
|
||||||
|
"node",
|
||||||
|
"-e",
|
||||||
|
"require('http').get('http://localhost:3000/', r => process.exit(r.statusCode < 400 ? 0 : 1)).on('error', () => process.exit(1))",
|
||||||
|
]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,13 @@ x-app: &app
|
||||||
networks:
|
networks:
|
||||||
- webgateway
|
- webgateway
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/', r => process.exit(r.statusCode < 400 ? 0 : 1)).on('error', () => process.exit(1))"]
|
test:
|
||||||
|
[
|
||||||
|
"CMD",
|
||||||
|
"node",
|
||||||
|
"-e",
|
||||||
|
"require('http').get('http://localhost:3000/', r => process.exit(r.statusCode < 400 ? 0 : 1)).on('error', () => process.exit(1))",
|
||||||
|
]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue