{"record":{"id":"ff024e1926ac73c0","repo":"koala73/worldmonitor","slug":"scenario-failed","errorCode":null,"errorMessage":"Scenario failed","messagePattern":"Scenario failed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/components/SupplyChainPanel.ts","lineNumber":1180,"sourceCode":"      const runResp = await runScenario({ scenarioId, iso2, disruptionPct }, { signal: runSignal });\n      const jobId = runResp.jobId;\n      let result: ScenarioResult | null = null;\n      // 60 × 1s = 60s max (worker typically completes in <1s). 1s poll keeps\n      // the perceived latency <2s in the common case. First iteration polls\n      // immediately (no sleep) in case the worker was already running on a\n      // previous job and blocked here only because of network round-trip.\n      for (let i = 0; i < 60; i++) {\n        if (signal.aborted) { resetButton('idle'); return; }\n        if (!this.content.isConnected) return; // panel gone — nothing to update\n        if (i > 0) await new Promise(r => setTimeout(r, 1000));\n        const status = await getScenarioStatus(jobId, { signal });\n        if (status.status === 'done') {\n          const r = status.result;\n          if (!r || !Array.isArray(r.topImpactCountries)) throw new Error('done without valid result');\n          result = r;\n          break;\n        }\n        if (status.status === 'failed') throw new Error('Scenario failed');\n      }\n      if (!result) throw new Error('Timeout — scenario worker may be down');\n      if (signal.aborted) { resetButton('idle'); return; }\n      if (!this.content.isConnected) return;\n      // After this callback fires, showScenarioSummary() → render() will rebuild\n      // the scenario-trigger DOM with the button already in its \"Active\" +\n      // disabled state (driven by activeScenarioState in renderChokepoints()).\n      // Do NOT touch the captured btn reference here — it's about to be detached\n      // by render()'s setContent(), and any imperative update would no-op\n      // silently while the fresh button shows the wrong state.\n      this.scenarioRunState.delete(scenarioId);\n      this.onScenarioActivate?.(scenarioId, result);\n    } catch (err) {\n      // Abort from a new click = user-triggered retry, no error banner needed.\n      if (err instanceof Error && err.name === 'AbortError') {\n        resetButton('idle');\n        return;\n      }","sourceCodeStart":1162,"sourceCodeEnd":1198,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/src/components/SupplyChainPanel.ts#L1162-L1198","documentation":"While polling a supply-chain scenario's status, a response with status 'failed' is treated as a terminal worker-side failure and surfaced as this generic Error. It means the backend/worker ran the scenario and reported failure, as opposed to timeout (no result in time) or a malformed done payload.","triggerScenarios":"Polling loop receives a status object where status.status === 'failed' — the scenario worker processed the request but its computation errored or was marked failed server-side.","commonSituations":"Worker crashed mid-computation for this particular scenario; upstream data source for the scenario unavailable; scenario parameters that hit an unsupported code path; transient backend deploy/restart.","solutions":["Retry the scenario — many worker failures are transient (restarts, upstream hiccups).","Check the scenario worker's logs for the corresponding job to find the underlying failure.","Simplify or adjust scenario parameters (smaller region/fewer layers) in case the specific input triggers the failure.","Verify worker health/deployment status; if persistent, file an issue with the scenario id."],"exampleFix":"// before\nif (status.status === 'failed') throw new Error('Scenario failed');\n// after\nif (status.status === 'failed') {\n  if (attempt < 2) { await sleep(backoff); continue poll; }\n  throw new Error(`Scenario failed: ${status.error ?? 'unknown worker error'}`);\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":"function isScenarioStatus(s: unknown): s is { status: 'done' | 'failed' | string; result?: { topImpactCountries: unknown[] } } {\n  return typeof s === 'object' && s !== null && 'status' in s;\n}","tryCatchPattern":"try {\n  const result = await runScenario(params);\n} catch (e) {\n  if (e.message === 'Scenario failed') {\n    await retryWithBackoff(() => runScenario(params), { attempts: 2 });\n  } else throw e;\n}","preventionTips":["Retry failed scenarios once or twice with backoff before surfacing an error.","Keep scenario parameters within supported ranges.","Monitor worker health and alert on rising 'failed' rates."],"tags":["worker","backend","polling","scenario"],"backgroundTag":"upstream-api-error","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-09-15T16:44:39.439Z","contentChangedAt":"2026-09-15T16:44:39.439Z","schemaVersion":2},"datasetVersion":"2026-09-15T18:17:12.389Z"}