From ee21d666f05647dd87001992d5dd19297efa2901 Mon Sep 17 00:00:00 2001 From: Mariusz Banach Date: Wed, 18 Feb 2026 02:14:19 +0100 Subject: [PATCH] MAESTRO: add ProgressIndicator component --- ...er-analyzer-Phase-05-Analysis-Execution.md | 2 +- frontend/src/components/ProgressIndicator.tsx | 146 ++++++++++++++++++ 2 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/ProgressIndicator.tsx 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 95b17a6..eb5e0e2 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 @@ -35,7 +35,7 @@ Frontend uses `fetch` with `ReadableStream` reader (not native `EventSource`, wh - [x] T026 [US3] Create `backend/app/schemas/analysis.py` (request/response schemas) and `backend/app/routers/analysis.py` — FastAPI router with `POST /api/analyse` using SSE for progress streaming. Accepts headers string + config (test IDs, resolve, decode-all). Invokes `HeaderAnalyzer` with 30s timeout (NFR-13). Streams progress events then final result. Sanitises input (NFR-09), validates size ≤1MB (NFR-10). Stateless — no job_id, no in-memory state (Assumption 3). Register router in `backend/app/main.py`. Verify `test_analysis_router.py` passes (TDD Green) - [x] T027 [US3] Write failing tests (TDD Red) in `frontend/src/__tests__/ProgressIndicator.test.tsx` (render at various states, timeout display) and `frontend/src/__tests__/useAnalysis.test.ts` (hook state transitions, SSE handling) - [x] T028 [P] [US3] Create `frontend/src/hooks/useAnalysis.ts` — custom hook managing analysis lifecycle. Submits to `POST /api/analyse` via API client, consumes SSE stream for real-time progress (no polling). States: idle, submitting, analysing (with progress), complete, error, timeout. Returns: `submit()`, `cancel()`, `progress`, `result`, `error`, `status`. Verify `useAnalysis.test.ts` passes (TDD Green) -- [ ] T029 [P] [US3] Create `frontend/src/components/ProgressIndicator.tsx` — progress bar with percentage, current test name (FR-22), countdown timer from 30s (NFR-13), elapsed time. Colour-coded: green progressing, amber near timeout, red on timeout. FontAwesome spinner. Timeout notification listing incomplete tests (NFR-14). Verify `ProgressIndicator.test.tsx` passes (TDD Green) +- [x] T029 [P] [US3] Create `frontend/src/components/ProgressIndicator.tsx` — progress bar with percentage, current test name (FR-22), countdown timer from 30s (NFR-13), elapsed time. Colour-coded: green progressing, amber near timeout, red on timeout. FontAwesome spinner. Timeout notification listing incomplete tests (NFR-14). Verify `ProgressIndicator.test.tsx` passes (TDD Green) ## Completion diff --git a/frontend/src/components/ProgressIndicator.tsx b/frontend/src/components/ProgressIndicator.tsx new file mode 100644 index 0000000..2ed9b79 --- /dev/null +++ b/frontend/src/components/ProgressIndicator.tsx @@ -0,0 +1,146 @@ +"use client"; + +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faSpinner } from "@fortawesome/free-solid-svg-icons"; + +import type { AnalysisProgress } from "../types/analysis"; +import type { AnalysisStatus } from "../hooks/useAnalysis"; + +type ProgressIndicatorProps = { + status: AnalysisStatus; + progress: AnalysisProgress | null; + timeoutSeconds: number; + incompleteTests?: string[]; +}; + +type ProgressVariant = "normal" | "warning" | "timeout"; + +const formatSeconds = (seconds: number): string => `${seconds}s`; + +const getVariant = ( + status: AnalysisStatus, + remainingSeconds: number, + timeoutSeconds: number, +): ProgressVariant => { + if (status === "timeout") { + return "timeout"; + } + + if (status !== "analysing") { + return "normal"; + } + + const warningThreshold = Math.max(5, Math.round(timeoutSeconds * 0.15)); + return remainingSeconds <= warningThreshold ? "warning" : "normal"; +}; + +const variantStyles: Record = { + normal: { + bar: "bg-clean", + badge: "text-clean border-clean/40", + track: "bg-clean/10", + }, + warning: { + bar: "bg-suspicious", + badge: "text-suspicious border-suspicious/40", + track: "bg-suspicious/10", + }, + timeout: { + bar: "bg-spam", + badge: "text-spam border-spam/40", + track: "bg-spam/10", + }, +}; + +export default function ProgressIndicator({ + status, + progress, + timeoutSeconds, + incompleteTests = [], +}: ProgressIndicatorProps) { + const elapsedSeconds = progress ? Math.floor(progress.elapsedMs / 1000) : 0; + const remainingSeconds = Math.max(0, timeoutSeconds - elapsedSeconds); + const percentage = progress ? Math.round(progress.percentage) : 0; + const variant = getVariant(status, remainingSeconds, timeoutSeconds); + const styles = variantStyles[variant]; + + return ( +
+
+
+ + {status === "analysing" ? "Analysing" : status} + + {status === "analysing" ? ( + + ) : null} +
+
+ {formatSeconds(elapsedSeconds)} + / + {formatSeconds(remainingSeconds)} +
+
+ +
+
+ + {progress?.currentTest ?? "Preparing analysis"} + + + {percentage}% + +
+
+
+
+
+ + {status === "timeout" ? ( +
+

+ Timeout reached at {timeoutSeconds} seconds. +

+

Incomplete tests:

+

+ {incompleteTests.length > 0 ? incompleteTests.join(", ") : "None"} +

+
+ ) : null} +
+ ); +}