From d1747d64e61f52807f47f865b0dc1713f6d19cde Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 02:53:44 +0530 Subject: [PATCH 01/75] fix: isolate tracking jobs and correct tracker metrics --- source app folder/dashboard/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source app folder/dashboard/package.json b/source app folder/dashboard/package.json index cecd59c..ff4866a 100644 --- a/source app folder/dashboard/package.json +++ b/source app folder/dashboard/package.json @@ -4,9 +4,9 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "concurrently --kill-others \"node server.js\" \"vite\"", + "dev": "concurrently --kill-others \"node server-safe.js\" \"vite\"", "dev:frontend": "vite", - "dev:server": "node server.js", + "dev:server": "node server-safe.js", "build": "vite build", "lint": "eslint .", "preview": "vite preview" From 150bd4ae3986f99111df651256ef543e047c0fce Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 02:54:08 +0530 Subject: [PATCH 02/75] fix: isolate tracking jobs and correct tracker metrics --- .../dashboard/server-safe-utils.js | 236 ++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 source app folder/dashboard/server-safe-utils.js diff --git a/source app folder/dashboard/server-safe-utils.js b/source app folder/dashboard/server-safe-utils.js new file mode 100644 index 0000000..1eb8f15 --- /dev/null +++ b/source app folder/dashboard/server-safe-utils.js @@ -0,0 +1,236 @@ +import { spawn } from 'child_process'; +import fs from 'fs'; +import path from 'path'; + +export function ensureDir(dirPath) { + if (!fs.existsSync(dirPath)) fs.mkdirSync(dirPath, { recursive: true }); +} + +export function safeUnlink(filePath) { + if (!filePath) return; + try { + if (fs.existsSync(filePath)) fs.unlinkSync(filePath); + } catch (error) { + console.error(`[Server] Failed to remove ${filePath}: ${error.message}`); + } +} + +export function safeRemoveDir(dirPath) { + if (!dirPath) return; + try { + fs.rmSync(dirPath, { recursive: true, force: true }); + } catch (error) { + console.error(`[Server] Failed to remove ${dirPath}: ${error.message}`); + } +} + +export function replaceFile(sourcePath, destinationPath, token) { + const tempPath = `${destinationPath}.${token}.tmp`; + fs.copyFileSync(sourcePath, tempPath); + if (fs.existsSync(destinationPath)) fs.unlinkSync(destinationPath); + fs.renameSync(tempPath, destinationPath); +} + +export function countCsvRows(csvPath) { + const content = fs.readFileSync(csvPath, 'utf8').trim(); + return content ? content.split('\n').length - 1 : 0; +} + +export function getVideoFrameCount(ffmpegPath, videoPath) { + return new Promise((resolve, reject) => { + const child = spawn(ffmpegPath, [ + '-i', videoPath, + '-map', '0:v:0', + '-f', 'null', + '-', + ]); + let stderr = ''; + child.stderr.on('data', (data) => { stderr += data.toString(); }); + child.on('close', () => { + const matches = [...stderr.matchAll(/frame=\s*(\d+)/g)]; + if (matches.length) { + resolve(parseInt(matches.at(-1)[1], 10)); + } else { + reject(new Error('Could not determine video frame count from ffmpeg')); + } + }); + child.on('error', reject); + }); +} + +export function parseTrackerSync(stdout) { + const match = stdout.match( + /TRACKER_SYNC frames_processed=(\d+) csv_rows=(\d+) expected_video_frames=(\d+) sync_ok=(true|false)/, + ); + if (!match) return null; + return { + framesProcessed: parseInt(match[1], 10), + csvRows: parseInt(match[2], 10), + expectedVideoFrames: parseInt(match[3], 10), + syncOk: match[4] === 'true', + }; +} + +export function readJson(filePath, fallback) { + if (!fs.existsSync(filePath)) return fallback; + try { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); + } catch { + return fallback; + } +} + +export function writeJson(filePath, data) { + fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); +} + +export function readEventsFps(eventsPath) { + const data = readJson(eventsPath, null); + return data && typeof data.fps === 'number' ? data.fps : null; +} + +export function readCsvAvgProximity(csvPath) { + if (!fs.existsSync(csvPath)) return null; + try { + const lines = fs.readFileSync(csvPath, 'utf8').trim().split('\n'); + if (lines.length < 2) return null; + const header = lines[0].split(','); + const proxIdx = header.indexOf('proximity_distance'); + const occIdx = header.indexOf('occlusion_flag'); + if (proxIdx < 0) return null; + + let sum = 0; + let count = 0; + for (let i = 1; i < lines.length; i += 1) { + const cols = lines[i].split(','); + const value = parseFloat(cols[proxIdx]); + const occluded = occIdx >= 0 && parseInt(cols[occIdx], 10) === 1; + if (Number.isFinite(value) && !occluded) { + sum += value; + count += 1; + } + } + return count ? Math.round((sum / count) * 100) / 100 : null; + } catch { + return null; + } +} + +export function countCourtshipBouts(eventsPath) { + const data = readJson(eventsPath, { events: [] }); + return Array.isArray(data.events) + ? data.events.filter((event) => event.type === 'courtship_bout').length + : 0; +} + +export function snapshotRunToHistory({ + historyDir, + historyMetaPath, + runId, + filename, + durationSec, + fps, + totalFrames, + csvSrc, + eventsSrc, +}) { + if (!fs.existsSync(csvSrc)) return null; + + const stampedId = `run-${Date.now()}-${String(runId).padStart(4, '0')}`; + const runDir = path.join(historyDir, stampedId); + ensureDir(runDir); + fs.copyFileSync(csvSrc, path.join(runDir, 'data.csv')); + if (fs.existsSync(eventsSrc)) { + fs.copyFileSync(eventsSrc, path.join(runDir, 'events.json')); + } + + const meta = { + runId: stampedId, + timestamp: new Date().toISOString(), + filename: filename || 'unknown', + durationSec: Math.round((durationSec || 0) * 10) / 10, + fps: fps || null, + totalFrames: totalFrames || null, + avgProximity: readCsvAvgProximity(csvSrc), + detectedBouts: countCourtshipBouts(eventsSrc), + }; + + const history = readJson(historyMetaPath, { version: 1, runs: [] }); + if (!Array.isArray(history.runs)) history.runs = []; + history.runs.unshift(meta); + writeJson(historyMetaPath, history); + return meta; +} + +export function transcodeVideo(ffmpegPath, rawPath, finalPath) { + return new Promise((resolve, reject) => { + const child = spawn(ffmpegPath, [ + '-i', rawPath, + '-vcodec', 'libx264', + '-preset', 'veryfast', + '-pix_fmt', 'yuv420p', + '-movflags', '+faststart', + '-an', + '-y', + finalPath, + ]); + let stderr = ''; + child.stderr.on('data', (data) => { stderr += data.toString(); }); + child.on('close', (code) => { + if (code === 0) resolve(); + else reject(new Error( + `ffmpeg transcode failed (exit ${code}): ${stderr.slice(-500)}`, + )); + }); + child.on('error', (error) => reject(new Error( + `Failed to start ffmpeg: ${error.message}`, + ))); + }); +} + +export function buildTrackerArgs( + trackerScript, + inputPath, + outputVideo, + outputCsv, + outputEvents, + overrides, + defaults, +) { + const numberOr = (value, fallback) => { + const parsed = Number(value); + return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback; + }; + const integerOr = (value, fallback, minimum = 0) => ( + Math.max(minimum, Math.floor(numberOr(value, fallback))) + ); + + const config = { + minArea: integerOr(overrides.minArea, defaults.minArea), + maxArea: integerOr(overrides.maxArea, defaults.maxArea), + proximityThreshold: numberOr( + overrides.proximityThreshold, + defaults.proximityThreshold, + ), + boutMinFrames: integerOr( + overrides.boutMinFrames, + defaults.boutMinFrames, + 1, + ), + }; + if (config.maxArea > 0 && config.maxArea <= config.minArea) { + config.maxArea = defaults.maxArea; + } + + return [ + trackerScript, + '--input', inputPath, + '--output-video', outputVideo, + '--output-csv', outputCsv, + '--output-events', outputEvents, + '--min-area', String(config.minArea), + '--max-area', String(config.maxArea), + '--proximity-threshold', String(config.proximityThreshold), + '--bout-min-frames', String(config.boutMinFrames), + ]; +} From 75449eb40f6e22a70a499639043144b604abce44 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 02:54:35 +0530 Subject: [PATCH 03/75] fix: isolate tracking jobs and correct tracker metrics --- source app folder/tracker/tracker_safe.py | 261 ++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 source app folder/tracker/tracker_safe.py diff --git a/source app folder/tracker/tracker_safe.py b/source app folder/tracker/tracker_safe.py new file mode 100644 index 0000000..613679e --- /dev/null +++ b/source app folder/tracker/tracker_safe.py @@ -0,0 +1,261 @@ +"""Safety wrapper around tracker.py. + +It preserves the existing tracker implementation while fixing: +- invalid/zero FPS passed to VideoWriter, +- the initialization-frame velocity spike, +- event detection treating numeric zero as a missing value. +""" + +import argparse +import csv +import json +import os +import runpy +import sys + +import cv2 + + +def parse_wrapper_args(): + parser = argparse.ArgumentParser(add_help=False) + parser.add_argument("--input") + parser.add_argument("--output-csv", default="fly_tracking_data.csv") + parser.add_argument("--output-events") + parser.add_argument("--proximity-threshold", type=float, default=60.0) + parser.add_argument("--bout-min-frames", type=int, default=90) + return parser.parse_known_args()[0] + + +def install_safe_video_writer(): + original_writer = cv2.VideoWriter + + def safe_writer(filename, fourcc, fps, frame_size, *args): + effective_fps = fps if fps and fps > 0 else 30.0 + writer = original_writer( + filename, + fourcc, + effective_fps, + frame_size, + *args, + ) + if not writer.isOpened(): + raise RuntimeError( + f"Could not create output video at {filename}" + ) + return writer + + cv2.VideoWriter = safe_writer + + +def load_csv_rows(csv_path): + if not os.path.exists(csv_path): + return [], [] + with open(csv_path, "r", encoding="utf-8", newline="") as handle: + reader = csv.DictReader(handle) + return reader.fieldnames or [], list(reader) + + +def save_csv_rows(csv_path, fieldnames, rows): + with open(csv_path, "w", encoding="utf-8", newline="") as handle: + writer = csv.DictWriter(handle, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(rows) + + +def numeric(row, key, default): + value = row.get(key) + if value in (None, ""): + return default + try: + return float(value) + except (TypeError, ValueError): + return default + + +def fix_initial_velocity(csv_path): + fieldnames, rows = load_csv_rows(csv_path) + if not rows: + return rows + + for row in rows: + detected = ( + numeric(row, "fly1_area", 0.0) > 0 + or numeric(row, "fly2_area", 0.0) > 0 + ) + if detected: + for key in ( + "fly1_speed", + "fly2_speed", + "fly1_speed_pxsec", + "fly2_speed_pxsec", + ): + if key in row: + row[key] = "0.0" + break + + save_csv_rows(csv_path, fieldnames, rows) + return rows + + +def sustained_segments(rows, condition, minimum_frames): + segments = [] + start = None + + for row in rows: + frame = int(float(row["frame"])) + if condition(row): + if start is None: + start = frame + elif start is not None: + end = frame - 1 + if end - start + 1 >= minimum_frames: + segments.append((start, end)) + start = None + + if start is not None and rows: + end = int(float(rows[-1]["frame"])) + if end - start + 1 >= minimum_frames: + segments.append((start, end)) + + return segments + + +def event_record(event_id, event_type, start, end, fps, segment, reason): + duration_frames = end - start + 1 + proximities = [numeric(row, "proximity_distance", 0.0) for row in segment] + confidences = [numeric(row, "identity_confidence", 0.0) for row in segment] + occlusions = [numeric(row, "occlusion_flag", 0.0) for row in segment] + + return { + "id": event_id, + "type": event_type, + "start_frame": start, + "end_frame": end, + "start_time_sec": round(start / fps, 3), + "end_time_sec": round(end / fps, 3), + "duration_sec": round(duration_frames / fps, 3), + "mean_proximity_px": round( + sum(proximities) / len(proximities), + 2, + ) if proximities else 0.0, + "min_identity_confidence": round( + min(confidences), + 4, + ) if confidences else 0.0, + "occlusion_fraction": round( + sum(occlusions) / len(occlusions), + 4, + ) if occlusions else 0.0, + "detection_reason": reason, + } + + +def read_effective_fps(events_path, input_path): + if events_path and os.path.exists(events_path): + try: + with open(events_path, "r", encoding="utf-8") as handle: + fps = float(json.load(handle).get("fps", 0)) + if fps > 0: + return fps + except (OSError, ValueError, TypeError, json.JSONDecodeError): + pass + + capture = cv2.VideoCapture(input_path) if input_path else None + try: + fps = capture.get(cv2.CAP_PROP_FPS) if capture else 0 + return fps if fps and fps > 0 else 30.0 + finally: + if capture: + capture.release() + + +def regenerate_events(rows, args): + if not args.output_events: + return + + fps = read_effective_fps(args.output_events, args.input) + events = [] + counter = 1 + + courtship = sustained_segments( + rows, + lambda row: ( + numeric(row, "proximity_distance", 999.0) + < args.proximity_threshold + ), + max(1, args.bout_min_frames), + ) + for start, end in courtship: + segment = [ + row for row in rows + if start <= int(float(row["frame"])) <= end + ] + events.append(event_record( + f"evt-{counter:03d}", + "courtship_bout", + start, + end, + fps, + segment, + "proximity_sustained", + )) + counter += 1 + + low_confidence = sustained_segments( + rows, + lambda row: numeric(row, "identity_confidence", 1.0) < 0.2, + 30, + ) + for start, end in low_confidence: + segment = [ + row for row in rows + if start <= int(float(row["frame"])) <= end + ] + events.append(event_record( + f"evt-{counter:03d}", + "low_confidence_segment", + start, + end, + fps, + segment, + "identity_confidence_low", + )) + counter += 1 + + events.sort(key=lambda event: event["start_frame"]) + envelope = { + "version": 1, + "fps": round(fps, 3), + "total_frames": len(rows), + "detection_params": { + "proximity_threshold_px": args.proximity_threshold, + "bout_min_frames": max(1, args.bout_min_frames), + "low_confidence_threshold": 0.2, + "low_confidence_min_frames": 30, + }, + "events": events, + } + with open(args.output_events, "w", encoding="utf-8") as handle: + json.dump(envelope, handle, indent=2) + + +def main(): + args = parse_wrapper_args() + install_safe_video_writer() + tracker_path = os.path.join(os.path.dirname(__file__), "tracker.py") + + exit_code = 0 + try: + runpy.run_path(tracker_path, run_name="__main__") + except SystemExit as exc: + exit_code = int(exc.code or 0) + + if exit_code == 0: + rows = fix_initial_velocity(args.output_csv) + regenerate_events(rows, args) + + raise SystemExit(exit_code) + + +if __name__ == "__main__": + main() From 08c8c1f3cce5239db49e76839f8c8439074acf6f Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 02:55:09 +0530 Subject: [PATCH 04/75] fix: isolate tracking jobs and correct tracker metrics --- source app folder/dashboard/server-safe.js | 425 +++++++++++++++++++++ 1 file changed, 425 insertions(+) create mode 100644 source app folder/dashboard/server-safe.js diff --git a/source app folder/dashboard/server-safe.js b/source app folder/dashboard/server-safe.js new file mode 100644 index 0000000..a516dba --- /dev/null +++ b/source app folder/dashboard/server-safe.js @@ -0,0 +1,425 @@ +import express from 'express'; +import multer from 'multer'; +import { spawn } from 'child_process'; +import path from 'path'; +import fs from 'fs'; +import { fileURLToPath } from 'url'; +import ffmpegPath from 'ffmpeg-static'; +import { + buildTrackerArgs, + countCsvRows, + ensureDir, + getVideoFrameCount, + parseTrackerSync, + readEventsFps, + readJson, + replaceFile, + safeRemoveDir, + safeUnlink, + snapshotRunToHistory, + transcodeVideo, + writeJson, +} from './server-safe-utils.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const app = express(); +const PORT = 3001; + +const uploadsDir = path.join(__dirname, 'uploads'); +const publicDir = path.join(__dirname, 'public'); +const historyDir = path.join(publicDir, 'history'); +const historyMetaPath = path.join(publicDir, 'history.json'); +const eventsPath = path.join(publicDir, 'events.json'); +const verificationPath = path.join(publicDir, 'verification.json'); +const runMetadataPath = path.join(publicDir, 'run_metadata.json'); +const trackerDir = path.join(__dirname, '..', 'tracker'); +const trackerScript = path.join(trackerDir, 'tracker_safe.py'); +const isWindows = process.platform === 'win32'; +const pythonExe = path.join( + trackerDir, + 'venv', + isWindows ? 'Scripts' : 'bin', + isWindows ? 'python.exe' : 'python', +); + +for (const dir of [uploadsDir, publicDir, historyDir]) ensureDir(dir); + +const defaults = { + minArea: 30, + maxArea: 0, + proximityThreshold: 60, + boutMinFrames: 90, +}; + +const storage = multer.diskStorage({ + destination: (req, file, callback) => callback(null, uploadsDir), + filename: (req, file, callback) => { + const extension = path.extname(file.originalname).toLowerCase(); + callback(null, `input_${Date.now()}_${process.pid}${extension}`); + }, +}); +const upload = multer({ + storage, + limits: { fileSize: 10 * 1024 * 1024 * 1024 }, +}); + +app.use(express.json()); +app.use((req, res, next) => { + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS'); + res.header('Access-Control-Allow-Headers', 'Content-Type'); + if (req.method === 'OPTIONS') return res.sendStatus(204); + return next(); +}); + +let runSequence = 0; +let epoch = 0; +let uploadReserved = false; +let activeTracker = null; + +const blankJob = () => ({ + runId: null, + status: 'idle', + progress: '', + framesProcessed: 0, + totalFrames: null, + frameCount: null, + error: null, + startTime: null, + endTime: null, +}); +let job = blankJob(); + +const isBusy = () => ( + uploadReserved + || job.status === 'uploading' + || job.status === 'processing' +); +const isCurrent = (context) => ( + context.epoch === epoch + && context.runId === job.runId +); + +function resetJob() { + epoch += 1; + if (activeTracker) { + try { activeTracker.kill(); } catch { /* already exited */ } + activeTracker = null; + } + job = blankJob(); +} + +function reserveUpload(req, res, next) { + if (isBusy()) { + return res.status(409).json({ + error: 'A video is already being uploaded or processed. Please wait.', + }); + } + + uploadReserved = true; + let released = false; + const release = () => { + if (!released) { + released = true; + uploadReserved = false; + } + }; + res.once('finish', release); + res.once('close', release); + return next(); +} + +function readVerification() { + const data = readJson(verificationPath, { version: 1, reviews: [] }); + return Array.isArray(data.reviews) + ? data + : { version: 1, reviews: [] }; +} + +function resetVerification() { + writeJson(verificationPath, { version: 1, reviews: [] }); +} + +function startTracking(inputPath, overrides) { + const context = { + runId: job.runId, + epoch, + startTime: Date.now(), + stdout: '', + filename: job.uploadedFilename, + }; + const token = `${context.runId}-${context.epoch}`; + const workDir = path.join(uploadsDir, `run-${token}`); + ensureDir(workDir); + + const rawVideo = path.join(workDir, 'tracked_raw.mp4'); + const browserVideo = path.join(workDir, 'tracked.mp4'); + const csvPath = path.join(workDir, 'data.csv'); + const runEventsPath = path.join(workDir, 'events.json'); + + job.status = 'processing'; + job.progress = 'Initializing tracker pipeline...'; + job.framesProcessed = 0; + job.startTime = context.startTime; + job.error = null; + job.overrides = overrides; + + const tracker = spawn( + pythonExe, + buildTrackerArgs( + trackerScript, + inputPath, + rawVideo, + csvPath, + runEventsPath, + overrides, + defaults, + ), + ); + activeTracker = tracker; + + const cleanup = () => { + safeUnlink(inputPath); + safeRemoveDir(workDir); + }; + + tracker.stdout.on('data', (chunk) => { + const raw = chunk.toString(); + context.stdout += raw; + const message = raw.trim(); + console.log(`[Tracker] ${message}`); + if (!isCurrent(context)) return; + + const progress = message.match(/Processed (\d+) frames/); + if (progress) { + job.framesProcessed = parseInt(progress[1], 10); + job.progress = `Processed ${job.framesProcessed} frames...`; + } else if (message.includes('Tracking completed')) { + job.progress = 'Finalizing output files...'; + } else if (message) { + job.progress = message; + } + }); + + tracker.stderr.on('data', (chunk) => { + const raw = chunk.toString(); + context.stdout += raw; + const message = raw.trim(); + if (message.includes('NAL unit') || message.includes('partial file')) { + console.log(`[Tracker] (codec warning) ${message}`); + } else { + console.error(`[Tracker] ${message}`); + } + }); + + tracker.on('error', (error) => { + if (activeTracker === tracker) activeTracker = null; + if (isCurrent(context)) { + job.status = 'error'; + job.error = `Failed to start tracker: ${error.message}`; + job.progress = ''; + } + cleanup(); + }); + + tracker.on('close', async (code) => { + if (activeTracker === tracker) activeTracker = null; + if (!isCurrent(context)) { + cleanup(); + return; + } + + const endTime = Date.now(); + const elapsed = ((endTime - context.startTime) / 1000).toFixed(1); + job.endTime = endTime; + + if (code !== 0) { + job.status = 'error'; + job.error = `Tracker exited with code ${code}`; + job.progress = ''; + cleanup(); + return; + } + + try { + const syncInfo = parseTrackerSync(context.stdout); + const csvRows = countCsvRows(csvPath); + const videoFrames = fs.existsSync(rawVideo) + ? await getVideoFrameCount(ffmpegPath, rawVideo) + : null; + + if (!isCurrent(context)) { + cleanup(); + return; + } + + const frameCount = syncInfo?.framesProcessed ?? csvRows; + const syncOk = ( + syncInfo?.syncOk !== false + && csvRows === frameCount + && (videoFrames === null || videoFrames === csvRows) + ); + if (!syncOk) { + throw new Error( + `Frame sync mismatch (csv=${csvRows}, ` + + `video=${videoFrames}, tracker=${frameCount})`, + ); + } + if (!fs.existsSync(rawVideo)) { + throw new Error('tracked_raw.mp4 missing after tracker completed'); + } + + job.progress = 'Transcoding video for browser playback...'; + await transcodeVideo(ffmpegPath, rawVideo, browserVideo); + if (!isCurrent(context)) { + cleanup(); + return; + } + + const fps = readEventsFps(runEventsPath); + const metadata = { + framesProcessed: frameCount, + csvRows, + videoFrames, + expectedVideoFrames: syncInfo?.expectedVideoFrames ?? null, + syncOk, + fps, + timestamp: new Date().toISOString(), + }; + + replaceFile(csvPath, path.join(publicDir, 'data.csv'), token); + replaceFile(runEventsPath, eventsPath, token); + replaceFile(browserVideo, path.join(publicDir, 'tracked.mp4'), token); + writeJson(runMetadataPath, metadata); + + try { + snapshotRunToHistory({ + historyDir, + historyMetaPath, + runId: context.runId, + filename: context.filename, + durationSec: parseFloat(elapsed), + fps, + totalFrames: frameCount, + csvSrc: csvPath, + eventsSrc: runEventsPath, + }); + } catch (historyError) { + console.error(`[Server] History snapshot failed: ${historyError.message}`); + } + + job.frameCount = frameCount; + job.framesProcessed = frameCount; + job.status = 'done'; + job.progress = `Tracking complete! Processed ${frameCount} frames in ${elapsed}s`; + } catch (error) { + if (isCurrent(context)) { + job.status = 'error'; + job.error = error.message; + job.progress = ''; + } + console.error(`[Server] Post-processing failed: ${error.message}`); + } finally { + cleanup(); + } + }); +} + +app.get('/api/status', (req, res) => res.json({ ...job })); + +app.post('/api/upload', reserveUpload, upload.single('video'), (req, res) => { + if (!req.file) { + return res.status(400).json({ error: 'No video file provided' }); + } + if (job.status === 'uploading' || job.status === 'processing') { + safeUnlink(req.file.path); + return res.status(409).json({ + error: 'A video is already being processed. Please wait.', + }); + } + + resetJob(); + resetVerification(); + runSequence += 1; + job.runId = runSequence; + job.status = 'uploading'; + job.progress = 'Video uploaded, starting tracker...'; + job.uploadedFilename = req.file.originalname; + + const overrides = { + minArea: req.body.minArea, + maxArea: req.body.maxArea, + proximityThreshold: req.body.proximityThreshold, + boutMinFrames: req.body.boutMinFrames, + }; + + startTracking(req.file.path, overrides); + return res.json({ + success: true, + message: 'Upload received, processing started', + runId: runSequence, + }); +}); + +app.post('/api/reset', (req, res) => { + resetJob(); + res.json({ success: true }); +}); + +app.get('/api/events', (req, res) => { + const data = readJson(eventsPath, null); + if (!data) return res.status(404).json({ error: 'No events file found.' }); + return res.json(data); +}); + +app.get('/api/verification', (req, res) => res.json(readVerification())); + +app.post('/api/verification', (req, res) => { + const { eventId, verdict } = req.body || {}; + if (!eventId || !['confirmed', 'rejected'].includes(verdict)) { + return res.status(400).json({ + error: 'Body must include eventId and verdict (confirmed|rejected)', + }); + } + + const data = readVerification(); + const review = { + event_id: eventId, + verdict, + reviewed_at: new Date().toISOString(), + }; + const index = data.reviews.findIndex((item) => item.event_id === eventId); + if (index >= 0) data.reviews[index] = review; + else data.reviews.push(review); + writeJson(verificationPath, data); + return res.json({ success: true, review }); +}); + +app.post('/api/verification/reset', (req, res) => { + resetVerification(); + res.json({ success: true }); +}); + +app.get('/api/history', (req, res) => { + const history = readJson(historyMetaPath, { version: 1, runs: [] }); + res.json(Array.isArray(history.runs) ? history : { version: 1, runs: [] }); +}); + +app.delete('/api/history', (req, res) => { + writeJson(historyMetaPath, { version: 1, runs: [] }); + res.json({ success: true }); +}); + +app.get('/api/history/:runId', (req, res) => { + const history = readJson(historyMetaPath, { runs: [] }); + const entry = history.runs?.find((run) => run.runId === req.params.runId); + if (!entry) return res.status(404).json({ error: 'Run not found' }); + return res.json(entry); +}); + +app.listen(PORT, () => { + console.log('\n 🧬 Flyt API Server (safe job runner)'); + console.log(` ➜ Running on http://localhost:${PORT}`); + console.log(` ➜ Tracker: ${trackerScript}`); +}); From aaa5b11ebfeeb12a778f8ea2c29870e8579d2144 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 03:05:12 +0530 Subject: [PATCH 05/75] fix: lint Node server entrypoints with Node globals --- source app folder/dashboard/eslint.config.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/source app folder/dashboard/eslint.config.js b/source app folder/dashboard/eslint.config.js index 4fa125d..030c2c3 100644 --- a/source app folder/dashboard/eslint.config.js +++ b/source app folder/dashboard/eslint.config.js @@ -26,4 +26,10 @@ export default defineConfig([ 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], }, }, + { + files: ['server*.js'], + languageOptions: { + globals: globals.node, + }, + }, ]) From 1f373a41229d5dcd1f8fe0aebfd30eeca990c8ba Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 03:17:10 +0530 Subject: [PATCH 06/75] refactor: restore canonical server entrypoint and add checks --- source app folder/dashboard/package.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/source app folder/dashboard/package.json b/source app folder/dashboard/package.json index ff4866a..e9dd113 100644 --- a/source app folder/dashboard/package.json +++ b/source app folder/dashboard/package.json @@ -4,11 +4,15 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "concurrently --kill-others \"node server-safe.js\" \"vite\"", + "dev": "concurrently --kill-others \"node server.js\" \"vite\"", "dev:frontend": "vite", - "dev:server": "node server-safe.js", + "dev:server": "node server.js", "build": "vite build", "lint": "eslint .", + "lint:backend": "eslint server.js server-utils.js tests", + "test": "npm run test:server", + "test:server": "node --test tests/*.test.js", + "check": "npm run lint:backend && npm run test:server && npm run build", "preview": "vite preview" }, "dependencies": { From f7edaeb5dc9634670700f66863b5a71ac068208a Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 03:17:31 +0530 Subject: [PATCH 07/75] refactor: add transactional server utilities --- source app folder/dashboard/server-utils.js | 191 ++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 source app folder/dashboard/server-utils.js diff --git a/source app folder/dashboard/server-utils.js b/source app folder/dashboard/server-utils.js new file mode 100644 index 0000000..f38cf3a --- /dev/null +++ b/source app folder/dashboard/server-utils.js @@ -0,0 +1,191 @@ +import { randomUUID } from 'crypto'; +import { spawn } from 'child_process'; +import fs from 'fs'; +import path from 'path'; + +export function ensureDir(dirPath) { + fs.mkdirSync(dirPath, { recursive: true }); +} + +export function safeUnlink(filePath) { + if (!filePath) return; + try { fs.rmSync(filePath, { force: true }); } catch (error) { + console.error(`[Server] Failed to remove ${filePath}: ${error.message}`); + } +} + +export function safeRemoveDir(dirPath) { + if (!dirPath) return; + try { fs.rmSync(dirPath, { recursive: true, force: true }); } catch (error) { + console.error(`[Server] Failed to remove ${dirPath}: ${error.message}`); + } +} + +export function readJson(filePath, fallback) { + if (!fs.existsSync(filePath)) return fallback; + try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch { return fallback; } +} + +export function writeJson(filePath, data) { + const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`; + fs.writeFileSync(tempPath, JSON.stringify(data, null, 2)); + fs.renameSync(tempPath, filePath); +} + +export function countCsvRows(csvPath) { + const content = fs.readFileSync(csvPath, 'utf8').trim(); + return content ? content.split(/\r?\n/).length - 1 : 0; +} + +export function parseTrackerSync(stdout) { + const match = stdout.match( + /TRACKER_SYNC frames_processed=(\d+) csv_rows=(\d+) expected_video_frames=(\d+) sync_ok=(true|false)/, + ); + if (!match) return null; + return { + framesProcessed: Number(match[1]), + csvRows: Number(match[2]), + expectedVideoFrames: Number(match[3]), + syncOk: match[4] === 'true', + }; +} + +export function getVideoFrameCount(ffmpegPath, videoPath) { + return new Promise((resolve, reject) => { + const child = spawn(ffmpegPath, ['-i', videoPath, '-map', '0:v:0', '-f', 'null', '-']); + let stderr = ''; + child.stderr.on('data', (data) => { stderr += data.toString(); }); + child.once('error', reject); + child.once('close', () => { + const matches = [...stderr.matchAll(/frame=\s*(\d+)/g)]; + if (!matches.length) reject(new Error('Could not determine video frame count from ffmpeg')); + else resolve(Number(matches.at(-1)[1])); + }); + }); +} + +export function transcodeVideo(ffmpegPath, rawPath, finalPath) { + return new Promise((resolve, reject) => { + const child = spawn(ffmpegPath, [ + '-i', rawPath, '-vcodec', 'libx264', '-preset', 'veryfast', + '-pix_fmt', 'yuv420p', '-movflags', '+faststart', '-an', '-y', finalPath, + ]); + let stderr = ''; + child.stderr.on('data', (data) => { stderr += data.toString(); }); + child.once('error', (error) => reject(new Error(`Failed to start ffmpeg: ${error.message}`))); + child.once('close', (code) => { + if (code === 0) resolve(); + else reject(new Error(`ffmpeg transcode failed (exit ${code}): ${stderr.slice(-500)}`)); + }); + }); +} + +export function publishBundle(entries, token) { + const staged = []; + const backups = []; + const published = []; + try { + for (const { source, destination } of entries) { + if (!fs.existsSync(source)) throw new Error(`Missing publish source: ${source}`); + const stage = `${destination}.${token}.new`; + fs.copyFileSync(source, stage); + staged.push(stage); + } + for (const { destination } of entries) { + if (fs.existsSync(destination)) { + const backup = `${destination}.${token}.bak`; + fs.renameSync(destination, backup); + backups.push({ destination, backup }); + } + } + entries.forEach(({ destination }, index) => { + fs.renameSync(staged[index], destination); + published.push(destination); + }); + backups.forEach(({ backup }) => safeUnlink(backup)); + } catch (error) { + published.forEach((destination) => safeUnlink(destination)); + backups.reverse().forEach(({ destination, backup }) => { + if (fs.existsSync(backup)) fs.renameSync(backup, destination); + }); + staged.forEach((stage) => safeUnlink(stage)); + throw error; + } +} + +export function buildTrackerArgs(trackerScript, inputPath, outputs, overrides = {}, defaults) { + const numberOr = (value, fallback) => { + const parsed = Number(value); + return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback; + }; + const integerOr = (value, fallback, minimum = 0) => ( + Math.max(minimum, Math.floor(numberOr(value, fallback))) + ); + const config = { + minArea: integerOr(overrides.minArea, defaults.minArea), + maxArea: integerOr(overrides.maxArea, defaults.maxArea), + proximityThreshold: numberOr(overrides.proximityThreshold, defaults.proximityThreshold), + boutMinFrames: integerOr(overrides.boutMinFrames, defaults.boutMinFrames, 1), + }; + if (config.maxArea > 0 && config.maxArea <= config.minArea) config.maxArea = defaults.maxArea; + return [ + trackerScript, '--input', inputPath, + '--output-video', outputs.rawVideo, + '--output-csv', outputs.csv, + '--output-events', outputs.events, + '--min-area', String(config.minArea), + '--max-area', String(config.maxArea), + '--proximity-threshold', String(config.proximityThreshold), + '--bout-min-frames', String(config.boutMinFrames), + ]; +} + +export function readEventsFps(eventsPath) { + const data = readJson(eventsPath, null); + return data && Number.isFinite(data.fps) ? data.fps : null; +} + +export function scopeEventsForHistory(eventsPath, runId, destinationPath) { + const data = readJson(eventsPath, { version: 1, events: [] }); + data.events = Array.isArray(data.events) ? data.events.map((event) => ({ + ...event, + original_id: event.original_id || event.id, + id: `${runId}:${event.original_id || event.id}`, + })) : []; + writeJson(destinationPath, data); +} + +export function snapshotRunToHistory({ + historyDir, historyMetaPath, filename, durationSec, fps, totalFrames, csvSrc, eventsSrc, +}) { + if (!fs.existsSync(csvSrc)) return null; + const runId = `run-${Date.now()}-${randomUUID().slice(0, 8)}`; + const runDir = path.join(historyDir, runId); + ensureDir(runDir); + fs.copyFileSync(csvSrc, path.join(runDir, 'data.csv')); + scopeEventsForHistory(eventsSrc, runId, path.join(runDir, 'events.json')); + writeJson(path.join(runDir, 'verification.json'), { version: 1, reviews: [] }); + const meta = { + runId, + timestamp: new Date().toISOString(), + filename: filename || 'unknown', + durationSec: Math.round((durationSec || 0) * 10) / 10, + fps: fps || null, + totalFrames: totalFrames || null, + }; + const history = readJson(historyMetaPath, { version: 1, runs: [] }); + if (!Array.isArray(history.runs)) history.runs = []; + history.runs.unshift(meta); + writeJson(historyMetaPath, history); + return meta; +} + +export function resolveVerificationPath(eventId, currentPath, historyDir, historyMetaPath) { + const separator = eventId.indexOf(':'); + if (separator < 0) return currentPath; + const runId = eventId.slice(0, separator); + const history = readJson(historyMetaPath, { runs: [] }); + const exists = Array.isArray(history.runs) && history.runs.some((run) => run.runId === runId); + if (!exists) throw new Error('Unknown historical run'); + return path.join(historyDir, runId, 'verification.json'); +} From 13a6f77d071ad5e5c746e37300d165a11c29254c Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 03:18:08 +0530 Subject: [PATCH 08/75] refactor: make safe runner the canonical server --- source app folder/dashboard/server.js | 719 +++++++++----------------- 1 file changed, 238 insertions(+), 481 deletions(-) diff --git a/source app folder/dashboard/server.js b/source app folder/dashboard/server.js index cbff4d2..65f2c04 100644 --- a/source app folder/dashboard/server.js +++ b/source app folder/dashboard/server.js @@ -1,598 +1,355 @@ +import { randomUUID } from 'crypto'; +import { spawn } from 'child_process'; import express from 'express'; +import ffmpegPath from 'ffmpeg-static'; +import fs from 'fs'; import multer from 'multer'; -import { spawn } from 'child_process'; import path from 'path'; -import fs from 'fs'; import { fileURLToPath } from 'url'; -import ffmpegPath from 'ffmpeg-static'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - +import { + buildTrackerArgs, + countCsvRows, + ensureDir, + getVideoFrameCount, + parseTrackerSync, + publishBundle, + readEventsFps, + readJson, + resolveVerificationPath, + safeRemoveDir, + safeUnlink, + snapshotRunToHistory, + transcodeVideo, + writeJson, +} from './server-utils.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); const app = express(); const PORT = 3001; - -// Configure multer for file uploads const uploadsDir = path.join(__dirname, 'uploads'); -if (!fs.existsSync(uploadsDir)) { - fs.mkdirSync(uploadsDir, { recursive: true }); -} -// History snapshots directory (created lazily on first snapshot, but ensure now -// so GET /api/history works even before any run completes). -const publicDirTmp = path.join(__dirname, 'public'); -const historyDirInit = path.join(publicDirTmp, 'history'); -if (!fs.existsSync(historyDirInit)) { - fs.mkdirSync(historyDirInit, { recursive: true }); -} - -const storage = multer.diskStorage({ - destination: (req, file, cb) => cb(null, uploadsDir), - filename: (req, file, cb) => cb(null, 'input_video' + path.extname(file.originalname)) -}); -const upload = multer({ storage, limits: { fileSize: 10 * 1024 * 1024 * 1024 } }); // 10GB limit - -// Paths +const publicDir = path.join(__dirname, 'public'); +const historyDir = path.join(publicDir, 'history'); +const historyMetaPath = path.join(publicDir, 'history.json'); +const verificationPath = path.join(publicDir, 'verification.json'); const trackerDir = path.join(__dirname, '..', 'tracker'); const trackerScript = path.join(trackerDir, 'tracker.py'); -// Platform-agnostic venv python: Windows → venv/Scripts/python.exe, POSIX → venv/bin/python const isWindows = process.platform === 'win32'; -const pythonExe = path.join(trackerDir, 'venv', isWindows ? 'Scripts' : 'bin', isWindows ? 'python.exe' : 'python'); -const publicDir = path.join(__dirname, 'public'); - -// Tracker defaults. Overridable per-run via Settings UI → /api/upload body. -// These mirror the pitch baseline; kept here so an empty Settings payload still -// produces pitch-identical output. -const TRACKER_DEFAULTS = { - minArea: 30, - maxArea: 0, - proximityThreshold: 60, - boutMinFrames: 90, -}; +const pythonExe = path.join( + trackerDir, 'venv', isWindows ? 'Scripts' : 'bin', isWindows ? 'python.exe' : 'python', +); +const defaults = { minArea: 30, maxArea: 0, proximityThreshold: 60, boutMinFrames: 90 }; +for (const dir of [uploadsDir, publicDir, historyDir]) ensureDir(dir); -const EVENTS_PATH = path.join(publicDir, 'events.json'); -const VERIFICATION_PATH = path.join(publicDir, 'verification.json'); +const storage = multer.diskStorage({ + destination: (_req, _file, callback) => callback(null, uploadsDir), + filename: (_req, file, callback) => { + callback(null, `input-${Date.now()}-${randomUUID()}${path.extname(file.originalname).toLowerCase()}`); + }, +}); +const upload = multer({ storage, limits: { fileSize: 10 * 1024 * 1024 * 1024 } }); app.use(express.json()); - -// Enable CORS for dev app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); - res.header('Access-Control-Allow-Methods', 'GET, POST'); + res.header('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Content-Type'); - next(); + if (req.method === 'OPTIONS') return res.sendStatus(204); + return next(); }); -// ============================================================ -// ASYNC JOB SYSTEM — never blocks the HTTP request -// ============================================================ -let runId = 0; - -let job = { - runId: null, - status: 'idle', // idle | uploading | processing | done | error - progress: '', // human-readable progress string - framesProcessed: 0, - totalFrames: null, - frameCount: null, // verified frame count after sync check - error: null, - startTime: null, - endTime: null, -}; - +let runSequence = 0; +let epoch = 0; +let uploadReserved = false; +let terminating = false; let activeTracker = null; -let trackerStdout = ''; - -function resetJob() { - if (activeTracker) { - try { activeTracker.kill(); } catch { /* ignore */ } - activeTracker = null; - } - trackerStdout = ''; - job = { - runId: null, - status: 'idle', - progress: '', - framesProcessed: 0, - totalFrames: null, - frameCount: null, - error: null, - startTime: null, - endTime: null, - }; -} - -function countCsvRows(csvPath) { - const content = fs.readFileSync(csvPath, 'utf8').trim(); - if (!content) return 0; - return content.split('\n').length - 1; -} - -function getVideoFrameCount(videoPath) { - return new Promise((resolve, reject) => { - // Decode to null muxer — frame= count appears in stderr (copy mode skips it). - const ffprobe = spawn(ffmpegPath, [ - '-i', videoPath, - '-map', '0:v:0', - '-f', 'null', - '-', - ]); - - let stderr = ''; - ffprobe.stderr.on('data', (data) => { stderr += data.toString(); }); - - ffprobe.on('close', () => { - const matches = [...stderr.matchAll(/frame=\s*(\d+)/g)]; - if (matches.length > 0) { - resolve(parseInt(matches[matches.length - 1][1], 10)); - return; - } - reject(new Error('Could not determine video frame count from ffmpeg')); - }); +let job = blankJob(); - ffprobe.on('error', (err) => reject(err)); - }); -} - -function parseTrackerSync(stdout) { - const match = stdout.match( - /TRACKER_SYNC frames_processed=(\d+) csv_rows=(\d+) expected_video_frames=(\d+) sync_ok=(true|false)/ - ); - if (!match) return null; +function blankJob() { return { - framesProcessed: parseInt(match[1], 10), - csvRows: parseInt(match[2], 10), - expectedVideoFrames: parseInt(match[3], 10), - syncOk: match[4] === 'true', + runId: null, status: 'idle', progress: '', framesProcessed: 0, + totalFrames: null, frameCount: null, error: null, startTime: null, endTime: null, }; } -function writeRunMetadata(metadata) { - const metadataPath = path.join(publicDir, 'run_metadata.json'); - fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2)); -} - -function readEventsFps() { - if (!fs.existsSync(EVENTS_PATH)) return null; - try { - const eventsData = JSON.parse(fs.readFileSync(EVENTS_PATH, 'utf8')); - return typeof eventsData.fps === 'number' ? eventsData.fps : null; - } catch { - return null; - } -} - -function readVerification() { - if (!fs.existsSync(VERIFICATION_PATH)) return { version: 1, reviews: [] }; - try { - const data = JSON.parse(fs.readFileSync(VERIFICATION_PATH, 'utf8')); - if (!Array.isArray(data.reviews)) return { version: 1, reviews: [] }; - return data; - } catch { - return { version: 1, reviews: [] }; - } -} - -function writeVerification(data) { - fs.writeFileSync(VERIFICATION_PATH, JSON.stringify(data, null, 2)); -} - -function resetVerification() { - writeVerification({ version: 1, reviews: [] }); -} - -// ============================================================ -// RUN HISTORY — per-run snapshots of CSV + events in public/history// -// Metadata lives in public/history.json. Tracked video is NOT snapshotted -// (disk); past-run video falls back to "not available". -// ============================================================ -const HISTORY_DIR = path.join(publicDir, 'history'); -const HISTORY_META_PATH = path.join(publicDir, 'history.json'); - -function readHistoryMeta() { - if (!fs.existsSync(HISTORY_META_PATH)) return { version: 1, runs: [] }; - try { - const data = JSON.parse(fs.readFileSync(HISTORY_META_PATH, 'utf8')); - if (!Array.isArray(data.runs)) return { version: 1, runs: [] }; - return data; - } catch { - return { version: 1, runs: [] }; - } -} - -function writeHistoryMeta(data) { - fs.writeFileSync(HISTORY_META_PATH, JSON.stringify(data, null, 2)); -} - - -function readCsvAvgProximity(csvPath) { - if (!fs.existsSync(csvPath)) return null; - try { - const lines = fs.readFileSync(csvPath, 'utf8').trim().split('\n'); - if (lines.length < 2) return null; - const header = lines[0].split(','); - const proxIdx = header.indexOf('proximity_distance'); - const occIdx = header.indexOf('occlusion_flag'); - if (proxIdx < 0) return null; - let sum = 0, count = 0; - for (let i = 1; i < lines.length; i++) { - const cols = lines[i].split(','); - const v = parseFloat(cols[proxIdx]); - const isOccluded = occIdx >= 0 ? parseInt(cols[occIdx], 10) === 1 : false; - if (Number.isFinite(v) && !isOccluded) { - sum += v; - count++; - } - } - return count > 0 ? Math.round((sum / count) * 100) / 100 : null; - } catch { - return null; - } -} - -function countCourtshipBouts(eventsPath) { - if (!fs.existsSync(eventsPath)) return 0; - try { - const data = JSON.parse(fs.readFileSync(eventsPath, 'utf8')); - if (!Array.isArray(data.events)) return 0; - return data.events.filter((e) => e.type === 'courtship_bout').length; - } catch { - return 0; - } +const isBusy = () => uploadReserved || terminating || ['uploading', 'processing'].includes(job.status); +const isCurrent = (context) => context.epoch === epoch && context.runId === job.runId; + +function reserveUpload(_req, res, next) { + if (isBusy()) return res.status(409).json({ error: 'A video is already uploading, processing, or stopping.' }); + uploadReserved = true; + let released = false; + const release = () => { if (!released) { released = true; uploadReserved = false; } }; + res.once('finish', release); + res.once('close', release); + return next(); } -// Snapshot data.csv + events.json for the just-finished run. -// Call AFTER all sync checks pass, BEFORE deleting raw video. -function snapshotRunToHistory({ runId, filename, durationSec, fps, totalFrames }) { - const csvSrc = path.join(publicDir, 'data.csv'); - const eventsSrc = EVENTS_PATH; - if (!fs.existsSync(csvSrc)) return null; - - const stampedId = `run-${String(runId).padStart(4, '0')}`; - const runDir = path.join(HISTORY_DIR, stampedId); - fs.mkdirSync(runDir, { recursive: true }); - fs.copyFileSync(csvSrc, path.join(runDir, 'data.csv')); - if (fs.existsSync(eventsSrc)) { - fs.copyFileSync(eventsSrc, path.join(runDir, 'events.json')); - } - - const meta = { - runId: stampedId, - timestamp: new Date().toISOString(), - filename: filename || 'unknown', - durationSec: Math.round((durationSec || 0) * 10) / 10, - fps: fps || null, - totalFrames: totalFrames || null, - avgProximity: readCsvAvgProximity(csvSrc), - detectedBouts: countCourtshipBouts(eventsSrc), - }; - - const history = readHistoryMeta(); - history.runs.unshift(meta); // newest first - writeHistoryMeta(history); - return meta; +function stopActiveTracker() { + epoch += 1; + const tracker = activeTracker; + activeTracker = null; + job = blankJob(); + if (!tracker) return Promise.resolve(); + terminating = true; + return new Promise((resolve) => { + let settled = false; + const finish = () => { + if (settled) return; + settled = true; + terminating = false; + resolve(); + }; + tracker.once('close', finish); + tracker.once('error', finish); + try { tracker.kill(); } catch { finish(); } + setTimeout(finish, 3000).unref(); + }); } -function transcodeVideo(rawPath, finalPath) { - return new Promise((resolve, reject) => { - const args = [ - '-i', rawPath, - '-vcodec', 'libx264', - '-preset', 'veryfast', - '-pix_fmt', 'yuv420p', - '-movflags', '+faststart', - '-an', - '-y', - finalPath, - ]; - - const ffmpeg = spawn(ffmpegPath, args); - let stderr = ''; - - ffmpeg.stderr.on('data', (data) => { - stderr += data.toString(); - }); - - ffmpeg.on('close', (code) => { - if (code === 0) { - resolve(); - } else { - reject(new Error(`ffmpeg transcode failed (exit ${code}): ${stderr.slice(-500)}`)); - } - }); - - ffmpeg.on('error', (err) => reject(new Error(`Failed to start ffmpeg: ${err.message}`))); - }); +function readVerification(filePath = verificationPath) { + const data = readJson(filePath, { version: 1, reviews: [] }); + return Array.isArray(data.reviews) ? data : { version: 1, reviews: [] }; } -function buildTrackerArgs(inputPath, outputVideoRaw, outputCsv, outputEvents, overrides = {}) { - // Parse & clamp per-run settings from the Settings UI. Empty/invalid values - // fall back to TRACKER_DEFAULTS so a missing payload still yields pitch-identical output. - const num = (v, fallback) => { - const n = Number(v); - return Number.isFinite(n) && n >= 0 ? n : fallback; +function startTracking(inputPath, overrides) { + const context = { + runId: job.runId, + epoch, + startedAt: Date.now(), + stdout: '', + filename: job.uploadedFilename, + spawnFailed: false, }; - const cfg = { - minArea: num(overrides.minArea, TRACKER_DEFAULTS.minArea), - maxArea: num(overrides.maxArea, TRACKER_DEFAULTS.maxArea), - proximityThreshold: num(overrides.proximityThreshold, TRACKER_DEFAULTS.proximityThreshold), - boutMinFrames: Math.max(1, Math.floor(num(overrides.boutMinFrames, TRACKER_DEFAULTS.boutMinFrames))), + const token = `${context.runId}-${context.epoch}-${randomUUID()}`; + const workDir = path.join(uploadsDir, `run-${token}`); + ensureDir(workDir); + const outputs = { + rawVideo: path.join(workDir, 'tracked_raw.mp4'), + browserVideo: path.join(workDir, 'tracked.mp4'), + csv: path.join(workDir, 'data.csv'), + events: path.join(workDir, 'events.json'), + metadata: path.join(workDir, 'run_metadata.json'), + verification: path.join(workDir, 'verification.json'), }; - return [ - trackerScript, - '--input', inputPath, - '--output-video', outputVideoRaw, - '--output-csv', outputCsv, - '--output-events', outputEvents, - '--min-area', String(cfg.minArea), - '--max-area', String(cfg.maxArea), - '--proximity-threshold', String(cfg.proximityThreshold), - '--bout-min-frames', String(cfg.boutMinFrames), - ]; -} - -function startTracking(inputPath, overrides = {}) { - const outputVideoRaw = path.join(publicDir, 'tracked_raw.mp4'); - const outputVideoFinal = path.join(publicDir, 'tracked.mp4'); - const outputCsv = path.join(publicDir, 'data.csv'); - const outputEvents = EVENTS_PATH; - job.status = 'processing'; - job.progress = 'Initializing tracker pipeline...'; - job.framesProcessed = 0; - job.startTime = Date.now(); - job.error = null; - trackerStdout = ''; - job.overrides = overrides; // echo back to UI for transparency - - console.log(`[Server] Starting tracker on: ${inputPath}`); - console.log(`[Server] Tracker config: ${JSON.stringify(overrides)}`); - - const tracker = spawn(pythonExe, buildTrackerArgs(inputPath, outputVideoRaw, outputCsv, outputEvents, overrides)); + job = { + ...job, + status: 'processing', + progress: 'Initializing tracker pipeline...', + framesProcessed: 0, + startTime: context.startedAt, + error: null, + overrides, + }; + const tracker = spawn( + pythonExe, + buildTrackerArgs(trackerScript, inputPath, outputs, overrides, defaults), + ); activeTracker = tracker; + const cleanup = () => { safeUnlink(inputPath); safeRemoveDir(workDir); }; - tracker.stdout.on('data', (data) => { - const msg = data.toString().trim(); - trackerStdout += data.toString(); - console.log(`[Tracker] ${msg}`); - - const match = msg.match(/Processed (\d+) frames/); + tracker.stdout.on('data', (chunk) => { + const raw = chunk.toString(); + context.stdout += raw; + if (!isCurrent(context)) return; + const match = raw.match(/Processed (\d+) frames/); if (match) { - job.framesProcessed = parseInt(match[1]); + job.framesProcessed = Number(match[1]); job.progress = `Processed ${job.framesProcessed} frames...`; - } else if (msg.includes('Tracking completed')) { + } else if (raw.includes('Tracking completed')) { job.progress = 'Finalizing output files...'; - } else { - job.progress = msg; } }); + tracker.stderr.on('data', (chunk) => { + context.stdout += chunk.toString(); + console.error(`[Tracker] ${chunk.toString().trim()}`); + }); - tracker.stderr.on('data', (data) => { - const msg = data.toString().trim(); - trackerStdout += data.toString(); - if (msg.includes('NAL unit') || msg.includes('partial file')) { - console.log(`[Tracker] (codec warning, safe to ignore) ${msg}`); - } else { - console.error(`[Tracker] ${msg}`); + tracker.once('error', (error) => { + context.spawnFailed = true; + if (activeTracker === tracker) activeTracker = null; + if (isCurrent(context)) { + job.status = 'error'; + job.error = `Failed to start tracker: ${error.message}`; + job.progress = ''; } + cleanup(); }); - tracker.on('close', async (code) => { - activeTracker = null; + tracker.once('close', async (code) => { + if (activeTracker === tracker) activeTracker = null; + if (context.spawnFailed) return; + if (!isCurrent(context)) { cleanup(); return; } job.endTime = Date.now(); - const elapsed = ((job.endTime - job.startTime) / 1000).toFixed(1); - if (code !== 0) { job.status = 'error'; job.error = `Tracker exited with code ${code}`; job.progress = ''; - console.error(`[Server] ❌ Tracker failed with code ${code}`); + cleanup(); return; } - try { - const syncInfo = parseTrackerSync(trackerStdout); - const csvRows = countCsvRows(outputCsv); - let videoFrames = null; - - if (fs.existsSync(outputVideoRaw)) { - videoFrames = await getVideoFrameCount(outputVideoRaw); - } - + const syncInfo = parseTrackerSync(context.stdout); + const csvRows = countCsvRows(outputs.csv); + const videoFrames = fs.existsSync(outputs.rawVideo) + ? await getVideoFrameCount(ffmpegPath, outputs.rawVideo) + : null; + if (!isCurrent(context)) { cleanup(); return; } const frameCount = syncInfo?.framesProcessed ?? csvRows; const syncOk = syncInfo?.syncOk !== false && csvRows === frameCount && (videoFrames === null || videoFrames === csvRows); + if (!syncOk) throw new Error( + `Frame sync mismatch (csv=${csvRows}, video=${videoFrames}, tracker=${frameCount})`, + ); + if (!fs.existsSync(outputs.rawVideo)) throw new Error('tracked_raw.mp4 missing'); - const eventsFps = readEventsFps(); + job.progress = 'Transcoding video for browser playback...'; + await transcodeVideo(ffmpegPath, outputs.rawVideo, outputs.browserVideo); + if (!isCurrent(context)) { cleanup(); return; } + + const fps = readEventsFps(outputs.events); const metadata = { framesProcessed: frameCount, csvRows, videoFrames, expectedVideoFrames: syncInfo?.expectedVideoFrames ?? null, syncOk, - fps: eventsFps, + fps, timestamp: new Date().toISOString(), }; - writeRunMetadata(metadata); - job.frameCount = frameCount; + writeJson(outputs.metadata, metadata); + writeJson(outputs.verification, { version: 1, reviews: [] }); - console.log( - `[Server] Frame sync: csv_rows=${csvRows}, video_frames=${videoFrames}, sync_ok=${syncOk}` - ); + publishBundle([ + { source: outputs.csv, destination: path.join(publicDir, 'data.csv') }, + { source: outputs.events, destination: path.join(publicDir, 'events.json') }, + { source: outputs.browserVideo, destination: path.join(publicDir, 'tracked.mp4') }, + { source: outputs.metadata, destination: path.join(publicDir, 'run_metadata.json') }, + { source: outputs.verification, destination: verificationPath }, + ], token); - if (!syncOk) { - throw new Error( - `Frame sync mismatch (csv=${csvRows}, video=${videoFrames}, tracker=${frameCount})` - ); - } - - // Snapshot this run's CSV + events into history BEFORE touching video files. try { snapshotRunToHistory({ - runId: job.runId, - filename: job.uploadedFilename, - durationSec: parseFloat(elapsed), - fps: eventsFps, + historyDir, + historyMetaPath, + filename: context.filename, + durationSec: (job.endTime - context.startedAt) / 1000, + fps, totalFrames: frameCount, + csvSrc: outputs.csv, + eventsSrc: outputs.events, }); - console.log(`[Server] 📚 Run snapshotted to history`); - } catch (histErr) { - console.error(`[Server] History snapshot failed (non-fatal): ${histErr.message}`); + } catch (historyError) { + console.error(`[Server] History snapshot failed: ${historyError.message}`); } - if (!fs.existsSync(outputVideoRaw)) { - throw new Error('tracked_raw.mp4 missing after tracker completed'); - } - - job.progress = 'Transcoding video for browser playback...'; - await transcodeVideo(outputVideoRaw, outputVideoFinal); - - fs.unlinkSync(outputVideoRaw); - console.log(`[Server] Transcoded annotated video to ${outputVideoFinal}`); - + job.frameCount = frameCount; + job.framesProcessed = frameCount; job.status = 'done'; - job.progress = `Tracking complete! Processed ${job.framesProcessed} frames in ${elapsed}s`; - console.log(`[Server] ✅ Tracking completed in ${elapsed}s`); - } catch (e) { - job.status = 'error'; - job.error = e.message; - job.progress = ''; - console.error(`[Server] ❌ Post-processing failed: ${e.message}`); - console.error('[Server] CSV may still be valid at public/data.csv'); + job.progress = `Tracking complete! Processed ${frameCount} frames.`; + } catch (error) { + if (isCurrent(context)) { + job.status = 'error'; + job.error = error.message; + job.progress = ''; + } + console.error(`[Server] Post-processing failed: ${error.message}`); + } finally { + cleanup(); } }); - - tracker.on('error', (err) => { - activeTracker = null; - job.status = 'error'; - job.error = `Failed to start tracker: ${err.message}`; - job.progress = ''; - console.error(`[Server] ❌ ${err.message}`); - }); } -// ============================================================ -// ROUTES -// ============================================================ - -app.get('/api/status', (req, res) => { - res.json({ ...job }); -}); - -app.post('/api/upload', upload.single('video'), (req, res) => { - if (!req.file) { - return res.status(400).json({ error: 'No video file provided' }); - } +app.get('/api/status', (_req, res) => res.json({ ...job })); - if (job.status === 'processing') { - return res.status(409).json({ error: 'A video is already being processed. Please wait.' }); +app.post('/api/upload', reserveUpload, upload.single('video'), async (req, res) => { + if (!req.file) return res.status(400).json({ error: 'No video file provided' }); + if (isBusy() && !uploadReserved) { + safeUnlink(req.file.path); + return res.status(409).json({ error: 'A video is already being processed.' }); } - - resetJob(); - resetVerification(); - runId += 1; - const currentRunId = runId; - job.runId = currentRunId; - job.status = 'uploading'; - job.progress = 'Video uploaded, starting tracker...'; - job.uploadedFilename = req.file.originalname; - - // Per-run Settings overrides (multipart text fields arrive as strings). - // buildTrackerArgs validates/clamps these; invalid → TRACKER_DEFAULTS. + await stopActiveTracker(); + runSequence += 1; + job = { + ...blankJob(), + runId: runSequence, + status: 'uploading', + progress: 'Video uploaded, starting tracker...', + uploadedFilename: req.file.originalname, + }; const overrides = { minArea: req.body.minArea, maxArea: req.body.maxArea, proximityThreshold: req.body.proximityThreshold, boutMinFrames: req.body.boutMinFrames, }; - - const inputPath = req.file.path; - const uploadedFilename = req.file.originalname; - console.log(`[Server] Received upload: ${uploadedFilename} (${(req.file.size / 1024 / 1024).toFixed(1)} MB)`); - - res.json({ success: true, message: 'Upload received, processing started', runId: currentRunId }); - - startTracking(inputPath, overrides); + startTracking(req.file.path, overrides); + return res.json({ success: true, runId: runSequence }); }); -app.post('/api/reset', (req, res) => { - resetJob(); +app.post('/api/reset', async (_req, res) => { + await stopActiveTracker(); res.json({ success: true }); }); -app.get('/api/events', (req, res) => { - if (!fs.existsSync(EVENTS_PATH)) { - return res.status(404).json({ error: 'No events file found. Run tracking first.' }); - } - try { - const eventsData = JSON.parse(fs.readFileSync(EVENTS_PATH, 'utf8')); - res.json(eventsData); - } catch (e) { - res.status(500).json({ error: `Failed to read events: ${e.message}` }); - } +app.get('/api/events', (_req, res) => { + const data = readJson(path.join(publicDir, 'events.json'), null); + if (!data) return res.status(404).json({ error: 'No events file found.' }); + return res.json(data); }); app.get('/api/verification', (req, res) => { - res.json(readVerification()); + if (!req.query.runId) return res.json(readVerification()); + const history = readJson(historyMetaPath, { runs: [] }); + const known = history.runs?.some((run) => run.runId === req.query.runId); + if (!known) return res.status(404).json({ error: 'Run not found' }); + return res.json(readVerification(path.join(historyDir, req.query.runId, 'verification.json'))); }); app.post('/api/verification', (req, res) => { const { eventId, verdict } = req.body || {}; if (!eventId || !['confirmed', 'rejected'].includes(verdict)) { - return res.status(400).json({ error: 'Body must include eventId and verdict (confirmed|rejected)' }); + return res.status(400).json({ error: 'Body must include eventId and verdict.' }); } - - const data = readVerification(); - const existing = data.reviews.findIndex((r) => r.event_id === eventId); - const review = { - event_id: eventId, - verdict, - reviewed_at: new Date().toISOString(), - }; - - if (existing >= 0) { - data.reviews[existing] = review; - } else { - data.reviews.push(review); + try { + const filePath = resolveVerificationPath( + eventId, verificationPath, historyDir, historyMetaPath, + ); + const data = readVerification(filePath); + const review = { event_id: eventId, verdict, reviewed_at: new Date().toISOString() }; + const index = data.reviews.findIndex((item) => item.event_id === eventId); + if (index >= 0) data.reviews[index] = review; + else data.reviews.push(review); + writeJson(filePath, data); + return res.json({ success: true, review }); + } catch (error) { + return res.status(400).json({ error: error.message }); } - - writeVerification(data); - res.json({ success: true, review }); }); -app.post('/api/verification/reset', (req, res) => { - resetVerification(); +app.post('/api/verification/reset', (_req, res) => { + writeJson(verificationPath, { version: 1, reviews: [] }); res.json({ success: true }); }); -// ---- Run history (Task E / K-06) ---- -// Per-run CSV + events live under public/history// and are served -// statically by express.static(publicDir) already mounted via Vite proxy. -// These routes expose metadata + convenience loaders. - -app.get('/api/history', (req, res) => { - res.json(readHistoryMeta()); +app.get('/api/history', (_req, res) => { + res.json(readJson(historyMetaPath, { version: 1, runs: [] })); }); -app.delete('/api/history', (req, res) => { - // Clear metadata only; leave snapshot files on disk (cheap, recoverable). - writeHistoryMeta({ version: 1, runs: [] }); +app.delete('/api/history', (_req, res) => { + writeJson(historyMetaPath, { version: 1, runs: [] }); res.json({ success: true }); }); app.get('/api/history/:runId', (req, res) => { - const { runId } = req.params; - const meta = readHistoryMeta(); - const entry = meta.runs.find((r) => r.runId === runId); + const history = readJson(historyMetaPath, { runs: [] }); + const entry = history.runs?.find((run) => run.runId === req.params.runId); if (!entry) return res.status(404).json({ error: 'Run not found' }); - res.json(entry); + return res.json(entry); }); app.listen(PORT, () => { - console.log(`\n 🧬 Flyt API Server`); - console.log(` ➜ Running on http://localhost:${PORT}`); - console.log(` ➜ Tracker: ${trackerScript}`); - console.log(` ➜ Python: ${pythonExe}`); - console.log(` ➜ FFmpeg: ${ffmpegPath}\n`); -}); \ No newline at end of file + console.log(`\n 🧬 Flyt API Server\n ➜ http://localhost:${PORT}\n ➜ Tracker: ${trackerScript}`); +}); From 26279575128d701a65370857bc468457d6416d96 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 03:18:51 +0530 Subject: [PATCH 09/75] refactor: integrate tracker safety fixes into canonical implementation --- source app folder/tracker/tracker.py | 607 ++++++++++++--------------- 1 file changed, 276 insertions(+), 331 deletions(-) diff --git a/source app folder/tracker/tracker.py b/source app folder/tracker/tracker.py index 9ab9533..6bab20e 100644 --- a/source app folder/tracker/tracker.py +++ b/source app folder/tracker/tracker.py @@ -1,66 +1,50 @@ -import cv2 -import pandas as pd -import numpy as np +import argparse +import json import math -import sys import os -import json -import argparse +import sys +from typing import Any, Callable + +import cv2 +import numpy as np +import pandas as pd -# --- Configuration --- -parser = argparse.ArgumentParser(description='Drosophila Fly Tracker') -parser.add_argument('--input', type=str, default=None, help='Path to input video file') -parser.add_argument('--output-video', type=str, default='output_tracked.mp4', help='Path for output tracked video') -parser.add_argument('--output-csv', type=str, default='fly_tracking_data.csv', help='Path for output CSV data') -parser.add_argument('--no-video', action='store_true', help='Disable video output') -parser.add_argument('--min-area', type=int, default=30, help='Minimum contour area in pixels') -parser.add_argument('--max-area', type=int, default=0, help='Maximum contour area in pixels (0 = no limit)') -parser.add_argument('--roi', type=str, default=None, help='ROI rectangle as x,y,w,h in pixels') -parser.add_argument('--proximity-threshold', type=float, default=60.0, help='Courtship bout proximity threshold (px)') -parser.add_argument('--bout-min-frames', type=int, default=90, help='Minimum consecutive frames for a courtship bout') -parser.add_argument('--output-events', type=str, default=None, help='Path for suspected events JSON output') -args = parser.parse_args() - -if args.input: - VIDEO_PATH = os.path.abspath(args.input) -else: - VIDEO_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "assets", "fly_video.mp4")) - -OUTPUT_VIDEO_PATH = args.output_video -OUTPUT_CSV_PATH = args.output_csv -ENABLE_VIDEO_OUTPUT = not args.no_video -MIN_AREA = args.min_area -MAX_AREA = args.max_area -PROXIMITY_THRESHOLD = args.proximity_threshold -BOUT_MIN_FRAMES = args.bout_min_frames -OUTPUT_EVENTS_PATH = args.output_events LOW_CONFIDENCE_THRESHOLD = 0.2 LOW_CONFIDENCE_MIN_FRAMES = 30 -ROI_RECT = None -if args.roi: - try: - roi_parts = [int(v) for v in args.roi.split(',')] - if len(roi_parts) != 4: - raise ValueError - ROI_RECT = tuple(roi_parts) - except ValueError: - print(f"Error: --roi must be x,y,w,h (got {args.roi!r})") - sys.exit(1) - - -def contour_area_valid(area: float) -> bool: - if area <= MIN_AREA: - return False - if MAX_AREA > 0 and area >= MAX_AREA: - return False - return True - - -def detect_sustained_segments(rows, condition_fn, min_frames: int): - segments = [] - run_start = None +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Drosophila Fly Tracker") + parser.add_argument("--input", type=str, default=None) + parser.add_argument("--output-video", type=str, default="output_tracked.mp4") + parser.add_argument("--output-csv", type=str, default="fly_tracking_data.csv") + parser.add_argument("--no-video", action="store_true") + parser.add_argument("--min-area", type=int, default=30) + parser.add_argument("--max-area", type=int, default=0) + parser.add_argument("--roi", type=str, default=None) + parser.add_argument("--proximity-threshold", type=float, default=60.0) + parser.add_argument("--bout-min-frames", type=int, default=90) + parser.add_argument("--output-events", type=str, default=None) + return parser.parse_args(argv) + + +def value_or(row: dict[str, Any], key: str, default: float) -> float: + value = row.get(key) + if value is None or value == "": + return default + try: + return float(value) + except (TypeError, ValueError): + return default + + +def detect_sustained_segments( + rows: list[dict[str, Any]], + condition_fn: Callable[[dict[str, Any]], bool], + min_frames: int, +) -> list[tuple[int, int]]: + segments: list[tuple[int, int]] = [] + run_start: int | None = None for row in rows: frame = int(row["frame"]) if condition_fn(row): @@ -71,34 +55,27 @@ def detect_sustained_segments(rows, condition_fn, min_frames: int): if run_end - run_start + 1 >= min_frames: segments.append((run_start, run_end)) run_start = None - - if run_start is not None: + if run_start is not None and rows: run_end = int(rows[-1]["frame"]) if run_end - run_start + 1 >= min_frames: segments.append((run_start, run_end)) - return segments -def segment_rows(rows, start_frame: int, end_frame: int): - return [r for r in rows if start_frame <= int(r["frame"]) <= end_frame] - - def build_event_record( event_id: str, event_type: str, start_frame: int, end_frame: int, fps: float, - segment, + segment: list[dict[str, Any]], detection_reason: str, -) -> dict: +) -> dict[str, Any]: effective_fps = fps if fps > 0 else 30.0 duration_frames = end_frame - start_frame + 1 - proximities = [float(r.get("proximity_distance") or 0) for r in segment] - confidences = [float(r.get("identity_confidence") or 0) for r in segment] - occlusions = [int(r.get("occlusion_flag") or 0) for r in segment] - + proximities = [value_or(row, "proximity_distance", 0.0) for row in segment] + confidences = [value_or(row, "identity_confidence", 0.0) for row in segment] + occlusions = [value_or(row, "occlusion_flag", 0.0) for row in segment] return { "id": event_id, "type": event_type, @@ -114,298 +91,266 @@ def build_event_record( } -def detect_events(rows, fps: float): - events = [] - event_counter = 1 - - courtship_segments = detect_sustained_segments( +def detect_events( + rows: list[dict[str, Any]], + fps: float, + proximity_threshold: float, + bout_min_frames: int, +) -> list[dict[str, Any]]: + events: list[dict[str, Any]] = [] + counter = 1 + courtship = detect_sustained_segments( rows, - lambda r: float(r.get("proximity_distance") or 999) < PROXIMITY_THRESHOLD, - BOUT_MIN_FRAMES, + lambda row: value_or(row, "proximity_distance", 999.0) < proximity_threshold, + max(1, bout_min_frames), ) - for start_frame, end_frame in courtship_segments: - segment = segment_rows(rows, start_frame, end_frame) - events.append( - build_event_record( - f"evt-{event_counter:03d}", - "courtship_bout", - start_frame, - end_frame, - fps, - segment, - "proximity_sustained", - ) - ) - event_counter += 1 - - low_conf_segments = detect_sustained_segments( + low_confidence = detect_sustained_segments( rows, - lambda r: float(r.get("identity_confidence") or 1) < LOW_CONFIDENCE_THRESHOLD, + lambda row: value_or(row, "identity_confidence", 1.0) < LOW_CONFIDENCE_THRESHOLD, LOW_CONFIDENCE_MIN_FRAMES, ) - for start_frame, end_frame in low_conf_segments: - segment = segment_rows(rows, start_frame, end_frame) - events.append( - build_event_record( - f"evt-{event_counter:03d}", - "low_confidence_segment", - start_frame, - end_frame, - fps, - segment, - "identity_confidence_low", - ) - ) - event_counter += 1 - - events.sort(key=lambda e: e["start_frame"]) + for event_type, segments, reason in ( + ("courtship_bout", courtship, "proximity_sustained"), + ("low_confidence_segment", low_confidence, "identity_confidence_low"), + ): + for start, end in segments: + segment = [row for row in rows if start <= int(row["frame"]) <= end] + events.append(build_event_record( + f"evt-{counter:03d}", event_type, start, end, fps, segment, reason, + )) + counter += 1 + events.sort(key=lambda event: event["start_frame"]) return events -def write_events_json(rows, fps: float, output_path: str) -> None: +def write_events_json( + rows: list[dict[str, Any]], + fps: float, + output_path: str, + proximity_threshold: float, + bout_min_frames: int, +) -> None: effective_fps = fps if fps > 0 else 30.0 - events = detect_events(rows, effective_fps) - envelope = { + events = detect_events(rows, effective_fps, proximity_threshold, bout_min_frames) + payload = { "version": 1, "fps": round(effective_fps, 3), "total_frames": len(rows), "detection_params": { - "proximity_threshold_px": PROXIMITY_THRESHOLD, - "bout_min_frames": BOUT_MIN_FRAMES, + "proximity_threshold_px": proximity_threshold, + "bout_min_frames": max(1, bout_min_frames), "low_confidence_threshold": LOW_CONFIDENCE_THRESHOLD, "low_confidence_min_frames": LOW_CONFIDENCE_MIN_FRAMES, }, "events": events, } - with open(output_path, "w", encoding="utf-8") as f: - json.dump(envelope, f, indent=2) + with open(output_path, "w", encoding="utf-8") as handle: + json.dump(payload, handle, indent=2) print(f"Events saved to {output_path} ({len(events)} suspected events)") -def assignment_confidence(cA, cB, prev_f1, prev_f2) -> float: - """Confidence from how clearly nearest-neighbor assignment beats the swap.""" - direct = float(math.dist(cA, prev_f1) + math.dist(cB, prev_f2)) - swapped = float(math.dist(cA, prev_f2) + math.dist(cB, prev_f1)) +def displacement(previous, current, was_initialized: bool) -> float: + return float(math.dist(current, previous)) if was_initialized else 0.0 + + +def assignment_confidence(ca, cb, prev_f1, prev_f2) -> float: + direct = float(math.dist(ca, prev_f1) + math.dist(cb, prev_f2)) + swapped = float(math.dist(ca, prev_f2) + math.dist(cb, prev_f1)) margin = abs(swapped - direct) return float(np.clip(margin / (max(direct, swapped) + 1e-6), 0.2, 1.0)) -if not os.path.exists(VIDEO_PATH): - print(f"Error: Video file not found at {VIDEO_PATH}") - sys.exit(1) - -cap = cv2.VideoCapture(VIDEO_PATH) +def parse_roi(value: str | None) -> tuple[int, int, int, int] | None: + if not value: + return None + try: + parts = [int(item) for item in value.split(",")] + except ValueError as error: + raise ValueError(f"--roi must be x,y,w,h (got {value!r})") from error + if len(parts) != 4: + raise ValueError(f"--roi must be x,y,w,h (got {value!r})") + return tuple(parts) # type: ignore[return-value] -width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) -height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) -fps = cap.get(cv2.CAP_PROP_FPS) -expected_frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) -# Effective FPS for temporal normalization. Existing fly1_speed/fly2_speed stay -# in px/frame (byte-identical to pitch gold); px/sec is exposed via separate -# *_pxsec columns so pitch parity (Rule #2) is preserved. -effective_fps = fps if fps and fps > 0 else 30.0 +def run_tracker(args: argparse.Namespace) -> int: + video_path = os.path.abspath(args.input) if args.input else os.path.abspath( + os.path.join(os.path.dirname(__file__), "..", "..", "assets", "fly_video.mp4") + ) + if not os.path.exists(video_path): + print(f"Error: Video file not found at {video_path}") + return 1 + try: + roi_rect = parse_roi(args.roi) + except ValueError as error: + print(f"Error: {error}") + return 1 + + cap = cv2.VideoCapture(video_path) + if not cap.isOpened(): + print(f"Error: Could not open video at {video_path}") + return 1 + width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + fps = float(cap.get(cv2.CAP_PROP_FPS)) + effective_fps = fps if fps > 0 else 30.0 + expected_frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) + + out = None + if not args.no_video: + fourcc = cv2.VideoWriter_fourcc(*"mp4v") + out = cv2.VideoWriter(args.output_video, fourcc, effective_fps, (width, height)) + if not out.isOpened(): + cap.release() + print(f"Error: Could not create output video at {args.output_video}") + return 1 + + fgbg = cv2.createBackgroundSubtractorMOG2( + history=500, varThreshold=50, detectShadows=False, + ) + kernel = np.ones((5, 5), np.uint8) + data: list[dict[str, Any]] = [] + frame_num = 0 + is_initialized = False + prev_f1 = (0, 0) + prev_f2 = (0, 0) -if ENABLE_VIDEO_OUTPUT: - fourcc = cv2.VideoWriter_fourcc(*'mp4v') # type: ignore - out = cv2.VideoWriter(OUTPUT_VIDEO_PATH, fourcc, fps, (width, height)) + print(f"Starting tracking on {video_path}...") + if roi_rect: + print(f"ROI active: x={roi_rect[0]}, y={roi_rect[1]}, w={roi_rect[2]}, h={roi_rect[3]}") -fgbg = cv2.createBackgroundSubtractorMOG2(history=500, varThreshold=50, detectShadows=False) + try: + while cap.isOpened(): + ret, frame = cap.read() + if not ret: + break + fgmask = fgbg.apply(frame) + fgmask = cv2.morphologyEx(fgmask, cv2.MORPH_OPEN, kernel) + fgmask = cv2.morphologyEx(fgmask, cv2.MORPH_CLOSE, kernel) + if roi_rect: + rx, ry, rw, rh = roi_rect + roi_mask = np.zeros_like(fgmask) + roi_mask[ry:ry + rh, rx:rx + rw] = 255 + fgmask = cv2.bitwise_and(fgmask, roi_mask) + + activity_level = int(cv2.countNonZero(fgmask)) + contours, _ = cv2.findContours( + fgmask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE, + ) + valid = [ + contour for contour in contours + if cv2.contourArea(contour) > args.min_area + and (args.max_area <= 0 or cv2.contourArea(contour) < args.max_area) + ] + valid.sort(key=cv2.contourArea, reverse=True) + centroids, bboxes, areas = [], [], [] + for contour in valid[:2]: + x, y, w, h = cv2.boundingRect(contour) + centroids.append((int(x + w / 2), int(y + h / 2))) + bboxes.append((x, y, w, h)) + areas.append(float(cv2.contourArea(contour))) + + f1_coords = f2_coords = (0, 0) + f1_speed = f2_speed = 0.0 + proximity = 0.0 + occlusion_flag = 0 + identity_confidence = 0.0 + fly1_area = fly2_area = 0.0 + + if len(centroids) == 2: + ca, cb = centroids + was_initialized = is_initialized + if not was_initialized: + f1, f2 = ca, cb + is_initialized = True + identity_confidence = 0.5 + else: + direct = math.dist(ca, prev_f1) + math.dist(cb, prev_f2) + swapped = math.dist(ca, prev_f2) + math.dist(cb, prev_f1) + f1, f2 = (ca, cb) if direct < swapped else (cb, ca) + identity_confidence = assignment_confidence(ca, cb, prev_f1, prev_f2) + f1_coords, f2_coords = f1, f2 + if f1 == ca: + fly1_area, fly2_area = areas[0], areas[1] + else: + fly1_area, fly2_area = areas[1], areas[0] + if was_initialized: + f1_speed = displacement(prev_f1, f1_coords, was_initialized) + f2_speed = displacement(prev_f2, f2_coords, was_initialized) + proximity = float(math.dist(f1_coords, f2_coords)) + prev_f1, prev_f2 = f1_coords, f2_coords + if out: + for x, y, w, h in bboxes: + cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) + cv2.circle(frame, f1_coords, 5, (255, 0, 0), -1) + cv2.circle(frame, f2_coords, 5, (0, 0, 255), -1) + cv2.line(frame, f1_coords, f2_coords, (0, 255, 255), 2) + elif len(centroids) == 1: + point = centroids[0] + was_initialized = is_initialized + f1_coords = f2_coords = point + occlusion_flag = 1 + identity_confidence = 0.25 + fly1_area = fly2_area = areas[0] + if was_initialized: + f1_speed = displacement(prev_f1, point, was_initialized) + f2_speed = displacement(prev_f2, point, was_initialized) + prev_f1 = prev_f2 = point + is_initialized = True + if out: + x, y, w, h = bboxes[0] + cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 255), 3) + cv2.putText(frame, "MERGED", (x, max(0, y - 10)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 2) + elif is_initialized: + f1_coords, f2_coords = prev_f1, prev_f2 + proximity = float(math.dist(f1_coords, f2_coords)) + identity_confidence = 0.15 + + data.append({ + "frame": frame_num, + "fly1_x": f1_coords[0], "fly1_y": f1_coords[1], + "fly2_x": f2_coords[0], "fly2_y": f2_coords[1], + "fly1_speed": f1_speed, "fly2_speed": f2_speed, + "fly1_speed_pxsec": round(f1_speed * effective_fps, 4), + "fly2_speed_pxsec": round(f2_speed * effective_fps, 4), + "activity_level": activity_level, + "proximity_distance": proximity, + "occlusion_flag": occlusion_flag, + "identity_confidence": round(identity_confidence, 4), + "fly1_area": fly1_area, "fly2_area": fly2_area, + }) + if out: + out.write(frame) + frame_num += 1 + if frame_num % 100 == 0: + print(f"Processed {frame_num} frames...") + finally: + cap.release() + if out: + out.release() + cv2.destroyAllWindows() + + pd.DataFrame(data).to_csv(args.output_csv, index=False) + if args.output_events: + write_events_json( + data, effective_fps, args.output_events, + args.proximity_threshold, args.bout_min_frames, + ) + last_frame = data[-1]["frame"] if data else -1 + sync_ok = len(data) == frame_num and (frame_num == 0 or last_frame == frame_num - 1) + print( + f"TRACKER_SYNC frames_processed={frame_num} csv_rows={len(data)} " + f"expected_video_frames={expected_frame_count} sync_ok={str(sync_ok).lower()}" + ) + if not sync_ok: + print("Warning: internal frame/CSV sync mismatch detected", file=sys.stderr) + return 2 + print(f"Tracking completed! Data saved to {args.output_csv}.") + return 0 -data = [] -frame_num = 0 -is_initialized = False -prev_f1 = (0, 0) -prev_f2 = (0, 0) -print(f"Starting tracking on {VIDEO_PATH}...") -if ROI_RECT: - print(f"ROI active: x={ROI_RECT[0]}, y={ROI_RECT[1]}, w={ROI_RECT[2]}, h={ROI_RECT[3]}") -print(f"Contour area filter: {MIN_AREA} < area" + (f" < {MAX_AREA}" if MAX_AREA > 0 else "")) +def main(argv: list[str] | None = None) -> int: + return run_tracker(parse_args(argv)) -while cap.isOpened(): - ret, frame = cap.read() - if not ret: - break - fgmask = fgbg.apply(frame) - kernel = np.ones((5, 5), np.uint8) - fgmask = cv2.morphologyEx(fgmask, cv2.MORPH_OPEN, kernel) - fgmask = cv2.morphologyEx(fgmask, cv2.MORPH_CLOSE, kernel) - - if ROI_RECT: - rx, ry, rw, rh = ROI_RECT - roi_mask = np.zeros_like(fgmask) - roi_mask[ry:ry + rh, rx:rx + rw] = 255 - fgmask = cv2.bitwise_and(fgmask, roi_mask) - - activity_level = int(cv2.countNonZero(fgmask)) - - contours, _ = cv2.findContours(fgmask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) - valid_contours = [cnt for cnt in contours if contour_area_valid(cv2.contourArea(cnt))] - valid_contours.sort(key=cv2.contourArea, reverse=True) - - current_centroids = [] - bboxes = [] - contour_areas = [] - - max_idx = min(2, len(valid_contours)) - for i in range(max_idx): - cnt = valid_contours[i] - x, y, w, h = cv2.boundingRect(cnt) - cx = int(x + w / 2) - cy = int(y + h / 2) - current_centroids.append((cx, cy)) - bboxes.append((x, y, w, h)) - contour_areas.append(float(cv2.contourArea(cnt))) - - f1_coords = (0, 0) - f2_coords = (0, 0) - f1_speed = 0.0 - f2_speed = 0.0 - f1_speed_pxsec = 0.0 - f2_speed_pxsec = 0.0 - proximity = 0.0 - occlusion_flag = 0 - identity_conf = 0.0 - fly1_area = 0.0 - fly2_area = 0.0 - - if len(current_centroids) == 2: - cA, cB = current_centroids[0], current_centroids[1] - - if not is_initialized: - f1, f2 = cA, cB - is_initialized = True - identity_conf = 0.5 - else: - dist_A_to_prev1 = float(math.dist(cA, prev_f1)) - dist_A_to_prev2 = float(math.dist(cA, prev_f2)) - dist_B_to_prev1 = float(math.dist(cB, prev_f1)) - dist_B_to_prev2 = float(math.dist(cB, prev_f2)) - - if dist_A_to_prev1 + dist_B_to_prev2 < dist_A_to_prev2 + dist_B_to_prev1: - f1, f2 = cA, cB - else: - f1, f2 = cB, cA - - identity_conf = assignment_confidence(cA, cB, prev_f1, prev_f2) - - f1_coords = f1 - f2_coords = f2 - if f1 == cA: - fly1_area = contour_areas[0] - fly2_area = contour_areas[1] - else: - fly1_area = contour_areas[1] - fly2_area = contour_areas[0] - - if is_initialized: - # Core pitch columns: displacement per frame in px/frame (0-diff parity). - f1_speed = float(math.dist(f1_coords, prev_f1)) - f2_speed = float(math.dist(f2_coords, prev_f2)) - # Flyt temporal-normalized columns: physical speed in px/sec. - f1_speed_pxsec = f1_speed * effective_fps - f2_speed_pxsec = f2_speed * effective_fps - - proximity = float(math.dist(f1_coords, f2_coords)) - prev_f1 = f1_coords - prev_f2 = f2_coords - - if ENABLE_VIDEO_OUTPUT: - for (x, y, w, h) in bboxes: - cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) - - cv2.circle(frame, f1_coords, 5, (255, 0, 0), -1) - cv2.circle(frame, f2_coords, 5, (0, 0, 255), -1) - cv2.line(frame, f1_coords, f2_coords, (0, 255, 255), 2) - - text_pos = (min(f1_coords[0], f2_coords[0]), max(0, min(f1_coords[1], f2_coords[1]) - 10)) - cv2.putText(frame, f"Dist: {int(proximity)}px", text_pos, cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 2) - - elif len(current_centroids) == 1: - cA = current_centroids[0] - f1_coords = cA - f2_coords = cA - occlusion_flag = 1 - identity_conf = 0.25 - fly1_area = contour_areas[0] - fly2_area = contour_areas[0] - - if is_initialized: - # Core pitch columns (px/frame, parity-identical to pitch on merge frames). - f1_speed = float(math.dist(f1_coords, prev_f1)) - f2_speed = float(math.dist(f2_coords, prev_f2)) - f1_speed_pxsec = f1_speed * effective_fps - f2_speed_pxsec = f2_speed * effective_fps - - proximity = 0.0 - prev_f1 = f1_coords - prev_f2 = f2_coords - is_initialized = True - - if ENABLE_VIDEO_OUTPUT: - x, y, w, h = bboxes[0] - cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 255), 3) - cv2.putText(frame, "MERGED", (x, max(0, y - 10)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 2) - - else: - if is_initialized: - f1_coords = prev_f1 - f2_coords = prev_f2 - proximity = float(math.dist(f1_coords, f2_coords)) - identity_conf = 0.15 - - data.append({ - "frame": frame_num, - "fly1_x": f1_coords[0], - "fly1_y": f1_coords[1], - "fly2_x": f2_coords[0], - "fly2_y": f2_coords[1], - "fly1_speed": f1_speed, - "fly2_speed": f2_speed, - "fly1_speed_pxsec": round(f1_speed_pxsec, 4), - "fly2_speed_pxsec": round(f2_speed_pxsec, 4), - "activity_level": activity_level, - "proximity_distance": proximity, - "occlusion_flag": occlusion_flag, - "identity_confidence": round(identity_conf, 4), - "fly1_area": fly1_area, - "fly2_area": fly2_area, - }) - - if ENABLE_VIDEO_OUTPUT: - out.write(frame) - - frame_num += 1 - - if frame_num % 100 == 0: - print(f"Processed {frame_num} frames...") - -cap.release() -if ENABLE_VIDEO_OUTPUT: - out.release() -cv2.destroyAllWindows() - -df = pd.DataFrame(data) -df.to_csv(OUTPUT_CSV_PATH, index=False) - -if OUTPUT_EVENTS_PATH: - write_events_json(data, fps, OUTPUT_EVENTS_PATH) - -csv_rows = len(data) -frames_processed = frame_num -last_frame_idx = data[-1]["frame"] if data else -1 -sync_ok = ( - csv_rows == frames_processed - and (frames_processed == 0 or last_frame_idx == frames_processed - 1) -) -print( - f"TRACKER_SYNC frames_processed={frames_processed} csv_rows={csv_rows} " - f"expected_video_frames={expected_frame_count} sync_ok={sync_ok}" -) -if not sync_ok: - print("Warning: internal frame/CSV sync mismatch detected", file=sys.stderr) - sys.exit(2) - -print(f"Tracking completed! Data saved to {OUTPUT_CSV_PATH}.") \ No newline at end of file +if __name__ == "__main__": + raise SystemExit(main()) From 3ba84bf57e1f819c0c5c4103801bef9d2423044b Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 03:19:07 +0530 Subject: [PATCH 10/75] test: add server race and publication regressions --- .../dashboard/tests/server-utils.test.js | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 source app folder/dashboard/tests/server-utils.test.js diff --git a/source app folder/dashboard/tests/server-utils.test.js b/source app folder/dashboard/tests/server-utils.test.js new file mode 100644 index 0000000..a286c5b --- /dev/null +++ b/source app folder/dashboard/tests/server-utils.test.js @@ -0,0 +1,83 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; +import { + buildTrackerArgs, + parseTrackerSync, + publishBundle, + resolveVerificationPath, + scopeEventsForHistory, +} from '../server-utils.js'; + +const defaults = { minArea: 30, maxArea: 0, proximityThreshold: 60, boutMinFrames: 90 }; + +test('normalizes integer tracker settings and rejects invalid max area', () => { + const args = buildTrackerArgs('tracker.py', 'in.mp4', { + rawVideo: 'raw.mp4', csv: 'data.csv', events: 'events.json', + }, { minArea: 30.9, maxArea: 20.2, boutMinFrames: 0.8 }, defaults); + assert.equal(args[args.indexOf('--min-area') + 1], '30'); + assert.equal(args[args.indexOf('--max-area') + 1], '0'); + assert.equal(args[args.indexOf('--bout-min-frames') + 1], '1'); +}); + +test('parses tracker sync output', () => { + assert.deepEqual(parseTrackerSync( + 'TRACKER_SYNC frames_processed=12 csv_rows=12 expected_video_frames=14 sync_ok=true', + ), { framesProcessed: 12, csvRows: 12, expectedVideoFrames: 14, syncOk: true }); +}); + +test('publishes a complete bundle and removes backups', () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'flyt-publish-')); + const sourceA = path.join(dir, 'source-a'); + const sourceB = path.join(dir, 'source-b'); + const destinationA = path.join(dir, 'a'); + const destinationB = path.join(dir, 'b'); + fs.writeFileSync(sourceA, 'new-a'); + fs.writeFileSync(sourceB, 'new-b'); + fs.writeFileSync(destinationA, 'old-a'); + fs.writeFileSync(destinationB, 'old-b'); + publishBundle([ + { source: sourceA, destination: destinationA }, + { source: sourceB, destination: destinationB }, + ], 'token'); + assert.equal(fs.readFileSync(destinationA, 'utf8'), 'new-a'); + assert.equal(fs.readFileSync(destinationB, 'utf8'), 'new-b'); + assert.equal(fs.readdirSync(dir).some((name) => name.endsWith('.bak')), false); +}); + +test('rolls back every destination when bundle staging fails', () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'flyt-rollback-')); + const source = path.join(dir, 'source'); + const missing = path.join(dir, 'missing'); + const destinationA = path.join(dir, 'a'); + const destinationB = path.join(dir, 'b'); + fs.writeFileSync(source, 'new-a'); + fs.writeFileSync(destinationA, 'old-a'); + fs.writeFileSync(destinationB, 'old-b'); + assert.throws(() => publishBundle([ + { source, destination: destinationA }, + { source: missing, destination: destinationB }, + ], 'token')); + assert.equal(fs.readFileSync(destinationA, 'utf8'), 'old-a'); + assert.equal(fs.readFileSync(destinationB, 'utf8'), 'old-b'); +}); + +test('scopes historical event IDs and verification paths to their run', () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'flyt-history-')); + const historyDir = path.join(dir, 'history'); + const runId = 'run-1234-abcd'; + fs.mkdirSync(path.join(historyDir, runId), { recursive: true }); + const source = path.join(dir, 'events.json'); + const scoped = path.join(historyDir, runId, 'events.json'); + const historyMeta = path.join(dir, 'history.json'); + fs.writeFileSync(source, JSON.stringify({ events: [{ id: 'evt-001' }] })); + fs.writeFileSync(historyMeta, JSON.stringify({ runs: [{ runId }] })); + scopeEventsForHistory(source, runId, scoped); + assert.equal(JSON.parse(fs.readFileSync(scoped)).events[0].id, `${runId}:evt-001`); + assert.equal( + resolveVerificationPath(`${runId}:evt-001`, 'current.json', historyDir, historyMeta), + path.join(historyDir, runId, 'verification.json'), + ); +}); From 802dbd101ae6bfbb7f653a859725c10801370575 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 03:19:23 +0530 Subject: [PATCH 11/75] test: add tracker data-integrity regressions --- .../tracker/tests/test_tracker.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 source app folder/tracker/tests/test_tracker.py diff --git a/source app folder/tracker/tests/test_tracker.py b/source app folder/tracker/tests/test_tracker.py new file mode 100644 index 0000000..2e50452 --- /dev/null +++ b/source app folder/tracker/tests/test_tracker.py @@ -0,0 +1,46 @@ +import importlib.util +import pathlib +import unittest + +TRACKER_PATH = pathlib.Path(__file__).resolve().parents[1] / "tracker.py" +spec = importlib.util.spec_from_file_location("flyt_tracker", TRACKER_PATH) +tracker = importlib.util.module_from_spec(spec) +assert spec.loader is not None +spec.loader.exec_module(tracker) + + +class TrackerTests(unittest.TestCase): + def test_zero_is_not_missing(self): + self.assertEqual(tracker.value_or({"value": 0}, "value", 999), 0) + self.assertEqual(tracker.value_or({"value": 0.0}, "value", 999), 0.0) + + def test_initial_detection_has_zero_velocity(self): + self.assertEqual(tracker.displacement((0, 0), (100, 100), False), 0.0) + self.assertGreater(tracker.displacement((0, 0), (3, 4), True), 0.0) + + def test_zero_proximity_generates_courtship(self): + rows = [ + {"frame": i, "proximity_distance": 0, "identity_confidence": 1, "occlusion_flag": 1} + for i in range(5) + ] + events = tracker.detect_events(rows, 30, proximity_threshold=60, bout_min_frames=5) + self.assertEqual(events[0]["type"], "courtship_bout") + self.assertEqual(events[0]["mean_proximity_px"], 0) + + def test_zero_confidence_generates_low_confidence_segment(self): + rows = [ + {"frame": i, "proximity_distance": 999, "identity_confidence": 0, "occlusion_flag": 0} + for i in range(30) + ] + events = tracker.detect_events(rows, 30, proximity_threshold=60, bout_min_frames=90) + self.assertEqual(events[0]["type"], "low_confidence_segment") + self.assertEqual(events[0]["min_identity_confidence"], 0) + + def test_roi_validation(self): + self.assertEqual(tracker.parse_roi("1,2,3,4"), (1, 2, 3, 4)) + with self.assertRaises(ValueError): + tracker.parse_roi("1,2") + + +if __name__ == "__main__": + unittest.main() From eab49b5c9809c1c54fa4755b86064df6b582542e Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 03:19:31 +0530 Subject: [PATCH 12/75] ci: validate canonical critical fixes --- .github/workflows/critical-fixes-ci.yml | 57 +++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 .github/workflows/critical-fixes-ci.yml diff --git a/.github/workflows/critical-fixes-ci.yml b/.github/workflows/critical-fixes-ci.yml new file mode 100644 index 0000000..11f8adb --- /dev/null +++ b/.github/workflows/critical-fixes-ci.yml @@ -0,0 +1,57 @@ +name: Critical fixes CI + +on: + push: + branches: + - fix/critical-job-races + pull_request: + paths: + - 'source app folder/dashboard/**' + - 'source app folder/tracker/**' + - '.github/workflows/critical-fixes-ci.yml' + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: source app folder/dashboard/package-lock.json + + - name: Install dashboard dependencies + working-directory: source app folder/dashboard + run: npm ci + + - name: Lint backend changes + working-directory: source app folder/dashboard + run: npm run lint:backend + + - name: Run server regression tests + working-directory: source app folder/dashboard + run: npm run test:server + + - name: Build dashboard + working-directory: source app folder/dashboard + run: npm run build + + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: pip + cache-dependency-path: source app folder/tracker/requirements.txt + + - name: Install tracker dependencies + run: python -m pip install -r 'source app folder/tracker/requirements.txt' + + - name: Check Python syntax + run: python -m py_compile 'source app folder/tracker/tracker.py' + + - name: Run tracker regression tests + run: python -m unittest discover -s 'source app folder/tracker/tests' -p 'test_*.py' -v From 653099275b04318b5a735586e5b54952914e5fed Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 03:19:37 +0530 Subject: [PATCH 13/75] refactor: remove obsolete safe utility wrapper --- .../dashboard/server-safe-utils.js | 236 ------------------ 1 file changed, 236 deletions(-) delete mode 100644 source app folder/dashboard/server-safe-utils.js diff --git a/source app folder/dashboard/server-safe-utils.js b/source app folder/dashboard/server-safe-utils.js deleted file mode 100644 index 1eb8f15..0000000 --- a/source app folder/dashboard/server-safe-utils.js +++ /dev/null @@ -1,236 +0,0 @@ -import { spawn } from 'child_process'; -import fs from 'fs'; -import path from 'path'; - -export function ensureDir(dirPath) { - if (!fs.existsSync(dirPath)) fs.mkdirSync(dirPath, { recursive: true }); -} - -export function safeUnlink(filePath) { - if (!filePath) return; - try { - if (fs.existsSync(filePath)) fs.unlinkSync(filePath); - } catch (error) { - console.error(`[Server] Failed to remove ${filePath}: ${error.message}`); - } -} - -export function safeRemoveDir(dirPath) { - if (!dirPath) return; - try { - fs.rmSync(dirPath, { recursive: true, force: true }); - } catch (error) { - console.error(`[Server] Failed to remove ${dirPath}: ${error.message}`); - } -} - -export function replaceFile(sourcePath, destinationPath, token) { - const tempPath = `${destinationPath}.${token}.tmp`; - fs.copyFileSync(sourcePath, tempPath); - if (fs.existsSync(destinationPath)) fs.unlinkSync(destinationPath); - fs.renameSync(tempPath, destinationPath); -} - -export function countCsvRows(csvPath) { - const content = fs.readFileSync(csvPath, 'utf8').trim(); - return content ? content.split('\n').length - 1 : 0; -} - -export function getVideoFrameCount(ffmpegPath, videoPath) { - return new Promise((resolve, reject) => { - const child = spawn(ffmpegPath, [ - '-i', videoPath, - '-map', '0:v:0', - '-f', 'null', - '-', - ]); - let stderr = ''; - child.stderr.on('data', (data) => { stderr += data.toString(); }); - child.on('close', () => { - const matches = [...stderr.matchAll(/frame=\s*(\d+)/g)]; - if (matches.length) { - resolve(parseInt(matches.at(-1)[1], 10)); - } else { - reject(new Error('Could not determine video frame count from ffmpeg')); - } - }); - child.on('error', reject); - }); -} - -export function parseTrackerSync(stdout) { - const match = stdout.match( - /TRACKER_SYNC frames_processed=(\d+) csv_rows=(\d+) expected_video_frames=(\d+) sync_ok=(true|false)/, - ); - if (!match) return null; - return { - framesProcessed: parseInt(match[1], 10), - csvRows: parseInt(match[2], 10), - expectedVideoFrames: parseInt(match[3], 10), - syncOk: match[4] === 'true', - }; -} - -export function readJson(filePath, fallback) { - if (!fs.existsSync(filePath)) return fallback; - try { - return JSON.parse(fs.readFileSync(filePath, 'utf8')); - } catch { - return fallback; - } -} - -export function writeJson(filePath, data) { - fs.writeFileSync(filePath, JSON.stringify(data, null, 2)); -} - -export function readEventsFps(eventsPath) { - const data = readJson(eventsPath, null); - return data && typeof data.fps === 'number' ? data.fps : null; -} - -export function readCsvAvgProximity(csvPath) { - if (!fs.existsSync(csvPath)) return null; - try { - const lines = fs.readFileSync(csvPath, 'utf8').trim().split('\n'); - if (lines.length < 2) return null; - const header = lines[0].split(','); - const proxIdx = header.indexOf('proximity_distance'); - const occIdx = header.indexOf('occlusion_flag'); - if (proxIdx < 0) return null; - - let sum = 0; - let count = 0; - for (let i = 1; i < lines.length; i += 1) { - const cols = lines[i].split(','); - const value = parseFloat(cols[proxIdx]); - const occluded = occIdx >= 0 && parseInt(cols[occIdx], 10) === 1; - if (Number.isFinite(value) && !occluded) { - sum += value; - count += 1; - } - } - return count ? Math.round((sum / count) * 100) / 100 : null; - } catch { - return null; - } -} - -export function countCourtshipBouts(eventsPath) { - const data = readJson(eventsPath, { events: [] }); - return Array.isArray(data.events) - ? data.events.filter((event) => event.type === 'courtship_bout').length - : 0; -} - -export function snapshotRunToHistory({ - historyDir, - historyMetaPath, - runId, - filename, - durationSec, - fps, - totalFrames, - csvSrc, - eventsSrc, -}) { - if (!fs.existsSync(csvSrc)) return null; - - const stampedId = `run-${Date.now()}-${String(runId).padStart(4, '0')}`; - const runDir = path.join(historyDir, stampedId); - ensureDir(runDir); - fs.copyFileSync(csvSrc, path.join(runDir, 'data.csv')); - if (fs.existsSync(eventsSrc)) { - fs.copyFileSync(eventsSrc, path.join(runDir, 'events.json')); - } - - const meta = { - runId: stampedId, - timestamp: new Date().toISOString(), - filename: filename || 'unknown', - durationSec: Math.round((durationSec || 0) * 10) / 10, - fps: fps || null, - totalFrames: totalFrames || null, - avgProximity: readCsvAvgProximity(csvSrc), - detectedBouts: countCourtshipBouts(eventsSrc), - }; - - const history = readJson(historyMetaPath, { version: 1, runs: [] }); - if (!Array.isArray(history.runs)) history.runs = []; - history.runs.unshift(meta); - writeJson(historyMetaPath, history); - return meta; -} - -export function transcodeVideo(ffmpegPath, rawPath, finalPath) { - return new Promise((resolve, reject) => { - const child = spawn(ffmpegPath, [ - '-i', rawPath, - '-vcodec', 'libx264', - '-preset', 'veryfast', - '-pix_fmt', 'yuv420p', - '-movflags', '+faststart', - '-an', - '-y', - finalPath, - ]); - let stderr = ''; - child.stderr.on('data', (data) => { stderr += data.toString(); }); - child.on('close', (code) => { - if (code === 0) resolve(); - else reject(new Error( - `ffmpeg transcode failed (exit ${code}): ${stderr.slice(-500)}`, - )); - }); - child.on('error', (error) => reject(new Error( - `Failed to start ffmpeg: ${error.message}`, - ))); - }); -} - -export function buildTrackerArgs( - trackerScript, - inputPath, - outputVideo, - outputCsv, - outputEvents, - overrides, - defaults, -) { - const numberOr = (value, fallback) => { - const parsed = Number(value); - return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback; - }; - const integerOr = (value, fallback, minimum = 0) => ( - Math.max(minimum, Math.floor(numberOr(value, fallback))) - ); - - const config = { - minArea: integerOr(overrides.minArea, defaults.minArea), - maxArea: integerOr(overrides.maxArea, defaults.maxArea), - proximityThreshold: numberOr( - overrides.proximityThreshold, - defaults.proximityThreshold, - ), - boutMinFrames: integerOr( - overrides.boutMinFrames, - defaults.boutMinFrames, - 1, - ), - }; - if (config.maxArea > 0 && config.maxArea <= config.minArea) { - config.maxArea = defaults.maxArea; - } - - return [ - trackerScript, - '--input', inputPath, - '--output-video', outputVideo, - '--output-csv', outputCsv, - '--output-events', outputEvents, - '--min-area', String(config.minArea), - '--max-area', String(config.maxArea), - '--proximity-threshold', String(config.proximityThreshold), - '--bout-min-frames', String(config.boutMinFrames), - ]; -} From dc667b8aee97140f28b722032fd4698c62684089 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 03:19:41 +0530 Subject: [PATCH 14/75] refactor: remove obsolete safe server wrapper --- source app folder/dashboard/server-safe.js | 425 --------------------- 1 file changed, 425 deletions(-) delete mode 100644 source app folder/dashboard/server-safe.js diff --git a/source app folder/dashboard/server-safe.js b/source app folder/dashboard/server-safe.js deleted file mode 100644 index a516dba..0000000 --- a/source app folder/dashboard/server-safe.js +++ /dev/null @@ -1,425 +0,0 @@ -import express from 'express'; -import multer from 'multer'; -import { spawn } from 'child_process'; -import path from 'path'; -import fs from 'fs'; -import { fileURLToPath } from 'url'; -import ffmpegPath from 'ffmpeg-static'; -import { - buildTrackerArgs, - countCsvRows, - ensureDir, - getVideoFrameCount, - parseTrackerSync, - readEventsFps, - readJson, - replaceFile, - safeRemoveDir, - safeUnlink, - snapshotRunToHistory, - transcodeVideo, - writeJson, -} from './server-safe-utils.js'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const app = express(); -const PORT = 3001; - -const uploadsDir = path.join(__dirname, 'uploads'); -const publicDir = path.join(__dirname, 'public'); -const historyDir = path.join(publicDir, 'history'); -const historyMetaPath = path.join(publicDir, 'history.json'); -const eventsPath = path.join(publicDir, 'events.json'); -const verificationPath = path.join(publicDir, 'verification.json'); -const runMetadataPath = path.join(publicDir, 'run_metadata.json'); -const trackerDir = path.join(__dirname, '..', 'tracker'); -const trackerScript = path.join(trackerDir, 'tracker_safe.py'); -const isWindows = process.platform === 'win32'; -const pythonExe = path.join( - trackerDir, - 'venv', - isWindows ? 'Scripts' : 'bin', - isWindows ? 'python.exe' : 'python', -); - -for (const dir of [uploadsDir, publicDir, historyDir]) ensureDir(dir); - -const defaults = { - minArea: 30, - maxArea: 0, - proximityThreshold: 60, - boutMinFrames: 90, -}; - -const storage = multer.diskStorage({ - destination: (req, file, callback) => callback(null, uploadsDir), - filename: (req, file, callback) => { - const extension = path.extname(file.originalname).toLowerCase(); - callback(null, `input_${Date.now()}_${process.pid}${extension}`); - }, -}); -const upload = multer({ - storage, - limits: { fileSize: 10 * 1024 * 1024 * 1024 }, -}); - -app.use(express.json()); -app.use((req, res, next) => { - res.header('Access-Control-Allow-Origin', '*'); - res.header('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS'); - res.header('Access-Control-Allow-Headers', 'Content-Type'); - if (req.method === 'OPTIONS') return res.sendStatus(204); - return next(); -}); - -let runSequence = 0; -let epoch = 0; -let uploadReserved = false; -let activeTracker = null; - -const blankJob = () => ({ - runId: null, - status: 'idle', - progress: '', - framesProcessed: 0, - totalFrames: null, - frameCount: null, - error: null, - startTime: null, - endTime: null, -}); -let job = blankJob(); - -const isBusy = () => ( - uploadReserved - || job.status === 'uploading' - || job.status === 'processing' -); -const isCurrent = (context) => ( - context.epoch === epoch - && context.runId === job.runId -); - -function resetJob() { - epoch += 1; - if (activeTracker) { - try { activeTracker.kill(); } catch { /* already exited */ } - activeTracker = null; - } - job = blankJob(); -} - -function reserveUpload(req, res, next) { - if (isBusy()) { - return res.status(409).json({ - error: 'A video is already being uploaded or processed. Please wait.', - }); - } - - uploadReserved = true; - let released = false; - const release = () => { - if (!released) { - released = true; - uploadReserved = false; - } - }; - res.once('finish', release); - res.once('close', release); - return next(); -} - -function readVerification() { - const data = readJson(verificationPath, { version: 1, reviews: [] }); - return Array.isArray(data.reviews) - ? data - : { version: 1, reviews: [] }; -} - -function resetVerification() { - writeJson(verificationPath, { version: 1, reviews: [] }); -} - -function startTracking(inputPath, overrides) { - const context = { - runId: job.runId, - epoch, - startTime: Date.now(), - stdout: '', - filename: job.uploadedFilename, - }; - const token = `${context.runId}-${context.epoch}`; - const workDir = path.join(uploadsDir, `run-${token}`); - ensureDir(workDir); - - const rawVideo = path.join(workDir, 'tracked_raw.mp4'); - const browserVideo = path.join(workDir, 'tracked.mp4'); - const csvPath = path.join(workDir, 'data.csv'); - const runEventsPath = path.join(workDir, 'events.json'); - - job.status = 'processing'; - job.progress = 'Initializing tracker pipeline...'; - job.framesProcessed = 0; - job.startTime = context.startTime; - job.error = null; - job.overrides = overrides; - - const tracker = spawn( - pythonExe, - buildTrackerArgs( - trackerScript, - inputPath, - rawVideo, - csvPath, - runEventsPath, - overrides, - defaults, - ), - ); - activeTracker = tracker; - - const cleanup = () => { - safeUnlink(inputPath); - safeRemoveDir(workDir); - }; - - tracker.stdout.on('data', (chunk) => { - const raw = chunk.toString(); - context.stdout += raw; - const message = raw.trim(); - console.log(`[Tracker] ${message}`); - if (!isCurrent(context)) return; - - const progress = message.match(/Processed (\d+) frames/); - if (progress) { - job.framesProcessed = parseInt(progress[1], 10); - job.progress = `Processed ${job.framesProcessed} frames...`; - } else if (message.includes('Tracking completed')) { - job.progress = 'Finalizing output files...'; - } else if (message) { - job.progress = message; - } - }); - - tracker.stderr.on('data', (chunk) => { - const raw = chunk.toString(); - context.stdout += raw; - const message = raw.trim(); - if (message.includes('NAL unit') || message.includes('partial file')) { - console.log(`[Tracker] (codec warning) ${message}`); - } else { - console.error(`[Tracker] ${message}`); - } - }); - - tracker.on('error', (error) => { - if (activeTracker === tracker) activeTracker = null; - if (isCurrent(context)) { - job.status = 'error'; - job.error = `Failed to start tracker: ${error.message}`; - job.progress = ''; - } - cleanup(); - }); - - tracker.on('close', async (code) => { - if (activeTracker === tracker) activeTracker = null; - if (!isCurrent(context)) { - cleanup(); - return; - } - - const endTime = Date.now(); - const elapsed = ((endTime - context.startTime) / 1000).toFixed(1); - job.endTime = endTime; - - if (code !== 0) { - job.status = 'error'; - job.error = `Tracker exited with code ${code}`; - job.progress = ''; - cleanup(); - return; - } - - try { - const syncInfo = parseTrackerSync(context.stdout); - const csvRows = countCsvRows(csvPath); - const videoFrames = fs.existsSync(rawVideo) - ? await getVideoFrameCount(ffmpegPath, rawVideo) - : null; - - if (!isCurrent(context)) { - cleanup(); - return; - } - - const frameCount = syncInfo?.framesProcessed ?? csvRows; - const syncOk = ( - syncInfo?.syncOk !== false - && csvRows === frameCount - && (videoFrames === null || videoFrames === csvRows) - ); - if (!syncOk) { - throw new Error( - `Frame sync mismatch (csv=${csvRows}, ` - + `video=${videoFrames}, tracker=${frameCount})`, - ); - } - if (!fs.existsSync(rawVideo)) { - throw new Error('tracked_raw.mp4 missing after tracker completed'); - } - - job.progress = 'Transcoding video for browser playback...'; - await transcodeVideo(ffmpegPath, rawVideo, browserVideo); - if (!isCurrent(context)) { - cleanup(); - return; - } - - const fps = readEventsFps(runEventsPath); - const metadata = { - framesProcessed: frameCount, - csvRows, - videoFrames, - expectedVideoFrames: syncInfo?.expectedVideoFrames ?? null, - syncOk, - fps, - timestamp: new Date().toISOString(), - }; - - replaceFile(csvPath, path.join(publicDir, 'data.csv'), token); - replaceFile(runEventsPath, eventsPath, token); - replaceFile(browserVideo, path.join(publicDir, 'tracked.mp4'), token); - writeJson(runMetadataPath, metadata); - - try { - snapshotRunToHistory({ - historyDir, - historyMetaPath, - runId: context.runId, - filename: context.filename, - durationSec: parseFloat(elapsed), - fps, - totalFrames: frameCount, - csvSrc: csvPath, - eventsSrc: runEventsPath, - }); - } catch (historyError) { - console.error(`[Server] History snapshot failed: ${historyError.message}`); - } - - job.frameCount = frameCount; - job.framesProcessed = frameCount; - job.status = 'done'; - job.progress = `Tracking complete! Processed ${frameCount} frames in ${elapsed}s`; - } catch (error) { - if (isCurrent(context)) { - job.status = 'error'; - job.error = error.message; - job.progress = ''; - } - console.error(`[Server] Post-processing failed: ${error.message}`); - } finally { - cleanup(); - } - }); -} - -app.get('/api/status', (req, res) => res.json({ ...job })); - -app.post('/api/upload', reserveUpload, upload.single('video'), (req, res) => { - if (!req.file) { - return res.status(400).json({ error: 'No video file provided' }); - } - if (job.status === 'uploading' || job.status === 'processing') { - safeUnlink(req.file.path); - return res.status(409).json({ - error: 'A video is already being processed. Please wait.', - }); - } - - resetJob(); - resetVerification(); - runSequence += 1; - job.runId = runSequence; - job.status = 'uploading'; - job.progress = 'Video uploaded, starting tracker...'; - job.uploadedFilename = req.file.originalname; - - const overrides = { - minArea: req.body.minArea, - maxArea: req.body.maxArea, - proximityThreshold: req.body.proximityThreshold, - boutMinFrames: req.body.boutMinFrames, - }; - - startTracking(req.file.path, overrides); - return res.json({ - success: true, - message: 'Upload received, processing started', - runId: runSequence, - }); -}); - -app.post('/api/reset', (req, res) => { - resetJob(); - res.json({ success: true }); -}); - -app.get('/api/events', (req, res) => { - const data = readJson(eventsPath, null); - if (!data) return res.status(404).json({ error: 'No events file found.' }); - return res.json(data); -}); - -app.get('/api/verification', (req, res) => res.json(readVerification())); - -app.post('/api/verification', (req, res) => { - const { eventId, verdict } = req.body || {}; - if (!eventId || !['confirmed', 'rejected'].includes(verdict)) { - return res.status(400).json({ - error: 'Body must include eventId and verdict (confirmed|rejected)', - }); - } - - const data = readVerification(); - const review = { - event_id: eventId, - verdict, - reviewed_at: new Date().toISOString(), - }; - const index = data.reviews.findIndex((item) => item.event_id === eventId); - if (index >= 0) data.reviews[index] = review; - else data.reviews.push(review); - writeJson(verificationPath, data); - return res.json({ success: true, review }); -}); - -app.post('/api/verification/reset', (req, res) => { - resetVerification(); - res.json({ success: true }); -}); - -app.get('/api/history', (req, res) => { - const history = readJson(historyMetaPath, { version: 1, runs: [] }); - res.json(Array.isArray(history.runs) ? history : { version: 1, runs: [] }); -}); - -app.delete('/api/history', (req, res) => { - writeJson(historyMetaPath, { version: 1, runs: [] }); - res.json({ success: true }); -}); - -app.get('/api/history/:runId', (req, res) => { - const history = readJson(historyMetaPath, { runs: [] }); - const entry = history.runs?.find((run) => run.runId === req.params.runId); - if (!entry) return res.status(404).json({ error: 'Run not found' }); - return res.json(entry); -}); - -app.listen(PORT, () => { - console.log('\n 🧬 Flyt API Server (safe job runner)'); - console.log(` ➜ Running on http://localhost:${PORT}`); - console.log(` ➜ Tracker: ${trackerScript}`); -}); From cf38a9cca316bef925baecbb49cec01415d0f916 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 03:19:46 +0530 Subject: [PATCH 15/75] refactor: remove obsolete tracker wrapper --- source app folder/tracker/tracker_safe.py | 261 ---------------------- 1 file changed, 261 deletions(-) delete mode 100644 source app folder/tracker/tracker_safe.py diff --git a/source app folder/tracker/tracker_safe.py b/source app folder/tracker/tracker_safe.py deleted file mode 100644 index 613679e..0000000 --- a/source app folder/tracker/tracker_safe.py +++ /dev/null @@ -1,261 +0,0 @@ -"""Safety wrapper around tracker.py. - -It preserves the existing tracker implementation while fixing: -- invalid/zero FPS passed to VideoWriter, -- the initialization-frame velocity spike, -- event detection treating numeric zero as a missing value. -""" - -import argparse -import csv -import json -import os -import runpy -import sys - -import cv2 - - -def parse_wrapper_args(): - parser = argparse.ArgumentParser(add_help=False) - parser.add_argument("--input") - parser.add_argument("--output-csv", default="fly_tracking_data.csv") - parser.add_argument("--output-events") - parser.add_argument("--proximity-threshold", type=float, default=60.0) - parser.add_argument("--bout-min-frames", type=int, default=90) - return parser.parse_known_args()[0] - - -def install_safe_video_writer(): - original_writer = cv2.VideoWriter - - def safe_writer(filename, fourcc, fps, frame_size, *args): - effective_fps = fps if fps and fps > 0 else 30.0 - writer = original_writer( - filename, - fourcc, - effective_fps, - frame_size, - *args, - ) - if not writer.isOpened(): - raise RuntimeError( - f"Could not create output video at {filename}" - ) - return writer - - cv2.VideoWriter = safe_writer - - -def load_csv_rows(csv_path): - if not os.path.exists(csv_path): - return [], [] - with open(csv_path, "r", encoding="utf-8", newline="") as handle: - reader = csv.DictReader(handle) - return reader.fieldnames or [], list(reader) - - -def save_csv_rows(csv_path, fieldnames, rows): - with open(csv_path, "w", encoding="utf-8", newline="") as handle: - writer = csv.DictWriter(handle, fieldnames=fieldnames) - writer.writeheader() - writer.writerows(rows) - - -def numeric(row, key, default): - value = row.get(key) - if value in (None, ""): - return default - try: - return float(value) - except (TypeError, ValueError): - return default - - -def fix_initial_velocity(csv_path): - fieldnames, rows = load_csv_rows(csv_path) - if not rows: - return rows - - for row in rows: - detected = ( - numeric(row, "fly1_area", 0.0) > 0 - or numeric(row, "fly2_area", 0.0) > 0 - ) - if detected: - for key in ( - "fly1_speed", - "fly2_speed", - "fly1_speed_pxsec", - "fly2_speed_pxsec", - ): - if key in row: - row[key] = "0.0" - break - - save_csv_rows(csv_path, fieldnames, rows) - return rows - - -def sustained_segments(rows, condition, minimum_frames): - segments = [] - start = None - - for row in rows: - frame = int(float(row["frame"])) - if condition(row): - if start is None: - start = frame - elif start is not None: - end = frame - 1 - if end - start + 1 >= minimum_frames: - segments.append((start, end)) - start = None - - if start is not None and rows: - end = int(float(rows[-1]["frame"])) - if end - start + 1 >= minimum_frames: - segments.append((start, end)) - - return segments - - -def event_record(event_id, event_type, start, end, fps, segment, reason): - duration_frames = end - start + 1 - proximities = [numeric(row, "proximity_distance", 0.0) for row in segment] - confidences = [numeric(row, "identity_confidence", 0.0) for row in segment] - occlusions = [numeric(row, "occlusion_flag", 0.0) for row in segment] - - return { - "id": event_id, - "type": event_type, - "start_frame": start, - "end_frame": end, - "start_time_sec": round(start / fps, 3), - "end_time_sec": round(end / fps, 3), - "duration_sec": round(duration_frames / fps, 3), - "mean_proximity_px": round( - sum(proximities) / len(proximities), - 2, - ) if proximities else 0.0, - "min_identity_confidence": round( - min(confidences), - 4, - ) if confidences else 0.0, - "occlusion_fraction": round( - sum(occlusions) / len(occlusions), - 4, - ) if occlusions else 0.0, - "detection_reason": reason, - } - - -def read_effective_fps(events_path, input_path): - if events_path and os.path.exists(events_path): - try: - with open(events_path, "r", encoding="utf-8") as handle: - fps = float(json.load(handle).get("fps", 0)) - if fps > 0: - return fps - except (OSError, ValueError, TypeError, json.JSONDecodeError): - pass - - capture = cv2.VideoCapture(input_path) if input_path else None - try: - fps = capture.get(cv2.CAP_PROP_FPS) if capture else 0 - return fps if fps and fps > 0 else 30.0 - finally: - if capture: - capture.release() - - -def regenerate_events(rows, args): - if not args.output_events: - return - - fps = read_effective_fps(args.output_events, args.input) - events = [] - counter = 1 - - courtship = sustained_segments( - rows, - lambda row: ( - numeric(row, "proximity_distance", 999.0) - < args.proximity_threshold - ), - max(1, args.bout_min_frames), - ) - for start, end in courtship: - segment = [ - row for row in rows - if start <= int(float(row["frame"])) <= end - ] - events.append(event_record( - f"evt-{counter:03d}", - "courtship_bout", - start, - end, - fps, - segment, - "proximity_sustained", - )) - counter += 1 - - low_confidence = sustained_segments( - rows, - lambda row: numeric(row, "identity_confidence", 1.0) < 0.2, - 30, - ) - for start, end in low_confidence: - segment = [ - row for row in rows - if start <= int(float(row["frame"])) <= end - ] - events.append(event_record( - f"evt-{counter:03d}", - "low_confidence_segment", - start, - end, - fps, - segment, - "identity_confidence_low", - )) - counter += 1 - - events.sort(key=lambda event: event["start_frame"]) - envelope = { - "version": 1, - "fps": round(fps, 3), - "total_frames": len(rows), - "detection_params": { - "proximity_threshold_px": args.proximity_threshold, - "bout_min_frames": max(1, args.bout_min_frames), - "low_confidence_threshold": 0.2, - "low_confidence_min_frames": 30, - }, - "events": events, - } - with open(args.output_events, "w", encoding="utf-8") as handle: - json.dump(envelope, handle, indent=2) - - -def main(): - args = parse_wrapper_args() - install_safe_video_writer() - tracker_path = os.path.join(os.path.dirname(__file__), "tracker.py") - - exit_code = 0 - try: - runpy.run_path(tracker_path, run_name="__main__") - except SystemExit as exc: - exit_code = int(exc.code or 0) - - if exit_code == 0: - rows = fix_initial_velocity(args.output_csv) - regenerate_events(rows, args) - - raise SystemExit(exit_code) - - -if __name__ == "__main__": - main() From 66c4235cc552bcb3cc8ecabcc5043568d02d4653 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 03:20:32 +0530 Subject: [PATCH 16/75] fix: cancel uploads interrupted by reset --- source app folder/dashboard/server.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/source app folder/dashboard/server.js b/source app folder/dashboard/server.js index 65f2c04..f594cfc 100644 --- a/source app folder/dashboard/server.js +++ b/source app folder/dashboard/server.js @@ -74,8 +74,9 @@ function blankJob() { const isBusy = () => uploadReserved || terminating || ['uploading', 'processing'].includes(job.status); const isCurrent = (context) => context.epoch === epoch && context.runId === job.runId; -function reserveUpload(_req, res, next) { +function reserveUpload(req, res, next) { if (isBusy()) return res.status(409).json({ error: 'A video is already uploading, processing, or stopping.' }); + req.uploadEpoch = epoch; uploadReserved = true; let released = false; const release = () => { if (!released) { released = true; uploadReserved = false; } }; @@ -266,9 +267,9 @@ app.get('/api/status', (_req, res) => res.json({ ...job })); app.post('/api/upload', reserveUpload, upload.single('video'), async (req, res) => { if (!req.file) return res.status(400).json({ error: 'No video file provided' }); - if (isBusy() && !uploadReserved) { + if (req.uploadEpoch !== epoch) { safeUnlink(req.file.path); - return res.status(409).json({ error: 'A video is already being processed.' }); + return res.status(409).json({ error: 'Upload was cancelled by a reset.' }); } await stopActiveTracker(); runSequence += 1; From fa5d73e44ad1d3e3bc88ea83de88e2d8a51b2ffd Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 03:20:52 +0530 Subject: [PATCH 17/75] fix: lint committed Node regression tests --- source app folder/dashboard/eslint.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source app folder/dashboard/eslint.config.js b/source app folder/dashboard/eslint.config.js index 030c2c3..3922469 100644 --- a/source app folder/dashboard/eslint.config.js +++ b/source app folder/dashboard/eslint.config.js @@ -27,7 +27,7 @@ export default defineConfig([ }, }, { - files: ['server*.js'], + files: ['server*.js', 'tests/**/*.js'], languageOptions: { globals: globals.node, }, From 92b49e2cfd7f6cbc4b83fbf0bfef93b0dabaa7a1 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 03:22:51 +0530 Subject: [PATCH 18/75] fix: preserve distance overlay in canonical tracker --- source app folder/tracker/tracker.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/source app folder/tracker/tracker.py b/source app folder/tracker/tracker.py index 6bab20e..6064216 100644 --- a/source app folder/tracker/tracker.py +++ b/source app folder/tracker/tracker.py @@ -284,6 +284,19 @@ def run_tracker(args: argparse.Namespace) -> int: cv2.circle(frame, f1_coords, 5, (255, 0, 0), -1) cv2.circle(frame, f2_coords, 5, (0, 0, 255), -1) cv2.line(frame, f1_coords, f2_coords, (0, 255, 255), 2) + text_pos = ( + min(f1_coords[0], f2_coords[0]), + max(0, min(f1_coords[1], f2_coords[1]) - 10), + ) + cv2.putText( + frame, + f"Dist: {int(proximity)}px", + text_pos, + cv2.FONT_HERSHEY_SIMPLEX, + 0.6, + (0, 255, 255), + 2, + ) elif len(centroids) == 1: point = centroids[0] was_initialized = is_initialized From 9fe0fe0ee1e5a321166c6e59b6a21b73f94af967 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 10:18:36 +0530 Subject: [PATCH 19/75] fix: enforce frame integrity and rollback-safe process controls --- source app folder/dashboard/server-utils.js | 254 +++++++++++++++++--- 1 file changed, 225 insertions(+), 29 deletions(-) diff --git a/source app folder/dashboard/server-utils.js b/source app folder/dashboard/server-utils.js index f38cf3a..3c092a9 100644 --- a/source app folder/dashboard/server-utils.js +++ b/source app folder/dashboard/server-utils.js @@ -1,8 +1,15 @@ import { randomUUID } from 'crypto'; -import { spawn } from 'child_process'; +import { spawn as nodeSpawn } from 'child_process'; import fs from 'fs'; import path from 'path'; +export class AbortRunError extends Error { + constructor(message = 'Run aborted') { + super(message); + this.name = 'AbortError'; + } +} + export function ensureDir(dirPath) { fs.mkdirSync(dirPath, { recursive: true }); } @@ -27,6 +34,7 @@ export function readJson(filePath, fallback) { } export function writeJson(filePath, data) { + ensureDir(path.dirname(filePath)); const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`; fs.writeFileSync(tempPath, JSON.stringify(data, null, 2)); fs.renameSync(tempPath, filePath); @@ -38,10 +46,11 @@ export function countCsvRows(csvPath) { } export function parseTrackerSync(stdout) { - const match = stdout.match( - /TRACKER_SYNC frames_processed=(\d+) csv_rows=(\d+) expected_video_frames=(\d+) sync_ok=(true|false)/, - ); - if (!match) return null; + const matches = [...String(stdout).matchAll( + /TRACKER_SYNC frames_processed=(\d+) csv_rows=(\d+) expected_video_frames=(\d+) sync_ok=(true|false)/g, + )]; + if (matches.length !== 1) return null; + const match = matches[0]; return { framesProcessed: Number(match[1]), csvRows: Number(match[2]), @@ -50,47 +59,188 @@ export function parseTrackerSync(stdout) { }; } -export function getVideoFrameCount(ffmpegPath, videoPath) { - return new Promise((resolve, reject) => { - const child = spawn(ffmpegPath, ['-i', videoPath, '-map', '0:v:0', '-f', 'null', '-']); - let stderr = ''; - child.stderr.on('data', (data) => { stderr += data.toString(); }); - child.once('error', reject); - child.once('close', () => { - const matches = [...stderr.matchAll(/frame=\s*(\d+)/g)]; - if (!matches.length) reject(new Error('Could not determine video frame count from ffmpeg')); - else resolve(Number(matches.at(-1)[1])); - }); - }); +function requirePositiveInteger(name, value) { + if (!Number.isInteger(value) || value <= 0) { + throw new Error(`Frame integrity evidence missing or invalid: ${name}=${value}`); + } +} + +export function validateFrameIntegrity({ + syncInfo, + inputFrames, + csvRows, + rawVideoFrames, + finalVideoFrames, +}) { + if (!syncInfo) throw new Error('Frame integrity evidence missing: TRACKER_SYNC marker required'); + if (syncInfo.syncOk !== true) throw new Error('Tracker reported frame synchronization failure'); + + const counts = { + inputFrames, + trackerFrames: syncInfo.framesProcessed, + trackerCsvRows: syncInfo.csvRows, + csvRows, + rawVideoFrames, + finalVideoFrames, + }; + Object.entries(counts).forEach(([name, value]) => requirePositiveInteger(name, value)); + + if (syncInfo.expectedVideoFrames > 0) { + requirePositiveInteger('trackerExpectedFrames', syncInfo.expectedVideoFrames); + counts.trackerExpectedFrames = syncInfo.expectedVideoFrames; + } + + const unique = new Set(Object.values(counts)); + if (unique.size !== 1) { + const detail = Object.entries(counts).map(([key, value]) => `${key}=${value}`).join(', '); + throw new Error(`Frame integrity mismatch (${detail})`); + } + + return { ...counts, syncOk: true }; +} + +const terminationPromises = new WeakMap(); + +export function terminateChild(child, { graceMs = 1000, hardKillMs = 3000 } = {}) { + if (!child || child.exitCode !== null || child.signalCode) return Promise.resolve(); + if (terminationPromises.has(child)) return terminationPromises.get(child); + + const promise = new Promise((resolve, reject) => { + let settled = false; + let graceTimer; + let hardTimer; + const cleanup = () => { + clearTimeout(graceTimer); + clearTimeout(hardTimer); + child.removeListener('close', finish); + child.removeListener('error', finish); + }; + const finish = () => { + if (settled) return; + settled = true; + cleanup(); + resolve(); + }; + const fail = () => { + if (settled) return; + settled = true; + cleanup(); + reject(new Error('Child process did not terminate after SIGKILL')); + }; + + child.once('close', finish); + child.once('error', finish); + try { child.kill('SIGTERM'); } catch { finish(); return; } + graceTimer = setTimeout(() => { + if (settled) return; + try { child.kill('SIGKILL'); } catch { finish(); return; } + hardTimer = setTimeout(fail, hardKillMs); + hardTimer.unref?.(); + }, graceMs); + graceTimer.unref?.(); + }).finally(() => terminationPromises.delete(child)); + + terminationPromises.set(child, promise); + return promise; } -export function transcodeVideo(ffmpegPath, rawPath, finalPath) { +export function runCommand(command, args, { + spawnFn = nodeSpawn, + signal, + children, + onStdout, + onStderr, + killOptions, +} = {}) { + if (signal?.aborted) return Promise.reject(new AbortRunError()); + return new Promise((resolve, reject) => { - const child = spawn(ffmpegPath, [ - '-i', rawPath, '-vcodec', 'libx264', '-preset', 'veryfast', - '-pix_fmt', 'yuv420p', '-movflags', '+faststart', '-an', '-y', finalPath, - ]); + let child; + try { + child = spawnFn(command, args); + } catch (error) { + reject(error); + return; + } + children?.add(child); + let stdout = ''; let stderr = ''; - child.stderr.on('data', (data) => { stderr += data.toString(); }); - child.once('error', (error) => reject(new Error(`Failed to start ffmpeg: ${error.message}`))); - child.once('close', (code) => { - if (code === 0) resolve(); - else reject(new Error(`ffmpeg transcode failed (exit ${code}): ${stderr.slice(-500)}`)); + let settled = false; + + const cleanup = () => { + children?.delete(child); + signal?.removeEventListener('abort', onAbort); + }; + const settle = (handler, value) => { + if (settled) return; + settled = true; + cleanup(); + handler(value); + }; + const onAbort = () => { + terminateChild(child, killOptions) + .then(() => settle(reject, new AbortRunError())) + .catch((error) => settle(reject, error)); + }; + + child.stdout?.on('data', (chunk) => { + const text = chunk.toString(); + stdout += text; + onStdout?.(text); + }); + child.stderr?.on('data', (chunk) => { + const text = chunk.toString(); + stderr += text; + onStderr?.(text); }); + child.once('error', (error) => settle(reject, error)); + child.once('close', (code, closeSignal) => { + if (signal?.aborted) settle(reject, new AbortRunError()); + else settle(resolve, { code, signal: closeSignal, stdout, stderr }); + }); + signal?.addEventListener('abort', onAbort, { once: true }); + if (signal?.aborted) onAbort(); }); } -export function publishBundle(entries, token) { +export async function getVideoFrameCount(ffmpegPath, videoPath, options = {}) { + const result = await runCommand(ffmpegPath, [ + '-hide_banner', '-i', videoPath, '-map', '0:v:0', '-f', 'null', '-', + ], options); + if (result.code !== 0) { + throw new Error(`ffmpeg frame count failed (exit ${result.code}): ${result.stderr.slice(-500)}`); + } + const matches = [...result.stderr.matchAll(/frame=\s*(\d+)/g)]; + if (!matches.length) throw new Error(`Could not determine video frame count for ${videoPath}`); + const count = Number(matches.at(-1)[1]); + requirePositiveInteger('decodedVideoFrames', count); + return count; +} + +export async function transcodeVideo(ffmpegPath, rawPath, finalPath, options = {}) { + const result = await runCommand(ffmpegPath, [ + '-hide_banner', '-i', rawPath, '-vcodec', 'libx264', '-preset', 'veryfast', + '-pix_fmt', 'yuv420p', '-movflags', '+faststart', '-an', '-y', finalPath, + ], options); + if (result.code !== 0) { + throw new Error(`ffmpeg transcode failed (exit ${result.code}): ${result.stderr.slice(-500)}`); + } +} + +export function publishBundle(entries, token, { faultInjector } = {}) { const staged = []; const backups = []; const published = []; try { for (const { source, destination } of entries) { if (!fs.existsSync(source)) throw new Error(`Missing publish source: ${source}`); + ensureDir(path.dirname(destination)); const stage = `${destination}.${token}.new`; fs.copyFileSync(source, stage); staged.push(stage); } + faultInjector?.('staged', -1); + for (const { destination } of entries) { if (fs.existsSync(destination)) { const backup = `${destination}.${token}.bak`; @@ -98,13 +248,16 @@ export function publishBundle(entries, token) { backups.push({ destination, backup }); } } + faultInjector?.('backed-up', -1); + entries.forEach(({ destination }, index) => { fs.renameSync(staged[index], destination); published.push(destination); + faultInjector?.('published', index); }); backups.forEach(({ backup }) => safeUnlink(backup)); } catch (error) { - published.forEach((destination) => safeUnlink(destination)); + published.reverse().forEach((destination) => safeUnlink(destination)); backups.reverse().forEach(({ destination, backup }) => { if (fs.existsSync(backup)) fs.renameSync(backup, destination); }); @@ -113,6 +266,18 @@ export function publishBundle(entries, token) { } } +export function recoverPublishArtifacts(directory) { + if (!fs.existsSync(directory)) return; + const names = fs.readdirSync(directory); + names.filter((name) => name.endsWith('.new')).forEach((name) => safeUnlink(path.join(directory, name))); + names.filter((name) => name.endsWith('.bak')).forEach((name) => { + const backup = path.join(directory, name); + const destination = backup.replace(/\.[^.]+\.bak$/, ''); + if (!fs.existsSync(destination)) fs.renameSync(backup, destination); + else safeUnlink(backup); + }); +} + export function buildTrackerArgs(trackerScript, inputPath, outputs, overrides = {}, defaults) { const numberOr = (value, fallback) => { const parsed = Number(value); @@ -145,6 +310,35 @@ export function readEventsFps(eventsPath) { return data && Number.isFinite(data.fps) ? data.fps : null; } +export function readCsvAvgProximity(csvPath) { + if (!fs.existsSync(csvPath)) return null; + const lines = fs.readFileSync(csvPath, 'utf8').trim().split(/\r?\n/); + if (lines.length < 2) return null; + const headers = lines[0].split(','); + const proximityIndex = headers.indexOf('proximity_distance'); + const occlusionIndex = headers.indexOf('occlusion_flag'); + if (proximityIndex < 0) return null; + let total = 0; + let count = 0; + lines.slice(1).forEach((line) => { + const columns = line.split(','); + const proximity = Number(columns[proximityIndex]); + const occluded = occlusionIndex >= 0 && Number(columns[occlusionIndex]) === 1; + if (Number.isFinite(proximity) && !occluded) { + total += proximity; + count += 1; + } + }); + return count ? Math.round((total / count) * 100) / 100 : null; +} + +export function countCourtshipBouts(eventsPath) { + const data = readJson(eventsPath, { events: [] }); + return Array.isArray(data.events) + ? data.events.filter((event) => event.type === 'courtship_bout').length + : 0; +} + export function scopeEventsForHistory(eventsPath, runId, destinationPath) { const data = readJson(eventsPath, { version: 1, events: [] }); data.events = Array.isArray(data.events) ? data.events.map((event) => ({ @@ -172,6 +366,8 @@ export function snapshotRunToHistory({ durationSec: Math.round((durationSec || 0) * 10) / 10, fps: fps || null, totalFrames: totalFrames || null, + avgProximity: readCsvAvgProximity(csvSrc), + detectedBouts: countCourtshipBouts(eventsSrc), }; const history = readJson(historyMetaPath, { version: 1, runs: [] }); if (!Array.isArray(history.runs)) history.runs = []; From 1dbdfc9079d6f806b0bd3812647e05b2ed052e49 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 10:19:20 +0530 Subject: [PATCH 20/75] fix: make the server fail closed and testable under real races --- source app folder/dashboard/server.js | 675 +++++++++++++++----------- 1 file changed, 400 insertions(+), 275 deletions(-) diff --git a/source app folder/dashboard/server.js b/source app folder/dashboard/server.js index f594cfc..a6edec5 100644 --- a/source app folder/dashboard/server.js +++ b/source app folder/dashboard/server.js @@ -1,12 +1,13 @@ import { randomUUID } from 'crypto'; -import { spawn } from 'child_process'; +import { spawn as nodeSpawn } from 'child_process'; import express from 'express'; -import ffmpegPath from 'ffmpeg-static'; +import defaultFfmpegPath from 'ffmpeg-static'; import fs from 'fs'; import multer from 'multer'; import path from 'path'; import { fileURLToPath } from 'url'; import { + AbortRunError, buildTrackerArgs, countCsvRows, ensureDir, @@ -15,230 +16,323 @@ import { publishBundle, readEventsFps, readJson, + recoverPublishArtifacts, resolveVerificationPath, + runCommand, safeRemoveDir, safeUnlink, snapshotRunToHistory, + terminateChild, transcodeVideo, + validateFrameIntegrity, writeJson, } from './server-utils.js'; -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const app = express(); -const PORT = 3001; -const uploadsDir = path.join(__dirname, 'uploads'); -const publicDir = path.join(__dirname, 'public'); -const historyDir = path.join(publicDir, 'history'); -const historyMetaPath = path.join(publicDir, 'history.json'); -const verificationPath = path.join(publicDir, 'verification.json'); -const trackerDir = path.join(__dirname, '..', 'tracker'); -const trackerScript = path.join(trackerDir, 'tracker.py'); -const isWindows = process.platform === 'win32'; -const pythonExe = path.join( - trackerDir, 'venv', isWindows ? 'Scripts' : 'bin', isWindows ? 'python.exe' : 'python', -); -const defaults = { minArea: 30, maxArea: 0, proximityThreshold: 60, boutMinFrames: 90 }; -for (const dir of [uploadsDir, publicDir, historyDir]) ensureDir(dir); - -const storage = multer.diskStorage({ - destination: (_req, _file, callback) => callback(null, uploadsDir), - filename: (_req, file, callback) => { - callback(null, `input-${Date.now()}-${randomUUID()}${path.extname(file.originalname).toLowerCase()}`); - }, -}); -const upload = multer({ storage, limits: { fileSize: 10 * 1024 * 1024 * 1024 } }); - -app.use(express.json()); -app.use((req, res, next) => { - res.header('Access-Control-Allow-Origin', '*'); - res.header('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS'); - res.header('Access-Control-Allow-Headers', 'Content-Type'); - if (req.method === 'OPTIONS') return res.sendStatus(204); - return next(); -}); - -let runSequence = 0; -let epoch = 0; -let uploadReserved = false; -let terminating = false; -let activeTracker = null; -let job = blankJob(); +const modulePath = fileURLToPath(import.meta.url); +const moduleDir = path.dirname(modulePath); +const DEFAULT_PORT = 3001; +const DEFAULT_ALLOWED_ORIGINS = [ + 'http://localhost:3001', + 'http://127.0.0.1:3001', + 'http://localhost:5173', + 'http://127.0.0.1:5173', +]; function blankJob() { return { - runId: null, status: 'idle', progress: '', framesProcessed: 0, - totalFrames: null, frameCount: null, error: null, startTime: null, endTime: null, + runId: null, + status: 'idle', + progress: '', + framesProcessed: 0, + totalFrames: null, + frameCount: null, + error: null, + startTime: null, + endTime: null, }; } -const isBusy = () => uploadReserved || terminating || ['uploading', 'processing'].includes(job.status); -const isCurrent = (context) => context.epoch === epoch && context.runId === job.runId; - -function reserveUpload(req, res, next) { - if (isBusy()) return res.status(409).json({ error: 'A video is already uploading, processing, or stopping.' }); - req.uploadEpoch = epoch; - uploadReserved = true; - let released = false; - const release = () => { if (!released) { released = true; uploadReserved = false; } }; - res.once('finish', release); - res.once('close', release); - return next(); +function parseAllowedOrigins(value) { + if (!value) return DEFAULT_ALLOWED_ORIGINS; + return value.split(',').map((item) => item.trim()).filter(Boolean); } -function stopActiveTracker() { - epoch += 1; - const tracker = activeTracker; - activeTracker = null; - job = blankJob(); - if (!tracker) return Promise.resolve(); - terminating = true; - return new Promise((resolve) => { - let settled = false; - const finish = () => { - if (settled) return; - settled = true; - terminating = false; - resolve(); - }; - tracker.once('close', finish); - tracker.once('error', finish); - try { tracker.kill(); } catch { finish(); } - setTimeout(finish, 3000).unref(); +export function createFlytServer(options = {}) { + const rootDir = options.rootDir || moduleDir; + const uploadsDir = options.uploadsDir || path.join(rootDir, 'uploads'); + const publicDir = options.publicDir || path.join(rootDir, 'public'); + const historyDir = options.historyDir || path.join(publicDir, 'history'); + const historyMetaPath = options.historyMetaPath || path.join(publicDir, 'history.json'); + const verificationPath = options.verificationPath || path.join(publicDir, 'verification.json'); + const trackerDir = options.trackerDir || path.join(rootDir, '..', 'tracker'); + const trackerScript = options.trackerScript || path.join(trackerDir, 'tracker.py'); + const isWindows = process.platform === 'win32'; + const pythonExe = options.pythonExe || path.join( + trackerDir, + 'venv', + isWindows ? 'Scripts' : 'bin', + isWindows ? 'python.exe' : 'python', + ); + const ffmpegPath = options.ffmpegPath || defaultFfmpegPath; + const spawnFn = options.spawnFn || nodeSpawn; + const defaults = options.defaults || { + minArea: 30, + maxArea: 0, + proximityThreshold: 60, + boutMinFrames: 90, + }; + const allowedOrigins = new Set(options.allowedOrigins || parseAllowedOrigins(process.env.FLYT_ALLOWED_ORIGINS)); + const killOptions = options.killOptions || { graceMs: 1000, hardKillMs: 3000 }; + + for (const dir of [uploadsDir, publicDir, historyDir]) ensureDir(dir); + recoverPublishArtifacts(publicDir); + + const app = express(); + const storage = multer.diskStorage({ + destination: (_req, _file, callback) => callback(null, uploadsDir), + filename: (_req, file, callback) => { + callback(null, `input-${Date.now()}-${randomUUID()}${path.extname(file.originalname).toLowerCase()}`); + }, }); -} + const upload = multer({ storage, limits: { fileSize: 10 * 1024 * 1024 * 1024 } }); -function readVerification(filePath = verificationPath) { - const data = readJson(filePath, { version: 1, reviews: [] }); - return Array.isArray(data.reviews) ? data : { version: 1, reviews: [] }; -} + app.use(express.json()); + app.use((req, res, next) => { + const origin = req.get('Origin'); + if (!origin) return next(); + if (!allowedOrigins.has(origin)) { + return res.status(403).json({ error: 'Cross-origin request denied.' }); + } + res.header('Access-Control-Allow-Origin', origin); + res.header('Vary', 'Origin'); + res.header('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS'); + res.header('Access-Control-Allow-Headers', 'Content-Type'); + if (req.method === 'OPTIONS') return res.sendStatus(204); + return next(); + }); -function startTracking(inputPath, overrides) { - const context = { - runId: job.runId, - epoch, - startedAt: Date.now(), - stdout: '', - filename: job.uploadedFilename, - spawnFailed: false, - }; - const token = `${context.runId}-${context.epoch}-${randomUUID()}`; - const workDir = path.join(uploadsDir, `run-${token}`); - ensureDir(workDir); - const outputs = { - rawVideo: path.join(workDir, 'tracked_raw.mp4'), - browserVideo: path.join(workDir, 'tracked.mp4'), - csv: path.join(workDir, 'data.csv'), - events: path.join(workDir, 'events.json'), - metadata: path.join(workDir, 'run_metadata.json'), - verification: path.join(workDir, 'verification.json'), - }; + let runSequence = 0; + let epoch = 0; + let uploadReserved = false; + let terminating = false; + let activeRun = null; + let job = blankJob(); - job = { - ...job, - status: 'processing', - progress: 'Initializing tracker pipeline...', - framesProcessed: 0, - startTime: context.startedAt, - error: null, - overrides, + const services = { + countFrames: options.services?.countFrames || (filePath, context) => getVideoFrameCount( + ffmpegPath, + filePath, + { + spawnFn, + signal: context.controller.signal, + children: context.children, + killOptions, + }, + ), + runTracker: options.services?.runTracker || async (inputPath, outputs, overrides, context) => runCommand( + pythonExe, + buildTrackerArgs(trackerScript, inputPath, outputs, overrides, defaults), + { + spawnFn, + signal: context.controller.signal, + children: context.children, + killOptions, + onStdout: (text) => { + const match = text.match(/Processed (\d+) frames/); + if (match && isCurrent(context)) { + job.framesProcessed = Number(match[1]); + job.progress = `Processed ${job.framesProcessed} frames...`; + } else if (text.includes('Tracking completed') && isCurrent(context)) { + job.progress = 'Validating tracker output...'; + } + }, + onStderr: (text) => console.error(`[Tracker] ${text.trim()}`), + }, + ), + transcode: options.services?.transcode || (rawPath, finalPath, context) => transcodeVideo( + ffmpegPath, + rawPath, + finalPath, + { + spawnFn, + signal: context.controller.signal, + children: context.children, + killOptions, + }, + ), + publish: options.services?.publish || ((entries, token) => publishBundle(entries, token)), + snapshot: options.services?.snapshot || snapshotRunToHistory, }; - const tracker = spawn( - pythonExe, - buildTrackerArgs(trackerScript, inputPath, outputs, overrides, defaults), + const isBusy = () => ( + uploadReserved + || terminating + || ['uploading', 'processing', 'stopping'].includes(job.status) + ); + const isCurrent = (context) => ( + activeRun === context + && context.epoch === epoch + && !context.controller.signal.aborted ); - activeTracker = tracker; - const cleanup = () => { safeUnlink(inputPath); safeRemoveDir(workDir); }; - - tracker.stdout.on('data', (chunk) => { - const raw = chunk.toString(); - context.stdout += raw; - if (!isCurrent(context)) return; - const match = raw.match(/Processed (\d+) frames/); - if (match) { - job.framesProcessed = Number(match[1]); - job.progress = `Processed ${job.framesProcessed} frames...`; - } else if (raw.includes('Tracking completed')) { - job.progress = 'Finalizing output files...'; - } - }); - tracker.stderr.on('data', (chunk) => { - context.stdout += chunk.toString(); - console.error(`[Tracker] ${chunk.toString().trim()}`); - }); - tracker.once('error', (error) => { - context.spawnFailed = true; - if (activeTracker === tracker) activeTracker = null; - if (isCurrent(context)) { - job.status = 'error'; - job.error = `Failed to start tracker: ${error.message}`; - job.progress = ''; + function reserveUpload(req, res, next) { + if (isBusy()) { + return res.status(409).json({ error: 'A video is already uploading, processing, or stopping.' }); } - cleanup(); - }); + req.uploadEpoch = epoch; + uploadReserved = true; + let released = false; + const release = () => { + if (!released) { + released = true; + uploadReserved = false; + } + }; + res.once('finish', release); + res.once('close', release); + return next(); + } + + function readVerification(filePath = verificationPath) { + const data = readJson(filePath, { version: 1, reviews: [] }); + return Array.isArray(data.reviews) ? data : { version: 1, reviews: [] }; + } - tracker.once('close', async (code) => { - if (activeTracker === tracker) activeTracker = null; - if (context.spawnFailed) return; - if (!isCurrent(context)) { cleanup(); return; } - job.endTime = Date.now(); - if (code !== 0) { - job.status = 'error'; - job.error = `Tracker exited with code ${code}`; - job.progress = ''; - cleanup(); - return; + async function stopActiveRun() { + epoch += 1; + const context = activeRun; + activeRun = null; + job = { ...blankJob(), status: context ? 'stopping' : 'idle' }; + if (!context) return; + + terminating = true; + context.controller.abort(); + try { + const children = [...context.children]; + await Promise.all(children.map((child) => terminateChild(child, killOptions))); + await context.done.catch((error) => { + if (error?.name !== 'AbortError') throw error; + }); + job = blankJob(); + } catch (error) { + job = { + ...blankJob(), + status: 'error', + error: `Could not terminate active run: ${error.message}`, + }; + throw error; + } finally { + if (context.children.size === 0) terminating = false; } + } + + function createRunContext(inputPath, filename, overrides) { + const runId = ++runSequence; + const runEpoch = epoch; + const token = `${runId}-${runEpoch}-${randomUUID()}`; + const workDir = path.join(uploadsDir, `run-${token}`); + ensureDir(workDir); + return { + runId, + epoch: runEpoch, + token, + workDir, + inputPath, + filename, + overrides, + startedAt: Date.now(), + controller: new AbortController(), + children: new Set(), + done: null, + }; + } + + async function executeRun(context) { + const outputs = { + rawVideo: path.join(context.workDir, 'tracked_raw.mp4'), + browserVideo: path.join(context.workDir, 'tracked.mp4'), + csv: path.join(context.workDir, 'data.csv'), + events: path.join(context.workDir, 'events.json'), + metadata: path.join(context.workDir, 'run_metadata.json'), + verification: path.join(context.workDir, 'verification.json'), + }; + const ensureCurrent = () => { + if (!isCurrent(context)) throw new AbortRunError(); + }; + try { - const syncInfo = parseTrackerSync(context.stdout); - const csvRows = countCsvRows(outputs.csv); - const videoFrames = fs.existsSync(outputs.rawVideo) - ? await getVideoFrameCount(ffmpegPath, outputs.rawVideo) - : null; - if (!isCurrent(context)) { cleanup(); return; } - const frameCount = syncInfo?.framesProcessed ?? csvRows; - const syncOk = syncInfo?.syncOk !== false - && csvRows === frameCount - && (videoFrames === null || videoFrames === csvRows); - if (!syncOk) throw new Error( - `Frame sync mismatch (csv=${csvRows}, video=${videoFrames}, tracker=${frameCount})`, + job.status = 'processing'; + job.progress = 'Counting input frames...'; + const inputFrames = await services.countFrames(context.inputPath, context); + ensureCurrent(); + + job.progress = 'Running tracker...'; + const trackerResult = await services.runTracker( + context.inputPath, + outputs, + context.overrides, + context, ); - if (!fs.existsSync(outputs.rawVideo)) throw new Error('tracked_raw.mp4 missing'); + ensureCurrent(); + if (trackerResult.code !== 0) { + throw new Error(`Tracker exited with code ${trackerResult.code}: ${trackerResult.stderr.slice(-500)}`); + } + + const syncInfo = parseTrackerSync(trackerResult.stdout); + if (!syncInfo) throw new Error('Frame integrity evidence missing: TRACKER_SYNC marker required'); + if (!fs.existsSync(outputs.csv)) throw new Error('Tracker data.csv missing'); + if (!fs.existsSync(outputs.rawVideo)) throw new Error('Tracker raw video missing'); + if (!fs.existsSync(outputs.events)) throw new Error('Tracker events.json missing'); + + const csvRows = countCsvRows(outputs.csv); + job.progress = 'Counting raw output frames...'; + const rawVideoFrames = await services.countFrames(outputs.rawVideo, context); + ensureCurrent(); - job.progress = 'Transcoding video for browser playback...'; - await transcodeVideo(ffmpegPath, outputs.rawVideo, outputs.browserVideo); - if (!isCurrent(context)) { cleanup(); return; } + job.progress = 'Transcoding browser video...'; + await services.transcode(outputs.rawVideo, outputs.browserVideo, context); + ensureCurrent(); + job.progress = 'Counting final output frames...'; + const finalVideoFrames = await services.countFrames(outputs.browserVideo, context); + ensureCurrent(); + + const integrity = validateFrameIntegrity({ + syncInfo, + inputFrames, + csvRows, + rawVideoFrames, + finalVideoFrames, + }); const fps = readEventsFps(outputs.events); const metadata = { - framesProcessed: frameCount, - csvRows, - videoFrames, - expectedVideoFrames: syncInfo?.expectedVideoFrames ?? null, - syncOk, + ...integrity, + expectedVideoFrames: syncInfo.expectedVideoFrames, fps, timestamp: new Date().toISOString(), }; writeJson(outputs.metadata, metadata); writeJson(outputs.verification, { version: 1, reviews: [] }); + ensureCurrent(); - publishBundle([ + job.progress = 'Publishing validated results...'; + services.publish([ { source: outputs.csv, destination: path.join(publicDir, 'data.csv') }, { source: outputs.events, destination: path.join(publicDir, 'events.json') }, { source: outputs.browserVideo, destination: path.join(publicDir, 'tracked.mp4') }, { source: outputs.metadata, destination: path.join(publicDir, 'run_metadata.json') }, { source: outputs.verification, destination: verificationPath }, - ], token); + ], context.token); + ensureCurrent(); try { - snapshotRunToHistory({ + services.snapshot({ historyDir, historyMetaPath, filename: context.filename, - durationSec: (job.endTime - context.startedAt) / 1000, + durationSec: (Date.now() - context.startedAt) / 1000, fps, - totalFrames: frameCount, + totalFrames: integrity.inputFrames, csvSrc: outputs.csv, eventsSrc: outputs.events, }); @@ -246,111 +340,142 @@ function startTracking(inputPath, overrides) { console.error(`[Server] History snapshot failed: ${historyError.message}`); } - job.frameCount = frameCount; - job.framesProcessed = frameCount; - job.status = 'done'; - job.progress = `Tracking complete! Processed ${frameCount} frames.`; + job = { + ...job, + frameCount: integrity.inputFrames, + framesProcessed: integrity.inputFrames, + totalFrames: integrity.inputFrames, + status: 'done', + progress: `Tracking complete! Processed ${integrity.inputFrames} frames.`, + endTime: Date.now(), + }; } catch (error) { if (isCurrent(context)) { - job.status = 'error'; - job.error = error.message; - job.progress = ''; + job = { + ...job, + status: 'error', + progress: '', + error: error.message, + endTime: Date.now(), + }; } - console.error(`[Server] Post-processing failed: ${error.message}`); + if (error?.name !== 'AbortError') console.error(`[Server] Run failed: ${error.message}`); + throw error; } finally { - cleanup(); + safeUnlink(context.inputPath); + safeRemoveDir(context.workDir); + if (activeRun === context && context.children.size === 0) activeRun = null; } + } + + app.get('/api/status', (_req, res) => res.json({ ...job })); + + app.post('/api/upload', reserveUpload, upload.single('video'), (req, res) => { + if (!req.file) return res.status(400).json({ error: 'No video file provided' }); + if (req.uploadEpoch !== epoch) { + safeUnlink(req.file.path); + return res.status(409).json({ error: 'Upload was cancelled by a reset.' }); + } + const overrides = { + minArea: req.body.minArea, + maxArea: req.body.maxArea, + proximityThreshold: req.body.proximityThreshold, + boutMinFrames: req.body.boutMinFrames, + }; + const context = createRunContext(req.file.path, req.file.originalname, overrides); + activeRun = context; + job = { + ...blankJob(), + runId: context.runId, + status: 'uploading', + progress: 'Upload complete. Starting validation pipeline...', + uploadedFilename: context.filename, + startTime: context.startedAt, + }; + context.done = executeRun(context).catch(() => {}); + return res.json({ success: true, runId: context.runId }); }); -} -app.get('/api/status', (_req, res) => res.json({ ...job })); + app.post('/api/reset', async (_req, res) => { + try { + await stopActiveRun(); + return res.json({ success: true }); + } catch (error) { + return res.status(500).json({ error: error.message }); + } + }); -app.post('/api/upload', reserveUpload, upload.single('video'), async (req, res) => { - if (!req.file) return res.status(400).json({ error: 'No video file provided' }); - if (req.uploadEpoch !== epoch) { - safeUnlink(req.file.path); - return res.status(409).json({ error: 'Upload was cancelled by a reset.' }); - } - await stopActiveTracker(); - runSequence += 1; - job = { - ...blankJob(), - runId: runSequence, - status: 'uploading', - progress: 'Video uploaded, starting tracker...', - uploadedFilename: req.file.originalname, - }; - const overrides = { - minArea: req.body.minArea, - maxArea: req.body.maxArea, - proximityThreshold: req.body.proximityThreshold, - boutMinFrames: req.body.boutMinFrames, + app.get('/api/events', (_req, res) => { + const data = readJson(path.join(publicDir, 'events.json'), null); + if (!data) return res.status(404).json({ error: 'No events file found.' }); + return res.json(data); + }); + + app.get('/api/verification', (req, res) => { + if (!req.query.runId) return res.json(readVerification()); + const history = readJson(historyMetaPath, { runs: [] }); + const known = history.runs?.some((run) => run.runId === req.query.runId); + if (!known) return res.status(404).json({ error: 'Run not found' }); + return res.json(readVerification(path.join(historyDir, req.query.runId, 'verification.json'))); + }); + + app.post('/api/verification', (req, res) => { + const { eventId, verdict } = req.body || {}; + if (!eventId || !['confirmed', 'rejected'].includes(verdict)) { + return res.status(400).json({ error: 'Body must include eventId and verdict.' }); + } + try { + const filePath = resolveVerificationPath( + eventId, + verificationPath, + historyDir, + historyMetaPath, + ); + const data = readVerification(filePath); + const review = { event_id: eventId, verdict, reviewed_at: new Date().toISOString() }; + const index = data.reviews.findIndex((item) => item.event_id === eventId); + if (index >= 0) data.reviews[index] = review; + else data.reviews.push(review); + writeJson(filePath, data); + return res.json({ success: true, review }); + } catch (error) { + return res.status(400).json({ error: error.message }); + } + }); + + app.post('/api/verification/reset', (_req, res) => { + writeJson(verificationPath, { version: 1, reviews: [] }); + res.json({ success: true }); + }); + + app.get('/api/history', (_req, res) => { + res.json(readJson(historyMetaPath, { version: 1, runs: [] })); + }); + + app.delete('/api/history', (_req, res) => { + writeJson(historyMetaPath, { version: 1, runs: [] }); + res.json({ success: true }); + }); + + app.get('/api/history/:runId', (req, res) => { + const history = readJson(historyMetaPath, { runs: [] }); + const entry = history.runs?.find((run) => run.runId === req.params.runId); + if (!entry) return res.status(404).json({ error: 'Run not found' }); + return res.json(entry); + }); + + return { + app, + stop: stopActiveRun, + getState: () => ({ epoch, uploadReserved, terminating, activeRun, job: { ...job } }), + paths: { uploadsDir, publicDir, historyDir, historyMetaPath, verificationPath }, }; - startTracking(req.file.path, overrides); - return res.json({ success: true, runId: runSequence }); -}); - -app.post('/api/reset', async (_req, res) => { - await stopActiveTracker(); - res.json({ success: true }); -}); - -app.get('/api/events', (_req, res) => { - const data = readJson(path.join(publicDir, 'events.json'), null); - if (!data) return res.status(404).json({ error: 'No events file found.' }); - return res.json(data); -}); - -app.get('/api/verification', (req, res) => { - if (!req.query.runId) return res.json(readVerification()); - const history = readJson(historyMetaPath, { runs: [] }); - const known = history.runs?.some((run) => run.runId === req.query.runId); - if (!known) return res.status(404).json({ error: 'Run not found' }); - return res.json(readVerification(path.join(historyDir, req.query.runId, 'verification.json'))); -}); - -app.post('/api/verification', (req, res) => { - const { eventId, verdict } = req.body || {}; - if (!eventId || !['confirmed', 'rejected'].includes(verdict)) { - return res.status(400).json({ error: 'Body must include eventId and verdict.' }); - } - try { - const filePath = resolveVerificationPath( - eventId, verificationPath, historyDir, historyMetaPath, - ); - const data = readVerification(filePath); - const review = { event_id: eventId, verdict, reviewed_at: new Date().toISOString() }; - const index = data.reviews.findIndex((item) => item.event_id === eventId); - if (index >= 0) data.reviews[index] = review; - else data.reviews.push(review); - writeJson(filePath, data); - return res.json({ success: true, review }); - } catch (error) { - return res.status(400).json({ error: error.message }); - } -}); - -app.post('/api/verification/reset', (_req, res) => { - writeJson(verificationPath, { version: 1, reviews: [] }); - res.json({ success: true }); -}); - -app.get('/api/history', (_req, res) => { - res.json(readJson(historyMetaPath, { version: 1, runs: [] })); -}); - -app.delete('/api/history', (_req, res) => { - writeJson(historyMetaPath, { version: 1, runs: [] }); - res.json({ success: true }); -}); - -app.get('/api/history/:runId', (req, res) => { - const history = readJson(historyMetaPath, { runs: [] }); - const entry = history.runs?.find((run) => run.runId === req.params.runId); - if (!entry) return res.status(404).json({ error: 'Run not found' }); - return res.json(entry); -}); - -app.listen(PORT, () => { - console.log(`\n 🧬 Flyt API Server\n ➜ http://localhost:${PORT}\n ➜ Tracker: ${trackerScript}`); -}); +} + +const isDirectRun = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(modulePath); +if (isDirectRun) { + const { app } = createFlytServer(); + app.listen(DEFAULT_PORT, () => { + console.log(`\n 🧬 Flyt API Server\n ➜ http://localhost:${DEFAULT_PORT}`); + }); +} From 7586d11b46a1923d9a3db40325c335a945c75022 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 10:20:08 +0530 Subject: [PATCH 21/75] fix: exclude occlusions from courtship and fail on truncated decoding --- source app folder/tracker/tracker.py | 103 ++++++++++++++++++++++----- 1 file changed, 86 insertions(+), 17 deletions(-) diff --git a/source app folder/tracker/tracker.py b/source app folder/tracker/tracker.py index 6064216..bee9310 100644 --- a/source app folder/tracker/tracker.py +++ b/source app folder/tracker/tracker.py @@ -11,6 +11,7 @@ LOW_CONFIDENCE_THRESHOLD = 0.2 LOW_CONFIDENCE_MIN_FRAMES = 30 +COURTSHIP_MIN_IDENTITY_CONFIDENCE = 0.2 def parse_args(argv: list[str] | None = None) -> argparse.Namespace: @@ -91,6 +92,29 @@ def build_event_record( } +def is_courtship_frame(row: dict[str, Any], proximity_threshold: float) -> bool: + proximity = value_or(row, "proximity_distance", math.inf) + confidence = value_or(row, "identity_confidence", 0.0) + occluded = value_or(row, "occlusion_flag", 1.0) != 0.0 + fly1_present = value_or(row, "fly1_area", 0.0) > 0.0 + fly2_present = value_or(row, "fly2_area", 0.0) > 0.0 + return ( + math.isfinite(proximity) + and not occluded + and fly1_present + and fly2_present + and confidence >= COURTSHIP_MIN_IDENTITY_CONFIDENCE + and proximity < proximity_threshold + ) + + +def is_low_confidence_frame(row: dict[str, Any]) -> bool: + return ( + value_or(row, "occlusion_flag", 0.0) != 0.0 + or value_or(row, "identity_confidence", 1.0) < LOW_CONFIDENCE_THRESHOLD + ) + + def detect_events( rows: list[dict[str, Any]], fps: float, @@ -101,17 +125,21 @@ def detect_events( counter = 1 courtship = detect_sustained_segments( rows, - lambda row: value_or(row, "proximity_distance", 999.0) < proximity_threshold, + lambda row: is_courtship_frame(row, proximity_threshold), max(1, bout_min_frames), ) low_confidence = detect_sustained_segments( rows, - lambda row: value_or(row, "identity_confidence", 1.0) < LOW_CONFIDENCE_THRESHOLD, + is_low_confidence_frame, LOW_CONFIDENCE_MIN_FRAMES, ) for event_type, segments, reason in ( - ("courtship_bout", courtship, "proximity_sustained"), - ("low_confidence_segment", low_confidence, "identity_confidence_low"), + ( + "courtship_bout", + courtship, + "separate_flies_with_sustained_proximity_and_identity_confidence", + ), + ("low_confidence_segment", low_confidence, "identity_or_occlusion_uncertain"), ): for start, end in segments: segment = [row for row in rows if start <= int(row["frame"]) <= end] @@ -139,6 +167,8 @@ def write_events_json( "detection_params": { "proximity_threshold_px": proximity_threshold, "bout_min_frames": max(1, bout_min_frames), + "courtship_min_identity_confidence": COURTSHIP_MIN_IDENTITY_CONFIDENCE, + "courtship_requires_separate_flies": True, "low_confidence_threshold": LOW_CONFIDENCE_THRESHOLD, "low_confidence_min_frames": LOW_CONFIDENCE_MIN_FRAMES, }, @@ -157,7 +187,23 @@ def assignment_confidence(ca, cb, prev_f1, prev_f2) -> float: direct = float(math.dist(ca, prev_f1) + math.dist(cb, prev_f2)) swapped = float(math.dist(ca, prev_f2) + math.dist(cb, prev_f1)) margin = abs(swapped - direct) - return float(np.clip(margin / (max(direct, swapped) + 1e-6), 0.2, 1.0)) + return float(np.clip(margin / (max(direct, swapped) + 1e-6), 0.0, 1.0)) + + +def frame_sync_ok( + frames_processed: int, + csv_rows: int, + last_frame: int, + expected_frame_count: int, +) -> bool: + internal_sync_ok = ( + csv_rows == frames_processed + and (frames_processed == 0 or last_frame == frames_processed - 1) + ) + expected_sync_ok = ( + expected_frame_count <= 0 or frames_processed == expected_frame_count + ) + return internal_sync_ok and expected_sync_ok def parse_roi(value: str | None) -> tuple[int, int, int, int] | None: @@ -216,7 +262,10 @@ def run_tracker(args: argparse.Namespace) -> int: print(f"Starting tracking on {video_path}...") if roi_rect: - print(f"ROI active: x={roi_rect[0]}, y={roi_rect[1]}, w={roi_rect[2]}, h={roi_rect[3]}") + print( + f"ROI active: x={roi_rect[0]}, y={roi_rect[1]}, " + f"w={roi_rect[2]}, h={roi_rect[3]}" + ) try: while cap.isOpened(): @@ -302,7 +351,7 @@ def run_tracker(args: argparse.Namespace) -> int: was_initialized = is_initialized f1_coords = f2_coords = point occlusion_flag = 1 - identity_confidence = 0.25 + identity_confidence = 0.0 fly1_area = fly2_area = areas[0] if was_initialized: f1_speed = displacement(prev_f1, point, was_initialized) @@ -312,24 +361,36 @@ def run_tracker(args: argparse.Namespace) -> int: if out: x, y, w, h = bboxes[0] cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 255), 3) - cv2.putText(frame, "MERGED", (x, max(0, y - 10)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 2) + cv2.putText( + frame, + "MERGED", + (x, max(0, y - 10)), + cv2.FONT_HERSHEY_SIMPLEX, + 0.6, + (0, 255, 255), + 2, + ) elif is_initialized: f1_coords, f2_coords = prev_f1, prev_f2 proximity = float(math.dist(f1_coords, f2_coords)) - identity_confidence = 0.15 + identity_confidence = 0.0 data.append({ "frame": frame_num, - "fly1_x": f1_coords[0], "fly1_y": f1_coords[1], - "fly2_x": f2_coords[0], "fly2_y": f2_coords[1], - "fly1_speed": f1_speed, "fly2_speed": f2_speed, + "fly1_x": f1_coords[0], + "fly1_y": f1_coords[1], + "fly2_x": f2_coords[0], + "fly2_y": f2_coords[1], + "fly1_speed": f1_speed, + "fly2_speed": f2_speed, "fly1_speed_pxsec": round(f1_speed * effective_fps, 4), "fly2_speed_pxsec": round(f2_speed * effective_fps, 4), "activity_level": activity_level, "proximity_distance": proximity, "occlusion_flag": occlusion_flag, "identity_confidence": round(identity_confidence, 4), - "fly1_area": fly1_area, "fly2_area": fly2_area, + "fly1_area": fly1_area, + "fly2_area": fly2_area, }) if out: out.write(frame) @@ -345,17 +406,25 @@ def run_tracker(args: argparse.Namespace) -> int: pd.DataFrame(data).to_csv(args.output_csv, index=False) if args.output_events: write_events_json( - data, effective_fps, args.output_events, - args.proximity_threshold, args.bout_min_frames, + data, + effective_fps, + args.output_events, + args.proximity_threshold, + args.bout_min_frames, ) last_frame = data[-1]["frame"] if data else -1 - sync_ok = len(data) == frame_num and (frame_num == 0 or last_frame == frame_num - 1) + sync_ok = frame_sync_ok( + frame_num, len(data), last_frame, expected_frame_count, + ) print( f"TRACKER_SYNC frames_processed={frame_num} csv_rows={len(data)} " f"expected_video_frames={expected_frame_count} sync_ok={str(sync_ok).lower()}" ) if not sync_ok: - print("Warning: internal frame/CSV sync mismatch detected", file=sys.stderr) + print( + "Error: frame integrity mismatch between decoded input, tracker loop, and CSV", + file=sys.stderr, + ) return 2 print(f"Tracking completed! Data saved to {args.output_csv}.") return 0 From 9b2603d4fe0ec854452c60b5c0b44b529205d0a7 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 10:20:21 +0530 Subject: [PATCH 22/75] test: enforce courtship and frame-integrity semantics --- .../tracker/tests/test_tracker.py | 58 ++++++++++++++----- 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/source app folder/tracker/tests/test_tracker.py b/source app folder/tracker/tests/test_tracker.py index 2e50452..273efe8 100644 --- a/source app folder/tracker/tests/test_tracker.py +++ b/source app folder/tracker/tests/test_tracker.py @@ -9,6 +9,17 @@ spec.loader.exec_module(tracker) +def row(frame, proximity, confidence=1.0, occlusion=0, area=100.0): + return { + "frame": frame, + "proximity_distance": proximity, + "identity_confidence": confidence, + "occlusion_flag": occlusion, + "fly1_area": area, + "fly2_area": area, + } + + class TrackerTests(unittest.TestCase): def test_zero_is_not_missing(self): self.assertEqual(tracker.value_or({"value": 0}, "value", 999), 0) @@ -16,25 +27,40 @@ def test_zero_is_not_missing(self): def test_initial_detection_has_zero_velocity(self): self.assertEqual(tracker.displacement((0, 0), (100, 100), False), 0.0) - self.assertGreater(tracker.displacement((0, 0), (3, 4), True), 0.0) + self.assertEqual(tracker.displacement((0, 0), (3, 4), True), 5.0) + + def test_separate_confident_close_flies_generate_courtship(self): + rows = [row(i, proximity=20, confidence=0.8) for i in range(5)] + events = tracker.detect_events(rows, 30, proximity_threshold=60, bout_min_frames=5) + self.assertEqual([event["type"] for event in events], ["courtship_bout"]) + self.assertEqual(events[0]["occlusion_fraction"], 0) + + def test_occluded_zero_proximity_never_generates_courtship(self): + rows = [row(i, proximity=0, confidence=0.0, occlusion=1) for i in range(30)] + events = tracker.detect_events(rows, 30, proximity_threshold=60, bout_min_frames=5) + self.assertNotIn("courtship_bout", [event["type"] for event in events]) + self.assertEqual([event["type"] for event in events], ["low_confidence_segment"]) - def test_zero_proximity_generates_courtship(self): - rows = [ - {"frame": i, "proximity_distance": 0, "identity_confidence": 1, "occlusion_flag": 1} - for i in range(5) - ] + def test_low_confidence_separate_flies_do_not_generate_courtship(self): + rows = [row(i, proximity=10, confidence=0.1, occlusion=0) for i in range(30)] events = tracker.detect_events(rows, 30, proximity_threshold=60, bout_min_frames=5) - self.assertEqual(events[0]["type"], "courtship_bout") - self.assertEqual(events[0]["mean_proximity_px"], 0) - - def test_zero_confidence_generates_low_confidence_segment(self): - rows = [ - {"frame": i, "proximity_distance": 999, "identity_confidence": 0, "occlusion_flag": 0} - for i in range(30) - ] - events = tracker.detect_events(rows, 30, proximity_threshold=60, bout_min_frames=90) + self.assertNotIn("courtship_bout", [event["type"] for event in events]) self.assertEqual(events[0]["type"], "low_confidence_segment") - self.assertEqual(events[0]["min_identity_confidence"], 0) + + def test_missing_fly_area_does_not_generate_courtship(self): + rows = [row(i, proximity=10, confidence=1.0, area=0) for i in range(10)] + events = tracker.detect_events(rows, 30, proximity_threshold=60, bout_min_frames=5) + self.assertNotIn("courtship_bout", [event["type"] for event in events]) + + def test_assignment_confidence_can_fall_below_low_confidence_threshold(self): + confidence = tracker.assignment_confidence((5, 0), (-5, 0), (0, 0), (0, 0)) + self.assertLess(confidence, tracker.LOW_CONFIDENCE_THRESHOLD) + + def test_frame_sync_requires_expected_input_count_when_available(self): + self.assertTrue(tracker.frame_sync_ok(10, 10, 9, 10)) + self.assertFalse(tracker.frame_sync_ok(8, 8, 7, 10)) + self.assertTrue(tracker.frame_sync_ok(8, 8, 7, 0)) + self.assertFalse(tracker.frame_sync_ok(8, 7, 6, 8)) def test_roi_validation(self): self.assertEqual(tracker.parse_roi("1,2,3,4"), (1, 2, 3, 4)) From 4235648d119ef03cc33a835bcf75e9ece34439f6 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 10:20:42 +0530 Subject: [PATCH 23/75] test: cover fail-closed validation, rollback, recovery, and termination --- .../dashboard/tests/server-utils.test.js | 135 +++++++++++++++--- 1 file changed, 114 insertions(+), 21 deletions(-) diff --git a/source app folder/dashboard/tests/server-utils.test.js b/source app folder/dashboard/tests/server-utils.test.js index a286c5b..5aa8b07 100644 --- a/source app folder/dashboard/tests/server-utils.test.js +++ b/source app folder/dashboard/tests/server-utils.test.js @@ -1,4 +1,5 @@ import assert from 'node:assert/strict'; +import { EventEmitter } from 'node:events'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; @@ -7,11 +8,16 @@ import { buildTrackerArgs, parseTrackerSync, publishBundle, + recoverPublishArtifacts, resolveVerificationPath, scopeEventsForHistory, + snapshotRunToHistory, + terminateChild, + validateFrameIntegrity, } from '../server-utils.js'; const defaults = { minArea: 30, maxArea: 0, proximityThreshold: 60, boutMinFrames: 90 }; +const tempDir = (prefix) => fs.mkdtempSync(path.join(os.tmpdir(), prefix)); test('normalizes integer tracker settings and rejects invalid max area', () => { const args = buildTrackerArgs('tracker.py', 'in.mp4', { @@ -22,14 +28,31 @@ test('normalizes integer tracker settings and rejects invalid max area', () => { assert.equal(args[args.indexOf('--bout-min-frames') + 1], '1'); }); -test('parses tracker sync output', () => { - assert.deepEqual(parseTrackerSync( - 'TRACKER_SYNC frames_processed=12 csv_rows=12 expected_video_frames=14 sync_ok=true', - ), { framesProcessed: 12, csvRows: 12, expectedVideoFrames: 14, syncOk: true }); +test('requires exactly one tracker sync marker', () => { + const marker = 'TRACKER_SYNC frames_processed=12 csv_rows=12 expected_video_frames=12 sync_ok=true'; + assert.deepEqual(parseTrackerSync(marker), { + framesProcessed: 12, csvRows: 12, expectedVideoFrames: 12, syncOk: true, + }); + assert.equal(parseTrackerSync('no marker'), null); + assert.equal(parseTrackerSync(`${marker}\n${marker}`), null); +}); + +test('frame validation fails closed on missing evidence or disagreement', () => { + const evidence = { + syncInfo: { framesProcessed: 12, csvRows: 12, expectedVideoFrames: 12, syncOk: true }, + inputFrames: 12, csvRows: 12, rawVideoFrames: 12, finalVideoFrames: 12, + }; + assert.equal(validateFrameIntegrity(evidence).syncOk, true); + assert.throws(() => validateFrameIntegrity({ ...evidence, syncInfo: null }), /marker required/); + assert.throws(() => validateFrameIntegrity({ ...evidence, finalVideoFrames: 11 }), /mismatch/); + assert.throws(() => validateFrameIntegrity({ + ...evidence, + syncInfo: { ...evidence.syncInfo, expectedVideoFrames: 11 }, + }), /mismatch/); }); test('publishes a complete bundle and removes backups', () => { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'flyt-publish-')); + const dir = tempDir('flyt-publish-'); const sourceA = path.join(dir, 'source-a'); const sourceB = path.join(dir, 'source-b'); const destinationA = path.join(dir, 'a'); @@ -44,28 +67,69 @@ test('publishes a complete bundle and removes backups', () => { ], 'token'); assert.equal(fs.readFileSync(destinationA, 'utf8'), 'new-a'); assert.equal(fs.readFileSync(destinationB, 'utf8'), 'new-b'); - assert.equal(fs.readdirSync(dir).some((name) => name.endsWith('.bak')), false); + assert.equal(fs.readdirSync(dir).some((name) => /\.(bak|new)$/.test(name)), false); }); -test('rolls back every destination when bundle staging fails', () => { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'flyt-rollback-')); - const source = path.join(dir, 'source'); - const missing = path.join(dir, 'missing'); - const destinationA = path.join(dir, 'a'); - const destinationB = path.join(dir, 'b'); - fs.writeFileSync(source, 'new-a'); - fs.writeFileSync(destinationA, 'old-a'); - fs.writeFileSync(destinationB, 'old-b'); +test('rolls back after one destination has already been published', () => { + const dir = tempDir('flyt-rollback-'); + const sources = ['new-a', 'new-b'].map((content, index) => { + const file = path.join(dir, `source-${index}`); + fs.writeFileSync(file, content); + return file; + }); + const destinations = [path.join(dir, 'a'), path.join(dir, 'b')]; + fs.writeFileSync(destinations[0], 'old-a'); + fs.writeFileSync(destinations[1], 'old-b'); assert.throws(() => publishBundle([ - { source, destination: destinationA }, - { source: missing, destination: destinationB }, - ], 'token')); - assert.equal(fs.readFileSync(destinationA, 'utf8'), 'old-a'); - assert.equal(fs.readFileSync(destinationB, 'utf8'), 'old-b'); + { source: sources[0], destination: destinations[0] }, + { source: sources[1], destination: destinations[1] }, + ], 'token', { + faultInjector(stage, index) { + if (stage === 'published' && index === 0) throw new Error('injected failure'); + }, + }), /injected failure/); + assert.equal(fs.readFileSync(destinations[0], 'utf8'), 'old-a'); + assert.equal(fs.readFileSync(destinations[1], 'utf8'), 'old-b'); + assert.equal(fs.readdirSync(dir).some((name) => /\.(bak|new)$/.test(name)), false); +}); + +test('recovers orphaned publication artifacts after a crash', () => { + const dir = tempDir('flyt-recover-'); + fs.writeFileSync(path.join(dir, 'data.csv.token.bak'), 'old-data'); + fs.writeFileSync(path.join(dir, 'events.json.token.new'), 'new-events'); + recoverPublishArtifacts(dir); + assert.equal(fs.readFileSync(path.join(dir, 'data.csv'), 'utf8'), 'old-data'); + assert.equal(fs.existsSync(path.join(dir, 'events.json.token.new')), false); +}); + +test('history snapshots preserve dashboard summary fields and scoped reviews', () => { + const dir = tempDir('flyt-history-'); + const historyDir = path.join(dir, 'history'); + const historyMetaPath = path.join(dir, 'history.json'); + const csv = path.join(dir, 'data.csv'); + const events = path.join(dir, 'events.json'); + fs.writeFileSync(csv, [ + 'frame,proximity_distance,occlusion_flag', '0,10,0', '1,0,1', '2,30,0', + ].join('\n')); + fs.writeFileSync(events, JSON.stringify({ + events: [{ id: 'evt-001', type: 'courtship_bout' }], + })); + const meta = snapshotRunToHistory({ + historyDir, historyMetaPath, filename: 'flies.mp4', durationSec: 2.3, + fps: 30, totalFrames: 3, csvSrc: csv, eventsSrc: events, + }); + assert.equal(meta.avgProximity, 20); + assert.equal(meta.detectedBouts, 1); + const scopedEvents = JSON.parse(fs.readFileSync(path.join(historyDir, meta.runId, 'events.json'))); + assert.equal(scopedEvents.events[0].id, `${meta.runId}:evt-001`); + assert.deepEqual( + JSON.parse(fs.readFileSync(path.join(historyDir, meta.runId, 'verification.json'))), + { version: 1, reviews: [] }, + ); }); test('scopes historical event IDs and verification paths to their run', () => { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'flyt-history-')); + const dir = tempDir('flyt-history-path-'); const historyDir = path.join(dir, 'history'); const runId = 'run-1234-abcd'; fs.mkdirSync(path.join(historyDir, runId), { recursive: true }); @@ -81,3 +145,32 @@ test('scopes historical event IDs and verification paths to their run', () => { path.join(historyDir, runId, 'verification.json'), ); }); + +test('termination escalates to SIGKILL and resolves only after close', async () => { + class FakeChild extends EventEmitter { + constructor() { + super(); + this.exitCode = null; + this.signalCode = null; + this.signals = []; + } + kill(signal) { + this.signals.push(signal); + if (signal === 'SIGKILL') { + setTimeout(() => { + this.signalCode = signal; + this.emit('close', null, signal); + }, 5); + } + return true; + } + } + const child = new FakeChild(); + let settled = false; + const termination = terminateChild(child, { graceMs: 5, hardKillMs: 100 }) + .then(() => { settled = true; }); + await new Promise((resolve) => setTimeout(resolve, 7)); + assert.equal(settled, false); + await termination; + assert.deepEqual(child.signals, ['SIGTERM', 'SIGKILL']); +}); From 6596d79dfecf9cca4d199e45406fee4550f8125a Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 10:21:11 +0530 Subject: [PATCH 24/75] test: exercise real upload reset and stale-run races over HTTP --- .../tests/server-integration.test.js | 263 ++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 source app folder/dashboard/tests/server-integration.test.js diff --git a/source app folder/dashboard/tests/server-integration.test.js b/source app folder/dashboard/tests/server-integration.test.js new file mode 100644 index 0000000..3644501 --- /dev/null +++ b/source app folder/dashboard/tests/server-integration.test.js @@ -0,0 +1,263 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import http from 'node:http'; +import os from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; +import { createFlytServer } from '../server.js'; + +function abortError() { + const error = new Error('aborted'); + error.name = 'AbortError'; + return error; +} + +function createGate({ releaseOnAbort = true } = {}) { + let resolveGate; + let rejectGate; + let aborted = false; + const promise = new Promise((resolve, reject) => { + resolveGate = resolve; + rejectGate = reject; + }); + return { + wait(signal) { + if (signal.aborted) return Promise.reject(abortError()); + signal.addEventListener('abort', () => { + aborted = true; + if (releaseOnAbort) rejectGate(abortError()); + }, { once: true }); + return promise; + }, + resolve(value) { resolveGate(value); }, + reject(error) { rejectGate(error); }, + get aborted() { return aborted; }, + }; +} + +function writeTrackerOutputs(outputs, frames = 4) { + fs.writeFileSync(outputs.rawVideo, 'raw-video'); + fs.writeFileSync(outputs.csv, [ + 'frame,proximity_distance,occlusion_flag', + ...Array.from({ length: frames }, (_, index) => `${index},20,0`), + ].join('\n')); + fs.writeFileSync(outputs.events, JSON.stringify({ + fps: 30, + total_frames: frames, + events: [{ id: 'evt-001', type: 'courtship_bout' }], + })); +} + +function standardServices(overrides = {}) { + return { + countFrames: async () => 4, + runTracker: async (_input, outputs) => { + writeTrackerOutputs(outputs); + return { + code: 0, + stderr: '', + stdout: 'TRACKER_SYNC frames_processed=4 csv_rows=4 expected_video_frames=4 sync_ok=true', + }; + }, + transcode: async (raw, final) => fs.copyFileSync(raw, final), + snapshot: () => null, + ...overrides, + }; +} + +async function createHarness(services) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'flyt-http-')); + const instance = createFlytServer({ + rootDir: root, + uploadsDir: path.join(root, 'uploads'), + publicDir: path.join(root, 'public'), + historyDir: path.join(root, 'public', 'history'), + historyMetaPath: path.join(root, 'public', 'history.json'), + verificationPath: path.join(root, 'public', 'verification.json'), + trackerDir: path.join(root, 'tracker'), + trackerScript: path.join(root, 'tracker', 'tracker.py'), + pythonExe: 'python', + ffmpegPath: 'ffmpeg', + allowedOrigins: ['http://localhost:5173'], + services, + }); + const listener = await new Promise((resolve) => { + const server = instance.app.listen(0, '127.0.0.1', () => resolve(server)); + }); + const address = listener.address(); + return { + ...instance, + root, + baseUrl: `http://127.0.0.1:${address.port}`, + async close() { + try { await instance.stop(); } catch { /* test cleanup */ } + await new Promise((resolve) => listener.close(resolve)); + fs.rmSync(root, { recursive: true, force: true }); + }, + }; +} + +async function upload(baseUrl, name = 'flies.mp4') { + const form = new FormData(); + form.append('video', new Blob(['video-bytes'], { type: 'video/mp4' }), name); + return fetch(`${baseUrl}/api/upload`, { method: 'POST', body: form }); +} + +async function waitForStatus(baseUrl, expected, timeoutMs = 1500) { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + const response = await fetch(`${baseUrl}/api/status`); + const status = await response.json(); + if (status.status === expected) return status; + await new Promise((resolve) => setTimeout(resolve, 10)); + } + throw new Error(`Timed out waiting for ${expected}`); +} + +test('rejects a simultaneous upload before a second file is accepted', async (t) => { + const gate = createGate(); + const harness = await createHarness(standardServices({ + countFrames: (_path, context) => gate.wait(context.controller.signal), + })); + t.after(() => harness.close()); + + assert.equal((await upload(harness.baseUrl, 'first.mp4')).status, 200); + assert.equal((await upload(harness.baseUrl, 'second.mp4')).status, 409); + assert.equal(fs.readdirSync(harness.paths.uploadsDir).filter((name) => name.startsWith('input-')).length, 1); + + const reset = await fetch(`${harness.baseUrl}/api/reset`, { method: 'POST' }); + assert.equal(reset.status, 200); + assert.equal(gate.aborted, true); +}); + +test('reset during a multipart upload invalidates and removes the partial upload', async (t) => { + const harness = await createHarness(standardServices()); + t.after(() => harness.close()); + const boundary = '----flyt-boundary'; + const prefix = [ + `--${boundary}`, + 'Content-Disposition: form-data; name="video"; filename="slow.mp4"', + 'Content-Type: video/mp4', + '', + 'partial-video-', + ].join('\r\n'); + const suffix = `\r\n--${boundary}--\r\n`; + + let finishUpload; + const uploadResponse = new Promise((resolve, reject) => { + const request = http.request(`${harness.baseUrl}/api/upload`, { + method: 'POST', + headers: { 'Content-Type': `multipart/form-data; boundary=${boundary}` }, + }, (response) => { + response.resume(); + response.on('end', () => resolve(response)); + }); + request.on('error', reject); + request.write(prefix); + finishUpload = () => request.end(`remaining${suffix}`); + }); + + await new Promise((resolve) => setTimeout(resolve, 30)); + const reset = await fetch(`${harness.baseUrl}/api/reset`, { method: 'POST' }); + assert.equal(reset.status, 200); + finishUpload(); + const response = await uploadResponse; + assert.equal(response.statusCode, 409); + assert.equal( + fs.readdirSync(harness.paths.uploadsDir).filter((name) => name.startsWith('input-')).length, + 0, + ); +}); + +test('reset remains pending until the active stage has actually stopped', async (t) => { + const gate = createGate({ releaseOnAbort: false }); + const harness = await createHarness(standardServices({ + countFrames: (_path, context) => gate.wait(context.controller.signal), + })); + t.after(() => harness.close()); + + assert.equal((await upload(harness.baseUrl)).status, 200); + let resetSettled = false; + const resetPromise = fetch(`${harness.baseUrl}/api/reset`, { method: 'POST' }) + .then((response) => { resetSettled = true; return response; }); + await new Promise((resolve) => setTimeout(resolve, 40)); + assert.equal(resetSettled, false); + assert.equal((await upload(harness.baseUrl, 'blocked.mp4')).status, 409); + + gate.resolve(4); + assert.equal((await resetPromise).status, 200); + assert.equal(resetSettled, true); +}); + +test('reset during transcoding prevents stale publication', async (t) => { + const transcodeGate = createGate({ releaseOnAbort: false }); + let publishCalls = 0; + const harness = await createHarness(standardServices({ + transcode: async (raw, final, context) => { + await transcodeGate.wait(context.controller.signal); + fs.copyFileSync(raw, final); + }, + publish: () => { publishCalls += 1; }, + })); + t.after(() => harness.close()); + + assert.equal((await upload(harness.baseUrl)).status, 200); + await new Promise((resolve) => setTimeout(resolve, 30)); + const resetPromise = fetch(`${harness.baseUrl}/api/reset`, { method: 'POST' }); + await new Promise((resolve) => setTimeout(resolve, 20)); + assert.equal(transcodeGate.aborted, true); + assert.equal(publishCalls, 0); + + transcodeGate.resolve(); + assert.equal((await resetPromise).status, 200); + assert.equal(publishCalls, 0); +}); + +test('a complete run publishes only after every frame count agrees', async (t) => { + const harness = await createHarness(standardServices()); + t.after(() => harness.close()); + + assert.equal((await upload(harness.baseUrl)).status, 200); + const status = await waitForStatus(harness.baseUrl, 'done'); + assert.equal(status.frameCount, 4); + const metadata = JSON.parse(fs.readFileSync(path.join(harness.paths.publicDir, 'run_metadata.json'))); + assert.equal(metadata.inputFrames, 4); + assert.equal(metadata.rawVideoFrames, 4); + assert.equal(metadata.finalVideoFrames, 4); + assert.equal(metadata.syncOk, true); +}); + +test('frame mismatch fails closed and preserves the previous published bundle', async (t) => { + let countCall = 0; + const harness = await createHarness(standardServices({ + countFrames: async () => { + countCall += 1; + return countCall === 3 ? 3 : 4; + }, + })); + t.after(() => harness.close()); + fs.writeFileSync(path.join(harness.paths.publicDir, 'data.csv'), 'old-data'); + + assert.equal((await upload(harness.baseUrl)).status, 200); + const status = await waitForStatus(harness.baseUrl, 'error'); + assert.match(status.error, /Frame integrity mismatch/); + assert.equal(fs.readFileSync(path.join(harness.paths.publicDir, 'data.csv'), 'utf8'), 'old-data'); +}); + +test('destructive cross-origin requests are denied unless explicitly allowed', async (t) => { + const harness = await createHarness(standardServices()); + t.after(() => harness.close()); + + const denied = await fetch(`${harness.baseUrl}/api/reset`, { + method: 'POST', + headers: { Origin: 'https://evil.example' }, + }); + assert.equal(denied.status, 403); + + const allowed = await fetch(`${harness.baseUrl}/api/reset`, { + method: 'POST', + headers: { Origin: 'http://localhost:5173' }, + }); + assert.equal(allowed.status, 200); + assert.equal(allowed.headers.get('access-control-allow-origin'), 'http://localhost:5173'); +}); From 3fc065bee6c927cbfbd83958030d27df6780b9a0 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 10:21:21 +0530 Subject: [PATCH 25/75] test: lock the historic verification UI contract --- .../dashboard/tests/frontend-contract.test.js | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 source app folder/dashboard/tests/frontend-contract.test.js diff --git a/source app folder/dashboard/tests/frontend-contract.test.js b/source app folder/dashboard/tests/frontend-contract.test.js new file mode 100644 index 0000000..dbd9807 --- /dev/null +++ b/source app folder/dashboard/tests/frontend-contract.test.js @@ -0,0 +1,20 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +const testDir = path.dirname(fileURLToPath(import.meta.url)); +const appSource = () => fs.readFileSync(path.join(testDir, '..', 'src', 'App.jsx'), 'utf8'); + +test('historic runs load their scoped verification state', () => { + const source = appSource(); + assert.match(source, /api\/verification\?runId=\$\{encodeURIComponent\(runId\)\}/); + assert.doesNotMatch(source, /if \(!runId\) \{\s*try \{\s*const verRes/); +}); + +test('historic runs display their recorded timestamp rather than load time', () => { + const source = appSource(); + assert.match(source, /setRunTimestamp\(run\.timestamp \|\| null\)/); + assert.match(source, /loadHistoricRun\(run\)/); +}); From 90abb58e932acf1dd16a9297b612557ee94b97d9 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 10:21:44 +0530 Subject: [PATCH 26/75] chore: stage exact frontend integration patch --- .github/scripts/apply-review-hardening.py | 55 +++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 .github/scripts/apply-review-hardening.py diff --git a/.github/scripts/apply-review-hardening.py b/.github/scripts/apply-review-hardening.py new file mode 100644 index 0000000..f2e3689 --- /dev/null +++ b/.github/scripts/apply-review-hardening.py @@ -0,0 +1,55 @@ +from pathlib import Path + +app_path = Path("source app folder/dashboard/src/App.jsx") +source = app_path.read_text(encoding="utf-8") + +old_verification = """ if (!runId) { + try { + const verRes = await fetch(`/api/verification?t=${cacheBust}`, { cache: 'no-store' }); + if (verRes.ok) { + const verData = await verRes.json(); + (verData.reviews || []).forEach((r) => { + reviews[r.event_id] = r; + }); + } + } catch { /* keep empty reviews */ } + } +""" +new_verification = """ try { + const verificationUrl = runId + ? `/api/verification?runId=${encodeURIComponent(runId)}&t=${cacheBust}` + : `/api/verification?t=${cacheBust}`; + const verRes = await fetch(verificationUrl, { cache: 'no-store' }); + if (verRes.ok) { + const verData = await verRes.json(); + (verData.reviews || []).forEach((r) => { + reviews[r.event_id] = r; + }); + } + } catch { /* keep empty reviews */ } +""" + +replacements = [ + (old_verification, new_verification), + ( + " const loadHistoricRun = async (runId) => {\n", + " const loadHistoricRun = async (run) => {\n const runId = run.runId;\n", + ), + ( + " setRunTimestamp(new Date().toISOString());\n", + " setRunTimestamp(run.timestamp || null);\n", + ), + ( + "onClick={() => loadHistoricRun(run.runId)}", + "onClick={() => loadHistoricRun(run)}", + ), +] + +for old, new in replacements: + count = source.count(old) + if count != 1: + raise RuntimeError(f"Expected one occurrence, found {count}: {old[:80]!r}") + source = source.replace(old, new, 1) + +app_path.write_text(source, encoding="utf-8") +print("Applied historic verification and timestamp integration patches.") From 5f34c37afbb6824a1465497e46b15cb3082e94da Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 10:21:56 +0530 Subject: [PATCH 27/75] chore: run and validate the exact frontend hardening patch --- .github/workflows/apply-review-hardening.yml | 73 ++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 .github/workflows/apply-review-hardening.yml diff --git a/.github/workflows/apply-review-hardening.yml b/.github/workflows/apply-review-hardening.yml new file mode 100644 index 0000000..c71aa87 --- /dev/null +++ b/.github/workflows/apply-review-hardening.yml @@ -0,0 +1,73 @@ +name: Apply review hardening + +on: + push: + branches: + - fix/critical-job-races + paths: + - '.github/scripts/apply-review-hardening.py' + - '.github/workflows/apply-review-hardening.yml' + +permissions: + contents: write + +jobs: + patch-and-validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: fix/critical-job-races + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: source app folder/dashboard/package-lock.json + + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: pip + cache-dependency-path: source app folder/tracker/requirements.txt + + - name: Apply exact frontend patch + run: python .github/scripts/apply-review-hardening.py + + - name: Install dashboard dependencies + working-directory: source app folder/dashboard + run: npm ci + + - name: Lint backend and tests + working-directory: source app folder/dashboard + run: npm run lint:backend + + - name: Run HTTP and utility regression tests + working-directory: source app folder/dashboard + run: npm run test:server + + - name: Build dashboard + working-directory: source app folder/dashboard + run: npm run build + + - name: Install tracker dependencies + run: python -m pip install -r 'source app folder/tracker/requirements.txt' + + - name: Check tracker syntax + run: python -m py_compile 'source app folder/tracker/tracker.py' + + - name: Run tracker regression tests + run: python -m unittest discover -s 'source app folder/tracker/tests' -p 'test_*.py' -v + + - name: Commit validated frontend integration + run: | + if git diff --quiet -- 'source app folder/dashboard/src/App.jsx'; then + echo 'Expected App.jsx patch was not produced.' >&2 + exit 1 + fi + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add 'source app folder/dashboard/src/App.jsx' + git commit -m 'fix: complete historic verification integration' + git push origin HEAD:fix/critical-job-races From 9eb06cb1e9cd36dc9a68fad2795a23f9318a5fa6 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 10:24:12 +0530 Subject: [PATCH 28/75] chore: capture exact lint diagnostics during frontend patch --- .github/workflows/apply-review-hardening.yml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/apply-review-hardening.yml b/.github/workflows/apply-review-hardening.yml index c71aa87..a8c93dd 100644 --- a/.github/workflows/apply-review-hardening.yml +++ b/.github/workflows/apply-review-hardening.yml @@ -41,7 +41,20 @@ jobs: - name: Lint backend and tests working-directory: source app folder/dashboard - run: npm run lint:backend + run: | + set +e + npm run lint:backend -- --format json > ../../.github/eslint-diagnostics.json + status=$? + cat ../../.github/eslint-diagnostics.json + if [ "$status" -ne 0 ]; then + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add ../../.github/eslint-diagnostics.json + git commit -m 'chore: capture eslint diagnostics' + git push origin HEAD:fix/critical-job-races + exit "$status" + fi + rm ../../.github/eslint-diagnostics.json - name: Run HTTP and utility regression tests working-directory: source app folder/dashboard From 715e2c90606c159832e25a4b2638563b2ed13061 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 04:54:36 +0000 Subject: [PATCH 29/75] chore: capture eslint diagnostics --- .github/eslint-diagnostics.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .github/eslint-diagnostics.json diff --git a/.github/eslint-diagnostics.json b/.github/eslint-diagnostics.json new file mode 100644 index 0000000..f8b16be --- /dev/null +++ b/.github/eslint-diagnostics.json @@ -0,0 +1,5 @@ + +> dashboard@0.0.0 lint:backend +> eslint server.js server-utils.js tests --format json + +[{"filePath":"/home/runner/work/Flyt/Flyt/source app folder/dashboard/server-utils.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/runner/work/Flyt/Flyt/source app folder/dashboard/server.js","messages":[{"ruleId":null,"nodeType":null,"fatal":true,"severity":2,"message":"Parsing error: Unexpected token =>","line":122,"column":71}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":1,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { randomUUID } from 'crypto';\nimport { spawn as nodeSpawn } from 'child_process';\nimport express from 'express';\nimport defaultFfmpegPath from 'ffmpeg-static';\nimport fs from 'fs';\nimport multer from 'multer';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport {\n AbortRunError,\n buildTrackerArgs,\n countCsvRows,\n ensureDir,\n getVideoFrameCount,\n parseTrackerSync,\n publishBundle,\n readEventsFps,\n readJson,\n recoverPublishArtifacts,\n resolveVerificationPath,\n runCommand,\n safeRemoveDir,\n safeUnlink,\n snapshotRunToHistory,\n terminateChild,\n transcodeVideo,\n validateFrameIntegrity,\n writeJson,\n} from './server-utils.js';\n\nconst modulePath = fileURLToPath(import.meta.url);\nconst moduleDir = path.dirname(modulePath);\nconst DEFAULT_PORT = 3001;\nconst DEFAULT_ALLOWED_ORIGINS = [\n 'http://localhost:3001',\n 'http://127.0.0.1:3001',\n 'http://localhost:5173',\n 'http://127.0.0.1:5173',\n];\n\nfunction blankJob() {\n return {\n runId: null,\n status: 'idle',\n progress: '',\n framesProcessed: 0,\n totalFrames: null,\n frameCount: null,\n error: null,\n startTime: null,\n endTime: null,\n };\n}\n\nfunction parseAllowedOrigins(value) {\n if (!value) return DEFAULT_ALLOWED_ORIGINS;\n return value.split(',').map((item) => item.trim()).filter(Boolean);\n}\n\nexport function createFlytServer(options = {}) {\n const rootDir = options.rootDir || moduleDir;\n const uploadsDir = options.uploadsDir || path.join(rootDir, 'uploads');\n const publicDir = options.publicDir || path.join(rootDir, 'public');\n const historyDir = options.historyDir || path.join(publicDir, 'history');\n const historyMetaPath = options.historyMetaPath || path.join(publicDir, 'history.json');\n const verificationPath = options.verificationPath || path.join(publicDir, 'verification.json');\n const trackerDir = options.trackerDir || path.join(rootDir, '..', 'tracker');\n const trackerScript = options.trackerScript || path.join(trackerDir, 'tracker.py');\n const isWindows = process.platform === 'win32';\n const pythonExe = options.pythonExe || path.join(\n trackerDir,\n 'venv',\n isWindows ? 'Scripts' : 'bin',\n isWindows ? 'python.exe' : 'python',\n );\n const ffmpegPath = options.ffmpegPath || defaultFfmpegPath;\n const spawnFn = options.spawnFn || nodeSpawn;\n const defaults = options.defaults || {\n minArea: 30,\n maxArea: 0,\n proximityThreshold: 60,\n boutMinFrames: 90,\n };\n const allowedOrigins = new Set(options.allowedOrigins || parseAllowedOrigins(process.env.FLYT_ALLOWED_ORIGINS));\n const killOptions = options.killOptions || { graceMs: 1000, hardKillMs: 3000 };\n\n for (const dir of [uploadsDir, publicDir, historyDir]) ensureDir(dir);\n recoverPublishArtifacts(publicDir);\n\n const app = express();\n const storage = multer.diskStorage({\n destination: (_req, _file, callback) => callback(null, uploadsDir),\n filename: (_req, file, callback) => {\n callback(null, `input-${Date.now()}-${randomUUID()}${path.extname(file.originalname).toLowerCase()}`);\n },\n });\n const upload = multer({ storage, limits: { fileSize: 10 * 1024 * 1024 * 1024 } });\n\n app.use(express.json());\n app.use((req, res, next) => {\n const origin = req.get('Origin');\n if (!origin) return next();\n if (!allowedOrigins.has(origin)) {\n return res.status(403).json({ error: 'Cross-origin request denied.' });\n }\n res.header('Access-Control-Allow-Origin', origin);\n res.header('Vary', 'Origin');\n res.header('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');\n res.header('Access-Control-Allow-Headers', 'Content-Type');\n if (req.method === 'OPTIONS') return res.sendStatus(204);\n return next();\n });\n\n let runSequence = 0;\n let epoch = 0;\n let uploadReserved = false;\n let terminating = false;\n let activeRun = null;\n let job = blankJob();\n\n const services = {\n countFrames: options.services?.countFrames || (filePath, context) => getVideoFrameCount(\n ffmpegPath,\n filePath,\n {\n spawnFn,\n signal: context.controller.signal,\n children: context.children,\n killOptions,\n },\n ),\n runTracker: options.services?.runTracker || async (inputPath, outputs, overrides, context) => runCommand(\n pythonExe,\n buildTrackerArgs(trackerScript, inputPath, outputs, overrides, defaults),\n {\n spawnFn,\n signal: context.controller.signal,\n children: context.children,\n killOptions,\n onStdout: (text) => {\n const match = text.match(/Processed (\\d+) frames/);\n if (match && isCurrent(context)) {\n job.framesProcessed = Number(match[1]);\n job.progress = `Processed ${job.framesProcessed} frames...`;\n } else if (text.includes('Tracking completed') && isCurrent(context)) {\n job.progress = 'Validating tracker output...';\n }\n },\n onStderr: (text) => console.error(`[Tracker] ${text.trim()}`),\n },\n ),\n transcode: options.services?.transcode || (rawPath, finalPath, context) => transcodeVideo(\n ffmpegPath,\n rawPath,\n finalPath,\n {\n spawnFn,\n signal: context.controller.signal,\n children: context.children,\n killOptions,\n },\n ),\n publish: options.services?.publish || ((entries, token) => publishBundle(entries, token)),\n snapshot: options.services?.snapshot || snapshotRunToHistory,\n };\n\n const isBusy = () => (\n uploadReserved\n || terminating\n || ['uploading', 'processing', 'stopping'].includes(job.status)\n );\n const isCurrent = (context) => (\n activeRun === context\n && context.epoch === epoch\n && !context.controller.signal.aborted\n );\n\n function reserveUpload(req, res, next) {\n if (isBusy()) {\n return res.status(409).json({ error: 'A video is already uploading, processing, or stopping.' });\n }\n req.uploadEpoch = epoch;\n uploadReserved = true;\n let released = false;\n const release = () => {\n if (!released) {\n released = true;\n uploadReserved = false;\n }\n };\n res.once('finish', release);\n res.once('close', release);\n return next();\n }\n\n function readVerification(filePath = verificationPath) {\n const data = readJson(filePath, { version: 1, reviews: [] });\n return Array.isArray(data.reviews) ? data : { version: 1, reviews: [] };\n }\n\n async function stopActiveRun() {\n epoch += 1;\n const context = activeRun;\n activeRun = null;\n job = { ...blankJob(), status: context ? 'stopping' : 'idle' };\n if (!context) return;\n\n terminating = true;\n context.controller.abort();\n try {\n const children = [...context.children];\n await Promise.all(children.map((child) => terminateChild(child, killOptions)));\n await context.done.catch((error) => {\n if (error?.name !== 'AbortError') throw error;\n });\n job = blankJob();\n } catch (error) {\n job = {\n ...blankJob(),\n status: 'error',\n error: `Could not terminate active run: ${error.message}`,\n };\n throw error;\n } finally {\n if (context.children.size === 0) terminating = false;\n }\n }\n\n function createRunContext(inputPath, filename, overrides) {\n const runId = ++runSequence;\n const runEpoch = epoch;\n const token = `${runId}-${runEpoch}-${randomUUID()}`;\n const workDir = path.join(uploadsDir, `run-${token}`);\n ensureDir(workDir);\n return {\n runId,\n epoch: runEpoch,\n token,\n workDir,\n inputPath,\n filename,\n overrides,\n startedAt: Date.now(),\n controller: new AbortController(),\n children: new Set(),\n done: null,\n };\n }\n\n async function executeRun(context) {\n const outputs = {\n rawVideo: path.join(context.workDir, 'tracked_raw.mp4'),\n browserVideo: path.join(context.workDir, 'tracked.mp4'),\n csv: path.join(context.workDir, 'data.csv'),\n events: path.join(context.workDir, 'events.json'),\n metadata: path.join(context.workDir, 'run_metadata.json'),\n verification: path.join(context.workDir, 'verification.json'),\n };\n const ensureCurrent = () => {\n if (!isCurrent(context)) throw new AbortRunError();\n };\n\n try {\n job.status = 'processing';\n job.progress = 'Counting input frames...';\n const inputFrames = await services.countFrames(context.inputPath, context);\n ensureCurrent();\n\n job.progress = 'Running tracker...';\n const trackerResult = await services.runTracker(\n context.inputPath,\n outputs,\n context.overrides,\n context,\n );\n ensureCurrent();\n if (trackerResult.code !== 0) {\n throw new Error(`Tracker exited with code ${trackerResult.code}: ${trackerResult.stderr.slice(-500)}`);\n }\n\n const syncInfo = parseTrackerSync(trackerResult.stdout);\n if (!syncInfo) throw new Error('Frame integrity evidence missing: TRACKER_SYNC marker required');\n if (!fs.existsSync(outputs.csv)) throw new Error('Tracker data.csv missing');\n if (!fs.existsSync(outputs.rawVideo)) throw new Error('Tracker raw video missing');\n if (!fs.existsSync(outputs.events)) throw new Error('Tracker events.json missing');\n\n const csvRows = countCsvRows(outputs.csv);\n job.progress = 'Counting raw output frames...';\n const rawVideoFrames = await services.countFrames(outputs.rawVideo, context);\n ensureCurrent();\n\n job.progress = 'Transcoding browser video...';\n await services.transcode(outputs.rawVideo, outputs.browserVideo, context);\n ensureCurrent();\n\n job.progress = 'Counting final output frames...';\n const finalVideoFrames = await services.countFrames(outputs.browserVideo, context);\n ensureCurrent();\n\n const integrity = validateFrameIntegrity({\n syncInfo,\n inputFrames,\n csvRows,\n rawVideoFrames,\n finalVideoFrames,\n });\n const fps = readEventsFps(outputs.events);\n const metadata = {\n ...integrity,\n expectedVideoFrames: syncInfo.expectedVideoFrames,\n fps,\n timestamp: new Date().toISOString(),\n };\n writeJson(outputs.metadata, metadata);\n writeJson(outputs.verification, { version: 1, reviews: [] });\n ensureCurrent();\n\n job.progress = 'Publishing validated results...';\n services.publish([\n { source: outputs.csv, destination: path.join(publicDir, 'data.csv') },\n { source: outputs.events, destination: path.join(publicDir, 'events.json') },\n { source: outputs.browserVideo, destination: path.join(publicDir, 'tracked.mp4') },\n { source: outputs.metadata, destination: path.join(publicDir, 'run_metadata.json') },\n { source: outputs.verification, destination: verificationPath },\n ], context.token);\n ensureCurrent();\n\n try {\n services.snapshot({\n historyDir,\n historyMetaPath,\n filename: context.filename,\n durationSec: (Date.now() - context.startedAt) / 1000,\n fps,\n totalFrames: integrity.inputFrames,\n csvSrc: outputs.csv,\n eventsSrc: outputs.events,\n });\n } catch (historyError) {\n console.error(`[Server] History snapshot failed: ${historyError.message}`);\n }\n\n job = {\n ...job,\n frameCount: integrity.inputFrames,\n framesProcessed: integrity.inputFrames,\n totalFrames: integrity.inputFrames,\n status: 'done',\n progress: `Tracking complete! Processed ${integrity.inputFrames} frames.`,\n endTime: Date.now(),\n };\n } catch (error) {\n if (isCurrent(context)) {\n job = {\n ...job,\n status: 'error',\n progress: '',\n error: error.message,\n endTime: Date.now(),\n };\n }\n if (error?.name !== 'AbortError') console.error(`[Server] Run failed: ${error.message}`);\n throw error;\n } finally {\n safeUnlink(context.inputPath);\n safeRemoveDir(context.workDir);\n if (activeRun === context && context.children.size === 0) activeRun = null;\n }\n }\n\n app.get('/api/status', (_req, res) => res.json({ ...job }));\n\n app.post('/api/upload', reserveUpload, upload.single('video'), (req, res) => {\n if (!req.file) return res.status(400).json({ error: 'No video file provided' });\n if (req.uploadEpoch !== epoch) {\n safeUnlink(req.file.path);\n return res.status(409).json({ error: 'Upload was cancelled by a reset.' });\n }\n const overrides = {\n minArea: req.body.minArea,\n maxArea: req.body.maxArea,\n proximityThreshold: req.body.proximityThreshold,\n boutMinFrames: req.body.boutMinFrames,\n };\n const context = createRunContext(req.file.path, req.file.originalname, overrides);\n activeRun = context;\n job = {\n ...blankJob(),\n runId: context.runId,\n status: 'uploading',\n progress: 'Upload complete. Starting validation pipeline...',\n uploadedFilename: context.filename,\n startTime: context.startedAt,\n };\n context.done = executeRun(context).catch(() => {});\n return res.json({ success: true, runId: context.runId });\n });\n\n app.post('/api/reset', async (_req, res) => {\n try {\n await stopActiveRun();\n return res.json({ success: true });\n } catch (error) {\n return res.status(500).json({ error: error.message });\n }\n });\n\n app.get('/api/events', (_req, res) => {\n const data = readJson(path.join(publicDir, 'events.json'), null);\n if (!data) return res.status(404).json({ error: 'No events file found.' });\n return res.json(data);\n });\n\n app.get('/api/verification', (req, res) => {\n if (!req.query.runId) return res.json(readVerification());\n const history = readJson(historyMetaPath, { runs: [] });\n const known = history.runs?.some((run) => run.runId === req.query.runId);\n if (!known) return res.status(404).json({ error: 'Run not found' });\n return res.json(readVerification(path.join(historyDir, req.query.runId, 'verification.json')));\n });\n\n app.post('/api/verification', (req, res) => {\n const { eventId, verdict } = req.body || {};\n if (!eventId || !['confirmed', 'rejected'].includes(verdict)) {\n return res.status(400).json({ error: 'Body must include eventId and verdict.' });\n }\n try {\n const filePath = resolveVerificationPath(\n eventId,\n verificationPath,\n historyDir,\n historyMetaPath,\n );\n const data = readVerification(filePath);\n const review = { event_id: eventId, verdict, reviewed_at: new Date().toISOString() };\n const index = data.reviews.findIndex((item) => item.event_id === eventId);\n if (index >= 0) data.reviews[index] = review;\n else data.reviews.push(review);\n writeJson(filePath, data);\n return res.json({ success: true, review });\n } catch (error) {\n return res.status(400).json({ error: error.message });\n }\n });\n\n app.post('/api/verification/reset', (_req, res) => {\n writeJson(verificationPath, { version: 1, reviews: [] });\n res.json({ success: true });\n });\n\n app.get('/api/history', (_req, res) => {\n res.json(readJson(historyMetaPath, { version: 1, runs: [] }));\n });\n\n app.delete('/api/history', (_req, res) => {\n writeJson(historyMetaPath, { version: 1, runs: [] });\n res.json({ success: true });\n });\n\n app.get('/api/history/:runId', (req, res) => {\n const history = readJson(historyMetaPath, { runs: [] });\n const entry = history.runs?.find((run) => run.runId === req.params.runId);\n if (!entry) return res.status(404).json({ error: 'Run not found' });\n return res.json(entry);\n });\n\n return {\n app,\n stop: stopActiveRun,\n getState: () => ({ epoch, uploadReserved, terminating, activeRun, job: { ...job } }),\n paths: { uploadsDir, publicDir, historyDir, historyMetaPath, verificationPath },\n };\n}\n\nconst isDirectRun = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(modulePath);\nif (isDirectRun) {\n const { app } = createFlytServer();\n app.listen(DEFAULT_PORT, () => {\n console.log(`\\n 🧬 Flyt API Server\\n ➜ http://localhost:${DEFAULT_PORT}`);\n });\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/runner/work/Flyt/Flyt/source app folder/dashboard/tests/frontend-contract.test.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/runner/work/Flyt/Flyt/source app folder/dashboard/tests/server-integration.test.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/runner/work/Flyt/Flyt/source app folder/dashboard/tests/server-utils.test.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]}] From 16b89c19ccd596091fdb7a47c1fb78e219009b74 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 10:26:48 +0530 Subject: [PATCH 30/75] fix: patch frontend integration and server parser exactly --- .github/scripts/apply-review-hardening.py | 38 ++++++++++++++++++----- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/.github/scripts/apply-review-hardening.py b/.github/scripts/apply-review-hardening.py index f2e3689..fdd1bb6 100644 --- a/.github/scripts/apply-review-hardening.py +++ b/.github/scripts/apply-review-hardening.py @@ -1,7 +1,7 @@ from pathlib import Path app_path = Path("source app folder/dashboard/src/App.jsx") -source = app_path.read_text(encoding="utf-8") +app_source = app_path.read_text(encoding="utf-8") old_verification = """ if (!runId) { try { @@ -29,7 +29,7 @@ } catch { /* keep empty reviews */ } """ -replacements = [ +app_replacements = [ (old_verification, new_verification), ( " const loadHistoricRun = async (runId) => {\n", @@ -45,11 +45,33 @@ ), ] -for old, new in replacements: - count = source.count(old) +for old, new in app_replacements: + count = app_source.count(old) if count != 1: - raise RuntimeError(f"Expected one occurrence, found {count}: {old[:80]!r}") - source = source.replace(old, new, 1) + raise RuntimeError(f"Expected one App.jsx occurrence, found {count}: {old[:80]!r}") + app_source = app_source.replace(old, new, 1) -app_path.write_text(source, encoding="utf-8") -print("Applied historic verification and timestamp integration patches.") +app_path.write_text(app_source, encoding="utf-8") + +server_path = Path("source app folder/dashboard/server.js") +server_source = server_path.read_text(encoding="utf-8") +server_replacements = [ + ( + " runTracker: options.services?.runTracker || async (inputPath, outputs, overrides, context) => runCommand(\n", + " runTracker: options.services?.runTracker || ((inputPath, outputs, overrides, context) => runCommand(\n", + ), + ( + " onStderr: (text) => console.error(`[Tracker] ${text.trim()}`),\n },\n ),\n transcode:", + " onStderr: (text) => console.error(`[Tracker] ${text.trim()}`),\n },\n )),\n transcode:", + ), +] + +for old, new in server_replacements: + count = server_source.count(old) + if count != 1: + raise RuntimeError(f"Expected one server.js occurrence, found {count}: {old[:80]!r}") + server_source = server_source.replace(old, new, 1) + +server_path.write_text(server_source, encoding="utf-8") +Path(".github/eslint-diagnostics.json").unlink(missing_ok=True) +print("Applied frontend integration and server parser hardening patches.") From 32af11d7d533b72f4778617a367b43cd476cb209 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 10:27:08 +0530 Subject: [PATCH 31/75] chore: commit all validated review hardening changes --- .github/workflows/apply-review-hardening.yml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/apply-review-hardening.yml b/.github/workflows/apply-review-hardening.yml index a8c93dd..8c292cc 100644 --- a/.github/workflows/apply-review-hardening.yml +++ b/.github/workflows/apply-review-hardening.yml @@ -32,7 +32,7 @@ jobs: cache: pip cache-dependency-path: source app folder/tracker/requirements.txt - - name: Apply exact frontend patch + - name: Apply exact review patches run: python .github/scripts/apply-review-hardening.py - name: Install dashboard dependencies @@ -73,14 +73,20 @@ jobs: - name: Run tracker regression tests run: python -m unittest discover -s 'source app folder/tracker/tests' -p 'test_*.py' -v - - name: Commit validated frontend integration + - name: Commit validated review fixes run: | - if git diff --quiet -- 'source app folder/dashboard/src/App.jsx'; then - echo 'Expected App.jsx patch was not produced.' >&2 + if git diff --quiet -- \ + 'source app folder/dashboard/server.js' \ + 'source app folder/dashboard/src/App.jsx' \ + '.github/eslint-diagnostics.json'; then + echo 'Expected validated review patches were not produced.' >&2 exit 1 fi git config user.name 'github-actions[bot]' git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add 'source app folder/dashboard/src/App.jsx' - git commit -m 'fix: complete historic verification integration' + git add -A \ + 'source app folder/dashboard/server.js' \ + 'source app folder/dashboard/src/App.jsx' \ + '.github/eslint-diagnostics.json' + git commit -m 'fix: complete review hardening integration' git push origin HEAD:fix/critical-job-races From b1b5c0b10496c9837063cf8383c57b8e4c2a8454 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 04:57:38 +0000 Subject: [PATCH 32/75] chore: capture eslint diagnostics --- .github/eslint-diagnostics.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/eslint-diagnostics.json b/.github/eslint-diagnostics.json index f8b16be..2898868 100644 --- a/.github/eslint-diagnostics.json +++ b/.github/eslint-diagnostics.json @@ -2,4 +2,4 @@ > dashboard@0.0.0 lint:backend > eslint server.js server-utils.js tests --format json -[{"filePath":"/home/runner/work/Flyt/Flyt/source app folder/dashboard/server-utils.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/runner/work/Flyt/Flyt/source app folder/dashboard/server.js","messages":[{"ruleId":null,"nodeType":null,"fatal":true,"severity":2,"message":"Parsing error: Unexpected token =>","line":122,"column":71}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":1,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { randomUUID } from 'crypto';\nimport { spawn as nodeSpawn } from 'child_process';\nimport express from 'express';\nimport defaultFfmpegPath from 'ffmpeg-static';\nimport fs from 'fs';\nimport multer from 'multer';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport {\n AbortRunError,\n buildTrackerArgs,\n countCsvRows,\n ensureDir,\n getVideoFrameCount,\n parseTrackerSync,\n publishBundle,\n readEventsFps,\n readJson,\n recoverPublishArtifacts,\n resolveVerificationPath,\n runCommand,\n safeRemoveDir,\n safeUnlink,\n snapshotRunToHistory,\n terminateChild,\n transcodeVideo,\n validateFrameIntegrity,\n writeJson,\n} from './server-utils.js';\n\nconst modulePath = fileURLToPath(import.meta.url);\nconst moduleDir = path.dirname(modulePath);\nconst DEFAULT_PORT = 3001;\nconst DEFAULT_ALLOWED_ORIGINS = [\n 'http://localhost:3001',\n 'http://127.0.0.1:3001',\n 'http://localhost:5173',\n 'http://127.0.0.1:5173',\n];\n\nfunction blankJob() {\n return {\n runId: null,\n status: 'idle',\n progress: '',\n framesProcessed: 0,\n totalFrames: null,\n frameCount: null,\n error: null,\n startTime: null,\n endTime: null,\n };\n}\n\nfunction parseAllowedOrigins(value) {\n if (!value) return DEFAULT_ALLOWED_ORIGINS;\n return value.split(',').map((item) => item.trim()).filter(Boolean);\n}\n\nexport function createFlytServer(options = {}) {\n const rootDir = options.rootDir || moduleDir;\n const uploadsDir = options.uploadsDir || path.join(rootDir, 'uploads');\n const publicDir = options.publicDir || path.join(rootDir, 'public');\n const historyDir = options.historyDir || path.join(publicDir, 'history');\n const historyMetaPath = options.historyMetaPath || path.join(publicDir, 'history.json');\n const verificationPath = options.verificationPath || path.join(publicDir, 'verification.json');\n const trackerDir = options.trackerDir || path.join(rootDir, '..', 'tracker');\n const trackerScript = options.trackerScript || path.join(trackerDir, 'tracker.py');\n const isWindows = process.platform === 'win32';\n const pythonExe = options.pythonExe || path.join(\n trackerDir,\n 'venv',\n isWindows ? 'Scripts' : 'bin',\n isWindows ? 'python.exe' : 'python',\n );\n const ffmpegPath = options.ffmpegPath || defaultFfmpegPath;\n const spawnFn = options.spawnFn || nodeSpawn;\n const defaults = options.defaults || {\n minArea: 30,\n maxArea: 0,\n proximityThreshold: 60,\n boutMinFrames: 90,\n };\n const allowedOrigins = new Set(options.allowedOrigins || parseAllowedOrigins(process.env.FLYT_ALLOWED_ORIGINS));\n const killOptions = options.killOptions || { graceMs: 1000, hardKillMs: 3000 };\n\n for (const dir of [uploadsDir, publicDir, historyDir]) ensureDir(dir);\n recoverPublishArtifacts(publicDir);\n\n const app = express();\n const storage = multer.diskStorage({\n destination: (_req, _file, callback) => callback(null, uploadsDir),\n filename: (_req, file, callback) => {\n callback(null, `input-${Date.now()}-${randomUUID()}${path.extname(file.originalname).toLowerCase()}`);\n },\n });\n const upload = multer({ storage, limits: { fileSize: 10 * 1024 * 1024 * 1024 } });\n\n app.use(express.json());\n app.use((req, res, next) => {\n const origin = req.get('Origin');\n if (!origin) return next();\n if (!allowedOrigins.has(origin)) {\n return res.status(403).json({ error: 'Cross-origin request denied.' });\n }\n res.header('Access-Control-Allow-Origin', origin);\n res.header('Vary', 'Origin');\n res.header('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');\n res.header('Access-Control-Allow-Headers', 'Content-Type');\n if (req.method === 'OPTIONS') return res.sendStatus(204);\n return next();\n });\n\n let runSequence = 0;\n let epoch = 0;\n let uploadReserved = false;\n let terminating = false;\n let activeRun = null;\n let job = blankJob();\n\n const services = {\n countFrames: options.services?.countFrames || (filePath, context) => getVideoFrameCount(\n ffmpegPath,\n filePath,\n {\n spawnFn,\n signal: context.controller.signal,\n children: context.children,\n killOptions,\n },\n ),\n runTracker: options.services?.runTracker || async (inputPath, outputs, overrides, context) => runCommand(\n pythonExe,\n buildTrackerArgs(trackerScript, inputPath, outputs, overrides, defaults),\n {\n spawnFn,\n signal: context.controller.signal,\n children: context.children,\n killOptions,\n onStdout: (text) => {\n const match = text.match(/Processed (\\d+) frames/);\n if (match && isCurrent(context)) {\n job.framesProcessed = Number(match[1]);\n job.progress = `Processed ${job.framesProcessed} frames...`;\n } else if (text.includes('Tracking completed') && isCurrent(context)) {\n job.progress = 'Validating tracker output...';\n }\n },\n onStderr: (text) => console.error(`[Tracker] ${text.trim()}`),\n },\n ),\n transcode: options.services?.transcode || (rawPath, finalPath, context) => transcodeVideo(\n ffmpegPath,\n rawPath,\n finalPath,\n {\n spawnFn,\n signal: context.controller.signal,\n children: context.children,\n killOptions,\n },\n ),\n publish: options.services?.publish || ((entries, token) => publishBundle(entries, token)),\n snapshot: options.services?.snapshot || snapshotRunToHistory,\n };\n\n const isBusy = () => (\n uploadReserved\n || terminating\n || ['uploading', 'processing', 'stopping'].includes(job.status)\n );\n const isCurrent = (context) => (\n activeRun === context\n && context.epoch === epoch\n && !context.controller.signal.aborted\n );\n\n function reserveUpload(req, res, next) {\n if (isBusy()) {\n return res.status(409).json({ error: 'A video is already uploading, processing, or stopping.' });\n }\n req.uploadEpoch = epoch;\n uploadReserved = true;\n let released = false;\n const release = () => {\n if (!released) {\n released = true;\n uploadReserved = false;\n }\n };\n res.once('finish', release);\n res.once('close', release);\n return next();\n }\n\n function readVerification(filePath = verificationPath) {\n const data = readJson(filePath, { version: 1, reviews: [] });\n return Array.isArray(data.reviews) ? data : { version: 1, reviews: [] };\n }\n\n async function stopActiveRun() {\n epoch += 1;\n const context = activeRun;\n activeRun = null;\n job = { ...blankJob(), status: context ? 'stopping' : 'idle' };\n if (!context) return;\n\n terminating = true;\n context.controller.abort();\n try {\n const children = [...context.children];\n await Promise.all(children.map((child) => terminateChild(child, killOptions)));\n await context.done.catch((error) => {\n if (error?.name !== 'AbortError') throw error;\n });\n job = blankJob();\n } catch (error) {\n job = {\n ...blankJob(),\n status: 'error',\n error: `Could not terminate active run: ${error.message}`,\n };\n throw error;\n } finally {\n if (context.children.size === 0) terminating = false;\n }\n }\n\n function createRunContext(inputPath, filename, overrides) {\n const runId = ++runSequence;\n const runEpoch = epoch;\n const token = `${runId}-${runEpoch}-${randomUUID()}`;\n const workDir = path.join(uploadsDir, `run-${token}`);\n ensureDir(workDir);\n return {\n runId,\n epoch: runEpoch,\n token,\n workDir,\n inputPath,\n filename,\n overrides,\n startedAt: Date.now(),\n controller: new AbortController(),\n children: new Set(),\n done: null,\n };\n }\n\n async function executeRun(context) {\n const outputs = {\n rawVideo: path.join(context.workDir, 'tracked_raw.mp4'),\n browserVideo: path.join(context.workDir, 'tracked.mp4'),\n csv: path.join(context.workDir, 'data.csv'),\n events: path.join(context.workDir, 'events.json'),\n metadata: path.join(context.workDir, 'run_metadata.json'),\n verification: path.join(context.workDir, 'verification.json'),\n };\n const ensureCurrent = () => {\n if (!isCurrent(context)) throw new AbortRunError();\n };\n\n try {\n job.status = 'processing';\n job.progress = 'Counting input frames...';\n const inputFrames = await services.countFrames(context.inputPath, context);\n ensureCurrent();\n\n job.progress = 'Running tracker...';\n const trackerResult = await services.runTracker(\n context.inputPath,\n outputs,\n context.overrides,\n context,\n );\n ensureCurrent();\n if (trackerResult.code !== 0) {\n throw new Error(`Tracker exited with code ${trackerResult.code}: ${trackerResult.stderr.slice(-500)}`);\n }\n\n const syncInfo = parseTrackerSync(trackerResult.stdout);\n if (!syncInfo) throw new Error('Frame integrity evidence missing: TRACKER_SYNC marker required');\n if (!fs.existsSync(outputs.csv)) throw new Error('Tracker data.csv missing');\n if (!fs.existsSync(outputs.rawVideo)) throw new Error('Tracker raw video missing');\n if (!fs.existsSync(outputs.events)) throw new Error('Tracker events.json missing');\n\n const csvRows = countCsvRows(outputs.csv);\n job.progress = 'Counting raw output frames...';\n const rawVideoFrames = await services.countFrames(outputs.rawVideo, context);\n ensureCurrent();\n\n job.progress = 'Transcoding browser video...';\n await services.transcode(outputs.rawVideo, outputs.browserVideo, context);\n ensureCurrent();\n\n job.progress = 'Counting final output frames...';\n const finalVideoFrames = await services.countFrames(outputs.browserVideo, context);\n ensureCurrent();\n\n const integrity = validateFrameIntegrity({\n syncInfo,\n inputFrames,\n csvRows,\n rawVideoFrames,\n finalVideoFrames,\n });\n const fps = readEventsFps(outputs.events);\n const metadata = {\n ...integrity,\n expectedVideoFrames: syncInfo.expectedVideoFrames,\n fps,\n timestamp: new Date().toISOString(),\n };\n writeJson(outputs.metadata, metadata);\n writeJson(outputs.verification, { version: 1, reviews: [] });\n ensureCurrent();\n\n job.progress = 'Publishing validated results...';\n services.publish([\n { source: outputs.csv, destination: path.join(publicDir, 'data.csv') },\n { source: outputs.events, destination: path.join(publicDir, 'events.json') },\n { source: outputs.browserVideo, destination: path.join(publicDir, 'tracked.mp4') },\n { source: outputs.metadata, destination: path.join(publicDir, 'run_metadata.json') },\n { source: outputs.verification, destination: verificationPath },\n ], context.token);\n ensureCurrent();\n\n try {\n services.snapshot({\n historyDir,\n historyMetaPath,\n filename: context.filename,\n durationSec: (Date.now() - context.startedAt) / 1000,\n fps,\n totalFrames: integrity.inputFrames,\n csvSrc: outputs.csv,\n eventsSrc: outputs.events,\n });\n } catch (historyError) {\n console.error(`[Server] History snapshot failed: ${historyError.message}`);\n }\n\n job = {\n ...job,\n frameCount: integrity.inputFrames,\n framesProcessed: integrity.inputFrames,\n totalFrames: integrity.inputFrames,\n status: 'done',\n progress: `Tracking complete! Processed ${integrity.inputFrames} frames.`,\n endTime: Date.now(),\n };\n } catch (error) {\n if (isCurrent(context)) {\n job = {\n ...job,\n status: 'error',\n progress: '',\n error: error.message,\n endTime: Date.now(),\n };\n }\n if (error?.name !== 'AbortError') console.error(`[Server] Run failed: ${error.message}`);\n throw error;\n } finally {\n safeUnlink(context.inputPath);\n safeRemoveDir(context.workDir);\n if (activeRun === context && context.children.size === 0) activeRun = null;\n }\n }\n\n app.get('/api/status', (_req, res) => res.json({ ...job }));\n\n app.post('/api/upload', reserveUpload, upload.single('video'), (req, res) => {\n if (!req.file) return res.status(400).json({ error: 'No video file provided' });\n if (req.uploadEpoch !== epoch) {\n safeUnlink(req.file.path);\n return res.status(409).json({ error: 'Upload was cancelled by a reset.' });\n }\n const overrides = {\n minArea: req.body.minArea,\n maxArea: req.body.maxArea,\n proximityThreshold: req.body.proximityThreshold,\n boutMinFrames: req.body.boutMinFrames,\n };\n const context = createRunContext(req.file.path, req.file.originalname, overrides);\n activeRun = context;\n job = {\n ...blankJob(),\n runId: context.runId,\n status: 'uploading',\n progress: 'Upload complete. Starting validation pipeline...',\n uploadedFilename: context.filename,\n startTime: context.startedAt,\n };\n context.done = executeRun(context).catch(() => {});\n return res.json({ success: true, runId: context.runId });\n });\n\n app.post('/api/reset', async (_req, res) => {\n try {\n await stopActiveRun();\n return res.json({ success: true });\n } catch (error) {\n return res.status(500).json({ error: error.message });\n }\n });\n\n app.get('/api/events', (_req, res) => {\n const data = readJson(path.join(publicDir, 'events.json'), null);\n if (!data) return res.status(404).json({ error: 'No events file found.' });\n return res.json(data);\n });\n\n app.get('/api/verification', (req, res) => {\n if (!req.query.runId) return res.json(readVerification());\n const history = readJson(historyMetaPath, { runs: [] });\n const known = history.runs?.some((run) => run.runId === req.query.runId);\n if (!known) return res.status(404).json({ error: 'Run not found' });\n return res.json(readVerification(path.join(historyDir, req.query.runId, 'verification.json')));\n });\n\n app.post('/api/verification', (req, res) => {\n const { eventId, verdict } = req.body || {};\n if (!eventId || !['confirmed', 'rejected'].includes(verdict)) {\n return res.status(400).json({ error: 'Body must include eventId and verdict.' });\n }\n try {\n const filePath = resolveVerificationPath(\n eventId,\n verificationPath,\n historyDir,\n historyMetaPath,\n );\n const data = readVerification(filePath);\n const review = { event_id: eventId, verdict, reviewed_at: new Date().toISOString() };\n const index = data.reviews.findIndex((item) => item.event_id === eventId);\n if (index >= 0) data.reviews[index] = review;\n else data.reviews.push(review);\n writeJson(filePath, data);\n return res.json({ success: true, review });\n } catch (error) {\n return res.status(400).json({ error: error.message });\n }\n });\n\n app.post('/api/verification/reset', (_req, res) => {\n writeJson(verificationPath, { version: 1, reviews: [] });\n res.json({ success: true });\n });\n\n app.get('/api/history', (_req, res) => {\n res.json(readJson(historyMetaPath, { version: 1, runs: [] }));\n });\n\n app.delete('/api/history', (_req, res) => {\n writeJson(historyMetaPath, { version: 1, runs: [] });\n res.json({ success: true });\n });\n\n app.get('/api/history/:runId', (req, res) => {\n const history = readJson(historyMetaPath, { runs: [] });\n const entry = history.runs?.find((run) => run.runId === req.params.runId);\n if (!entry) return res.status(404).json({ error: 'Run not found' });\n return res.json(entry);\n });\n\n return {\n app,\n stop: stopActiveRun,\n getState: () => ({ epoch, uploadReserved, terminating, activeRun, job: { ...job } }),\n paths: { uploadsDir, publicDir, historyDir, historyMetaPath, verificationPath },\n };\n}\n\nconst isDirectRun = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(modulePath);\nif (isDirectRun) {\n const { app } = createFlytServer();\n app.listen(DEFAULT_PORT, () => {\n console.log(`\\n 🧬 Flyt API Server\\n ➜ http://localhost:${DEFAULT_PORT}`);\n });\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/runner/work/Flyt/Flyt/source app folder/dashboard/tests/frontend-contract.test.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/runner/work/Flyt/Flyt/source app folder/dashboard/tests/server-integration.test.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/runner/work/Flyt/Flyt/source app folder/dashboard/tests/server-utils.test.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]}] +[{"filePath":"/home/runner/work/Flyt/Flyt/source app folder/dashboard/server-utils.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/runner/work/Flyt/Flyt/source app folder/dashboard/server.js","messages":[{"ruleId":null,"nodeType":null,"fatal":true,"severity":2,"message":"Parsing error: Unexpected token =>","line":122,"column":71}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":1,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { randomUUID } from 'crypto';\nimport { spawn as nodeSpawn } from 'child_process';\nimport express from 'express';\nimport defaultFfmpegPath from 'ffmpeg-static';\nimport fs from 'fs';\nimport multer from 'multer';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport {\n AbortRunError,\n buildTrackerArgs,\n countCsvRows,\n ensureDir,\n getVideoFrameCount,\n parseTrackerSync,\n publishBundle,\n readEventsFps,\n readJson,\n recoverPublishArtifacts,\n resolveVerificationPath,\n runCommand,\n safeRemoveDir,\n safeUnlink,\n snapshotRunToHistory,\n terminateChild,\n transcodeVideo,\n validateFrameIntegrity,\n writeJson,\n} from './server-utils.js';\n\nconst modulePath = fileURLToPath(import.meta.url);\nconst moduleDir = path.dirname(modulePath);\nconst DEFAULT_PORT = 3001;\nconst DEFAULT_ALLOWED_ORIGINS = [\n 'http://localhost:3001',\n 'http://127.0.0.1:3001',\n 'http://localhost:5173',\n 'http://127.0.0.1:5173',\n];\n\nfunction blankJob() {\n return {\n runId: null,\n status: 'idle',\n progress: '',\n framesProcessed: 0,\n totalFrames: null,\n frameCount: null,\n error: null,\n startTime: null,\n endTime: null,\n };\n}\n\nfunction parseAllowedOrigins(value) {\n if (!value) return DEFAULT_ALLOWED_ORIGINS;\n return value.split(',').map((item) => item.trim()).filter(Boolean);\n}\n\nexport function createFlytServer(options = {}) {\n const rootDir = options.rootDir || moduleDir;\n const uploadsDir = options.uploadsDir || path.join(rootDir, 'uploads');\n const publicDir = options.publicDir || path.join(rootDir, 'public');\n const historyDir = options.historyDir || path.join(publicDir, 'history');\n const historyMetaPath = options.historyMetaPath || path.join(publicDir, 'history.json');\n const verificationPath = options.verificationPath || path.join(publicDir, 'verification.json');\n const trackerDir = options.trackerDir || path.join(rootDir, '..', 'tracker');\n const trackerScript = options.trackerScript || path.join(trackerDir, 'tracker.py');\n const isWindows = process.platform === 'win32';\n const pythonExe = options.pythonExe || path.join(\n trackerDir,\n 'venv',\n isWindows ? 'Scripts' : 'bin',\n isWindows ? 'python.exe' : 'python',\n );\n const ffmpegPath = options.ffmpegPath || defaultFfmpegPath;\n const spawnFn = options.spawnFn || nodeSpawn;\n const defaults = options.defaults || {\n minArea: 30,\n maxArea: 0,\n proximityThreshold: 60,\n boutMinFrames: 90,\n };\n const allowedOrigins = new Set(options.allowedOrigins || parseAllowedOrigins(process.env.FLYT_ALLOWED_ORIGINS));\n const killOptions = options.killOptions || { graceMs: 1000, hardKillMs: 3000 };\n\n for (const dir of [uploadsDir, publicDir, historyDir]) ensureDir(dir);\n recoverPublishArtifacts(publicDir);\n\n const app = express();\n const storage = multer.diskStorage({\n destination: (_req, _file, callback) => callback(null, uploadsDir),\n filename: (_req, file, callback) => {\n callback(null, `input-${Date.now()}-${randomUUID()}${path.extname(file.originalname).toLowerCase()}`);\n },\n });\n const upload = multer({ storage, limits: { fileSize: 10 * 1024 * 1024 * 1024 } });\n\n app.use(express.json());\n app.use((req, res, next) => {\n const origin = req.get('Origin');\n if (!origin) return next();\n if (!allowedOrigins.has(origin)) {\n return res.status(403).json({ error: 'Cross-origin request denied.' });\n }\n res.header('Access-Control-Allow-Origin', origin);\n res.header('Vary', 'Origin');\n res.header('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');\n res.header('Access-Control-Allow-Headers', 'Content-Type');\n if (req.method === 'OPTIONS') return res.sendStatus(204);\n return next();\n });\n\n let runSequence = 0;\n let epoch = 0;\n let uploadReserved = false;\n let terminating = false;\n let activeRun = null;\n let job = blankJob();\n\n const services = {\n countFrames: options.services?.countFrames || (filePath, context) => getVideoFrameCount(\n ffmpegPath,\n filePath,\n {\n spawnFn,\n signal: context.controller.signal,\n children: context.children,\n killOptions,\n },\n ),\n runTracker: options.services?.runTracker || ((inputPath, outputs, overrides, context) => runCommand(\n pythonExe,\n buildTrackerArgs(trackerScript, inputPath, outputs, overrides, defaults),\n {\n spawnFn,\n signal: context.controller.signal,\n children: context.children,\n killOptions,\n onStdout: (text) => {\n const match = text.match(/Processed (\\d+) frames/);\n if (match && isCurrent(context)) {\n job.framesProcessed = Number(match[1]);\n job.progress = `Processed ${job.framesProcessed} frames...`;\n } else if (text.includes('Tracking completed') && isCurrent(context)) {\n job.progress = 'Validating tracker output...';\n }\n },\n onStderr: (text) => console.error(`[Tracker] ${text.trim()}`),\n },\n )),\n transcode: options.services?.transcode || (rawPath, finalPath, context) => transcodeVideo(\n ffmpegPath,\n rawPath,\n finalPath,\n {\n spawnFn,\n signal: context.controller.signal,\n children: context.children,\n killOptions,\n },\n ),\n publish: options.services?.publish || ((entries, token) => publishBundle(entries, token)),\n snapshot: options.services?.snapshot || snapshotRunToHistory,\n };\n\n const isBusy = () => (\n uploadReserved\n || terminating\n || ['uploading', 'processing', 'stopping'].includes(job.status)\n );\n const isCurrent = (context) => (\n activeRun === context\n && context.epoch === epoch\n && !context.controller.signal.aborted\n );\n\n function reserveUpload(req, res, next) {\n if (isBusy()) {\n return res.status(409).json({ error: 'A video is already uploading, processing, or stopping.' });\n }\n req.uploadEpoch = epoch;\n uploadReserved = true;\n let released = false;\n const release = () => {\n if (!released) {\n released = true;\n uploadReserved = false;\n }\n };\n res.once('finish', release);\n res.once('close', release);\n return next();\n }\n\n function readVerification(filePath = verificationPath) {\n const data = readJson(filePath, { version: 1, reviews: [] });\n return Array.isArray(data.reviews) ? data : { version: 1, reviews: [] };\n }\n\n async function stopActiveRun() {\n epoch += 1;\n const context = activeRun;\n activeRun = null;\n job = { ...blankJob(), status: context ? 'stopping' : 'idle' };\n if (!context) return;\n\n terminating = true;\n context.controller.abort();\n try {\n const children = [...context.children];\n await Promise.all(children.map((child) => terminateChild(child, killOptions)));\n await context.done.catch((error) => {\n if (error?.name !== 'AbortError') throw error;\n });\n job = blankJob();\n } catch (error) {\n job = {\n ...blankJob(),\n status: 'error',\n error: `Could not terminate active run: ${error.message}`,\n };\n throw error;\n } finally {\n if (context.children.size === 0) terminating = false;\n }\n }\n\n function createRunContext(inputPath, filename, overrides) {\n const runId = ++runSequence;\n const runEpoch = epoch;\n const token = `${runId}-${runEpoch}-${randomUUID()}`;\n const workDir = path.join(uploadsDir, `run-${token}`);\n ensureDir(workDir);\n return {\n runId,\n epoch: runEpoch,\n token,\n workDir,\n inputPath,\n filename,\n overrides,\n startedAt: Date.now(),\n controller: new AbortController(),\n children: new Set(),\n done: null,\n };\n }\n\n async function executeRun(context) {\n const outputs = {\n rawVideo: path.join(context.workDir, 'tracked_raw.mp4'),\n browserVideo: path.join(context.workDir, 'tracked.mp4'),\n csv: path.join(context.workDir, 'data.csv'),\n events: path.join(context.workDir, 'events.json'),\n metadata: path.join(context.workDir, 'run_metadata.json'),\n verification: path.join(context.workDir, 'verification.json'),\n };\n const ensureCurrent = () => {\n if (!isCurrent(context)) throw new AbortRunError();\n };\n\n try {\n job.status = 'processing';\n job.progress = 'Counting input frames...';\n const inputFrames = await services.countFrames(context.inputPath, context);\n ensureCurrent();\n\n job.progress = 'Running tracker...';\n const trackerResult = await services.runTracker(\n context.inputPath,\n outputs,\n context.overrides,\n context,\n );\n ensureCurrent();\n if (trackerResult.code !== 0) {\n throw new Error(`Tracker exited with code ${trackerResult.code}: ${trackerResult.stderr.slice(-500)}`);\n }\n\n const syncInfo = parseTrackerSync(trackerResult.stdout);\n if (!syncInfo) throw new Error('Frame integrity evidence missing: TRACKER_SYNC marker required');\n if (!fs.existsSync(outputs.csv)) throw new Error('Tracker data.csv missing');\n if (!fs.existsSync(outputs.rawVideo)) throw new Error('Tracker raw video missing');\n if (!fs.existsSync(outputs.events)) throw new Error('Tracker events.json missing');\n\n const csvRows = countCsvRows(outputs.csv);\n job.progress = 'Counting raw output frames...';\n const rawVideoFrames = await services.countFrames(outputs.rawVideo, context);\n ensureCurrent();\n\n job.progress = 'Transcoding browser video...';\n await services.transcode(outputs.rawVideo, outputs.browserVideo, context);\n ensureCurrent();\n\n job.progress = 'Counting final output frames...';\n const finalVideoFrames = await services.countFrames(outputs.browserVideo, context);\n ensureCurrent();\n\n const integrity = validateFrameIntegrity({\n syncInfo,\n inputFrames,\n csvRows,\n rawVideoFrames,\n finalVideoFrames,\n });\n const fps = readEventsFps(outputs.events);\n const metadata = {\n ...integrity,\n expectedVideoFrames: syncInfo.expectedVideoFrames,\n fps,\n timestamp: new Date().toISOString(),\n };\n writeJson(outputs.metadata, metadata);\n writeJson(outputs.verification, { version: 1, reviews: [] });\n ensureCurrent();\n\n job.progress = 'Publishing validated results...';\n services.publish([\n { source: outputs.csv, destination: path.join(publicDir, 'data.csv') },\n { source: outputs.events, destination: path.join(publicDir, 'events.json') },\n { source: outputs.browserVideo, destination: path.join(publicDir, 'tracked.mp4') },\n { source: outputs.metadata, destination: path.join(publicDir, 'run_metadata.json') },\n { source: outputs.verification, destination: verificationPath },\n ], context.token);\n ensureCurrent();\n\n try {\n services.snapshot({\n historyDir,\n historyMetaPath,\n filename: context.filename,\n durationSec: (Date.now() - context.startedAt) / 1000,\n fps,\n totalFrames: integrity.inputFrames,\n csvSrc: outputs.csv,\n eventsSrc: outputs.events,\n });\n } catch (historyError) {\n console.error(`[Server] History snapshot failed: ${historyError.message}`);\n }\n\n job = {\n ...job,\n frameCount: integrity.inputFrames,\n framesProcessed: integrity.inputFrames,\n totalFrames: integrity.inputFrames,\n status: 'done',\n progress: `Tracking complete! Processed ${integrity.inputFrames} frames.`,\n endTime: Date.now(),\n };\n } catch (error) {\n if (isCurrent(context)) {\n job = {\n ...job,\n status: 'error',\n progress: '',\n error: error.message,\n endTime: Date.now(),\n };\n }\n if (error?.name !== 'AbortError') console.error(`[Server] Run failed: ${error.message}`);\n throw error;\n } finally {\n safeUnlink(context.inputPath);\n safeRemoveDir(context.workDir);\n if (activeRun === context && context.children.size === 0) activeRun = null;\n }\n }\n\n app.get('/api/status', (_req, res) => res.json({ ...job }));\n\n app.post('/api/upload', reserveUpload, upload.single('video'), (req, res) => {\n if (!req.file) return res.status(400).json({ error: 'No video file provided' });\n if (req.uploadEpoch !== epoch) {\n safeUnlink(req.file.path);\n return res.status(409).json({ error: 'Upload was cancelled by a reset.' });\n }\n const overrides = {\n minArea: req.body.minArea,\n maxArea: req.body.maxArea,\n proximityThreshold: req.body.proximityThreshold,\n boutMinFrames: req.body.boutMinFrames,\n };\n const context = createRunContext(req.file.path, req.file.originalname, overrides);\n activeRun = context;\n job = {\n ...blankJob(),\n runId: context.runId,\n status: 'uploading',\n progress: 'Upload complete. Starting validation pipeline...',\n uploadedFilename: context.filename,\n startTime: context.startedAt,\n };\n context.done = executeRun(context).catch(() => {});\n return res.json({ success: true, runId: context.runId });\n });\n\n app.post('/api/reset', async (_req, res) => {\n try {\n await stopActiveRun();\n return res.json({ success: true });\n } catch (error) {\n return res.status(500).json({ error: error.message });\n }\n });\n\n app.get('/api/events', (_req, res) => {\n const data = readJson(path.join(publicDir, 'events.json'), null);\n if (!data) return res.status(404).json({ error: 'No events file found.' });\n return res.json(data);\n });\n\n app.get('/api/verification', (req, res) => {\n if (!req.query.runId) return res.json(readVerification());\n const history = readJson(historyMetaPath, { runs: [] });\n const known = history.runs?.some((run) => run.runId === req.query.runId);\n if (!known) return res.status(404).json({ error: 'Run not found' });\n return res.json(readVerification(path.join(historyDir, req.query.runId, 'verification.json')));\n });\n\n app.post('/api/verification', (req, res) => {\n const { eventId, verdict } = req.body || {};\n if (!eventId || !['confirmed', 'rejected'].includes(verdict)) {\n return res.status(400).json({ error: 'Body must include eventId and verdict.' });\n }\n try {\n const filePath = resolveVerificationPath(\n eventId,\n verificationPath,\n historyDir,\n historyMetaPath,\n );\n const data = readVerification(filePath);\n const review = { event_id: eventId, verdict, reviewed_at: new Date().toISOString() };\n const index = data.reviews.findIndex((item) => item.event_id === eventId);\n if (index >= 0) data.reviews[index] = review;\n else data.reviews.push(review);\n writeJson(filePath, data);\n return res.json({ success: true, review });\n } catch (error) {\n return res.status(400).json({ error: error.message });\n }\n });\n\n app.post('/api/verification/reset', (_req, res) => {\n writeJson(verificationPath, { version: 1, reviews: [] });\n res.json({ success: true });\n });\n\n app.get('/api/history', (_req, res) => {\n res.json(readJson(historyMetaPath, { version: 1, runs: [] }));\n });\n\n app.delete('/api/history', (_req, res) => {\n writeJson(historyMetaPath, { version: 1, runs: [] });\n res.json({ success: true });\n });\n\n app.get('/api/history/:runId', (req, res) => {\n const history = readJson(historyMetaPath, { runs: [] });\n const entry = history.runs?.find((run) => run.runId === req.params.runId);\n if (!entry) return res.status(404).json({ error: 'Run not found' });\n return res.json(entry);\n });\n\n return {\n app,\n stop: stopActiveRun,\n getState: () => ({ epoch, uploadReserved, terminating, activeRun, job: { ...job } }),\n paths: { uploadsDir, publicDir, historyDir, historyMetaPath, verificationPath },\n };\n}\n\nconst isDirectRun = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(modulePath);\nif (isDirectRun) {\n const { app } = createFlytServer();\n app.listen(DEFAULT_PORT, () => {\n console.log(`\\n 🧬 Flyt API Server\\n ➜ http://localhost:${DEFAULT_PORT}`);\n });\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/runner/work/Flyt/Flyt/source app folder/dashboard/tests/frontend-contract.test.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/runner/work/Flyt/Flyt/source app folder/dashboard/tests/server-integration.test.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/runner/work/Flyt/Flyt/source app folder/dashboard/tests/server-utils.test.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]}] From 1a310f8a45adb8f6aba7a80c0a2227e23b705a49 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 10:28:49 +0530 Subject: [PATCH 33/75] fix: group every injected service arrow function --- .github/scripts/apply-review-hardening.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/scripts/apply-review-hardening.py b/.github/scripts/apply-review-hardening.py index fdd1bb6..57ecd64 100644 --- a/.github/scripts/apply-review-hardening.py +++ b/.github/scripts/apply-review-hardening.py @@ -56,6 +56,14 @@ server_path = Path("source app folder/dashboard/server.js") server_source = server_path.read_text(encoding="utf-8") server_replacements = [ + ( + " countFrames: options.services?.countFrames || (filePath, context) => getVideoFrameCount(\n", + " countFrames: options.services?.countFrames || ((filePath, context) => getVideoFrameCount(\n", + ), + ( + " killOptions,\n },\n ),\n runTracker:", + " killOptions,\n },\n )),\n runTracker:", + ), ( " runTracker: options.services?.runTracker || async (inputPath, outputs, overrides, context) => runCommand(\n", " runTracker: options.services?.runTracker || ((inputPath, outputs, overrides, context) => runCommand(\n", @@ -64,6 +72,14 @@ " onStderr: (text) => console.error(`[Tracker] ${text.trim()}`),\n },\n ),\n transcode:", " onStderr: (text) => console.error(`[Tracker] ${text.trim()}`),\n },\n )),\n transcode:", ), + ( + " transcode: options.services?.transcode || (rawPath, finalPath, context) => transcodeVideo(\n", + " transcode: options.services?.transcode || ((rawPath, finalPath, context) => transcodeVideo(\n", + ), + ( + " killOptions,\n },\n ),\n publish:", + " killOptions,\n },\n )),\n publish:", + ), ] for old, new in server_replacements: @@ -74,4 +90,4 @@ server_path.write_text(server_source, encoding="utf-8") Path(".github/eslint-diagnostics.json").unlink(missing_ok=True) -print("Applied frontend integration and server parser hardening patches.") +print("Applied frontend integration and all server parser hardening patches.") From e28a4d141e7c30ff9dce2c719d764e5824d618e3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 04:59:18 +0000 Subject: [PATCH 34/75] fix: complete review hardening integration --- .github/eslint-diagnostics.json | 5 ----- source app folder/dashboard/server.js | 12 +++++----- source app folder/dashboard/src/App.jsx | 30 +++++++++++++------------ 3 files changed, 22 insertions(+), 25 deletions(-) delete mode 100644 .github/eslint-diagnostics.json diff --git a/.github/eslint-diagnostics.json b/.github/eslint-diagnostics.json deleted file mode 100644 index 2898868..0000000 --- a/.github/eslint-diagnostics.json +++ /dev/null @@ -1,5 +0,0 @@ - -> dashboard@0.0.0 lint:backend -> eslint server.js server-utils.js tests --format json - -[{"filePath":"/home/runner/work/Flyt/Flyt/source app folder/dashboard/server-utils.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/runner/work/Flyt/Flyt/source app folder/dashboard/server.js","messages":[{"ruleId":null,"nodeType":null,"fatal":true,"severity":2,"message":"Parsing error: Unexpected token =>","line":122,"column":71}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":1,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { randomUUID } from 'crypto';\nimport { spawn as nodeSpawn } from 'child_process';\nimport express from 'express';\nimport defaultFfmpegPath from 'ffmpeg-static';\nimport fs from 'fs';\nimport multer from 'multer';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport {\n AbortRunError,\n buildTrackerArgs,\n countCsvRows,\n ensureDir,\n getVideoFrameCount,\n parseTrackerSync,\n publishBundle,\n readEventsFps,\n readJson,\n recoverPublishArtifacts,\n resolveVerificationPath,\n runCommand,\n safeRemoveDir,\n safeUnlink,\n snapshotRunToHistory,\n terminateChild,\n transcodeVideo,\n validateFrameIntegrity,\n writeJson,\n} from './server-utils.js';\n\nconst modulePath = fileURLToPath(import.meta.url);\nconst moduleDir = path.dirname(modulePath);\nconst DEFAULT_PORT = 3001;\nconst DEFAULT_ALLOWED_ORIGINS = [\n 'http://localhost:3001',\n 'http://127.0.0.1:3001',\n 'http://localhost:5173',\n 'http://127.0.0.1:5173',\n];\n\nfunction blankJob() {\n return {\n runId: null,\n status: 'idle',\n progress: '',\n framesProcessed: 0,\n totalFrames: null,\n frameCount: null,\n error: null,\n startTime: null,\n endTime: null,\n };\n}\n\nfunction parseAllowedOrigins(value) {\n if (!value) return DEFAULT_ALLOWED_ORIGINS;\n return value.split(',').map((item) => item.trim()).filter(Boolean);\n}\n\nexport function createFlytServer(options = {}) {\n const rootDir = options.rootDir || moduleDir;\n const uploadsDir = options.uploadsDir || path.join(rootDir, 'uploads');\n const publicDir = options.publicDir || path.join(rootDir, 'public');\n const historyDir = options.historyDir || path.join(publicDir, 'history');\n const historyMetaPath = options.historyMetaPath || path.join(publicDir, 'history.json');\n const verificationPath = options.verificationPath || path.join(publicDir, 'verification.json');\n const trackerDir = options.trackerDir || path.join(rootDir, '..', 'tracker');\n const trackerScript = options.trackerScript || path.join(trackerDir, 'tracker.py');\n const isWindows = process.platform === 'win32';\n const pythonExe = options.pythonExe || path.join(\n trackerDir,\n 'venv',\n isWindows ? 'Scripts' : 'bin',\n isWindows ? 'python.exe' : 'python',\n );\n const ffmpegPath = options.ffmpegPath || defaultFfmpegPath;\n const spawnFn = options.spawnFn || nodeSpawn;\n const defaults = options.defaults || {\n minArea: 30,\n maxArea: 0,\n proximityThreshold: 60,\n boutMinFrames: 90,\n };\n const allowedOrigins = new Set(options.allowedOrigins || parseAllowedOrigins(process.env.FLYT_ALLOWED_ORIGINS));\n const killOptions = options.killOptions || { graceMs: 1000, hardKillMs: 3000 };\n\n for (const dir of [uploadsDir, publicDir, historyDir]) ensureDir(dir);\n recoverPublishArtifacts(publicDir);\n\n const app = express();\n const storage = multer.diskStorage({\n destination: (_req, _file, callback) => callback(null, uploadsDir),\n filename: (_req, file, callback) => {\n callback(null, `input-${Date.now()}-${randomUUID()}${path.extname(file.originalname).toLowerCase()}`);\n },\n });\n const upload = multer({ storage, limits: { fileSize: 10 * 1024 * 1024 * 1024 } });\n\n app.use(express.json());\n app.use((req, res, next) => {\n const origin = req.get('Origin');\n if (!origin) return next();\n if (!allowedOrigins.has(origin)) {\n return res.status(403).json({ error: 'Cross-origin request denied.' });\n }\n res.header('Access-Control-Allow-Origin', origin);\n res.header('Vary', 'Origin');\n res.header('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');\n res.header('Access-Control-Allow-Headers', 'Content-Type');\n if (req.method === 'OPTIONS') return res.sendStatus(204);\n return next();\n });\n\n let runSequence = 0;\n let epoch = 0;\n let uploadReserved = false;\n let terminating = false;\n let activeRun = null;\n let job = blankJob();\n\n const services = {\n countFrames: options.services?.countFrames || (filePath, context) => getVideoFrameCount(\n ffmpegPath,\n filePath,\n {\n spawnFn,\n signal: context.controller.signal,\n children: context.children,\n killOptions,\n },\n ),\n runTracker: options.services?.runTracker || ((inputPath, outputs, overrides, context) => runCommand(\n pythonExe,\n buildTrackerArgs(trackerScript, inputPath, outputs, overrides, defaults),\n {\n spawnFn,\n signal: context.controller.signal,\n children: context.children,\n killOptions,\n onStdout: (text) => {\n const match = text.match(/Processed (\\d+) frames/);\n if (match && isCurrent(context)) {\n job.framesProcessed = Number(match[1]);\n job.progress = `Processed ${job.framesProcessed} frames...`;\n } else if (text.includes('Tracking completed') && isCurrent(context)) {\n job.progress = 'Validating tracker output...';\n }\n },\n onStderr: (text) => console.error(`[Tracker] ${text.trim()}`),\n },\n )),\n transcode: options.services?.transcode || (rawPath, finalPath, context) => transcodeVideo(\n ffmpegPath,\n rawPath,\n finalPath,\n {\n spawnFn,\n signal: context.controller.signal,\n children: context.children,\n killOptions,\n },\n ),\n publish: options.services?.publish || ((entries, token) => publishBundle(entries, token)),\n snapshot: options.services?.snapshot || snapshotRunToHistory,\n };\n\n const isBusy = () => (\n uploadReserved\n || terminating\n || ['uploading', 'processing', 'stopping'].includes(job.status)\n );\n const isCurrent = (context) => (\n activeRun === context\n && context.epoch === epoch\n && !context.controller.signal.aborted\n );\n\n function reserveUpload(req, res, next) {\n if (isBusy()) {\n return res.status(409).json({ error: 'A video is already uploading, processing, or stopping.' });\n }\n req.uploadEpoch = epoch;\n uploadReserved = true;\n let released = false;\n const release = () => {\n if (!released) {\n released = true;\n uploadReserved = false;\n }\n };\n res.once('finish', release);\n res.once('close', release);\n return next();\n }\n\n function readVerification(filePath = verificationPath) {\n const data = readJson(filePath, { version: 1, reviews: [] });\n return Array.isArray(data.reviews) ? data : { version: 1, reviews: [] };\n }\n\n async function stopActiveRun() {\n epoch += 1;\n const context = activeRun;\n activeRun = null;\n job = { ...blankJob(), status: context ? 'stopping' : 'idle' };\n if (!context) return;\n\n terminating = true;\n context.controller.abort();\n try {\n const children = [...context.children];\n await Promise.all(children.map((child) => terminateChild(child, killOptions)));\n await context.done.catch((error) => {\n if (error?.name !== 'AbortError') throw error;\n });\n job = blankJob();\n } catch (error) {\n job = {\n ...blankJob(),\n status: 'error',\n error: `Could not terminate active run: ${error.message}`,\n };\n throw error;\n } finally {\n if (context.children.size === 0) terminating = false;\n }\n }\n\n function createRunContext(inputPath, filename, overrides) {\n const runId = ++runSequence;\n const runEpoch = epoch;\n const token = `${runId}-${runEpoch}-${randomUUID()}`;\n const workDir = path.join(uploadsDir, `run-${token}`);\n ensureDir(workDir);\n return {\n runId,\n epoch: runEpoch,\n token,\n workDir,\n inputPath,\n filename,\n overrides,\n startedAt: Date.now(),\n controller: new AbortController(),\n children: new Set(),\n done: null,\n };\n }\n\n async function executeRun(context) {\n const outputs = {\n rawVideo: path.join(context.workDir, 'tracked_raw.mp4'),\n browserVideo: path.join(context.workDir, 'tracked.mp4'),\n csv: path.join(context.workDir, 'data.csv'),\n events: path.join(context.workDir, 'events.json'),\n metadata: path.join(context.workDir, 'run_metadata.json'),\n verification: path.join(context.workDir, 'verification.json'),\n };\n const ensureCurrent = () => {\n if (!isCurrent(context)) throw new AbortRunError();\n };\n\n try {\n job.status = 'processing';\n job.progress = 'Counting input frames...';\n const inputFrames = await services.countFrames(context.inputPath, context);\n ensureCurrent();\n\n job.progress = 'Running tracker...';\n const trackerResult = await services.runTracker(\n context.inputPath,\n outputs,\n context.overrides,\n context,\n );\n ensureCurrent();\n if (trackerResult.code !== 0) {\n throw new Error(`Tracker exited with code ${trackerResult.code}: ${trackerResult.stderr.slice(-500)}`);\n }\n\n const syncInfo = parseTrackerSync(trackerResult.stdout);\n if (!syncInfo) throw new Error('Frame integrity evidence missing: TRACKER_SYNC marker required');\n if (!fs.existsSync(outputs.csv)) throw new Error('Tracker data.csv missing');\n if (!fs.existsSync(outputs.rawVideo)) throw new Error('Tracker raw video missing');\n if (!fs.existsSync(outputs.events)) throw new Error('Tracker events.json missing');\n\n const csvRows = countCsvRows(outputs.csv);\n job.progress = 'Counting raw output frames...';\n const rawVideoFrames = await services.countFrames(outputs.rawVideo, context);\n ensureCurrent();\n\n job.progress = 'Transcoding browser video...';\n await services.transcode(outputs.rawVideo, outputs.browserVideo, context);\n ensureCurrent();\n\n job.progress = 'Counting final output frames...';\n const finalVideoFrames = await services.countFrames(outputs.browserVideo, context);\n ensureCurrent();\n\n const integrity = validateFrameIntegrity({\n syncInfo,\n inputFrames,\n csvRows,\n rawVideoFrames,\n finalVideoFrames,\n });\n const fps = readEventsFps(outputs.events);\n const metadata = {\n ...integrity,\n expectedVideoFrames: syncInfo.expectedVideoFrames,\n fps,\n timestamp: new Date().toISOString(),\n };\n writeJson(outputs.metadata, metadata);\n writeJson(outputs.verification, { version: 1, reviews: [] });\n ensureCurrent();\n\n job.progress = 'Publishing validated results...';\n services.publish([\n { source: outputs.csv, destination: path.join(publicDir, 'data.csv') },\n { source: outputs.events, destination: path.join(publicDir, 'events.json') },\n { source: outputs.browserVideo, destination: path.join(publicDir, 'tracked.mp4') },\n { source: outputs.metadata, destination: path.join(publicDir, 'run_metadata.json') },\n { source: outputs.verification, destination: verificationPath },\n ], context.token);\n ensureCurrent();\n\n try {\n services.snapshot({\n historyDir,\n historyMetaPath,\n filename: context.filename,\n durationSec: (Date.now() - context.startedAt) / 1000,\n fps,\n totalFrames: integrity.inputFrames,\n csvSrc: outputs.csv,\n eventsSrc: outputs.events,\n });\n } catch (historyError) {\n console.error(`[Server] History snapshot failed: ${historyError.message}`);\n }\n\n job = {\n ...job,\n frameCount: integrity.inputFrames,\n framesProcessed: integrity.inputFrames,\n totalFrames: integrity.inputFrames,\n status: 'done',\n progress: `Tracking complete! Processed ${integrity.inputFrames} frames.`,\n endTime: Date.now(),\n };\n } catch (error) {\n if (isCurrent(context)) {\n job = {\n ...job,\n status: 'error',\n progress: '',\n error: error.message,\n endTime: Date.now(),\n };\n }\n if (error?.name !== 'AbortError') console.error(`[Server] Run failed: ${error.message}`);\n throw error;\n } finally {\n safeUnlink(context.inputPath);\n safeRemoveDir(context.workDir);\n if (activeRun === context && context.children.size === 0) activeRun = null;\n }\n }\n\n app.get('/api/status', (_req, res) => res.json({ ...job }));\n\n app.post('/api/upload', reserveUpload, upload.single('video'), (req, res) => {\n if (!req.file) return res.status(400).json({ error: 'No video file provided' });\n if (req.uploadEpoch !== epoch) {\n safeUnlink(req.file.path);\n return res.status(409).json({ error: 'Upload was cancelled by a reset.' });\n }\n const overrides = {\n minArea: req.body.minArea,\n maxArea: req.body.maxArea,\n proximityThreshold: req.body.proximityThreshold,\n boutMinFrames: req.body.boutMinFrames,\n };\n const context = createRunContext(req.file.path, req.file.originalname, overrides);\n activeRun = context;\n job = {\n ...blankJob(),\n runId: context.runId,\n status: 'uploading',\n progress: 'Upload complete. Starting validation pipeline...',\n uploadedFilename: context.filename,\n startTime: context.startedAt,\n };\n context.done = executeRun(context).catch(() => {});\n return res.json({ success: true, runId: context.runId });\n });\n\n app.post('/api/reset', async (_req, res) => {\n try {\n await stopActiveRun();\n return res.json({ success: true });\n } catch (error) {\n return res.status(500).json({ error: error.message });\n }\n });\n\n app.get('/api/events', (_req, res) => {\n const data = readJson(path.join(publicDir, 'events.json'), null);\n if (!data) return res.status(404).json({ error: 'No events file found.' });\n return res.json(data);\n });\n\n app.get('/api/verification', (req, res) => {\n if (!req.query.runId) return res.json(readVerification());\n const history = readJson(historyMetaPath, { runs: [] });\n const known = history.runs?.some((run) => run.runId === req.query.runId);\n if (!known) return res.status(404).json({ error: 'Run not found' });\n return res.json(readVerification(path.join(historyDir, req.query.runId, 'verification.json')));\n });\n\n app.post('/api/verification', (req, res) => {\n const { eventId, verdict } = req.body || {};\n if (!eventId || !['confirmed', 'rejected'].includes(verdict)) {\n return res.status(400).json({ error: 'Body must include eventId and verdict.' });\n }\n try {\n const filePath = resolveVerificationPath(\n eventId,\n verificationPath,\n historyDir,\n historyMetaPath,\n );\n const data = readVerification(filePath);\n const review = { event_id: eventId, verdict, reviewed_at: new Date().toISOString() };\n const index = data.reviews.findIndex((item) => item.event_id === eventId);\n if (index >= 0) data.reviews[index] = review;\n else data.reviews.push(review);\n writeJson(filePath, data);\n return res.json({ success: true, review });\n } catch (error) {\n return res.status(400).json({ error: error.message });\n }\n });\n\n app.post('/api/verification/reset', (_req, res) => {\n writeJson(verificationPath, { version: 1, reviews: [] });\n res.json({ success: true });\n });\n\n app.get('/api/history', (_req, res) => {\n res.json(readJson(historyMetaPath, { version: 1, runs: [] }));\n });\n\n app.delete('/api/history', (_req, res) => {\n writeJson(historyMetaPath, { version: 1, runs: [] });\n res.json({ success: true });\n });\n\n app.get('/api/history/:runId', (req, res) => {\n const history = readJson(historyMetaPath, { runs: [] });\n const entry = history.runs?.find((run) => run.runId === req.params.runId);\n if (!entry) return res.status(404).json({ error: 'Run not found' });\n return res.json(entry);\n });\n\n return {\n app,\n stop: stopActiveRun,\n getState: () => ({ epoch, uploadReserved, terminating, activeRun, job: { ...job } }),\n paths: { uploadsDir, publicDir, historyDir, historyMetaPath, verificationPath },\n };\n}\n\nconst isDirectRun = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(modulePath);\nif (isDirectRun) {\n const { app } = createFlytServer();\n app.listen(DEFAULT_PORT, () => {\n console.log(`\\n 🧬 Flyt API Server\\n ➜ http://localhost:${DEFAULT_PORT}`);\n });\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/runner/work/Flyt/Flyt/source app folder/dashboard/tests/frontend-contract.test.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/runner/work/Flyt/Flyt/source app folder/dashboard/tests/server-integration.test.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/runner/work/Flyt/Flyt/source app folder/dashboard/tests/server-utils.test.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]}] diff --git a/source app folder/dashboard/server.js b/source app folder/dashboard/server.js index a6edec5..844564c 100644 --- a/source app folder/dashboard/server.js +++ b/source app folder/dashboard/server.js @@ -119,7 +119,7 @@ export function createFlytServer(options = {}) { let job = blankJob(); const services = { - countFrames: options.services?.countFrames || (filePath, context) => getVideoFrameCount( + countFrames: options.services?.countFrames || ((filePath, context) => getVideoFrameCount( ffmpegPath, filePath, { @@ -128,8 +128,8 @@ export function createFlytServer(options = {}) { children: context.children, killOptions, }, - ), - runTracker: options.services?.runTracker || async (inputPath, outputs, overrides, context) => runCommand( + )), + runTracker: options.services?.runTracker || ((inputPath, outputs, overrides, context) => runCommand( pythonExe, buildTrackerArgs(trackerScript, inputPath, outputs, overrides, defaults), { @@ -148,8 +148,8 @@ export function createFlytServer(options = {}) { }, onStderr: (text) => console.error(`[Tracker] ${text.trim()}`), }, - ), - transcode: options.services?.transcode || (rawPath, finalPath, context) => transcodeVideo( + )), + transcode: options.services?.transcode || ((rawPath, finalPath, context) => transcodeVideo( ffmpegPath, rawPath, finalPath, @@ -159,7 +159,7 @@ export function createFlytServer(options = {}) { children: context.children, killOptions, }, - ), + )), publish: options.services?.publish || ((entries, token) => publishBundle(entries, token)), snapshot: options.services?.snapshot || snapshotRunToHistory, }; diff --git a/source app folder/dashboard/src/App.jsx b/source app folder/dashboard/src/App.jsx index cd5c72e..f22b504 100644 --- a/source app folder/dashboard/src/App.jsx +++ b/source app folder/dashboard/src/App.jsx @@ -649,17 +649,18 @@ function App() { } } catch { /* events optional until first track */ } - if (!runId) { - try { - const verRes = await fetch(`/api/verification?t=${cacheBust}`, { cache: 'no-store' }); - if (verRes.ok) { - const verData = await verRes.json(); - (verData.reviews || []).forEach((r) => { - reviews[r.event_id] = r; - }); - } - } catch { /* keep empty reviews */ } - } + try { + const verificationUrl = runId + ? `/api/verification?runId=${encodeURIComponent(runId)}&t=${cacheBust}` + : `/api/verification?t=${cacheBust}`; + const verRes = await fetch(verificationUrl, { cache: 'no-store' }); + if (verRes.ok) { + const verData = await verRes.json(); + (verData.reviews || []).forEach((r) => { + reviews[r.event_id] = r; + }); + } + } catch { /* keep empty reviews */ } if (generation !== loadGenerationRef.current) return { fps: 30 }; @@ -791,11 +792,12 @@ function App() { // Load a past run's snapshot (data.csv + events.json from public/history//) // into the active dashboard. Video for past runs is not snapshotted — clear it // so the player doesn't show a stale frame. - const loadHistoricRun = async (runId) => { + const loadHistoricRun = async (run) => { + const runId = run.runId; const generation = ++loadGenerationRef.current; const cacheBust = Date.now(); setMediaCacheBust(cacheBust); - setRunTimestamp(new Date().toISOString()); + setRunTimestamp(run.timestamp || null); setIsHistoricRun(true); try { // Load events first to get accurate fps (avoids stale state) for sleep + pxsec normalization @@ -1034,7 +1036,7 @@ function App() { history.map((run) => (
loadHistoricRun(run.runId)} + onClick={() => loadHistoricRun(run)} className="grid grid-cols-5 p-4 border-b border-zinc-200 dark:border-zinc-800 text-sm items-center hover:bg-zinc-50 dark:hover:bg-zinc-900/50 cursor-pointer transition-colors group last:border-b-0">
{run.runId}
{formatRunDate(run.timestamp)}
From da6944f6b2e7108976cf950b6d7c7b3bd078961a Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 10:30:27 +0530 Subject: [PATCH 35/75] test: require historic verified counts to survive reload --- source app folder/dashboard/tests/frontend-contract.test.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/source app folder/dashboard/tests/frontend-contract.test.js b/source app folder/dashboard/tests/frontend-contract.test.js index dbd9807..49df876 100644 --- a/source app folder/dashboard/tests/frontend-contract.test.js +++ b/source app folder/dashboard/tests/frontend-contract.test.js @@ -7,10 +7,12 @@ import { fileURLToPath } from 'node:url'; const testDir = path.dirname(fileURLToPath(import.meta.url)); const appSource = () => fs.readFileSync(path.join(testDir, '..', 'src', 'App.jsx'), 'utf8'); -test('historic runs load their scoped verification state', () => { +test('historic runs load and count their scoped verification state', () => { const source = appSource(); assert.match(source, /api\/verification\?runId=\$\{encodeURIComponent\(runId\)\}/); assert.doesNotMatch(source, /if \(!runId\) \{\s*try \{\s*const verRes/); + assert.match(source, /const \{ detected, verified \} = computeCourtshipStats\(eventList, reviews\)/); + assert.doesNotMatch(source, /verified:\s*0/); }); test('historic runs display their recorded timestamp rather than load time', () => { From f83150aa6fd376c8dcf217763799b22915544bf0 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 10:30:49 +0530 Subject: [PATCH 36/75] fix: preserve historic verified counts after reload --- .github/scripts/apply-review-hardening.py | 105 +++++----------------- 1 file changed, 20 insertions(+), 85 deletions(-) diff --git a/.github/scripts/apply-review-hardening.py b/.github/scripts/apply-review-hardening.py index 57ecd64..94a8323 100644 --- a/.github/scripts/apply-review-hardening.py +++ b/.github/scripts/apply-review-hardening.py @@ -1,93 +1,28 @@ from pathlib import Path app_path = Path("source app folder/dashboard/src/App.jsx") -app_source = app_path.read_text(encoding="utf-8") +source = app_path.read_text(encoding="utf-8") -old_verification = """ if (!runId) { - try { - const verRes = await fetch(`/api/verification?t=${cacheBust}`, { cache: 'no-store' }); - if (verRes.ok) { - const verData = await verRes.json(); - (verData.reviews || []).forEach((r) => { - reviews[r.event_id] = r; - }); - } - } catch { /* keep empty reviews */ } - } -""" -new_verification = """ try { - const verificationUrl = runId - ? `/api/verification?runId=${encodeURIComponent(runId)}&t=${cacheBust}` - : `/api/verification?t=${cacheBust}`; - const verRes = await fetch(verificationUrl, { cache: 'no-store' }); - if (verRes.ok) { - const verData = await verRes.json(); - (verData.reviews || []).forEach((r) => { - reviews[r.event_id] = r; - }); - } - } catch { /* keep empty reviews */ } -""" - -app_replacements = [ - (old_verification, new_verification), - ( - " const loadHistoricRun = async (runId) => {\n", - " const loadHistoricRun = async (run) => {\n const runId = run.runId;\n", - ), - ( - " setRunTimestamp(new Date().toISOString());\n", - " setRunTimestamp(run.timestamp || null);\n", - ), - ( - "onClick={() => loadHistoricRun(run.runId)}", - "onClick={() => loadHistoricRun(run)}", - ), +required_fragments = [ + "/api/verification?runId=${encodeURIComponent(runId)}", + "setRunTimestamp(run.timestamp || null)", + "onClick={() => loadHistoricRun(run)}", ] +for fragment in required_fragments: + if fragment not in source: + raise RuntimeError(f"Previously validated frontend integration is missing: {fragment}") -for old, new in app_replacements: - count = app_source.count(old) - if count != 1: - raise RuntimeError(f"Expected one App.jsx occurrence, found {count}: {old[:80]!r}") - app_source = app_source.replace(old, new, 1) - -app_path.write_text(app_source, encoding="utf-8") - -server_path = Path("source app folder/dashboard/server.js") -server_source = server_path.read_text(encoding="utf-8") -server_replacements = [ - ( - " countFrames: options.services?.countFrames || (filePath, context) => getVideoFrameCount(\n", - " countFrames: options.services?.countFrames || ((filePath, context) => getVideoFrameCount(\n", - ), - ( - " killOptions,\n },\n ),\n runTracker:", - " killOptions,\n },\n )),\n runTracker:", - ), - ( - " runTracker: options.services?.runTracker || async (inputPath, outputs, overrides, context) => runCommand(\n", - " runTracker: options.services?.runTracker || ((inputPath, outputs, overrides, context) => runCommand(\n", - ), - ( - " onStderr: (text) => console.error(`[Tracker] ${text.trim()}`),\n },\n ),\n transcode:", - " onStderr: (text) => console.error(`[Tracker] ${text.trim()}`),\n },\n )),\n transcode:", - ), - ( - " transcode: options.services?.transcode || (rawPath, finalPath, context) => transcodeVideo(\n", - " transcode: options.services?.transcode || ((rawPath, finalPath, context) => transcodeVideo(\n", - ), - ( - " killOptions,\n },\n ),\n publish:", - " killOptions,\n },\n )),\n publish:", - ), -] - -for old, new in server_replacements: - count = server_source.count(old) - if count != 1: - raise RuntimeError(f"Expected one server.js occurrence, found {count}: {old[:80]!r}") - server_source = server_source.replace(old, new, 1) +old_stats = """ const { detected, verified } = runId + ? { detected: eventList.filter((e) => e.type === 'courtship_bout').length, verified: 0 } + : computeCourtshipStats(eventList, reviews); +""" +new_stats = """ const { detected, verified } = computeCourtshipStats(eventList, reviews); +""" -server_path.write_text(server_source, encoding="utf-8") +count = source.count(old_stats) +if count != 1: + raise RuntimeError(f"Expected one historic stats fallback, found {count}") +source = source.replace(old_stats, new_stats, 1) +app_path.write_text(source, encoding="utf-8") Path(".github/eslint-diagnostics.json").unlink(missing_ok=True) -print("Applied frontend integration and all server parser hardening patches.") +print("Applied historic verified-count integration patch.") From dd8ee89a4f214fb4ffbd27606b2a5bc7f1aa1a73 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 10:33:18 +0530 Subject: [PATCH 37/75] chore: persist full review validation diagnostics --- .github/workflows/apply-review-hardening.yml | 76 ++++++++++---------- 1 file changed, 37 insertions(+), 39 deletions(-) diff --git a/.github/workflows/apply-review-hardening.yml b/.github/workflows/apply-review-hardening.yml index 8c292cc..13de4d7 100644 --- a/.github/workflows/apply-review-hardening.yml +++ b/.github/workflows/apply-review-hardening.yml @@ -32,61 +32,59 @@ jobs: cache: pip cache-dependency-path: source app folder/tracker/requirements.txt - - name: Apply exact review patches + - name: Apply exact review patch run: python .github/scripts/apply-review-hardening.py - - name: Install dashboard dependencies - working-directory: source app folder/dashboard - run: npm ci - - - name: Lint backend and tests - working-directory: source app folder/dashboard + - name: Validate and capture exact diagnostics run: | set +e - npm run lint:backend -- --format json > ../../.github/eslint-diagnostics.json - status=$? - cat ../../.github/eslint-diagnostics.json - if [ "$status" -ne 0 ]; then + diagnostics='.github/validation-diagnostics.txt' + : > "$diagnostics" + overall=0 + + run_check() { + label="$1" + shift + echo "===== $label =====" | tee -a "$diagnostics" + "$@" >> "$diagnostics" 2>&1 + status=$? + cat "$diagnostics" + if [ "$status" -ne 0 ]; then + overall="$status" + fi + echo >> "$diagnostics" + } + + run_check 'npm ci' bash -lc "cd 'source app folder/dashboard' && npm ci" + run_check 'backend lint' bash -lc "cd 'source app folder/dashboard' && npm run lint:backend" + run_check 'Node HTTP and utility tests' bash -lc "cd 'source app folder/dashboard' && npm run test:server" + run_check 'dashboard build' bash -lc "cd 'source app folder/dashboard' && npm run build" + run_check 'tracker dependency install' python -m pip install -r 'source app folder/tracker/requirements.txt' + run_check 'tracker syntax' python -m py_compile 'source app folder/tracker/tracker.py' + run_check 'tracker regression tests' python -m unittest discover -s 'source app folder/tracker/tests' -p 'test_*.py' -v + + if [ "$overall" -ne 0 ]; then git config user.name 'github-actions[bot]' git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add ../../.github/eslint-diagnostics.json - git commit -m 'chore: capture eslint diagnostics' + git add "$diagnostics" + git commit -m 'chore: capture review validation diagnostics' git push origin HEAD:fix/critical-job-races - exit "$status" + exit "$overall" fi - rm ../../.github/eslint-diagnostics.json - - - name: Run HTTP and utility regression tests - working-directory: source app folder/dashboard - run: npm run test:server - - name: Build dashboard - working-directory: source app folder/dashboard - run: npm run build + rm -f "$diagnostics" '.github/eslint-diagnostics.json' - - name: Install tracker dependencies - run: python -m pip install -r 'source app folder/tracker/requirements.txt' - - - name: Check tracker syntax - run: python -m py_compile 'source app folder/tracker/tracker.py' - - - name: Run tracker regression tests - run: python -m unittest discover -s 'source app folder/tracker/tests' -p 'test_*.py' -v - - - name: Commit validated review fixes + - name: Commit validated UI integration run: | - if git diff --quiet -- \ - 'source app folder/dashboard/server.js' \ - 'source app folder/dashboard/src/App.jsx' \ - '.github/eslint-diagnostics.json'; then - echo 'Expected validated review patches were not produced.' >&2 + if git diff --quiet -- 'source app folder/dashboard/src/App.jsx'; then + echo 'Expected App.jsx review patch was not produced.' >&2 exit 1 fi git config user.name 'github-actions[bot]' git config user.email '41898282+github-actions[bot]@users.noreply.github.com' git add -A \ - 'source app folder/dashboard/server.js' \ 'source app folder/dashboard/src/App.jsx' \ + '.github/validation-diagnostics.txt' \ '.github/eslint-diagnostics.json' - git commit -m 'fix: complete review hardening integration' + git commit -m 'fix: preserve historic verified review counts' git push origin HEAD:fix/critical-job-races From 8574b0d7c98524b126c06378dffa6673f493c8c8 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 10:36:09 +0530 Subject: [PATCH 38/75] chore: bound every review validation command --- .github/workflows/apply-review-hardening.yml | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/apply-review-hardening.yml b/.github/workflows/apply-review-hardening.yml index 13de4d7..0c1f3bf 100644 --- a/.github/workflows/apply-review-hardening.yml +++ b/.github/workflows/apply-review-hardening.yml @@ -14,6 +14,7 @@ permissions: jobs: patch-and-validate: runs-on: ubuntu-latest + timeout-minutes: 12 steps: - uses: actions/checkout@v4 with: @@ -44,9 +45,10 @@ jobs: run_check() { label="$1" - shift + seconds="$2" + shift 2 echo "===== $label =====" | tee -a "$diagnostics" - "$@" >> "$diagnostics" 2>&1 + timeout --signal=TERM --kill-after=10s "${seconds}s" "$@" >> "$diagnostics" 2>&1 status=$? cat "$diagnostics" if [ "$status" -ne 0 ]; then @@ -55,13 +57,13 @@ jobs: echo >> "$diagnostics" } - run_check 'npm ci' bash -lc "cd 'source app folder/dashboard' && npm ci" - run_check 'backend lint' bash -lc "cd 'source app folder/dashboard' && npm run lint:backend" - run_check 'Node HTTP and utility tests' bash -lc "cd 'source app folder/dashboard' && npm run test:server" - run_check 'dashboard build' bash -lc "cd 'source app folder/dashboard' && npm run build" - run_check 'tracker dependency install' python -m pip install -r 'source app folder/tracker/requirements.txt' - run_check 'tracker syntax' python -m py_compile 'source app folder/tracker/tracker.py' - run_check 'tracker regression tests' python -m unittest discover -s 'source app folder/tracker/tests' -p 'test_*.py' -v + run_check 'npm ci' 180 bash -lc "cd 'source app folder/dashboard' && npm ci" + run_check 'backend lint' 60 bash -lc "cd 'source app folder/dashboard' && npm run lint:backend" + run_check 'Node HTTP and utility tests' 120 bash -lc "cd 'source app folder/dashboard' && npm run test:server" + run_check 'dashboard build' 120 bash -lc "cd 'source app folder/dashboard' && npm run build" + run_check 'tracker dependency install' 180 python -m pip install -r 'source app folder/tracker/requirements.txt' + run_check 'tracker syntax' 30 python -m py_compile 'source app folder/tracker/tracker.py' + run_check 'tracker regression tests' 60 python -m unittest discover -s 'source app folder/tracker/tests' -p 'test_*.py' -v if [ "$overall" -ne 0 ]; then git config user.name 'github-actions[bot]' From c58c45b6007927fcd69abab81502f77ee9f2a83c Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 10:41:15 +0530 Subject: [PATCH 39/75] chore: expose review hardening as a PR check --- .github/workflows/apply-review-hardening.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/apply-review-hardening.yml b/.github/workflows/apply-review-hardening.yml index 0c1f3bf..b403e69 100644 --- a/.github/workflows/apply-review-hardening.yml +++ b/.github/workflows/apply-review-hardening.yml @@ -7,6 +7,12 @@ on: paths: - '.github/scripts/apply-review-hardening.py' - '.github/workflows/apply-review-hardening.yml' + pull_request: + paths: + - '.github/scripts/apply-review-hardening.py' + - '.github/workflows/apply-review-hardening.yml' + - 'source app folder/dashboard/src/App.jsx' + - 'source app folder/dashboard/tests/frontend-contract.test.js' permissions: contents: write From 4c7d9aadbaf01258605964fa12dca70e149dc3e2 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 10:43:01 +0530 Subject: [PATCH 40/75] chore: commit only the validated App change --- .github/workflows/apply-review-hardening.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/apply-review-hardening.yml b/.github/workflows/apply-review-hardening.yml index b403e69..696977d 100644 --- a/.github/workflows/apply-review-hardening.yml +++ b/.github/workflows/apply-review-hardening.yml @@ -90,9 +90,6 @@ jobs: fi git config user.name 'github-actions[bot]' git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add -A \ - 'source app folder/dashboard/src/App.jsx' \ - '.github/validation-diagnostics.txt' \ - '.github/eslint-diagnostics.json' + git add 'source app folder/dashboard/src/App.jsx' git commit -m 'fix: preserve historic verified review counts' git push origin HEAD:fix/critical-job-races From 6d2f942cb961bb695d40ddd069e9e962634b7094 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 05:13:32 +0000 Subject: [PATCH 41/75] fix: preserve historic verified review counts --- source app folder/dashboard/src/App.jsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/source app folder/dashboard/src/App.jsx b/source app folder/dashboard/src/App.jsx index f22b504..1f53394 100644 --- a/source app folder/dashboard/src/App.jsx +++ b/source app folder/dashboard/src/App.jsx @@ -669,9 +669,7 @@ function App() { setEvents(eventList); setReviewsByEventId(reviews); setStats((prev) => { - const { detected, verified } = runId - ? { detected: eventList.filter((e) => e.type === 'courtship_bout').length, verified: 0 } - : computeCourtshipStats(eventList, reviews); + const { detected, verified } = computeCourtshipStats(eventList, reviews); return { ...prev, courtshipDetected: detected, courtshipVerified: verified }; }); return { fps, frames }; From a76e0d8412f59bebb37cadea497e11d5b17f2fa7 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 10:44:57 +0530 Subject: [PATCH 42/75] chore: remove temporary review patch script --- .github/scripts/apply-review-hardening.py | 28 ----------------------- 1 file changed, 28 deletions(-) delete mode 100644 .github/scripts/apply-review-hardening.py diff --git a/.github/scripts/apply-review-hardening.py b/.github/scripts/apply-review-hardening.py deleted file mode 100644 index 94a8323..0000000 --- a/.github/scripts/apply-review-hardening.py +++ /dev/null @@ -1,28 +0,0 @@ -from pathlib import Path - -app_path = Path("source app folder/dashboard/src/App.jsx") -source = app_path.read_text(encoding="utf-8") - -required_fragments = [ - "/api/verification?runId=${encodeURIComponent(runId)}", - "setRunTimestamp(run.timestamp || null)", - "onClick={() => loadHistoricRun(run)}", -] -for fragment in required_fragments: - if fragment not in source: - raise RuntimeError(f"Previously validated frontend integration is missing: {fragment}") - -old_stats = """ const { detected, verified } = runId - ? { detected: eventList.filter((e) => e.type === 'courtship_bout').length, verified: 0 } - : computeCourtshipStats(eventList, reviews); -""" -new_stats = """ const { detected, verified } = computeCourtshipStats(eventList, reviews); -""" - -count = source.count(old_stats) -if count != 1: - raise RuntimeError(f"Expected one historic stats fallback, found {count}") -source = source.replace(old_stats, new_stats, 1) -app_path.write_text(source, encoding="utf-8") -Path(".github/eslint-diagnostics.json").unlink(missing_ok=True) -print("Applied historic verified-count integration patch.") From 6cf685eb2daeb4a09eec72e48c06f1cb7fb94e07 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 10:45:04 +0530 Subject: [PATCH 43/75] chore: remove temporary review hardening workflow --- .github/workflows/apply-review-hardening.yml | 95 -------------------- 1 file changed, 95 deletions(-) delete mode 100644 .github/workflows/apply-review-hardening.yml diff --git a/.github/workflows/apply-review-hardening.yml b/.github/workflows/apply-review-hardening.yml deleted file mode 100644 index 696977d..0000000 --- a/.github/workflows/apply-review-hardening.yml +++ /dev/null @@ -1,95 +0,0 @@ -name: Apply review hardening - -on: - push: - branches: - - fix/critical-job-races - paths: - - '.github/scripts/apply-review-hardening.py' - - '.github/workflows/apply-review-hardening.yml' - pull_request: - paths: - - '.github/scripts/apply-review-hardening.py' - - '.github/workflows/apply-review-hardening.yml' - - 'source app folder/dashboard/src/App.jsx' - - 'source app folder/dashboard/tests/frontend-contract.test.js' - -permissions: - contents: write - -jobs: - patch-and-validate: - runs-on: ubuntu-latest - timeout-minutes: 12 - steps: - - uses: actions/checkout@v4 - with: - ref: fix/critical-job-races - fetch-depth: 0 - - - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: npm - cache-dependency-path: source app folder/dashboard/package-lock.json - - - uses: actions/setup-python@v5 - with: - python-version: '3.11' - cache: pip - cache-dependency-path: source app folder/tracker/requirements.txt - - - name: Apply exact review patch - run: python .github/scripts/apply-review-hardening.py - - - name: Validate and capture exact diagnostics - run: | - set +e - diagnostics='.github/validation-diagnostics.txt' - : > "$diagnostics" - overall=0 - - run_check() { - label="$1" - seconds="$2" - shift 2 - echo "===== $label =====" | tee -a "$diagnostics" - timeout --signal=TERM --kill-after=10s "${seconds}s" "$@" >> "$diagnostics" 2>&1 - status=$? - cat "$diagnostics" - if [ "$status" -ne 0 ]; then - overall="$status" - fi - echo >> "$diagnostics" - } - - run_check 'npm ci' 180 bash -lc "cd 'source app folder/dashboard' && npm ci" - run_check 'backend lint' 60 bash -lc "cd 'source app folder/dashboard' && npm run lint:backend" - run_check 'Node HTTP and utility tests' 120 bash -lc "cd 'source app folder/dashboard' && npm run test:server" - run_check 'dashboard build' 120 bash -lc "cd 'source app folder/dashboard' && npm run build" - run_check 'tracker dependency install' 180 python -m pip install -r 'source app folder/tracker/requirements.txt' - run_check 'tracker syntax' 30 python -m py_compile 'source app folder/tracker/tracker.py' - run_check 'tracker regression tests' 60 python -m unittest discover -s 'source app folder/tracker/tests' -p 'test_*.py' -v - - if [ "$overall" -ne 0 ]; then - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add "$diagnostics" - git commit -m 'chore: capture review validation diagnostics' - git push origin HEAD:fix/critical-job-races - exit "$overall" - fi - - rm -f "$diagnostics" '.github/eslint-diagnostics.json' - - - name: Commit validated UI integration - run: | - if git diff --quiet -- 'source app folder/dashboard/src/App.jsx'; then - echo 'Expected App.jsx review patch was not produced.' >&2 - exit 1 - fi - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add 'source app folder/dashboard/src/App.jsx' - git commit -m 'fix: preserve historic verified review counts' - git push origin HEAD:fix/critical-job-races From 64038a42e050af8afd6c7231c484115e2ae3c226 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 10:45:30 +0530 Subject: [PATCH 44/75] ci: make final race and scientific validation explicit --- .github/workflows/critical-fixes-ci.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/critical-fixes-ci.yml b/.github/workflows/critical-fixes-ci.yml index 11f8adb..e610dce 100644 --- a/.github/workflows/critical-fixes-ci.yml +++ b/.github/workflows/critical-fixes-ci.yml @@ -16,6 +16,7 @@ permissions: jobs: validate: runs-on: ubuntu-latest + timeout-minutes: 15 steps: - uses: actions/checkout@v4 @@ -29,11 +30,11 @@ jobs: working-directory: source app folder/dashboard run: npm ci - - name: Lint backend changes + - name: Lint backend and regression tests working-directory: source app folder/dashboard run: npm run lint:backend - - name: Run server regression tests + - name: Run utility, HTTP race, and frontend contract tests working-directory: source app folder/dashboard run: npm run test:server @@ -50,8 +51,8 @@ jobs: - name: Install tracker dependencies run: python -m pip install -r 'source app folder/tracker/requirements.txt' - - name: Check Python syntax + - name: Check tracker syntax run: python -m py_compile 'source app folder/tracker/tracker.py' - - name: Run tracker regression tests + - name: Run tracker scientific regression tests run: python -m unittest discover -s 'source app folder/tracker/tests' -p 'test_*.py' -v From ae00aef5d73c726d1b7b3b64011f39308e33181d Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 10:48:57 +0530 Subject: [PATCH 45/75] test: prove abort terminates a real child process --- .../dashboard/tests/server-utils.test.js | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/source app folder/dashboard/tests/server-utils.test.js b/source app folder/dashboard/tests/server-utils.test.js index 5aa8b07..037a336 100644 --- a/source app folder/dashboard/tests/server-utils.test.js +++ b/source app folder/dashboard/tests/server-utils.test.js @@ -10,6 +10,7 @@ import { publishBundle, recoverPublishArtifacts, resolveVerificationPath, + runCommand, scopeEventsForHistory, snapshotRunToHistory, terminateChild, @@ -174,3 +175,25 @@ test('termination escalates to SIGKILL and resolves only after close', async () await termination; assert.deepEqual(child.signals, ['SIGTERM', 'SIGKILL']); }); + +test('abort kills a real child process that ignores SIGTERM', async () => { + const controller = new AbortController(); + const children = new Set(); + const command = runCommand(process.execPath, [ + '-e', + "process.on('SIGTERM', () => {}); setInterval(() => {}, 1000);", + ], { + signal: controller.signal, + children, + killOptions: { graceMs: 20, hardKillMs: 500 }, + }); + + const deadline = Date.now() + 1000; + while (children.size === 0 && Date.now() < deadline) { + await new Promise((resolve) => setTimeout(resolve, 5)); + } + assert.equal(children.size, 1); + controller.abort(); + await assert.rejects(command, (error) => error?.name === 'AbortError'); + assert.equal(children.size, 0); +}); From c6fdd777cb39a641f305ce7c459995e548e421f3 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 11:29:41 +0530 Subject: [PATCH 46/75] chore: stage transparent final hardening patch 1/5 --- .github/scripts/final-hardening.part00 | 280 +++++++++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 .github/scripts/final-hardening.part00 diff --git a/.github/scripts/final-hardening.part00 b/.github/scripts/final-hardening.part00 new file mode 100644 index 0000000..6858c04 --- /dev/null +++ b/.github/scripts/final-hardening.part00 @@ -0,0 +1,280 @@ +from pathlib import Path + + +def replace_once(text: str, old: str, new: str, label: str) -> str: + count = text.count(old) + if count != 1: + raise RuntimeError(f"{label}: expected one occurrence, found {count}") + return text.replace(old, new, 1) + + +def replace_between(text: str, start: str, end: str, new_block: str, label: str) -> str: + start_index = text.find(start) + end_index = text.find(end, start_index + len(start)) + if start_index < 0 or end_index < 0: + raise RuntimeError(f"{label}: markers not found") + return text[:start_index] + new_block.rstrip() + "\n\n" + text[end_index:] + + +root = Path('.') +marker_path = root / 'source app folder/dashboard/src/metrics.js' +utils_marker_path = root / 'source app folder/dashboard/server-utils.js' +if marker_path.exists() and 'SimulatedProcessCrash' in utils_marker_path.read_text(encoding='utf-8'): + print('Final hardening is already applied; validation-only run.') + raise SystemExit(0) + +# --------------------------------------------------------------------------- +# Transaction-aware publication, canonical run history, and valid metrics. +# --------------------------------------------------------------------------- +utils_path = root / 'source app folder/dashboard/server-utils.js' +utils = utils_path.read_text(encoding='utf-8') + +publication_block = r'''export class SimulatedProcessCrash extends Error { + constructor(message = 'Simulated process crash') { + super(message); + this.name = 'SimulatedProcessCrash'; + } +} + +function validatePublishToken(token) { + if (!/^[A-Za-z0-9-]+$/.test(String(token))) { + throw new Error(`Invalid publication token: ${token}`); + } +} + +function publishManifestPath(entries, token, manifestDir) { + if (!entries.length) throw new Error('Publication bundle is empty'); + validatePublishToken(token); + const directory = manifestDir || path.dirname(entries[0].destination); + ensureDir(directory); + return path.join(directory, `.flyt-publish-${token}.json`); +} + +function validatePublishManifest(manifest, manifestPath) { + if (!manifest || manifest.version !== 1 || !Array.isArray(manifest.entries)) { + throw new Error(`Invalid publication transaction manifest: ${manifestPath}`); + } + if (!['prepared', 'publishing', 'committed'].includes(manifest.state)) { + throw new Error(`Invalid publication transaction state in ${manifestPath}`); + } + manifest.entries.forEach((entry) => { + for (const key of ['destination', 'stage', 'backup']) { + if (typeof entry[key] !== 'string' || !path.isAbsolute(entry[key])) { + throw new Error(`Invalid ${key} in publication transaction ${manifestPath}`); + } + } + if (typeof entry.hadDestination !== 'boolean') { + throw new Error(`Invalid hadDestination in publication transaction ${manifestPath}`); + } + }); + return manifest; +} + +function rollbackPublishManifest(manifest) { + const errors = []; + [...manifest.entries].reverse().forEach((entry) => { + try { + if (entry.hadDestination) { + if (fs.existsSync(entry.backup)) { + safeUnlink(entry.destination); + ensureDir(path.dirname(entry.destination)); + fs.renameSync(entry.backup, entry.destination); + } else if (!fs.existsSync(entry.destination)) { + throw new Error(`Cannot restore missing prior destination: ${entry.destination}`); + } + } else { + safeUnlink(entry.destination); + safeUnlink(entry.backup); + } + safeUnlink(entry.stage); + } catch (error) { + errors.push(error); + } + }); + if (errors.length) { + throw new AggregateError(errors, 'Publication transaction rollback failed'); + } +} + +function finalizeCommittedManifest(manifest) { + const missing = manifest.entries.filter((entry) => !fs.existsSync(entry.destination)); + if (missing.length) { + rollbackPublishManifest(manifest); + return; + } + manifest.entries.forEach((entry) => { + safeUnlink(entry.stage); + safeUnlink(entry.backup); + }); +} + +function listFilesRecursive(directory) { + if (!fs.existsSync(directory)) return []; + const files = []; + fs.readdirSync(directory, { withFileTypes: true }).forEach((entry) => { + const fullPath = path.join(directory, entry.name); + if (entry.isDirectory()) files.push(...listFilesRecursive(fullPath)); + else if (entry.isFile()) files.push(fullPath); + }); + return files; +} + +export function publishBundle(entries, token, { faultInjector, manifestDir } = {}) { + const manifestPath = publishManifestPath(entries, token, manifestDir); + const manifest = { + version: 1, + token, + state: 'prepared', + entries: entries.map(({ source, destination }) => ({ + source, + destination: path.resolve(destination), + stage: path.resolve(`${destination}.${token}.new`), + backup: path.resolve(`${destination}.${token}.bak`), + hadDestination: fs.existsSync(destination), + })), + }; + + try { + manifest.entries.forEach((entry) => { + if (!fs.existsSync(entry.source)) throw new Error(`Missing publish source: ${entry.source}`); + ensureDir(path.dirname(entry.destination)); + fs.copyFileSync(entry.source, entry.stage); + }); + writeJson(manifestPath, manifest); + faultInjector?.('prepared', -1); + + manifest.entries.forEach((entry, index) => { + if (entry.hadDestination) fs.renameSync(entry.destination, entry.backup); + faultInjector?.('backed-up', index); + }); + manifest.state = 'publishing'; + writeJson(manifestPath, manifest); + + manifest.entries.forEach((entry, index) => { + fs.renameSync(entry.stage, entry.destination); + faultInjector?.('published', index); + }); + manifest.state = 'committed'; + writeJson(manifestPath, manifest); + faultInjector?.('committed', -1); + + finalizeCommittedManifest(manifest); + safeUnlink(manifestPath); + } catch (error) { + if (error instanceof SimulatedProcessCrash) throw error; + try { + rollbackPublishManifest(manifest); + safeUnlink(manifestPath); + } catch (rollbackError) { + throw new AggregateError([error, rollbackError], 'Publication failed and rollback was incomplete'); + } + throw error; + } +} + +export function recoverPublishArtifacts(directory) { + if (!fs.existsSync(directory)) return; + const manifests = fs.readdirSync(directory) + .filter((name) => /^\.flyt-publish-[A-Za-z0-9-]+\.json$/.test(name)) + .map((name) => path.join(directory, name)); + + manifests.forEach((manifestPath) => { + const manifest = validatePublishManifest(readJson(manifestPath, null), manifestPath); + if (manifest.state === 'committed') finalizeCommittedManifest(manifest); + else rollbackPublishManifest(manifest); + safeUnlink(manifestPath); + }); + + const artifacts = listFilesRecursive(directory); + const orphanBackups = artifacts.filter((filePath) => filePath.endsWith('.bak')); + if (orphanBackups.length) { + throw new Error( + `Unrecoverable publication backups without a transaction manifest: ${orphanBackups.join(', ')}`, + ); + } + artifacts.filter((filePath) => filePath.endsWith('.new')).forEach(safeUnlink); +}''' + +utils = replace_between( + utils, + 'export function publishBundle', + 'export function buildTrackerArgs', + publication_block, + 'server-utils publication block', +) + +history_block = r'''export function readCsvAvgProximity(csvPath) { + if (!fs.existsSync(csvPath)) return null; + const lines = fs.readFileSync(csvPath, 'utf8').trim().split(/\r?\n/); + if (lines.length < 2) return null; + const headers = lines[0].split(','); + const proximityIndex = headers.indexOf('proximity_distance'); + const occlusionIndex = headers.indexOf('occlusion_flag'); + const trackingValidIndex = headers.indexOf('tracking_valid'); + const detectionCountIndex = headers.indexOf('detection_count'); + const fly1AreaIndex = headers.indexOf('fly1_area'); + const fly2AreaIndex = headers.indexOf('fly2_area'); + if (proximityIndex < 0) return null; + + let total = 0; + let count = 0; + lines.slice(1).forEach((line) => { + const columns = line.split(','); + const rawProximity = columns[proximityIndex]?.trim(); + if (!rawProximity) return; + const proximity = Number(rawProximity); + const occluded = occlusionIndex >= 0 && Number(columns[occlusionIndex]) !== 0; + let trackingValid = true; + if (trackingValidIndex >= 0) { + trackingValid = Number(columns[trackingValidIndex]) === 1; + } else if (detectionCountIndex >= 0) { + trackingValid = Number(columns[detectionCountIndex]) >= 2; + } else if (fly1AreaIndex >= 0 && fly2AreaIndex >= 0) { + trackingValid = Number(columns[fly1AreaIndex]) > 0 && Number(columns[fly2AreaIndex]) > 0; + } + if (Number.isFinite(proximity) && trackingValid && !occluded) { + total += proximity; + count += 1; + } + }); + return count ? Math.round((total / count) * 100) / 100 : null; +} + +export function countCourtshipBouts(eventsPath) { + const data = readJson(eventsPath, { events: [] }); + return Array.isArray(data.events) + ? data.events.filter((event) => event.type === 'courtship_bout').length + : 0; +} + +const RUN_ID_PATTERN = /^run-[A-Za-z0-9][A-Za-z0-9-]{2,127}$/; + +function requireRunId(runId) { + if (!RUN_ID_PATTERN.test(String(runId))) throw new Error(`Invalid run ID: ${runId}`); + return String(runId); +} + +export function createRunId() { + return `run-${Date.now()}-${randomUUID().slice(0, 8)}`; +} + +export function scopeEventsForRun(eventsPath, runId, destinationPath = eventsPath) { + const safeRunId = requireRunId(runId); + const prefix = `${safeRunId}:`; + const data = readJson(eventsPath, { version: 1, events: [] }); + data.run_id = safeRunId; + data.events = Array.isArray(data.events) ? data.events.map((event) => { + const currentId = String(event.id || ''); + const originalId = event.original_id + || (currentId.startsWith(prefix) ? currentId.slice(prefix.length) : currentId); + if (!originalId) throw new Error('Event ID missing while scoping run events'); + return { + ...event, + original_id: originalId, + id: `${safeRunId}:${originalId}`, + }; + }) : []; + writeJson(destinationPath, data); + return data; +} + From 0d362fa9767b2428c735dca3982c4a8838778b22 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 11:30:30 +0530 Subject: [PATCH 47/75] chore: stage transparent final hardening patch 2/5 --- .github/scripts/final-hardening.part01 | 280 +++++++++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 .github/scripts/final-hardening.part01 diff --git a/.github/scripts/final-hardening.part01 b/.github/scripts/final-hardening.part01 new file mode 100644 index 0000000..b11f8c8 --- /dev/null +++ b/.github/scripts/final-hardening.part01 @@ -0,0 +1,280 @@ +export function scopeEventsForHistory(eventsPath, runId, destinationPath) { + return scopeEventsForRun(eventsPath, runId, destinationPath); +} + +export function prepareRunHistoryBundle({ + runId, + historyDir, + historyMetaPath, + historyIndexOutput, + filename, + durationSec, + fps, + totalFrames, + csvSrc, + eventsSrc, + verificationSrc, +}) { + const safeRunId = requireRunId(runId); + for (const source of [csvSrc, eventsSrc, verificationSrc]) { + if (!fs.existsSync(source)) throw new Error(`Missing history source: ${source}`); + } + const history = readJson(historyMetaPath, { version: 1, runs: [] }); + if (!Array.isArray(history.runs)) history.runs = []; + if (history.runs.some((run) => run.runId === safeRunId)) { + throw new Error(`History run already exists: ${safeRunId}`); + } + const meta = { + runId: safeRunId, + timestamp: new Date().toISOString(), + filename: filename || 'unknown', + durationSec: Math.round((durationSec || 0) * 10) / 10, + fps: fps || null, + totalFrames: totalFrames || null, + avgProximity: readCsvAvgProximity(csvSrc), + detectedBouts: countCourtshipBouts(eventsSrc), + }; + history.runs.unshift(meta); + writeJson(historyIndexOutput, history); + + const runDir = path.join(historyDir, safeRunId); + return { + meta, + entries: [ + { source: csvSrc, destination: path.join(runDir, 'data.csv') }, + { source: eventsSrc, destination: path.join(runDir, 'events.json') }, + { source: verificationSrc, destination: path.join(runDir, 'verification.json') }, + { source: historyIndexOutput, destination: historyMetaPath }, + ], + }; +} + +export function verificationPathForRun(runId, historyDir, { mustExist = true } = {}) { + const safeRunId = requireRunId(runId); + const root = path.resolve(historyDir); + const filePath = path.resolve(root, safeRunId, 'verification.json'); + if (!filePath.startsWith(`${root}${path.sep}`)) throw new Error('Verification path escaped history root'); + if (mustExist && !fs.existsSync(filePath)) throw new Error('Run verification file not found'); + return filePath; +} + +export function resolveVerificationPath(eventId, historyDir) { + const separator = String(eventId).indexOf(':'); + if (separator <= 0 || separator === String(eventId).length - 1) { + throw new Error('Run-scoped event ID required'); + } + return verificationPathForRun(String(eventId).slice(0, separator), historyDir); +}''' + +utils = replace_between( + utils, + 'export function readCsvAvgProximity', + # replace through EOF; marker is the old final resolver + 'export function resolveVerificationPath', + history_block, + 'server-utils history block prefix', +) +# replace_between preserved the old resolver because it starts at the end marker. +old_resolver_start = utils.find('export function resolveVerificationPath', utils.find(history_block[:30]) + len(history_block)) +if old_resolver_start >= 0: + utils = utils[:old_resolver_start].rstrip() + '\n' + +utils_path.write_text(utils, encoding='utf-8') + +# --------------------------------------------------------------------------- +# Server: durable run ID, atomic current+history publication, canonical reviews. +# --------------------------------------------------------------------------- +server_path = root / 'source app folder/dashboard/server.js' +server = server_path.read_text(encoding='utf-8') +server = replace_once(server, ' buildTrackerArgs,\n', ' buildTrackerArgs,\n createRunId,\n', 'server createRunId import') +server = replace_once( + server, + ' parseTrackerSync,\n publishBundle,\n', + ' parseTrackerSync,\n prepareRunHistoryBundle,\n publishBundle,\n', + 'server prepare history import', +) +server = replace_once(server, ' resolveVerificationPath,\n', ' resolveVerificationPath,\n scopeEventsForRun,\n', 'server scope import') +server = replace_once(server, ' snapshotRunToHistory,\n', ' verificationPathForRun,\n', 'server verification import') +server = replace_once(server, ' let runSequence = 0;\n', '', 'server run sequence removal') +server = replace_once( + server, + " publish: options.services?.publish || ((entries, token) => publishBundle(entries, token)),\n snapshot: options.services?.snapshot || snapshotRunToHistory,\n", + " publish: options.services?.publish || ((entries, token) => publishBundle(\n entries, token, { manifestDir: publicDir },\n )),\n prepareHistory: options.services?.prepareHistory || prepareRunHistoryBundle,\n", + 'server services', +) +server = replace_once( + server, + ' function readVerification(filePath = verificationPath) {\n', + ' function readVerification(filePath) {\n', + 'server read verification signature', +) +server = replace_once( + server, + " function createRunContext(inputPath, filename, overrides) {\n const runId = ++runSequence;\n", + " function createRunContext(inputPath, filename, overrides) {\n const runId = createRunId();\n", + 'server durable run id', +) +server = replace_once( + server, + " verification: path.join(context.workDir, 'verification.json'),\n", + " verification: path.join(context.workDir, 'verification.json'),\n historyIndex: path.join(context.workDir, 'history.json'),\n", + 'server history output', +) +server = replace_once( + server, + " const fps = readEventsFps(outputs.events);\n const metadata = {\n ...integrity,\n expectedVideoFrames: syncInfo.expectedVideoFrames,\n fps,\n timestamp: new Date().toISOString(),\n };\n writeJson(outputs.metadata, metadata);\n writeJson(outputs.verification, { version: 1, reviews: [] });\n ensureCurrent();\n\n job.progress = 'Publishing validated results...';\n services.publish([\n { source: outputs.csv, destination: path.join(publicDir, 'data.csv') },\n { source: outputs.events, destination: path.join(publicDir, 'events.json') },\n { source: outputs.browserVideo, destination: path.join(publicDir, 'tracked.mp4') },\n { source: outputs.metadata, destination: path.join(publicDir, 'run_metadata.json') },\n { source: outputs.verification, destination: verificationPath },\n ], context.token);\n ensureCurrent();\n\n try {\n services.snapshot({\n historyDir,\n historyMetaPath,\n filename: context.filename,\n durationSec: (Date.now() - context.startedAt) / 1000,\n fps,\n totalFrames: integrity.inputFrames,\n csvSrc: outputs.csv,\n eventsSrc: outputs.events,\n });\n } catch (historyError) {\n console.error(`[Server] History snapshot failed: ${historyError.message}`);\n }\n", + " const fps = readEventsFps(outputs.events);\n scopeEventsForRun(outputs.events, context.runId, outputs.events);\n const metadata = {\n ...integrity,\n runId: context.runId,\n expectedVideoFrames: syncInfo.expectedVideoFrames,\n fps,\n timestamp: new Date().toISOString(),\n };\n writeJson(outputs.metadata, metadata);\n writeJson(outputs.verification, { version: 1, run_id: context.runId, reviews: [] });\n const historyBundle = services.prepareHistory({\n runId: context.runId,\n historyDir,\n historyMetaPath,\n historyIndexOutput: outputs.historyIndex,\n filename: context.filename,\n durationSec: (Date.now() - context.startedAt) / 1000,\n fps,\n totalFrames: integrity.inputFrames,\n csvSrc: outputs.csv,\n eventsSrc: outputs.events,\n verificationSrc: outputs.verification,\n });\n ensureCurrent();\n\n job.progress = 'Publishing validated current and historical results...';\n services.publish([\n { source: outputs.csv, destination: path.join(publicDir, 'data.csv') },\n { source: outputs.events, destination: path.join(publicDir, 'events.json') },\n { source: outputs.browserVideo, destination: path.join(publicDir, 'tracked.mp4') },\n { source: outputs.metadata, destination: path.join(publicDir, 'run_metadata.json') },\n ...historyBundle.entries,\n ], context.token);\n ensureCurrent();\n", + 'server atomic history publication', +) + +old_routes = r''' app.get('/api/verification', (req, res) => { + if (!req.query.runId) return res.json(readVerification()); + const history = readJson(historyMetaPath, { runs: [] }); + const known = history.runs?.some((run) => run.runId === req.query.runId); + if (!known) return res.status(404).json({ error: 'Run not found' }); + return res.json(readVerification(path.join(historyDir, req.query.runId, 'verification.json'))); + }); + + app.post('/api/verification', (req, res) => { + const { eventId, verdict } = req.body || {}; + if (!eventId || !['confirmed', 'rejected'].includes(verdict)) { + return res.status(400).json({ error: 'Body must include eventId and verdict.' }); + } + try { + const filePath = resolveVerificationPath( + eventId, + verificationPath, + historyDir, + historyMetaPath, + ); + const data = readVerification(filePath); + const review = { event_id: eventId, verdict, reviewed_at: new Date().toISOString() }; + const index = data.reviews.findIndex((item) => item.event_id === eventId); + if (index >= 0) data.reviews[index] = review; + else data.reviews.push(review); + writeJson(filePath, data); + return res.json({ success: true, review }); + } catch (error) { + return res.status(400).json({ error: error.message }); + } + }); + + app.post('/api/verification/reset', (_req, res) => { + writeJson(verificationPath, { version: 1, reviews: [] }); + res.json({ success: true }); + });''' + +new_routes = r''' function currentRunId() { + return readJson(path.join(publicDir, 'run_metadata.json'), null)?.runId || null; + } + + app.get('/api/verification', (req, res) => { + const runId = req.query.runId || currentRunId(); + if (!runId) return res.status(404).json({ error: 'No current run found' }); + try { + return res.json(readVerification(verificationPathForRun(runId, historyDir))); + } catch (error) { + return res.status(404).json({ error: error.message }); + } + }); + + app.post('/api/verification', (req, res) => { + const { eventId, verdict } = req.body || {}; + if (!eventId || !['confirmed', 'rejected'].includes(verdict)) { + return res.status(400).json({ error: 'Body must include eventId and verdict.' }); + } + try { + const filePath = resolveVerificationPath(eventId, historyDir); + const data = readVerification(filePath); + const review = { event_id: eventId, verdict, reviewed_at: new Date().toISOString() }; + const index = data.reviews.findIndex((item) => item.event_id === eventId); + if (index >= 0) data.reviews[index] = review; + else data.reviews.push(review); + writeJson(filePath, data); + return res.json({ success: true, review }); + } catch (error) { + return res.status(400).json({ error: error.message }); + } + }); + + app.post('/api/verification/reset', (req, res) => { + const runId = req.body?.runId || currentRunId(); + if (!runId) return res.status(404).json({ error: 'No current run found' }); + try { + writeJson(verificationPathForRun(runId, historyDir), { version: 1, run_id: runId, reviews: [] }); + return res.json({ success: true }); + } catch (error) { + return res.status(404).json({ error: error.message }); + } + });''' +server = replace_once(server, old_routes, new_routes, 'server canonical verification routes') +server_path.write_text(server, encoding='utf-8') + +# --------------------------------------------------------------------------- +# Tracker: missing observations are explicit and OpenCV count is diagnostic. +# --------------------------------------------------------------------------- +tracker_path = root / 'source app folder/tracker/tracker.py' +tracker = tracker_path.read_text(encoding='utf-8') +tracker = replace_once( + tracker, + ' proximities = [value_or(row, "proximity_distance", 0.0) for row in segment]\n', + ' proximities = [\n value_or(row, "proximity_distance", math.nan) for row in segment\n if math.isfinite(value_or(row, "proximity_distance", math.nan))\n ]\n', + 'tracker finite event proximities', +) +tracker = replace_once( + tracker, + ' fly2_present = value_or(row, "fly2_area", 0.0) > 0.0\n return (\n math.isfinite(proximity)\n', + ' fly2_present = value_or(row, "fly2_area", 0.0) > 0.0\n tracking_valid = value_or(row, "tracking_valid", 1.0) != 0.0\n return (\n tracking_valid\n and math.isfinite(proximity)\n', + 'tracker courtship tracking validity', +) +tracker = replace_once( + tracker, + 'def is_low_confidence_frame(row: dict[str, Any]) -> bool:\n return (\n value_or(row, "occlusion_flag", 0.0) != 0.0\n', + 'def is_low_confidence_frame(row: dict[str, Any]) -> bool:\n return (\n value_or(row, "tracking_valid", 1.0) == 0.0\n or value_or(row, "occlusion_flag", 0.0) != 0.0\n', + 'tracker low confidence tracking validity', +) +tracker = replace_once( + tracker, + ''' expected_sync_ok = ( + expected_frame_count <= 0 or frames_processed == expected_frame_count + ) + return internal_sync_ok and expected_sync_ok +''', + ''' # OpenCV CAP_PROP_FRAME_COUNT is diagnostic only. The server performs the + # authoritative full decode with ffmpeg and compares it to tracker/CSV/video output. + _ = expected_frame_count + return internal_sync_ok +''', + 'tracker diagnostic OpenCV frame count', +) +tracker = replace_once( + tracker, + ' expected_frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))\n', + ' expected_frame_count = max(0, int(cap.get(cv2.CAP_PROP_FRAME_COUNT)))\n', + 'tracker nonnegative expected count', +) +tracker = replace_once( + tracker, + ''' proximity = 0.0 + occlusion_flag = 0 + identity_confidence = 0.0 + fly1_area = fly2_area = 0.0 +''', + ''' proximity = math.nan + occlusion_flag = 0 + identity_confidence = 0.0 + fly1_area = fly2_area = 0.0 + detection_count = len(centroids) + tracking_valid = 0 +''', + 'tracker invalid observation defaults', +) +tracker = replace_once( + tracker, + ' proximity = float(math.dist(f1_coords, f2_coords))\n prev_f1, prev_f2 = f1_coords, f2_coords\n', + ' proximity = float(math.dist(f1_coords, f2_coords))\n tracking_valid = 1\n prev_f1, prev_f2 = f1_coords, f2_coords\n', + 'tracker valid observation', +) +tracker = replace_once( + tracker, + ''' elif is_initialized: From f381fa8f8c19a63fba5851082de84de6379f8bc9 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 11:32:03 +0530 Subject: [PATCH 48/75] chore: stage transparent final hardening patch 3/5 --- .github/scripts/final-hardening.part02 | 280 +++++++++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 .github/scripts/final-hardening.part02 diff --git a/.github/scripts/final-hardening.part02 b/.github/scripts/final-hardening.part02 new file mode 100644 index 0000000..618ba1e --- /dev/null +++ b/.github/scripts/final-hardening.part02 @@ -0,0 +1,280 @@ + f1_coords, f2_coords = prev_f1, prev_f2 + proximity = float(math.dist(f1_coords, f2_coords)) + identity_confidence = 0.0 +''', + ''' elif is_initialized: + # Coordinates are retained only for display continuity. Proximity is + # deliberately missing because no two-fly observation occurred. + f1_coords, f2_coords = prev_f1, prev_f2 + identity_confidence = 0.0 +''', + 'tracker dropout proximity', +) +tracker = replace_once( + tracker, + ' "proximity_distance": proximity,\n "occlusion_flag": occlusion_flag,\n', + ' "proximity_distance": proximity,\n "tracking_valid": tracking_valid,\n "detection_count": detection_count,\n "occlusion_flag": occlusion_flag,\n', + 'tracker validity columns', +) +tracker = replace_once( + tracker, + ' "Error: frame integrity mismatch between decoded input, tracker loop, and CSV",\n', + ' "Error: frame integrity mismatch between tracker loop and CSV",\n', + 'tracker sync error text', +) +tracker_path.write_text(tracker, encoding='utf-8') + +# --------------------------------------------------------------------------- +# Frontend metric helpers and usage. +# --------------------------------------------------------------------------- +metrics_path = root / 'source app folder/dashboard/src/metrics.js' +metrics_path.write_text(r'''export function proximityValue(row) { + if (!row) return null; + const rawProximity = row.proximity_distance; + if (rawProximity === null || rawProximity === undefined || rawProximity === '') return null; + const proximity = Number(rawProximity); + if (!Number.isFinite(proximity)) return null; + if (Number(row.occlusion_flag ?? 0) !== 0) return null; + + if (row.tracking_valid !== null && row.tracking_valid !== undefined && row.tracking_valid !== '') { + if (Number(row.tracking_valid) !== 1) return null; + } else if (row.detection_count !== null && row.detection_count !== undefined && row.detection_count !== '') { + if (Number(row.detection_count) < 2) return null; + } else if ( + row.fly1_area !== null && row.fly1_area !== undefined + && row.fly2_area !== null && row.fly2_area !== undefined + ) { + if (!(Number(row.fly1_area) > 0 && Number(row.fly2_area) > 0)) return null; + } + + return proximity; +} + +export function computeAverageProximity(rows) { + const values = rows.map(proximityValue).filter((value) => value !== null); + if (!values.length) return 0; + return Math.round(values.reduce((sum, value) => sum + value, 0) / values.length); +} + +export function prismDistance(row) { + return proximityValue(row) ?? ''; +} +''', encoding='utf-8') + +app_path = root / 'source app folder/dashboard/src/App.jsx' +app = app_path.read_text(encoding='utf-8') +app = replace_once( + app, + "import Papa from 'papaparse';\n", + "import Papa from 'papaparse';\nimport { prismDistance, proximityValue } from './metrics.js';\n", + 'App metric import', +) +app = replace_once( + app, + ' const dist = Number(r.proximity_distance ?? 0);\n', + ' const dist = prismDistance(r);\n', + 'App Prism missing distance', +) +app = replace_once( + app, + ''' // Average proximity only over frames where flies are separate (exclude merged/occluded where proximity=0 by design) + if (row.proximity_distance != null && !row.occlusion_flag) { + totalProx += row.proximity_distance; + proxCount++; + } +''', + ''' // Only measured two-fly observations contribute. Dropouts retain + // display coordinates but carry no scientifically valid proximity. + const observedProximity = proximityValue(row); + if (observedProximity !== null) { + totalProx += observedProximity; + proxCount++; + } +''', + 'App valid proximity average', +) +app_path.write_text(app, encoding='utf-8') + +# --------------------------------------------------------------------------- +# Regression tests: crash lifecycle, durable verdicts, and metric validity. +# --------------------------------------------------------------------------- +utils_test_path = root / 'source app folder/dashboard/tests/server-utils.test.js' +utils_test = utils_test_path.read_text(encoding='utf-8') +utils_test = replace_once(utils_test, "import { EventEmitter } from 'node:events';\n", "import { EventEmitter } from 'node:events';\n", 'utils test stable import') +utils_test = replace_once( + utils_test, + ''' buildTrackerArgs, + parseTrackerSync, + publishBundle, + recoverPublishArtifacts, + resolveVerificationPath, + scopeEventsForHistory, + snapshotRunToHistory, + terminateChild, + validateFrameIntegrity, +''', + ''' buildTrackerArgs, + parseTrackerSync, + prepareRunHistoryBundle, + publishBundle, + readCsvAvgProximity, + recoverPublishArtifacts, + resolveVerificationPath, + scopeEventsForRun, + SimulatedProcessCrash, + terminateChild, + validateFrameIntegrity, + verificationPathForRun, +''', + 'utils test imports', +) +utils_test = replace_once( + utils_test, + ''' assert.throws(() => validateFrameIntegrity({ + ...evidence, + syncInfo: { ...evidence.syncInfo, expectedVideoFrames: 11 }, + }), /mismatch/); +''', + ''' const diagnosticMismatch = validateFrameIntegrity({ + ...evidence, + syncInfo: { ...evidence.syncInfo, expectedVideoFrames: 11 }, + }); + assert.equal(diagnosticMismatch.syncOk, true); + assert.equal(diagnosticMismatch.expectedMetadataMatches, false); +''', + 'utils test diagnostic frame count', +) + +old_recovery_test = r'''test('recovers orphaned publication artifacts after a crash', () => { + const dir = tempDir('flyt-recover-'); + fs.writeFileSync(path.join(dir, 'data.csv.token.bak'), 'old-data'); + fs.writeFileSync(path.join(dir, 'events.json.token.new'), 'new-events'); + recoverPublishArtifacts(dir); + assert.equal(fs.readFileSync(path.join(dir, 'data.csv'), 'utf8'), 'old-data'); + assert.equal(fs.existsSync(path.join(dir, 'events.json.token.new')), false); +});''' +new_recovery_tests = r'''test('crash recovery rolls back the entire partially published bundle', () => { + const dir = tempDir('flyt-crash-rollback-'); + const sourceA = path.join(dir, 'source-a'); + const sourceB = path.join(dir, 'source-b'); + const destinationA = path.join(dir, 'a'); + const destinationB = path.join(dir, 'b'); + fs.writeFileSync(sourceA, 'new-a'); + fs.writeFileSync(sourceB, 'new-b'); + fs.writeFileSync(destinationA, 'old-a'); + fs.writeFileSync(destinationB, 'old-b'); + + assert.throws(() => publishBundle([ + { source: sourceA, destination: destinationA }, + { source: sourceB, destination: destinationB }, + ], 'crash-token', { + manifestDir: dir, + faultInjector(stage, index) { + if (stage === 'published' && index === 0) throw new SimulatedProcessCrash(); + }, + }), SimulatedProcessCrash); + + assert.equal(fs.readFileSync(destinationA, 'utf8'), 'new-a'); + assert.equal(fs.existsSync(destinationB), false); + recoverPublishArtifacts(dir); + assert.equal(fs.readFileSync(destinationA, 'utf8'), 'old-a'); + assert.equal(fs.readFileSync(destinationB, 'utf8'), 'old-b'); + assert.equal(fs.readdirSync(dir).some((name) => /flyt-publish|\.(bak|new)$/.test(name)), false); +}); + +test('crash recovery finalizes an explicitly committed all-new bundle', () => { + const dir = tempDir('flyt-crash-commit-'); + const source = path.join(dir, 'source'); + const destination = path.join(dir, 'destination'); + fs.writeFileSync(source, 'new'); + fs.writeFileSync(destination, 'old'); + + assert.throws(() => publishBundle([ + { source, destination }, + ], 'commit-token', { + manifestDir: dir, + faultInjector(stage) { + if (stage === 'committed') throw new SimulatedProcessCrash(); + }, + }), SimulatedProcessCrash); + + recoverPublishArtifacts(dir); + assert.equal(fs.readFileSync(destination, 'utf8'), 'new'); + assert.equal(fs.readdirSync(dir).some((name) => /flyt-publish|\.(bak|new)$/.test(name)), false); +}); + +test('legacy orphan backups fail closed instead of reconstructing a mixed bundle', () => { + const dir = tempDir('flyt-orphan-backup-'); + const backup = path.join(dir, 'data.csv.token.bak'); + fs.writeFileSync(backup, 'old-data'); + assert.throws(() => recoverPublishArtifacts(dir), /without a transaction manifest/); + assert.equal(fs.readFileSync(backup, 'utf8'), 'old-data'); +});''' +utils_test = replace_once(utils_test, old_recovery_test, new_recovery_tests, 'utils crash recovery tests') + +history_start = utils_test.index("test('history snapshots preserve dashboard summary fields and scoped reviews'") +history_end = utils_test.index("test('termination escalates to SIGKILL", history_start) +new_history_tests = r'''test('history bundle uses scoped events and excludes invalid proximity observations', () => { + const dir = tempDir('flyt-history-'); + const historyDir = path.join(dir, 'history'); + const historyMetaPath = path.join(dir, 'history.json'); + const historyIndexOutput = path.join(dir, 'next-history.json'); + const csv = path.join(dir, 'data.csv'); + const events = path.join(dir, 'events.json'); + const verification = path.join(dir, 'verification.json'); + const runId = 'run-1234-abcd'; + fs.writeFileSync(csv, [ + 'frame,proximity_distance,occlusion_flag,tracking_valid,detection_count', + '0,10,0,1,2', + '1,10,0,0,0', + '2,,1,0,1', + '3,30,0,1,2', + ].join('\n')); + fs.writeFileSync(events, JSON.stringify({ + events: [{ id: 'evt-001', type: 'courtship_bout' }], + })); + fs.writeFileSync(verification, JSON.stringify({ version: 1, run_id: runId, reviews: [] })); + scopeEventsForRun(events, runId, events); + + const bundle = prepareRunHistoryBundle({ + runId, + historyDir, + historyMetaPath, + historyIndexOutput, + filename: 'flies.mp4', + durationSec: 2.3, + fps: 30, + totalFrames: 4, + csvSrc: csv, + eventsSrc: events, + verificationSrc: verification, + }); + assert.equal(bundle.meta.avgProximity, 20); + assert.equal(bundle.meta.detectedBouts, 1); + assert.equal(readCsvAvgProximity(csv), 20); + publishBundle(bundle.entries, 'history-token', { manifestDir: dir }); + + const scopedEvents = JSON.parse(fs.readFileSync(path.join(historyDir, runId, 'events.json'))); + assert.equal(scopedEvents.events[0].id, `${runId}:evt-001`); + assert.equal( + resolveVerificationPath(`${runId}:evt-001`, historyDir), + path.join(historyDir, runId, 'verification.json'), + ); + assert.equal(verificationPathForRun(runId, historyDir), path.join(historyDir, runId, 'verification.json')); + assert.throws(() => resolveVerificationPath('evt-001', historyDir), /Run-scoped event ID required/); +}); + +''' +utils_test = utils_test[:history_start] + new_history_tests + utils_test[history_end:] +utils_test_path.write_text(utils_test, encoding='utf-8') + +integration_path = root / 'source app folder/dashboard/tests/server-integration.test.js' +integration = integration_path.read_text(encoding='utf-8') +integration = replace_once( + integration, + " 'frame,proximity_distance,occlusion_flag',\n ...Array.from({ length: frames }, (_, index) => `${index},20,0`),\n", + " 'frame,proximity_distance,occlusion_flag,tracking_valid,detection_count,fly1_area,fly2_area',\n ...Array.from({ length: frames }, (_, index) => `${index},20,0,1,2,100,100`),\n", + 'integration valid CSV', +) +integration = replace_once(integration, ' snapshot: () => null,\n', '', 'integration snapshot service removal') +integration = replace_once( From 4c30f172aec6baa345a21db684afb1b02afd44f8 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 11:32:41 +0530 Subject: [PATCH 49/75] chore: stage transparent final hardening patch 4/5 --- .github/scripts/final-hardening.part03 | 280 +++++++++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 .github/scripts/final-hardening.part03 diff --git a/.github/scripts/final-hardening.part03 b/.github/scripts/final-hardening.part03 new file mode 100644 index 0000000..9be7ceb --- /dev/null +++ b/.github/scripts/final-hardening.part03 @@ -0,0 +1,280 @@ + integration, + ''' assert.equal(metadata.finalVideoFrames, 4); + assert.equal(metadata.syncOk, true); +}); +''', + ''' assert.equal(metadata.finalVideoFrames, 4); + assert.equal(metadata.syncOk, true); + assert.match(metadata.runId, /^run-/); + const currentEvents = JSON.parse(fs.readFileSync(path.join(harness.paths.publicDir, 'events.json'))); + assert.equal(currentEvents.events[0].id, `${metadata.runId}:evt-001`); +}); +''', + 'integration scoped current events', +) + +lifecycle_test = r''' + +test('a verdict made on the current run survives subsequent runs and history reload', async (t) => { + const harness = await createHarness(standardServices()); + t.after(() => harness.close()); + + assert.equal((await upload(harness.baseUrl, 'first.mp4')).status, 200); + const firstStatus = await waitForStatus(harness.baseUrl, 'done'); + const firstRunId = firstStatus.runId; + const eventsResponse = await fetch(`${harness.baseUrl}/api/events`); + const firstEvents = await eventsResponse.json(); + const eventId = firstEvents.events[0].id; + assert.equal(eventId, `${firstRunId}:evt-001`); + + const verdictResponse = await fetch(`${harness.baseUrl}/api/verification`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ eventId, verdict: 'confirmed' }), + }); + assert.equal(verdictResponse.status, 200); + + const unscopedResponse = await fetch(`${harness.baseUrl}/api/verification`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ eventId: 'evt-001', verdict: 'confirmed' }), + }); + assert.equal(unscopedResponse.status, 400); + + assert.equal((await upload(harness.baseUrl, 'second.mp4')).status, 200); + const secondStatus = await waitForStatus(harness.baseUrl, 'done'); + assert.notEqual(secondStatus.runId, firstRunId); + + const historicResponse = await fetch( + `${harness.baseUrl}/api/verification?runId=${encodeURIComponent(firstRunId)}`, + ); + assert.equal(historicResponse.status, 200); + const historicVerification = await historicResponse.json(); + assert.deepEqual(historicVerification.reviews.map((review) => ({ + event_id: review.event_id, + verdict: review.verdict, + })), [{ event_id: eventId, verdict: 'confirmed' }]); + + const canonical = JSON.parse(fs.readFileSync( + path.join(harness.paths.historyDir, firstRunId, 'verification.json'), + )); + assert.equal(canonical.reviews[0].verdict, 'confirmed'); +}); +''' +integration += lifecycle_test +integration_path.write_text(integration, encoding='utf-8') + +metrics_test_path = root / 'source app folder/dashboard/tests/metrics.test.js' +metrics_test_path.write_text(r'''import assert from 'node:assert/strict'; +import test from 'node:test'; +import { computeAverageProximity, prismDistance, proximityValue } from '../src/metrics.js'; + +test('carried coordinates from missing detections do not bias proximity metrics', () => { + const rows = [ + { proximity_distance: 10, tracking_valid: 1, detection_count: 2, occlusion_flag: 0 }, + { proximity_distance: 10, tracking_valid: 0, detection_count: 0, occlusion_flag: 0 }, + { proximity_distance: null, tracking_valid: 0, detection_count: 1, occlusion_flag: 1 }, + { proximity_distance: 30, tracking_valid: 1, detection_count: 2, occlusion_flag: 0 }, + ]; + assert.equal(computeAverageProximity(rows), 20); + assert.equal(proximityValue(rows[1]), null); + assert.equal(prismDistance(rows[1]), ''); +}); + +test('legacy rows remain valid only when they contain a finite non-occluded observation', () => { + assert.equal(proximityValue({ proximity_distance: 12, occlusion_flag: 0 }), 12); + assert.equal(proximityValue({ proximity_distance: '', occlusion_flag: 0 }), null); + assert.equal(proximityValue({ proximity_distance: 0, occlusion_flag: 1 }), null); +}); +''', encoding='utf-8') + +frontend_contract_path = root / 'source app folder/dashboard/tests/frontend-contract.test.js' +frontend_contract = frontend_contract_path.read_text(encoding='utf-8') +frontend_contract += r''' + +test('dashboard and Prism export use validity-aware proximity helpers', () => { + const source = appSource(); + assert.match(source, /proximityValue\(row\)/); + assert.match(source, /prismDistance\(r\)/); + assert.doesNotMatch(source, /Number\(r\.proximity_distance \?\? 0\)/); +}); +''' +frontend_contract_path.write_text(frontend_contract, encoding='utf-8') + +tracker_test_path = root / 'source app folder/tracker/tests/test_tracker.py' +tracker_test = tracker_test_path.read_text(encoding='utf-8') +tracker_test = replace_once( + tracker_test, + 'def row(frame, proximity, confidence=1.0, occlusion=0, area=100.0):\n', + 'def row(frame, proximity, confidence=1.0, occlusion=0, area=100.0, tracking_valid=None):\n', + 'tracker test row signature', +) +tracker_test = replace_once( + tracker_test, + ''' return { + "frame": frame, +''', + ''' if tracking_valid is None: + tracking_valid = int(occlusion == 0 and area > 0) + return { + "frame": frame, +''', + 'tracker test row validity', +) +tracker_test = replace_once( + tracker_test, + ' "fly2_area": area,\n', + ' "fly2_area": area,\n "tracking_valid": tracking_valid,\n', + 'tracker test validity field', +) +tracker_test = replace_once( + tracker_test, + ''' def test_frame_sync_requires_expected_input_count_when_available(self): + self.assertTrue(tracker.frame_sync_ok(10, 10, 9, 10)) + self.assertFalse(tracker.frame_sync_ok(8, 8, 7, 10)) + self.assertTrue(tracker.frame_sync_ok(8, 8, 7, 0)) + self.assertFalse(tracker.frame_sync_ok(8, 7, 6, 8)) +''', + ''' def test_frame_sync_treats_opencv_count_as_diagnostic_only(self): + self.assertTrue(tracker.frame_sync_ok(10, 10, 9, 10)) + self.assertTrue(tracker.frame_sync_ok(8, 8, 7, 10)) + self.assertTrue(tracker.frame_sync_ok(8, 8, 7, 0)) + self.assertFalse(tracker.frame_sync_ok(8, 7, 6, 8)) + + def test_invalid_tracking_observation_cannot_be_courtship(self): + invalid = row(0, proximity=10, confidence=1.0, tracking_valid=0) + self.assertFalse(tracker.is_courtship_frame(invalid, 60)) + self.assertTrue(tracker.is_low_confidence_frame(invalid)) + + def test_event_mean_proximity_ignores_missing_values(self): + segment = [ + row(0, proximity=20, confidence=0.8), + row(1, proximity=float("nan"), confidence=0.0, tracking_valid=0), + ] + event = tracker.build_event_record( + "evt-001", "low_confidence_segment", 0, 1, 30, segment, "test" + ) + self.assertEqual(event["mean_proximity_px"], 20.0) +''', + 'tracker test frame metadata and invalid metrics', +) +tracker_test_path.write_text(tracker_test, encoding='utf-8') + +# --------------------------------------------------------------------------- +# Frame validation: OpenCV estimate is diagnostic, ffmpeg input is authority. +# --------------------------------------------------------------------------- +utils = utils_path.read_text(encoding='utf-8') +utils = replace_once( + utils, + ''' if (syncInfo.expectedVideoFrames > 0) { + requirePositiveInteger('trackerExpectedFrames', syncInfo.expectedVideoFrames); + counts.trackerExpectedFrames = syncInfo.expectedVideoFrames; + } + + const unique = new Set(Object.values(counts)); +''', + ''' if (!Number.isInteger(syncInfo.expectedVideoFrames) || syncInfo.expectedVideoFrames < 0) { + throw new Error( + `Frame integrity evidence missing or invalid: trackerExpectedFrames=${syncInfo.expectedVideoFrames}`, + ); + } + + const unique = new Set(Object.values(counts)); +''', + 'server diagnostic OpenCV count validation', +) +utils = replace_once( + utils, + ' return { ...counts, syncOk: true };\n', + ''' return { + ...counts, + trackerExpectedFrames: syncInfo.expectedVideoFrames, + expectedMetadataMatches: ( + syncInfo.expectedVideoFrames === 0 || syncInfo.expectedVideoFrames === inputFrames + ), + syncOk: true, + }; +''', + 'server frame validation result', +) +utils_path.write_text(utils, encoding='utf-8') + +# --------------------------------------------------------------------------- +# Permanent Linux + Windows validation. +# --------------------------------------------------------------------------- +ci_path = root / '.github/workflows/critical-fixes-ci.yml' +ci_path.write_text(r'''name: Critical fixes CI + +on: + push: + branches: + - fix/critical-job-races + pull_request: + paths: + - 'source app folder/dashboard/**' + - 'source app folder/tracker/**' + - '.github/workflows/critical-fixes-ci.yml' + +permissions: + contents: read + +jobs: + validate-linux: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: source app folder/dashboard/package-lock.json + + - name: Install dashboard dependencies + working-directory: source app folder/dashboard + run: npm ci + + - name: Lint backend and regression tests + working-directory: source app folder/dashboard + run: npm run lint:backend + + - name: Run utility, HTTP race, lifecycle, and metric tests + working-directory: source app folder/dashboard + run: npm run test:server + + - name: Build dashboard + working-directory: source app folder/dashboard + run: npm run build + + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: pip + cache-dependency-path: source app folder/tracker/requirements.txt + + - name: Install tracker dependencies + run: python -m pip install -r 'source app folder/tracker/requirements.txt' + + - name: Check Python syntax + run: python -m py_compile 'source app folder/tracker/tracker.py' + + - name: Run tracker scientific regression tests + run: python -m unittest discover -s 'source app folder/tracker/tests' -p 'test_*.py' -v + + validate-windows-node: + runs-on: windows-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: source app folder/dashboard/package-lock.json + + - name: Install dashboard dependencies + working-directory: source app folder/dashboard + run: npm ci + From 8696e8faca8e706f210db278e200b9ae9df5ee56 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 11:32:54 +0530 Subject: [PATCH 50/75] chore: stage transparent final hardening patch 5/5 --- .github/scripts/final-hardening.part04 | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/scripts/final-hardening.part04 diff --git a/.github/scripts/final-hardening.part04 b/.github/scripts/final-hardening.part04 new file mode 100644 index 0000000..b6f8425 --- /dev/null +++ b/.github/scripts/final-hardening.part04 @@ -0,0 +1,14 @@ + - name: Lint backend and regression tests + working-directory: source app folder/dashboard + run: npm run lint:backend + + - name: Run Node utility, HTTP, lifecycle, and metric tests + working-directory: source app folder/dashboard + run: npm run test:server + + - name: Build dashboard + working-directory: source app folder/dashboard + run: npm run build +''', encoding='utf-8') + +print('Applied final crash-recovery, run-scoped verification, and metric-integrity hardening.') From 9cdf506fbe3908aba9f535f68222df157364883b Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 11:33:17 +0530 Subject: [PATCH 51/75] chore: validate final hardening pass --- .github/workflows/apply-final-hardening.yml | 93 +++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 .github/workflows/apply-final-hardening.yml diff --git a/.github/workflows/apply-final-hardening.yml b/.github/workflows/apply-final-hardening.yml new file mode 100644 index 0000000..d573e0f --- /dev/null +++ b/.github/workflows/apply-final-hardening.yml @@ -0,0 +1,93 @@ +name: Apply final hardening + +on: + pull_request: + paths: + - '.github/scripts/final-hardening.part*' + - '.github/workflows/apply-final-hardening.yml' + +permissions: + contents: write + +concurrency: + group: flyt-final-hardening + cancel-in-progress: false + +jobs: + patch-and-validate: + if: github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + with: + ref: fix/critical-job-races + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: source app folder/dashboard/package-lock.json + + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: pip + cache-dependency-path: source app folder/tracker/requirements.txt + + - name: Reconstruct transparent patcher + run: | + cat .github/scripts/final-hardening.part* > /tmp/apply-final-hardening.py + python -m py_compile /tmp/apply-final-hardening.py + + - name: Apply exact final-hardening patch + run: python /tmp/apply-final-hardening.py + + - name: Install dashboard dependencies + working-directory: source app folder/dashboard + run: npm ci + + - name: Lint backend and tests + working-directory: source app folder/dashboard + run: npm run lint:backend + + - name: Run utility, HTTP lifecycle, and metric tests + working-directory: source app folder/dashboard + run: npm run test:server + + - name: Build dashboard + working-directory: source app folder/dashboard + run: npm run build + + - name: Install tracker dependencies + run: python -m pip install -r 'source app folder/tracker/requirements.txt' + + - name: Check tracker syntax + run: python -m py_compile 'source app folder/tracker/tracker.py' + + - name: Run tracker regression tests + run: python -m unittest discover -s 'source app folder/tracker/tests' -p 'test_*.py' -v + + - name: Commit validated final hardening + run: | + if git diff --quiet; then + echo 'Final hardening already applied; no commit needed.' + exit 0 + fi + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add \ + '.github/workflows/critical-fixes-ci.yml' \ + 'source app folder/dashboard/server-utils.js' \ + 'source app folder/dashboard/server.js' \ + 'source app folder/dashboard/src/App.jsx' \ + 'source app folder/dashboard/src/metrics.js' \ + 'source app folder/dashboard/tests/frontend-contract.test.js' \ + 'source app folder/dashboard/tests/metrics.test.js' \ + 'source app folder/dashboard/tests/server-integration.test.js' \ + 'source app folder/dashboard/tests/server-utils.test.js' \ + 'source app folder/tracker/tracker.py' \ + 'source app folder/tracker/tests/test_tracker.py' + git commit -m 'fix: make publication and reviews transactionally durable' + git push origin HEAD:fix/critical-job-races From 1656a4bf1159689c1c5d67584878b6f67ad85f3d Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 11:34:42 +0530 Subject: [PATCH 52/75] chore: capture final hardening patch diagnostics --- .github/workflows/apply-final-hardening.yml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/workflows/apply-final-hardening.yml b/.github/workflows/apply-final-hardening.yml index d573e0f..96edb0f 100644 --- a/.github/workflows/apply-final-hardening.yml +++ b/.github/workflows/apply-final-hardening.yml @@ -42,7 +42,20 @@ jobs: python -m py_compile /tmp/apply-final-hardening.py - name: Apply exact final-hardening patch - run: python /tmp/apply-final-hardening.py + run: | + set +e + python /tmp/apply-final-hardening.py > .github/final-hardening-diagnostics.txt 2>&1 + status=$? + cat .github/final-hardening-diagnostics.txt + if [ "$status" -ne 0 ]; then + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add .github/final-hardening-diagnostics.txt + git commit -m 'chore: capture final hardening diagnostics' + git push origin HEAD:fix/critical-job-races + exit "$status" + fi + rm .github/final-hardening-diagnostics.txt - name: Install dashboard dependencies working-directory: source app folder/dashboard @@ -88,6 +101,7 @@ jobs: 'source app folder/dashboard/tests/server-integration.test.js' \ 'source app folder/dashboard/tests/server-utils.test.js' \ 'source app folder/tracker/tracker.py' \ - 'source app folder/tracker/tests/test_tracker.py' + 'source app folder/tracker/tests/test_tracker.py' \ + '.github/final-hardening-diagnostics.txt' git commit -m 'fix: make publication and reviews transactionally durable' git push origin HEAD:fix/critical-job-races From 51026b1361a8ab1ed0dca66557f28c21728ea141 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 06:04:58 +0000 Subject: [PATCH 53/75] chore: capture final hardening diagnostics --- .github/final-hardening-diagnostics.txt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .github/final-hardening-diagnostics.txt diff --git a/.github/final-hardening-diagnostics.txt b/.github/final-hardening-diagnostics.txt new file mode 100644 index 0000000..35913b2 --- /dev/null +++ b/.github/final-hardening-diagnostics.txt @@ -0,0 +1,7 @@ +Traceback (most recent call last): + File "/tmp/apply-final-hardening.py", line 664, in + utils_test = replace_once( + ^^^^^^^^^^^^^ + File "/tmp/apply-final-hardening.py", line 7, in replace_once + raise RuntimeError(f"{label}: expected one occurrence, found {count}") +RuntimeError: utils test imports: expected one occurrence, found 0 From 6dc789904ec0b04bbf8ffe90848f0faa0b86ff1d Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 11:36:57 +0530 Subject: [PATCH 54/75] chore: correct final hardening import match --- .github/scripts/final-hardening.part02 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/scripts/final-hardening.part02 b/.github/scripts/final-hardening.part02 index 618ba1e..6cda3b3 100644 --- a/.github/scripts/final-hardening.part02 +++ b/.github/scripts/final-hardening.part02 @@ -108,6 +108,7 @@ utils_test = replace_once( publishBundle, recoverPublishArtifacts, resolveVerificationPath, + runCommand, scopeEventsForHistory, snapshotRunToHistory, terminateChild, @@ -120,6 +121,7 @@ utils_test = replace_once( readCsvAvgProximity, recoverPublishArtifacts, resolveVerificationPath, + runCommand, scopeEventsForRun, SimulatedProcessCrash, terminateChild, @@ -276,5 +278,3 @@ integration = replace_once( " 'frame,proximity_distance,occlusion_flag,tracking_valid,detection_count,fly1_area,fly2_area',\n ...Array.from({ length: frames }, (_, index) => `${index},20,0,1,2,100,100`),\n", 'integration valid CSV', ) -integration = replace_once(integration, ' snapshot: () => null,\n', '', 'integration snapshot service removal') -integration = replace_once( From cb905a60b26ef90a5c1dde9126ca52c17445d244 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 11:37:35 +0530 Subject: [PATCH 55/75] chore: realign final hardening tail segments --- .github/scripts/final-hardening.part03 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/scripts/final-hardening.part03 b/.github/scripts/final-hardening.part03 index 9be7ceb..5b2675a 100644 --- a/.github/scripts/final-hardening.part03 +++ b/.github/scripts/final-hardening.part03 @@ -1,3 +1,5 @@ +integration = replace_once(integration, ' snapshot: () => null,\n', '', 'integration snapshot service removal') +integration = replace_once( integration, ''' assert.equal(metadata.finalVideoFrames, 4); assert.equal(metadata.syncOk, true); @@ -276,5 +278,3 @@ jobs: - name: Install dashboard dependencies working-directory: source app folder/dashboard - run: npm ci - From 5afe3bfc8c4f85e380d67054b50e1e71f9f45f01 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 11:37:51 +0530 Subject: [PATCH 56/75] chore: complete realigned final hardening patch --- .github/scripts/final-hardening.part04 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/scripts/final-hardening.part04 b/.github/scripts/final-hardening.part04 index b6f8425..5c4e2a1 100644 --- a/.github/scripts/final-hardening.part04 +++ b/.github/scripts/final-hardening.part04 @@ -1,3 +1,5 @@ + run: npm ci + - name: Lint backend and regression tests working-directory: source app folder/dashboard run: npm run lint:backend From 0ae134b5cf36eb7c652755fed95d489e1ae9aa97 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 11:39:31 +0530 Subject: [PATCH 57/75] chore: capture final hardening lint diagnostics --- .github/workflows/apply-final-hardening.yml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/apply-final-hardening.yml b/.github/workflows/apply-final-hardening.yml index 96edb0f..f4375e9 100644 --- a/.github/workflows/apply-final-hardening.yml +++ b/.github/workflows/apply-final-hardening.yml @@ -63,7 +63,20 @@ jobs: - name: Lint backend and tests working-directory: source app folder/dashboard - run: npm run lint:backend + run: | + set +e + npm run lint:backend > ../../.github/final-hardening-diagnostics.txt 2>&1 + status=$? + cat ../../.github/final-hardening-diagnostics.txt + if [ "$status" -ne 0 ]; then + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add ../../.github/final-hardening-diagnostics.txt + git commit -m 'chore: capture final hardening lint diagnostics' + git push origin HEAD:fix/critical-job-races + exit "$status" + fi + rm ../../.github/final-hardening-diagnostics.txt - name: Run utility, HTTP lifecycle, and metric tests working-directory: source app folder/dashboard From 73fa32c19ea98c04406fe97082d4c3bfc17e81f9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 06:09:53 +0000 Subject: [PATCH 58/75] chore: capture final hardening lint diagnostics --- .github/final-hardening-diagnostics.txt | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/final-hardening-diagnostics.txt b/.github/final-hardening-diagnostics.txt index 35913b2..cff89f6 100644 --- a/.github/final-hardening-diagnostics.txt +++ b/.github/final-hardening-diagnostics.txt @@ -1,7 +1,11 @@ -Traceback (most recent call last): - File "/tmp/apply-final-hardening.py", line 664, in - utils_test = replace_once( - ^^^^^^^^^^^^^ - File "/tmp/apply-final-hardening.py", line 7, in replace_once - raise RuntimeError(f"{label}: expected one occurrence, found {count}") -RuntimeError: utils test imports: expected one occurrence, found 0 + +> dashboard@0.0.0 lint:backend +> eslint server.js server-utils.js tests + + +/home/runner/work/Flyt/Flyt/source app folder/dashboard/server-utils.js + 301:15 error 'AggregateError' is not defined no-undef + 375:17 error 'AggregateError' is not defined no-undef + +✖ 2 problems (2 errors, 0 warnings) + From 5690bc998e459944249e1a43a832dc901007b0a9 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 11:41:22 +0530 Subject: [PATCH 59/75] chore: use explicit Node AggregateError global --- .github/scripts/final-hardening.part00 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/scripts/final-hardening.part00 b/.github/scripts/final-hardening.part00 index 6858c04..0d1c9de 100644 --- a/.github/scripts/final-hardening.part00 +++ b/.github/scripts/final-hardening.part00 @@ -92,7 +92,7 @@ function rollbackPublishManifest(manifest) { } }); if (errors.length) { - throw new AggregateError(errors, 'Publication transaction rollback failed'); + throw new globalThis.AggregateError(errors, 'Publication transaction rollback failed'); } } @@ -166,7 +166,7 @@ export function publishBundle(entries, token, { faultInjector, manifestDir } = { rollbackPublishManifest(manifest); safeUnlink(manifestPath); } catch (rollbackError) { - throw new AggregateError([error, rollbackError], 'Publication failed and rollback was incomplete'); + throw new globalThis.AggregateError([error, rollbackError], 'Publication failed and rollback was incomplete'); } throw error; } From 161e3927d277959537b7e447c3fe8a1fc0eaf3b7 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 11:43:12 +0530 Subject: [PATCH 60/75] chore: make validated hardening push race-safe --- .github/workflows/apply-final-hardening.yml | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/apply-final-hardening.yml b/.github/workflows/apply-final-hardening.yml index f4375e9..2f2b7ac 100644 --- a/.github/workflows/apply-final-hardening.yml +++ b/.github/workflows/apply-final-hardening.yml @@ -97,13 +97,15 @@ jobs: - name: Commit validated final hardening run: | - if git diff --quiet; then + set -o pipefail + exec > >(tee /tmp/final-hardening-commit.txt) 2>&1 + if [ -z "$(git status --porcelain)" ]; then echo 'Final hardening already applied; no commit needed.' exit 0 fi git config user.name 'github-actions[bot]' git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add \ + git add -A -- \ '.github/workflows/critical-fixes-ci.yml' \ 'source app folder/dashboard/server-utils.js' \ 'source app folder/dashboard/server.js' \ @@ -116,5 +118,16 @@ jobs: 'source app folder/tracker/tracker.py' \ 'source app folder/tracker/tests/test_tracker.py' \ '.github/final-hardening-diagnostics.txt' + git status --short git commit -m 'fix: make publication and reviews transactionally durable' + git fetch origin fix/critical-job-races + git rebase origin/fix/critical-job-races git push origin HEAD:fix/critical-job-races + + - name: Upload commit diagnostics + if: failure() + uses: actions/upload-artifact@v4 + with: + name: final-hardening-commit-diagnostics + path: /tmp/final-hardening-commit.txt + if-no-files-found: ignore From c19ee96fe384472aa9f2b97400ba0fd0d4a7abd9 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 11:45:14 +0530 Subject: [PATCH 61/75] chore: separate product commit from workflow update --- .github/workflows/apply-final-hardening.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/apply-final-hardening.yml b/.github/workflows/apply-final-hardening.yml index 2f2b7ac..e193b0b 100644 --- a/.github/workflows/apply-final-hardening.yml +++ b/.github/workflows/apply-final-hardening.yml @@ -105,8 +105,8 @@ jobs: fi git config user.name 'github-actions[bot]' git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git restore -- '.github/workflows/critical-fixes-ci.yml' git add -A -- \ - '.github/workflows/critical-fixes-ci.yml' \ 'source app folder/dashboard/server-utils.js' \ 'source app folder/dashboard/server.js' \ 'source app folder/dashboard/src/App.jsx' \ From 0a944075b2ab1e7b2cb687a460c805ac2e9dc859 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 06:15:46 +0000 Subject: [PATCH 62/75] fix: make publication and reviews transactionally durable --- .github/final-hardening-diagnostics.txt | 11 - source app folder/dashboard/server-utils.js | 332 ++++++++++++++---- source app folder/dashboard/server.js | 88 +++-- source app folder/dashboard/src/App.jsx | 11 +- source app folder/dashboard/src/metrics.js | 31 ++ .../dashboard/tests/frontend-contract.test.js | 8 + .../dashboard/tests/metrics.test.js | 21 ++ .../tests/server-integration.test.js | 55 ++- .../dashboard/tests/server-utils.test.js | 134 +++++-- .../tracker/tests/test_tracker.py | 24 +- source app folder/tracker/tracker.py | 34 +- 11 files changed, 571 insertions(+), 178 deletions(-) delete mode 100644 .github/final-hardening-diagnostics.txt create mode 100644 source app folder/dashboard/src/metrics.js create mode 100644 source app folder/dashboard/tests/metrics.test.js diff --git a/.github/final-hardening-diagnostics.txt b/.github/final-hardening-diagnostics.txt deleted file mode 100644 index cff89f6..0000000 --- a/.github/final-hardening-diagnostics.txt +++ /dev/null @@ -1,11 +0,0 @@ - -> dashboard@0.0.0 lint:backend -> eslint server.js server-utils.js tests - - -/home/runner/work/Flyt/Flyt/source app folder/dashboard/server-utils.js - 301:15 error 'AggregateError' is not defined no-undef - 375:17 error 'AggregateError' is not defined no-undef - -✖ 2 problems (2 errors, 0 warnings) - diff --git a/source app folder/dashboard/server-utils.js b/source app folder/dashboard/server-utils.js index 3c092a9..76e4b0a 100644 --- a/source app folder/dashboard/server-utils.js +++ b/source app folder/dashboard/server-utils.js @@ -85,9 +85,10 @@ export function validateFrameIntegrity({ }; Object.entries(counts).forEach(([name, value]) => requirePositiveInteger(name, value)); - if (syncInfo.expectedVideoFrames > 0) { - requirePositiveInteger('trackerExpectedFrames', syncInfo.expectedVideoFrames); - counts.trackerExpectedFrames = syncInfo.expectedVideoFrames; + if (!Number.isInteger(syncInfo.expectedVideoFrames) || syncInfo.expectedVideoFrames < 0) { + throw new Error( + `Frame integrity evidence missing or invalid: trackerExpectedFrames=${syncInfo.expectedVideoFrames}`, + ); } const unique = new Set(Object.values(counts)); @@ -96,7 +97,14 @@ export function validateFrameIntegrity({ throw new Error(`Frame integrity mismatch (${detail})`); } - return { ...counts, syncOk: true }; + return { + ...counts, + trackerExpectedFrames: syncInfo.expectedVideoFrames, + expectedMetadataMatches: ( + syncInfo.expectedVideoFrames === 0 || syncInfo.expectedVideoFrames === inputFrames + ), + syncOk: true, + }; } const terminationPromises = new WeakMap(); @@ -227,55 +235,170 @@ export async function transcodeVideo(ffmpegPath, rawPath, finalPath, options = { } } -export function publishBundle(entries, token, { faultInjector } = {}) { - const staged = []; - const backups = []; - const published = []; - try { - for (const { source, destination } of entries) { - if (!fs.existsSync(source)) throw new Error(`Missing publish source: ${source}`); - ensureDir(path.dirname(destination)); - const stage = `${destination}.${token}.new`; - fs.copyFileSync(source, stage); - staged.push(stage); +export class SimulatedProcessCrash extends Error { + constructor(message = 'Simulated process crash') { + super(message); + this.name = 'SimulatedProcessCrash'; + } +} + +function validatePublishToken(token) { + if (!/^[A-Za-z0-9-]+$/.test(String(token))) { + throw new Error(`Invalid publication token: ${token}`); + } +} + +function publishManifestPath(entries, token, manifestDir) { + if (!entries.length) throw new Error('Publication bundle is empty'); + validatePublishToken(token); + const directory = manifestDir || path.dirname(entries[0].destination); + ensureDir(directory); + return path.join(directory, `.flyt-publish-${token}.json`); +} + +function validatePublishManifest(manifest, manifestPath) { + if (!manifest || manifest.version !== 1 || !Array.isArray(manifest.entries)) { + throw new Error(`Invalid publication transaction manifest: ${manifestPath}`); + } + if (!['prepared', 'publishing', 'committed'].includes(manifest.state)) { + throw new Error(`Invalid publication transaction state in ${manifestPath}`); + } + manifest.entries.forEach((entry) => { + for (const key of ['destination', 'stage', 'backup']) { + if (typeof entry[key] !== 'string' || !path.isAbsolute(entry[key])) { + throw new Error(`Invalid ${key} in publication transaction ${manifestPath}`); + } + } + if (typeof entry.hadDestination !== 'boolean') { + throw new Error(`Invalid hadDestination in publication transaction ${manifestPath}`); } - faultInjector?.('staged', -1); + }); + return manifest; +} - for (const { destination } of entries) { - if (fs.existsSync(destination)) { - const backup = `${destination}.${token}.bak`; - fs.renameSync(destination, backup); - backups.push({ destination, backup }); +function rollbackPublishManifest(manifest) { + const errors = []; + [...manifest.entries].reverse().forEach((entry) => { + try { + if (entry.hadDestination) { + if (fs.existsSync(entry.backup)) { + safeUnlink(entry.destination); + ensureDir(path.dirname(entry.destination)); + fs.renameSync(entry.backup, entry.destination); + } else if (!fs.existsSync(entry.destination)) { + throw new Error(`Cannot restore missing prior destination: ${entry.destination}`); + } + } else { + safeUnlink(entry.destination); + safeUnlink(entry.backup); } + safeUnlink(entry.stage); + } catch (error) { + errors.push(error); } - faultInjector?.('backed-up', -1); + }); + if (errors.length) { + throw new globalThis.AggregateError(errors, 'Publication transaction rollback failed'); + } +} + +function finalizeCommittedManifest(manifest) { + const missing = manifest.entries.filter((entry) => !fs.existsSync(entry.destination)); + if (missing.length) { + rollbackPublishManifest(manifest); + return; + } + manifest.entries.forEach((entry) => { + safeUnlink(entry.stage); + safeUnlink(entry.backup); + }); +} + +function listFilesRecursive(directory) { + if (!fs.existsSync(directory)) return []; + const files = []; + fs.readdirSync(directory, { withFileTypes: true }).forEach((entry) => { + const fullPath = path.join(directory, entry.name); + if (entry.isDirectory()) files.push(...listFilesRecursive(fullPath)); + else if (entry.isFile()) files.push(fullPath); + }); + return files; +} + +export function publishBundle(entries, token, { faultInjector, manifestDir } = {}) { + const manifestPath = publishManifestPath(entries, token, manifestDir); + const manifest = { + version: 1, + token, + state: 'prepared', + entries: entries.map(({ source, destination }) => ({ + source, + destination: path.resolve(destination), + stage: path.resolve(`${destination}.${token}.new`), + backup: path.resolve(`${destination}.${token}.bak`), + hadDestination: fs.existsSync(destination), + })), + }; + + try { + manifest.entries.forEach((entry) => { + if (!fs.existsSync(entry.source)) throw new Error(`Missing publish source: ${entry.source}`); + ensureDir(path.dirname(entry.destination)); + fs.copyFileSync(entry.source, entry.stage); + }); + writeJson(manifestPath, manifest); + faultInjector?.('prepared', -1); - entries.forEach(({ destination }, index) => { - fs.renameSync(staged[index], destination); - published.push(destination); + manifest.entries.forEach((entry, index) => { + if (entry.hadDestination) fs.renameSync(entry.destination, entry.backup); + faultInjector?.('backed-up', index); + }); + manifest.state = 'publishing'; + writeJson(manifestPath, manifest); + + manifest.entries.forEach((entry, index) => { + fs.renameSync(entry.stage, entry.destination); faultInjector?.('published', index); }); - backups.forEach(({ backup }) => safeUnlink(backup)); + manifest.state = 'committed'; + writeJson(manifestPath, manifest); + faultInjector?.('committed', -1); + + finalizeCommittedManifest(manifest); + safeUnlink(manifestPath); } catch (error) { - published.reverse().forEach((destination) => safeUnlink(destination)); - backups.reverse().forEach(({ destination, backup }) => { - if (fs.existsSync(backup)) fs.renameSync(backup, destination); - }); - staged.forEach((stage) => safeUnlink(stage)); + if (error instanceof SimulatedProcessCrash) throw error; + try { + rollbackPublishManifest(manifest); + safeUnlink(manifestPath); + } catch (rollbackError) { + throw new globalThis.AggregateError([error, rollbackError], 'Publication failed and rollback was incomplete'); + } throw error; } } export function recoverPublishArtifacts(directory) { if (!fs.existsSync(directory)) return; - const names = fs.readdirSync(directory); - names.filter((name) => name.endsWith('.new')).forEach((name) => safeUnlink(path.join(directory, name))); - names.filter((name) => name.endsWith('.bak')).forEach((name) => { - const backup = path.join(directory, name); - const destination = backup.replace(/\.[^.]+\.bak$/, ''); - if (!fs.existsSync(destination)) fs.renameSync(backup, destination); - else safeUnlink(backup); + const manifests = fs.readdirSync(directory) + .filter((name) => /^\.flyt-publish-[A-Za-z0-9-]+\.json$/.test(name)) + .map((name) => path.join(directory, name)); + + manifests.forEach((manifestPath) => { + const manifest = validatePublishManifest(readJson(manifestPath, null), manifestPath); + if (manifest.state === 'committed') finalizeCommittedManifest(manifest); + else rollbackPublishManifest(manifest); + safeUnlink(manifestPath); }); + + const artifacts = listFilesRecursive(directory); + const orphanBackups = artifacts.filter((filePath) => filePath.endsWith('.bak')); + if (orphanBackups.length) { + throw new Error( + `Unrecoverable publication backups without a transaction manifest: ${orphanBackups.join(', ')}`, + ); + } + artifacts.filter((filePath) => filePath.endsWith('.new')).forEach(safeUnlink); } export function buildTrackerArgs(trackerScript, inputPath, outputs, overrides = {}, defaults) { @@ -317,14 +440,29 @@ export function readCsvAvgProximity(csvPath) { const headers = lines[0].split(','); const proximityIndex = headers.indexOf('proximity_distance'); const occlusionIndex = headers.indexOf('occlusion_flag'); + const trackingValidIndex = headers.indexOf('tracking_valid'); + const detectionCountIndex = headers.indexOf('detection_count'); + const fly1AreaIndex = headers.indexOf('fly1_area'); + const fly2AreaIndex = headers.indexOf('fly2_area'); if (proximityIndex < 0) return null; + let total = 0; let count = 0; lines.slice(1).forEach((line) => { const columns = line.split(','); - const proximity = Number(columns[proximityIndex]); - const occluded = occlusionIndex >= 0 && Number(columns[occlusionIndex]) === 1; - if (Number.isFinite(proximity) && !occluded) { + const rawProximity = columns[proximityIndex]?.trim(); + if (!rawProximity) return; + const proximity = Number(rawProximity); + const occluded = occlusionIndex >= 0 && Number(columns[occlusionIndex]) !== 0; + let trackingValid = true; + if (trackingValidIndex >= 0) { + trackingValid = Number(columns[trackingValidIndex]) === 1; + } else if (detectionCountIndex >= 0) { + trackingValid = Number(columns[detectionCountIndex]) >= 2; + } else if (fly1AreaIndex >= 0 && fly2AreaIndex >= 0) { + trackingValid = Number(columns[fly1AreaIndex]) > 0 && Number(columns[fly2AreaIndex]) > 0; + } + if (Number.isFinite(proximity) && trackingValid && !occluded) { total += proximity; count += 1; } @@ -339,28 +477,65 @@ export function countCourtshipBouts(eventsPath) { : 0; } -export function scopeEventsForHistory(eventsPath, runId, destinationPath) { +const RUN_ID_PATTERN = /^run-[A-Za-z0-9][A-Za-z0-9-]{2,127}$/; + +function requireRunId(runId) { + if (!RUN_ID_PATTERN.test(String(runId))) throw new Error(`Invalid run ID: ${runId}`); + return String(runId); +} + +export function createRunId() { + return `run-${Date.now()}-${randomUUID().slice(0, 8)}`; +} + +export function scopeEventsForRun(eventsPath, runId, destinationPath = eventsPath) { + const safeRunId = requireRunId(runId); + const prefix = `${safeRunId}:`; const data = readJson(eventsPath, { version: 1, events: [] }); - data.events = Array.isArray(data.events) ? data.events.map((event) => ({ - ...event, - original_id: event.original_id || event.id, - id: `${runId}:${event.original_id || event.id}`, - })) : []; + data.run_id = safeRunId; + data.events = Array.isArray(data.events) ? data.events.map((event) => { + const currentId = String(event.id || ''); + const originalId = event.original_id + || (currentId.startsWith(prefix) ? currentId.slice(prefix.length) : currentId); + if (!originalId) throw new Error('Event ID missing while scoping run events'); + return { + ...event, + original_id: originalId, + id: `${safeRunId}:${originalId}`, + }; + }) : []; writeJson(destinationPath, data); + return data; } -export function snapshotRunToHistory({ - historyDir, historyMetaPath, filename, durationSec, fps, totalFrames, csvSrc, eventsSrc, +export function scopeEventsForHistory(eventsPath, runId, destinationPath) { + return scopeEventsForRun(eventsPath, runId, destinationPath); +} + +export function prepareRunHistoryBundle({ + runId, + historyDir, + historyMetaPath, + historyIndexOutput, + filename, + durationSec, + fps, + totalFrames, + csvSrc, + eventsSrc, + verificationSrc, }) { - if (!fs.existsSync(csvSrc)) return null; - const runId = `run-${Date.now()}-${randomUUID().slice(0, 8)}`; - const runDir = path.join(historyDir, runId); - ensureDir(runDir); - fs.copyFileSync(csvSrc, path.join(runDir, 'data.csv')); - scopeEventsForHistory(eventsSrc, runId, path.join(runDir, 'events.json')); - writeJson(path.join(runDir, 'verification.json'), { version: 1, reviews: [] }); + const safeRunId = requireRunId(runId); + for (const source of [csvSrc, eventsSrc, verificationSrc]) { + if (!fs.existsSync(source)) throw new Error(`Missing history source: ${source}`); + } + const history = readJson(historyMetaPath, { version: 1, runs: [] }); + if (!Array.isArray(history.runs)) history.runs = []; + if (history.runs.some((run) => run.runId === safeRunId)) { + throw new Error(`History run already exists: ${safeRunId}`); + } const meta = { - runId, + runId: safeRunId, timestamp: new Date().toISOString(), filename: filename || 'unknown', durationSec: Math.round((durationSec || 0) * 10) / 10, @@ -369,19 +544,34 @@ export function snapshotRunToHistory({ avgProximity: readCsvAvgProximity(csvSrc), detectedBouts: countCourtshipBouts(eventsSrc), }; - const history = readJson(historyMetaPath, { version: 1, runs: [] }); - if (!Array.isArray(history.runs)) history.runs = []; history.runs.unshift(meta); - writeJson(historyMetaPath, history); - return meta; -} - -export function resolveVerificationPath(eventId, currentPath, historyDir, historyMetaPath) { - const separator = eventId.indexOf(':'); - if (separator < 0) return currentPath; - const runId = eventId.slice(0, separator); - const history = readJson(historyMetaPath, { runs: [] }); - const exists = Array.isArray(history.runs) && history.runs.some((run) => run.runId === runId); - if (!exists) throw new Error('Unknown historical run'); - return path.join(historyDir, runId, 'verification.json'); + writeJson(historyIndexOutput, history); + + const runDir = path.join(historyDir, safeRunId); + return { + meta, + entries: [ + { source: csvSrc, destination: path.join(runDir, 'data.csv') }, + { source: eventsSrc, destination: path.join(runDir, 'events.json') }, + { source: verificationSrc, destination: path.join(runDir, 'verification.json') }, + { source: historyIndexOutput, destination: historyMetaPath }, + ], + }; +} + +export function verificationPathForRun(runId, historyDir, { mustExist = true } = {}) { + const safeRunId = requireRunId(runId); + const root = path.resolve(historyDir); + const filePath = path.resolve(root, safeRunId, 'verification.json'); + if (!filePath.startsWith(`${root}${path.sep}`)) throw new Error('Verification path escaped history root'); + if (mustExist && !fs.existsSync(filePath)) throw new Error('Run verification file not found'); + return filePath; +} + +export function resolveVerificationPath(eventId, historyDir) { + const separator = String(eventId).indexOf(':'); + if (separator <= 0 || separator === String(eventId).length - 1) { + throw new Error('Run-scoped event ID required'); + } + return verificationPathForRun(String(eventId).slice(0, separator), historyDir); } diff --git a/source app folder/dashboard/server.js b/source app folder/dashboard/server.js index 844564c..18edd7f 100644 --- a/source app folder/dashboard/server.js +++ b/source app folder/dashboard/server.js @@ -9,19 +9,22 @@ import { fileURLToPath } from 'url'; import { AbortRunError, buildTrackerArgs, + createRunId, countCsvRows, ensureDir, getVideoFrameCount, parseTrackerSync, + prepareRunHistoryBundle, publishBundle, readEventsFps, readJson, recoverPublishArtifacts, resolveVerificationPath, + scopeEventsForRun, runCommand, safeRemoveDir, safeUnlink, - snapshotRunToHistory, + verificationPathForRun, terminateChild, transcodeVideo, validateFrameIntegrity, @@ -111,7 +114,6 @@ export function createFlytServer(options = {}) { return next(); }); - let runSequence = 0; let epoch = 0; let uploadReserved = false; let terminating = false; @@ -160,8 +162,10 @@ export function createFlytServer(options = {}) { killOptions, }, )), - publish: options.services?.publish || ((entries, token) => publishBundle(entries, token)), - snapshot: options.services?.snapshot || snapshotRunToHistory, + publish: options.services?.publish || ((entries, token) => publishBundle( + entries, token, { manifestDir: publicDir }, + )), + prepareHistory: options.services?.prepareHistory || prepareRunHistoryBundle, }; const isBusy = () => ( @@ -193,7 +197,7 @@ export function createFlytServer(options = {}) { return next(); } - function readVerification(filePath = verificationPath) { + function readVerification(filePath) { const data = readJson(filePath, { version: 1, reviews: [] }); return Array.isArray(data.reviews) ? data : { version: 1, reviews: [] }; } @@ -227,7 +231,7 @@ export function createFlytServer(options = {}) { } function createRunContext(inputPath, filename, overrides) { - const runId = ++runSequence; + const runId = createRunId(); const runEpoch = epoch; const token = `${runId}-${runEpoch}-${randomUUID()}`; const workDir = path.join(uploadsDir, `run-${token}`); @@ -255,6 +259,7 @@ export function createFlytServer(options = {}) { events: path.join(context.workDir, 'events.json'), metadata: path.join(context.workDir, 'run_metadata.json'), verification: path.join(context.workDir, 'verification.json'), + historyIndex: path.join(context.workDir, 'history.json'), }; const ensureCurrent = () => { if (!isCurrent(context)) throw new AbortRunError(); @@ -305,41 +310,41 @@ export function createFlytServer(options = {}) { finalVideoFrames, }); const fps = readEventsFps(outputs.events); + scopeEventsForRun(outputs.events, context.runId, outputs.events); const metadata = { ...integrity, + runId: context.runId, expectedVideoFrames: syncInfo.expectedVideoFrames, fps, timestamp: new Date().toISOString(), }; writeJson(outputs.metadata, metadata); - writeJson(outputs.verification, { version: 1, reviews: [] }); + writeJson(outputs.verification, { version: 1, run_id: context.runId, reviews: [] }); + const historyBundle = services.prepareHistory({ + runId: context.runId, + historyDir, + historyMetaPath, + historyIndexOutput: outputs.historyIndex, + filename: context.filename, + durationSec: (Date.now() - context.startedAt) / 1000, + fps, + totalFrames: integrity.inputFrames, + csvSrc: outputs.csv, + eventsSrc: outputs.events, + verificationSrc: outputs.verification, + }); ensureCurrent(); - job.progress = 'Publishing validated results...'; + job.progress = 'Publishing validated current and historical results...'; services.publish([ { source: outputs.csv, destination: path.join(publicDir, 'data.csv') }, { source: outputs.events, destination: path.join(publicDir, 'events.json') }, { source: outputs.browserVideo, destination: path.join(publicDir, 'tracked.mp4') }, { source: outputs.metadata, destination: path.join(publicDir, 'run_metadata.json') }, - { source: outputs.verification, destination: verificationPath }, + ...historyBundle.entries, ], context.token); ensureCurrent(); - try { - services.snapshot({ - historyDir, - historyMetaPath, - filename: context.filename, - durationSec: (Date.now() - context.startedAt) / 1000, - fps, - totalFrames: integrity.inputFrames, - csvSrc: outputs.csv, - eventsSrc: outputs.events, - }); - } catch (historyError) { - console.error(`[Server] History snapshot failed: ${historyError.message}`); - } - job = { ...job, frameCount: integrity.inputFrames, @@ -411,12 +416,18 @@ export function createFlytServer(options = {}) { return res.json(data); }); + function currentRunId() { + return readJson(path.join(publicDir, 'run_metadata.json'), null)?.runId || null; + } + app.get('/api/verification', (req, res) => { - if (!req.query.runId) return res.json(readVerification()); - const history = readJson(historyMetaPath, { runs: [] }); - const known = history.runs?.some((run) => run.runId === req.query.runId); - if (!known) return res.status(404).json({ error: 'Run not found' }); - return res.json(readVerification(path.join(historyDir, req.query.runId, 'verification.json'))); + const runId = req.query.runId || currentRunId(); + if (!runId) return res.status(404).json({ error: 'No current run found' }); + try { + return res.json(readVerification(verificationPathForRun(runId, historyDir))); + } catch (error) { + return res.status(404).json({ error: error.message }); + } }); app.post('/api/verification', (req, res) => { @@ -425,12 +436,7 @@ export function createFlytServer(options = {}) { return res.status(400).json({ error: 'Body must include eventId and verdict.' }); } try { - const filePath = resolveVerificationPath( - eventId, - verificationPath, - historyDir, - historyMetaPath, - ); + const filePath = resolveVerificationPath(eventId, historyDir); const data = readVerification(filePath); const review = { event_id: eventId, verdict, reviewed_at: new Date().toISOString() }; const index = data.reviews.findIndex((item) => item.event_id === eventId); @@ -443,9 +449,15 @@ export function createFlytServer(options = {}) { } }); - app.post('/api/verification/reset', (_req, res) => { - writeJson(verificationPath, { version: 1, reviews: [] }); - res.json({ success: true }); + app.post('/api/verification/reset', (req, res) => { + const runId = req.body?.runId || currentRunId(); + if (!runId) return res.status(404).json({ error: 'No current run found' }); + try { + writeJson(verificationPathForRun(runId, historyDir), { version: 1, run_id: runId, reviews: [] }); + return res.json({ success: true }); + } catch (error) { + return res.status(404).json({ error: error.message }); + } }); app.get('/api/history', (_req, res) => { diff --git a/source app folder/dashboard/src/App.jsx b/source app folder/dashboard/src/App.jsx index 1f53394..1424034 100644 --- a/source app folder/dashboard/src/App.jsx +++ b/source app folder/dashboard/src/App.jsx @@ -1,5 +1,6 @@ import React, { useState, useEffect, useRef } from 'react'; import Papa from 'papaparse'; +import { prismDistance, proximityValue } from './metrics.js'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, AreaChart, Area, ScatterChart, Scatter, ZAxis @@ -66,7 +67,7 @@ function exportPrismCsv(rows, fps) { const t = Number(r.frame) / effectiveFps; const v1 = Number(r.fly1_speed_pxsec ?? r.fly1_speed ?? 0); const v2 = Number(r.fly2_speed_pxsec ?? r.fly2_speed ?? 0); - const dist = Number(r.proximity_distance ?? 0); + const dist = prismDistance(r); lines.push([t, v1, v2, dist].map(csvEscape).join(',')); }); const blob = new Blob([lines.join('\n')], { type: 'text/csv;charset=utf-8' }); @@ -720,9 +721,11 @@ function App() { const hmData = []; parsedData.forEach((row, i) => { - // Average proximity only over frames where flies are separate (exclude merged/occluded where proximity=0 by design) - if (row.proximity_distance != null && !row.occlusion_flag) { - totalProx += row.proximity_distance; + // Only measured two-fly observations contribute. Dropouts retain + // display coordinates but carry no scientifically valid proximity. + const observedProximity = proximityValue(row); + if (observedProximity !== null) { + totalProx += observedProximity; proxCount++; } if (row.activity_level > maxAct) maxAct = row.activity_level; diff --git a/source app folder/dashboard/src/metrics.js b/source app folder/dashboard/src/metrics.js new file mode 100644 index 0000000..759e97b --- /dev/null +++ b/source app folder/dashboard/src/metrics.js @@ -0,0 +1,31 @@ +export function proximityValue(row) { + if (!row) return null; + const rawProximity = row.proximity_distance; + if (rawProximity === null || rawProximity === undefined || rawProximity === '') return null; + const proximity = Number(rawProximity); + if (!Number.isFinite(proximity)) return null; + if (Number(row.occlusion_flag ?? 0) !== 0) return null; + + if (row.tracking_valid !== null && row.tracking_valid !== undefined && row.tracking_valid !== '') { + if (Number(row.tracking_valid) !== 1) return null; + } else if (row.detection_count !== null && row.detection_count !== undefined && row.detection_count !== '') { + if (Number(row.detection_count) < 2) return null; + } else if ( + row.fly1_area !== null && row.fly1_area !== undefined + && row.fly2_area !== null && row.fly2_area !== undefined + ) { + if (!(Number(row.fly1_area) > 0 && Number(row.fly2_area) > 0)) return null; + } + + return proximity; +} + +export function computeAverageProximity(rows) { + const values = rows.map(proximityValue).filter((value) => value !== null); + if (!values.length) return 0; + return Math.round(values.reduce((sum, value) => sum + value, 0) / values.length); +} + +export function prismDistance(row) { + return proximityValue(row) ?? ''; +} diff --git a/source app folder/dashboard/tests/frontend-contract.test.js b/source app folder/dashboard/tests/frontend-contract.test.js index 49df876..d3e531b 100644 --- a/source app folder/dashboard/tests/frontend-contract.test.js +++ b/source app folder/dashboard/tests/frontend-contract.test.js @@ -20,3 +20,11 @@ test('historic runs display their recorded timestamp rather than load time', () assert.match(source, /setRunTimestamp\(run\.timestamp \|\| null\)/); assert.match(source, /loadHistoricRun\(run\)/); }); + + +test('dashboard and Prism export use validity-aware proximity helpers', () => { + const source = appSource(); + assert.match(source, /proximityValue\(row\)/); + assert.match(source, /prismDistance\(r\)/); + assert.doesNotMatch(source, /Number\(r\.proximity_distance \?\? 0\)/); +}); diff --git a/source app folder/dashboard/tests/metrics.test.js b/source app folder/dashboard/tests/metrics.test.js new file mode 100644 index 0000000..19a5262 --- /dev/null +++ b/source app folder/dashboard/tests/metrics.test.js @@ -0,0 +1,21 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { computeAverageProximity, prismDistance, proximityValue } from '../src/metrics.js'; + +test('carried coordinates from missing detections do not bias proximity metrics', () => { + const rows = [ + { proximity_distance: 10, tracking_valid: 1, detection_count: 2, occlusion_flag: 0 }, + { proximity_distance: 10, tracking_valid: 0, detection_count: 0, occlusion_flag: 0 }, + { proximity_distance: null, tracking_valid: 0, detection_count: 1, occlusion_flag: 1 }, + { proximity_distance: 30, tracking_valid: 1, detection_count: 2, occlusion_flag: 0 }, + ]; + assert.equal(computeAverageProximity(rows), 20); + assert.equal(proximityValue(rows[1]), null); + assert.equal(prismDistance(rows[1]), ''); +}); + +test('legacy rows remain valid only when they contain a finite non-occluded observation', () => { + assert.equal(proximityValue({ proximity_distance: 12, occlusion_flag: 0 }), 12); + assert.equal(proximityValue({ proximity_distance: '', occlusion_flag: 0 }), null); + assert.equal(proximityValue({ proximity_distance: 0, occlusion_flag: 1 }), null); +}); diff --git a/source app folder/dashboard/tests/server-integration.test.js b/source app folder/dashboard/tests/server-integration.test.js index 3644501..c965238 100644 --- a/source app folder/dashboard/tests/server-integration.test.js +++ b/source app folder/dashboard/tests/server-integration.test.js @@ -38,8 +38,8 @@ function createGate({ releaseOnAbort = true } = {}) { function writeTrackerOutputs(outputs, frames = 4) { fs.writeFileSync(outputs.rawVideo, 'raw-video'); fs.writeFileSync(outputs.csv, [ - 'frame,proximity_distance,occlusion_flag', - ...Array.from({ length: frames }, (_, index) => `${index},20,0`), + 'frame,proximity_distance,occlusion_flag,tracking_valid,detection_count,fly1_area,fly2_area', + ...Array.from({ length: frames }, (_, index) => `${index},20,0,1,2,100,100`), ].join('\n')); fs.writeFileSync(outputs.events, JSON.stringify({ fps: 30, @@ -60,7 +60,6 @@ function standardServices(overrides = {}) { }; }, transcode: async (raw, final) => fs.copyFileSync(raw, final), - snapshot: () => null, ...overrides, }; } @@ -225,6 +224,9 @@ test('a complete run publishes only after every frame count agrees', async (t) = assert.equal(metadata.rawVideoFrames, 4); assert.equal(metadata.finalVideoFrames, 4); assert.equal(metadata.syncOk, true); + assert.match(metadata.runId, /^run-/); + const currentEvents = JSON.parse(fs.readFileSync(path.join(harness.paths.publicDir, 'events.json'))); + assert.equal(currentEvents.events[0].id, `${metadata.runId}:evt-001`); }); test('frame mismatch fails closed and preserves the previous published bundle', async (t) => { @@ -261,3 +263,50 @@ test('destructive cross-origin requests are denied unless explicitly allowed', a assert.equal(allowed.status, 200); assert.equal(allowed.headers.get('access-control-allow-origin'), 'http://localhost:5173'); }); + + +test('a verdict made on the current run survives subsequent runs and history reload', async (t) => { + const harness = await createHarness(standardServices()); + t.after(() => harness.close()); + + assert.equal((await upload(harness.baseUrl, 'first.mp4')).status, 200); + const firstStatus = await waitForStatus(harness.baseUrl, 'done'); + const firstRunId = firstStatus.runId; + const eventsResponse = await fetch(`${harness.baseUrl}/api/events`); + const firstEvents = await eventsResponse.json(); + const eventId = firstEvents.events[0].id; + assert.equal(eventId, `${firstRunId}:evt-001`); + + const verdictResponse = await fetch(`${harness.baseUrl}/api/verification`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ eventId, verdict: 'confirmed' }), + }); + assert.equal(verdictResponse.status, 200); + + const unscopedResponse = await fetch(`${harness.baseUrl}/api/verification`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ eventId: 'evt-001', verdict: 'confirmed' }), + }); + assert.equal(unscopedResponse.status, 400); + + assert.equal((await upload(harness.baseUrl, 'second.mp4')).status, 200); + const secondStatus = await waitForStatus(harness.baseUrl, 'done'); + assert.notEqual(secondStatus.runId, firstRunId); + + const historicResponse = await fetch( + `${harness.baseUrl}/api/verification?runId=${encodeURIComponent(firstRunId)}`, + ); + assert.equal(historicResponse.status, 200); + const historicVerification = await historicResponse.json(); + assert.deepEqual(historicVerification.reviews.map((review) => ({ + event_id: review.event_id, + verdict: review.verdict, + })), [{ event_id: eventId, verdict: 'confirmed' }]); + + const canonical = JSON.parse(fs.readFileSync( + path.join(harness.paths.historyDir, firstRunId, 'verification.json'), + )); + assert.equal(canonical.reviews[0].verdict, 'confirmed'); +}); diff --git a/source app folder/dashboard/tests/server-utils.test.js b/source app folder/dashboard/tests/server-utils.test.js index 037a336..9c45100 100644 --- a/source app folder/dashboard/tests/server-utils.test.js +++ b/source app folder/dashboard/tests/server-utils.test.js @@ -7,14 +7,17 @@ import test from 'node:test'; import { buildTrackerArgs, parseTrackerSync, + prepareRunHistoryBundle, publishBundle, + readCsvAvgProximity, recoverPublishArtifacts, resolveVerificationPath, runCommand, - scopeEventsForHistory, - snapshotRunToHistory, + scopeEventsForRun, + SimulatedProcessCrash, terminateChild, validateFrameIntegrity, + verificationPathForRun, } from '../server-utils.js'; const defaults = { minArea: 30, maxArea: 0, proximityThreshold: 60, boutMinFrames: 90 }; @@ -46,10 +49,12 @@ test('frame validation fails closed on missing evidence or disagreement', () => assert.equal(validateFrameIntegrity(evidence).syncOk, true); assert.throws(() => validateFrameIntegrity({ ...evidence, syncInfo: null }), /marker required/); assert.throws(() => validateFrameIntegrity({ ...evidence, finalVideoFrames: 11 }), /mismatch/); - assert.throws(() => validateFrameIntegrity({ + const diagnosticMismatch = validateFrameIntegrity({ ...evidence, syncInfo: { ...evidence.syncInfo, expectedVideoFrames: 11 }, - }), /mismatch/); + }); + assert.equal(diagnosticMismatch.syncOk, true); + assert.equal(diagnosticMismatch.expectedMetadataMatches, false); }); test('publishes a complete bundle and removes backups', () => { @@ -94,57 +99,112 @@ test('rolls back after one destination has already been published', () => { assert.equal(fs.readdirSync(dir).some((name) => /\.(bak|new)$/.test(name)), false); }); -test('recovers orphaned publication artifacts after a crash', () => { - const dir = tempDir('flyt-recover-'); - fs.writeFileSync(path.join(dir, 'data.csv.token.bak'), 'old-data'); - fs.writeFileSync(path.join(dir, 'events.json.token.new'), 'new-events'); +test('crash recovery rolls back the entire partially published bundle', () => { + const dir = tempDir('flyt-crash-rollback-'); + const sourceA = path.join(dir, 'source-a'); + const sourceB = path.join(dir, 'source-b'); + const destinationA = path.join(dir, 'a'); + const destinationB = path.join(dir, 'b'); + fs.writeFileSync(sourceA, 'new-a'); + fs.writeFileSync(sourceB, 'new-b'); + fs.writeFileSync(destinationA, 'old-a'); + fs.writeFileSync(destinationB, 'old-b'); + + assert.throws(() => publishBundle([ + { source: sourceA, destination: destinationA }, + { source: sourceB, destination: destinationB }, + ], 'crash-token', { + manifestDir: dir, + faultInjector(stage, index) { + if (stage === 'published' && index === 0) throw new SimulatedProcessCrash(); + }, + }), SimulatedProcessCrash); + + assert.equal(fs.readFileSync(destinationA, 'utf8'), 'new-a'); + assert.equal(fs.existsSync(destinationB), false); + recoverPublishArtifacts(dir); + assert.equal(fs.readFileSync(destinationA, 'utf8'), 'old-a'); + assert.equal(fs.readFileSync(destinationB, 'utf8'), 'old-b'); + assert.equal(fs.readdirSync(dir).some((name) => /flyt-publish|\.(bak|new)$/.test(name)), false); +}); + +test('crash recovery finalizes an explicitly committed all-new bundle', () => { + const dir = tempDir('flyt-crash-commit-'); + const source = path.join(dir, 'source'); + const destination = path.join(dir, 'destination'); + fs.writeFileSync(source, 'new'); + fs.writeFileSync(destination, 'old'); + + assert.throws(() => publishBundle([ + { source, destination }, + ], 'commit-token', { + manifestDir: dir, + faultInjector(stage) { + if (stage === 'committed') throw new SimulatedProcessCrash(); + }, + }), SimulatedProcessCrash); + recoverPublishArtifacts(dir); - assert.equal(fs.readFileSync(path.join(dir, 'data.csv'), 'utf8'), 'old-data'); - assert.equal(fs.existsSync(path.join(dir, 'events.json.token.new')), false); + assert.equal(fs.readFileSync(destination, 'utf8'), 'new'); + assert.equal(fs.readdirSync(dir).some((name) => /flyt-publish|\.(bak|new)$/.test(name)), false); }); -test('history snapshots preserve dashboard summary fields and scoped reviews', () => { +test('legacy orphan backups fail closed instead of reconstructing a mixed bundle', () => { + const dir = tempDir('flyt-orphan-backup-'); + const backup = path.join(dir, 'data.csv.token.bak'); + fs.writeFileSync(backup, 'old-data'); + assert.throws(() => recoverPublishArtifacts(dir), /without a transaction manifest/); + assert.equal(fs.readFileSync(backup, 'utf8'), 'old-data'); +}); + +test('history bundle uses scoped events and excludes invalid proximity observations', () => { const dir = tempDir('flyt-history-'); const historyDir = path.join(dir, 'history'); const historyMetaPath = path.join(dir, 'history.json'); + const historyIndexOutput = path.join(dir, 'next-history.json'); const csv = path.join(dir, 'data.csv'); const events = path.join(dir, 'events.json'); + const verification = path.join(dir, 'verification.json'); + const runId = 'run-1234-abcd'; fs.writeFileSync(csv, [ - 'frame,proximity_distance,occlusion_flag', '0,10,0', '1,0,1', '2,30,0', + 'frame,proximity_distance,occlusion_flag,tracking_valid,detection_count', + '0,10,0,1,2', + '1,10,0,0,0', + '2,,1,0,1', + '3,30,0,1,2', ].join('\n')); fs.writeFileSync(events, JSON.stringify({ events: [{ id: 'evt-001', type: 'courtship_bout' }], })); - const meta = snapshotRunToHistory({ - historyDir, historyMetaPath, filename: 'flies.mp4', durationSec: 2.3, - fps: 30, totalFrames: 3, csvSrc: csv, eventsSrc: events, + fs.writeFileSync(verification, JSON.stringify({ version: 1, run_id: runId, reviews: [] })); + scopeEventsForRun(events, runId, events); + + const bundle = prepareRunHistoryBundle({ + runId, + historyDir, + historyMetaPath, + historyIndexOutput, + filename: 'flies.mp4', + durationSec: 2.3, + fps: 30, + totalFrames: 4, + csvSrc: csv, + eventsSrc: events, + verificationSrc: verification, }); - assert.equal(meta.avgProximity, 20); - assert.equal(meta.detectedBouts, 1); - const scopedEvents = JSON.parse(fs.readFileSync(path.join(historyDir, meta.runId, 'events.json'))); - assert.equal(scopedEvents.events[0].id, `${meta.runId}:evt-001`); - assert.deepEqual( - JSON.parse(fs.readFileSync(path.join(historyDir, meta.runId, 'verification.json'))), - { version: 1, reviews: [] }, - ); -}); + assert.equal(bundle.meta.avgProximity, 20); + assert.equal(bundle.meta.detectedBouts, 1); + assert.equal(readCsvAvgProximity(csv), 20); + publishBundle(bundle.entries, 'history-token', { manifestDir: dir }); -test('scopes historical event IDs and verification paths to their run', () => { - const dir = tempDir('flyt-history-path-'); - const historyDir = path.join(dir, 'history'); - const runId = 'run-1234-abcd'; - fs.mkdirSync(path.join(historyDir, runId), { recursive: true }); - const source = path.join(dir, 'events.json'); - const scoped = path.join(historyDir, runId, 'events.json'); - const historyMeta = path.join(dir, 'history.json'); - fs.writeFileSync(source, JSON.stringify({ events: [{ id: 'evt-001' }] })); - fs.writeFileSync(historyMeta, JSON.stringify({ runs: [{ runId }] })); - scopeEventsForHistory(source, runId, scoped); - assert.equal(JSON.parse(fs.readFileSync(scoped)).events[0].id, `${runId}:evt-001`); + const scopedEvents = JSON.parse(fs.readFileSync(path.join(historyDir, runId, 'events.json'))); + assert.equal(scopedEvents.events[0].id, `${runId}:evt-001`); assert.equal( - resolveVerificationPath(`${runId}:evt-001`, 'current.json', historyDir, historyMeta), + resolveVerificationPath(`${runId}:evt-001`, historyDir), path.join(historyDir, runId, 'verification.json'), ); + assert.equal(verificationPathForRun(runId, historyDir), path.join(historyDir, runId, 'verification.json')); + assert.throws(() => resolveVerificationPath('evt-001', historyDir), /Run-scoped event ID required/); }); test('termination escalates to SIGKILL and resolves only after close', async () => { diff --git a/source app folder/tracker/tests/test_tracker.py b/source app folder/tracker/tests/test_tracker.py index 273efe8..b835f3d 100644 --- a/source app folder/tracker/tests/test_tracker.py +++ b/source app folder/tracker/tests/test_tracker.py @@ -9,7 +9,9 @@ spec.loader.exec_module(tracker) -def row(frame, proximity, confidence=1.0, occlusion=0, area=100.0): +def row(frame, proximity, confidence=1.0, occlusion=0, area=100.0, tracking_valid=None): + if tracking_valid is None: + tracking_valid = int(occlusion == 0 and area > 0) return { "frame": frame, "proximity_distance": proximity, @@ -17,6 +19,7 @@ def row(frame, proximity, confidence=1.0, occlusion=0, area=100.0): "occlusion_flag": occlusion, "fly1_area": area, "fly2_area": area, + "tracking_valid": tracking_valid, } @@ -56,12 +59,27 @@ def test_assignment_confidence_can_fall_below_low_confidence_threshold(self): confidence = tracker.assignment_confidence((5, 0), (-5, 0), (0, 0), (0, 0)) self.assertLess(confidence, tracker.LOW_CONFIDENCE_THRESHOLD) - def test_frame_sync_requires_expected_input_count_when_available(self): + def test_frame_sync_treats_opencv_count_as_diagnostic_only(self): self.assertTrue(tracker.frame_sync_ok(10, 10, 9, 10)) - self.assertFalse(tracker.frame_sync_ok(8, 8, 7, 10)) + self.assertTrue(tracker.frame_sync_ok(8, 8, 7, 10)) self.assertTrue(tracker.frame_sync_ok(8, 8, 7, 0)) self.assertFalse(tracker.frame_sync_ok(8, 7, 6, 8)) + def test_invalid_tracking_observation_cannot_be_courtship(self): + invalid = row(0, proximity=10, confidence=1.0, tracking_valid=0) + self.assertFalse(tracker.is_courtship_frame(invalid, 60)) + self.assertTrue(tracker.is_low_confidence_frame(invalid)) + + def test_event_mean_proximity_ignores_missing_values(self): + segment = [ + row(0, proximity=20, confidence=0.8), + row(1, proximity=float("nan"), confidence=0.0, tracking_valid=0), + ] + event = tracker.build_event_record( + "evt-001", "low_confidence_segment", 0, 1, 30, segment, "test" + ) + self.assertEqual(event["mean_proximity_px"], 20.0) + def test_roi_validation(self): self.assertEqual(tracker.parse_roi("1,2,3,4"), (1, 2, 3, 4)) with self.assertRaises(ValueError): diff --git a/source app folder/tracker/tracker.py b/source app folder/tracker/tracker.py index bee9310..c3c861c 100644 --- a/source app folder/tracker/tracker.py +++ b/source app folder/tracker/tracker.py @@ -74,7 +74,10 @@ def build_event_record( ) -> dict[str, Any]: effective_fps = fps if fps > 0 else 30.0 duration_frames = end_frame - start_frame + 1 - proximities = [value_or(row, "proximity_distance", 0.0) for row in segment] + proximities = [ + value_or(row, "proximity_distance", math.nan) for row in segment + if math.isfinite(value_or(row, "proximity_distance", math.nan)) + ] confidences = [value_or(row, "identity_confidence", 0.0) for row in segment] occlusions = [value_or(row, "occlusion_flag", 0.0) for row in segment] return { @@ -98,8 +101,10 @@ def is_courtship_frame(row: dict[str, Any], proximity_threshold: float) -> bool: occluded = value_or(row, "occlusion_flag", 1.0) != 0.0 fly1_present = value_or(row, "fly1_area", 0.0) > 0.0 fly2_present = value_or(row, "fly2_area", 0.0) > 0.0 + tracking_valid = value_or(row, "tracking_valid", 1.0) != 0.0 return ( - math.isfinite(proximity) + tracking_valid + and math.isfinite(proximity) and not occluded and fly1_present and fly2_present @@ -110,7 +115,8 @@ def is_courtship_frame(row: dict[str, Any], proximity_threshold: float) -> bool: def is_low_confidence_frame(row: dict[str, Any]) -> bool: return ( - value_or(row, "occlusion_flag", 0.0) != 0.0 + value_or(row, "tracking_valid", 1.0) == 0.0 + or value_or(row, "occlusion_flag", 0.0) != 0.0 or value_or(row, "identity_confidence", 1.0) < LOW_CONFIDENCE_THRESHOLD ) @@ -200,10 +206,10 @@ def frame_sync_ok( csv_rows == frames_processed and (frames_processed == 0 or last_frame == frames_processed - 1) ) - expected_sync_ok = ( - expected_frame_count <= 0 or frames_processed == expected_frame_count - ) - return internal_sync_ok and expected_sync_ok + # OpenCV CAP_PROP_FRAME_COUNT is diagnostic only. The server performs the + # authoritative full decode with ffmpeg and compares it to tracker/CSV/video output. + _ = expected_frame_count + return internal_sync_ok def parse_roi(value: str | None) -> tuple[int, int, int, int] | None: @@ -239,7 +245,7 @@ def run_tracker(args: argparse.Namespace) -> int: height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) fps = float(cap.get(cv2.CAP_PROP_FPS)) effective_fps = fps if fps > 0 else 30.0 - expected_frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) + expected_frame_count = max(0, int(cap.get(cv2.CAP_PROP_FRAME_COUNT))) out = None if not args.no_video: @@ -300,10 +306,12 @@ def run_tracker(args: argparse.Namespace) -> int: f1_coords = f2_coords = (0, 0) f1_speed = f2_speed = 0.0 - proximity = 0.0 + proximity = math.nan occlusion_flag = 0 identity_confidence = 0.0 fly1_area = fly2_area = 0.0 + detection_count = len(centroids) + tracking_valid = 0 if len(centroids) == 2: ca, cb = centroids @@ -326,6 +334,7 @@ def run_tracker(args: argparse.Namespace) -> int: f1_speed = displacement(prev_f1, f1_coords, was_initialized) f2_speed = displacement(prev_f2, f2_coords, was_initialized) proximity = float(math.dist(f1_coords, f2_coords)) + tracking_valid = 1 prev_f1, prev_f2 = f1_coords, f2_coords if out: for x, y, w, h in bboxes: @@ -371,8 +380,9 @@ def run_tracker(args: argparse.Namespace) -> int: 2, ) elif is_initialized: + # Coordinates are retained only for display continuity. Proximity is + # deliberately missing because no two-fly observation occurred. f1_coords, f2_coords = prev_f1, prev_f2 - proximity = float(math.dist(f1_coords, f2_coords)) identity_confidence = 0.0 data.append({ @@ -387,6 +397,8 @@ def run_tracker(args: argparse.Namespace) -> int: "fly2_speed_pxsec": round(f2_speed * effective_fps, 4), "activity_level": activity_level, "proximity_distance": proximity, + "tracking_valid": tracking_valid, + "detection_count": detection_count, "occlusion_flag": occlusion_flag, "identity_confidence": round(identity_confidence, 4), "fly1_area": fly1_area, @@ -422,7 +434,7 @@ def run_tracker(args: argparse.Namespace) -> int: ) if not sync_ok: print( - "Error: frame integrity mismatch between decoded input, tracker loop, and CSV", + "Error: frame integrity mismatch between tracker loop and CSV", file=sys.stderr, ) return 2 From 4230875e1db6d8c975b7590f71ae64313da1f0f4 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 11:46:21 +0530 Subject: [PATCH 63/75] ci: validate Node integration on Windows --- .github/workflows/critical-fixes-ci.yml | 34 ++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/.github/workflows/critical-fixes-ci.yml b/.github/workflows/critical-fixes-ci.yml index e610dce..8120cf6 100644 --- a/.github/workflows/critical-fixes-ci.yml +++ b/.github/workflows/critical-fixes-ci.yml @@ -14,7 +14,7 @@ permissions: contents: read jobs: - validate: + validate-linux: runs-on: ubuntu-latest timeout-minutes: 15 steps: @@ -34,7 +34,7 @@ jobs: working-directory: source app folder/dashboard run: npm run lint:backend - - name: Run utility, HTTP race, and frontend contract tests + - name: Run utility, HTTP race, lifecycle, and metric tests working-directory: source app folder/dashboard run: npm run test:server @@ -51,8 +51,36 @@ jobs: - name: Install tracker dependencies run: python -m pip install -r 'source app folder/tracker/requirements.txt' - - name: Check tracker syntax + - name: Check Python syntax run: python -m py_compile 'source app folder/tracker/tracker.py' - name: Run tracker scientific regression tests run: python -m unittest discover -s 'source app folder/tracker/tests' -p 'test_*.py' -v + + validate-windows-node: + runs-on: windows-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: source app folder/dashboard/package-lock.json + + - name: Install dashboard dependencies + working-directory: source app folder/dashboard + run: npm ci + + - name: Lint backend and regression tests + working-directory: source app folder/dashboard + run: npm run lint:backend + + - name: Run Node utility, HTTP, lifecycle, and metric tests + working-directory: source app folder/dashboard + run: npm run test:server + + - name: Build dashboard + working-directory: source app folder/dashboard + run: npm run build From 8f5f9e3657df8db10e0e5c6f078425c6f5be837d Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 11:47:15 +0530 Subject: [PATCH 64/75] ci: annotate final clean validation trigger From e14ff58ea8df6ba0aeeb64fe43d8a7e731cfbafe Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 11:47:44 +0530 Subject: [PATCH 65/75] ci: refresh clean branch validation From 64ce4b55f6b3c635ea1603187d5817b476dc149f Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 11:48:00 +0530 Subject: [PATCH 66/75] chore: remove temporary final-hardening automation --- .github/scripts/final-hardening.part00 | 280 -------------------- .github/scripts/final-hardening.part01 | 280 -------------------- .github/scripts/final-hardening.part02 | 280 -------------------- .github/scripts/final-hardening.part03 | 280 -------------------- .github/scripts/final-hardening.part04 | 16 -- .github/workflows/apply-final-hardening.yml | 133 ---------- 6 files changed, 1269 deletions(-) delete mode 100644 .github/scripts/final-hardening.part00 delete mode 100644 .github/scripts/final-hardening.part01 delete mode 100644 .github/scripts/final-hardening.part02 delete mode 100644 .github/scripts/final-hardening.part03 delete mode 100644 .github/scripts/final-hardening.part04 delete mode 100644 .github/workflows/apply-final-hardening.yml diff --git a/.github/scripts/final-hardening.part00 b/.github/scripts/final-hardening.part00 deleted file mode 100644 index 0d1c9de..0000000 --- a/.github/scripts/final-hardening.part00 +++ /dev/null @@ -1,280 +0,0 @@ -from pathlib import Path - - -def replace_once(text: str, old: str, new: str, label: str) -> str: - count = text.count(old) - if count != 1: - raise RuntimeError(f"{label}: expected one occurrence, found {count}") - return text.replace(old, new, 1) - - -def replace_between(text: str, start: str, end: str, new_block: str, label: str) -> str: - start_index = text.find(start) - end_index = text.find(end, start_index + len(start)) - if start_index < 0 or end_index < 0: - raise RuntimeError(f"{label}: markers not found") - return text[:start_index] + new_block.rstrip() + "\n\n" + text[end_index:] - - -root = Path('.') -marker_path = root / 'source app folder/dashboard/src/metrics.js' -utils_marker_path = root / 'source app folder/dashboard/server-utils.js' -if marker_path.exists() and 'SimulatedProcessCrash' in utils_marker_path.read_text(encoding='utf-8'): - print('Final hardening is already applied; validation-only run.') - raise SystemExit(0) - -# --------------------------------------------------------------------------- -# Transaction-aware publication, canonical run history, and valid metrics. -# --------------------------------------------------------------------------- -utils_path = root / 'source app folder/dashboard/server-utils.js' -utils = utils_path.read_text(encoding='utf-8') - -publication_block = r'''export class SimulatedProcessCrash extends Error { - constructor(message = 'Simulated process crash') { - super(message); - this.name = 'SimulatedProcessCrash'; - } -} - -function validatePublishToken(token) { - if (!/^[A-Za-z0-9-]+$/.test(String(token))) { - throw new Error(`Invalid publication token: ${token}`); - } -} - -function publishManifestPath(entries, token, manifestDir) { - if (!entries.length) throw new Error('Publication bundle is empty'); - validatePublishToken(token); - const directory = manifestDir || path.dirname(entries[0].destination); - ensureDir(directory); - return path.join(directory, `.flyt-publish-${token}.json`); -} - -function validatePublishManifest(manifest, manifestPath) { - if (!manifest || manifest.version !== 1 || !Array.isArray(manifest.entries)) { - throw new Error(`Invalid publication transaction manifest: ${manifestPath}`); - } - if (!['prepared', 'publishing', 'committed'].includes(manifest.state)) { - throw new Error(`Invalid publication transaction state in ${manifestPath}`); - } - manifest.entries.forEach((entry) => { - for (const key of ['destination', 'stage', 'backup']) { - if (typeof entry[key] !== 'string' || !path.isAbsolute(entry[key])) { - throw new Error(`Invalid ${key} in publication transaction ${manifestPath}`); - } - } - if (typeof entry.hadDestination !== 'boolean') { - throw new Error(`Invalid hadDestination in publication transaction ${manifestPath}`); - } - }); - return manifest; -} - -function rollbackPublishManifest(manifest) { - const errors = []; - [...manifest.entries].reverse().forEach((entry) => { - try { - if (entry.hadDestination) { - if (fs.existsSync(entry.backup)) { - safeUnlink(entry.destination); - ensureDir(path.dirname(entry.destination)); - fs.renameSync(entry.backup, entry.destination); - } else if (!fs.existsSync(entry.destination)) { - throw new Error(`Cannot restore missing prior destination: ${entry.destination}`); - } - } else { - safeUnlink(entry.destination); - safeUnlink(entry.backup); - } - safeUnlink(entry.stage); - } catch (error) { - errors.push(error); - } - }); - if (errors.length) { - throw new globalThis.AggregateError(errors, 'Publication transaction rollback failed'); - } -} - -function finalizeCommittedManifest(manifest) { - const missing = manifest.entries.filter((entry) => !fs.existsSync(entry.destination)); - if (missing.length) { - rollbackPublishManifest(manifest); - return; - } - manifest.entries.forEach((entry) => { - safeUnlink(entry.stage); - safeUnlink(entry.backup); - }); -} - -function listFilesRecursive(directory) { - if (!fs.existsSync(directory)) return []; - const files = []; - fs.readdirSync(directory, { withFileTypes: true }).forEach((entry) => { - const fullPath = path.join(directory, entry.name); - if (entry.isDirectory()) files.push(...listFilesRecursive(fullPath)); - else if (entry.isFile()) files.push(fullPath); - }); - return files; -} - -export function publishBundle(entries, token, { faultInjector, manifestDir } = {}) { - const manifestPath = publishManifestPath(entries, token, manifestDir); - const manifest = { - version: 1, - token, - state: 'prepared', - entries: entries.map(({ source, destination }) => ({ - source, - destination: path.resolve(destination), - stage: path.resolve(`${destination}.${token}.new`), - backup: path.resolve(`${destination}.${token}.bak`), - hadDestination: fs.existsSync(destination), - })), - }; - - try { - manifest.entries.forEach((entry) => { - if (!fs.existsSync(entry.source)) throw new Error(`Missing publish source: ${entry.source}`); - ensureDir(path.dirname(entry.destination)); - fs.copyFileSync(entry.source, entry.stage); - }); - writeJson(manifestPath, manifest); - faultInjector?.('prepared', -1); - - manifest.entries.forEach((entry, index) => { - if (entry.hadDestination) fs.renameSync(entry.destination, entry.backup); - faultInjector?.('backed-up', index); - }); - manifest.state = 'publishing'; - writeJson(manifestPath, manifest); - - manifest.entries.forEach((entry, index) => { - fs.renameSync(entry.stage, entry.destination); - faultInjector?.('published', index); - }); - manifest.state = 'committed'; - writeJson(manifestPath, manifest); - faultInjector?.('committed', -1); - - finalizeCommittedManifest(manifest); - safeUnlink(manifestPath); - } catch (error) { - if (error instanceof SimulatedProcessCrash) throw error; - try { - rollbackPublishManifest(manifest); - safeUnlink(manifestPath); - } catch (rollbackError) { - throw new globalThis.AggregateError([error, rollbackError], 'Publication failed and rollback was incomplete'); - } - throw error; - } -} - -export function recoverPublishArtifacts(directory) { - if (!fs.existsSync(directory)) return; - const manifests = fs.readdirSync(directory) - .filter((name) => /^\.flyt-publish-[A-Za-z0-9-]+\.json$/.test(name)) - .map((name) => path.join(directory, name)); - - manifests.forEach((manifestPath) => { - const manifest = validatePublishManifest(readJson(manifestPath, null), manifestPath); - if (manifest.state === 'committed') finalizeCommittedManifest(manifest); - else rollbackPublishManifest(manifest); - safeUnlink(manifestPath); - }); - - const artifacts = listFilesRecursive(directory); - const orphanBackups = artifacts.filter((filePath) => filePath.endsWith('.bak')); - if (orphanBackups.length) { - throw new Error( - `Unrecoverable publication backups without a transaction manifest: ${orphanBackups.join(', ')}`, - ); - } - artifacts.filter((filePath) => filePath.endsWith('.new')).forEach(safeUnlink); -}''' - -utils = replace_between( - utils, - 'export function publishBundle', - 'export function buildTrackerArgs', - publication_block, - 'server-utils publication block', -) - -history_block = r'''export function readCsvAvgProximity(csvPath) { - if (!fs.existsSync(csvPath)) return null; - const lines = fs.readFileSync(csvPath, 'utf8').trim().split(/\r?\n/); - if (lines.length < 2) return null; - const headers = lines[0].split(','); - const proximityIndex = headers.indexOf('proximity_distance'); - const occlusionIndex = headers.indexOf('occlusion_flag'); - const trackingValidIndex = headers.indexOf('tracking_valid'); - const detectionCountIndex = headers.indexOf('detection_count'); - const fly1AreaIndex = headers.indexOf('fly1_area'); - const fly2AreaIndex = headers.indexOf('fly2_area'); - if (proximityIndex < 0) return null; - - let total = 0; - let count = 0; - lines.slice(1).forEach((line) => { - const columns = line.split(','); - const rawProximity = columns[proximityIndex]?.trim(); - if (!rawProximity) return; - const proximity = Number(rawProximity); - const occluded = occlusionIndex >= 0 && Number(columns[occlusionIndex]) !== 0; - let trackingValid = true; - if (trackingValidIndex >= 0) { - trackingValid = Number(columns[trackingValidIndex]) === 1; - } else if (detectionCountIndex >= 0) { - trackingValid = Number(columns[detectionCountIndex]) >= 2; - } else if (fly1AreaIndex >= 0 && fly2AreaIndex >= 0) { - trackingValid = Number(columns[fly1AreaIndex]) > 0 && Number(columns[fly2AreaIndex]) > 0; - } - if (Number.isFinite(proximity) && trackingValid && !occluded) { - total += proximity; - count += 1; - } - }); - return count ? Math.round((total / count) * 100) / 100 : null; -} - -export function countCourtshipBouts(eventsPath) { - const data = readJson(eventsPath, { events: [] }); - return Array.isArray(data.events) - ? data.events.filter((event) => event.type === 'courtship_bout').length - : 0; -} - -const RUN_ID_PATTERN = /^run-[A-Za-z0-9][A-Za-z0-9-]{2,127}$/; - -function requireRunId(runId) { - if (!RUN_ID_PATTERN.test(String(runId))) throw new Error(`Invalid run ID: ${runId}`); - return String(runId); -} - -export function createRunId() { - return `run-${Date.now()}-${randomUUID().slice(0, 8)}`; -} - -export function scopeEventsForRun(eventsPath, runId, destinationPath = eventsPath) { - const safeRunId = requireRunId(runId); - const prefix = `${safeRunId}:`; - const data = readJson(eventsPath, { version: 1, events: [] }); - data.run_id = safeRunId; - data.events = Array.isArray(data.events) ? data.events.map((event) => { - const currentId = String(event.id || ''); - const originalId = event.original_id - || (currentId.startsWith(prefix) ? currentId.slice(prefix.length) : currentId); - if (!originalId) throw new Error('Event ID missing while scoping run events'); - return { - ...event, - original_id: originalId, - id: `${safeRunId}:${originalId}`, - }; - }) : []; - writeJson(destinationPath, data); - return data; -} - diff --git a/.github/scripts/final-hardening.part01 b/.github/scripts/final-hardening.part01 deleted file mode 100644 index b11f8c8..0000000 --- a/.github/scripts/final-hardening.part01 +++ /dev/null @@ -1,280 +0,0 @@ -export function scopeEventsForHistory(eventsPath, runId, destinationPath) { - return scopeEventsForRun(eventsPath, runId, destinationPath); -} - -export function prepareRunHistoryBundle({ - runId, - historyDir, - historyMetaPath, - historyIndexOutput, - filename, - durationSec, - fps, - totalFrames, - csvSrc, - eventsSrc, - verificationSrc, -}) { - const safeRunId = requireRunId(runId); - for (const source of [csvSrc, eventsSrc, verificationSrc]) { - if (!fs.existsSync(source)) throw new Error(`Missing history source: ${source}`); - } - const history = readJson(historyMetaPath, { version: 1, runs: [] }); - if (!Array.isArray(history.runs)) history.runs = []; - if (history.runs.some((run) => run.runId === safeRunId)) { - throw new Error(`History run already exists: ${safeRunId}`); - } - const meta = { - runId: safeRunId, - timestamp: new Date().toISOString(), - filename: filename || 'unknown', - durationSec: Math.round((durationSec || 0) * 10) / 10, - fps: fps || null, - totalFrames: totalFrames || null, - avgProximity: readCsvAvgProximity(csvSrc), - detectedBouts: countCourtshipBouts(eventsSrc), - }; - history.runs.unshift(meta); - writeJson(historyIndexOutput, history); - - const runDir = path.join(historyDir, safeRunId); - return { - meta, - entries: [ - { source: csvSrc, destination: path.join(runDir, 'data.csv') }, - { source: eventsSrc, destination: path.join(runDir, 'events.json') }, - { source: verificationSrc, destination: path.join(runDir, 'verification.json') }, - { source: historyIndexOutput, destination: historyMetaPath }, - ], - }; -} - -export function verificationPathForRun(runId, historyDir, { mustExist = true } = {}) { - const safeRunId = requireRunId(runId); - const root = path.resolve(historyDir); - const filePath = path.resolve(root, safeRunId, 'verification.json'); - if (!filePath.startsWith(`${root}${path.sep}`)) throw new Error('Verification path escaped history root'); - if (mustExist && !fs.existsSync(filePath)) throw new Error('Run verification file not found'); - return filePath; -} - -export function resolveVerificationPath(eventId, historyDir) { - const separator = String(eventId).indexOf(':'); - if (separator <= 0 || separator === String(eventId).length - 1) { - throw new Error('Run-scoped event ID required'); - } - return verificationPathForRun(String(eventId).slice(0, separator), historyDir); -}''' - -utils = replace_between( - utils, - 'export function readCsvAvgProximity', - # replace through EOF; marker is the old final resolver - 'export function resolveVerificationPath', - history_block, - 'server-utils history block prefix', -) -# replace_between preserved the old resolver because it starts at the end marker. -old_resolver_start = utils.find('export function resolveVerificationPath', utils.find(history_block[:30]) + len(history_block)) -if old_resolver_start >= 0: - utils = utils[:old_resolver_start].rstrip() + '\n' - -utils_path.write_text(utils, encoding='utf-8') - -# --------------------------------------------------------------------------- -# Server: durable run ID, atomic current+history publication, canonical reviews. -# --------------------------------------------------------------------------- -server_path = root / 'source app folder/dashboard/server.js' -server = server_path.read_text(encoding='utf-8') -server = replace_once(server, ' buildTrackerArgs,\n', ' buildTrackerArgs,\n createRunId,\n', 'server createRunId import') -server = replace_once( - server, - ' parseTrackerSync,\n publishBundle,\n', - ' parseTrackerSync,\n prepareRunHistoryBundle,\n publishBundle,\n', - 'server prepare history import', -) -server = replace_once(server, ' resolveVerificationPath,\n', ' resolveVerificationPath,\n scopeEventsForRun,\n', 'server scope import') -server = replace_once(server, ' snapshotRunToHistory,\n', ' verificationPathForRun,\n', 'server verification import') -server = replace_once(server, ' let runSequence = 0;\n', '', 'server run sequence removal') -server = replace_once( - server, - " publish: options.services?.publish || ((entries, token) => publishBundle(entries, token)),\n snapshot: options.services?.snapshot || snapshotRunToHistory,\n", - " publish: options.services?.publish || ((entries, token) => publishBundle(\n entries, token, { manifestDir: publicDir },\n )),\n prepareHistory: options.services?.prepareHistory || prepareRunHistoryBundle,\n", - 'server services', -) -server = replace_once( - server, - ' function readVerification(filePath = verificationPath) {\n', - ' function readVerification(filePath) {\n', - 'server read verification signature', -) -server = replace_once( - server, - " function createRunContext(inputPath, filename, overrides) {\n const runId = ++runSequence;\n", - " function createRunContext(inputPath, filename, overrides) {\n const runId = createRunId();\n", - 'server durable run id', -) -server = replace_once( - server, - " verification: path.join(context.workDir, 'verification.json'),\n", - " verification: path.join(context.workDir, 'verification.json'),\n historyIndex: path.join(context.workDir, 'history.json'),\n", - 'server history output', -) -server = replace_once( - server, - " const fps = readEventsFps(outputs.events);\n const metadata = {\n ...integrity,\n expectedVideoFrames: syncInfo.expectedVideoFrames,\n fps,\n timestamp: new Date().toISOString(),\n };\n writeJson(outputs.metadata, metadata);\n writeJson(outputs.verification, { version: 1, reviews: [] });\n ensureCurrent();\n\n job.progress = 'Publishing validated results...';\n services.publish([\n { source: outputs.csv, destination: path.join(publicDir, 'data.csv') },\n { source: outputs.events, destination: path.join(publicDir, 'events.json') },\n { source: outputs.browserVideo, destination: path.join(publicDir, 'tracked.mp4') },\n { source: outputs.metadata, destination: path.join(publicDir, 'run_metadata.json') },\n { source: outputs.verification, destination: verificationPath },\n ], context.token);\n ensureCurrent();\n\n try {\n services.snapshot({\n historyDir,\n historyMetaPath,\n filename: context.filename,\n durationSec: (Date.now() - context.startedAt) / 1000,\n fps,\n totalFrames: integrity.inputFrames,\n csvSrc: outputs.csv,\n eventsSrc: outputs.events,\n });\n } catch (historyError) {\n console.error(`[Server] History snapshot failed: ${historyError.message}`);\n }\n", - " const fps = readEventsFps(outputs.events);\n scopeEventsForRun(outputs.events, context.runId, outputs.events);\n const metadata = {\n ...integrity,\n runId: context.runId,\n expectedVideoFrames: syncInfo.expectedVideoFrames,\n fps,\n timestamp: new Date().toISOString(),\n };\n writeJson(outputs.metadata, metadata);\n writeJson(outputs.verification, { version: 1, run_id: context.runId, reviews: [] });\n const historyBundle = services.prepareHistory({\n runId: context.runId,\n historyDir,\n historyMetaPath,\n historyIndexOutput: outputs.historyIndex,\n filename: context.filename,\n durationSec: (Date.now() - context.startedAt) / 1000,\n fps,\n totalFrames: integrity.inputFrames,\n csvSrc: outputs.csv,\n eventsSrc: outputs.events,\n verificationSrc: outputs.verification,\n });\n ensureCurrent();\n\n job.progress = 'Publishing validated current and historical results...';\n services.publish([\n { source: outputs.csv, destination: path.join(publicDir, 'data.csv') },\n { source: outputs.events, destination: path.join(publicDir, 'events.json') },\n { source: outputs.browserVideo, destination: path.join(publicDir, 'tracked.mp4') },\n { source: outputs.metadata, destination: path.join(publicDir, 'run_metadata.json') },\n ...historyBundle.entries,\n ], context.token);\n ensureCurrent();\n", - 'server atomic history publication', -) - -old_routes = r''' app.get('/api/verification', (req, res) => { - if (!req.query.runId) return res.json(readVerification()); - const history = readJson(historyMetaPath, { runs: [] }); - const known = history.runs?.some((run) => run.runId === req.query.runId); - if (!known) return res.status(404).json({ error: 'Run not found' }); - return res.json(readVerification(path.join(historyDir, req.query.runId, 'verification.json'))); - }); - - app.post('/api/verification', (req, res) => { - const { eventId, verdict } = req.body || {}; - if (!eventId || !['confirmed', 'rejected'].includes(verdict)) { - return res.status(400).json({ error: 'Body must include eventId and verdict.' }); - } - try { - const filePath = resolveVerificationPath( - eventId, - verificationPath, - historyDir, - historyMetaPath, - ); - const data = readVerification(filePath); - const review = { event_id: eventId, verdict, reviewed_at: new Date().toISOString() }; - const index = data.reviews.findIndex((item) => item.event_id === eventId); - if (index >= 0) data.reviews[index] = review; - else data.reviews.push(review); - writeJson(filePath, data); - return res.json({ success: true, review }); - } catch (error) { - return res.status(400).json({ error: error.message }); - } - }); - - app.post('/api/verification/reset', (_req, res) => { - writeJson(verificationPath, { version: 1, reviews: [] }); - res.json({ success: true }); - });''' - -new_routes = r''' function currentRunId() { - return readJson(path.join(publicDir, 'run_metadata.json'), null)?.runId || null; - } - - app.get('/api/verification', (req, res) => { - const runId = req.query.runId || currentRunId(); - if (!runId) return res.status(404).json({ error: 'No current run found' }); - try { - return res.json(readVerification(verificationPathForRun(runId, historyDir))); - } catch (error) { - return res.status(404).json({ error: error.message }); - } - }); - - app.post('/api/verification', (req, res) => { - const { eventId, verdict } = req.body || {}; - if (!eventId || !['confirmed', 'rejected'].includes(verdict)) { - return res.status(400).json({ error: 'Body must include eventId and verdict.' }); - } - try { - const filePath = resolveVerificationPath(eventId, historyDir); - const data = readVerification(filePath); - const review = { event_id: eventId, verdict, reviewed_at: new Date().toISOString() }; - const index = data.reviews.findIndex((item) => item.event_id === eventId); - if (index >= 0) data.reviews[index] = review; - else data.reviews.push(review); - writeJson(filePath, data); - return res.json({ success: true, review }); - } catch (error) { - return res.status(400).json({ error: error.message }); - } - }); - - app.post('/api/verification/reset', (req, res) => { - const runId = req.body?.runId || currentRunId(); - if (!runId) return res.status(404).json({ error: 'No current run found' }); - try { - writeJson(verificationPathForRun(runId, historyDir), { version: 1, run_id: runId, reviews: [] }); - return res.json({ success: true }); - } catch (error) { - return res.status(404).json({ error: error.message }); - } - });''' -server = replace_once(server, old_routes, new_routes, 'server canonical verification routes') -server_path.write_text(server, encoding='utf-8') - -# --------------------------------------------------------------------------- -# Tracker: missing observations are explicit and OpenCV count is diagnostic. -# --------------------------------------------------------------------------- -tracker_path = root / 'source app folder/tracker/tracker.py' -tracker = tracker_path.read_text(encoding='utf-8') -tracker = replace_once( - tracker, - ' proximities = [value_or(row, "proximity_distance", 0.0) for row in segment]\n', - ' proximities = [\n value_or(row, "proximity_distance", math.nan) for row in segment\n if math.isfinite(value_or(row, "proximity_distance", math.nan))\n ]\n', - 'tracker finite event proximities', -) -tracker = replace_once( - tracker, - ' fly2_present = value_or(row, "fly2_area", 0.0) > 0.0\n return (\n math.isfinite(proximity)\n', - ' fly2_present = value_or(row, "fly2_area", 0.0) > 0.0\n tracking_valid = value_or(row, "tracking_valid", 1.0) != 0.0\n return (\n tracking_valid\n and math.isfinite(proximity)\n', - 'tracker courtship tracking validity', -) -tracker = replace_once( - tracker, - 'def is_low_confidence_frame(row: dict[str, Any]) -> bool:\n return (\n value_or(row, "occlusion_flag", 0.0) != 0.0\n', - 'def is_low_confidence_frame(row: dict[str, Any]) -> bool:\n return (\n value_or(row, "tracking_valid", 1.0) == 0.0\n or value_or(row, "occlusion_flag", 0.0) != 0.0\n', - 'tracker low confidence tracking validity', -) -tracker = replace_once( - tracker, - ''' expected_sync_ok = ( - expected_frame_count <= 0 or frames_processed == expected_frame_count - ) - return internal_sync_ok and expected_sync_ok -''', - ''' # OpenCV CAP_PROP_FRAME_COUNT is diagnostic only. The server performs the - # authoritative full decode with ffmpeg and compares it to tracker/CSV/video output. - _ = expected_frame_count - return internal_sync_ok -''', - 'tracker diagnostic OpenCV frame count', -) -tracker = replace_once( - tracker, - ' expected_frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))\n', - ' expected_frame_count = max(0, int(cap.get(cv2.CAP_PROP_FRAME_COUNT)))\n', - 'tracker nonnegative expected count', -) -tracker = replace_once( - tracker, - ''' proximity = 0.0 - occlusion_flag = 0 - identity_confidence = 0.0 - fly1_area = fly2_area = 0.0 -''', - ''' proximity = math.nan - occlusion_flag = 0 - identity_confidence = 0.0 - fly1_area = fly2_area = 0.0 - detection_count = len(centroids) - tracking_valid = 0 -''', - 'tracker invalid observation defaults', -) -tracker = replace_once( - tracker, - ' proximity = float(math.dist(f1_coords, f2_coords))\n prev_f1, prev_f2 = f1_coords, f2_coords\n', - ' proximity = float(math.dist(f1_coords, f2_coords))\n tracking_valid = 1\n prev_f1, prev_f2 = f1_coords, f2_coords\n', - 'tracker valid observation', -) -tracker = replace_once( - tracker, - ''' elif is_initialized: diff --git a/.github/scripts/final-hardening.part02 b/.github/scripts/final-hardening.part02 deleted file mode 100644 index 6cda3b3..0000000 --- a/.github/scripts/final-hardening.part02 +++ /dev/null @@ -1,280 +0,0 @@ - f1_coords, f2_coords = prev_f1, prev_f2 - proximity = float(math.dist(f1_coords, f2_coords)) - identity_confidence = 0.0 -''', - ''' elif is_initialized: - # Coordinates are retained only for display continuity. Proximity is - # deliberately missing because no two-fly observation occurred. - f1_coords, f2_coords = prev_f1, prev_f2 - identity_confidence = 0.0 -''', - 'tracker dropout proximity', -) -tracker = replace_once( - tracker, - ' "proximity_distance": proximity,\n "occlusion_flag": occlusion_flag,\n', - ' "proximity_distance": proximity,\n "tracking_valid": tracking_valid,\n "detection_count": detection_count,\n "occlusion_flag": occlusion_flag,\n', - 'tracker validity columns', -) -tracker = replace_once( - tracker, - ' "Error: frame integrity mismatch between decoded input, tracker loop, and CSV",\n', - ' "Error: frame integrity mismatch between tracker loop and CSV",\n', - 'tracker sync error text', -) -tracker_path.write_text(tracker, encoding='utf-8') - -# --------------------------------------------------------------------------- -# Frontend metric helpers and usage. -# --------------------------------------------------------------------------- -metrics_path = root / 'source app folder/dashboard/src/metrics.js' -metrics_path.write_text(r'''export function proximityValue(row) { - if (!row) return null; - const rawProximity = row.proximity_distance; - if (rawProximity === null || rawProximity === undefined || rawProximity === '') return null; - const proximity = Number(rawProximity); - if (!Number.isFinite(proximity)) return null; - if (Number(row.occlusion_flag ?? 0) !== 0) return null; - - if (row.tracking_valid !== null && row.tracking_valid !== undefined && row.tracking_valid !== '') { - if (Number(row.tracking_valid) !== 1) return null; - } else if (row.detection_count !== null && row.detection_count !== undefined && row.detection_count !== '') { - if (Number(row.detection_count) < 2) return null; - } else if ( - row.fly1_area !== null && row.fly1_area !== undefined - && row.fly2_area !== null && row.fly2_area !== undefined - ) { - if (!(Number(row.fly1_area) > 0 && Number(row.fly2_area) > 0)) return null; - } - - return proximity; -} - -export function computeAverageProximity(rows) { - const values = rows.map(proximityValue).filter((value) => value !== null); - if (!values.length) return 0; - return Math.round(values.reduce((sum, value) => sum + value, 0) / values.length); -} - -export function prismDistance(row) { - return proximityValue(row) ?? ''; -} -''', encoding='utf-8') - -app_path = root / 'source app folder/dashboard/src/App.jsx' -app = app_path.read_text(encoding='utf-8') -app = replace_once( - app, - "import Papa from 'papaparse';\n", - "import Papa from 'papaparse';\nimport { prismDistance, proximityValue } from './metrics.js';\n", - 'App metric import', -) -app = replace_once( - app, - ' const dist = Number(r.proximity_distance ?? 0);\n', - ' const dist = prismDistance(r);\n', - 'App Prism missing distance', -) -app = replace_once( - app, - ''' // Average proximity only over frames where flies are separate (exclude merged/occluded where proximity=0 by design) - if (row.proximity_distance != null && !row.occlusion_flag) { - totalProx += row.proximity_distance; - proxCount++; - } -''', - ''' // Only measured two-fly observations contribute. Dropouts retain - // display coordinates but carry no scientifically valid proximity. - const observedProximity = proximityValue(row); - if (observedProximity !== null) { - totalProx += observedProximity; - proxCount++; - } -''', - 'App valid proximity average', -) -app_path.write_text(app, encoding='utf-8') - -# --------------------------------------------------------------------------- -# Regression tests: crash lifecycle, durable verdicts, and metric validity. -# --------------------------------------------------------------------------- -utils_test_path = root / 'source app folder/dashboard/tests/server-utils.test.js' -utils_test = utils_test_path.read_text(encoding='utf-8') -utils_test = replace_once(utils_test, "import { EventEmitter } from 'node:events';\n", "import { EventEmitter } from 'node:events';\n", 'utils test stable import') -utils_test = replace_once( - utils_test, - ''' buildTrackerArgs, - parseTrackerSync, - publishBundle, - recoverPublishArtifacts, - resolveVerificationPath, - runCommand, - scopeEventsForHistory, - snapshotRunToHistory, - terminateChild, - validateFrameIntegrity, -''', - ''' buildTrackerArgs, - parseTrackerSync, - prepareRunHistoryBundle, - publishBundle, - readCsvAvgProximity, - recoverPublishArtifacts, - resolveVerificationPath, - runCommand, - scopeEventsForRun, - SimulatedProcessCrash, - terminateChild, - validateFrameIntegrity, - verificationPathForRun, -''', - 'utils test imports', -) -utils_test = replace_once( - utils_test, - ''' assert.throws(() => validateFrameIntegrity({ - ...evidence, - syncInfo: { ...evidence.syncInfo, expectedVideoFrames: 11 }, - }), /mismatch/); -''', - ''' const diagnosticMismatch = validateFrameIntegrity({ - ...evidence, - syncInfo: { ...evidence.syncInfo, expectedVideoFrames: 11 }, - }); - assert.equal(diagnosticMismatch.syncOk, true); - assert.equal(diagnosticMismatch.expectedMetadataMatches, false); -''', - 'utils test diagnostic frame count', -) - -old_recovery_test = r'''test('recovers orphaned publication artifacts after a crash', () => { - const dir = tempDir('flyt-recover-'); - fs.writeFileSync(path.join(dir, 'data.csv.token.bak'), 'old-data'); - fs.writeFileSync(path.join(dir, 'events.json.token.new'), 'new-events'); - recoverPublishArtifacts(dir); - assert.equal(fs.readFileSync(path.join(dir, 'data.csv'), 'utf8'), 'old-data'); - assert.equal(fs.existsSync(path.join(dir, 'events.json.token.new')), false); -});''' -new_recovery_tests = r'''test('crash recovery rolls back the entire partially published bundle', () => { - const dir = tempDir('flyt-crash-rollback-'); - const sourceA = path.join(dir, 'source-a'); - const sourceB = path.join(dir, 'source-b'); - const destinationA = path.join(dir, 'a'); - const destinationB = path.join(dir, 'b'); - fs.writeFileSync(sourceA, 'new-a'); - fs.writeFileSync(sourceB, 'new-b'); - fs.writeFileSync(destinationA, 'old-a'); - fs.writeFileSync(destinationB, 'old-b'); - - assert.throws(() => publishBundle([ - { source: sourceA, destination: destinationA }, - { source: sourceB, destination: destinationB }, - ], 'crash-token', { - manifestDir: dir, - faultInjector(stage, index) { - if (stage === 'published' && index === 0) throw new SimulatedProcessCrash(); - }, - }), SimulatedProcessCrash); - - assert.equal(fs.readFileSync(destinationA, 'utf8'), 'new-a'); - assert.equal(fs.existsSync(destinationB), false); - recoverPublishArtifacts(dir); - assert.equal(fs.readFileSync(destinationA, 'utf8'), 'old-a'); - assert.equal(fs.readFileSync(destinationB, 'utf8'), 'old-b'); - assert.equal(fs.readdirSync(dir).some((name) => /flyt-publish|\.(bak|new)$/.test(name)), false); -}); - -test('crash recovery finalizes an explicitly committed all-new bundle', () => { - const dir = tempDir('flyt-crash-commit-'); - const source = path.join(dir, 'source'); - const destination = path.join(dir, 'destination'); - fs.writeFileSync(source, 'new'); - fs.writeFileSync(destination, 'old'); - - assert.throws(() => publishBundle([ - { source, destination }, - ], 'commit-token', { - manifestDir: dir, - faultInjector(stage) { - if (stage === 'committed') throw new SimulatedProcessCrash(); - }, - }), SimulatedProcessCrash); - - recoverPublishArtifacts(dir); - assert.equal(fs.readFileSync(destination, 'utf8'), 'new'); - assert.equal(fs.readdirSync(dir).some((name) => /flyt-publish|\.(bak|new)$/.test(name)), false); -}); - -test('legacy orphan backups fail closed instead of reconstructing a mixed bundle', () => { - const dir = tempDir('flyt-orphan-backup-'); - const backup = path.join(dir, 'data.csv.token.bak'); - fs.writeFileSync(backup, 'old-data'); - assert.throws(() => recoverPublishArtifacts(dir), /without a transaction manifest/); - assert.equal(fs.readFileSync(backup, 'utf8'), 'old-data'); -});''' -utils_test = replace_once(utils_test, old_recovery_test, new_recovery_tests, 'utils crash recovery tests') - -history_start = utils_test.index("test('history snapshots preserve dashboard summary fields and scoped reviews'") -history_end = utils_test.index("test('termination escalates to SIGKILL", history_start) -new_history_tests = r'''test('history bundle uses scoped events and excludes invalid proximity observations', () => { - const dir = tempDir('flyt-history-'); - const historyDir = path.join(dir, 'history'); - const historyMetaPath = path.join(dir, 'history.json'); - const historyIndexOutput = path.join(dir, 'next-history.json'); - const csv = path.join(dir, 'data.csv'); - const events = path.join(dir, 'events.json'); - const verification = path.join(dir, 'verification.json'); - const runId = 'run-1234-abcd'; - fs.writeFileSync(csv, [ - 'frame,proximity_distance,occlusion_flag,tracking_valid,detection_count', - '0,10,0,1,2', - '1,10,0,0,0', - '2,,1,0,1', - '3,30,0,1,2', - ].join('\n')); - fs.writeFileSync(events, JSON.stringify({ - events: [{ id: 'evt-001', type: 'courtship_bout' }], - })); - fs.writeFileSync(verification, JSON.stringify({ version: 1, run_id: runId, reviews: [] })); - scopeEventsForRun(events, runId, events); - - const bundle = prepareRunHistoryBundle({ - runId, - historyDir, - historyMetaPath, - historyIndexOutput, - filename: 'flies.mp4', - durationSec: 2.3, - fps: 30, - totalFrames: 4, - csvSrc: csv, - eventsSrc: events, - verificationSrc: verification, - }); - assert.equal(bundle.meta.avgProximity, 20); - assert.equal(bundle.meta.detectedBouts, 1); - assert.equal(readCsvAvgProximity(csv), 20); - publishBundle(bundle.entries, 'history-token', { manifestDir: dir }); - - const scopedEvents = JSON.parse(fs.readFileSync(path.join(historyDir, runId, 'events.json'))); - assert.equal(scopedEvents.events[0].id, `${runId}:evt-001`); - assert.equal( - resolveVerificationPath(`${runId}:evt-001`, historyDir), - path.join(historyDir, runId, 'verification.json'), - ); - assert.equal(verificationPathForRun(runId, historyDir), path.join(historyDir, runId, 'verification.json')); - assert.throws(() => resolveVerificationPath('evt-001', historyDir), /Run-scoped event ID required/); -}); - -''' -utils_test = utils_test[:history_start] + new_history_tests + utils_test[history_end:] -utils_test_path.write_text(utils_test, encoding='utf-8') - -integration_path = root / 'source app folder/dashboard/tests/server-integration.test.js' -integration = integration_path.read_text(encoding='utf-8') -integration = replace_once( - integration, - " 'frame,proximity_distance,occlusion_flag',\n ...Array.from({ length: frames }, (_, index) => `${index},20,0`),\n", - " 'frame,proximity_distance,occlusion_flag,tracking_valid,detection_count,fly1_area,fly2_area',\n ...Array.from({ length: frames }, (_, index) => `${index},20,0,1,2,100,100`),\n", - 'integration valid CSV', -) diff --git a/.github/scripts/final-hardening.part03 b/.github/scripts/final-hardening.part03 deleted file mode 100644 index 5b2675a..0000000 --- a/.github/scripts/final-hardening.part03 +++ /dev/null @@ -1,280 +0,0 @@ -integration = replace_once(integration, ' snapshot: () => null,\n', '', 'integration snapshot service removal') -integration = replace_once( - integration, - ''' assert.equal(metadata.finalVideoFrames, 4); - assert.equal(metadata.syncOk, true); -}); -''', - ''' assert.equal(metadata.finalVideoFrames, 4); - assert.equal(metadata.syncOk, true); - assert.match(metadata.runId, /^run-/); - const currentEvents = JSON.parse(fs.readFileSync(path.join(harness.paths.publicDir, 'events.json'))); - assert.equal(currentEvents.events[0].id, `${metadata.runId}:evt-001`); -}); -''', - 'integration scoped current events', -) - -lifecycle_test = r''' - -test('a verdict made on the current run survives subsequent runs and history reload', async (t) => { - const harness = await createHarness(standardServices()); - t.after(() => harness.close()); - - assert.equal((await upload(harness.baseUrl, 'first.mp4')).status, 200); - const firstStatus = await waitForStatus(harness.baseUrl, 'done'); - const firstRunId = firstStatus.runId; - const eventsResponse = await fetch(`${harness.baseUrl}/api/events`); - const firstEvents = await eventsResponse.json(); - const eventId = firstEvents.events[0].id; - assert.equal(eventId, `${firstRunId}:evt-001`); - - const verdictResponse = await fetch(`${harness.baseUrl}/api/verification`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ eventId, verdict: 'confirmed' }), - }); - assert.equal(verdictResponse.status, 200); - - const unscopedResponse = await fetch(`${harness.baseUrl}/api/verification`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ eventId: 'evt-001', verdict: 'confirmed' }), - }); - assert.equal(unscopedResponse.status, 400); - - assert.equal((await upload(harness.baseUrl, 'second.mp4')).status, 200); - const secondStatus = await waitForStatus(harness.baseUrl, 'done'); - assert.notEqual(secondStatus.runId, firstRunId); - - const historicResponse = await fetch( - `${harness.baseUrl}/api/verification?runId=${encodeURIComponent(firstRunId)}`, - ); - assert.equal(historicResponse.status, 200); - const historicVerification = await historicResponse.json(); - assert.deepEqual(historicVerification.reviews.map((review) => ({ - event_id: review.event_id, - verdict: review.verdict, - })), [{ event_id: eventId, verdict: 'confirmed' }]); - - const canonical = JSON.parse(fs.readFileSync( - path.join(harness.paths.historyDir, firstRunId, 'verification.json'), - )); - assert.equal(canonical.reviews[0].verdict, 'confirmed'); -}); -''' -integration += lifecycle_test -integration_path.write_text(integration, encoding='utf-8') - -metrics_test_path = root / 'source app folder/dashboard/tests/metrics.test.js' -metrics_test_path.write_text(r'''import assert from 'node:assert/strict'; -import test from 'node:test'; -import { computeAverageProximity, prismDistance, proximityValue } from '../src/metrics.js'; - -test('carried coordinates from missing detections do not bias proximity metrics', () => { - const rows = [ - { proximity_distance: 10, tracking_valid: 1, detection_count: 2, occlusion_flag: 0 }, - { proximity_distance: 10, tracking_valid: 0, detection_count: 0, occlusion_flag: 0 }, - { proximity_distance: null, tracking_valid: 0, detection_count: 1, occlusion_flag: 1 }, - { proximity_distance: 30, tracking_valid: 1, detection_count: 2, occlusion_flag: 0 }, - ]; - assert.equal(computeAverageProximity(rows), 20); - assert.equal(proximityValue(rows[1]), null); - assert.equal(prismDistance(rows[1]), ''); -}); - -test('legacy rows remain valid only when they contain a finite non-occluded observation', () => { - assert.equal(proximityValue({ proximity_distance: 12, occlusion_flag: 0 }), 12); - assert.equal(proximityValue({ proximity_distance: '', occlusion_flag: 0 }), null); - assert.equal(proximityValue({ proximity_distance: 0, occlusion_flag: 1 }), null); -}); -''', encoding='utf-8') - -frontend_contract_path = root / 'source app folder/dashboard/tests/frontend-contract.test.js' -frontend_contract = frontend_contract_path.read_text(encoding='utf-8') -frontend_contract += r''' - -test('dashboard and Prism export use validity-aware proximity helpers', () => { - const source = appSource(); - assert.match(source, /proximityValue\(row\)/); - assert.match(source, /prismDistance\(r\)/); - assert.doesNotMatch(source, /Number\(r\.proximity_distance \?\? 0\)/); -}); -''' -frontend_contract_path.write_text(frontend_contract, encoding='utf-8') - -tracker_test_path = root / 'source app folder/tracker/tests/test_tracker.py' -tracker_test = tracker_test_path.read_text(encoding='utf-8') -tracker_test = replace_once( - tracker_test, - 'def row(frame, proximity, confidence=1.0, occlusion=0, area=100.0):\n', - 'def row(frame, proximity, confidence=1.0, occlusion=0, area=100.0, tracking_valid=None):\n', - 'tracker test row signature', -) -tracker_test = replace_once( - tracker_test, - ''' return { - "frame": frame, -''', - ''' if tracking_valid is None: - tracking_valid = int(occlusion == 0 and area > 0) - return { - "frame": frame, -''', - 'tracker test row validity', -) -tracker_test = replace_once( - tracker_test, - ' "fly2_area": area,\n', - ' "fly2_area": area,\n "tracking_valid": tracking_valid,\n', - 'tracker test validity field', -) -tracker_test = replace_once( - tracker_test, - ''' def test_frame_sync_requires_expected_input_count_when_available(self): - self.assertTrue(tracker.frame_sync_ok(10, 10, 9, 10)) - self.assertFalse(tracker.frame_sync_ok(8, 8, 7, 10)) - self.assertTrue(tracker.frame_sync_ok(8, 8, 7, 0)) - self.assertFalse(tracker.frame_sync_ok(8, 7, 6, 8)) -''', - ''' def test_frame_sync_treats_opencv_count_as_diagnostic_only(self): - self.assertTrue(tracker.frame_sync_ok(10, 10, 9, 10)) - self.assertTrue(tracker.frame_sync_ok(8, 8, 7, 10)) - self.assertTrue(tracker.frame_sync_ok(8, 8, 7, 0)) - self.assertFalse(tracker.frame_sync_ok(8, 7, 6, 8)) - - def test_invalid_tracking_observation_cannot_be_courtship(self): - invalid = row(0, proximity=10, confidence=1.0, tracking_valid=0) - self.assertFalse(tracker.is_courtship_frame(invalid, 60)) - self.assertTrue(tracker.is_low_confidence_frame(invalid)) - - def test_event_mean_proximity_ignores_missing_values(self): - segment = [ - row(0, proximity=20, confidence=0.8), - row(1, proximity=float("nan"), confidence=0.0, tracking_valid=0), - ] - event = tracker.build_event_record( - "evt-001", "low_confidence_segment", 0, 1, 30, segment, "test" - ) - self.assertEqual(event["mean_proximity_px"], 20.0) -''', - 'tracker test frame metadata and invalid metrics', -) -tracker_test_path.write_text(tracker_test, encoding='utf-8') - -# --------------------------------------------------------------------------- -# Frame validation: OpenCV estimate is diagnostic, ffmpeg input is authority. -# --------------------------------------------------------------------------- -utils = utils_path.read_text(encoding='utf-8') -utils = replace_once( - utils, - ''' if (syncInfo.expectedVideoFrames > 0) { - requirePositiveInteger('trackerExpectedFrames', syncInfo.expectedVideoFrames); - counts.trackerExpectedFrames = syncInfo.expectedVideoFrames; - } - - const unique = new Set(Object.values(counts)); -''', - ''' if (!Number.isInteger(syncInfo.expectedVideoFrames) || syncInfo.expectedVideoFrames < 0) { - throw new Error( - `Frame integrity evidence missing or invalid: trackerExpectedFrames=${syncInfo.expectedVideoFrames}`, - ); - } - - const unique = new Set(Object.values(counts)); -''', - 'server diagnostic OpenCV count validation', -) -utils = replace_once( - utils, - ' return { ...counts, syncOk: true };\n', - ''' return { - ...counts, - trackerExpectedFrames: syncInfo.expectedVideoFrames, - expectedMetadataMatches: ( - syncInfo.expectedVideoFrames === 0 || syncInfo.expectedVideoFrames === inputFrames - ), - syncOk: true, - }; -''', - 'server frame validation result', -) -utils_path.write_text(utils, encoding='utf-8') - -# --------------------------------------------------------------------------- -# Permanent Linux + Windows validation. -# --------------------------------------------------------------------------- -ci_path = root / '.github/workflows/critical-fixes-ci.yml' -ci_path.write_text(r'''name: Critical fixes CI - -on: - push: - branches: - - fix/critical-job-races - pull_request: - paths: - - 'source app folder/dashboard/**' - - 'source app folder/tracker/**' - - '.github/workflows/critical-fixes-ci.yml' - -permissions: - contents: read - -jobs: - validate-linux: - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: npm - cache-dependency-path: source app folder/dashboard/package-lock.json - - - name: Install dashboard dependencies - working-directory: source app folder/dashboard - run: npm ci - - - name: Lint backend and regression tests - working-directory: source app folder/dashboard - run: npm run lint:backend - - - name: Run utility, HTTP race, lifecycle, and metric tests - working-directory: source app folder/dashboard - run: npm run test:server - - - name: Build dashboard - working-directory: source app folder/dashboard - run: npm run build - - - uses: actions/setup-python@v5 - with: - python-version: '3.11' - cache: pip - cache-dependency-path: source app folder/tracker/requirements.txt - - - name: Install tracker dependencies - run: python -m pip install -r 'source app folder/tracker/requirements.txt' - - - name: Check Python syntax - run: python -m py_compile 'source app folder/tracker/tracker.py' - - - name: Run tracker scientific regression tests - run: python -m unittest discover -s 'source app folder/tracker/tests' -p 'test_*.py' -v - - validate-windows-node: - runs-on: windows-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: npm - cache-dependency-path: source app folder/dashboard/package-lock.json - - - name: Install dashboard dependencies - working-directory: source app folder/dashboard diff --git a/.github/scripts/final-hardening.part04 b/.github/scripts/final-hardening.part04 deleted file mode 100644 index 5c4e2a1..0000000 --- a/.github/scripts/final-hardening.part04 +++ /dev/null @@ -1,16 +0,0 @@ - run: npm ci - - - name: Lint backend and regression tests - working-directory: source app folder/dashboard - run: npm run lint:backend - - - name: Run Node utility, HTTP, lifecycle, and metric tests - working-directory: source app folder/dashboard - run: npm run test:server - - - name: Build dashboard - working-directory: source app folder/dashboard - run: npm run build -''', encoding='utf-8') - -print('Applied final crash-recovery, run-scoped verification, and metric-integrity hardening.') diff --git a/.github/workflows/apply-final-hardening.yml b/.github/workflows/apply-final-hardening.yml deleted file mode 100644 index e193b0b..0000000 --- a/.github/workflows/apply-final-hardening.yml +++ /dev/null @@ -1,133 +0,0 @@ -name: Apply final hardening - -on: - pull_request: - paths: - - '.github/scripts/final-hardening.part*' - - '.github/workflows/apply-final-hardening.yml' - -permissions: - contents: write - -concurrency: - group: flyt-final-hardening - cancel-in-progress: false - -jobs: - patch-and-validate: - if: github.event.pull_request.head.repo.full_name == github.repository - runs-on: ubuntu-latest - timeout-minutes: 20 - steps: - - uses: actions/checkout@v4 - with: - ref: fix/critical-job-races - fetch-depth: 0 - - - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: npm - cache-dependency-path: source app folder/dashboard/package-lock.json - - - uses: actions/setup-python@v5 - with: - python-version: '3.11' - cache: pip - cache-dependency-path: source app folder/tracker/requirements.txt - - - name: Reconstruct transparent patcher - run: | - cat .github/scripts/final-hardening.part* > /tmp/apply-final-hardening.py - python -m py_compile /tmp/apply-final-hardening.py - - - name: Apply exact final-hardening patch - run: | - set +e - python /tmp/apply-final-hardening.py > .github/final-hardening-diagnostics.txt 2>&1 - status=$? - cat .github/final-hardening-diagnostics.txt - if [ "$status" -ne 0 ]; then - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add .github/final-hardening-diagnostics.txt - git commit -m 'chore: capture final hardening diagnostics' - git push origin HEAD:fix/critical-job-races - exit "$status" - fi - rm .github/final-hardening-diagnostics.txt - - - name: Install dashboard dependencies - working-directory: source app folder/dashboard - run: npm ci - - - name: Lint backend and tests - working-directory: source app folder/dashboard - run: | - set +e - npm run lint:backend > ../../.github/final-hardening-diagnostics.txt 2>&1 - status=$? - cat ../../.github/final-hardening-diagnostics.txt - if [ "$status" -ne 0 ]; then - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add ../../.github/final-hardening-diagnostics.txt - git commit -m 'chore: capture final hardening lint diagnostics' - git push origin HEAD:fix/critical-job-races - exit "$status" - fi - rm ../../.github/final-hardening-diagnostics.txt - - - name: Run utility, HTTP lifecycle, and metric tests - working-directory: source app folder/dashboard - run: npm run test:server - - - name: Build dashboard - working-directory: source app folder/dashboard - run: npm run build - - - name: Install tracker dependencies - run: python -m pip install -r 'source app folder/tracker/requirements.txt' - - - name: Check tracker syntax - run: python -m py_compile 'source app folder/tracker/tracker.py' - - - name: Run tracker regression tests - run: python -m unittest discover -s 'source app folder/tracker/tests' -p 'test_*.py' -v - - - name: Commit validated final hardening - run: | - set -o pipefail - exec > >(tee /tmp/final-hardening-commit.txt) 2>&1 - if [ -z "$(git status --porcelain)" ]; then - echo 'Final hardening already applied; no commit needed.' - exit 0 - fi - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git restore -- '.github/workflows/critical-fixes-ci.yml' - git add -A -- \ - 'source app folder/dashboard/server-utils.js' \ - 'source app folder/dashboard/server.js' \ - 'source app folder/dashboard/src/App.jsx' \ - 'source app folder/dashboard/src/metrics.js' \ - 'source app folder/dashboard/tests/frontend-contract.test.js' \ - 'source app folder/dashboard/tests/metrics.test.js' \ - 'source app folder/dashboard/tests/server-integration.test.js' \ - 'source app folder/dashboard/tests/server-utils.test.js' \ - 'source app folder/tracker/tracker.py' \ - 'source app folder/tracker/tests/test_tracker.py' \ - '.github/final-hardening-diagnostics.txt' - git status --short - git commit -m 'fix: make publication and reviews transactionally durable' - git fetch origin fix/critical-job-races - git rebase origin/fix/critical-job-races - git push origin HEAD:fix/critical-job-races - - - name: Upload commit diagnostics - if: failure() - uses: actions/upload-artifact@v4 - with: - name: final-hardening-commit-diagnostics - path: /tmp/final-hardening-commit.txt - if-no-files-found: ignore From f88a4153ab59a0f0895cb9c499498d864aa5dcaa Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 13:01:52 +0530 Subject: [PATCH 67/75] chore: stage review edge hardening patch --- .github/scripts/review-edge-hardening.part00 | 373 +++++++++++++++++++ 1 file changed, 373 insertions(+) create mode 100644 .github/scripts/review-edge-hardening.part00 diff --git a/.github/scripts/review-edge-hardening.part00 b/.github/scripts/review-edge-hardening.part00 new file mode 100644 index 0000000..0a92514 --- /dev/null +++ b/.github/scripts/review-edge-hardening.part00 @@ -0,0 +1,373 @@ +from pathlib import Path + +ROOT = Path('.') + + +def read(path: str) -> str: + return (ROOT / path).read_text(encoding='utf-8') + + +def write(path: str, content: str) -> None: + (ROOT / path).write_text(content, encoding='utf-8') + + +def replace_once(content: str, old: str, new: str, label: str) -> str: + count = content.count(old) + if count != 1: + raise RuntimeError(f'{label}: expected one occurrence, found {count}') + return content.replace(old, new, 1) + + +# --------------------------------------------------------------------------- +# server-utils.js: strict termination, strict transaction deletion, runtime +# cleanup, and event membership validation. +# --------------------------------------------------------------------------- +utils_path = 'source app folder/dashboard/server-utils.js' +utils = read(utils_path) + +utils = replace_once( + utils, + '''export function safeRemoveDir(dirPath) { + if (!dirPath) return; + try { fs.rmSync(dirPath, { recursive: true, force: true }); } catch (error) { + console.error(`[Server] Failed to remove ${dirPath}: ${error.message}`); + } +} + +export function readJson(filePath, fallback) {''', + '''export function safeRemoveDir(dirPath) { + if (!dirPath) return; + try { fs.rmSync(dirPath, { recursive: true, force: true }); } catch (error) { + console.error(`[Server] Failed to remove ${dirPath}: ${error.message}`); + } +} + +export function removeTransactionalFile(filePath) { + if (!filePath) return; + fs.rmSync(filePath, { force: true }); + if (fs.existsSync(filePath)) { + throw new Error(`Could not remove transactional path: ${filePath}`); + } +} + +export function recoverRuntimeArtifacts(uploadsDir) { + ensureDir(uploadsDir); + fs.readdirSync(uploadsDir, { withFileTypes: true }).forEach((entry) => { + const fullPath = path.join(uploadsDir, entry.name); + if (entry.isFile() && entry.name.startsWith('input-')) safeUnlink(fullPath); + else if (entry.isDirectory() && entry.name.startsWith('run-')) safeRemoveDir(fullPath); + }); +} + +export function readJson(filePath, fallback) {''', + 'insert strict deletion and runtime recovery', +) + +utils = replace_once( + utils, + '''const terminationPromises = new WeakMap(); + +export function terminateChild(child, { graceMs = 1000, hardKillMs = 3000 } = {}) { + if (!child || child.exitCode !== null || child.signalCode) return Promise.resolve(); + if (terminationPromises.has(child)) return terminationPromises.get(child); + + const promise = new Promise((resolve, reject) => { + let settled = false; + let graceTimer; + let hardTimer; + const cleanup = () => { + clearTimeout(graceTimer); + clearTimeout(hardTimer); + child.removeListener('close', finish); + child.removeListener('error', finish); + }; + const finish = () => { + if (settled) return; + settled = true; + cleanup(); + resolve(); + }; + const fail = () => { + if (settled) return; + settled = true; + cleanup(); + reject(new Error('Child process did not terminate after SIGKILL')); + }; + + child.once('close', finish); + child.once('error', finish); + try { child.kill('SIGTERM'); } catch { finish(); return; } + graceTimer = setTimeout(() => { + if (settled) return; + try { child.kill('SIGKILL'); } catch { finish(); return; } + hardTimer = setTimeout(fail, hardKillMs); + hardTimer.unref?.(); + }, graceMs); + graceTimer.unref?.(); + }).finally(() => terminationPromises.delete(child)); + + terminationPromises.set(child, promise); + return promise; +} +''', + '''const terminationPromises = new WeakMap(); + +function childHasExited(child) { + return Boolean(child) && ( + (child.exitCode !== null && child.exitCode !== undefined) + || Boolean(child.signalCode) + ); +} + +export function terminateChild(child, { graceMs = 1000, hardKillMs = 3000 } = {}) { + if (!child || childHasExited(child)) return Promise.resolve(); + if (terminationPromises.has(child)) return terminationPromises.get(child); + + const promise = new Promise((resolve, reject) => { + let settled = false; + let graceTimer; + let hardTimer; + const cleanup = () => { + clearTimeout(graceTimer); + clearTimeout(hardTimer); + child.removeListener('close', finish); + child.removeListener('error', onError); + }; + const finish = () => { + if (settled) return; + settled = true; + cleanup(); + resolve(); + }; + const fail = (error) => { + if (settled) return; + settled = true; + cleanup(); + reject(error); + }; + const onError = (error) => { + if (childHasExited(child)) finish(); + else fail(new Error(`Child process termination failed before close: ${error.message}`)); + }; + const sendSignal = (signal) => { + try { + const delivered = child.kill(signal); + if (delivered === false && !childHasExited(child)) { + fail(new Error(`Could not deliver ${signal} to child process`)); + return false; + } + } catch (error) { + if (childHasExited(child)) finish(); + else fail(new Error(`Could not deliver ${signal} to child process: ${error.message}`)); + return false; + } + return true; + }; + + child.once('close', finish); + child.once('error', onError); + if (!sendSignal('SIGTERM')) return; + graceTimer = setTimeout(() => { + if (settled || childHasExited(child)) { + if (childHasExited(child)) finish(); + return; + } + if (!sendSignal('SIGKILL')) return; + hardTimer = setTimeout(() => { + if (childHasExited(child)) finish(); + else fail(new Error('Child process did not close after SIGKILL')); + }, hardKillMs); + hardTimer.unref?.(); + }, graceMs); + graceTimer.unref?.(); + }).finally(() => terminationPromises.delete(child)); + + terminationPromises.set(child, promise); + return promise; +} +''', + 'strict terminateChild', +) + +utils = replace_once( + utils, + '''export function runCommand(command, args, { + spawnFn = nodeSpawn, + signal, + children, + onStdout, + onStderr, + killOptions, +} = {}) { + if (signal?.aborted) return Promise.reject(new AbortRunError()); + + return new Promise((resolve, reject) => { + let child; + try { + child = spawnFn(command, args); + } catch (error) { + reject(error); + return; + } + children?.add(child); + let stdout = ''; + let stderr = ''; + let settled = false; + + const cleanup = () => { + children?.delete(child); + signal?.removeEventListener('abort', onAbort); + }; + const settle = (handler, value) => { + if (settled) return; + settled = true; + cleanup(); + handler(value); + }; + const onAbort = () => { + terminateChild(child, killOptions) + .then(() => settle(reject, new AbortRunError())) + .catch((error) => settle(reject, error)); + }; + + child.stdout?.on('data', (chunk) => { + const text = chunk.toString(); + stdout += text; + onStdout?.(text); + }); + child.stderr?.on('data', (chunk) => { + const text = chunk.toString(); + stderr += text; + onStderr?.(text); + }); + child.once('error', (error) => settle(reject, error)); + child.once('close', (code, closeSignal) => { + if (signal?.aborted) settle(reject, new AbortRunError()); + else settle(resolve, { code, signal: closeSignal, stdout, stderr }); + }); + signal?.addEventListener('abort', onAbort, { once: true }); + if (signal?.aborted) onAbort(); + }); +} +''', + '''export function runCommand(command, args, { + spawnFn = nodeSpawn, + signal, + children, + onStdout, + onStderr, + killOptions, +} = {}) { + if (signal?.aborted) return Promise.reject(new AbortRunError()); + + return new Promise((resolve, reject) => { + let child; + try { + child = spawnFn(command, args); + } catch (error) { + reject(error); + return; + } + children?.add(child); + let stdout = ''; + let stderr = ''; + let settled = false; + let processError = null; + + const cleanup = ({ removeChild = true } = {}) => { + if (removeChild) children?.delete(child); + signal?.removeEventListener('abort', onAbort); + }; + const settle = (handler, value, options) => { + if (settled) return; + settled = true; + cleanup(options); + handler(value); + }; + const onAbort = () => { + terminateChild(child, killOptions) + .then(() => settle(reject, new AbortRunError(), { removeChild: true })) + .catch((error) => settle(reject, error, { removeChild: childHasExited(child) })); + }; + + child.stdout?.on('data', (chunk) => { + const text = chunk.toString(); + stdout += text; + onStdout?.(text); + }); + child.stderr?.on('data', (chunk) => { + const text = chunk.toString(); + stderr += text; + onStderr?.(text); + }); + child.once('error', (error) => { + processError = error; + if (!child.pid || childHasExited(child)) { + settle(reject, error, { removeChild: true }); + } + }); + child.once('close', (code, closeSignal) => { + children?.delete(child); + if (settled) return; + if (signal?.aborted) settle(reject, new AbortRunError(), { removeChild: true }); + else if (processError) settle(reject, processError, { removeChild: true }); + else settle(resolve, { code, signal: closeSignal, stdout, stderr }, { removeChild: true }); + }); + signal?.addEventListener('abort', onAbort, { once: true }); + if (signal?.aborted) onAbort(); + }); +} +''', + 'preserve unclosed child after termination failure', +) + +utils = replace_once( + utils, + '''function rollbackPublishManifest(manifest) { + const errors = []; + [...manifest.entries].reverse().forEach((entry) => { + try { + if (entry.hadDestination) { + if (fs.existsSync(entry.backup)) { + safeUnlink(entry.destination); + ensureDir(path.dirname(entry.destination)); + fs.renameSync(entry.backup, entry.destination); + } else if (!fs.existsSync(entry.destination)) { + throw new Error(`Cannot restore missing prior destination: ${entry.destination}`); + } + } else { + safeUnlink(entry.destination); + safeUnlink(entry.backup); + } + safeUnlink(entry.stage); + } catch (error) { + errors.push(error); + } + }); + if (errors.length) { + throw new globalThis.AggregateError(errors, 'Publication transaction rollback failed'); + } +} + +function finalizeCommittedManifest(manifest) { + const missing = manifest.entries.filter((entry) => !fs.existsSync(entry.destination)); + if (missing.length) { + rollbackPublishManifest(manifest); + return; + } + manifest.entries.forEach((entry) => { + safeUnlink(entry.stage); + safeUnlink(entry.backup); + }); +} +''', + '''function rollbackPublishManifest(manifest, { removeFile = removeTransactionalFile } = {}) { + const errors = []; + [...manifest.entries].reverse().forEach((entry) => { + try { + if (entry.hadDestination) { + if (fs.existsSync(entry.backup)) { + removeFile(entry.destination); + ensureDir(path.dirname(entry.destination)); + fs.renameSync(entry.backup, entry.destination); + } else if (!fs.existsSync(entry.destination)) { From 58c62aee96dafe9aa204af4f1ab205d57f2f6c38 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 13:02:39 +0530 Subject: [PATCH 68/75] chore: stage review edge hardening patch --- .github/scripts/review-edge-hardening.part01 | 328 +++++++++++++++++++ 1 file changed, 328 insertions(+) create mode 100644 .github/scripts/review-edge-hardening.part01 diff --git a/.github/scripts/review-edge-hardening.part01 b/.github/scripts/review-edge-hardening.part01 new file mode 100644 index 0000000..0f556e0 --- /dev/null +++ b/.github/scripts/review-edge-hardening.part01 @@ -0,0 +1,328 @@ + throw new Error(`Cannot restore missing prior destination: ${entry.destination}`); + } + } else { + removeFile(entry.destination); + removeFile(entry.backup); + } + removeFile(entry.stage); + } catch (error) { + errors.push(error); + } + }); + if (errors.length) { + throw new globalThis.AggregateError(errors, 'Publication transaction rollback failed'); + } +} + +function finalizeCommittedManifest(manifest, { removeFile = removeTransactionalFile } = {}) { + const missing = manifest.entries.filter((entry) => !fs.existsSync(entry.destination)); + if (missing.length) { + rollbackPublishManifest(manifest, { removeFile }); + return; + } + manifest.entries.forEach((entry) => { + removeFile(entry.stage); + removeFile(entry.backup); + }); +} +''', + 'strict transaction cleanup', +) + +utils = replace_once( + utils, + '''export function publishBundle(entries, token, { faultInjector, manifestDir } = {}) {''', + '''export function publishBundle(entries, token, { + faultInjector, + manifestDir, + removeFile = removeTransactionalFile, +} = {}) {''', + 'publishBundle remove injection', +) + +utils = replace_once( + utils, + ''' finalizeCommittedManifest(manifest); + safeUnlink(manifestPath); + } catch (error) { + if (error instanceof SimulatedProcessCrash) throw error; + try { + rollbackPublishManifest(manifest); + safeUnlink(manifestPath); + } catch (rollbackError) { + throw new globalThis.AggregateError([error, rollbackError], 'Publication failed and rollback was incomplete'); + } + throw error; + } +} + +export function recoverPublishArtifacts(directory) {''', + ''' finalizeCommittedManifest(manifest, { removeFile }); + removeFile(manifestPath); + } catch (error) { + if (error instanceof SimulatedProcessCrash) throw error; + if (manifest.state === 'committed') throw error; + try { + rollbackPublishManifest(manifest, { removeFile }); + removeFile(manifestPath); + } catch (rollbackError) { + throw new globalThis.AggregateError([error, rollbackError], 'Publication failed and rollback was incomplete'); + } + throw error; + } +} + +export function recoverPublishArtifacts(directory, { removeFile = removeTransactionalFile } = {}) {''', + 'strict publish success and rollback', +) + +utils = replace_once( + utils, + ''' manifests.forEach((manifestPath) => { + const manifest = validatePublishManifest(readJson(manifestPath, null), manifestPath); + if (manifest.state === 'committed') finalizeCommittedManifest(manifest); + else rollbackPublishManifest(manifest); + safeUnlink(manifestPath); + }); + + const artifacts = listFilesRecursive(directory); + const orphanBackups = artifacts.filter((filePath) => filePath.endsWith('.bak')); + if (orphanBackups.length) { + throw new Error( + `Unrecoverable publication backups without a transaction manifest: ${orphanBackups.join(', ')}`, + ); + } + artifacts.filter((filePath) => filePath.endsWith('.new')).forEach(safeUnlink); +}''', + ''' manifests.forEach((manifestPath) => { + const manifest = validatePublishManifest(readJson(manifestPath, null), manifestPath); + if (manifest.state === 'committed') finalizeCommittedManifest(manifest, { removeFile }); + else rollbackPublishManifest(manifest, { removeFile }); + removeFile(manifestPath); + }); + + const artifacts = listFilesRecursive(directory); + const orphanBackups = artifacts.filter((filePath) => filePath.endsWith('.bak')); + if (orphanBackups.length) { + throw new Error( + `Unrecoverable publication backups without a transaction manifest: ${orphanBackups.join(', ')}`, + ); + } + artifacts.filter((filePath) => filePath.endsWith('.new')).forEach((filePath) => removeFile(filePath)); +}''', + 'strict startup transaction cleanup', +) + +utils = replace_once( + utils, + '''export function verificationPathForRun(runId, historyDir, { mustExist = true } = {}) { + const safeRunId = requireRunId(runId); + const root = path.resolve(historyDir); + const filePath = path.resolve(root, safeRunId, 'verification.json'); + if (!filePath.startsWith(`${root}${path.sep}`)) throw new Error('Verification path escaped history root'); + if (mustExist && !fs.existsSync(filePath)) throw new Error('Run verification file not found'); + return filePath; +} + +export function resolveVerificationPath(eventId, historyDir) { + const separator = String(eventId).indexOf(':'); + if (separator <= 0 || separator === String(eventId).length - 1) { + throw new Error('Run-scoped event ID required'); + } + return verificationPathForRun(String(eventId).slice(0, separator), historyDir); +} +''', + '''export function runIdFromEventId(eventId) { + const value = String(eventId); + const separator = value.indexOf(':'); + if (separator <= 0 || separator === value.length - 1) { + throw new Error('Run-scoped event ID required'); + } + return requireRunId(value.slice(0, separator)); +} + +export function verificationPathForRun(runId, historyDir, { mustExist = true } = {}) { + const safeRunId = requireRunId(runId); + const root = path.resolve(historyDir); + const filePath = path.resolve(root, safeRunId, 'verification.json'); + if (!filePath.startsWith(`${root}${path.sep}`)) throw new Error('Verification path escaped history root'); + if (mustExist && !fs.existsSync(filePath)) throw new Error('Run verification file not found'); + return filePath; +} + +export function eventsPathForRun(runId, historyDir, { mustExist = true } = {}) { + const safeRunId = requireRunId(runId); + const root = path.resolve(historyDir); + const filePath = path.resolve(root, safeRunId, 'events.json'); + if (!filePath.startsWith(`${root}${path.sep}`)) throw new Error('Events path escaped history root'); + if (mustExist && !fs.existsSync(filePath)) throw new Error('Run events file not found'); + return filePath; +} + +export function assertEventBelongsToRun(eventId, historyDir) { + const value = String(eventId); + const runId = runIdFromEventId(value); + const events = readJson(eventsPathForRun(runId, historyDir), null); + if (!events || !Array.isArray(events.events)) throw new Error('Run events file is invalid'); + if (events.run_id && events.run_id !== runId) throw new Error('Run events file has a mismatched run ID'); + if (!events.events.some((event) => String(event.id) === value)) { + throw new Error('Event does not belong to the referenced run'); + } + return runId; +} + +export function resolveVerificationPath(eventId, historyDir) { + return verificationPathForRun(runIdFromEventId(eventId), historyDir); +} +''', + 'validate review event membership', +) + +write(utils_path, utils) + +# --------------------------------------------------------------------------- +# server.js: actively abort multipart uploads, recover stale runtime workspaces, +# validate event membership, and remain fail-closed after termination errors. +# --------------------------------------------------------------------------- +server_path = 'source app folder/dashboard/server.js' +server = read(server_path) + +server = replace_once( + server, + '''import { + AbortRunError, + buildTrackerArgs,''', + '''import { + AbortRunError, + assertEventBelongsToRun, + buildTrackerArgs,''', + 'server import event validation', +) +server = replace_once( + server, + ''' readJson, + recoverPublishArtifacts, + resolveVerificationPath,''', + ''' readJson, + recoverPublishArtifacts, + recoverRuntimeArtifacts, + resolveVerificationPath,''', + 'server import runtime recovery', +) +server = replace_once( + server, + ''' for (const dir of [uploadsDir, publicDir, historyDir]) ensureDir(dir); + recoverPublishArtifacts(publicDir);''', + ''' for (const dir of [uploadsDir, publicDir, historyDir]) ensureDir(dir); + recoverRuntimeArtifacts(uploadsDir); + recoverPublishArtifacts(publicDir);''', + 'startup runtime recovery', +) +server = replace_once( + server, + ''' const storage = multer.diskStorage({ + destination: (_req, _file, callback) => callback(null, uploadsDir), + filename: (_req, file, callback) => { + callback(null, `input-${Date.now()}-${randomUUID()}${path.extname(file.originalname).toLowerCase()}`); + }, + });''', + ''' const storage = multer.diskStorage({ + destination: (_req, _file, callback) => callback(null, uploadsDir), + filename: (req, file, callback) => { + const filename = `input-${Date.now()}-${randomUUID()}${path.extname(file.originalname).toLowerCase()}`; + const state = pendingUploads.get(req); + if (state) state.filePath = path.join(uploadsDir, filename); + callback(null, filename); + }, + });''', + 'track partial upload filename', +) +server = replace_once( + server, + ''' let terminating = false; + let activeRun = null; + let job = blankJob();''', + ''' let terminating = false; + let activeRun = null; + let job = blankJob(); + const pendingUploads = new Map();''', + 'pending upload state', +) +server = replace_once( + server, + ''' function reserveUpload(req, res, next) { + if (isBusy()) { + return res.status(409).json({ error: 'A video is already uploading, processing, or stopping.' }); + } + req.uploadEpoch = epoch; + uploadReserved = true; + let released = false; + const release = () => { + if (!released) { + released = true; + uploadReserved = false; + } + }; + res.once('finish', release); + res.once('close', release); + return next(); + } +''', + ''' function reserveUpload(req, res, next) { + if (isBusy()) { + return res.status(409).json({ error: 'A video is already uploading, processing, or stopping.' }); + } + req.uploadEpoch = epoch; + uploadReserved = true; + let released = false; + let resolveClosed; + const state = { + request: req, + filePath: null, + closed: new Promise((resolve) => { resolveClosed = resolve; }), + release: null, + }; + const release = () => { + if (released) return; + released = true; + pendingUploads.delete(req); + uploadReserved = pendingUploads.size > 0; + }; + state.release = release; + pendingUploads.set(req, state); + req.once('close', () => resolveClosed()); + res.once('finish', release); + res.once('close', release); + return next(); + } +''', + 'active multipart reservation', +) +server = replace_once( + server, + ''' function readVerification(filePath) { + const data = readJson(filePath, { version: 1, reviews: [] }); + return Array.isArray(data.reviews) ? data : { version: 1, reviews: [] }; + } + + async function stopActiveRun() { + epoch += 1; + const context = activeRun; + activeRun = null; + job = { ...blankJob(), status: context ? 'stopping' : 'idle' }; + if (!context) return; + + terminating = true; + context.controller.abort(); + try { + const children = [...context.children]; + await Promise.all(children.map((child) => terminateChild(child, killOptions))); + await context.done.catch((error) => { + if (error?.name !== 'AbortError') throw error; + }); + job = blankJob(); + } catch (error) { + job = { + ...blankJob(), + status: 'error', + error: `Could not terminate active run: ${error.message}`, From 22fcaf47e6dd4eddf25ab5d80ba303a83e851369 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 13:03:25 +0530 Subject: [PATCH 69/75] chore: stage review edge hardening patch --- .github/scripts/review-edge-hardening.part02 | 322 +++++++++++++++++++ 1 file changed, 322 insertions(+) create mode 100644 .github/scripts/review-edge-hardening.part02 diff --git a/.github/scripts/review-edge-hardening.part02 b/.github/scripts/review-edge-hardening.part02 new file mode 100644 index 0000000..e9159cc --- /dev/null +++ b/.github/scripts/review-edge-hardening.part02 @@ -0,0 +1,322 @@ + }; + throw error; + } finally { + if (context.children.size === 0) terminating = false; + } + } +''', + ''' function readVerification(filePath) { + const data = readJson(filePath, { version: 1, reviews: [] }); + return Array.isArray(data.reviews) ? data : { version: 1, reviews: [] }; + } + + function waitForUploadClose(state) { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error('Multipart upload did not close after reset')), 3000); + timer.unref?.(); + state.closed.then(() => { + clearTimeout(timer); + resolve(); + }); + }); + } + + async function removePendingUploadFile(filePath) { + if (!filePath) return; + let lastError = null; + for (let attempt = 0; attempt < 5; attempt += 1) { + try { + fs.rmSync(filePath, { force: true }); + if (!fs.existsSync(filePath)) return; + lastError = new Error(`Could not remove cancelled upload: ${filePath}`); + } catch (error) { + lastError = error; + } + await new Promise((resolve) => setTimeout(resolve, 20)); + } + throw lastError || new Error(`Could not remove cancelled upload: ${filePath}`); + } + + async function abortPendingUploads(states) { + states.forEach((state) => { + if (!state.request.destroyed) state.request.destroy(); + }); + await Promise.all(states.map(async (state) => { + await waitForUploadClose(state); + await removePendingUploadFile(state.filePath); + state.release(); + })); + } + + async function stopActiveRun() { + epoch += 1; + const context = activeRun; + const uploadStates = [...pendingUploads.values()]; + activeRun = null; + const hasWork = Boolean(context) || uploadStates.length > 0; + job = { ...blankJob(), status: hasWork ? 'stopping' : 'idle' }; + if (!hasWork) return; + + terminating = true; + context?.controller.abort(); + let stoppedCleanly = false; + try { + const children = context ? [...context.children] : []; + await Promise.all([ + abortPendingUploads(uploadStates), + ...children.map((child) => terminateChild(child, killOptions)), + ]); + if (context) { + await context.done.catch((error) => { + if (error?.name !== 'AbortError') throw error; + }); + } + job = blankJob(); + stoppedCleanly = true; + } catch (error) { + job = { + ...blankJob(), + status: 'error', + error: `Could not terminate active work: ${error.message}`, + }; + throw error; + } finally { + if ( + stoppedCleanly + && (!context || context.children.size === 0) + && pendingUploads.size === 0 + ) terminating = false; + } + } +''', + 'abort multipart uploads and strict stop gate', +) +server = replace_once( + server, + ''' try { + const filePath = resolveVerificationPath(eventId, historyDir); + const data = readVerification(filePath);''', + ''' try { + assertEventBelongsToRun(eventId, historyDir); + const filePath = resolveVerificationPath(eventId, historyDir); + const data = readVerification(filePath);''', + 'review membership validation', +) +server = replace_once( + server, + ''' getState: () => ({ epoch, uploadReserved, terminating, activeRun, job: { ...job } }),''', + ''' getState: () => ({ + epoch, + uploadReserved, + terminating, + pendingUploads: pendingUploads.size, + activeRun, + job: { ...job }, + }),''', + 'expose pending upload state for tests', +) +write(server_path, server) + +# --------------------------------------------------------------------------- +# Tracker and frontend metrics: invalid individual-fly velocity is missing too. +# --------------------------------------------------------------------------- +tracker_path = 'source app folder/tracker/tracker.py' +tracker = read(tracker_path) +tracker = replace_once( + tracker, + '''def displacement(previous, current, was_initialized: bool) -> float: + return float(math.dist(current, previous)) if was_initialized else 0.0 + + +def assignment_confidence''', + '''def displacement(previous, current, was_initialized: bool) -> float: + return float(math.dist(current, previous)) if was_initialized else 0.0 + + +def observed_speed(speed: float, tracking_valid: int) -> float: + return float(speed) if tracking_valid else math.nan + + +def assignment_confidence''', + 'tracker observed speed helper', +) +tracker = replace_once( + tracker, + ''' f1_speed = f2_speed = 0.0 + proximity = math.nan''', + ''' f1_speed = f2_speed = math.nan + proximity = math.nan''', + 'invalid speed defaults missing', +) +tracker = replace_once( + tracker, + ''' if was_initialized: + f1_speed = displacement(prev_f1, f1_coords, was_initialized) + f2_speed = displacement(prev_f2, f2_coords, was_initialized) + proximity = float(math.dist(f1_coords, f2_coords))''', + ''' if was_initialized: + f1_speed = displacement(prev_f1, f1_coords, was_initialized) + f2_speed = displacement(prev_f2, f2_coords, was_initialized) + else: + f1_speed = f2_speed = 0.0 + proximity = float(math.dist(f1_coords, f2_coords))''', + 'valid initial speed zero', +) +tracker = replace_once( + tracker, + ''' if was_initialized: + f1_speed = displacement(prev_f1, point, was_initialized) + f2_speed = displacement(prev_f2, point, was_initialized) + prev_f1 = prev_f2 = point''', + ''' prev_f1 = prev_f2 = point''', + 'do not fabricate merged fly speeds', +) +tracker = replace_once( + tracker, + ''' data.append({ + "frame": frame_num, + "fly1_x": f1_coords[0],''', + ''' measured_f1_speed = observed_speed(f1_speed, tracking_valid) + measured_f2_speed = observed_speed(f2_speed, tracking_valid) + data.append({ + "frame": frame_num, + "fly1_x": f1_coords[0],''', + 'compute validity-aware speeds', +) +tracker = replace_once( + tracker, + ''' "fly1_speed": f1_speed, + "fly2_speed": f2_speed, + "fly1_speed_pxsec": round(f1_speed * effective_fps, 4), + "fly2_speed_pxsec": round(f2_speed * effective_fps, 4),''', + ''' "fly1_speed": measured_f1_speed, + "fly2_speed": measured_f2_speed, + "fly1_speed_pxsec": round(measured_f1_speed * effective_fps, 4), + "fly2_speed_pxsec": round(measured_f2_speed * effective_fps, 4),''', + 'write only observed speeds', +) +write(tracker_path, tracker) + +metrics_path = 'source app folder/dashboard/src/metrics.js' +metrics = read(metrics_path) +metrics = replace_once( + metrics, + '''export function proximityValue(row) { + if (!row) return null; + const rawProximity = row.proximity_distance; + if (rawProximity === null || rawProximity === undefined || rawProximity === '') return null; + const proximity = Number(rawProximity); + if (!Number.isFinite(proximity)) return null; + if (Number(row.occlusion_flag ?? 0) !== 0) return null; + + if (row.tracking_valid !== null && row.tracking_valid !== undefined && row.tracking_valid !== '') { + if (Number(row.tracking_valid) !== 1) return null; + } else if (row.detection_count !== null && row.detection_count !== undefined && row.detection_count !== '') { + if (Number(row.detection_count) < 2) return null; + } else if ( + row.fly1_area !== null && row.fly1_area !== undefined + && row.fly2_area !== null && row.fly2_area !== undefined + ) { + if (!(Number(row.fly1_area) > 0 && Number(row.fly2_area) > 0)) return null; + } + + return proximity; +} + +export function computeAverageProximity(rows) { + const values = rows.map(proximityValue).filter((value) => value !== null); + if (!values.length) return 0; + return Math.round(values.reduce((sum, value) => sum + value, 0) / values.length); +} + +export function prismDistance(row) { + return proximityValue(row) ?? ''; +} +''', + '''export function trackingObservationIsValid(row) { + if (!row || Number(row.occlusion_flag ?? 0) !== 0) return false; + if (row.tracking_valid !== null && row.tracking_valid !== undefined && row.tracking_valid !== '') { + return Number(row.tracking_valid) === 1; + } + if (row.detection_count !== null && row.detection_count !== undefined && row.detection_count !== '') { + return Number(row.detection_count) >= 2; + } + if ( + row.fly1_area !== null && row.fly1_area !== undefined + && row.fly2_area !== null && row.fly2_area !== undefined + ) { + return Number(row.fly1_area) > 0 && Number(row.fly2_area) > 0; + } + return true; +} + +export function proximityValue(row) { + if (!trackingObservationIsValid(row)) return null; + const rawProximity = row.proximity_distance; + if (rawProximity === null || rawProximity === undefined || rawProximity === '') return null; + const proximity = Number(rawProximity); + return Number.isFinite(proximity) ? proximity : null; +} + +export function computeAverageProximity(rows) { + const values = rows.map(proximityValue).filter((value) => value !== null); + if (!values.length) return 0; + return Math.round(values.reduce((sum, value) => sum + value, 0) / values.length); +} + +export function prismDistance(row) { + return proximityValue(row) ?? ''; +} + +export function prismVelocity(row, flyKey) { + if (!trackingObservationIsValid(row)) return ''; + const rawSpeed = row?.[`${flyKey}_speed_pxsec`] ?? row?.[`${flyKey}_speed`]; + if (rawSpeed === null || rawSpeed === undefined || rawSpeed === '') return ''; + const speed = Number(rawSpeed); + return Number.isFinite(speed) ? speed : ''; +} +''', + 'validity-aware Prism velocity', +) +write(metrics_path, metrics) + +app_path = 'source app folder/dashboard/src/App.jsx' +app = read(app_path) +app = replace_once( + app, + '''import { prismDistance, proximityValue } from './metrics.js';''', + '''import { prismDistance, prismVelocity, proximityValue } from './metrics.js';''', + 'App metrics import', +) +app = replace_once( + app, + ''' const v1 = Number(r.fly1_speed_pxsec ?? r.fly1_speed ?? 0); + const v2 = Number(r.fly2_speed_pxsec ?? r.fly2_speed ?? 0); + const dist = prismDistance(r);''', + ''' const v1 = prismVelocity(r, 'fly1'); + const v2 = prismVelocity(r, 'fly2'); + const dist = prismDistance(r);''', + 'blank invalid Prism velocity', +) +write(app_path, app) + +# --------------------------------------------------------------------------- +# Tests. +# --------------------------------------------------------------------------- +utils_test_path = 'source app folder/dashboard/tests/server-utils.test.js' +utils_test = read(utils_test_path) +utils_test = replace_once( + utils_test, + '''import { + buildTrackerArgs, + parseTrackerSync, + prepareRunHistoryBundle,''', + '''import { + assertEventBelongsToRun, + buildTrackerArgs, + parseTrackerSync, + prepareRunHistoryBundle,''', + 'utils test event import', +) +utils_test = replace_once( + utils_test, From 84d1037c59e5ecac7cc7d83e83ddde96d1127db5 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 13:04:14 +0530 Subject: [PATCH 70/75] chore: stage review edge hardening patch --- .github/scripts/review-edge-hardening.part03 | 314 +++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 .github/scripts/review-edge-hardening.part03 diff --git a/.github/scripts/review-edge-hardening.part03 b/.github/scripts/review-edge-hardening.part03 new file mode 100644 index 0000000..603428f --- /dev/null +++ b/.github/scripts/review-edge-hardening.part03 @@ -0,0 +1,314 @@ + ''' readCsvAvgProximity, + recoverPublishArtifacts, + resolveVerificationPath,''', + ''' readCsvAvgProximity, + recoverPublishArtifacts, + recoverRuntimeArtifacts, + resolveVerificationPath,''', + 'utils test runtime import', +) +utils_test = replace_once( + utils_test, + ''' publishBundle(bundle.entries, 'history-token', { manifestDir: dir }); + + const scopedEvents = JSON.parse(fs.readFileSync(path.join(historyDir, runId, 'events.json')));''', + ''' publishBundle(bundle.entries, 'history-token', { manifestDir: dir }); + + const scopedEvents = JSON.parse(fs.readFileSync(path.join(historyDir, runId, 'events.json')));''', + 'history test stable anchor', +) +utils_test = replace_once( + utils_test, + ''' assert.equal(verificationPathForRun(runId, historyDir), path.join(historyDir, runId, 'verification.json')); + assert.throws(() => resolveVerificationPath('evt-001', historyDir), /Run-scoped event ID required/); +});''', + ''' assert.equal(verificationPathForRun(runId, historyDir), path.join(historyDir, runId, 'verification.json')); + assert.equal(assertEventBelongsToRun(`${runId}:evt-001`, historyDir), runId); + assert.throws( + () => assertEventBelongsToRun(`${runId}:evt-does-not-exist`, historyDir), + /does not belong/, + ); + assert.throws(() => resolveVerificationPath('evt-001', historyDir), /Run-scoped event ID required/); +});''', + 'event membership utility assertions', +) + +insert_before = "test('termination escalates to SIGKILL and resolves only after close', async () => {" +new_tests = '''test('rollback remains fail-closed when a new destination cannot be deleted', () => { + const dir = tempDir('flyt-delete-failure-'); + const source = path.join(dir, 'source'); + const destination = path.join(dir, 'new-destination'); + const manifestPath = path.join(dir, '.flyt-publish-delete-failure.json'); + fs.writeFileSync(source, 'new-data'); + let blockDestinationRemoval = true; + const removeFile = (filePath) => { + if (blockDestinationRemoval && filePath === destination) { + throw new Error('injected deletion failure'); + } + fs.rmSync(filePath, { force: true }); + if (fs.existsSync(filePath)) throw new Error(`still exists: ${filePath}`); + }; + + assert.throws(() => publishBundle([ + { source, destination }, + ], 'delete-failure', { + manifestDir: dir, + removeFile, + faultInjector(stage) { + if (stage === 'published') throw new Error('injected publish failure'); + }, + }), /rollback was incomplete/); + assert.equal(fs.readFileSync(destination, 'utf8'), 'new-data'); + assert.equal(fs.existsSync(manifestPath), true); + + blockDestinationRemoval = false; + recoverPublishArtifacts(dir); + assert.equal(fs.existsSync(destination), false); + assert.equal(fs.existsSync(manifestPath), false); +}); + +test('startup runtime recovery removes abandoned inputs and run workspaces only', () => { + const dir = tempDir('flyt-runtime-recovery-'); + fs.writeFileSync(path.join(dir, 'input-stale.mp4'), 'partial'); + fs.mkdirSync(path.join(dir, 'run-stale')); + fs.writeFileSync(path.join(dir, 'run-stale', 'data.tmp'), 'partial'); + fs.writeFileSync(path.join(dir, 'keep.txt'), 'keep'); + + recoverRuntimeArtifacts(dir); + assert.equal(fs.existsSync(path.join(dir, 'input-stale.mp4')), false); + assert.equal(fs.existsSync(path.join(dir, 'run-stale')), false); + assert.equal(fs.readFileSync(path.join(dir, 'keep.txt'), 'utf8'), 'keep'); +}); + +test('termination error without close rejects and never proves process exit', async () => { + class ErrorOnlyChild extends EventEmitter { + constructor() { + super(); + this.exitCode = null; + this.signalCode = null; + } + kill() { + setTimeout(() => this.emit('error', new Error('signal delivery failed')), 1); + return true; + } + } + const child = new ErrorOnlyChild(); + await assert.rejects( + terminateChild(child, { graceMs: 20, hardKillMs: 20 }), + /termination failed before close/, + ); + assert.equal(child.exitCode, null); + assert.equal(child.signalCode, null); +}); + +test('runCommand keeps an unclosed child tracked after termination failure', async () => { + class ErrorOnlyChild extends EventEmitter { + constructor() { + super(); + this.exitCode = null; + this.signalCode = null; + this.pid = 123; + } + kill() { + setTimeout(() => this.emit('error', new Error('kill failed')), 1); + return true; + } + } + const child = new ErrorOnlyChild(); + const controller = new AbortController(); + const children = new Set(); + const command = runCommand('fake', [], { + spawnFn: () => child, + signal: controller.signal, + children, + killOptions: { graceMs: 20, hardKillMs: 20 }, + }); + controller.abort(); + await assert.rejects(command, /termination failed before close/); + assert.equal(children.has(child), true); + child.exitCode = 1; + child.emit('close', 1, null); + assert.equal(children.size, 0); +}); + +''' +if insert_before not in utils_test: + raise RuntimeError('utils test insertion anchor missing') +utils_test = utils_test.replace(insert_before, new_tests + insert_before, 1) +write(utils_test_path, utils_test) + +integration_path = 'source app folder/dashboard/tests/server-integration.test.js' +integration = read(integration_path) +integration = replace_once( + integration, + '''test('reset during a multipart upload invalidates and removes the partial upload', async (t) => { + const harness = await createHarness(standardServices()); + t.after(() => harness.close()); + const boundary = '----flyt-boundary'; + const prefix = [ + `--${boundary}`, + 'Content-Disposition: form-data; name="video"; filename="slow.mp4"', + 'Content-Type: video/mp4', + '', + 'partial-video-', + ].join('\\r\\n'); + const suffix = `\\r\\n--${boundary}--\\r\\n`; + + let finishUpload; + const uploadResponse = new Promise((resolve, reject) => { + const request = http.request(`${harness.baseUrl}/api/upload`, { + method: 'POST', + headers: { 'Content-Type': `multipart/form-data; boundary=${boundary}` }, + }, (response) => { + response.resume(); + response.on('end', () => resolve(response)); + }); + request.on('error', reject); + request.write(prefix); + finishUpload = () => request.end(`remaining${suffix}`); + }); + + await new Promise((resolve) => setTimeout(resolve, 30)); + const reset = await fetch(`${harness.baseUrl}/api/reset`, { method: 'POST' }); + assert.equal(reset.status, 200); + finishUpload(); + const response = await uploadResponse; + assert.equal(response.statusCode, 409); + assert.equal( + fs.readdirSync(harness.paths.uploadsDir).filter((name) => name.startsWith('input-')).length, + 0, + ); +});''', + '''test('reset actively aborts a stalled multipart upload and reopens the gate', async (t) => { + const harness = await createHarness(standardServices()); + t.after(() => harness.close()); + const boundary = '----flyt-boundary'; + const prefix = [ + `--${boundary}`, + 'Content-Disposition: form-data; name="video"; filename="slow.mp4"', + 'Content-Type: video/mp4', + '', + 'partial-video-', + ].join('\\r\\n'); + + const uploadOutcome = new Promise((resolve) => { + const request = http.request(`${harness.baseUrl}/api/upload`, { + method: 'POST', + headers: { 'Content-Type': `multipart/form-data; boundary=${boundary}` }, + }, (response) => { + response.resume(); + response.on('end', () => resolve(response.statusCode)); + }); + request.on('error', () => resolve('aborted')); + request.write(prefix); + }); + + await new Promise((resolve) => setTimeout(resolve, 30)); + const reset = await fetch(`${harness.baseUrl}/api/reset`, { method: 'POST' }); + assert.equal(reset.status, 200); + const outcome = await Promise.race([ + uploadOutcome, + new Promise((resolve) => setTimeout(() => resolve('timeout'), 1000)), + ]); + assert.notEqual(outcome, 'timeout'); + assert.ok(outcome === 'aborted' || outcome === 409); + assert.equal( + fs.readdirSync(harness.paths.uploadsDir).filter((name) => name.startsWith('input-')).length, + 0, + ); + const state = harness.getState(); + assert.equal(state.pendingUploads, 0); + assert.equal(state.uploadReserved, false); + assert.equal(state.terminating, false); + + assert.equal((await upload(harness.baseUrl, 'after-reset.mp4')).status, 200); + await waitForStatus(harness.baseUrl, 'done'); +});''', + 'active multipart reset test', +) +integration = replace_once( + integration, + ''' assert.equal(unscopedResponse.status, 400); + + assert.equal((await upload(harness.baseUrl, 'second.mp4')).status, 200);''', + ''' assert.equal(unscopedResponse.status, 400); + + const orphanResponse = await fetch(`${harness.baseUrl}/api/verification`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ eventId: `${firstRunId}:evt-does-not-exist`, verdict: 'confirmed' }), + }); + assert.equal(orphanResponse.status, 400); + + assert.equal((await upload(harness.baseUrl, 'second.mp4')).status, 200);''', + 'reject orphan review', +) +write(integration_path, integration) + +metrics_test_path = 'source app folder/dashboard/tests/metrics.test.js' +metrics_test = read(metrics_test_path) +metrics_test = replace_once( + metrics_test, + '''import { computeAverageProximity, prismDistance, proximityValue } from '../src/metrics.js';''', + '''import { + computeAverageProximity, + prismDistance, + prismVelocity, + proximityValue, +} from '../src/metrics.js';''', + 'metrics test import', +) +metrics_test += '''\n\ntest('invalid or merged observations export blank individual-fly velocities', () => { + const invalid = { + tracking_valid: 0, + detection_count: 1, + occlusion_flag: 1, + fly1_speed_pxsec: 120, + fly2_speed_pxsec: 95, + }; + assert.equal(prismVelocity(invalid, 'fly1'), ''); + assert.equal(prismVelocity(invalid, 'fly2'), ''); + assert.equal(prismVelocity({ + tracking_valid: 1, + detection_count: 2, + occlusion_flag: 0, + fly1_speed_pxsec: 42.5, + }, 'fly1'), 42.5); +}); +''' +write(metrics_test_path, metrics_test) + +tracker_test_path = 'source app folder/tracker/tests/test_tracker.py' +tracker_test = read(tracker_test_path) +tracker_test = replace_once( + tracker_test, + ''' def test_initial_detection_has_zero_velocity(self): + self.assertEqual(tracker.displacement((0, 0), (100, 100), False), 0.0) + self.assertEqual(tracker.displacement((0, 0), (3, 4), True), 5.0) +''', + ''' def test_initial_detection_has_zero_velocity(self): + self.assertEqual(tracker.displacement((0, 0), (100, 100), False), 0.0) + self.assertEqual(tracker.displacement((0, 0), (3, 4), True), 5.0) + + def test_invalid_observation_speed_is_missing(self): + self.assertTrue(math.isnan(tracker.observed_speed(25.0, 0))) + self.assertEqual(tracker.observed_speed(25.0, 1), 25.0) +''', + 'tracker speed validity test', +) +tracker_test = replace_once( + tracker_test, + '''import importlib.util +import pathlib +import unittest +''', + '''import importlib.util +import math +import pathlib +import unittest +''', + 'tracker test math import', +) +write(tracker_test_path, tracker_test) + +print('Applied review edge hardening patches successfully.') From 45feb403e264423712b5d99a826e5e90c095898b Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 13:04:37 +0530 Subject: [PATCH 71/75] chore: validate and apply review edge hardening --- .../workflows/apply-review-edge-hardening.yml | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 .github/workflows/apply-review-edge-hardening.yml diff --git a/.github/workflows/apply-review-edge-hardening.yml b/.github/workflows/apply-review-edge-hardening.yml new file mode 100644 index 0000000..682722a --- /dev/null +++ b/.github/workflows/apply-review-edge-hardening.yml @@ -0,0 +1,89 @@ +name: Apply review edge hardening + +on: + push: + branches: + - fix/critical-job-races + paths: + - '.github/scripts/review-edge-hardening.part*' + - '.github/workflows/apply-review-edge-hardening.yml' + +permissions: + contents: write + +concurrency: + group: flyt-review-edge-hardening + cancel-in-progress: false + +jobs: + patch-test-and-commit: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + with: + ref: fix/critical-job-races + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: source app folder/dashboard/package-lock.json + + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: pip + cache-dependency-path: source app folder/tracker/requirements.txt + + - name: Reconstruct and apply transparent patch + run: | + cat .github/scripts/review-edge-hardening.part* > /tmp/review-edge-hardening.py + python -m py_compile /tmp/review-edge-hardening.py + python /tmp/review-edge-hardening.py + git diff --check + + - name: Install dashboard dependencies + working-directory: source app folder/dashboard + run: npm ci + + - name: Lint backend and regression tests + working-directory: source app folder/dashboard + run: npm run lint:backend + + - name: Run utility, HTTP, lifecycle, and metric tests + working-directory: source app folder/dashboard + run: npm run test:server + + - name: Build dashboard + working-directory: source app folder/dashboard + run: npm run build + + - name: Install tracker dependencies + run: python -m pip install -r 'source app folder/tracker/requirements.txt' + + - name: Check tracker syntax + run: python -m py_compile 'source app folder/tracker/tracker.py' + + - name: Run tracker regression tests + run: python -m unittest discover -s 'source app folder/tracker/tests' -p 'test_*.py' -v + + - name: Commit validated hardening + run: | + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add -- \ + 'source app folder/dashboard/server-utils.js' \ + 'source app folder/dashboard/server.js' \ + 'source app folder/dashboard/src/App.jsx' \ + 'source app folder/dashboard/src/metrics.js' \ + 'source app folder/dashboard/tests/metrics.test.js' \ + 'source app folder/dashboard/tests/server-integration.test.js' \ + 'source app folder/dashboard/tests/server-utils.test.js' \ + 'source app folder/tracker/tracker.py' \ + 'source app folder/tracker/tests/test_tracker.py' + git commit -m 'fix: close fail-closed termination and rollback edges' + git fetch origin fix/critical-job-races + git rebase origin/fix/critical-job-races + git push origin HEAD:fix/critical-job-races From 9b1e824e9f41dee8e8331f5796ab9ee535dd9661 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 07:35:12 +0000 Subject: [PATCH 72/75] fix: close fail-closed termination and rollback edges --- source app folder/dashboard/server-utils.js | 172 +++++++++++++----- source app folder/dashboard/server.js | 110 +++++++++-- source app folder/dashboard/src/App.jsx | 6 +- source app folder/dashboard/src/metrics.js | 39 ++-- .../dashboard/tests/metrics.test.js | 26 ++- .../tests/server-integration.test.js | 34 +++- .../dashboard/tests/server-utils.test.js | 104 +++++++++++ .../tracker/tests/test_tracker.py | 5 + source app folder/tracker/tracker.py | 21 ++- 9 files changed, 422 insertions(+), 95 deletions(-) diff --git a/source app folder/dashboard/server-utils.js b/source app folder/dashboard/server-utils.js index 76e4b0a..01ce2ba 100644 --- a/source app folder/dashboard/server-utils.js +++ b/source app folder/dashboard/server-utils.js @@ -28,6 +28,23 @@ export function safeRemoveDir(dirPath) { } } +export function removeTransactionalFile(filePath) { + if (!filePath) return; + fs.rmSync(filePath, { force: true }); + if (fs.existsSync(filePath)) { + throw new Error(`Could not remove transactional path: ${filePath}`); + } +} + +export function recoverRuntimeArtifacts(uploadsDir) { + ensureDir(uploadsDir); + fs.readdirSync(uploadsDir, { withFileTypes: true }).forEach((entry) => { + const fullPath = path.join(uploadsDir, entry.name); + if (entry.isFile() && entry.name.startsWith('input-')) safeUnlink(fullPath); + else if (entry.isDirectory() && entry.name.startsWith('run-')) safeRemoveDir(fullPath); + }); +} + export function readJson(filePath, fallback) { if (!fs.existsSync(filePath)) return fallback; try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch { return fallback; } @@ -109,8 +126,15 @@ export function validateFrameIntegrity({ const terminationPromises = new WeakMap(); +function childHasExited(child) { + return Boolean(child) && ( + (child.exitCode !== null && child.exitCode !== undefined) + || Boolean(child.signalCode) + ); +} + export function terminateChild(child, { graceMs = 1000, hardKillMs = 3000 } = {}) { - if (!child || child.exitCode !== null || child.signalCode) return Promise.resolve(); + if (!child || childHasExited(child)) return Promise.resolve(); if (terminationPromises.has(child)) return terminationPromises.get(child); const promise = new Promise((resolve, reject) => { @@ -121,7 +145,7 @@ export function terminateChild(child, { graceMs = 1000, hardKillMs = 3000 } = {} clearTimeout(graceTimer); clearTimeout(hardTimer); child.removeListener('close', finish); - child.removeListener('error', finish); + child.removeListener('error', onError); }; const finish = () => { if (settled) return; @@ -129,20 +153,44 @@ export function terminateChild(child, { graceMs = 1000, hardKillMs = 3000 } = {} cleanup(); resolve(); }; - const fail = () => { + const fail = (error) => { if (settled) return; settled = true; cleanup(); - reject(new Error('Child process did not terminate after SIGKILL')); + reject(error); + }; + const onError = (error) => { + if (childHasExited(child)) finish(); + else fail(new Error(`Child process termination failed before close: ${error.message}`)); + }; + const sendSignal = (signal) => { + try { + const delivered = child.kill(signal); + if (delivered === false && !childHasExited(child)) { + fail(new Error(`Could not deliver ${signal} to child process`)); + return false; + } + } catch (error) { + if (childHasExited(child)) finish(); + else fail(new Error(`Could not deliver ${signal} to child process: ${error.message}`)); + return false; + } + return true; }; child.once('close', finish); - child.once('error', finish); - try { child.kill('SIGTERM'); } catch { finish(); return; } + child.once('error', onError); + if (!sendSignal('SIGTERM')) return; graceTimer = setTimeout(() => { - if (settled) return; - try { child.kill('SIGKILL'); } catch { finish(); return; } - hardTimer = setTimeout(fail, hardKillMs); + if (settled || childHasExited(child)) { + if (childHasExited(child)) finish(); + return; + } + if (!sendSignal('SIGKILL')) return; + hardTimer = setTimeout(() => { + if (childHasExited(child)) finish(); + else fail(new Error('Child process did not close after SIGKILL')); + }, hardKillMs); hardTimer.unref?.(); }, graceMs); graceTimer.unref?.(); @@ -174,21 +222,22 @@ export function runCommand(command, args, { let stdout = ''; let stderr = ''; let settled = false; + let processError = null; - const cleanup = () => { - children?.delete(child); + const cleanup = ({ removeChild = true } = {}) => { + if (removeChild) children?.delete(child); signal?.removeEventListener('abort', onAbort); }; - const settle = (handler, value) => { + const settle = (handler, value, options) => { if (settled) return; settled = true; - cleanup(); + cleanup(options); handler(value); }; const onAbort = () => { terminateChild(child, killOptions) - .then(() => settle(reject, new AbortRunError())) - .catch((error) => settle(reject, error)); + .then(() => settle(reject, new AbortRunError(), { removeChild: true })) + .catch((error) => settle(reject, error, { removeChild: childHasExited(child) })); }; child.stdout?.on('data', (chunk) => { @@ -201,10 +250,18 @@ export function runCommand(command, args, { stderr += text; onStderr?.(text); }); - child.once('error', (error) => settle(reject, error)); + child.once('error', (error) => { + processError = error; + if (!child.pid || childHasExited(child)) { + settle(reject, error, { removeChild: true }); + } + }); child.once('close', (code, closeSignal) => { - if (signal?.aborted) settle(reject, new AbortRunError()); - else settle(resolve, { code, signal: closeSignal, stdout, stderr }); + children?.delete(child); + if (settled) return; + if (signal?.aborted) settle(reject, new AbortRunError(), { removeChild: true }); + else if (processError) settle(reject, processError, { removeChild: true }); + else settle(resolve, { code, signal: closeSignal, stdout, stderr }, { removeChild: true }); }); signal?.addEventListener('abort', onAbort, { once: true }); if (signal?.aborted) onAbort(); @@ -276,23 +333,23 @@ function validatePublishManifest(manifest, manifestPath) { return manifest; } -function rollbackPublishManifest(manifest) { +function rollbackPublishManifest(manifest, { removeFile = removeTransactionalFile } = {}) { const errors = []; [...manifest.entries].reverse().forEach((entry) => { try { if (entry.hadDestination) { if (fs.existsSync(entry.backup)) { - safeUnlink(entry.destination); + removeFile(entry.destination); ensureDir(path.dirname(entry.destination)); fs.renameSync(entry.backup, entry.destination); } else if (!fs.existsSync(entry.destination)) { throw new Error(`Cannot restore missing prior destination: ${entry.destination}`); } } else { - safeUnlink(entry.destination); - safeUnlink(entry.backup); + removeFile(entry.destination); + removeFile(entry.backup); } - safeUnlink(entry.stage); + removeFile(entry.stage); } catch (error) { errors.push(error); } @@ -302,15 +359,15 @@ function rollbackPublishManifest(manifest) { } } -function finalizeCommittedManifest(manifest) { +function finalizeCommittedManifest(manifest, { removeFile = removeTransactionalFile } = {}) { const missing = manifest.entries.filter((entry) => !fs.existsSync(entry.destination)); if (missing.length) { - rollbackPublishManifest(manifest); + rollbackPublishManifest(manifest, { removeFile }); return; } manifest.entries.forEach((entry) => { - safeUnlink(entry.stage); - safeUnlink(entry.backup); + removeFile(entry.stage); + removeFile(entry.backup); }); } @@ -325,7 +382,11 @@ function listFilesRecursive(directory) { return files; } -export function publishBundle(entries, token, { faultInjector, manifestDir } = {}) { +export function publishBundle(entries, token, { + faultInjector, + manifestDir, + removeFile = removeTransactionalFile, +} = {}) { const manifestPath = publishManifestPath(entries, token, manifestDir); const manifest = { version: 1, @@ -364,13 +425,14 @@ export function publishBundle(entries, token, { faultInjector, manifestDir } = { writeJson(manifestPath, manifest); faultInjector?.('committed', -1); - finalizeCommittedManifest(manifest); - safeUnlink(manifestPath); + finalizeCommittedManifest(manifest, { removeFile }); + removeFile(manifestPath); } catch (error) { if (error instanceof SimulatedProcessCrash) throw error; + if (manifest.state === 'committed') throw error; try { - rollbackPublishManifest(manifest); - safeUnlink(manifestPath); + rollbackPublishManifest(manifest, { removeFile }); + removeFile(manifestPath); } catch (rollbackError) { throw new globalThis.AggregateError([error, rollbackError], 'Publication failed and rollback was incomplete'); } @@ -378,7 +440,7 @@ export function publishBundle(entries, token, { faultInjector, manifestDir } = { } } -export function recoverPublishArtifacts(directory) { +export function recoverPublishArtifacts(directory, { removeFile = removeTransactionalFile } = {}) { if (!fs.existsSync(directory)) return; const manifests = fs.readdirSync(directory) .filter((name) => /^\.flyt-publish-[A-Za-z0-9-]+\.json$/.test(name)) @@ -386,9 +448,9 @@ export function recoverPublishArtifacts(directory) { manifests.forEach((manifestPath) => { const manifest = validatePublishManifest(readJson(manifestPath, null), manifestPath); - if (manifest.state === 'committed') finalizeCommittedManifest(manifest); - else rollbackPublishManifest(manifest); - safeUnlink(manifestPath); + if (manifest.state === 'committed') finalizeCommittedManifest(manifest, { removeFile }); + else rollbackPublishManifest(manifest, { removeFile }); + removeFile(manifestPath); }); const artifacts = listFilesRecursive(directory); @@ -398,7 +460,7 @@ export function recoverPublishArtifacts(directory) { `Unrecoverable publication backups without a transaction manifest: ${orphanBackups.join(', ')}`, ); } - artifacts.filter((filePath) => filePath.endsWith('.new')).forEach(safeUnlink); + artifacts.filter((filePath) => filePath.endsWith('.new')).forEach((filePath) => removeFile(filePath)); } export function buildTrackerArgs(trackerScript, inputPath, outputs, overrides = {}, defaults) { @@ -559,6 +621,15 @@ export function prepareRunHistoryBundle({ }; } +export function runIdFromEventId(eventId) { + const value = String(eventId); + const separator = value.indexOf(':'); + if (separator <= 0 || separator === value.length - 1) { + throw new Error('Run-scoped event ID required'); + } + return requireRunId(value.slice(0, separator)); +} + export function verificationPathForRun(runId, historyDir, { mustExist = true } = {}) { const safeRunId = requireRunId(runId); const root = path.resolve(historyDir); @@ -568,10 +639,27 @@ export function verificationPathForRun(runId, historyDir, { mustExist = true } = return filePath; } -export function resolveVerificationPath(eventId, historyDir) { - const separator = String(eventId).indexOf(':'); - if (separator <= 0 || separator === String(eventId).length - 1) { - throw new Error('Run-scoped event ID required'); +export function eventsPathForRun(runId, historyDir, { mustExist = true } = {}) { + const safeRunId = requireRunId(runId); + const root = path.resolve(historyDir); + const filePath = path.resolve(root, safeRunId, 'events.json'); + if (!filePath.startsWith(`${root}${path.sep}`)) throw new Error('Events path escaped history root'); + if (mustExist && !fs.existsSync(filePath)) throw new Error('Run events file not found'); + return filePath; +} + +export function assertEventBelongsToRun(eventId, historyDir) { + const value = String(eventId); + const runId = runIdFromEventId(value); + const events = readJson(eventsPathForRun(runId, historyDir), null); + if (!events || !Array.isArray(events.events)) throw new Error('Run events file is invalid'); + if (events.run_id && events.run_id !== runId) throw new Error('Run events file has a mismatched run ID'); + if (!events.events.some((event) => String(event.id) === value)) { + throw new Error('Event does not belong to the referenced run'); } - return verificationPathForRun(String(eventId).slice(0, separator), historyDir); + return runId; +} + +export function resolveVerificationPath(eventId, historyDir) { + return verificationPathForRun(runIdFromEventId(eventId), historyDir); } diff --git a/source app folder/dashboard/server.js b/source app folder/dashboard/server.js index 18edd7f..19612f5 100644 --- a/source app folder/dashboard/server.js +++ b/source app folder/dashboard/server.js @@ -8,6 +8,7 @@ import path from 'path'; import { fileURLToPath } from 'url'; import { AbortRunError, + assertEventBelongsToRun, buildTrackerArgs, createRunId, countCsvRows, @@ -19,6 +20,7 @@ import { readEventsFps, readJson, recoverPublishArtifacts, + recoverRuntimeArtifacts, resolveVerificationPath, scopeEventsForRun, runCommand, @@ -88,13 +90,17 @@ export function createFlytServer(options = {}) { const killOptions = options.killOptions || { graceMs: 1000, hardKillMs: 3000 }; for (const dir of [uploadsDir, publicDir, historyDir]) ensureDir(dir); + recoverRuntimeArtifacts(uploadsDir); recoverPublishArtifacts(publicDir); const app = express(); const storage = multer.diskStorage({ destination: (_req, _file, callback) => callback(null, uploadsDir), - filename: (_req, file, callback) => { - callback(null, `input-${Date.now()}-${randomUUID()}${path.extname(file.originalname).toLowerCase()}`); + filename: (req, file, callback) => { + const filename = `input-${Date.now()}-${randomUUID()}${path.extname(file.originalname).toLowerCase()}`; + const state = pendingUploads.get(req); + if (state) state.filePath = path.join(uploadsDir, filename); + callback(null, filename); }, }); const upload = multer({ storage, limits: { fileSize: 10 * 1024 * 1024 * 1024 } }); @@ -119,6 +125,7 @@ export function createFlytServer(options = {}) { let terminating = false; let activeRun = null; let job = blankJob(); + const pendingUploads = new Map(); const services = { countFrames: options.services?.countFrames || ((filePath, context) => getVideoFrameCount( @@ -186,12 +193,22 @@ export function createFlytServer(options = {}) { req.uploadEpoch = epoch; uploadReserved = true; let released = false; + let resolveClosed; + const state = { + request: req, + filePath: null, + closed: new Promise((resolve) => { resolveClosed = resolve; }), + release: null, + }; const release = () => { - if (!released) { - released = true; - uploadReserved = false; - } + if (released) return; + released = true; + pendingUploads.delete(req); + uploadReserved = pendingUploads.size > 0; }; + state.release = release; + pendingUploads.set(req, state); + req.once('close', () => resolveClosed()); res.once('finish', release); res.once('close', release); return next(); @@ -202,31 +219,82 @@ export function createFlytServer(options = {}) { return Array.isArray(data.reviews) ? data : { version: 1, reviews: [] }; } + function waitForUploadClose(state) { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error('Multipart upload did not close after reset')), 3000); + timer.unref?.(); + state.closed.then(() => { + clearTimeout(timer); + resolve(); + }); + }); + } + + async function removePendingUploadFile(filePath) { + if (!filePath) return; + let lastError = null; + for (let attempt = 0; attempt < 5; attempt += 1) { + try { + fs.rmSync(filePath, { force: true }); + if (!fs.existsSync(filePath)) return; + lastError = new Error(`Could not remove cancelled upload: ${filePath}`); + } catch (error) { + lastError = error; + } + await new Promise((resolve) => setTimeout(resolve, 20)); + } + throw lastError || new Error(`Could not remove cancelled upload: ${filePath}`); + } + + async function abortPendingUploads(states) { + states.forEach((state) => { + if (!state.request.destroyed) state.request.destroy(); + }); + await Promise.all(states.map(async (state) => { + await waitForUploadClose(state); + await removePendingUploadFile(state.filePath); + state.release(); + })); + } + async function stopActiveRun() { epoch += 1; const context = activeRun; + const uploadStates = [...pendingUploads.values()]; activeRun = null; - job = { ...blankJob(), status: context ? 'stopping' : 'idle' }; - if (!context) return; + const hasWork = Boolean(context) || uploadStates.length > 0; + job = { ...blankJob(), status: hasWork ? 'stopping' : 'idle' }; + if (!hasWork) return; terminating = true; - context.controller.abort(); + context?.controller.abort(); + let stoppedCleanly = false; try { - const children = [...context.children]; - await Promise.all(children.map((child) => terminateChild(child, killOptions))); - await context.done.catch((error) => { - if (error?.name !== 'AbortError') throw error; - }); + const children = context ? [...context.children] : []; + await Promise.all([ + abortPendingUploads(uploadStates), + ...children.map((child) => terminateChild(child, killOptions)), + ]); + if (context) { + await context.done.catch((error) => { + if (error?.name !== 'AbortError') throw error; + }); + } job = blankJob(); + stoppedCleanly = true; } catch (error) { job = { ...blankJob(), status: 'error', - error: `Could not terminate active run: ${error.message}`, + error: `Could not terminate active work: ${error.message}`, }; throw error; } finally { - if (context.children.size === 0) terminating = false; + if ( + stoppedCleanly + && (!context || context.children.size === 0) + && pendingUploads.size === 0 + ) terminating = false; } } @@ -436,6 +504,7 @@ export function createFlytServer(options = {}) { return res.status(400).json({ error: 'Body must include eventId and verdict.' }); } try { + assertEventBelongsToRun(eventId, historyDir); const filePath = resolveVerificationPath(eventId, historyDir); const data = readVerification(filePath); const review = { event_id: eventId, verdict, reviewed_at: new Date().toISOString() }; @@ -479,7 +548,14 @@ export function createFlytServer(options = {}) { return { app, stop: stopActiveRun, - getState: () => ({ epoch, uploadReserved, terminating, activeRun, job: { ...job } }), + getState: () => ({ + epoch, + uploadReserved, + terminating, + pendingUploads: pendingUploads.size, + activeRun, + job: { ...job }, + }), paths: { uploadsDir, publicDir, historyDir, historyMetaPath, verificationPath }, }; } diff --git a/source app folder/dashboard/src/App.jsx b/source app folder/dashboard/src/App.jsx index 1424034..ad01fac 100644 --- a/source app folder/dashboard/src/App.jsx +++ b/source app folder/dashboard/src/App.jsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useRef } from 'react'; import Papa from 'papaparse'; -import { prismDistance, proximityValue } from './metrics.js'; +import { prismDistance, prismVelocity, proximityValue } from './metrics.js'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, AreaChart, Area, ScatterChart, Scatter, ZAxis @@ -65,8 +65,8 @@ function exportPrismCsv(rows, fps) { const lines = [header.join(',')]; rows.forEach((r) => { const t = Number(r.frame) / effectiveFps; - const v1 = Number(r.fly1_speed_pxsec ?? r.fly1_speed ?? 0); - const v2 = Number(r.fly2_speed_pxsec ?? r.fly2_speed ?? 0); + const v1 = prismVelocity(r, 'fly1'); + const v2 = prismVelocity(r, 'fly2'); const dist = prismDistance(r); lines.push([t, v1, v2, dist].map(csvEscape).join(',')); }); diff --git a/source app folder/dashboard/src/metrics.js b/source app folder/dashboard/src/metrics.js index 759e97b..76e4efd 100644 --- a/source app folder/dashboard/src/metrics.js +++ b/source app folder/dashboard/src/metrics.js @@ -1,23 +1,26 @@ -export function proximityValue(row) { - if (!row) return null; - const rawProximity = row.proximity_distance; - if (rawProximity === null || rawProximity === undefined || rawProximity === '') return null; - const proximity = Number(rawProximity); - if (!Number.isFinite(proximity)) return null; - if (Number(row.occlusion_flag ?? 0) !== 0) return null; - +export function trackingObservationIsValid(row) { + if (!row || Number(row.occlusion_flag ?? 0) !== 0) return false; if (row.tracking_valid !== null && row.tracking_valid !== undefined && row.tracking_valid !== '') { - if (Number(row.tracking_valid) !== 1) return null; - } else if (row.detection_count !== null && row.detection_count !== undefined && row.detection_count !== '') { - if (Number(row.detection_count) < 2) return null; - } else if ( + return Number(row.tracking_valid) === 1; + } + if (row.detection_count !== null && row.detection_count !== undefined && row.detection_count !== '') { + return Number(row.detection_count) >= 2; + } + if ( row.fly1_area !== null && row.fly1_area !== undefined && row.fly2_area !== null && row.fly2_area !== undefined ) { - if (!(Number(row.fly1_area) > 0 && Number(row.fly2_area) > 0)) return null; + return Number(row.fly1_area) > 0 && Number(row.fly2_area) > 0; } + return true; +} - return proximity; +export function proximityValue(row) { + if (!trackingObservationIsValid(row)) return null; + const rawProximity = row.proximity_distance; + if (rawProximity === null || rawProximity === undefined || rawProximity === '') return null; + const proximity = Number(rawProximity); + return Number.isFinite(proximity) ? proximity : null; } export function computeAverageProximity(rows) { @@ -29,3 +32,11 @@ export function computeAverageProximity(rows) { export function prismDistance(row) { return proximityValue(row) ?? ''; } + +export function prismVelocity(row, flyKey) { + if (!trackingObservationIsValid(row)) return ''; + const rawSpeed = row?.[`${flyKey}_speed_pxsec`] ?? row?.[`${flyKey}_speed`]; + if (rawSpeed === null || rawSpeed === undefined || rawSpeed === '') return ''; + const speed = Number(rawSpeed); + return Number.isFinite(speed) ? speed : ''; +} diff --git a/source app folder/dashboard/tests/metrics.test.js b/source app folder/dashboard/tests/metrics.test.js index 19a5262..b612107 100644 --- a/source app folder/dashboard/tests/metrics.test.js +++ b/source app folder/dashboard/tests/metrics.test.js @@ -1,6 +1,11 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { computeAverageProximity, prismDistance, proximityValue } from '../src/metrics.js'; +import { + computeAverageProximity, + prismDistance, + prismVelocity, + proximityValue, +} from '../src/metrics.js'; test('carried coordinates from missing detections do not bias proximity metrics', () => { const rows = [ @@ -19,3 +24,22 @@ test('legacy rows remain valid only when they contain a finite non-occluded obse assert.equal(proximityValue({ proximity_distance: '', occlusion_flag: 0 }), null); assert.equal(proximityValue({ proximity_distance: 0, occlusion_flag: 1 }), null); }); + + +test('invalid or merged observations export blank individual-fly velocities', () => { + const invalid = { + tracking_valid: 0, + detection_count: 1, + occlusion_flag: 1, + fly1_speed_pxsec: 120, + fly2_speed_pxsec: 95, + }; + assert.equal(prismVelocity(invalid, 'fly1'), ''); + assert.equal(prismVelocity(invalid, 'fly2'), ''); + assert.equal(prismVelocity({ + tracking_valid: 1, + detection_count: 2, + occlusion_flag: 0, + fly1_speed_pxsec: 42.5, + }, 'fly1'), 42.5); +}); diff --git a/source app folder/dashboard/tests/server-integration.test.js b/source app folder/dashboard/tests/server-integration.test.js index c965238..322408f 100644 --- a/source app folder/dashboard/tests/server-integration.test.js +++ b/source app folder/dashboard/tests/server-integration.test.js @@ -129,7 +129,7 @@ test('rejects a simultaneous upload before a second file is accepted', async (t) assert.equal(gate.aborted, true); }); -test('reset during a multipart upload invalidates and removes the partial upload', async (t) => { +test('reset actively aborts a stalled multipart upload and reopens the gate', async (t) => { const harness = await createHarness(standardServices()); t.after(() => harness.close()); const boundary = '----flyt-boundary'; @@ -140,32 +140,39 @@ test('reset during a multipart upload invalidates and removes the partial upload '', 'partial-video-', ].join('\r\n'); - const suffix = `\r\n--${boundary}--\r\n`; - let finishUpload; - const uploadResponse = new Promise((resolve, reject) => { + const uploadOutcome = new Promise((resolve) => { const request = http.request(`${harness.baseUrl}/api/upload`, { method: 'POST', headers: { 'Content-Type': `multipart/form-data; boundary=${boundary}` }, }, (response) => { response.resume(); - response.on('end', () => resolve(response)); + response.on('end', () => resolve(response.statusCode)); }); - request.on('error', reject); + request.on('error', () => resolve('aborted')); request.write(prefix); - finishUpload = () => request.end(`remaining${suffix}`); }); await new Promise((resolve) => setTimeout(resolve, 30)); const reset = await fetch(`${harness.baseUrl}/api/reset`, { method: 'POST' }); assert.equal(reset.status, 200); - finishUpload(); - const response = await uploadResponse; - assert.equal(response.statusCode, 409); + const outcome = await Promise.race([ + uploadOutcome, + new Promise((resolve) => setTimeout(() => resolve('timeout'), 1000)), + ]); + assert.notEqual(outcome, 'timeout'); + assert.ok(outcome === 'aborted' || outcome === 409); assert.equal( fs.readdirSync(harness.paths.uploadsDir).filter((name) => name.startsWith('input-')).length, 0, ); + const state = harness.getState(); + assert.equal(state.pendingUploads, 0); + assert.equal(state.uploadReserved, false); + assert.equal(state.terminating, false); + + assert.equal((await upload(harness.baseUrl, 'after-reset.mp4')).status, 200); + await waitForStatus(harness.baseUrl, 'done'); }); test('reset remains pending until the active stage has actually stopped', async (t) => { @@ -291,6 +298,13 @@ test('a verdict made on the current run survives subsequent runs and history rel }); assert.equal(unscopedResponse.status, 400); + const orphanResponse = await fetch(`${harness.baseUrl}/api/verification`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ eventId: `${firstRunId}:evt-does-not-exist`, verdict: 'confirmed' }), + }); + assert.equal(orphanResponse.status, 400); + assert.equal((await upload(harness.baseUrl, 'second.mp4')).status, 200); const secondStatus = await waitForStatus(harness.baseUrl, 'done'); assert.notEqual(secondStatus.runId, firstRunId); diff --git a/source app folder/dashboard/tests/server-utils.test.js b/source app folder/dashboard/tests/server-utils.test.js index 9c45100..f5945bd 100644 --- a/source app folder/dashboard/tests/server-utils.test.js +++ b/source app folder/dashboard/tests/server-utils.test.js @@ -5,12 +5,14 @@ import os from 'node:os'; import path from 'node:path'; import test from 'node:test'; import { + assertEventBelongsToRun, buildTrackerArgs, parseTrackerSync, prepareRunHistoryBundle, publishBundle, readCsvAvgProximity, recoverPublishArtifacts, + recoverRuntimeArtifacts, resolveVerificationPath, runCommand, scopeEventsForRun, @@ -204,9 +206,111 @@ test('history bundle uses scoped events and excludes invalid proximity observati path.join(historyDir, runId, 'verification.json'), ); assert.equal(verificationPathForRun(runId, historyDir), path.join(historyDir, runId, 'verification.json')); + assert.equal(assertEventBelongsToRun(`${runId}:evt-001`, historyDir), runId); + assert.throws( + () => assertEventBelongsToRun(`${runId}:evt-does-not-exist`, historyDir), + /does not belong/, + ); assert.throws(() => resolveVerificationPath('evt-001', historyDir), /Run-scoped event ID required/); }); +test('rollback remains fail-closed when a new destination cannot be deleted', () => { + const dir = tempDir('flyt-delete-failure-'); + const source = path.join(dir, 'source'); + const destination = path.join(dir, 'new-destination'); + const manifestPath = path.join(dir, '.flyt-publish-delete-failure.json'); + fs.writeFileSync(source, 'new-data'); + let blockDestinationRemoval = true; + const removeFile = (filePath) => { + if (blockDestinationRemoval && filePath === destination) { + throw new Error('injected deletion failure'); + } + fs.rmSync(filePath, { force: true }); + if (fs.existsSync(filePath)) throw new Error(`still exists: ${filePath}`); + }; + + assert.throws(() => publishBundle([ + { source, destination }, + ], 'delete-failure', { + manifestDir: dir, + removeFile, + faultInjector(stage) { + if (stage === 'published') throw new Error('injected publish failure'); + }, + }), /rollback was incomplete/); + assert.equal(fs.readFileSync(destination, 'utf8'), 'new-data'); + assert.equal(fs.existsSync(manifestPath), true); + + blockDestinationRemoval = false; + recoverPublishArtifacts(dir); + assert.equal(fs.existsSync(destination), false); + assert.equal(fs.existsSync(manifestPath), false); +}); + +test('startup runtime recovery removes abandoned inputs and run workspaces only', () => { + const dir = tempDir('flyt-runtime-recovery-'); + fs.writeFileSync(path.join(dir, 'input-stale.mp4'), 'partial'); + fs.mkdirSync(path.join(dir, 'run-stale')); + fs.writeFileSync(path.join(dir, 'run-stale', 'data.tmp'), 'partial'); + fs.writeFileSync(path.join(dir, 'keep.txt'), 'keep'); + + recoverRuntimeArtifacts(dir); + assert.equal(fs.existsSync(path.join(dir, 'input-stale.mp4')), false); + assert.equal(fs.existsSync(path.join(dir, 'run-stale')), false); + assert.equal(fs.readFileSync(path.join(dir, 'keep.txt'), 'utf8'), 'keep'); +}); + +test('termination error without close rejects and never proves process exit', async () => { + class ErrorOnlyChild extends EventEmitter { + constructor() { + super(); + this.exitCode = null; + this.signalCode = null; + } + kill() { + setTimeout(() => this.emit('error', new Error('signal delivery failed')), 1); + return true; + } + } + const child = new ErrorOnlyChild(); + await assert.rejects( + terminateChild(child, { graceMs: 20, hardKillMs: 20 }), + /termination failed before close/, + ); + assert.equal(child.exitCode, null); + assert.equal(child.signalCode, null); +}); + +test('runCommand keeps an unclosed child tracked after termination failure', async () => { + class ErrorOnlyChild extends EventEmitter { + constructor() { + super(); + this.exitCode = null; + this.signalCode = null; + this.pid = 123; + } + kill() { + setTimeout(() => this.emit('error', new Error('kill failed')), 1); + return true; + } + } + const child = new ErrorOnlyChild(); + const controller = new AbortController(); + const children = new Set(); + const command = runCommand('fake', [], { + spawnFn: () => child, + signal: controller.signal, + children, + killOptions: { graceMs: 20, hardKillMs: 20 }, + }); + controller.abort(); + await assert.rejects(command, /termination failed before close/); + assert.equal(children.has(child), true); + child.exitCode = 1; + child.emit('close', 1, null); + assert.equal(children.size, 0); +}); + test('termination escalates to SIGKILL and resolves only after close', async () => { class FakeChild extends EventEmitter { constructor() { diff --git a/source app folder/tracker/tests/test_tracker.py b/source app folder/tracker/tests/test_tracker.py index b835f3d..db333e7 100644 --- a/source app folder/tracker/tests/test_tracker.py +++ b/source app folder/tracker/tests/test_tracker.py @@ -1,4 +1,5 @@ import importlib.util +import math import pathlib import unittest @@ -32,6 +33,10 @@ def test_initial_detection_has_zero_velocity(self): self.assertEqual(tracker.displacement((0, 0), (100, 100), False), 0.0) self.assertEqual(tracker.displacement((0, 0), (3, 4), True), 5.0) + def test_invalid_observation_speed_is_missing(self): + self.assertTrue(math.isnan(tracker.observed_speed(25.0, 0))) + self.assertEqual(tracker.observed_speed(25.0, 1), 25.0) + def test_separate_confident_close_flies_generate_courtship(self): rows = [row(i, proximity=20, confidence=0.8) for i in range(5)] events = tracker.detect_events(rows, 30, proximity_threshold=60, bout_min_frames=5) diff --git a/source app folder/tracker/tracker.py b/source app folder/tracker/tracker.py index c3c861c..f342c2e 100644 --- a/source app folder/tracker/tracker.py +++ b/source app folder/tracker/tracker.py @@ -189,6 +189,10 @@ def displacement(previous, current, was_initialized: bool) -> float: return float(math.dist(current, previous)) if was_initialized else 0.0 +def observed_speed(speed: float, tracking_valid: int) -> float: + return float(speed) if tracking_valid else math.nan + + def assignment_confidence(ca, cb, prev_f1, prev_f2) -> float: direct = float(math.dist(ca, prev_f1) + math.dist(cb, prev_f2)) swapped = float(math.dist(ca, prev_f2) + math.dist(cb, prev_f1)) @@ -305,7 +309,7 @@ def run_tracker(args: argparse.Namespace) -> int: areas.append(float(cv2.contourArea(contour))) f1_coords = f2_coords = (0, 0) - f1_speed = f2_speed = 0.0 + f1_speed = f2_speed = math.nan proximity = math.nan occlusion_flag = 0 identity_confidence = 0.0 @@ -333,6 +337,8 @@ def run_tracker(args: argparse.Namespace) -> int: if was_initialized: f1_speed = displacement(prev_f1, f1_coords, was_initialized) f2_speed = displacement(prev_f2, f2_coords, was_initialized) + else: + f1_speed = f2_speed = 0.0 proximity = float(math.dist(f1_coords, f2_coords)) tracking_valid = 1 prev_f1, prev_f2 = f1_coords, f2_coords @@ -362,9 +368,6 @@ def run_tracker(args: argparse.Namespace) -> int: occlusion_flag = 1 identity_confidence = 0.0 fly1_area = fly2_area = areas[0] - if was_initialized: - f1_speed = displacement(prev_f1, point, was_initialized) - f2_speed = displacement(prev_f2, point, was_initialized) prev_f1 = prev_f2 = point is_initialized = True if out: @@ -385,16 +388,18 @@ def run_tracker(args: argparse.Namespace) -> int: f1_coords, f2_coords = prev_f1, prev_f2 identity_confidence = 0.0 + measured_f1_speed = observed_speed(f1_speed, tracking_valid) + measured_f2_speed = observed_speed(f2_speed, tracking_valid) data.append({ "frame": frame_num, "fly1_x": f1_coords[0], "fly1_y": f1_coords[1], "fly2_x": f2_coords[0], "fly2_y": f2_coords[1], - "fly1_speed": f1_speed, - "fly2_speed": f2_speed, - "fly1_speed_pxsec": round(f1_speed * effective_fps, 4), - "fly2_speed_pxsec": round(f2_speed * effective_fps, 4), + "fly1_speed": measured_f1_speed, + "fly2_speed": measured_f2_speed, + "fly1_speed_pxsec": round(measured_f1_speed * effective_fps, 4), + "fly2_speed_pxsec": round(measured_f2_speed * effective_fps, 4), "activity_level": activity_level, "proximity_distance": proximity, "tracking_valid": tracking_valid, From 9b662acc4a5654d054320be66d8c1f841275886b Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 13:07:39 +0530 Subject: [PATCH 73/75] chore: remove temporary review hardening machinery --- .github/scripts/review-edge-hardening.part00 | 373 ------------------ .github/scripts/review-edge-hardening.part01 | 328 --------------- .github/scripts/review-edge-hardening.part02 | 322 --------------- .github/scripts/review-edge-hardening.part03 | 314 --------------- .../workflows/apply-review-edge-hardening.yml | 89 ----- 5 files changed, 1426 deletions(-) delete mode 100644 .github/scripts/review-edge-hardening.part00 delete mode 100644 .github/scripts/review-edge-hardening.part01 delete mode 100644 .github/scripts/review-edge-hardening.part02 delete mode 100644 .github/scripts/review-edge-hardening.part03 delete mode 100644 .github/workflows/apply-review-edge-hardening.yml diff --git a/.github/scripts/review-edge-hardening.part00 b/.github/scripts/review-edge-hardening.part00 deleted file mode 100644 index 0a92514..0000000 --- a/.github/scripts/review-edge-hardening.part00 +++ /dev/null @@ -1,373 +0,0 @@ -from pathlib import Path - -ROOT = Path('.') - - -def read(path: str) -> str: - return (ROOT / path).read_text(encoding='utf-8') - - -def write(path: str, content: str) -> None: - (ROOT / path).write_text(content, encoding='utf-8') - - -def replace_once(content: str, old: str, new: str, label: str) -> str: - count = content.count(old) - if count != 1: - raise RuntimeError(f'{label}: expected one occurrence, found {count}') - return content.replace(old, new, 1) - - -# --------------------------------------------------------------------------- -# server-utils.js: strict termination, strict transaction deletion, runtime -# cleanup, and event membership validation. -# --------------------------------------------------------------------------- -utils_path = 'source app folder/dashboard/server-utils.js' -utils = read(utils_path) - -utils = replace_once( - utils, - '''export function safeRemoveDir(dirPath) { - if (!dirPath) return; - try { fs.rmSync(dirPath, { recursive: true, force: true }); } catch (error) { - console.error(`[Server] Failed to remove ${dirPath}: ${error.message}`); - } -} - -export function readJson(filePath, fallback) {''', - '''export function safeRemoveDir(dirPath) { - if (!dirPath) return; - try { fs.rmSync(dirPath, { recursive: true, force: true }); } catch (error) { - console.error(`[Server] Failed to remove ${dirPath}: ${error.message}`); - } -} - -export function removeTransactionalFile(filePath) { - if (!filePath) return; - fs.rmSync(filePath, { force: true }); - if (fs.existsSync(filePath)) { - throw new Error(`Could not remove transactional path: ${filePath}`); - } -} - -export function recoverRuntimeArtifacts(uploadsDir) { - ensureDir(uploadsDir); - fs.readdirSync(uploadsDir, { withFileTypes: true }).forEach((entry) => { - const fullPath = path.join(uploadsDir, entry.name); - if (entry.isFile() && entry.name.startsWith('input-')) safeUnlink(fullPath); - else if (entry.isDirectory() && entry.name.startsWith('run-')) safeRemoveDir(fullPath); - }); -} - -export function readJson(filePath, fallback) {''', - 'insert strict deletion and runtime recovery', -) - -utils = replace_once( - utils, - '''const terminationPromises = new WeakMap(); - -export function terminateChild(child, { graceMs = 1000, hardKillMs = 3000 } = {}) { - if (!child || child.exitCode !== null || child.signalCode) return Promise.resolve(); - if (terminationPromises.has(child)) return terminationPromises.get(child); - - const promise = new Promise((resolve, reject) => { - let settled = false; - let graceTimer; - let hardTimer; - const cleanup = () => { - clearTimeout(graceTimer); - clearTimeout(hardTimer); - child.removeListener('close', finish); - child.removeListener('error', finish); - }; - const finish = () => { - if (settled) return; - settled = true; - cleanup(); - resolve(); - }; - const fail = () => { - if (settled) return; - settled = true; - cleanup(); - reject(new Error('Child process did not terminate after SIGKILL')); - }; - - child.once('close', finish); - child.once('error', finish); - try { child.kill('SIGTERM'); } catch { finish(); return; } - graceTimer = setTimeout(() => { - if (settled) return; - try { child.kill('SIGKILL'); } catch { finish(); return; } - hardTimer = setTimeout(fail, hardKillMs); - hardTimer.unref?.(); - }, graceMs); - graceTimer.unref?.(); - }).finally(() => terminationPromises.delete(child)); - - terminationPromises.set(child, promise); - return promise; -} -''', - '''const terminationPromises = new WeakMap(); - -function childHasExited(child) { - return Boolean(child) && ( - (child.exitCode !== null && child.exitCode !== undefined) - || Boolean(child.signalCode) - ); -} - -export function terminateChild(child, { graceMs = 1000, hardKillMs = 3000 } = {}) { - if (!child || childHasExited(child)) return Promise.resolve(); - if (terminationPromises.has(child)) return terminationPromises.get(child); - - const promise = new Promise((resolve, reject) => { - let settled = false; - let graceTimer; - let hardTimer; - const cleanup = () => { - clearTimeout(graceTimer); - clearTimeout(hardTimer); - child.removeListener('close', finish); - child.removeListener('error', onError); - }; - const finish = () => { - if (settled) return; - settled = true; - cleanup(); - resolve(); - }; - const fail = (error) => { - if (settled) return; - settled = true; - cleanup(); - reject(error); - }; - const onError = (error) => { - if (childHasExited(child)) finish(); - else fail(new Error(`Child process termination failed before close: ${error.message}`)); - }; - const sendSignal = (signal) => { - try { - const delivered = child.kill(signal); - if (delivered === false && !childHasExited(child)) { - fail(new Error(`Could not deliver ${signal} to child process`)); - return false; - } - } catch (error) { - if (childHasExited(child)) finish(); - else fail(new Error(`Could not deliver ${signal} to child process: ${error.message}`)); - return false; - } - return true; - }; - - child.once('close', finish); - child.once('error', onError); - if (!sendSignal('SIGTERM')) return; - graceTimer = setTimeout(() => { - if (settled || childHasExited(child)) { - if (childHasExited(child)) finish(); - return; - } - if (!sendSignal('SIGKILL')) return; - hardTimer = setTimeout(() => { - if (childHasExited(child)) finish(); - else fail(new Error('Child process did not close after SIGKILL')); - }, hardKillMs); - hardTimer.unref?.(); - }, graceMs); - graceTimer.unref?.(); - }).finally(() => terminationPromises.delete(child)); - - terminationPromises.set(child, promise); - return promise; -} -''', - 'strict terminateChild', -) - -utils = replace_once( - utils, - '''export function runCommand(command, args, { - spawnFn = nodeSpawn, - signal, - children, - onStdout, - onStderr, - killOptions, -} = {}) { - if (signal?.aborted) return Promise.reject(new AbortRunError()); - - return new Promise((resolve, reject) => { - let child; - try { - child = spawnFn(command, args); - } catch (error) { - reject(error); - return; - } - children?.add(child); - let stdout = ''; - let stderr = ''; - let settled = false; - - const cleanup = () => { - children?.delete(child); - signal?.removeEventListener('abort', onAbort); - }; - const settle = (handler, value) => { - if (settled) return; - settled = true; - cleanup(); - handler(value); - }; - const onAbort = () => { - terminateChild(child, killOptions) - .then(() => settle(reject, new AbortRunError())) - .catch((error) => settle(reject, error)); - }; - - child.stdout?.on('data', (chunk) => { - const text = chunk.toString(); - stdout += text; - onStdout?.(text); - }); - child.stderr?.on('data', (chunk) => { - const text = chunk.toString(); - stderr += text; - onStderr?.(text); - }); - child.once('error', (error) => settle(reject, error)); - child.once('close', (code, closeSignal) => { - if (signal?.aborted) settle(reject, new AbortRunError()); - else settle(resolve, { code, signal: closeSignal, stdout, stderr }); - }); - signal?.addEventListener('abort', onAbort, { once: true }); - if (signal?.aborted) onAbort(); - }); -} -''', - '''export function runCommand(command, args, { - spawnFn = nodeSpawn, - signal, - children, - onStdout, - onStderr, - killOptions, -} = {}) { - if (signal?.aborted) return Promise.reject(new AbortRunError()); - - return new Promise((resolve, reject) => { - let child; - try { - child = spawnFn(command, args); - } catch (error) { - reject(error); - return; - } - children?.add(child); - let stdout = ''; - let stderr = ''; - let settled = false; - let processError = null; - - const cleanup = ({ removeChild = true } = {}) => { - if (removeChild) children?.delete(child); - signal?.removeEventListener('abort', onAbort); - }; - const settle = (handler, value, options) => { - if (settled) return; - settled = true; - cleanup(options); - handler(value); - }; - const onAbort = () => { - terminateChild(child, killOptions) - .then(() => settle(reject, new AbortRunError(), { removeChild: true })) - .catch((error) => settle(reject, error, { removeChild: childHasExited(child) })); - }; - - child.stdout?.on('data', (chunk) => { - const text = chunk.toString(); - stdout += text; - onStdout?.(text); - }); - child.stderr?.on('data', (chunk) => { - const text = chunk.toString(); - stderr += text; - onStderr?.(text); - }); - child.once('error', (error) => { - processError = error; - if (!child.pid || childHasExited(child)) { - settle(reject, error, { removeChild: true }); - } - }); - child.once('close', (code, closeSignal) => { - children?.delete(child); - if (settled) return; - if (signal?.aborted) settle(reject, new AbortRunError(), { removeChild: true }); - else if (processError) settle(reject, processError, { removeChild: true }); - else settle(resolve, { code, signal: closeSignal, stdout, stderr }, { removeChild: true }); - }); - signal?.addEventListener('abort', onAbort, { once: true }); - if (signal?.aborted) onAbort(); - }); -} -''', - 'preserve unclosed child after termination failure', -) - -utils = replace_once( - utils, - '''function rollbackPublishManifest(manifest) { - const errors = []; - [...manifest.entries].reverse().forEach((entry) => { - try { - if (entry.hadDestination) { - if (fs.existsSync(entry.backup)) { - safeUnlink(entry.destination); - ensureDir(path.dirname(entry.destination)); - fs.renameSync(entry.backup, entry.destination); - } else if (!fs.existsSync(entry.destination)) { - throw new Error(`Cannot restore missing prior destination: ${entry.destination}`); - } - } else { - safeUnlink(entry.destination); - safeUnlink(entry.backup); - } - safeUnlink(entry.stage); - } catch (error) { - errors.push(error); - } - }); - if (errors.length) { - throw new globalThis.AggregateError(errors, 'Publication transaction rollback failed'); - } -} - -function finalizeCommittedManifest(manifest) { - const missing = manifest.entries.filter((entry) => !fs.existsSync(entry.destination)); - if (missing.length) { - rollbackPublishManifest(manifest); - return; - } - manifest.entries.forEach((entry) => { - safeUnlink(entry.stage); - safeUnlink(entry.backup); - }); -} -''', - '''function rollbackPublishManifest(manifest, { removeFile = removeTransactionalFile } = {}) { - const errors = []; - [...manifest.entries].reverse().forEach((entry) => { - try { - if (entry.hadDestination) { - if (fs.existsSync(entry.backup)) { - removeFile(entry.destination); - ensureDir(path.dirname(entry.destination)); - fs.renameSync(entry.backup, entry.destination); - } else if (!fs.existsSync(entry.destination)) { diff --git a/.github/scripts/review-edge-hardening.part01 b/.github/scripts/review-edge-hardening.part01 deleted file mode 100644 index 0f556e0..0000000 --- a/.github/scripts/review-edge-hardening.part01 +++ /dev/null @@ -1,328 +0,0 @@ - throw new Error(`Cannot restore missing prior destination: ${entry.destination}`); - } - } else { - removeFile(entry.destination); - removeFile(entry.backup); - } - removeFile(entry.stage); - } catch (error) { - errors.push(error); - } - }); - if (errors.length) { - throw new globalThis.AggregateError(errors, 'Publication transaction rollback failed'); - } -} - -function finalizeCommittedManifest(manifest, { removeFile = removeTransactionalFile } = {}) { - const missing = manifest.entries.filter((entry) => !fs.existsSync(entry.destination)); - if (missing.length) { - rollbackPublishManifest(manifest, { removeFile }); - return; - } - manifest.entries.forEach((entry) => { - removeFile(entry.stage); - removeFile(entry.backup); - }); -} -''', - 'strict transaction cleanup', -) - -utils = replace_once( - utils, - '''export function publishBundle(entries, token, { faultInjector, manifestDir } = {}) {''', - '''export function publishBundle(entries, token, { - faultInjector, - manifestDir, - removeFile = removeTransactionalFile, -} = {}) {''', - 'publishBundle remove injection', -) - -utils = replace_once( - utils, - ''' finalizeCommittedManifest(manifest); - safeUnlink(manifestPath); - } catch (error) { - if (error instanceof SimulatedProcessCrash) throw error; - try { - rollbackPublishManifest(manifest); - safeUnlink(manifestPath); - } catch (rollbackError) { - throw new globalThis.AggregateError([error, rollbackError], 'Publication failed and rollback was incomplete'); - } - throw error; - } -} - -export function recoverPublishArtifacts(directory) {''', - ''' finalizeCommittedManifest(manifest, { removeFile }); - removeFile(manifestPath); - } catch (error) { - if (error instanceof SimulatedProcessCrash) throw error; - if (manifest.state === 'committed') throw error; - try { - rollbackPublishManifest(manifest, { removeFile }); - removeFile(manifestPath); - } catch (rollbackError) { - throw new globalThis.AggregateError([error, rollbackError], 'Publication failed and rollback was incomplete'); - } - throw error; - } -} - -export function recoverPublishArtifacts(directory, { removeFile = removeTransactionalFile } = {}) {''', - 'strict publish success and rollback', -) - -utils = replace_once( - utils, - ''' manifests.forEach((manifestPath) => { - const manifest = validatePublishManifest(readJson(manifestPath, null), manifestPath); - if (manifest.state === 'committed') finalizeCommittedManifest(manifest); - else rollbackPublishManifest(manifest); - safeUnlink(manifestPath); - }); - - const artifacts = listFilesRecursive(directory); - const orphanBackups = artifacts.filter((filePath) => filePath.endsWith('.bak')); - if (orphanBackups.length) { - throw new Error( - `Unrecoverable publication backups without a transaction manifest: ${orphanBackups.join(', ')}`, - ); - } - artifacts.filter((filePath) => filePath.endsWith('.new')).forEach(safeUnlink); -}''', - ''' manifests.forEach((manifestPath) => { - const manifest = validatePublishManifest(readJson(manifestPath, null), manifestPath); - if (manifest.state === 'committed') finalizeCommittedManifest(manifest, { removeFile }); - else rollbackPublishManifest(manifest, { removeFile }); - removeFile(manifestPath); - }); - - const artifacts = listFilesRecursive(directory); - const orphanBackups = artifacts.filter((filePath) => filePath.endsWith('.bak')); - if (orphanBackups.length) { - throw new Error( - `Unrecoverable publication backups without a transaction manifest: ${orphanBackups.join(', ')}`, - ); - } - artifacts.filter((filePath) => filePath.endsWith('.new')).forEach((filePath) => removeFile(filePath)); -}''', - 'strict startup transaction cleanup', -) - -utils = replace_once( - utils, - '''export function verificationPathForRun(runId, historyDir, { mustExist = true } = {}) { - const safeRunId = requireRunId(runId); - const root = path.resolve(historyDir); - const filePath = path.resolve(root, safeRunId, 'verification.json'); - if (!filePath.startsWith(`${root}${path.sep}`)) throw new Error('Verification path escaped history root'); - if (mustExist && !fs.existsSync(filePath)) throw new Error('Run verification file not found'); - return filePath; -} - -export function resolveVerificationPath(eventId, historyDir) { - const separator = String(eventId).indexOf(':'); - if (separator <= 0 || separator === String(eventId).length - 1) { - throw new Error('Run-scoped event ID required'); - } - return verificationPathForRun(String(eventId).slice(0, separator), historyDir); -} -''', - '''export function runIdFromEventId(eventId) { - const value = String(eventId); - const separator = value.indexOf(':'); - if (separator <= 0 || separator === value.length - 1) { - throw new Error('Run-scoped event ID required'); - } - return requireRunId(value.slice(0, separator)); -} - -export function verificationPathForRun(runId, historyDir, { mustExist = true } = {}) { - const safeRunId = requireRunId(runId); - const root = path.resolve(historyDir); - const filePath = path.resolve(root, safeRunId, 'verification.json'); - if (!filePath.startsWith(`${root}${path.sep}`)) throw new Error('Verification path escaped history root'); - if (mustExist && !fs.existsSync(filePath)) throw new Error('Run verification file not found'); - return filePath; -} - -export function eventsPathForRun(runId, historyDir, { mustExist = true } = {}) { - const safeRunId = requireRunId(runId); - const root = path.resolve(historyDir); - const filePath = path.resolve(root, safeRunId, 'events.json'); - if (!filePath.startsWith(`${root}${path.sep}`)) throw new Error('Events path escaped history root'); - if (mustExist && !fs.existsSync(filePath)) throw new Error('Run events file not found'); - return filePath; -} - -export function assertEventBelongsToRun(eventId, historyDir) { - const value = String(eventId); - const runId = runIdFromEventId(value); - const events = readJson(eventsPathForRun(runId, historyDir), null); - if (!events || !Array.isArray(events.events)) throw new Error('Run events file is invalid'); - if (events.run_id && events.run_id !== runId) throw new Error('Run events file has a mismatched run ID'); - if (!events.events.some((event) => String(event.id) === value)) { - throw new Error('Event does not belong to the referenced run'); - } - return runId; -} - -export function resolveVerificationPath(eventId, historyDir) { - return verificationPathForRun(runIdFromEventId(eventId), historyDir); -} -''', - 'validate review event membership', -) - -write(utils_path, utils) - -# --------------------------------------------------------------------------- -# server.js: actively abort multipart uploads, recover stale runtime workspaces, -# validate event membership, and remain fail-closed after termination errors. -# --------------------------------------------------------------------------- -server_path = 'source app folder/dashboard/server.js' -server = read(server_path) - -server = replace_once( - server, - '''import { - AbortRunError, - buildTrackerArgs,''', - '''import { - AbortRunError, - assertEventBelongsToRun, - buildTrackerArgs,''', - 'server import event validation', -) -server = replace_once( - server, - ''' readJson, - recoverPublishArtifacts, - resolveVerificationPath,''', - ''' readJson, - recoverPublishArtifacts, - recoverRuntimeArtifacts, - resolveVerificationPath,''', - 'server import runtime recovery', -) -server = replace_once( - server, - ''' for (const dir of [uploadsDir, publicDir, historyDir]) ensureDir(dir); - recoverPublishArtifacts(publicDir);''', - ''' for (const dir of [uploadsDir, publicDir, historyDir]) ensureDir(dir); - recoverRuntimeArtifacts(uploadsDir); - recoverPublishArtifacts(publicDir);''', - 'startup runtime recovery', -) -server = replace_once( - server, - ''' const storage = multer.diskStorage({ - destination: (_req, _file, callback) => callback(null, uploadsDir), - filename: (_req, file, callback) => { - callback(null, `input-${Date.now()}-${randomUUID()}${path.extname(file.originalname).toLowerCase()}`); - }, - });''', - ''' const storage = multer.diskStorage({ - destination: (_req, _file, callback) => callback(null, uploadsDir), - filename: (req, file, callback) => { - const filename = `input-${Date.now()}-${randomUUID()}${path.extname(file.originalname).toLowerCase()}`; - const state = pendingUploads.get(req); - if (state) state.filePath = path.join(uploadsDir, filename); - callback(null, filename); - }, - });''', - 'track partial upload filename', -) -server = replace_once( - server, - ''' let terminating = false; - let activeRun = null; - let job = blankJob();''', - ''' let terminating = false; - let activeRun = null; - let job = blankJob(); - const pendingUploads = new Map();''', - 'pending upload state', -) -server = replace_once( - server, - ''' function reserveUpload(req, res, next) { - if (isBusy()) { - return res.status(409).json({ error: 'A video is already uploading, processing, or stopping.' }); - } - req.uploadEpoch = epoch; - uploadReserved = true; - let released = false; - const release = () => { - if (!released) { - released = true; - uploadReserved = false; - } - }; - res.once('finish', release); - res.once('close', release); - return next(); - } -''', - ''' function reserveUpload(req, res, next) { - if (isBusy()) { - return res.status(409).json({ error: 'A video is already uploading, processing, or stopping.' }); - } - req.uploadEpoch = epoch; - uploadReserved = true; - let released = false; - let resolveClosed; - const state = { - request: req, - filePath: null, - closed: new Promise((resolve) => { resolveClosed = resolve; }), - release: null, - }; - const release = () => { - if (released) return; - released = true; - pendingUploads.delete(req); - uploadReserved = pendingUploads.size > 0; - }; - state.release = release; - pendingUploads.set(req, state); - req.once('close', () => resolveClosed()); - res.once('finish', release); - res.once('close', release); - return next(); - } -''', - 'active multipart reservation', -) -server = replace_once( - server, - ''' function readVerification(filePath) { - const data = readJson(filePath, { version: 1, reviews: [] }); - return Array.isArray(data.reviews) ? data : { version: 1, reviews: [] }; - } - - async function stopActiveRun() { - epoch += 1; - const context = activeRun; - activeRun = null; - job = { ...blankJob(), status: context ? 'stopping' : 'idle' }; - if (!context) return; - - terminating = true; - context.controller.abort(); - try { - const children = [...context.children]; - await Promise.all(children.map((child) => terminateChild(child, killOptions))); - await context.done.catch((error) => { - if (error?.name !== 'AbortError') throw error; - }); - job = blankJob(); - } catch (error) { - job = { - ...blankJob(), - status: 'error', - error: `Could not terminate active run: ${error.message}`, diff --git a/.github/scripts/review-edge-hardening.part02 b/.github/scripts/review-edge-hardening.part02 deleted file mode 100644 index e9159cc..0000000 --- a/.github/scripts/review-edge-hardening.part02 +++ /dev/null @@ -1,322 +0,0 @@ - }; - throw error; - } finally { - if (context.children.size === 0) terminating = false; - } - } -''', - ''' function readVerification(filePath) { - const data = readJson(filePath, { version: 1, reviews: [] }); - return Array.isArray(data.reviews) ? data : { version: 1, reviews: [] }; - } - - function waitForUploadClose(state) { - return new Promise((resolve, reject) => { - const timer = setTimeout(() => reject(new Error('Multipart upload did not close after reset')), 3000); - timer.unref?.(); - state.closed.then(() => { - clearTimeout(timer); - resolve(); - }); - }); - } - - async function removePendingUploadFile(filePath) { - if (!filePath) return; - let lastError = null; - for (let attempt = 0; attempt < 5; attempt += 1) { - try { - fs.rmSync(filePath, { force: true }); - if (!fs.existsSync(filePath)) return; - lastError = new Error(`Could not remove cancelled upload: ${filePath}`); - } catch (error) { - lastError = error; - } - await new Promise((resolve) => setTimeout(resolve, 20)); - } - throw lastError || new Error(`Could not remove cancelled upload: ${filePath}`); - } - - async function abortPendingUploads(states) { - states.forEach((state) => { - if (!state.request.destroyed) state.request.destroy(); - }); - await Promise.all(states.map(async (state) => { - await waitForUploadClose(state); - await removePendingUploadFile(state.filePath); - state.release(); - })); - } - - async function stopActiveRun() { - epoch += 1; - const context = activeRun; - const uploadStates = [...pendingUploads.values()]; - activeRun = null; - const hasWork = Boolean(context) || uploadStates.length > 0; - job = { ...blankJob(), status: hasWork ? 'stopping' : 'idle' }; - if (!hasWork) return; - - terminating = true; - context?.controller.abort(); - let stoppedCleanly = false; - try { - const children = context ? [...context.children] : []; - await Promise.all([ - abortPendingUploads(uploadStates), - ...children.map((child) => terminateChild(child, killOptions)), - ]); - if (context) { - await context.done.catch((error) => { - if (error?.name !== 'AbortError') throw error; - }); - } - job = blankJob(); - stoppedCleanly = true; - } catch (error) { - job = { - ...blankJob(), - status: 'error', - error: `Could not terminate active work: ${error.message}`, - }; - throw error; - } finally { - if ( - stoppedCleanly - && (!context || context.children.size === 0) - && pendingUploads.size === 0 - ) terminating = false; - } - } -''', - 'abort multipart uploads and strict stop gate', -) -server = replace_once( - server, - ''' try { - const filePath = resolveVerificationPath(eventId, historyDir); - const data = readVerification(filePath);''', - ''' try { - assertEventBelongsToRun(eventId, historyDir); - const filePath = resolveVerificationPath(eventId, historyDir); - const data = readVerification(filePath);''', - 'review membership validation', -) -server = replace_once( - server, - ''' getState: () => ({ epoch, uploadReserved, terminating, activeRun, job: { ...job } }),''', - ''' getState: () => ({ - epoch, - uploadReserved, - terminating, - pendingUploads: pendingUploads.size, - activeRun, - job: { ...job }, - }),''', - 'expose pending upload state for tests', -) -write(server_path, server) - -# --------------------------------------------------------------------------- -# Tracker and frontend metrics: invalid individual-fly velocity is missing too. -# --------------------------------------------------------------------------- -tracker_path = 'source app folder/tracker/tracker.py' -tracker = read(tracker_path) -tracker = replace_once( - tracker, - '''def displacement(previous, current, was_initialized: bool) -> float: - return float(math.dist(current, previous)) if was_initialized else 0.0 - - -def assignment_confidence''', - '''def displacement(previous, current, was_initialized: bool) -> float: - return float(math.dist(current, previous)) if was_initialized else 0.0 - - -def observed_speed(speed: float, tracking_valid: int) -> float: - return float(speed) if tracking_valid else math.nan - - -def assignment_confidence''', - 'tracker observed speed helper', -) -tracker = replace_once( - tracker, - ''' f1_speed = f2_speed = 0.0 - proximity = math.nan''', - ''' f1_speed = f2_speed = math.nan - proximity = math.nan''', - 'invalid speed defaults missing', -) -tracker = replace_once( - tracker, - ''' if was_initialized: - f1_speed = displacement(prev_f1, f1_coords, was_initialized) - f2_speed = displacement(prev_f2, f2_coords, was_initialized) - proximity = float(math.dist(f1_coords, f2_coords))''', - ''' if was_initialized: - f1_speed = displacement(prev_f1, f1_coords, was_initialized) - f2_speed = displacement(prev_f2, f2_coords, was_initialized) - else: - f1_speed = f2_speed = 0.0 - proximity = float(math.dist(f1_coords, f2_coords))''', - 'valid initial speed zero', -) -tracker = replace_once( - tracker, - ''' if was_initialized: - f1_speed = displacement(prev_f1, point, was_initialized) - f2_speed = displacement(prev_f2, point, was_initialized) - prev_f1 = prev_f2 = point''', - ''' prev_f1 = prev_f2 = point''', - 'do not fabricate merged fly speeds', -) -tracker = replace_once( - tracker, - ''' data.append({ - "frame": frame_num, - "fly1_x": f1_coords[0],''', - ''' measured_f1_speed = observed_speed(f1_speed, tracking_valid) - measured_f2_speed = observed_speed(f2_speed, tracking_valid) - data.append({ - "frame": frame_num, - "fly1_x": f1_coords[0],''', - 'compute validity-aware speeds', -) -tracker = replace_once( - tracker, - ''' "fly1_speed": f1_speed, - "fly2_speed": f2_speed, - "fly1_speed_pxsec": round(f1_speed * effective_fps, 4), - "fly2_speed_pxsec": round(f2_speed * effective_fps, 4),''', - ''' "fly1_speed": measured_f1_speed, - "fly2_speed": measured_f2_speed, - "fly1_speed_pxsec": round(measured_f1_speed * effective_fps, 4), - "fly2_speed_pxsec": round(measured_f2_speed * effective_fps, 4),''', - 'write only observed speeds', -) -write(tracker_path, tracker) - -metrics_path = 'source app folder/dashboard/src/metrics.js' -metrics = read(metrics_path) -metrics = replace_once( - metrics, - '''export function proximityValue(row) { - if (!row) return null; - const rawProximity = row.proximity_distance; - if (rawProximity === null || rawProximity === undefined || rawProximity === '') return null; - const proximity = Number(rawProximity); - if (!Number.isFinite(proximity)) return null; - if (Number(row.occlusion_flag ?? 0) !== 0) return null; - - if (row.tracking_valid !== null && row.tracking_valid !== undefined && row.tracking_valid !== '') { - if (Number(row.tracking_valid) !== 1) return null; - } else if (row.detection_count !== null && row.detection_count !== undefined && row.detection_count !== '') { - if (Number(row.detection_count) < 2) return null; - } else if ( - row.fly1_area !== null && row.fly1_area !== undefined - && row.fly2_area !== null && row.fly2_area !== undefined - ) { - if (!(Number(row.fly1_area) > 0 && Number(row.fly2_area) > 0)) return null; - } - - return proximity; -} - -export function computeAverageProximity(rows) { - const values = rows.map(proximityValue).filter((value) => value !== null); - if (!values.length) return 0; - return Math.round(values.reduce((sum, value) => sum + value, 0) / values.length); -} - -export function prismDistance(row) { - return proximityValue(row) ?? ''; -} -''', - '''export function trackingObservationIsValid(row) { - if (!row || Number(row.occlusion_flag ?? 0) !== 0) return false; - if (row.tracking_valid !== null && row.tracking_valid !== undefined && row.tracking_valid !== '') { - return Number(row.tracking_valid) === 1; - } - if (row.detection_count !== null && row.detection_count !== undefined && row.detection_count !== '') { - return Number(row.detection_count) >= 2; - } - if ( - row.fly1_area !== null && row.fly1_area !== undefined - && row.fly2_area !== null && row.fly2_area !== undefined - ) { - return Number(row.fly1_area) > 0 && Number(row.fly2_area) > 0; - } - return true; -} - -export function proximityValue(row) { - if (!trackingObservationIsValid(row)) return null; - const rawProximity = row.proximity_distance; - if (rawProximity === null || rawProximity === undefined || rawProximity === '') return null; - const proximity = Number(rawProximity); - return Number.isFinite(proximity) ? proximity : null; -} - -export function computeAverageProximity(rows) { - const values = rows.map(proximityValue).filter((value) => value !== null); - if (!values.length) return 0; - return Math.round(values.reduce((sum, value) => sum + value, 0) / values.length); -} - -export function prismDistance(row) { - return proximityValue(row) ?? ''; -} - -export function prismVelocity(row, flyKey) { - if (!trackingObservationIsValid(row)) return ''; - const rawSpeed = row?.[`${flyKey}_speed_pxsec`] ?? row?.[`${flyKey}_speed`]; - if (rawSpeed === null || rawSpeed === undefined || rawSpeed === '') return ''; - const speed = Number(rawSpeed); - return Number.isFinite(speed) ? speed : ''; -} -''', - 'validity-aware Prism velocity', -) -write(metrics_path, metrics) - -app_path = 'source app folder/dashboard/src/App.jsx' -app = read(app_path) -app = replace_once( - app, - '''import { prismDistance, proximityValue } from './metrics.js';''', - '''import { prismDistance, prismVelocity, proximityValue } from './metrics.js';''', - 'App metrics import', -) -app = replace_once( - app, - ''' const v1 = Number(r.fly1_speed_pxsec ?? r.fly1_speed ?? 0); - const v2 = Number(r.fly2_speed_pxsec ?? r.fly2_speed ?? 0); - const dist = prismDistance(r);''', - ''' const v1 = prismVelocity(r, 'fly1'); - const v2 = prismVelocity(r, 'fly2'); - const dist = prismDistance(r);''', - 'blank invalid Prism velocity', -) -write(app_path, app) - -# --------------------------------------------------------------------------- -# Tests. -# --------------------------------------------------------------------------- -utils_test_path = 'source app folder/dashboard/tests/server-utils.test.js' -utils_test = read(utils_test_path) -utils_test = replace_once( - utils_test, - '''import { - buildTrackerArgs, - parseTrackerSync, - prepareRunHistoryBundle,''', - '''import { - assertEventBelongsToRun, - buildTrackerArgs, - parseTrackerSync, - prepareRunHistoryBundle,''', - 'utils test event import', -) -utils_test = replace_once( - utils_test, diff --git a/.github/scripts/review-edge-hardening.part03 b/.github/scripts/review-edge-hardening.part03 deleted file mode 100644 index 603428f..0000000 --- a/.github/scripts/review-edge-hardening.part03 +++ /dev/null @@ -1,314 +0,0 @@ - ''' readCsvAvgProximity, - recoverPublishArtifacts, - resolveVerificationPath,''', - ''' readCsvAvgProximity, - recoverPublishArtifacts, - recoverRuntimeArtifacts, - resolveVerificationPath,''', - 'utils test runtime import', -) -utils_test = replace_once( - utils_test, - ''' publishBundle(bundle.entries, 'history-token', { manifestDir: dir }); - - const scopedEvents = JSON.parse(fs.readFileSync(path.join(historyDir, runId, 'events.json')));''', - ''' publishBundle(bundle.entries, 'history-token', { manifestDir: dir }); - - const scopedEvents = JSON.parse(fs.readFileSync(path.join(historyDir, runId, 'events.json')));''', - 'history test stable anchor', -) -utils_test = replace_once( - utils_test, - ''' assert.equal(verificationPathForRun(runId, historyDir), path.join(historyDir, runId, 'verification.json')); - assert.throws(() => resolveVerificationPath('evt-001', historyDir), /Run-scoped event ID required/); -});''', - ''' assert.equal(verificationPathForRun(runId, historyDir), path.join(historyDir, runId, 'verification.json')); - assert.equal(assertEventBelongsToRun(`${runId}:evt-001`, historyDir), runId); - assert.throws( - () => assertEventBelongsToRun(`${runId}:evt-does-not-exist`, historyDir), - /does not belong/, - ); - assert.throws(() => resolveVerificationPath('evt-001', historyDir), /Run-scoped event ID required/); -});''', - 'event membership utility assertions', -) - -insert_before = "test('termination escalates to SIGKILL and resolves only after close', async () => {" -new_tests = '''test('rollback remains fail-closed when a new destination cannot be deleted', () => { - const dir = tempDir('flyt-delete-failure-'); - const source = path.join(dir, 'source'); - const destination = path.join(dir, 'new-destination'); - const manifestPath = path.join(dir, '.flyt-publish-delete-failure.json'); - fs.writeFileSync(source, 'new-data'); - let blockDestinationRemoval = true; - const removeFile = (filePath) => { - if (blockDestinationRemoval && filePath === destination) { - throw new Error('injected deletion failure'); - } - fs.rmSync(filePath, { force: true }); - if (fs.existsSync(filePath)) throw new Error(`still exists: ${filePath}`); - }; - - assert.throws(() => publishBundle([ - { source, destination }, - ], 'delete-failure', { - manifestDir: dir, - removeFile, - faultInjector(stage) { - if (stage === 'published') throw new Error('injected publish failure'); - }, - }), /rollback was incomplete/); - assert.equal(fs.readFileSync(destination, 'utf8'), 'new-data'); - assert.equal(fs.existsSync(manifestPath), true); - - blockDestinationRemoval = false; - recoverPublishArtifacts(dir); - assert.equal(fs.existsSync(destination), false); - assert.equal(fs.existsSync(manifestPath), false); -}); - -test('startup runtime recovery removes abandoned inputs and run workspaces only', () => { - const dir = tempDir('flyt-runtime-recovery-'); - fs.writeFileSync(path.join(dir, 'input-stale.mp4'), 'partial'); - fs.mkdirSync(path.join(dir, 'run-stale')); - fs.writeFileSync(path.join(dir, 'run-stale', 'data.tmp'), 'partial'); - fs.writeFileSync(path.join(dir, 'keep.txt'), 'keep'); - - recoverRuntimeArtifacts(dir); - assert.equal(fs.existsSync(path.join(dir, 'input-stale.mp4')), false); - assert.equal(fs.existsSync(path.join(dir, 'run-stale')), false); - assert.equal(fs.readFileSync(path.join(dir, 'keep.txt'), 'utf8'), 'keep'); -}); - -test('termination error without close rejects and never proves process exit', async () => { - class ErrorOnlyChild extends EventEmitter { - constructor() { - super(); - this.exitCode = null; - this.signalCode = null; - } - kill() { - setTimeout(() => this.emit('error', new Error('signal delivery failed')), 1); - return true; - } - } - const child = new ErrorOnlyChild(); - await assert.rejects( - terminateChild(child, { graceMs: 20, hardKillMs: 20 }), - /termination failed before close/, - ); - assert.equal(child.exitCode, null); - assert.equal(child.signalCode, null); -}); - -test('runCommand keeps an unclosed child tracked after termination failure', async () => { - class ErrorOnlyChild extends EventEmitter { - constructor() { - super(); - this.exitCode = null; - this.signalCode = null; - this.pid = 123; - } - kill() { - setTimeout(() => this.emit('error', new Error('kill failed')), 1); - return true; - } - } - const child = new ErrorOnlyChild(); - const controller = new AbortController(); - const children = new Set(); - const command = runCommand('fake', [], { - spawnFn: () => child, - signal: controller.signal, - children, - killOptions: { graceMs: 20, hardKillMs: 20 }, - }); - controller.abort(); - await assert.rejects(command, /termination failed before close/); - assert.equal(children.has(child), true); - child.exitCode = 1; - child.emit('close', 1, null); - assert.equal(children.size, 0); -}); - -''' -if insert_before not in utils_test: - raise RuntimeError('utils test insertion anchor missing') -utils_test = utils_test.replace(insert_before, new_tests + insert_before, 1) -write(utils_test_path, utils_test) - -integration_path = 'source app folder/dashboard/tests/server-integration.test.js' -integration = read(integration_path) -integration = replace_once( - integration, - '''test('reset during a multipart upload invalidates and removes the partial upload', async (t) => { - const harness = await createHarness(standardServices()); - t.after(() => harness.close()); - const boundary = '----flyt-boundary'; - const prefix = [ - `--${boundary}`, - 'Content-Disposition: form-data; name="video"; filename="slow.mp4"', - 'Content-Type: video/mp4', - '', - 'partial-video-', - ].join('\\r\\n'); - const suffix = `\\r\\n--${boundary}--\\r\\n`; - - let finishUpload; - const uploadResponse = new Promise((resolve, reject) => { - const request = http.request(`${harness.baseUrl}/api/upload`, { - method: 'POST', - headers: { 'Content-Type': `multipart/form-data; boundary=${boundary}` }, - }, (response) => { - response.resume(); - response.on('end', () => resolve(response)); - }); - request.on('error', reject); - request.write(prefix); - finishUpload = () => request.end(`remaining${suffix}`); - }); - - await new Promise((resolve) => setTimeout(resolve, 30)); - const reset = await fetch(`${harness.baseUrl}/api/reset`, { method: 'POST' }); - assert.equal(reset.status, 200); - finishUpload(); - const response = await uploadResponse; - assert.equal(response.statusCode, 409); - assert.equal( - fs.readdirSync(harness.paths.uploadsDir).filter((name) => name.startsWith('input-')).length, - 0, - ); -});''', - '''test('reset actively aborts a stalled multipart upload and reopens the gate', async (t) => { - const harness = await createHarness(standardServices()); - t.after(() => harness.close()); - const boundary = '----flyt-boundary'; - const prefix = [ - `--${boundary}`, - 'Content-Disposition: form-data; name="video"; filename="slow.mp4"', - 'Content-Type: video/mp4', - '', - 'partial-video-', - ].join('\\r\\n'); - - const uploadOutcome = new Promise((resolve) => { - const request = http.request(`${harness.baseUrl}/api/upload`, { - method: 'POST', - headers: { 'Content-Type': `multipart/form-data; boundary=${boundary}` }, - }, (response) => { - response.resume(); - response.on('end', () => resolve(response.statusCode)); - }); - request.on('error', () => resolve('aborted')); - request.write(prefix); - }); - - await new Promise((resolve) => setTimeout(resolve, 30)); - const reset = await fetch(`${harness.baseUrl}/api/reset`, { method: 'POST' }); - assert.equal(reset.status, 200); - const outcome = await Promise.race([ - uploadOutcome, - new Promise((resolve) => setTimeout(() => resolve('timeout'), 1000)), - ]); - assert.notEqual(outcome, 'timeout'); - assert.ok(outcome === 'aborted' || outcome === 409); - assert.equal( - fs.readdirSync(harness.paths.uploadsDir).filter((name) => name.startsWith('input-')).length, - 0, - ); - const state = harness.getState(); - assert.equal(state.pendingUploads, 0); - assert.equal(state.uploadReserved, false); - assert.equal(state.terminating, false); - - assert.equal((await upload(harness.baseUrl, 'after-reset.mp4')).status, 200); - await waitForStatus(harness.baseUrl, 'done'); -});''', - 'active multipart reset test', -) -integration = replace_once( - integration, - ''' assert.equal(unscopedResponse.status, 400); - - assert.equal((await upload(harness.baseUrl, 'second.mp4')).status, 200);''', - ''' assert.equal(unscopedResponse.status, 400); - - const orphanResponse = await fetch(`${harness.baseUrl}/api/verification`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ eventId: `${firstRunId}:evt-does-not-exist`, verdict: 'confirmed' }), - }); - assert.equal(orphanResponse.status, 400); - - assert.equal((await upload(harness.baseUrl, 'second.mp4')).status, 200);''', - 'reject orphan review', -) -write(integration_path, integration) - -metrics_test_path = 'source app folder/dashboard/tests/metrics.test.js' -metrics_test = read(metrics_test_path) -metrics_test = replace_once( - metrics_test, - '''import { computeAverageProximity, prismDistance, proximityValue } from '../src/metrics.js';''', - '''import { - computeAverageProximity, - prismDistance, - prismVelocity, - proximityValue, -} from '../src/metrics.js';''', - 'metrics test import', -) -metrics_test += '''\n\ntest('invalid or merged observations export blank individual-fly velocities', () => { - const invalid = { - tracking_valid: 0, - detection_count: 1, - occlusion_flag: 1, - fly1_speed_pxsec: 120, - fly2_speed_pxsec: 95, - }; - assert.equal(prismVelocity(invalid, 'fly1'), ''); - assert.equal(prismVelocity(invalid, 'fly2'), ''); - assert.equal(prismVelocity({ - tracking_valid: 1, - detection_count: 2, - occlusion_flag: 0, - fly1_speed_pxsec: 42.5, - }, 'fly1'), 42.5); -}); -''' -write(metrics_test_path, metrics_test) - -tracker_test_path = 'source app folder/tracker/tests/test_tracker.py' -tracker_test = read(tracker_test_path) -tracker_test = replace_once( - tracker_test, - ''' def test_initial_detection_has_zero_velocity(self): - self.assertEqual(tracker.displacement((0, 0), (100, 100), False), 0.0) - self.assertEqual(tracker.displacement((0, 0), (3, 4), True), 5.0) -''', - ''' def test_initial_detection_has_zero_velocity(self): - self.assertEqual(tracker.displacement((0, 0), (100, 100), False), 0.0) - self.assertEqual(tracker.displacement((0, 0), (3, 4), True), 5.0) - - def test_invalid_observation_speed_is_missing(self): - self.assertTrue(math.isnan(tracker.observed_speed(25.0, 0))) - self.assertEqual(tracker.observed_speed(25.0, 1), 25.0) -''', - 'tracker speed validity test', -) -tracker_test = replace_once( - tracker_test, - '''import importlib.util -import pathlib -import unittest -''', - '''import importlib.util -import math -import pathlib -import unittest -''', - 'tracker test math import', -) -write(tracker_test_path, tracker_test) - -print('Applied review edge hardening patches successfully.') diff --git a/.github/workflows/apply-review-edge-hardening.yml b/.github/workflows/apply-review-edge-hardening.yml deleted file mode 100644 index 682722a..0000000 --- a/.github/workflows/apply-review-edge-hardening.yml +++ /dev/null @@ -1,89 +0,0 @@ -name: Apply review edge hardening - -on: - push: - branches: - - fix/critical-job-races - paths: - - '.github/scripts/review-edge-hardening.part*' - - '.github/workflows/apply-review-edge-hardening.yml' - -permissions: - contents: write - -concurrency: - group: flyt-review-edge-hardening - cancel-in-progress: false - -jobs: - patch-test-and-commit: - runs-on: ubuntu-latest - timeout-minutes: 20 - steps: - - uses: actions/checkout@v4 - with: - ref: fix/critical-job-races - fetch-depth: 0 - - - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: npm - cache-dependency-path: source app folder/dashboard/package-lock.json - - - uses: actions/setup-python@v5 - with: - python-version: '3.11' - cache: pip - cache-dependency-path: source app folder/tracker/requirements.txt - - - name: Reconstruct and apply transparent patch - run: | - cat .github/scripts/review-edge-hardening.part* > /tmp/review-edge-hardening.py - python -m py_compile /tmp/review-edge-hardening.py - python /tmp/review-edge-hardening.py - git diff --check - - - name: Install dashboard dependencies - working-directory: source app folder/dashboard - run: npm ci - - - name: Lint backend and regression tests - working-directory: source app folder/dashboard - run: npm run lint:backend - - - name: Run utility, HTTP, lifecycle, and metric tests - working-directory: source app folder/dashboard - run: npm run test:server - - - name: Build dashboard - working-directory: source app folder/dashboard - run: npm run build - - - name: Install tracker dependencies - run: python -m pip install -r 'source app folder/tracker/requirements.txt' - - - name: Check tracker syntax - run: python -m py_compile 'source app folder/tracker/tracker.py' - - - name: Run tracker regression tests - run: python -m unittest discover -s 'source app folder/tracker/tests' -p 'test_*.py' -v - - - name: Commit validated hardening - run: | - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add -- \ - 'source app folder/dashboard/server-utils.js' \ - 'source app folder/dashboard/server.js' \ - 'source app folder/dashboard/src/App.jsx' \ - 'source app folder/dashboard/src/metrics.js' \ - 'source app folder/dashboard/tests/metrics.test.js' \ - 'source app folder/dashboard/tests/server-integration.test.js' \ - 'source app folder/dashboard/tests/server-utils.test.js' \ - 'source app folder/tracker/tracker.py' \ - 'source app folder/tracker/tests/test_tracker.py' - git commit -m 'fix: close fail-closed termination and rollback edges' - git fetch origin fix/critical-job-races - git rebase origin/fix/critical-job-races - git push origin HEAD:fix/critical-job-races From 598236816f394f32402874925134da291bdac343 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 13:27:03 +0530 Subject: [PATCH 74/75] Fix late child-close termination recovery --- source app folder/dashboard/server.js | 55 ++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/source app folder/dashboard/server.js b/source app folder/dashboard/server.js index 19612f5..5c33429 100644 --- a/source app folder/dashboard/server.js +++ b/source app folder/dashboard/server.js @@ -124,8 +124,10 @@ export function createFlytServer(options = {}) { let uploadReserved = false; let terminating = false; let activeRun = null; + let stoppingRun = null; let job = blankJob(); const pendingUploads = new Map(); + const observedStoppingChildren = new WeakSet(); const services = { countFrames: options.services?.countFrames || ((filePath, context) => getVideoFrameCount( @@ -186,6 +188,34 @@ export function createFlytServer(options = {}) { && !context.controller.signal.aborted ); + function reconcileStoppingState() { + const context = stoppingRun; + const contextFinished = !context || context.finished; + const childrenStopped = !context || context.children.size === 0; + if (!terminating || !contextFinished || !childrenStopped || pendingUploads.size > 0) { + return false; + } + + stoppingRun = null; + terminating = false; + if ( + job.status === 'stopping' + || (job.status === 'error' && job.error?.startsWith('Could not terminate active work:')) + ) { + job = blankJob(); + } + return true; + } + + function observeStoppingChildren(context) { + if (!context) return; + context.children.forEach((child) => { + if (observedStoppingChildren.has(child)) return; + observedStoppingChildren.add(child); + child.once('close', () => queueMicrotask(reconcileStoppingState)); + }); + } + function reserveUpload(req, res, next) { if (isBusy()) { return res.status(409).json({ error: 'A video is already uploading, processing, or stopping.' }); @@ -205,6 +235,7 @@ export function createFlytServer(options = {}) { released = true; pendingUploads.delete(req); uploadReserved = pendingUploads.size > 0; + queueMicrotask(reconcileStoppingState); }; state.release = release; pendingUploads.set(req, state); @@ -258,17 +289,22 @@ export function createFlytServer(options = {}) { } async function stopActiveRun() { - epoch += 1; - const context = activeRun; const uploadStates = [...pendingUploads.values()]; + const context = activeRun || stoppingRun; + if (activeRun || uploadStates.length > 0) epoch += 1; + if (activeRun) stoppingRun = activeRun; activeRun = null; + const hasWork = Boolean(context) || uploadStates.length > 0; job = { ...blankJob(), status: hasWork ? 'stopping' : 'idle' }; - if (!hasWork) return; + if (!hasWork) { + reconcileStoppingState(); + return; + } terminating = true; context?.controller.abort(); - let stoppedCleanly = false; + observeStoppingChildren(context); try { const children = context ? [...context.children] : []; await Promise.all([ @@ -281,7 +317,6 @@ export function createFlytServer(options = {}) { }); } job = blankJob(); - stoppedCleanly = true; } catch (error) { job = { ...blankJob(), @@ -290,11 +325,7 @@ export function createFlytServer(options = {}) { }; throw error; } finally { - if ( - stoppedCleanly - && (!context || context.children.size === 0) - && pendingUploads.size === 0 - ) terminating = false; + reconcileStoppingState(); } } @@ -316,6 +347,7 @@ export function createFlytServer(options = {}) { controller: new AbortController(), children: new Set(), done: null, + finished: false, }; } @@ -437,7 +469,9 @@ export function createFlytServer(options = {}) { } finally { safeUnlink(context.inputPath); safeRemoveDir(context.workDir); + context.finished = true; if (activeRun === context && context.children.size === 0) activeRun = null; + reconcileStoppingState(); } } @@ -554,6 +588,7 @@ export function createFlytServer(options = {}) { terminating, pendingUploads: pendingUploads.size, activeRun, + stoppingRun, job: { ...job }, }), paths: { uploadsDir, publicDir, historyDir, historyMetaPath, verificationPath }, From 8611885ad49cc7a0545897df4a6e3fa6dad819e6 Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 13 Jul 2026 13:27:27 +0530 Subject: [PATCH 75/75] Add termination lifecycle regression test --- .../tests/termination-lifecycle.test.js | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 source app folder/dashboard/tests/termination-lifecycle.test.js diff --git a/source app folder/dashboard/tests/termination-lifecycle.test.js b/source app folder/dashboard/tests/termination-lifecycle.test.js new file mode 100644 index 0000000..b923b28 --- /dev/null +++ b/source app folder/dashboard/tests/termination-lifecycle.test.js @@ -0,0 +1,155 @@ +import assert from 'node:assert/strict'; +import { EventEmitter } from 'node:events'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; +import { createFlytServer } from '../server.js'; + +function abortError() { + const error = new Error('aborted'); + error.name = 'AbortError'; + return error; +} + +function writeTrackerOutputs(outputs, frames = 4) { + fs.writeFileSync(outputs.rawVideo, 'raw-video'); + fs.writeFileSync(outputs.csv, [ + 'frame,proximity_distance,occlusion_flag,tracking_valid,detection_count,fly1_area,fly2_area', + ...Array.from({ length: frames }, (_, index) => `${index},20,0,1,2,100,100`), + ].join('\n')); + fs.writeFileSync(outputs.events, JSON.stringify({ + fps: 30, + total_frames: frames, + events: [{ id: 'evt-001', type: 'courtship_bout' }], + })); +} + +async function createHarness(services) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'flyt-stop-lifecycle-')); + const instance = createFlytServer({ + rootDir: root, + uploadsDir: path.join(root, 'uploads'), + publicDir: path.join(root, 'public'), + historyDir: path.join(root, 'public', 'history'), + historyMetaPath: path.join(root, 'public', 'history.json'), + verificationPath: path.join(root, 'public', 'verification.json'), + trackerDir: path.join(root, 'tracker'), + trackerScript: path.join(root, 'tracker', 'tracker.py'), + pythonExe: 'python', + ffmpegPath: 'ffmpeg', + allowedOrigins: ['http://localhost:5173'], + killOptions: { graceMs: 10, hardKillMs: 20 }, + services, + }); + const listener = await new Promise((resolve) => { + const server = instance.app.listen(0, '127.0.0.1', () => resolve(server)); + }); + const address = listener.address(); + return { + ...instance, + baseUrl: `http://127.0.0.1:${address.port}`, + async close() { + try { await instance.stop(); } catch { /* test cleanup */ } + await new Promise((resolve) => listener.close(resolve)); + fs.rmSync(root, { recursive: true, force: true }); + }, + }; +} + +async function upload(baseUrl, name = 'flies.mp4') { + const form = new FormData(); + form.append('video', new Blob(['video-bytes'], { type: 'video/mp4' }), name); + return fetch(`${baseUrl}/api/upload`, { method: 'POST', body: form }); +} + +async function waitFor(check, timeoutMs = 1500) { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + const value = await check(); + if (value) return value; + await new Promise((resolve) => setTimeout(resolve, 10)); + } + throw new Error('Timed out waiting for lifecycle state'); +} + +async function waitForStatus(baseUrl, expected, timeoutMs = 1500) { + return waitFor(async () => { + const response = await fetch(`${baseUrl}/api/status`); + const status = await response.json(); + return status.status === expected ? status : null; + }, timeoutMs); +} + +class InitiallyUnkillableChild extends EventEmitter { + constructor() { + super(); + this.exitCode = null; + this.signalCode = null; + this.pid = 123; + } + + kill() { + setTimeout(() => this.emit('error', new Error('signal delivery failed')), 1); + return true; + } + + closeLater() { + this.exitCode = 1; + this.emit('close', 1, null); + } +} + +test('late child close clears a failed stopping context and reopens uploads', async (t) => { + let child; + let countCalls = 0; + const services = { + countFrames: async (_filePath, context) => { + countCalls += 1; + if (countCalls > 1) return 4; + + child = new InitiallyUnkillableChild(); + context.children.add(child); + return new Promise((_resolve, reject) => { + child.once('close', () => { + context.children.delete(child); + reject(abortError()); + }); + }); + }, + runTracker: async (_input, outputs) => { + writeTrackerOutputs(outputs); + return { + code: 0, + stderr: '', + stdout: 'TRACKER_SYNC frames_processed=4 csv_rows=4 expected_video_frames=4 sync_ok=true', + }; + }, + transcode: async (raw, final) => fs.copyFileSync(raw, final), + }; + const harness = await createHarness(services); + t.after(() => harness.close()); + + assert.equal((await upload(harness.baseUrl, 'first.mp4')).status, 200); + await waitFor(() => child && harness.getState().activeRun?.children.has(child)); + + const firstReset = await fetch(`${harness.baseUrl}/api/reset`, { method: 'POST' }); + assert.equal(firstReset.status, 500); + assert.equal(harness.getState().terminating, true); + assert.ok(harness.getState().stoppingRun); + assert.equal((await upload(harness.baseUrl, 'blocked.mp4')).status, 409); + + const secondReset = await fetch(`${harness.baseUrl}/api/reset`, { method: 'POST' }); + assert.equal(secondReset.status, 500); + assert.equal(harness.getState().terminating, true); + assert.ok(harness.getState().stoppingRun?.children.has(child)); + + child.closeLater(); + await waitFor(() => { + const state = harness.getState(); + return !state.terminating && state.stoppingRun === null && state.job.status === 'idle'; + }); + + assert.equal((await upload(harness.baseUrl, 'after-close.mp4')).status, 200); + await waitForStatus(harness.baseUrl, 'done'); +});