MAESTRO: add live countdown timer to progress indicator

This commit is contained in:
Mariusz Banach
2026-02-18 02:25:52 +01:00
parent 31c458c4d8
commit c2cb756eeb
2 changed files with 38 additions and 2 deletions

View File

@@ -43,7 +43,7 @@ Frontend uses `fetch` with `ReadableStream` reader (not native `EventSource`, wh
- [x] All vitest tests pass: `npx vitest run src/__tests__/ProgressIndicator.test.tsx src/__tests__/useAnalysis.test.ts` - [x] All vitest tests pass: `npx vitest run src/__tests__/ProgressIndicator.test.tsx src/__tests__/useAnalysis.test.ts`
- [x] Submitting headers triggers backend analysis with SSE streaming - [x] Submitting headers triggers backend analysis with SSE streaming
- [x] Progress bar updates in real-time showing current test name and percentage - [x] Progress bar updates in real-time showing current test name and percentage
- [ ] Countdown timer counts down from 30 seconds - [x] Countdown timer counts down from 30 seconds
- [ ] Partial failures show inline error indicators per FR-25 - [ ] Partial failures show inline error indicators per FR-25
- [ ] Timeout at 30s displays partial results with notification listing incomplete tests - [ ] Timeout at 30s displays partial results with notification listing incomplete tests
- [ ] Empty input returns 400, oversized >1MB returns 413 - [ ] Empty input returns 400, oversized >1MB returns 413

View File

@@ -1,5 +1,6 @@
"use client"; "use client";
import { useEffect, useRef, useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSpinner } from "@fortawesome/free-solid-svg-icons"; import { faSpinner } from "@fortawesome/free-solid-svg-icons";
@@ -62,7 +63,42 @@ export default function ProgressIndicator({
timeoutSeconds, timeoutSeconds,
incompleteTests = [], incompleteTests = [],
}: ProgressIndicatorProps) { }: ProgressIndicatorProps) {
const elapsedSeconds = progress ? Math.floor(progress.elapsedMs / 1000) : 0; const [nowMs, setNowMs] = useState(() => Date.now());
const anchorRef = useRef<{ elapsedMs: number; timestamp: number } | null>(null);
useEffect(() => {
if (status !== "analysing") {
return;
}
const interval = window.setInterval(() => {
setNowMs(Date.now());
}, 1000);
return () => {
window.clearInterval(interval);
};
}, [status]);
useEffect(() => {
if (!progress || status !== "analysing") {
anchorRef.current = null;
return;
}
anchorRef.current = {
elapsedMs: progress.elapsedMs,
timestamp: Date.now(),
};
}, [progress?.elapsedMs, status]);
const baseElapsedMs = progress?.elapsedMs ?? 0;
const anchor = anchorRef.current;
const elapsedMs =
status === "analysing" && progress && anchor
? anchor.elapsedMs + Math.max(0, nowMs - anchor.timestamp)
: baseElapsedMs;
const elapsedSeconds = Math.floor(elapsedMs / 1000);
const remainingSeconds = Math.max(0, timeoutSeconds - elapsedSeconds); const remainingSeconds = Math.max(0, timeoutSeconds - elapsedSeconds);
const percentage = progress ? Math.round(progress.percentage) : 0; const percentage = progress ? Math.round(progress.percentage) : 0;
const variant = getVariant(status, remainingSeconds, timeoutSeconds); const variant = getVariant(status, remainingSeconds, timeoutSeconds);