{"record":{"id":"011d408afe28aa75","repo":"koala73/worldmonitor","slug":"unavailable","errorCode":"unavailable","errorMessage":"Dashboard unavailable: ${boundedText(error.message, 160)} Reason: ${error.reason}.","messagePattern":"Dashboard unavailable: (.+?) Reason: (.+?)\\.","errorType":"error_code","errorClass":"SafeWebMcpError","httpStatus":null,"severity":"error","filePath":"src/services/webmcp.ts","lineNumber":791,"sourceCode":"function withInvocationLogging(\n  name: WebMcpSpaToolName,\n  fn: (\n    input: Record<string, unknown>,\n    extra?: { signal?: AbortSignal },\n  ) => Promise<unknown> | unknown,\n  trackEvent: WebMcpAnalytics,\n  hooks: WebMcpInvocationHooks = {},\n): DashboardWebMcpTool['execute'] {\n  return async (args, extra?: WebMcpToolExecutionContext) => {\n    const signal = isAbortSignal(extra?.signal) ? extra.signal : undefined;\n    const execution = signal ? { signal } : undefined;\n    markLcpDebug('wm:webmcp:tool-start', {\n      tool: name,\n      targetCancellationSupported: Boolean(signal),\n    });\n    try {\n      throwIfWebMcpAborted(signal);\n      const preflightResult = await hooks.preflight?.(args, execution);\n      throwIfWebMcpAborted(signal);\n      let result: unknown;\n      if (preflightResult !== undefined) {\n        result = preflightResult;\n      } else if (CANCELLATION_REQUIRED_WEBMCP_TOOLS.has(name) && !signal) {\n        // This tool declared that a phantom completion would be unsafe, and\n        // the host cannot deliver the target-side signal. Return a structured\n        // denial because some hosts erase the name and message of errors\n        // raised by the page callback.\n        result = unsupportedCancellationResult();\n      } else {\n        result = await fn(args, execution);\n        // Browser cancellation rejects executeTool independently of this\n        // callback. Re-check here so late work cannot publish success telemetry\n        // after the host has already cancelled the invocation.\n        throwIfWebMcpAborted(signal);\n      }\n      enforceOutputBudget(result);","sourceCodeStart":773,"sourceCodeEnd":809,"githubUrl":"https://github.com/koala73/worldmonitor/blob/e586b8b4b80f595aa7ece295eec10d76f2921240/src/services/webmcp.ts#L773-L809","documentation":"withInvocationLogging wraps every WorldMonitor WebMCP tool invocation. When the wrapped tool throws a DashboardUnavailableError, it is re-thrown as a SafeWebMcpError with code 'unavailable', embedding a bounded (160-char) version of the underlying error message plus the error's reason field. It signals that the dashboard SPA surface the tool needs to act on is not currently reachable, not that the tool arguments were wrong.","triggerScenarios":"Any WebMCP tool call (via `tools`) whose handler throws DashboardUnavailableError — e.g. the dashboard app instance is not mounted/initialized, or the SPA reports itself unavailable with a reason such as the page not being hydrated — at the moment the tool executes.","commonSituations":"Calling a dashboard tool before the WorldMonitor app finishes booting; the dashboard being torn down or navigated away; an internal app state machine marking itself unavailable with a specific reason after a failed init; embedded/iframe contexts where the SPA never mounted.","solutions":["Wait for the dashboard 'ready' signal (app mount/boot completion) before invoking WebMCP dashboard tools.","Read the `Reason:` suffix in the message — it is error.reason from DashboardUnavailableError and points at the exact unavailability cause.","Retry the tool call after a short delay if the dashboard was mid-boot; unavailability is often transient.","If reason indicates a permanent mount failure, fix the app bootstrap (missing container, JS init error) instead of retrying the tool."],"exampleFix":"// before\nconst ctx = await tools.call('get_dashboard_context');\n// after\nif (!dashboard.isReady()) await dashboard.whenReady();\ntry {\n  const ctx = await tools.call('get_dashboard_context');\n} catch (e) {\n  if (e.message.includes('Dashboard unavailable')) await dashboard.whenReady();\n}","handlingStrategy":"retry","validationCode":"function isDashboardReady(dashboard) {\n  return Boolean(dashboard && typeof dashboard.isReady === 'function' && dashboard.isReady());\n}","typeGuard":"function isDashboardUnavailableError(e) {\n  return e instanceof Error && e.message.startsWith('Dashboard unavailable:');\n}","tryCatchPattern":"try {\n  await tools.call('get_dashboard_context', {});\n} catch (e) {\n  if (isDashboardUnavailableError(e)) {\n    await dashboard.whenReady(); // or setTimeout retry with backoff\n    return retry();\n  }\n  throw e;\n}","preventionTips":["Gate all WebMCP dashboard tool calls behind the app's ready/booted signal.","Parse the 'Reason:' suffix to distinguish transient boot from permanent mount failure.","Use exponential backoff on retry instead of tight loops.","Log the reason field for diagnostics before retrying."],"tags":["webmcp","dashboard","lifecycle","unavailable"],"backgroundTag":"dashboard-unavailable","analyzedSha":"e586b8b4b80f595aa7ece295eec10d76f2921240","analyzedAt":"2026-09-01T10:32:37.851Z","contentChangedAt":"2026-09-01T10:32:37.851Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}