From c2cb756eeb5dd596b3d4fe6ae993e9e3ad2a3007 Mon Sep 17 00:00:00 2001 From: Mariusz Banach Date: Wed, 18 Feb 2026 02:25:52 +0100 Subject: [PATCH] MAESTRO: add live countdown timer to progress indicator --- ...er-analyzer-Phase-05-Analysis-Execution.md | 2 +- frontend/src/components/ProgressIndicator.tsx | 38 ++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/Auto Run Docs/SpecKit-web-header-analyzer-Phase-05-Analysis-Execution.md b/Auto Run Docs/SpecKit-web-header-analyzer-Phase-05-Analysis-Execution.md index a38994d..a6c4d85 100644 --- a/Auto Run Docs/SpecKit-web-header-analyzer-Phase-05-Analysis-Execution.md +++ b/Auto Run Docs/SpecKit-web-header-analyzer-Phase-05-Analysis-Execution.md @@ -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] Submitting headers triggers backend analysis with SSE streaming - [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 - [ ] Timeout at 30s displays partial results with notification listing incomplete tests - [ ] Empty input returns 400, oversized >1MB returns 413 diff --git a/frontend/src/components/ProgressIndicator.tsx b/frontend/src/components/ProgressIndicator.tsx index 2ed9b79..e4c802d 100644 --- a/frontend/src/components/ProgressIndicator.tsx +++ b/frontend/src/components/ProgressIndicator.tsx @@ -1,5 +1,6 @@ "use client"; +import { useEffect, useRef, useState } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faSpinner } from "@fortawesome/free-solid-svg-icons"; @@ -62,7 +63,42 @@ export default function ProgressIndicator({ timeoutSeconds, incompleteTests = [], }: 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 percentage = progress ? Math.round(progress.percentage) : 0; const variant = getVariant(status, remainingSeconds, timeoutSeconds);