fix: commit missing ScanTimer component and useScanTimer hook
All checks were successful
Deploy / deploy (push) Successful in 31s
All checks were successful
Deploy / deploy (push) Successful in 31s
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>
This commit is contained in:
51
flight-comparator/frontend/src/components/ScanTimer.tsx
Normal file
51
flight-comparator/frontend/src/components/ScanTimer.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import type { ScanTimerResult } from '../hooks/useScanTimer';
|
||||||
|
|
||||||
|
/** Format a non-negative number of seconds into a human-readable string. */
|
||||||
|
export function formatDuration(totalSeconds: number): string {
|
||||||
|
const s = Math.floor(totalSeconds);
|
||||||
|
if (s < 60) return `${s}s`;
|
||||||
|
const m = Math.floor(s / 60);
|
||||||
|
const rem = s % 60;
|
||||||
|
if (m < 60) return rem > 0 ? `${m}m ${rem}s` : `${m}m`;
|
||||||
|
const h = Math.floor(m / 60);
|
||||||
|
const remM = m % 60;
|
||||||
|
return remM > 0 ? `${h}h ${remM}m` : `${h}h`;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScanTimerProps extends ScanTimerResult {
|
||||||
|
/** When true, renders a compact single-line format for the completed stat card. */
|
||||||
|
compact?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays elapsed time and ETA for an active scan, or final duration for a
|
||||||
|
* completed/failed scan.
|
||||||
|
*/
|
||||||
|
export default function ScanTimer({ elapsedSeconds, remainingSeconds, isEstimating, compact }: ScanTimerProps) {
|
||||||
|
if (compact) {
|
||||||
|
return <span>{formatDuration(elapsedSeconds)}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remainingLabel = isEstimating
|
||||||
|
? 'Estimating…'
|
||||||
|
: remainingSeconds !== null
|
||||||
|
? `~${formatDuration(remainingSeconds)}`
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-3 grid grid-cols-2 gap-x-4 gap-y-0.5 text-xs">
|
||||||
|
<span className="text-on-surface-variant">Elapsed</span>
|
||||||
|
<span className="font-mono text-on-surface tabular-nums">
|
||||||
|
{formatDuration(elapsedSeconds)}
|
||||||
|
</span>
|
||||||
|
{remainingLabel !== null && (
|
||||||
|
<>
|
||||||
|
<span className="text-on-surface-variant">Remaining</span>
|
||||||
|
<span className="font-mono text-on-surface tabular-nums">
|
||||||
|
{remainingLabel}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
88
flight-comparator/frontend/src/hooks/useScanTimer.ts
Normal file
88
flight-comparator/frontend/src/hooks/useScanTimer.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
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 };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user