Files
soroban-abacus-flashcards/apps/web/scripts/cleanupVisionRecordings.ts
Thomas Hallock e2816ae88b feat(vision): improve remote camera calibration UX
- Add dual-stream calibration: phone sends both raw and cropped preview
  frames during calibration so users can see what practice will look like
- Add "Adjust" button to modify existing manual calibration without
  resetting to auto-detection first
- Hide calibration quad editor overlay when not in calibration mode
- Fix rotation buttons to update cropped preview immediately
- Add rate limiting (10fps) for cropped preview frames during calibration
- Fix multiple bugs preventing dual-stream mode from working:
  - Don't mark calibration as complete during preview mode
  - Don't stop detection loop when receiving preview calibration
  - Sync refs properly in frame mode change effects

Also includes accumulated formatting and cleanup changes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 10:51:59 -06:00

117 lines
3.1 KiB
TypeScript

#!/usr/bin/env npx tsx
/**
* Cleanup script for expired vision recordings
*
* Deletes recordings that have passed their expiresAt timestamp.
* Should be run periodically via cron job (e.g., daily).
*
* Usage:
* npx tsx scripts/cleanupVisionRecordings.ts
*
* Options:
* --dry-run Show what would be deleted without actually deleting
* --verbose Show detailed output
*/
import { eq, lt } from 'drizzle-orm'
import { rm } from 'fs/promises'
import path from 'path'
import { db } from '../src/db'
import { visionRecordings } from '../src/db/schema/vision-recordings'
async function main() {
const args = process.argv.slice(2)
const dryRun = args.includes('--dry-run')
const verbose = args.includes('--verbose')
console.log('='.repeat(60))
console.log('Vision Recording Cleanup')
console.log('='.repeat(60))
console.log(`Mode: ${dryRun ? 'DRY RUN' : 'LIVE'}`)
console.log(`Time: ${new Date().toISOString()}`)
console.log('')
const now = new Date()
// Find expired recordings
const expiredRecordings = await db.query.visionRecordings.findMany({
where: lt(visionRecordings.expiresAt, now),
})
console.log(`Found ${expiredRecordings.length} expired recording(s)`)
if (expiredRecordings.length === 0) {
console.log('Nothing to clean up.')
return
}
let deletedCount = 0
let errorCount = 0
let totalSizeBytes = 0
for (const recording of expiredRecordings) {
const recordingDir = path.join(
process.cwd(),
'data',
'uploads',
'vision-recordings',
recording.playerId,
recording.id
)
if (verbose) {
console.log(`\nProcessing: ${recording.id}`)
console.log(` Session: ${recording.sessionId}`)
console.log(` Player: ${recording.playerId}`)
console.log(` Expired: ${recording.expiresAt.toISOString()}`)
console.log(
` Size: ${recording.fileSize ? `${(recording.fileSize / 1024 / 1024).toFixed(2)} MB` : 'unknown'}`
)
console.log(` Path: ${recordingDir}`)
}
if (recording.fileSize) {
totalSizeBytes += recording.fileSize
}
if (dryRun) {
console.log(`[DRY RUN] Would delete: ${recording.id}`)
deletedCount++
continue
}
try {
// Delete files from disk
await rm(recordingDir, { recursive: true, force: true })
// Delete database record
await db.delete(visionRecordings).where(eq(visionRecordings.id, recording.id))
deletedCount++
console.log(`Deleted: ${recording.id}`)
} catch (error) {
errorCount++
console.error(`Error deleting ${recording.id}:`, error)
}
}
console.log('')
console.log('-'.repeat(60))
console.log('Summary:')
console.log(` Total expired: ${expiredRecordings.length}`)
console.log(` Deleted: ${deletedCount}`)
console.log(` Errors: ${errorCount}`)
console.log(` Space recovered: ${(totalSizeBytes / 1024 / 1024).toFixed(2)} MB`)
console.log('')
if (dryRun) {
console.log('NOTE: This was a dry run. No files were actually deleted.')
console.log('Run without --dry-run to perform actual cleanup.')
}
}
main().catch((error) => {
console.error('Fatal error:', error)
process.exit(1)
})