{"record":{"id":"48b8f28385e4db94","repo":"koala73/worldmonitor","slug":"cable-health-unavailable","errorCode":null,"errorMessage":"Cable health unavailable","messagePattern":"Cable health unavailable","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/services/cable-health.ts","lineNumber":70,"sourceCode":"        && typeof e.ts === 'number' && Number.isFinite(new Date(e.ts).getTime())));\n}\n\nfunction retainedResponse(): CableHealthResponse | null {\n  if (!cachedResponse) return null;\n  const age = Date.now() - Date.parse(cachedResponse.generatedAt);\n  return age >= 0 && age < MAX_RETAINED_AGE_MS ? cachedResponse : null;\n}\n\n// ---- Public API ----\n\nexport async function fetchCableHealth(): Promise<CableHealthResponse> {\n  const now = Date.now();\n  const retained = retainedResponse();\n  if (retained && now < cacheExpiry) return retained;\n\n  const resp = await breaker.execute(async () => {\n    const response = await client.getCableHealth({});\n    if (!isUsableResponse(response)) throw new Error('Cable health unavailable');\n    return response;\n  }, emptyFallback, { shouldCache: isUsableResponse, staleRefreshMode: 'await' });\n\n  if (!isUsableResponse(resp)) {\n    const lastGood = retainedResponse();\n    if (lastGood) return lastGood;\n    throw new Error('Cable health unavailable');\n  }\n\n  const cables: Record<string, CableHealthRecord> = {};\n  for (const [id, proto] of Object.entries(resp.cables)) {\n    cables[id] = toRecord(proto);\n  }\n\n  const result: CableHealthResponse = {\n    generatedAt: new Date(resp.generatedAt).toISOString(),\n    cables,\n  };","sourceCodeStart":52,"sourceCodeEnd":88,"githubUrl":"https://github.com/koala73/worldmonitor/blob/e586b8b4b80f595aa7ece295eec10d76f2921240/src/services/cable-health.ts#L52-L88","documentation":"fetchCableHealth throws 'Cable health unavailable' inside the circuit-breaker execute callback when the RPC response from InfrastructureServiceClient.getCableHealth fails the isUsableResponse validity check (src/services/cable-health.ts:70). isUsableResponse requires a positive finite generatedAt within 90 minutes, a non-empty object-shaped cables map, and every record with valid lastUpdated/evidence fields — so the throw fires when the backend returns an empty, stale, or structurally invalid snapshot.","triggerScenarios":"client.getCableHealth({}) resolves but the response has generatedAt <= 0 or older than 90 minutes, cables missing/not an object, or any cable record lacking valid lastUpdated/evidence entries. Also thrown when breaker.execute itself fails validation so the emptyFallback is returned and the post-check path (line 74-78) finds no retained last-good response.","commonSituations":"Infrastructure worker down or seeded with zero cable records (empty cables map + generatedAt 0); RPC base URL misconfigured so the client hits a wrong endpoint returning an empty/default proto; clock skew or stale persisted cache making generatedAt appear older than the 90-minute retention window; proto schema drift after regeneration so fields arrive unset.","solutions":["Verify the infrastructure RPC service returns a fresh snapshot: call getCableHealth directly against getRpcBaseUrl() and inspect generatedAt and cables contents.","Check the cable-health data worker is running and writing valid records (positive generatedAt, populated evidence arrays); re-run its seed/refresh job.","Confirm src/generated clients are up to date with proto (make generate) — schema drift yields default-valued fields that fail isUsableResponse.","Check client clock/system time; a skewed clock invalidates the age window (age < 0 or > 90 min) and rejects otherwise-good data.","If the response is valid but rejected, add logging in isUsableResponse to identify which predicate fails and fix the producer accordingly."],"exampleFix":"// before\nconst response = await client.getCableHealth({});\nif (!isUsableResponse(response)) throw new Error('Cable health unavailable');\nreturn response;\n// after\nconst response = await client.getCableHealth({});\nif (!isUsableResponse(response)) {\n  console.warn('[cable-health] unusable snapshot:', { generatedAt: response?.generatedAt, cableCount: Object.keys(response?.cables ?? {}).length });\n  throw new Error('Cable health unavailable');\n}\nreturn response;","handlingStrategy":"validation","validationCode":"function looksLikeCableSnapshot(r: unknown): boolean {\n  const v = r as any;\n  return !!v && Number.isFinite(v.generatedAt) && v.generatedAt > 0\n    && v.cables && typeof v.cables === 'object' && !Array.isArray(v.cables);\n}","typeGuard":"function isUsableCableHealth(r: unknown): r is GetCableHealthResponse {\n  const v = r as any;\n  if (!v || typeof v !== 'object') return false;\n  const age = Date.now() - v.generatedAt;\n  return Number.isFinite(v.generatedAt) && v.generatedAt > 0 && age >= 0 && age < 90 * 60 * 1000\n    && v.cables && typeof v.cables === 'object' && !Array.isArray(v.cables);\n}","tryCatchPattern":"try {\n  const health = await fetchCableHealth();\n  renderCableHealth(health);\n} catch (err) {\n  if (err.message === 'Cable health unavailable') renderCableLayerEmpty();\n  else throw err;\n}","preventionTips":["Keep the cable-health producer running and refreshing generatedAt within the 90-minute window","Regenerate proto clients after schema changes so fields don't deserialize as defaults","Validate RPC base URL per environment before shipping","Watch system clock skew in desktop environments; it invalidates the freshness window"],"tags":["rpc","validation","circuit-breaker","grpc"],"backgroundTag":"unexpected-api-response-shape","analyzedSha":"e586b8b4b80f595aa7ece295eec10d76f2921240","analyzedAt":"2026-09-22T01:50:49.965Z","contentChangedAt":"2026-09-22T01:50:49.965Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}