Files
ciaovolo/flight-comparator/frontend/src/hooks/useScanTimer.ts
domverse 3cad8a8447
All checks were successful
Deploy / deploy (push) Successful in 31s
fix: commit missing ScanTimer component and useScanTimer hook
These files were referenced by ScanDetails.tsx but never committed,
breaking the Docker build (tsc could not resolve the imports).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 18:12:47 +01:00

89 lines
2.8 KiB
TypeScript

import { useEffect, useRef, useState } from 'react';
import type { Scan } from '../api';
export interface ScanTimerResult {
/** Seconds elapsed since the scan started processing. */
elapsedSeconds: number;
/**
* Estimated seconds remaining, or null when not enough data yet
* (fewer than 5 routes scanned or elapsed time is 0).
*/
remainingSeconds: number | null;
/** True while the estimate is still too early to be reliable. */
isEstimating: boolean;
}
const MIN_ROUTES_FOR_ESTIMATE = 5;
function calcElapsed(startedAt: string): number {
return Math.max(0, (Date.now() - new Date(startedAt).getTime()) / 1000);
}
function calcRemaining(
elapsed: number,
routesScanned: number,
totalRoutes: number,
): number | null {
if (elapsed <= 0 || routesScanned < MIN_ROUTES_FOR_ESTIMATE || totalRoutes <= 0) {
return null;
}
const rate = routesScanned / elapsed; // routes per second
const remaining = (totalRoutes - routesScanned) / rate;
return Math.max(0, remaining);
}
export function useScanTimer(scan: Scan | null): ScanTimerResult {
const [elapsedSeconds, setElapsedSeconds] = useState(0);
const [remainingSeconds, setRemainingSeconds] = useState<number | null>(null);
const intervalRef = useRef<ReturnType<typeof setInterval> | undefined>(undefined);
useEffect(() => {
if (!scan) return;
// For completed / failed scans with both timestamps: compute static duration.
if (
(scan.status === 'completed' || scan.status === 'failed') &&
scan.started_at &&
scan.completed_at
) {
const duration = Math.max(
0,
(new Date(scan.completed_at).getTime() - new Date(scan.started_at).getTime()) / 1000,
);
setElapsedSeconds(duration);
setRemainingSeconds(0);
return;
}
// For running scans with a start time: run a live 1-second timer.
if (scan.status === 'running' && scan.started_at) {
const tick = () => {
const elapsed = calcElapsed(scan.started_at!);
const remaining = calcRemaining(elapsed, scan.routes_scanned, scan.total_routes);
setElapsedSeconds(elapsed);
setRemainingSeconds(remaining);
};
tick(); // run immediately
intervalRef.current = setInterval(tick, 1000);
return () => {
if (intervalRef.current !== undefined) {
clearInterval(intervalRef.current);
intervalRef.current = undefined;
}
};
}
// Pending or no started_at: reset
setElapsedSeconds(0);
setRemainingSeconds(null);
}, [scan?.status, scan?.started_at, scan?.completed_at, scan?.routes_scanned, scan?.total_routes]);
const isEstimating =
scan?.status === 'running' &&
(scan.routes_scanned < MIN_ROUTES_FOR_ESTIMATE || scan.total_routes <= 0);
return { elapsedSeconds, remainingSeconds, isEstimating };
}