+
+
+ {this.props.title ?? 'This view failed to render'}
+
+
+ Your analysis is safe — retry to re-render it, or reload the page.
+
+ {error.message ? (
+
{error.message}
+ ) : null}
+
+
+
+
+
+
+ );
+ }
+}
diff --git a/apps/ui/src/services/analyzer.ts b/apps/ui/src/services/analyzer.ts
index ce416b7d..f527e841 100644
--- a/apps/ui/src/services/analyzer.ts
+++ b/apps/ui/src/services/analyzer.ts
@@ -9,11 +9,21 @@ import {
projectPhase2FromRun,
projectStemSummaryFromRun,
} from './analysisRunsClient';
-import { createUserCancelledError, mapBackendError } from './backendPhase1Client';
+import { createClientTimeoutError, createUserCancelledError, mapBackendError } from './backendPhase1Client';
import { MEASUREMENT_LABEL, INTERPRETATION_LABEL, INTERPRETATION_SKIPPED_LABEL } from './phaseLabels';
import { validatePhase2Consistency } from './phase2Validator';
const DEFAULT_POLL_INTERVAL_MS = 1_000;
+// Per-poll fetch deadline. Generous: the box may be under multi-GB MT3/Demucs
+// memory pressure, so a snapshot read can be momentarily slow on a healthy run.
+// A single poll exceeding this fails the run (consistent with other transport
+// errors) rather than hanging the client forever.
+const DEFAULT_POLL_REQUEST_TIMEOUT_MS = 60_000;
+// Overall wall-clock backstop. Worst single-run case is measurement +
+// max(pitch-note 600s, MT3 1800s) (concurrent peers) + interpretation, so a
+// real run finishes well under this; it only bounds a backend that never
+// terminalizes a stage.
+const DEFAULT_RUN_WALL_CLOCK_MS = 90 * 60 * 1_000;
export interface AnalyzeAudioUpdate {
runId: string;
@@ -32,6 +42,10 @@ export interface AnalyzeAudioOptions {
signal?: AbortSignal;
onRunUpdate?: (update: AnalyzeAudioUpdate) => void;
pollIntervalMs?: number;
+ /** Per-poll fetch deadline (ms). Defaults to DEFAULT_POLL_REQUEST_TIMEOUT_MS. */
+ pollRequestTimeoutMs?: number;
+ /** Overall wall-clock budget for the run (ms). Defaults to DEFAULT_RUN_WALL_CLOCK_MS. */
+ maxRunDurationMs?: number;
transcribe?: boolean;
phase2Requested?: boolean;
phase2ConfigEnabled?: boolean;
@@ -64,6 +78,49 @@ function delay(ms: number, signal?: AbortSignal): Promise