@64mb/freelens-power 1.2.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +3 -1
  2. package/out/main/common/store/monitoring-preferences-store.js +5 -0
  3. package/out/main/common/store/monitoring-preferences-store.js.map +1 -1
  4. package/out/renderer/ai-diagnostic-handoff.js +52 -0
  5. package/out/renderer/ai-diagnostic-handoff.js.map +1 -0
  6. package/out/renderer/ai-extras.js +40 -4
  7. package/out/renderer/ai-extras.js.map +1 -1
  8. package/out/renderer/ai.js +48 -56
  9. package/out/renderer/ai.js.map +1 -1
  10. package/out/renderer/details/hpa-diagnostics-details.js +46 -0
  11. package/out/renderer/details/hpa-diagnostics-details.js.map +1 -0
  12. package/out/renderer/details/namespace-health-details.js +91 -0
  13. package/out/renderer/details/namespace-health-details.js.map +1 -0
  14. package/out/renderer/details/resource-health-details.js +131 -0
  15. package/out/renderer/details/resource-health-details.js.map +1 -0
  16. package/out/renderer/details/service-diagnostics-details.js +163 -0
  17. package/out/renderer/details/service-diagnostics-details.js.map +1 -0
  18. package/out/renderer/diagnostic-explain-button.js +16 -0
  19. package/out/renderer/diagnostic-explain-button.js.map +1 -0
  20. package/out/renderer/diagnostics-state.js +51 -0
  21. package/out/renderer/diagnostics-state.js.map +1 -0
  22. package/out/renderer/diagnostics.css.js +9 -0
  23. package/out/renderer/diagnostics.css.js.map +1 -1
  24. package/out/renderer/diagnostics.js +369 -73
  25. package/out/renderer/diagnostics.js.map +1 -1
  26. package/out/renderer/incident-copy-button.js +50 -0
  27. package/out/renderer/incident-copy-button.js.map +1 -0
  28. package/out/renderer/index.js +91 -19
  29. package/out/renderer/index.js.map +1 -1
  30. package/out/renderer/monitoring-history-collection.js +1 -1
  31. package/out/renderer/preferences/monitoring-preferences.js +10 -0
  32. package/out/renderer/preferences/monitoring-preferences.js.map +1 -1
  33. package/out/renderer/preferences/preferences.js +88 -0
  34. package/out/renderer/preferences/preferences.js.map +1 -1
  35. package/out/renderer/resource-relationships.css.js +7 -0
  36. package/out/renderer/resource-relationships.css.js.map +1 -0
  37. package/out/renderer/resource-relationships.js +556 -0
  38. package/out/renderer/resource-relationships.js.map +1 -0
  39. package/out/renderer/resource-search-distance.js +24 -0
  40. package/out/renderer/resource-search-distance.js.map +1 -0
  41. package/out/renderer/resource-search-fields.js +55 -0
  42. package/out/renderer/resource-search-fields.js.map +1 -0
  43. package/out/renderer/resource-search-model.js +193 -0
  44. package/out/renderer/resource-search-model.js.map +1 -0
  45. package/out/renderer/resource-search.js +365 -0
  46. package/out/renderer/resource-search.js.map +1 -0
  47. package/out/renderer/src/common/hpa-diagnostics.js +24 -0
  48. package/out/renderer/src/common/hpa-diagnostics.js.map +1 -0
  49. package/out/renderer/src/common/incident-summary.js +73 -0
  50. package/out/renderer/src/common/incident-summary.js.map +1 -0
  51. package/out/renderer/src/common/resource-health.js +92 -0
  52. package/out/renderer/src/common/resource-health.js.map +1 -0
  53. package/out/renderer/src/common/resource-relationships.js +155 -0
  54. package/out/renderer/src/common/resource-relationships.js.map +1 -0
  55. package/out/renderer/src/common/service-diagnostics.js +50 -0
  56. package/out/renderer/src/common/service-diagnostics.js.map +1 -0
  57. package/out/renderer/src/common/store/monitoring-preferences-store.js +5 -0
  58. package/out/renderer/src/common/store/monitoring-preferences-store.js.map +1 -1
  59. package/out/renderer/src/common/timeline-groups.js +36 -0
  60. package/out/renderer/src/common/timeline-groups.js.map +1 -0
  61. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource-search.js","names":[],"sources":["../../src/renderer/resource-search.tsx"],"sourcesContent":["import { Renderer } from \"@freelensapp/extensions\";\nimport { reaction } from \"mobx\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport ReactDOM from \"react-dom\";\nimport { explainDiagnosticFinding } from \"./ai-diagnostic-handoff\";\nimport { AiIcon } from \"./ai-icon\";\nimport { DiagnosticExplainButton } from \"./diagnostic-explain-button\";\nimport { projectSearchFields } from \"./resource-search-fields\";\nimport {\n createDoubleShift,\n createResourceSearchIndex,\n queryTokens,\n type SearchResource,\n} from \"./resource-search-model\";\n\nconst OPEN = \"power-resource-search\";\nconst activeId = () => Renderer.Catalog.activeCluster.get()?.metadata.uid;\nconst inventory = () =>\n [\n [\"Pod\", Renderer.K8sApi.podsStore, \"view_in_ar\"],\n [\"Node\", Renderer.K8sApi.nodesStore, \"dns\"],\n [\"Deployment\", Renderer.K8sApi.deploymentStore, \"layers\"],\n [\"StatefulSet\", Renderer.K8sApi.statefulSetStore, \"layers\"],\n [\"DaemonSet\", Renderer.K8sApi.daemonSetStore, \"layers\"],\n [\"ReplicaSet\", Renderer.K8sApi.replicaSetStore, \"layers\"],\n [\"Job\", Renderer.K8sApi.jobStore, \"work\"],\n [\"CronJob\", Renderer.K8sApi.cronJobStore, \"schedule\"],\n [\"Service\", Renderer.K8sApi.serviceStore, \"settings_ethernet\"],\n [\"Ingress\", Renderer.K8sApi.ingressStore, \"alt_route\"],\n [\"Namespace\", Renderer.K8sApi.namespaceStore, \"folder\"],\n [\"HorizontalPodAutoscaler\", Renderer.K8sApi.hpaStore, \"swap_vert\"],\n ] as const;\nconst icons = new Map(inventory().map(([kind, , icon]) => [kind as string, icon]));\nfunction project(\n kind: string,\n objects: {\n metadata: { name?: string; namespace?: string; labels?: Partial<Record<string, string>> };\n selfLink: string;\n }[],\n): SearchResource[] {\n return objects.slice(0, 500).flatMap((object) =>\n object.metadata.name\n ? [\n {\n kind,\n name: object.metadata.name,\n namespace: object.metadata.namespace,\n labels: Object.fromEntries(\n Object.entries(object.metadata.labels ?? {}).filter(\n (entry): entry is [string, string] => typeof entry[1] === \"string\",\n ),\n ),\n path: object.selfLink,\n fields: projectSearchFields(kind, object),\n },\n ]\n : [],\n );\n}\n\nexport function ResourceSearchLauncher() {\n return (\n <button\n type=\"button\"\n title=\"Search resources (double Shift)\"\n aria-label=\"Search resources\"\n onClick={() => window.dispatchEvent(new Event(OPEN))}\n >\n <AiIcon material=\"search\" />\n </button>\n );\n}\n\nfunction SearchSnippet({ field, query }: { field?: { label: string; text: string }; query: string }) {\n if (!field) return null;\n const token = queryTokens(query).find((token) => field.text.toLocaleLowerCase().includes(token));\n const offset = token ? field.text.toLocaleLowerCase().indexOf(token) : -1;\n return (\n <small className=\"power-resource-search-snippet\">\n {field.label}:{\" \"}\n {offset < 0 || !token ? (\n field.text\n ) : (\n <>\n {field.text.slice(0, offset)}\n <mark>{field.text.slice(offset, offset + token.length)}</mark>\n {field.text.slice(offset + token.length)}\n </>\n )}\n </small>\n );\n}\n\nfunction SearchDialog({ close, clusterId }: { close: () => void; clusterId: string }) {\n const [query, setQuery] = useState(\"\");\n const [resources, setResources] = useState<SearchResource[]>([]);\n const [loading, setLoading] = useState(true);\n const [failed, setFailed] = useState(0);\n const [selectedPath, setSelectedPath] = useState<string>();\n const [error, setError] = useState(\"\");\n const dialog = useRef<HTMLDivElement>(null);\n const input = useRef<HTMLInputElement>(null);\n const searchIndex = useMemo(() => createResourceSearchIndex(resources), [resources]);\n const matches = searchIndex.search(query);\n const selectedResource = matches.find((resource) => resource.path === selectedPath) ?? matches[0];\n const results = matches.slice(0, 100);\n if (selectedResource && !results.includes(selectedResource)) results[results.length - 1] = selectedResource;\n const selected = results.findIndex((resource) => resource.path === selectedResource?.path);\n useEffect(() => {\n if (selectedPath !== undefined && selectedPath !== selectedResource?.path) setSelectedPath(undefined);\n }, [selectedPath, selectedResource?.path]);\n useEffect(() => {\n const previous = document.activeElement as HTMLElement | null;\n input.current?.focus();\n const controller = new AbortController();\n const timeout = window.setTimeout(() => controller.abort(), 15000);\n let live = true;\n const sources = inventory();\n const parts = sources.map(([kind, store]) => project(kind, store.items));\n setResources(parts.flat());\n let next = 0;\n const worker = async () => {\n while (next < sources.length && live && activeId() === clusterId) {\n const index = next++;\n const [kind, store] = sources[index];\n try {\n const objects = await store.api.list(\n { namespace: \"\", reqInit: { signal: controller.signal } },\n { limit: 500 },\n );\n if (!live || activeId() !== clusterId) return;\n parts[index] = project(kind, objects ?? []);\n setResources(parts.flat());\n } catch {\n if (live && activeId() === clusterId) setFailed((count) => count + 1);\n }\n }\n };\n void Promise.all([worker(), worker()]).then(() => {\n if (live) setLoading(false);\n });\n return () => {\n live = false;\n window.clearTimeout(timeout);\n controller.abort();\n if (previous?.isConnected) previous.focus();\n };\n }, [clusterId]);\n useEffect(() => {\n document.getElementById(`power-resource-result-${selected}`)?.scrollIntoView?.({ block: \"nearest\" });\n }, [selected, selectedResource?.path]);\n const open = (resource: SearchResource) => {\n if (activeId() !== clusterId) return;\n void Promise.resolve()\n .then(() => Renderer.Navigation.showDetails(resource.path))\n .then(() => {\n if (activeId() === clusterId) close();\n })\n .catch(() => setError(\"Could not open this resource. Try again or refresh the inventory.\"));\n };\n return (\n <div\n className=\"power-resource-search-backdrop\"\n data-freelens-power-ai-overlay=\"\"\n onClick={(event) => event.stopPropagation()}\n onMouseDown={(event) => {\n event.stopPropagation();\n if (event.target === event.currentTarget) close();\n }}\n >\n <div\n ref={dialog}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"power-resource-search-title\"\n className=\"power-resource-search-dialog\"\n onKeyDown={(event) => {\n if (event.key === \"Escape\") {\n event.stopPropagation();\n close();\n }\n if (event.key === \"Tab\") {\n const controls = Array.from(\n dialog.current?.querySelectorAll<HTMLElement>(\"input, button:not(:disabled)\") ?? [],\n );\n const index = controls.indexOf(document.activeElement as HTMLElement);\n if (event.shiftKey && index <= 0) {\n event.preventDefault();\n controls.at(-1)?.focus();\n } else if (!event.shiftKey && index === controls.length - 1) {\n event.preventDefault();\n controls[0]?.focus();\n }\n }\n }}\n >\n <div className=\"power-resource-search-heading\">\n <strong id=\"power-resource-search-title\" style={{ display: \"flex\", alignItems: \"center\", gap: 8 }}>\n <AiIcon material=\"search\" size={18} />\n Search resources\n </strong>\n <button type=\"button\" aria-label=\"Close resource search\" onClick={close}>\n <AiIcon material=\"close\" />\n </button>\n </div>\n <input\n ref={input}\n aria-label=\"Resource name, namespace, kind, label or indexed text\"\n aria-controls=\"power-resource-search-results\"\n placeholder=\"Name, namespace, label, image, condition…\"\n maxLength={300}\n value={query}\n onChange={(event) => {\n setQuery(event.target.value);\n setSelectedPath(undefined);\n }}\n onKeyDown={(event) => {\n if (event.key === \"ArrowDown\" || event.key === \"ArrowUp\") {\n event.preventDefault();\n const next = results.length\n ? (selected + (event.key === \"ArrowDown\" ? 1 : -1) + results.length) % results.length\n : 0;\n setSelectedPath(results[next]?.path);\n }\n if (event.key === \"Enter\" && !event.nativeEvent.isComposing && results[selected]) {\n event.preventDefault();\n open(results[selected]);\n }\n }}\n />\n <p role=\"status\">\n {loading ? \"Loading supported kinds; cached results are available. \" : \"Supported kinds loaded. \"}Partial\n inventory: up to 500 objects per kind, subject to permissions. Secrets, ConfigMaps and custom resources are\n excluded. Full-text index covers safe metadata, images, selectors and status only.\n {failed > 0 && ` ${failed} kinds unavailable; cached results retained.`}\n </p>\n {error && <p role=\"alert\">{error}</p>}\n <div\n id=\"power-resource-search-results\"\n role=\"list\"\n aria-label=\"Matching resources\"\n className=\"power-resource-search-results\"\n >\n {results.map((resource, index) => (\n <div\n key={resource.path}\n id={`power-resource-result-${index}`}\n role=\"listitem\"\n className={`power-resource-search-row ${selected === index ? \"selected\" : \"\"}`}\n >\n <button className=\"power-resource-search-open\" type=\"button\" onClick={() => open(resource)}>\n <AiIcon material={icons.get(resource.kind) ?? \"view_in_ar\"} />\n <span>\n <strong>{resource.name}</strong>\n <small>\n {resource.kind} · {resource.namespace ?? \"cluster scoped\"}\n </small>\n {searchIndex.nameParts(resource) && <small>Name parts</small>}\n {searchIndex.similarName(resource) && <small>Similar name</small>}\n <SearchSnippet field={searchIndex.snippet(resource, query)} query={query} />\n </span>\n </button>\n <DiagnosticExplainButton\n className=\"power-resource-search-ai\"\n aria-label={`Ask AI about ${resource.kind} ${resource.name}`}\n title=\"Ask AI about this resource\"\n onClick={() => {\n if (activeId() !== clusterId) return;\n close();\n explainDiagnosticFinding({\n clusterId,\n title: `${resource.kind}: ${resource.name}`,\n summary: `Resource selected from search. Kind: ${resource.kind}; namespace: ${resource.namespace ?? \"cluster scoped\"}; name: ${resource.name}. No live diagnostic data was collected.`,\n resource: { kind: resource.kind, name: resource.name, namespace: resource.namespace },\n });\n }}\n >\n <AiIcon material=\"auto_awesome\" />\n </DiagnosticExplainButton>\n </div>\n ))}\n {!results.length && <p>{loading ? \"Searching…\" : \"No matching resources in the available inventory\"}</p>}\n </div>\n {matches.length > 100 && <p>Showing 100 matches. Refine your search.</p>}\n <p className=\"power-resource-search-shortcuts\">↑↓ Navigate · Enter Open · Esc Close</p>\n </div>\n </div>\n );\n}\n\nexport function startResourceSearch(): () => void {\n const container = document.createElement(\"div\");\n const style = document.createElement(\"style\");\n style.textContent = `.power-resource-search-backdrop{position:fixed;inset:0;z-index:10000;background:#0007;display:flex;align-items:flex-start;justify-content:center;padding:12vh 20px}.power-resource-search-dialog{width:680px;max-width:100%;max-height:75vh;display:flex;flex-direction:column;padding:20px;gap:12px;background:var(--mainBackground,#222);color:var(--textColorPrimary,#eee);border:1px solid var(--borderColor,#555);border-radius:8px;box-shadow:0 16px 60px #0008}.power-resource-search-heading{display:flex;align-items:center;justify-content:space-between}.power-resource-search-dialog input{padding:12px;font-size:16px;background:var(--inputControlBackground,#333);color:inherit;border:1px solid var(--borderColor,#666);border-radius:4px}.power-resource-search-dialog p{margin:0;font-size:12px;color:var(--textColorSecondary,#bbb)}.power-resource-search-results{overflow:auto;min-height:60px}.power-resource-search-row{display:flex;align-items:center;gap:12px;padding:4px;border-radius:4px}.power-resource-search-row{border-left:3px solid transparent}.power-resource-search-row.selected{background:var(--tableSelectedBackground,#394b66);border-left-color:var(--primary,#668fff)}.power-resource-search-row.selected .Icon{color:var(--primary,#8daaff)}.power-resource-search-row:hover{background:var(--tableHoveredBackground,#ffffff0b)}.power-resource-search-dialog button:hover:not(:disabled){background:#ffffff12;border-radius:4px}.power-resource-search-row>button:last-child{border:1px solid var(--borderColor,#555);border-radius:4px}.power-resource-search-shortcuts{padding-top:8px;border-top:1px solid var(--borderColor,#555)}.power-resource-search-dialog button{min-width:32px;min-height:32px;padding:6px;color:inherit;background:transparent;border:0;cursor:pointer}.power-resource-search-dialog button:disabled{opacity:.45;cursor:default}.power-resource-search-open{display:flex;align-items:center;gap:12px;flex:1;text-align:left;min-width:0}.power-resource-search-open span{min-width:0}.power-resource-search-open strong,.power-resource-search-open small{display:block;overflow-wrap:anywhere}.power-resource-search-open small{opacity:.7}.power-resource-search-row>button.power-resource-search-ai{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:34px;height:34px;min-width:34px;padding:0;background:var(--contentColor,#292d32);border:1px solid var(--borderColor,#666)}.power-resource-search-row.selected>button.power-resource-search-ai{background:var(--mainBackground,#202326);border-color:color-mix(in srgb,var(--textColorPrimary,#ddd) 55%,var(--borderColor,#666));box-shadow:0 1px 3px #0002}.power-resource-search-row.selected>button.power-resource-search-ai .Icon{color:var(--textColorAccent,#eee)}.power-resource-search-row>button.power-resource-search-ai:hover:not(:disabled){background:var(--contentColor,#30343a);border-color:var(--primary,#8daaff);color:var(--primary,#8daaff)}.power-resource-search-row>button.power-resource-search-ai:hover:not(:disabled) .Icon{color:inherit}.power-resource-search-dialog button:focus-visible{outline:2px solid var(--primary,#668fff)}`;\n document.head.append(style);\n document.body.append(container);\n let visible = false;\n const close = () => {\n if (visible) {\n visible = false;\n ReactDOM.unmountComponentAtNode(container);\n }\n };\n const show = () => {\n const clusterId = activeId();\n if (\n window.top === window ||\n !clusterId ||\n visible ||\n document.querySelector('[aria-modal=\"true\"], .Dialog.is-opened')\n )\n return;\n visible = true;\n ReactDOM.render(<SearchDialog close={close} clusterId={clusterId} />, container);\n };\n let shift = createDoubleShift();\n const resetShift = () => {\n shift = createDoubleShift();\n };\n const key = (event: KeyboardEvent) => {\n if (event.isComposing || event.key === \"Process\") {\n resetShift();\n return;\n }\n if (shift(event, performance.now())) show();\n };\n window.addEventListener(\"blur\", resetShift);\n window.addEventListener(\"keydown\", key);\n window.addEventListener(\"keyup\", key);\n window.addEventListener(OPEN, show);\n const dispose = reaction(activeId, close);\n return () => {\n close();\n dispose();\n window.removeEventListener(\"blur\", resetShift);\n window.removeEventListener(\"keydown\", key);\n window.removeEventListener(\"keyup\", key);\n window.removeEventListener(OPEN, show);\n container.remove();\n style.remove();\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ //#region src/common/hpa-diagnostics.ts
2
+ function diagnoseHpa(hpa) {
3
+ const min = hpa.spec?.minReplicas ?? 1;
4
+ const max = hpa.spec?.maxReplicas;
5
+ const conditions = hpa.status?.conditions ?? [];
6
+ const evidence = conditions.map((c) => `${c.type}=${c.status}${c.reason ? ` (${c.reason})` : ""}${c.message ? `: ${c.message}` : ""}`);
7
+ const disabled = hpa.status?.currentReplicas === 0 && conditions.some((c) => c.type === "ScalingActive" && c.status === "False" && c.reason === "ScalingDisabled");
8
+ const failed = conditions.some((c) => (c.type === "ScalingActive" || c.type === "AbleToScale") && c.status === "False" && !(disabled && c.type === "ScalingActive" && c.reason === "ScalingDisabled"));
9
+ const limited = conditions.find((c) => c.type === "ScalingLimited" && c.status === "True");
10
+ const maxBlocked = limited?.reason === "TooManyReplicas" && min !== max;
11
+ return {
12
+ state: failed || maxBlocked ? "warning" : "neutral",
13
+ summary: failed ? "Autoscaling reports a controller or metric failure" : disabled ? "Autoscaling is disabled while the target has zero replicas" : min === max ? "Replica count is fixed by equal minimum and maximum" : maxBlocked ? "Desired scale exceeds the configured maximum" : limited?.reason === "TooFewReplicas" ? "Minimum replicas constrain downscaling" : limited ? "Scaling is limited; inspect the controller reason" : conditions.length ? "No scaling blocker reported by available conditions" : "Autoscaling conditions are unavailable",
14
+ evidence,
15
+ current: hpa.status?.currentReplicas,
16
+ desired: hpa.status?.desiredReplicas,
17
+ min,
18
+ max
19
+ };
20
+ }
21
+ //#endregion
22
+ exports.diagnoseHpa = diagnoseHpa;
23
+
24
+ //# sourceMappingURL=hpa-diagnostics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hpa-diagnostics.js","names":[],"sources":["../../../../src/common/hpa-diagnostics.ts"],"sourcesContent":["export interface HpaDiagnosticsInput {\n spec?: { minReplicas?: number; maxReplicas: number };\n status?: {\n currentReplicas?: number;\n desiredReplicas?: number;\n conditions?: { type: string; status: string; reason?: string; message?: string }[];\n };\n}\nexport function diagnoseHpa(hpa: HpaDiagnosticsInput) {\n const min = hpa.spec?.minReplicas ?? 1;\n const max = hpa.spec?.maxReplicas;\n const conditions = hpa.status?.conditions ?? [];\n const evidence = conditions.map(\n (c) => `${c.type}=${c.status}${c.reason ? ` (${c.reason})` : \"\"}${c.message ? `: ${c.message}` : \"\"}`,\n );\n const disabled =\n hpa.status?.currentReplicas === 0 &&\n conditions.some((c) => c.type === \"ScalingActive\" && c.status === \"False\" && c.reason === \"ScalingDisabled\");\n const failed = conditions.some(\n (c) =>\n (c.type === \"ScalingActive\" || c.type === \"AbleToScale\") &&\n c.status === \"False\" &&\n !(disabled && c.type === \"ScalingActive\" && c.reason === \"ScalingDisabled\"),\n );\n const limited = conditions.find((c) => c.type === \"ScalingLimited\" && c.status === \"True\");\n const maxBlocked = limited?.reason === \"TooManyReplicas\" && min !== max;\n const state = failed || maxBlocked ? \"warning\" : \"neutral\";\n const summary = failed\n ? \"Autoscaling reports a controller or metric failure\"\n : disabled\n ? \"Autoscaling is disabled while the target has zero replicas\"\n : min === max\n ? \"Replica count is fixed by equal minimum and maximum\"\n : maxBlocked\n ? \"Desired scale exceeds the configured maximum\"\n : limited?.reason === \"TooFewReplicas\"\n ? \"Minimum replicas constrain downscaling\"\n : limited\n ? \"Scaling is limited; inspect the controller reason\"\n : conditions.length\n ? \"No scaling blocker reported by available conditions\"\n : \"Autoscaling conditions are unavailable\";\n return {\n state,\n summary,\n evidence,\n current: hpa.status?.currentReplicas,\n desired: hpa.status?.desiredReplicas,\n min,\n max,\n };\n}\n"],"mappings":";AAQA,SAAgB,YAAY,KAA0B;CACpD,MAAM,MAAM,IAAI,MAAM,eAAe;CACrC,MAAM,MAAM,IAAI,MAAM;CACtB,MAAM,aAAa,IAAI,QAAQ,cAAc,CAAC;CAC9C,MAAM,WAAW,WAAW,KACzB,MAAM,GAAG,EAAE,KAAK,GAAG,EAAE,SAAS,EAAE,SAAS,KAAK,EAAE,OAAO,KAAK,KAAK,EAAE,UAAU,KAAK,EAAE,YAAY,IACnG;CACA,MAAM,WACJ,IAAI,QAAQ,oBAAoB,KAChC,WAAW,MAAM,MAAM,EAAE,SAAS,mBAAmB,EAAE,WAAW,WAAW,EAAE,WAAW,iBAAiB;CAC7G,MAAM,SAAS,WAAW,MACvB,OACE,EAAE,SAAS,mBAAmB,EAAE,SAAS,kBAC1C,EAAE,WAAW,WACb,EAAE,YAAY,EAAE,SAAS,mBAAmB,EAAE,WAAW,kBAC7D;CACA,MAAM,UAAU,WAAW,MAAM,MAAM,EAAE,SAAS,oBAAoB,EAAE,WAAW,MAAM;CACzF,MAAM,aAAa,SAAS,WAAW,qBAAqB,QAAQ;CAiBpE,OAAO;EACL,OAjBY,UAAU,aAAa,YAAY;EAkB/C,SAjBc,SACZ,uDACA,WACE,+DACA,QAAQ,MACN,wDACA,aACE,iDACA,SAAS,WAAW,mBAClB,2CACA,UACE,sDACA,WAAW,SACT,wDACA;EAId;EACA,SAAS,IAAI,QAAQ;EACrB,SAAS,IAAI,QAAQ;EACrB;EACA;CACF;AACF"}
@@ -0,0 +1,73 @@
1
+ const require_ai_terminal_context = require("./ai-terminal-context.js");
2
+ const require_date_time = require("./date-time.js");
3
+ //#region src/common/incident-summary.ts
4
+ var api = {
5
+ Pod: ["/api/v1", "pods"],
6
+ Node: [
7
+ "/api/v1",
8
+ "nodes",
9
+ "cluster"
10
+ ],
11
+ Namespace: [
12
+ "/api/v1",
13
+ "namespaces",
14
+ "cluster"
15
+ ],
16
+ PersistentVolume: [
17
+ "/api/v1",
18
+ "persistentvolumes",
19
+ "cluster"
20
+ ],
21
+ PersistentVolumeClaim: ["/api/v1", "persistentvolumeclaims"],
22
+ Service: ["/api/v1", "services"],
23
+ HorizontalPodAutoscaler: ["/apis/autoscaling/v2", "horizontalpodautoscalers"],
24
+ Ingress: ["/apis/networking.k8s.io/v1", "ingresses"],
25
+ EndpointSlice: ["/apis/discovery.k8s.io/v1", "endpointslices"],
26
+ Deployment: ["/apis/apps/v1", "deployments"],
27
+ ReplicaSet: ["/apis/apps/v1", "replicasets"],
28
+ StatefulSet: ["/apis/apps/v1", "statefulsets"],
29
+ DaemonSet: ["/apis/apps/v1", "daemonsets"],
30
+ Job: ["/apis/batch/v1", "jobs"],
31
+ CronJob: ["/apis/batch/v1", "cronjobs"]
32
+ };
33
+ function incidentResourcePath(resource) {
34
+ if (!Object.hasOwn(api, resource.kind)) return;
35
+ const route = api[resource.kind];
36
+ const validName = (value) => typeof value === "string" && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,252}$/.test(value);
37
+ if (!validName(resource.name)) return;
38
+ if (route[2] === "cluster") {
39
+ if (resource.namespace !== void 0) return;
40
+ return `${route[0]}/${route[1]}/${encodeURIComponent(resource.name)}`;
41
+ }
42
+ if (!validName(resource.namespace)) return;
43
+ return `${route[0]}/namespaces/${encodeURIComponent(resource.namespace)}/${route[1]}/${encodeURIComponent(resource.name)}`;
44
+ }
45
+ var safe = (value, limit) => require_ai_terminal_context.redactAiTerminalText(value).slice(0, limit);
46
+ function formatIncidentSummary(incident) {
47
+ const date = incident.time === void 0 ? void 0 : new Date(incident.time);
48
+ const offset = date && Number.isFinite(date.getTime()) ? -date.getTimezoneOffset() : void 0;
49
+ const timezone = offset === void 0 ? "" : ` UTC${offset < 0 ? "-" : "+"}${String(Math.floor(Math.abs(offset) / 60)).padStart(2, "0")}:${String(Math.abs(offset) % 60).padStart(2, "0")}`;
50
+ const links = [...new Set((incident.resources ?? []).slice(0, 12).map(incidentResourcePath).filter((value) => !!value))];
51
+ const lines = [
52
+ `Problem: ${safe(incident.problem, 240)}`,
53
+ ...incident.clusterName ? [`Cluster: ${safe(incident.clusterName, 160)}`] : [],
54
+ `Snapshot time: ${date ? require_date_time.formatDateTime(date) : "Unavailable"}${timezone}`,
55
+ "Facts:",
56
+ ...incident.facts.slice(0, 12).map((fact) => `- ${safe(fact, 400)}`),
57
+ ...links.length ? ["Resource API links (Freelens resource detail paths):", ...links.map((link) => `- ${link}`)] : []
58
+ ];
59
+ const footer = "Read-only snapshot; evidence may be partial or stale";
60
+ const selected = [];
61
+ let length = 52;
62
+ for (const line of lines) {
63
+ if (length + line.length + 1 > 8e3) break;
64
+ selected.push(line);
65
+ length += line.length + 1;
66
+ }
67
+ return [...selected, footer].join("\n");
68
+ }
69
+ //#endregion
70
+ exports.formatIncidentSummary = formatIncidentSummary;
71
+ exports.incidentResourcePath = incidentResourcePath;
72
+
73
+ //# sourceMappingURL=incident-summary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"incident-summary.js","names":[],"sources":["../../../../src/common/incident-summary.ts"],"sourcesContent":["import { redactAiTerminalText } from \"./ai-terminal-context\";\nimport { formatDateTime } from \"./date-time\";\nexport interface IncidentResource {\n kind: string;\n namespace?: string;\n name: string;\n}\nexport interface IncidentSummary {\n clusterName?: string;\n problem: string;\n facts: string[];\n time?: string | number;\n resources?: IncidentResource[];\n}\nconst api: Record<string, [string, string, \"cluster\"?]> = {\n Pod: [\"/api/v1\", \"pods\"],\n Node: [\"/api/v1\", \"nodes\", \"cluster\"],\n Namespace: [\"/api/v1\", \"namespaces\", \"cluster\"],\n PersistentVolume: [\"/api/v1\", \"persistentvolumes\", \"cluster\"],\n PersistentVolumeClaim: [\"/api/v1\", \"persistentvolumeclaims\"],\n Service: [\"/api/v1\", \"services\"],\n HorizontalPodAutoscaler: [\"/apis/autoscaling/v2\", \"horizontalpodautoscalers\"],\n Ingress: [\"/apis/networking.k8s.io/v1\", \"ingresses\"],\n EndpointSlice: [\"/apis/discovery.k8s.io/v1\", \"endpointslices\"],\n Deployment: [\"/apis/apps/v1\", \"deployments\"],\n ReplicaSet: [\"/apis/apps/v1\", \"replicasets\"],\n StatefulSet: [\"/apis/apps/v1\", \"statefulsets\"],\n DaemonSet: [\"/apis/apps/v1\", \"daemonsets\"],\n Job: [\"/apis/batch/v1\", \"jobs\"],\n CronJob: [\"/apis/batch/v1\", \"cronjobs\"],\n};\nexport function incidentResourcePath(resource: IncidentResource): string | undefined {\n if (!Object.hasOwn(api, resource.kind)) return;\n const route = api[resource.kind];\n const validName = (value: string | undefined) =>\n typeof value === \"string\" && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,252}$/.test(value);\n if (!validName(resource.name)) return;\n if (route[2] === \"cluster\") {\n if (resource.namespace !== undefined) return;\n return `${route[0]}/${route[1]}/${encodeURIComponent(resource.name)}`;\n }\n if (!validName(resource.namespace)) return;\n return `${route[0]}/namespaces/${encodeURIComponent(resource.namespace!)}/${route[1]}/${encodeURIComponent(resource.name)}`;\n}\nconst safe = (value: string, limit: number) => redactAiTerminalText(value).slice(0, limit);\nexport function formatIncidentSummary(incident: IncidentSummary): string {\n const date = incident.time === undefined ? undefined : new Date(incident.time);\n const offset = date && Number.isFinite(date.getTime()) ? -date.getTimezoneOffset() : undefined;\n const timezone =\n offset === undefined\n ? \"\"\n : ` UTC${offset < 0 ? \"-\" : \"+\"}${String(Math.floor(Math.abs(offset) / 60)).padStart(2, \"0\")}:${String(Math.abs(offset) % 60).padStart(2, \"0\")}`;\n const links = [\n ...new Set(\n (incident.resources ?? [])\n .slice(0, 12)\n .map(incidentResourcePath)\n .filter((value): value is string => !!value),\n ),\n ];\n const lines = [\n `Problem: ${safe(incident.problem, 240)}`,\n ...(incident.clusterName ? [`Cluster: ${safe(incident.clusterName, 160)}`] : []),\n `Snapshot time: ${date ? formatDateTime(date) : \"Unavailable\"}${timezone}`,\n \"Facts:\",\n ...incident.facts.slice(0, 12).map((fact) => `- ${safe(fact, 400)}`),\n ...(links.length\n ? [\"Resource API links (Freelens resource detail paths):\", ...links.map((link) => `- ${link}`)]\n : []),\n ];\n const footer = \"Read-only snapshot; evidence may be partial or stale\";\n const selected: string[] = [];\n let length = footer.length;\n for (const line of lines) {\n if (length + line.length + 1 > 8000) break;\n selected.push(line);\n length += line.length + 1;\n }\n return [...selected, footer].join(\"\\n\");\n}\n"],"mappings":"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"}
@@ -0,0 +1,92 @@
1
+ //#region src/common/resource-health.ts
2
+ function summarizeObjectHealth(object) {
3
+ const status = object.status;
4
+ const conditions = status?.conditions ?? [];
5
+ const rows = [];
6
+ let ready;
7
+ let issue;
8
+ if (object.kind === "Pod") {
9
+ rows.push({
10
+ name: "Phase",
11
+ value: status?.phase ?? "Unknown"
12
+ });
13
+ const condition = conditions.find((condition) => condition.type === "Ready");
14
+ ready = condition?.status === "True" ? true : condition?.status === "False" ? false : void 0;
15
+ const containers = status?.containerStatuses ?? [];
16
+ rows.push({
17
+ name: "Readiness",
18
+ value: status?.phase === "Succeeded" ? "Completed" : ready === void 0 ? "Readiness unknown" : ready ? "Ready" : "Not ready"
19
+ });
20
+ rows.push({
21
+ name: "Containers ready",
22
+ value: `${containers.filter((container) => container.ready).length}/${object.spec?.containers?.length ?? "?"}`
23
+ });
24
+ rows.push({
25
+ name: "Restarts",
26
+ value: String([...containers, ...status?.initContainerStatuses ?? []].reduce((sum, container) => sum + container.restartCount, 0))
27
+ });
28
+ if (status?.phase === "Succeeded") ready = true;
29
+ if (status?.phase === "Failed") {
30
+ ready = false;
31
+ issue = "Pod failed";
32
+ }
33
+ } else if (object.kind === "Node") {
34
+ const condition = conditions.find((condition) => condition.type === "Ready");
35
+ ready = condition?.status === "True" ? true : condition?.status === "False" ? false : void 0;
36
+ rows.push({
37
+ name: "Readiness",
38
+ value: ready === void 0 ? "Readiness unknown" : ready ? "Ready" : "Not ready"
39
+ });
40
+ rows.push({
41
+ name: "Scheduling",
42
+ value: object.spec?.unschedulable ? "Cordoned" : "Enabled"
43
+ });
44
+ const pressure = conditions.find((condition) => /Pressure$|NetworkUnavailable/.test(condition.type) && condition.status === "True");
45
+ if (pressure) {
46
+ issue = pressure.reason || pressure.type;
47
+ ready = false;
48
+ }
49
+ } else if (object.kind === "Namespace") {
50
+ rows.push({
51
+ name: "Phase",
52
+ value: status?.phase ?? "Unknown"
53
+ });
54
+ ready = status?.phase ? status.phase === "Active" : void 0;
55
+ } else {
56
+ const desired = object.kind === "DaemonSet" ? status?.desiredNumberScheduled : object.spec?.replicas ?? 1;
57
+ const count = object.kind === "DaemonSet" ? status?.numberReady : status?.readyReplicas;
58
+ const observed = status?.observedGeneration;
59
+ const stale = object.metadata.generation !== void 0 && (observed === void 0 || observed < object.metadata.generation);
60
+ ready = status && desired !== void 0 ? (count ?? 0) >= desired && !stale : void 0;
61
+ rows.push({
62
+ name: "Ready replicas",
63
+ value: `${status ? count ?? 0 : "?"}/${desired ?? "?"}`
64
+ });
65
+ const updated = object.kind === "DaemonSet" ? status?.updatedNumberScheduled : status?.updatedReplicas;
66
+ rows.push({
67
+ name: "Updated replicas",
68
+ value: status ? String(updated ?? 0) : "Unknown"
69
+ });
70
+ if (stale) issue = "Controller has not observed the latest generation";
71
+ }
72
+ const failure = conditions.find((condition) => condition.type === "ReplicaFailure" && condition.status === "True" || [
73
+ "Ready",
74
+ "Available",
75
+ "Progressing",
76
+ "PodScheduled"
77
+ ].includes(condition.type) && condition.status === "False");
78
+ if (!issue && failure && !(object.kind === "Pod" && status?.phase === "Succeeded")) {
79
+ issue = failure.reason || `${failure.type}: ${failure.status}`;
80
+ ready = false;
81
+ }
82
+ if (!issue && ready === false) issue = object.kind === "Namespace" ? "Namespace is not active" : "Readiness target is not met";
83
+ return {
84
+ state: ready === void 0 ? "unknown" : ready ? "healthy" : "warning",
85
+ rows,
86
+ issue
87
+ };
88
+ }
89
+ //#endregion
90
+ exports.summarizeObjectHealth = summarizeObjectHealth;
91
+
92
+ //# sourceMappingURL=resource-health.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource-health.js","names":[],"sources":["../../../../src/common/resource-health.ts"],"sourcesContent":["import type { DiagnosticsObject } from \"./resource-diagnostics\";\n\nexport type HealthObject = DiagnosticsObject & {\n spec?: DiagnosticsObject[\"spec\"] & { replicas?: number; minReadySeconds?: number };\n status?: DiagnosticsObject[\"status\"] & {\n replicas?: number;\n availableReplicas?: number;\n desiredNumberScheduled?: number;\n numberReady?: number;\n updatedNumberScheduled?: number;\n initContainerStatuses?: { name: string; restartCount: number; ready?: boolean }[];\n };\n};\nexport interface ObjectHealth {\n state: \"healthy\" | \"warning\" | \"unknown\";\n rows: { name: string; value: string }[];\n issue?: string;\n}\nexport function summarizeObjectHealth(object: HealthObject): ObjectHealth {\n const status = object.status;\n const conditions = status?.conditions ?? [];\n const rows: ObjectHealth[\"rows\"] = [];\n let ready: boolean | undefined;\n let issue: string | undefined;\n if (object.kind === \"Pod\") {\n rows.push({ name: \"Phase\", value: status?.phase ?? \"Unknown\" });\n const condition = conditions.find((condition) => condition.type === \"Ready\");\n ready = condition?.status === \"True\" ? true : condition?.status === \"False\" ? false : undefined;\n const containers = status?.containerStatuses ?? [];\n rows.push({\n name: \"Readiness\",\n value:\n status?.phase === \"Succeeded\"\n ? \"Completed\"\n : ready === undefined\n ? \"Readiness unknown\"\n : ready\n ? \"Ready\"\n : \"Not ready\",\n });\n rows.push({\n name: \"Containers ready\",\n value: `${containers.filter((container) => container.ready).length}/${object.spec?.containers?.length ?? \"?\"}`,\n });\n rows.push({\n name: \"Restarts\",\n value: String(\n [...containers, ...(status?.initContainerStatuses ?? [])].reduce(\n (sum, container) => sum + container.restartCount,\n 0,\n ),\n ),\n });\n if (status?.phase === \"Succeeded\") ready = true;\n if (status?.phase === \"Failed\") {\n ready = false;\n issue = \"Pod failed\";\n }\n } else if (object.kind === \"Node\") {\n const condition = conditions.find((condition) => condition.type === \"Ready\");\n ready = condition?.status === \"True\" ? true : condition?.status === \"False\" ? false : undefined;\n rows.push({ name: \"Readiness\", value: ready === undefined ? \"Readiness unknown\" : ready ? \"Ready\" : \"Not ready\" });\n rows.push({ name: \"Scheduling\", value: object.spec?.unschedulable ? \"Cordoned\" : \"Enabled\" });\n const pressure = conditions.find(\n (condition) => /Pressure$|NetworkUnavailable/.test(condition.type) && condition.status === \"True\",\n );\n if (pressure) {\n issue = pressure.reason || pressure.type;\n ready = false;\n }\n } else if (object.kind === \"Namespace\") {\n rows.push({ name: \"Phase\", value: status?.phase ?? \"Unknown\" });\n ready = status?.phase ? status.phase === \"Active\" : undefined;\n } else {\n const desired = object.kind === \"DaemonSet\" ? status?.desiredNumberScheduled : (object.spec?.replicas ?? 1);\n const count = object.kind === \"DaemonSet\" ? status?.numberReady : status?.readyReplicas;\n const observed = status?.observedGeneration;\n const stale =\n object.metadata.generation !== undefined && (observed === undefined || observed < object.metadata.generation);\n ready = status && desired !== undefined ? (count ?? 0) >= desired && !stale : undefined;\n rows.push({ name: \"Ready replicas\", value: `${status ? (count ?? 0) : \"?\"}/${desired ?? \"?\"}` });\n const updated = object.kind === \"DaemonSet\" ? status?.updatedNumberScheduled : status?.updatedReplicas;\n rows.push({ name: \"Updated replicas\", value: status ? String(updated ?? 0) : \"Unknown\" });\n if (stale) issue = \"Controller has not observed the latest generation\";\n }\n const failure = conditions.find(\n (condition) =>\n (condition.type === \"ReplicaFailure\" && condition.status === \"True\") ||\n ([\"Ready\", \"Available\", \"Progressing\", \"PodScheduled\"].includes(condition.type) && condition.status === \"False\"),\n );\n if (!issue && failure && !(object.kind === \"Pod\" && status?.phase === \"Succeeded\")) {\n issue = failure.reason || `${failure.type}: ${failure.status}`;\n ready = false;\n }\n if (!issue && ready === false)\n issue = object.kind === \"Namespace\" ? \"Namespace is not active\" : \"Readiness target is not met\";\n return { state: ready === undefined ? \"unknown\" : ready ? \"healthy\" : \"warning\", rows, issue };\n}\n"],"mappings":"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"}
@@ -0,0 +1,155 @@
1
+ const require_service_diagnostics = require("./service-diagnostics.js");
2
+ const require_incident_summary = require("./incident-summary.js");
3
+ //#region src/common/resource-relationships.ts
4
+ function relationshipReferencePath(kind, name, namespace) {
5
+ return require_incident_summary.incidentResourcePath({
6
+ kind,
7
+ name,
8
+ namespace
9
+ });
10
+ }
11
+ function buildResourceRelationships(service, inventory, focusedPod) {
12
+ const external = service.spec?.type === "ExternalName";
13
+ const diagnostics = require_service_diagnostics.diagnoseService(service, external ? [] : inventory.pods, external ? [] : inventory.slices);
14
+ const nodes = [];
15
+ const edges = [];
16
+ const gaps = [...inventory.gaps ?? []];
17
+ let omitted = 0;
18
+ const namespace = service.metadata.namespace;
19
+ const add = (node) => {
20
+ if (!nodes.some((n) => n.id === node.id)) nodes.push(node);
21
+ return node.id;
22
+ };
23
+ const svc = add({
24
+ id: "service",
25
+ kind: "Service",
26
+ name: service.metadata.name ?? "Unnamed",
27
+ role: "service",
28
+ detail: diagnostics.summary,
29
+ tone: "neutral",
30
+ selfLink: service.selfLink
31
+ });
32
+ const upstream = inventory.ingresses?.filter((ingress) => ingress.metadata.namespace === namespace && [
33
+ ingress.spec?.defaultBackend?.service?.name,
34
+ ingress.spec?.backend?.serviceName,
35
+ ...(ingress.spec?.rules ?? []).flatMap((rule) => (rule.http?.paths ?? []).map((path) => path.backend?.service?.name ?? path.backend?.serviceName))
36
+ ].includes(service.metadata.name)) ?? [];
37
+ omitted += Math.max(0, upstream.length - 5);
38
+ for (const ingress of upstream.slice(0, 5)) {
39
+ const id = add({
40
+ id: `ingress/${ingress.metadata.uid ?? ingress.metadata.name}`,
41
+ kind: "Ingress",
42
+ name: ingress.metadata.name ?? "Unnamed",
43
+ role: "upstream",
44
+ detail: "Backend Service reference",
45
+ tone: "neutral",
46
+ selfLink: ingress.selfLink
47
+ });
48
+ edges.push({
49
+ from: id,
50
+ to: svc,
51
+ label: "references backend"
52
+ });
53
+ }
54
+ const pods = diagnostics.matchingPods?.filter((pod) => !focusedPod || pod.metadata.name === focusedPod) ?? [];
55
+ omitted += Math.max(0, pods.length - 8);
56
+ for (const pod of pods.slice(0, 8)) {
57
+ const ready = pod.status?.conditions?.find((c) => c.type === "Ready");
58
+ const id = add({
59
+ id: `pod/${pod.metadata.uid ?? pod.metadata.name}`,
60
+ kind: "Pod",
61
+ name: pod.metadata.name ?? "Unnamed",
62
+ role: "backend",
63
+ detail: pod.status?.phase === "Succeeded" ? "Completed" : pod.status?.phase === "Failed" ? "Failed" : pod.metadata.deletionTimestamp ? "Terminating" : pod.status?.phase === "Pending" ? "Pending" : ready ? `Ready: ${ready.status}` : "Readiness unknown",
64
+ tone: pod.status?.phase === "Succeeded" ? "healthy" : pod.status?.phase === "Failed" || pod.status?.phase === "Pending" || pod.metadata.deletionTimestamp || ready?.status === "False" ? "warning" : ready?.status === "True" ? "healthy" : "neutral",
65
+ selfLink: pod.selfLink
66
+ });
67
+ edges.push({
68
+ from: svc,
69
+ to: id,
70
+ label: "selector matches"
71
+ });
72
+ if (pod.spec?.nodeName) {
73
+ const node = add({
74
+ id: `node/${pod.spec.nodeName}`,
75
+ kind: "Node",
76
+ name: pod.spec.nodeName,
77
+ role: "placement",
78
+ detail: "Assignment reference; node health not queried",
79
+ tone: "neutral",
80
+ selfLink: relationshipReferencePath("Node", pod.spec.nodeName),
81
+ inferred: true
82
+ });
83
+ edges.push({
84
+ from: id,
85
+ to: node,
86
+ label: "assigned to"
87
+ });
88
+ }
89
+ let child = id;
90
+ let refs = pod.metadata.ownerReferences;
91
+ const visited = /* @__PURE__ */ new Set();
92
+ for (let depth = 0; depth < 4; depth++) {
93
+ const ref = refs?.find((r) => r.controller) ?? (refs?.length === 1 ? refs[0] : void 0);
94
+ if (!ref || visited.has(ref.uid)) break;
95
+ visited.add(ref.uid);
96
+ const owner = inventory.owners?.find((o) => o.metadata.uid === ref.uid && o.metadata.namespace === namespace);
97
+ const ownerId = add({
98
+ id: `owner/${ref.uid}`,
99
+ kind: ref.kind ?? "Owner",
100
+ name: ref.name ?? "Unnamed",
101
+ role: "placement",
102
+ detail: owner ? "Controller object observed" : "Owner reference only; object not observed",
103
+ tone: "neutral",
104
+ selfLink: owner?.selfLink ?? relationshipReferencePath(ref.kind ?? "", ref.name ?? "", namespace) ?? relationshipReferencePath(ref.kind ?? "", ref.name ?? ""),
105
+ inferred: !owner
106
+ });
107
+ edges.push({
108
+ from: child,
109
+ to: ownerId,
110
+ label: "owned by"
111
+ });
112
+ child = ownerId;
113
+ refs = owner?.metadata.ownerReferences;
114
+ }
115
+ }
116
+ const slices = diagnostics.matchingSlices ?? [];
117
+ omitted += Math.max(0, slices.length - 5);
118
+ for (const slice of slices.slice(0, 5)) {
119
+ const ready = slice.endpoints?.filter((e) => e.conditions?.ready !== false && !e.conditions?.terminating).length;
120
+ const id = add({
121
+ id: `slice/${slice.metadata.name}`,
122
+ kind: "EndpointSlice",
123
+ name: slice.metadata.name ?? "Unnamed",
124
+ role: "backend",
125
+ detail: `${slice.addressType} · ${ready ?? "unknown"} ready endpoint entries`,
126
+ tone: ready === 0 ? "warning" : "neutral",
127
+ selfLink: slice.selfLink
128
+ });
129
+ edges.push({
130
+ from: svc,
131
+ to: id,
132
+ label: "published in"
133
+ });
134
+ }
135
+ if (!external && !inventory.pods) gaps.push("Pod inventory unavailable; selector relationships are unknown");
136
+ if (!inventory.slices && !diagnostics.external) gaps.push("EndpointSlice inventory unavailable; published backends are unknown");
137
+ if (!inventory.ingresses) gaps.push("Ingress inventory unavailable; upstream references are unknown");
138
+ return {
139
+ nodes,
140
+ edges: [...new Map(edges.map((edge) => [JSON.stringify([
141
+ edge.from,
142
+ edge.to,
143
+ edge.label
144
+ ]), edge])).values()],
145
+ omitted,
146
+ gaps: [...new Set(gaps)],
147
+ diagnostics,
148
+ podOptions: diagnostics.matchingPods?.map((p) => p.metadata.name).filter((name) => !!name) ?? []
149
+ };
150
+ }
151
+ //#endregion
152
+ exports.buildResourceRelationships = buildResourceRelationships;
153
+ exports.relationshipReferencePath = relationshipReferencePath;
154
+
155
+ //# sourceMappingURL=resource-relationships.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource-relationships.js","names":[],"sources":["../../../../src/common/resource-relationships.ts"],"sourcesContent":["import { incidentResourcePath } from \"./incident-summary\";\nimport {\n diagnoseService,\n type ServiceDiagnosticObject,\n type ServiceDiagnosticPod,\n type ServiceDiagnosticSlice,\n} from \"./service-diagnostics\";\n\nexport interface RelationshipMetadata {\n uid?: string;\n name?: string;\n namespace?: string;\n ownerReferences?: { uid: string; name?: string; kind?: string; controller?: boolean }[];\n}\nexport interface RelationshipObject {\n kind?: string;\n metadata: RelationshipMetadata;\n selfLink?: string;\n}\nexport interface RelationshipPod extends ServiceDiagnosticPod {\n metadata: ServiceDiagnosticPod[\"metadata\"] & RelationshipMetadata;\n spec?: ServiceDiagnosticPod[\"spec\"] & { nodeName?: string };\n status?: ServiceDiagnosticPod[\"status\"] & { phase?: string };\n selfLink?: string;\n}\nexport interface RelationshipSlice extends ServiceDiagnosticSlice {\n selfLink?: string;\n}\nexport interface RelationshipIngress extends RelationshipObject {\n spec?: {\n defaultBackend?: { service?: { name: string } };\n backend?: { serviceName?: string };\n rules?: { http?: { paths?: { backend?: { service?: { name: string }; serviceName?: string } }[] } }[];\n };\n}\nexport interface RelationshipNode {\n id: string;\n kind: string;\n name: string;\n role: \"upstream\" | \"service\" | \"backend\" | \"placement\";\n detail: string;\n tone: \"neutral\" | \"warning\" | \"healthy\";\n selfLink?: string;\n inferred?: boolean;\n}\nexport interface RelationshipEdge {\n from: string;\n to: string;\n label: string;\n}\nexport interface RelationshipInventory {\n pods?: RelationshipPod[];\n slices?: RelationshipSlice[];\n ingresses?: RelationshipIngress[];\n owners?: RelationshipObject[];\n gaps?: string[];\n}\nexport function relationshipReferencePath(kind: string, name: string, namespace?: string) {\n return incidentResourcePath({ kind, name, namespace });\n}\nexport function buildResourceRelationships(\n service: ServiceDiagnosticObject & { selfLink?: string },\n inventory: RelationshipInventory,\n focusedPod?: string,\n) {\n const external = service.spec?.type === \"ExternalName\";\n const diagnostics = diagnoseService(service, external ? [] : inventory.pods, external ? [] : inventory.slices);\n const nodes: RelationshipNode[] = [];\n const edges: RelationshipEdge[] = [];\n const gaps = [...(inventory.gaps ?? [])];\n let omitted = 0;\n const namespace = service.metadata.namespace;\n const add = (node: RelationshipNode) => {\n if (!nodes.some((n) => n.id === node.id)) nodes.push(node);\n return node.id;\n };\n const svc = add({\n id: \"service\",\n kind: \"Service\",\n name: service.metadata.name ?? \"Unnamed\",\n role: \"service\",\n detail: diagnostics.summary,\n tone: \"neutral\",\n selfLink: service.selfLink,\n });\n const upstream =\n inventory.ingresses?.filter(\n (ingress) =>\n ingress.metadata.namespace === namespace &&\n [\n ingress.spec?.defaultBackend?.service?.name,\n ingress.spec?.backend?.serviceName,\n ...(ingress.spec?.rules ?? []).flatMap((rule) =>\n (rule.http?.paths ?? []).map((path) => path.backend?.service?.name ?? path.backend?.serviceName),\n ),\n ].includes(service.metadata.name),\n ) ?? [];\n omitted += Math.max(0, upstream.length - 5);\n for (const ingress of upstream.slice(0, 5)) {\n const id = add({\n id: `ingress/${ingress.metadata.uid ?? ingress.metadata.name}`,\n kind: \"Ingress\",\n name: ingress.metadata.name ?? \"Unnamed\",\n role: \"upstream\",\n detail: \"Backend Service reference\",\n tone: \"neutral\",\n selfLink: ingress.selfLink,\n });\n edges.push({ from: id, to: svc, label: \"references backend\" });\n }\n const pods = diagnostics.matchingPods?.filter((pod) => !focusedPod || pod.metadata.name === focusedPod) ?? [];\n omitted += Math.max(0, pods.length - 8);\n for (const pod of pods.slice(0, 8)) {\n const ready = pod.status?.conditions?.find((c) => c.type === \"Ready\");\n const id = add({\n id: `pod/${pod.metadata.uid ?? pod.metadata.name}`,\n kind: \"Pod\",\n name: pod.metadata.name ?? \"Unnamed\",\n role: \"backend\",\n detail:\n pod.status?.phase === \"Succeeded\"\n ? \"Completed\"\n : pod.status?.phase === \"Failed\"\n ? \"Failed\"\n : pod.metadata.deletionTimestamp\n ? \"Terminating\"\n : pod.status?.phase === \"Pending\"\n ? \"Pending\"\n : ready\n ? `Ready: ${ready.status}`\n : \"Readiness unknown\",\n tone:\n pod.status?.phase === \"Succeeded\"\n ? \"healthy\"\n : pod.status?.phase === \"Failed\" ||\n pod.status?.phase === \"Pending\" ||\n pod.metadata.deletionTimestamp ||\n ready?.status === \"False\"\n ? \"warning\"\n : ready?.status === \"True\"\n ? \"healthy\"\n : \"neutral\",\n selfLink: pod.selfLink,\n });\n edges.push({ from: svc, to: id, label: \"selector matches\" });\n if (pod.spec?.nodeName) {\n const node = add({\n id: `node/${pod.spec.nodeName}`,\n kind: \"Node\",\n name: pod.spec.nodeName,\n role: \"placement\",\n detail: \"Assignment reference; node health not queried\",\n tone: \"neutral\",\n selfLink: relationshipReferencePath(\"Node\", pod.spec.nodeName),\n inferred: true,\n });\n edges.push({ from: id, to: node, label: \"assigned to\" });\n }\n let child = id;\n let refs = pod.metadata.ownerReferences;\n const visited = new Set<string>();\n for (let depth = 0; depth < 4; depth++) {\n const ref = refs?.find((r) => r.controller) ?? (refs?.length === 1 ? refs[0] : undefined);\n if (!ref || visited.has(ref.uid)) break;\n visited.add(ref.uid);\n const owner = inventory.owners?.find((o) => o.metadata.uid === ref.uid && o.metadata.namespace === namespace);\n const ownerId = add({\n id: `owner/${ref.uid}`,\n kind: ref.kind ?? \"Owner\",\n name: ref.name ?? \"Unnamed\",\n role: \"placement\",\n detail: owner ? \"Controller object observed\" : \"Owner reference only; object not observed\",\n tone: \"neutral\",\n selfLink:\n owner?.selfLink ??\n relationshipReferencePath(ref.kind ?? \"\", ref.name ?? \"\", namespace) ??\n relationshipReferencePath(ref.kind ?? \"\", ref.name ?? \"\"),\n inferred: !owner,\n });\n edges.push({ from: child, to: ownerId, label: \"owned by\" });\n child = ownerId;\n refs = owner?.metadata.ownerReferences;\n }\n }\n const slices = diagnostics.matchingSlices ?? [];\n omitted += Math.max(0, slices.length - 5);\n for (const slice of slices.slice(0, 5)) {\n const ready = slice.endpoints?.filter((e) => e.conditions?.ready !== false && !e.conditions?.terminating).length;\n const id = add({\n id: `slice/${slice.metadata.name}`,\n kind: \"EndpointSlice\",\n name: slice.metadata.name ?? \"Unnamed\",\n role: \"backend\",\n detail: `${slice.addressType} · ${ready ?? \"unknown\"} ready endpoint entries`,\n tone: ready === 0 ? \"warning\" : \"neutral\",\n selfLink: slice.selfLink,\n });\n edges.push({ from: svc, to: id, label: \"published in\" });\n }\n if (!external && !inventory.pods) gaps.push(\"Pod inventory unavailable; selector relationships are unknown\");\n if (!inventory.slices && !diagnostics.external)\n gaps.push(\"EndpointSlice inventory unavailable; published backends are unknown\");\n if (!inventory.ingresses) gaps.push(\"Ingress inventory unavailable; upstream references are unknown\");\n return {\n nodes,\n edges: [...new Map(edges.map((edge) => [JSON.stringify([edge.from, edge.to, edge.label]), edge])).values()],\n omitted,\n gaps: [...new Set(gaps)],\n diagnostics,\n podOptions: diagnostics.matchingPods?.map((p) => p.metadata.name).filter((name): name is string => !!name) ?? [],\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ //#region src/common/service-diagnostics.ts
2
+ function diagnoseService(service, pods, slices) {
3
+ const selector = Object.entries(service.spec?.selector ?? {});
4
+ const external = service.spec?.type === "ExternalName";
5
+ const matchingPods = selector.length ? pods?.filter((p) => p.metadata.namespace === service.metadata.namespace && selector.every(([k, v]) => p.metadata.labels?.[k] === v)) : void 0;
6
+ const readyPods = matchingPods?.filter((p) => !p.metadata.deletionTimestamp && p.status?.conditions?.some((c) => c.type === "Ready" && c.status === "True"));
7
+ const matchingSlices = slices?.filter((s) => s.metadata.namespace === service.metadata.namespace && s.metadata.labels?.["kubernetes.io/service-name"] === service.metadata.name);
8
+ const families = [...new Set(matchingSlices?.map((s) => s.addressType) ?? [])];
9
+ const readyEndpoints = matchingSlices?.reduce((sum, s) => sum + (s.endpoints?.filter((e) => e.conditions?.ready !== false && !e.conditions?.terminating && e.addresses.length).length ?? 0), 0);
10
+ const limitations = ["API configuration and readiness evidence only; network reachability is not tested"];
11
+ if (pods === void 0 && selector.length) limitations.push("Pod data unavailable; access may be restricted");
12
+ if (slices === void 0 && !external) limitations.push("EndpointSlice data unavailable; access may be restricted");
13
+ if (!selector.length && !external) limitations.push("Selectorless service: endpoints are managed separately; pod matching is not applicable");
14
+ if (service.spec?.publishNotReadyAddresses) limitations.push("Service publishes not-ready addresses; endpoint readiness does not establish pod readiness");
15
+ if (families.length > 1) limitations.push("Endpoint counts include separate address families and may count the same backend more than once");
16
+ for (const family of service.spec?.ipFamilies ?? []) if (matchingSlices && !families.includes(family)) limitations.push(`No EndpointSlice evidence for configured address family ${family}`);
17
+ const ports = (service.spec?.ports ?? []).map((port) => {
18
+ const target = port.targetPort ?? port.port;
19
+ const protocol = port.protocol ?? "TCP";
20
+ const unresolved = typeof target === "string" ? matchingPods?.filter((p) => ![...p.spec?.containers ?? [], ...(p.spec?.initContainers ?? []).filter((c) => c.restartPolicy === "Always")].some((c) => c.ports?.some((p) => p.name === target && (p.protocol ?? "TCP") === protocol))).length : void 0;
21
+ const slicePorts = matchingSlices?.flatMap((s) => (s.ports ?? []).filter((p) => (p.name ?? "") === (port.name ?? "") && (p.protocol ?? "TCP") === protocol).flatMap((p) => p.port == null ? [] : [p.port]));
22
+ return {
23
+ name: port.name ?? String(port.port),
24
+ target,
25
+ protocol,
26
+ unresolved,
27
+ endpointPorts: [...new Set(slicePorts ?? [])],
28
+ missingEndpointPort: matchingSlices !== void 0 && !slicePorts?.length,
29
+ numericMismatch: typeof target === "number" && !!slicePorts?.some((value) => value !== target)
30
+ };
31
+ });
32
+ const missingEndpointPorts = ports.some((port) => port.missingEndpointPort);
33
+ if (missingEndpointPorts) limitations.push("Matching EndpointSlice port and protocol evidence is missing for one or more Service ports");
34
+ return {
35
+ summary: external ? "ExternalName delegates service discovery to DNS" : !selector.length ? "Inspect manually managed EndpointSlices" : matchingPods === void 0 || matchingSlices === void 0 ? "Partial evidence; backend health cannot be established" : !matchingPods.length ? "Selector matches no pods" : !readyEndpoints ? "No ready EndpointSlice backends reported" : missingEndpointPorts ? "Partial evidence; matching EndpointSlice port and protocol are unavailable" : `${readyPods?.length ?? 0}/${matchingPods.length} selected pods ready; ${readyEndpoints} ready endpoint entries`,
36
+ external,
37
+ missingEndpointPorts,
38
+ matchingPods,
39
+ readyPods,
40
+ matchingSlices,
41
+ readyEndpoints,
42
+ families,
43
+ ports,
44
+ limitations
45
+ };
46
+ }
47
+ //#endregion
48
+ exports.diagnoseService = diagnoseService;
49
+
50
+ //# sourceMappingURL=service-diagnostics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service-diagnostics.js","names":[],"sources":["../../../../src/common/service-diagnostics.ts"],"sourcesContent":["export interface ServiceDiagnosticObject {\n metadata: { name?: string; namespace?: string; labels?: Partial<Record<string, string>> };\n spec?: {\n type?: string;\n externalName?: string;\n selector?: Partial<Record<string, string>>;\n publishNotReadyAddresses?: boolean;\n ipFamilies?: string[];\n ports?: { name?: string; port: number; targetPort?: string | number; protocol?: string }[];\n };\n}\nexport interface ServiceDiagnosticPod {\n metadata: { name?: string; namespace?: string; labels?: Partial<Record<string, string>>; deletionTimestamp?: string };\n spec?: {\n containers?: { ports?: { name?: string; containerPort: number; protocol?: string }[] }[];\n initContainers?: {\n restartPolicy?: string;\n ports?: { name?: string; containerPort: number; protocol?: string }[];\n }[];\n };\n status?: { conditions?: { type: string; status: string }[] };\n}\nexport interface ServiceDiagnosticSlice {\n metadata: { name?: string; namespace?: string; labels?: Partial<Record<string, string>> };\n addressType: string;\n endpoints?: { addresses: string[]; conditions?: { ready?: boolean; terminating?: boolean } }[] | null;\n ports?: { name?: string; port?: number | null; protocol?: string }[] | null;\n}\nexport function diagnoseService<P extends ServiceDiagnosticPod, S extends ServiceDiagnosticSlice>(\n service: ServiceDiagnosticObject,\n pods: P[] | undefined,\n slices: S[] | undefined,\n) {\n const selector = Object.entries(service.spec?.selector ?? {});\n const external = service.spec?.type === \"ExternalName\";\n const matchingPods = selector.length\n ? pods?.filter(\n (p) =>\n p.metadata.namespace === service.metadata.namespace &&\n selector.every(([k, v]) => p.metadata.labels?.[k] === v),\n )\n : undefined;\n const readyPods = matchingPods?.filter(\n (p) =>\n !p.metadata.deletionTimestamp && p.status?.conditions?.some((c) => c.type === \"Ready\" && c.status === \"True\"),\n );\n const matchingSlices = slices?.filter(\n (s) =>\n s.metadata.namespace === service.metadata.namespace &&\n s.metadata.labels?.[\"kubernetes.io/service-name\"] === service.metadata.name,\n );\n const families = [...new Set(matchingSlices?.map((s) => s.addressType) ?? [])];\n const readyEndpoints = matchingSlices?.reduce(\n (sum, s) =>\n sum +\n (s.endpoints?.filter((e) => e.conditions?.ready !== false && !e.conditions?.terminating && e.addresses.length)\n .length ?? 0),\n 0,\n );\n const limitations = [\"API configuration and readiness evidence only; network reachability is not tested\"];\n if (pods === undefined && selector.length) limitations.push(\"Pod data unavailable; access may be restricted\");\n if (slices === undefined && !external) limitations.push(\"EndpointSlice data unavailable; access may be restricted\");\n if (!selector.length && !external)\n limitations.push(\"Selectorless service: endpoints are managed separately; pod matching is not applicable\");\n if (service.spec?.publishNotReadyAddresses)\n limitations.push(\"Service publishes not-ready addresses; endpoint readiness does not establish pod readiness\");\n if (families.length > 1)\n limitations.push(\"Endpoint counts include separate address families and may count the same backend more than once\");\n for (const family of service.spec?.ipFamilies ?? []) {\n if (matchingSlices && !families.includes(family))\n limitations.push(`No EndpointSlice evidence for configured address family ${family}`);\n }\n const ports = (service.spec?.ports ?? []).map((port) => {\n const target = port.targetPort ?? port.port;\n const protocol = port.protocol ?? \"TCP\";\n const unresolved =\n typeof target === \"string\"\n ? matchingPods?.filter(\n (p) =>\n ![\n ...(p.spec?.containers ?? []),\n ...(p.spec?.initContainers ?? []).filter((c) => c.restartPolicy === \"Always\"),\n ].some((c) => c.ports?.some((p) => p.name === target && (p.protocol ?? \"TCP\") === protocol)),\n ).length\n : undefined;\n const slicePorts = matchingSlices?.flatMap((s) =>\n (s.ports ?? [])\n .filter((p) => (p.name ?? \"\") === (port.name ?? \"\") && (p.protocol ?? \"TCP\") === protocol)\n .flatMap((p) => (p.port == null ? [] : [p.port])),\n );\n return {\n name: port.name ?? String(port.port),\n target,\n protocol,\n unresolved,\n endpointPorts: [...new Set(slicePorts ?? [])],\n missingEndpointPort: matchingSlices !== undefined && !slicePorts?.length,\n numericMismatch: typeof target === \"number\" && !!slicePorts?.some((value) => value !== target),\n };\n });\n const missingEndpointPorts = ports.some((port) => port.missingEndpointPort);\n if (missingEndpointPorts)\n limitations.push(\"Matching EndpointSlice port and protocol evidence is missing for one or more Service ports\");\n const summary = external\n ? \"ExternalName delegates service discovery to DNS\"\n : !selector.length\n ? \"Inspect manually managed EndpointSlices\"\n : matchingPods === undefined || matchingSlices === undefined\n ? \"Partial evidence; backend health cannot be established\"\n : !matchingPods.length\n ? \"Selector matches no pods\"\n : !readyEndpoints\n ? \"No ready EndpointSlice backends reported\"\n : missingEndpointPorts\n ? \"Partial evidence; matching EndpointSlice port and protocol are unavailable\"\n : `${readyPods?.length ?? 0}/${matchingPods.length} selected pods ready; ${readyEndpoints} ready endpoint entries`;\n return {\n summary,\n external,\n missingEndpointPorts,\n matchingPods,\n readyPods,\n matchingSlices,\n readyEndpoints,\n families,\n ports,\n limitations,\n };\n}\n"],"mappings":"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"}
@@ -5,6 +5,7 @@ const require_monitoring_history = require("../monitoring-history.js");
5
5
  var MonitoringPreferencesStore = class extends require_extensions.Common.Store.ExtensionStore {
6
6
  enabled = false;
7
7
  diagnosticsVisible = true;
8
+ relationshipsVisible = true;
8
9
  historyEnabled = false;
9
10
  trackedHistory = [];
10
11
  constructor() {
@@ -13,6 +14,7 @@ var MonitoringPreferencesStore = class extends require_extensions.Common.Store.E
13
14
  defaults: {
14
15
  enabled: false,
15
16
  diagnosticsVisible: true,
17
+ relationshipsVisible: true,
16
18
  historyEnabled: false,
17
19
  trackedHistory: []
18
20
  }
@@ -20,6 +22,7 @@ var MonitoringPreferencesStore = class extends require_extensions.Common.Store.E
20
22
  require__global_external_mobx.makeObservable(this, {
21
23
  enabled: require__global_external_mobx.observable,
22
24
  diagnosticsVisible: require__global_external_mobx.observable,
25
+ relationshipsVisible: require__global_external_mobx.observable,
23
26
  historyEnabled: require__global_external_mobx.observable,
24
27
  trackedHistory: require__global_external_mobx.observable,
25
28
  fromStore: require__global_external_mobx.action
@@ -28,6 +31,7 @@ var MonitoringPreferencesStore = class extends require_extensions.Common.Store.E
28
31
  fromStore(data) {
29
32
  this.enabled = data?.enabled === true;
30
33
  this.diagnosticsVisible = data?.diagnosticsVisible !== false;
34
+ this.relationshipsVisible = data?.relationshipsVisible !== false;
31
35
  this.historyEnabled = data?.historyEnabled === true;
32
36
  this.trackedHistory = Array.isArray(data?.trackedHistory) ? data.trackedHistory.filter(require_monitoring_history.validHistoryIdentity).slice(0, 100).map(({ clusterId, scope, uid }) => ({
33
37
  clusterId,
@@ -39,6 +43,7 @@ var MonitoringPreferencesStore = class extends require_extensions.Common.Store.E
39
43
  return {
40
44
  enabled: this.enabled,
41
45
  diagnosticsVisible: this.diagnosticsVisible,
46
+ relationshipsVisible: this.relationshipsVisible,
42
47
  historyEnabled: this.historyEnabled,
43
48
  trackedHistory: this.trackedHistory.map(({ clusterId, scope, uid }) => ({
44
49
  clusterId,
@@ -1 +1 @@
1
- {"version":3,"file":"monitoring-preferences-store.js","names":[],"sources":["../../../../../src/common/store/monitoring-preferences-store.ts"],"sourcesContent":["import { Common } from \"@freelensapp/extensions\";\nimport { action, makeObservable, observable } from \"mobx\";\nimport { type HistoryIdentity, validHistoryIdentity } from \"../monitoring-history\";\n\nexport class MonitoringPreferencesStore extends Common.Store.ExtensionStore<{\n enabled: boolean;\n diagnosticsVisible: boolean;\n historyEnabled: boolean;\n trackedHistory: HistoryIdentity[];\n}> {\n enabled = false;\n diagnosticsVisible = true;\n historyEnabled = false;\n trackedHistory: HistoryIdentity[] = [];\n\n constructor() {\n super({\n configName: \"monitoring-preferences\",\n defaults: { enabled: false, diagnosticsVisible: true, historyEnabled: false, trackedHistory: [] },\n });\n makeObservable(this, {\n enabled: observable,\n diagnosticsVisible: observable,\n historyEnabled: observable,\n trackedHistory: observable,\n fromStore: action,\n });\n }\n\n fromStore(data: {\n enabled?: boolean;\n diagnosticsVisible?: boolean;\n historyEnabled?: boolean;\n trackedHistory?: HistoryIdentity[];\n }): void {\n this.enabled = data?.enabled === true;\n this.diagnosticsVisible = data?.diagnosticsVisible !== false;\n this.historyEnabled = data?.historyEnabled === true;\n this.trackedHistory = Array.isArray(data?.trackedHistory)\n ? data.trackedHistory\n .filter(validHistoryIdentity)\n .slice(0, 100)\n .map(({ clusterId, scope, uid }) => ({ clusterId, scope, uid }))\n : [];\n }\n\n toJSON(): {\n enabled: boolean;\n diagnosticsVisible: boolean;\n historyEnabled: boolean;\n trackedHistory: HistoryIdentity[];\n } {\n return {\n enabled: this.enabled,\n diagnosticsVisible: this.diagnosticsVisible,\n historyEnabled: this.historyEnabled,\n trackedHistory: this.trackedHistory.map(({ clusterId, scope, uid }) => ({ clusterId, scope, uid })),\n };\n }\n}\n"],"mappings":";;;;AAIA,IAAa,6BAAb,cAAgD,mBAAA,OAAO,MAAM,eAK1D;CACD,UAAU;CACV,qBAAqB;CACrB,iBAAiB;CACjB,iBAAoC,CAAC;CAErC,cAAc;EACZ,MAAM;GACJ,YAAY;GACZ,UAAU;IAAE,SAAS;IAAO,oBAAoB;IAAM,gBAAgB;IAAO,gBAAgB,CAAC;GAAE;EAClG,CAAC;EACD,8BAAA,eAAe,MAAM;GACnB,SAAS,8BAAA;GACT,oBAAoB,8BAAA;GACpB,gBAAgB,8BAAA;GAChB,gBAAgB,8BAAA;GAChB,WAAW,8BAAA;EACb,CAAC;CACH;CAEA,UAAU,MAKD;EACP,KAAK,UAAU,MAAM,YAAY;EACjC,KAAK,qBAAqB,MAAM,uBAAuB;EACvD,KAAK,iBAAiB,MAAM,mBAAmB;EAC/C,KAAK,iBAAiB,MAAM,QAAQ,MAAM,cAAc,IACpD,KAAK,eACF,OAAO,2BAAA,oBAAoB,CAAC,CAC5B,MAAM,GAAG,GAAG,CAAC,CACb,KAAK,EAAE,WAAW,OAAO,WAAW;GAAE;GAAW;GAAO;EAAI,EAAE,IACjE,CAAC;CACP;CAEA,SAKE;EACA,OAAO;GACL,SAAS,KAAK;GACd,oBAAoB,KAAK;GACzB,gBAAgB,KAAK;GACrB,gBAAgB,KAAK,eAAe,KAAK,EAAE,WAAW,OAAO,WAAW;IAAE;IAAW;IAAO;GAAI,EAAE;EACpG;CACF;AACF"}
1
+ {"version":3,"file":"monitoring-preferences-store.js","names":[],"sources":["../../../../../src/common/store/monitoring-preferences-store.ts"],"sourcesContent":["import { Common } from \"@freelensapp/extensions\";\nimport { action, makeObservable, observable } from \"mobx\";\nimport { type HistoryIdentity, validHistoryIdentity } from \"../monitoring-history\";\n\nexport class MonitoringPreferencesStore extends Common.Store.ExtensionStore<{\n enabled: boolean;\n diagnosticsVisible: boolean;\n relationshipsVisible: boolean;\n historyEnabled: boolean;\n trackedHistory: HistoryIdentity[];\n}> {\n enabled = false;\n diagnosticsVisible = true;\n relationshipsVisible = true;\n historyEnabled = false;\n trackedHistory: HistoryIdentity[] = [];\n\n constructor() {\n super({\n configName: \"monitoring-preferences\",\n defaults: {\n enabled: false,\n diagnosticsVisible: true,\n relationshipsVisible: true,\n historyEnabled: false,\n trackedHistory: [],\n },\n });\n makeObservable(this, {\n enabled: observable,\n diagnosticsVisible: observable,\n relationshipsVisible: observable,\n historyEnabled: observable,\n trackedHistory: observable,\n fromStore: action,\n });\n }\n\n fromStore(data: {\n enabled?: boolean;\n diagnosticsVisible?: boolean;\n relationshipsVisible?: boolean;\n historyEnabled?: boolean;\n trackedHistory?: HistoryIdentity[];\n }): void {\n this.enabled = data?.enabled === true;\n this.diagnosticsVisible = data?.diagnosticsVisible !== false;\n this.relationshipsVisible = data?.relationshipsVisible !== false;\n this.historyEnabled = data?.historyEnabled === true;\n this.trackedHistory = Array.isArray(data?.trackedHistory)\n ? data.trackedHistory\n .filter(validHistoryIdentity)\n .slice(0, 100)\n .map(({ clusterId, scope, uid }) => ({ clusterId, scope, uid }))\n : [];\n }\n\n toJSON(): {\n enabled: boolean;\n diagnosticsVisible: boolean;\n relationshipsVisible: boolean;\n historyEnabled: boolean;\n trackedHistory: HistoryIdentity[];\n } {\n return {\n enabled: this.enabled,\n diagnosticsVisible: this.diagnosticsVisible,\n relationshipsVisible: this.relationshipsVisible,\n historyEnabled: this.historyEnabled,\n trackedHistory: this.trackedHistory.map(({ clusterId, scope, uid }) => ({ clusterId, scope, uid })),\n };\n }\n}\n"],"mappings":";;;;AAIA,IAAa,6BAAb,cAAgD,mBAAA,OAAO,MAAM,eAM1D;CACD,UAAU;CACV,qBAAqB;CACrB,uBAAuB;CACvB,iBAAiB;CACjB,iBAAoC,CAAC;CAErC,cAAc;EACZ,MAAM;GACJ,YAAY;GACZ,UAAU;IACR,SAAS;IACT,oBAAoB;IACpB,sBAAsB;IACtB,gBAAgB;IAChB,gBAAgB,CAAC;GACnB;EACF,CAAC;EACD,8BAAA,eAAe,MAAM;GACnB,SAAS,8BAAA;GACT,oBAAoB,8BAAA;GACpB,sBAAsB,8BAAA;GACtB,gBAAgB,8BAAA;GAChB,gBAAgB,8BAAA;GAChB,WAAW,8BAAA;EACb,CAAC;CACH;CAEA,UAAU,MAMD;EACP,KAAK,UAAU,MAAM,YAAY;EACjC,KAAK,qBAAqB,MAAM,uBAAuB;EACvD,KAAK,uBAAuB,MAAM,yBAAyB;EAC3D,KAAK,iBAAiB,MAAM,mBAAmB;EAC/C,KAAK,iBAAiB,MAAM,QAAQ,MAAM,cAAc,IACpD,KAAK,eACF,OAAO,2BAAA,oBAAoB,CAAC,CAC5B,MAAM,GAAG,GAAG,CAAC,CACb,KAAK,EAAE,WAAW,OAAO,WAAW;GAAE;GAAW;GAAO;EAAI,EAAE,IACjE,CAAC;CACP;CAEA,SAME;EACA,OAAO;GACL,SAAS,KAAK;GACd,oBAAoB,KAAK;GACzB,sBAAsB,KAAK;GAC3B,gBAAgB,KAAK;GACrB,gBAAgB,KAAK,eAAe,KAAK,EAAE,WAAW,OAAO,WAAW;IAAE;IAAW;IAAO;GAAI,EAAE;EACpG;CACF;AACF"}