@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
@@ -3,10 +3,15 @@ const require__global_external_mobx = require("./_virtual/_global-external_mobx.
3
3
  const require__global_external_react = require("./_virtual/_global-external_react.js");
4
4
  const require_jsx_runtime = require("./_virtual/_global-external_react/jsx-runtime.js");
5
5
  const require_ai_icon = require("./ai-icon.js");
6
+ const require_ai_diagnostic_handoff = require("./ai-diagnostic-handoff.js");
6
7
  const require_date_time = require("./src/common/date-time.js");
8
+ const require_monitoring_collector = require("./monitoring-collector.js");
7
9
  const require_resource_diagnostics = require("./src/common/resource-diagnostics.js");
10
+ const require_timeline_groups = require("./src/common/timeline-groups.js");
11
+ const require_diagnostic_explain_button = require("./diagnostic-explain-button.js");
8
12
  const require_diagnostics_css = require("./diagnostics.css.js");
9
- const require_monitoring_collector = require("./monitoring-collector.js");
13
+ const require_diagnostics_state = require("./diagnostics-state.js");
14
+ const require_incident_copy_button = require("./incident-copy-button.js");
10
15
  //#region src/renderer/diagnostics.tsx
11
16
  var viewDescriptions = {
12
17
  Consumers: ["What is using cluster resources?", "Compare workload usage and requests, then expand a workload to inspect its pods."],
@@ -86,6 +91,7 @@ function consumerStatus(pods) {
86
91
  "InvalidImageName"
87
92
  ]);
88
93
  for (const pod of pods) {
94
+ if (pod.status?.phase === "Succeeded") continue;
89
95
  const status = pod.status;
90
96
  const reason = [...status?.containerStatuses ?? [], ...status?.initContainerStatuses ?? []].map((status) => status.state?.waiting?.reason).find((value) => value && failureReasons.has(value));
91
97
  if (pod.status?.phase === "Failed" || reason) return {
@@ -100,14 +106,18 @@ function consumerStatus(pods) {
100
106
  label: `${pendingCount} Pending`,
101
107
  icon: "hourglass_empty"
102
108
  };
103
- if (pods.some((pod) => pod.status?.conditions?.some((condition) => condition.type === "Ready" && condition.status === "False"))) return {
109
+ if (pods.some((pod) => pod.status?.phase !== "Succeeded" && pod.status?.conditions?.some((condition) => condition.type === "Ready" && condition.status === "False"))) return {
104
110
  tone: "warning",
105
111
  label: "Not ready",
106
112
  icon: "warning_amber"
107
113
  };
108
- if (pods.length && pods.every((pod) => pod.status?.phase === "Running" && pod.status.conditions?.some((condition) => condition.type === "Ready" && condition.status === "True"))) return {
114
+ if (pods.length && pods.every((pod) => pod.status?.phase === "Succeeded" || pod.status?.phase === "Running" && pod.status.conditions?.some((condition) => condition.type === "Ready" && condition.status === "True"))) return pods.every((pod) => pod.status?.phase === "Succeeded") ? {
109
115
  tone: "healthy",
110
- label: "Ready",
116
+ label: "Completed",
117
+ icon: "check_circle"
118
+ } : {
119
+ tone: "healthy",
120
+ label: pods.some((pod) => pod.status?.phase === "Succeeded") ? "Ready / Completed" : "Ready",
111
121
  icon: "check_circle_outline"
112
122
  };
113
123
  return {
@@ -209,20 +219,68 @@ function projection(data) {
209
219
  return value;
210
220
  }
211
221
  function DiagnosticsPage() {
212
- const [tab, setTab] = require__global_external_react.useState("Consumers");
222
+ const initial = require__global_external_react.useRef(require_diagnostics_state.readDiagnosticsState(activeId() ?? "")).current;
223
+ const scrollContainer = require__global_external_react.useRef(null);
224
+ const stateCluster = require__global_external_react.useRef(activeId() ?? "");
225
+ const [tab, setTab] = require__global_external_react.useState(initial.tab);
213
226
  const [snapshot, setSnapshot] = require__global_external_react.useState();
214
227
  const [error, setError] = require__global_external_react.useState("");
215
- const [filter, setFilter] = require__global_external_react.useState("");
216
- const [needsAttention, setNeedsAttention] = require__global_external_react.useState(true);
217
- const [groupMode, setGroupMode] = require__global_external_react.useState("workload");
218
- const [namespacesOpen, setNamespacesOpen] = require__global_external_react.useState([]);
228
+ const [filter, setFilter] = require__global_external_react.useState(initial.filter);
229
+ const [namespaceScope, setNamespaceScope] = require__global_external_react.useState(initial.namespaceScope);
230
+ const pendingScroll = require__global_external_react.useRef({
231
+ clusterId: stateCluster.current,
232
+ top: initial.scrollTop
233
+ });
234
+ const [needsAttention, setNeedsAttention] = require__global_external_react.useState(initial.needsAttention);
235
+ const [groupMode, setGroupMode] = require__global_external_react.useState(initial.groupMode);
236
+ const [namespacesOpen, setNamespacesOpen] = require__global_external_react.useState(initial.namespacesOpen);
219
237
  const known = require__global_external_react.useRef(/* @__PURE__ */ new Map());
220
- const [sort, setSort] = require__global_external_react.useState("cpu");
221
- const [expanded, setExpanded] = require__global_external_react.useState([]);
222
- const [selected, setSelected] = require__global_external_react.useState("");
223
- const [pendingUid, setPendingUid] = require__global_external_react.useState("");
224
- const [reason, setReason] = require__global_external_react.useState("");
225
- const [warningOnly, setWarningOnly] = require__global_external_react.useState(true);
238
+ const [sort, setSort] = require__global_external_react.useState(initial.sort);
239
+ const [expanded, setExpanded] = require__global_external_react.useState(initial.expanded);
240
+ const [selected, setSelected] = require__global_external_react.useState(initial.selected);
241
+ const [pendingUid, setPendingUid] = require__global_external_react.useState(initial.pendingUid);
242
+ const [reason, setReason] = require__global_external_react.useState(initial.reason);
243
+ const [warningOnly, setWarningOnly] = require__global_external_react.useState(initial.warningOnly);
244
+ const [groupedTimeline, setGroupedTimeline] = require__global_external_react.useState(initial.groupedTimeline);
245
+ const [openTimeline, setOpenTimeline] = require__global_external_react.useState(initial.openTimeline);
246
+ const viewState = require__global_external_react.useRef(initial);
247
+ viewState.current = {
248
+ tab,
249
+ filter,
250
+ namespaceScope,
251
+ needsAttention,
252
+ groupMode,
253
+ namespacesOpen,
254
+ sort,
255
+ expanded,
256
+ selected,
257
+ pendingUid,
258
+ reason,
259
+ warningOnly,
260
+ groupedTimeline,
261
+ openTimeline,
262
+ scrollTop: viewState.current.scrollTop
263
+ };
264
+ require__global_external_react.useEffect(() => {
265
+ return () => require_diagnostics_state.saveDiagnosticsState(stateCluster.current, viewState.current);
266
+ }, [initial]);
267
+ require__global_external_react.useEffect(() => {
268
+ const focusNamespace = (event) => {
269
+ const detail = event.detail;
270
+ if (!detail || detail.clusterId !== stateCluster.current || detail.clusterId !== activeId() || typeof detail.namespace !== "string" || !detail.namespace) return;
271
+ setTab("Consumers");
272
+ setNamespaceScope(detail.namespace);
273
+ setFilter("");
274
+ setNeedsAttention(false);
275
+ setGroupMode("namespace");
276
+ setNamespacesOpen([detail.namespace]);
277
+ viewState.current.scrollTop = 0;
278
+ pendingScroll.current = void 0;
279
+ if (scrollContainer.current) scrollContainer.current.scrollTop = 0;
280
+ };
281
+ window.addEventListener(require_diagnostics_state.DIAGNOSTICS_NAMESPACE_EVENT, focusNamespace);
282
+ return () => window.removeEventListener(require_diagnostics_state.DIAGNOSTICS_NAMESPACE_EVENT, focusNamespace);
283
+ }, []);
226
284
  const [observed, setObserved] = require__global_external_react.useState([]);
227
285
  const previous = require__global_external_react.useRef();
228
286
  require__global_external_react.useEffect(() => {
@@ -236,9 +294,28 @@ function DiagnosticsPage() {
236
294
  known.current.clear();
237
295
  setSnapshot(void 0);
238
296
  setObserved([]);
239
- setSelected("");
240
- setPendingUid("");
241
- setExpanded([]);
297
+ require_diagnostics_state.saveDiagnosticsState(stateCluster.current, viewState.current);
298
+ stateCluster.current = activeId() ?? "";
299
+ const restored = require_diagnostics_state.readDiagnosticsState(stateCluster.current);
300
+ setTab(restored.tab);
301
+ setFilter(restored.filter);
302
+ setNamespaceScope(restored.namespaceScope);
303
+ setNeedsAttention(restored.needsAttention);
304
+ setGroupMode(restored.groupMode);
305
+ setNamespacesOpen(restored.namespacesOpen);
306
+ setSort(restored.sort);
307
+ setExpanded(restored.expanded);
308
+ setSelected(restored.selected);
309
+ setPendingUid(restored.pendingUid);
310
+ setReason(restored.reason);
311
+ setWarningOnly(restored.warningOnly);
312
+ setGroupedTimeline(restored.groupedTimeline);
313
+ setOpenTimeline(restored.openTimeline);
314
+ viewState.current = restored;
315
+ pendingScroll.current = {
316
+ clusterId: stateCluster.current,
317
+ top: restored.scrollTop
318
+ };
242
319
  setError("");
243
320
  };
244
321
  const load = async () => {
@@ -301,20 +378,30 @@ function DiagnosticsPage() {
301
378
  };
302
379
  }, []);
303
380
  const data = snapshot?.clusterId === activeId() ? snapshot : void 0;
381
+ require__global_external_react.useLayoutEffect(() => {
382
+ const pending = pendingScroll.current;
383
+ if (!data || !pending || pending.clusterId !== data.clusterId || !scrollContainer.current) return;
384
+ scrollContainer.current.scrollTop = pending.top;
385
+ const frame = requestAnimationFrame(() => {
386
+ if (pendingScroll.current !== pending || !scrollContainer.current) return;
387
+ scrollContainer.current.scrollTop = pending.top;
388
+ viewState.current.scrollTop = scrollContainer.current.scrollTop;
389
+ pendingScroll.current = void 0;
390
+ });
391
+ return () => cancelAnimationFrame(frame);
392
+ }, [data]);
304
393
  const projected = require__global_external_react.useMemo(() => data ? projection(data) : void 0, [data]);
305
394
  const groups = projected?.groups ?? [];
306
- const filtered = require__global_external_react.useMemo(() => groups.filter((group) => `${group.namespace} ${group.name} ${group.kind}`.toLowerCase().includes(filter.toLowerCase())).filter((group) => !needsAttention || ["error", "warning"].includes(consumerStatus(group.pods).tone)).sort((a, b) => sort === "name" ? a.namespace.localeCompare(b.namespace) || a.name.localeCompare(b.name) || a.kind.localeCompare(b.kind) || a.identity.localeCompare(b.identity) : sort === "requests" || sort === "memoryRequests" ? b[sort === "requests" ? "cpu" : "memory"].requests - a[sort === "requests" ? "cpu" : "memory"].requests : (b[sort === "memory" ? "memory" : "cpu"].usage ?? -1) - (a[sort === "memory" ? "memory" : "cpu"].usage ?? -1)), [
395
+ const filtered = require__global_external_react.useMemo(() => groups.filter((group) => !namespaceScope || group.namespace === namespaceScope).filter((group) => `${group.namespace} ${group.name} ${group.kind}`.toLowerCase().includes(filter.toLowerCase())).filter((group) => !needsAttention || ["error", "warning"].includes(consumerStatus(group.pods).tone)).sort((a, b) => sort === "name" ? a.namespace.localeCompare(b.namespace) || a.name.localeCompare(b.name) || a.kind.localeCompare(b.kind) || a.identity.localeCompare(b.identity) : sort === "requests" || sort === "memoryRequests" ? b[sort === "requests" ? "cpu" : "memory"].requests - a[sort === "requests" ? "cpu" : "memory"].requests : (b[sort === "memory" ? "memory" : "cpu"].usage ?? -1) - (a[sort === "memory" ? "memory" : "cpu"].usage ?? -1)), [
307
396
  groups,
308
397
  filter,
398
+ namespaceScope,
309
399
  sort,
310
400
  needsAttention
311
401
  ]);
312
402
  const warningUids = new Set((data?.events ?? []).filter((event) => event.type === "Warning").map((event) => event.involvedObject?.uid ?? event.regarding?.uid).filter((uid) => !!uid));
313
403
  const applications = [...known.current.values()].filter((item) => !warningOnly || [...item.uids].some((uid) => warningUids.has(uid))).map((item) => item.group);
314
404
  const selectionAvailable = applications.some((group) => groupKey(group) === selected);
315
- require__global_external_react.useEffect(() => {
316
- if (selected && !selectionAvailable) setSelected("");
317
- }, [selected, selectionAvailable]);
318
405
  const application = applications.find((group) => groupKey(group) === selected);
319
406
  const uids = new Set(known.current.get(selected)?.uids ?? []);
320
407
  const normalizedEvents = require_resource_diagnostics.normalizeEvents(data?.events ?? []);
@@ -324,11 +411,26 @@ function DiagnosticsPage() {
324
411
  return [entry.id, event?.involvedObject ?? event?.regarding];
325
412
  }));
326
413
  const timeline = [...normalizedEvents, ...observed].filter((entry) => (!application || !!entry.objectUid && uids.has(entry.objectUid)) && (!reason || `${entry.reason} ${entry.message}`.toLowerCase().includes(reason.toLowerCase())) && (!warningOnly || warningIds.has(entry.id))).sort((a, b) => (Date.parse(b.time ?? "") || 0) - (Date.parse(a.time ?? "") || 0));
414
+ const timelineResource = (entry) => {
415
+ const object = entry.objectUid ? projected?.objects.get(entry.objectUid) : void 0;
416
+ const ref = eventReferences.get(entry.id);
417
+ return `${object?.metadata.namespace ?? ref?.namespace ?? "Unknown namespace"} / ${object?.kind ?? ref?.kind ?? "Resource"} / ${object?.metadata.name ?? ref?.name ?? entry.objectUid ?? entry.id}`;
418
+ };
419
+ const timelineGroups = require_timeline_groups.groupTimelineEntries(timeline, (entry) => {
420
+ if (!warningIds.has(entry.id)) return entry.id;
421
+ const owner = [...known.current.entries()].find(([, item]) => !!entry.objectUid && item.uids.has(entry.objectUid));
422
+ return owner ? `${owner[1].group.namespace}/${owner[1].group.kind}/${owner[1].group.name}` : timelineResource(entry);
423
+ }, timelineResource);
424
+ const displayedTimeline = groupedTimeline ? timelineGroups.map((group) => group.latest) : timeline;
327
425
  const pending = (data?.pods ?? []).filter((pod) => pod.status?.phase === "Pending");
328
426
  const PendingCard = pending.length && data?.pods ? "button" : "div";
329
427
  const selectedPod = pending.find((pod) => pod.metadata.uid === pendingUid);
330
428
  const diagnosis = selectedPod ? require_resource_diagnostics.diagnosePendingPod(selectedPod, data?.nodes ?? [], data?.events ?? [], data?.pvcs) : void 0;
331
429
  return /* @__PURE__ */ require_jsx_runtime.jsxs("div", {
430
+ ref: scrollContainer,
431
+ onScroll: (event) => {
432
+ if (!pendingScroll.current) viewState.current.scrollTop = event.currentTarget.scrollTop;
433
+ },
332
434
  className: "power-diagnostics",
333
435
  style: {
334
436
  padding: 24,
@@ -428,6 +530,22 @@ function DiagnosticsPage() {
428
530
  children: "Loading resource diagnostics…"
429
531
  }),
430
532
  tab === "Consumers" && data && /* @__PURE__ */ require_jsx_runtime.jsxs(require_jsx_runtime.Fragment, { children: [
533
+ namespaceScope && /* @__PURE__ */ require_jsx_runtime.jsxs("div", {
534
+ className: "namespace-scope",
535
+ role: "status",
536
+ children: [/* @__PURE__ */ require_jsx_runtime.jsxs("span", {
537
+ className: "namespace-scope-label",
538
+ children: ["Namespace: ", namespaceScope]
539
+ }), /* @__PURE__ */ require_jsx_runtime.jsxs("button", {
540
+ type: "button",
541
+ "aria-label": "Clear namespace scope",
542
+ onClick: () => setNamespaceScope(void 0),
543
+ children: [/* @__PURE__ */ require_jsx_runtime.jsx(require_ai_icon.AiIcon, {
544
+ material: "close",
545
+ size: 13
546
+ }), "Show all namespaces"]
547
+ })]
548
+ }),
431
549
  /* @__PURE__ */ require_jsx_runtime.jsxs("div", {
432
550
  className: "toolbar consumers-toolbar",
433
551
  children: [
@@ -530,6 +648,7 @@ function DiagnosticsPage() {
530
648
  /* @__PURE__ */ require_jsx_runtime.jsx("tbody", { children: (groupMode === "namespace" ? [...new Set(filtered.map((group) => group.namespace))] : [""]).map((namespace) => /* @__PURE__ */ require_jsx_runtime.jsx(NamespaceRows, {
531
649
  namespace,
532
650
  groups: namespace ? filtered.filter((group) => group.namespace === namespace) : filtered,
651
+ snapshotTime: data.time,
533
652
  clusterId: data?.clusterId ?? "",
534
653
  usage: projected?.usage ?? [],
535
654
  open: !namespace || namespacesOpen.includes(namespace),
@@ -612,6 +731,14 @@ function DiagnosticsPage() {
612
731
  className: "diagnostics-explainer",
613
732
  children: [/* @__PURE__ */ require_jsx_runtime.jsx("summary", { children: "Timeline coverage and timing" }), /* @__PURE__ */ require_jsx_runtime.jsx("p", { children: "Kubernetes events have limited retention. Observed changes begin with this open view, retain at most 300 entries, and have unknown transition times between snapshots. Related identities observed in this view are retained for at most 100 applications and 500 identities per application; older identity coverage can be incomplete." })]
614
733
  }),
734
+ /* @__PURE__ */ require_jsx_runtime.jsx("div", {
735
+ className: "warning-filter",
736
+ children: /* @__PURE__ */ require_jsx_runtime.jsx(require_extensions.Renderer.Component.Checkbox, {
737
+ label: "Group recurring warnings",
738
+ value: groupedTimeline,
739
+ onChange: setGroupedTimeline
740
+ })
741
+ }),
615
742
  /* @__PURE__ */ require_jsx_runtime.jsx("div", {
616
743
  className: "timeline-legend",
617
744
  "aria-label": "Event types",
@@ -630,7 +757,8 @@ function DiagnosticsPage() {
630
757
  /* @__PURE__ */ require_jsx_runtime.jsx("ol", {
631
758
  className: "diagnostics-timeline",
632
759
  "aria-label": "Workload event timeline",
633
- children: timeline.map((entry) => {
760
+ children: displayedTimeline.map((entry) => {
761
+ const recurrence = groupedTimeline ? timelineGroups.find((group) => group.latest === entry) : void 0;
634
762
  const kind = eventCategory(entry.reason);
635
763
  const category = warningIds.has(entry.id) ? "warning" : kind === "event" && entry.certainty === "observed" ? "observed" : kind;
636
764
  const label = category === "warning" ? "Warning" : category === "rollout" ? "Rollout" : category === "restart" ? "Restart" : category === "readiness" ? "Readiness" : entry.certainty === "observed" ? "Observed change" : "Kubernetes event";
@@ -649,41 +777,120 @@ function DiagnosticsPage() {
649
777
  }), /* @__PURE__ */ require_jsx_runtime.jsxs("div", {
650
778
  className: "timeline-content",
651
779
  children: [
652
- object && data && /* @__PURE__ */ require_jsx_runtime.jsx("button", {
653
- type: "button",
654
- className: "timeline-resource-action",
655
- "aria-label": "Open resource",
656
- title: `Open ${object.kind ?? "Pod"} ${object.metadata.namespace} / ${object.metadata.name}`,
657
- onClick: (event) => openResource(object, data.clusterId, event),
658
- children: /* @__PURE__ */ require_jsx_runtime.jsx(require_ai_icon.AiIcon, { material: "open_in_new" })
659
- }),
660
780
  /* @__PURE__ */ require_jsx_runtime.jsxs("div", {
661
- className: "timeline-card-heading",
662
- children: [
663
- " ",
664
- /* @__PURE__ */ require_jsx_runtime.jsx("span", {
665
- className: `timeline-type ${category}`,
666
- children: label
667
- }),
668
- /* @__PURE__ */ require_jsx_runtime.jsx("h4", { children: entry.reason }),
669
- /* @__PURE__ */ require_jsx_runtime.jsxs("small", {
670
- className: "timeline-resource-identity",
671
- children: [/* @__PURE__ */ require_jsx_runtime.jsx(require_ai_icon.AiIcon, {
672
- material: referenceKind === "Pod" ? "view_in_ar" : "account_tree",
673
- size: 12
674
- }), metadata?.name ? `${metadata.namespace ?? "Namespace unknown"} / ${referenceKind ?? "Resource"} / ${metadata.name}` : "Resource identity unavailable"]
675
- })
676
- ]
781
+ className: "timeline-card-top",
782
+ children: [/* @__PURE__ */ require_jsx_runtime.jsxs("div", {
783
+ className: "timeline-card-heading",
784
+ children: [
785
+ " ",
786
+ /* @__PURE__ */ require_jsx_runtime.jsx("span", {
787
+ className: `timeline-type ${category}`,
788
+ children: label
789
+ }),
790
+ /* @__PURE__ */ require_jsx_runtime.jsx("h4", { children: entry.reason }),
791
+ /* @__PURE__ */ require_jsx_runtime.jsxs("small", {
792
+ className: "timeline-resource-identity",
793
+ children: [/* @__PURE__ */ require_jsx_runtime.jsx(require_ai_icon.AiIcon, {
794
+ material: referenceKind === "Pod" ? "view_in_ar" : "account_tree",
795
+ size: 12
796
+ }), metadata?.name ? `${metadata.namespace ?? "Namespace unknown"} / ${referenceKind ?? "Resource"} / ${metadata.name}` : "Resource identity unavailable"]
797
+ })
798
+ ]
799
+ }), /* @__PURE__ */ require_jsx_runtime.jsxs("div", {
800
+ className: "timeline-card-actions",
801
+ role: "group",
802
+ "aria-label": "Event actions",
803
+ children: [
804
+ object && data && /* @__PURE__ */ require_jsx_runtime.jsx("button", {
805
+ type: "button",
806
+ className: "timeline-resource-action",
807
+ "aria-label": "Open resource",
808
+ title: `Open ${object.kind ?? "Pod"} ${object.metadata.namespace} / ${object.metadata.name}`,
809
+ onClick: (event) => openResource(object, data.clusterId, event),
810
+ children: /* @__PURE__ */ require_jsx_runtime.jsx(require_ai_icon.AiIcon, { material: "open_in_new" })
811
+ }),
812
+ /* @__PURE__ */ require_jsx_runtime.jsx(require_incident_copy_button.IncidentCopyButton, {
813
+ clusterId: data.clusterId,
814
+ incident: {
815
+ problem: entry.reason,
816
+ facts: [
817
+ entry.message,
818
+ `Reported count ${recurrence?.count ?? entry.count}`,
819
+ ...(recurrence?.affected ?? []).length > 1 ? [`Affected resources: ${recurrence?.affected.join("; ")}`] : []
820
+ ],
821
+ time: entry.time ?? data.time,
822
+ resources: (recurrence?.entries ?? [entry]).slice(0, 12).flatMap((original) => {
823
+ const originalObject = original.objectUid ? projected?.objects.get(original.objectUid) : void 0;
824
+ const originalReference = eventReferences.get(original.id);
825
+ const originalMetadata = originalObject?.metadata ?? originalReference;
826
+ const originalKind = originalObject?.kind ?? originalReference?.kind;
827
+ return originalMetadata?.name && originalKind ? [{
828
+ kind: originalKind,
829
+ namespace: originalMetadata.namespace,
830
+ name: originalMetadata.name
831
+ }] : [];
832
+ })
833
+ }
834
+ }),
835
+ /* @__PURE__ */ require_jsx_runtime.jsxs(require_diagnostic_explain_button.DiagnosticExplainButton, {
836
+ type: "button",
837
+ className: "explain-finding",
838
+ "aria-label": "Explain with AI",
839
+ title: "Explain with AI",
840
+ onClick: () => require_ai_diagnostic_handoff.explainDiagnosticFinding({
841
+ clusterId: data.clusterId,
842
+ title: entry.reason,
843
+ summary: `${timelineResource(entry)}: ${entry.message}. Reported count ${recurrence?.count ?? entry.count}`,
844
+ resource: metadata?.name && referenceKind ? {
845
+ kind: referenceKind,
846
+ namespace: metadata.namespace,
847
+ name: metadata.name
848
+ } : void 0
849
+ }),
850
+ children: [/* @__PURE__ */ require_jsx_runtime.jsx(require_ai_icon.AiIcon, {
851
+ material: "auto_awesome",
852
+ size: 13
853
+ }), /* @__PURE__ */ require_jsx_runtime.jsx("span", {
854
+ className: "timeline-action-label",
855
+ children: "Explain with AI"
856
+ })]
857
+ })
858
+ ]
859
+ })]
677
860
  }),
678
861
  /* @__PURE__ */ require_jsx_runtime.jsx("p", { children: entry.message }),
679
862
  /* @__PURE__ */ require_jsx_runtime.jsxs("small", { children: [
680
863
  entry.certainty === "reported" ? "Reported" : entry.certainty === "observed" ? "Observed" : "Unknown",
681
864
  " ",
682
865
  "· Count ",
683
- entry.count,
866
+ recurrence?.count ?? entry.count,
684
867
  entry.firstTime ? ` · First ${require_date_time.formatDateTime(entry.firstTime)}` : ""
685
868
  ] }),
686
- entry.gap && /* @__PURE__ */ require_jsx_runtime.jsx("small", { children: entry.gap })
869
+ entry.gap && /* @__PURE__ */ require_jsx_runtime.jsx("small", { children: entry.gap }),
870
+ recurrence && recurrence.entries.length > 1 && /* @__PURE__ */ require_jsx_runtime.jsxs("details", {
871
+ open: openTimeline.includes(recurrence.key),
872
+ onToggle: (event) => {
873
+ const open = event.currentTarget.open;
874
+ setOpenTimeline((items) => open ? [.../* @__PURE__ */ new Set([...items, recurrence.key])] : items.filter((item) => item !== recurrence.key));
875
+ },
876
+ children: [
877
+ /* @__PURE__ */ require_jsx_runtime.jsxs("summary", { children: [
878
+ recurrence.affected.length,
879
+ " affected resources · ",
880
+ recurrence.entries.length,
881
+ " original entries"
882
+ ] }),
883
+ /* @__PURE__ */ require_jsx_runtime.jsx("small", { children: recurrence.affected.join(" · ") }),
884
+ /* @__PURE__ */ require_jsx_runtime.jsx("ul", { children: recurrence.entries.map((original, index) => /* @__PURE__ */ require_jsx_runtime.jsxs("li", { children: [/* @__PURE__ */ require_jsx_runtime.jsxs("small", { children: [
885
+ original.time ? require_date_time.formatDateTime(original.time) : "Time unknown",
886
+ " ·",
887
+ " ",
888
+ timelineResource(original),
889
+ " · Count ",
890
+ original.count
891
+ ] }), original.message] }, `${original.id}/${index}`)) })
892
+ ]
893
+ })
687
894
  ]
688
895
  })]
689
896
  }, entry.id);
@@ -780,6 +987,41 @@ function DiagnosticsPage() {
780
987
  /* @__PURE__ */ require_jsx_runtime.jsx("p", { children: selectedPod.status?.conditions?.map((condition) => `${condition.type}: ${condition.status}${condition.message ? ` (${condition.message})` : ""}`).join(" · ") })
781
988
  ]
782
989
  }),
990
+ /* @__PURE__ */ require_jsx_runtime.jsx(require_incident_copy_button.IncidentCopyButton, {
991
+ clusterId: data.clusterId,
992
+ incident: {
993
+ problem: "Pending pod",
994
+ facts: [
995
+ diagnosis.state,
996
+ `CPU request ${podRequest(selectedPod, "cpu")}; memory request ${podRequest(selectedPod, "memory")}`,
997
+ ...diagnosis.evidence.slice(0, 8).map((entry) => entry.message)
998
+ ],
999
+ time: data.time,
1000
+ resources: [{
1001
+ kind: "Pod",
1002
+ namespace: selectedPod.metadata.namespace,
1003
+ name: selectedPod.metadata.name
1004
+ }]
1005
+ }
1006
+ }),
1007
+ /* @__PURE__ */ require_jsx_runtime.jsxs(require_diagnostic_explain_button.DiagnosticExplainButton, {
1008
+ type: "button",
1009
+ className: "explain-finding",
1010
+ onClick: () => require_ai_diagnostic_handoff.explainDiagnosticFinding({
1011
+ clusterId: data.clusterId,
1012
+ title: "Pending pod",
1013
+ summary: `${diagnosis.state}. CPU request ${podRequest(selectedPod, "cpu")}, memory request ${podRequest(selectedPod, "memory")}. ${diagnosis.evidence.slice(0, 8).map((entry) => entry.message).join("; ")}`,
1014
+ resource: {
1015
+ kind: "Pod",
1016
+ namespace: selectedPod.metadata.namespace,
1017
+ name: selectedPod.metadata.name
1018
+ }
1019
+ }),
1020
+ children: [/* @__PURE__ */ require_jsx_runtime.jsx(require_ai_icon.AiIcon, {
1021
+ material: "auto_awesome",
1022
+ size: 13
1023
+ }), "Explain with AI"]
1024
+ }),
783
1025
  /* @__PURE__ */ require_jsx_runtime.jsx("h3", { children: "Scheduling evidence" }),
784
1026
  /* @__PURE__ */ require_jsx_runtime.jsx("div", {
785
1027
  className: "diagnostics-table",
@@ -822,7 +1064,7 @@ function DiagnosticsPage() {
822
1064
  ]
823
1065
  });
824
1066
  }
825
- function ConsumerRows({ group, clusterId, expanded, toggle, usage, depth }) {
1067
+ function ConsumerRows({ group, clusterId, snapshotTime, expanded, toggle, usage, depth }) {
826
1068
  return /* @__PURE__ */ require_jsx_runtime.jsxs(require_jsx_runtime.Fragment, { children: [/* @__PURE__ */ require_jsx_runtime.jsxs("tr", {
827
1069
  className: `consumer-workload-row consumer-depth-${depth} consumer-tone-${consumerStatus(group.pods).tone}`,
828
1070
  "data-row-depth": depth,
@@ -873,20 +1115,59 @@ function ConsumerRows({ group, clusterId, expanded, toggle, usage, depth }) {
873
1115
  group.ownerResolved ? "" : " · Owner chain incomplete"
874
1116
  ]
875
1117
  })]
876
- }), /* @__PURE__ */ require_jsx_runtime.jsx("div", {
1118
+ }), /* @__PURE__ */ require_jsx_runtime.jsxs("div", {
877
1119
  className: "consumer-row-actions",
878
- children: /* @__PURE__ */ require_jsx_runtime.jsx("span", {
879
- className: "consumer-expand-slot",
880
- children: /* @__PURE__ */ require_jsx_runtime.jsx("button", {
881
- className: "consumer-expand",
1120
+ children: [
1121
+ ["warning", "error"].includes(consumerStatus(group.pods).tone) && /* @__PURE__ */ require_jsx_runtime.jsx(require_incident_copy_button.IncidentCopyButton, {
1122
+ clusterId,
1123
+ incident: {
1124
+ problem: `${group.kind} ${group.name}: ${consumerStatus(group.pods).label}`,
1125
+ facts: [
1126
+ `${group.pods.length} pods`,
1127
+ `CPU ${resourceText(group.cpu.usage, "cpu")} used / ${resourceText(group.cpu.requests, "cpu")} requested`,
1128
+ `Memory ${resourceText(group.memory.usage, "memory")} used / ${resourceText(group.memory.requests, "memory")} requested`
1129
+ ],
1130
+ time: snapshotTime,
1131
+ resources: [{
1132
+ kind: group.kind,
1133
+ namespace: group.namespace,
1134
+ name: group.name
1135
+ }]
1136
+ }
1137
+ }),
1138
+ ["warning", "error"].includes(consumerStatus(group.pods).tone) && /* @__PURE__ */ require_jsx_runtime.jsx(require_diagnostic_explain_button.DiagnosticExplainButton, {
882
1139
  type: "button",
883
- title: expanded ? "Collapse pods" : "Expand pods",
884
- "aria-label": `${expanded ? "Collapse" : "Expand"} pods for ${group.name}`,
885
- "aria-expanded": expanded,
886
- onClick: toggle,
887
- children: /* @__PURE__ */ require_jsx_runtime.jsx(require_ai_icon.AiIcon, { material: expanded ? "expand_less" : "expand_more" })
1140
+ className: "explain-finding",
1141
+ title: "Explain with AI",
1142
+ "aria-label": `Explain finding for ${group.name}`,
1143
+ onClick: () => require_ai_diagnostic_handoff.explainDiagnosticFinding({
1144
+ clusterId,
1145
+ title: `${group.kind} ${group.name}`,
1146
+ summary: `${consumerStatus(group.pods).label}; ${group.pods.length} pods. CPU ${resourceText(group.cpu.usage, "cpu")} used / ${resourceText(group.cpu.requests, "cpu")} requested; memory ${resourceText(group.memory.usage, "memory")} used / ${resourceText(group.memory.requests, "memory")} requested`,
1147
+ resource: {
1148
+ kind: group.kind,
1149
+ namespace: group.namespace,
1150
+ name: group.name
1151
+ }
1152
+ }),
1153
+ children: /* @__PURE__ */ require_jsx_runtime.jsx(require_ai_icon.AiIcon, {
1154
+ material: "auto_awesome",
1155
+ size: 13
1156
+ })
1157
+ }),
1158
+ /* @__PURE__ */ require_jsx_runtime.jsx("span", {
1159
+ className: "consumer-expand-slot",
1160
+ children: /* @__PURE__ */ require_jsx_runtime.jsx("button", {
1161
+ className: "consumer-expand",
1162
+ type: "button",
1163
+ title: expanded ? "Collapse pods" : "Expand pods",
1164
+ "aria-label": `${expanded ? "Collapse" : "Expand"} pods for ${group.name}`,
1165
+ "aria-expanded": expanded,
1166
+ onClick: toggle,
1167
+ children: /* @__PURE__ */ require_jsx_runtime.jsx(require_ai_icon.AiIcon, { material: expanded ? "expand_less" : "expand_more" })
1168
+ })
888
1169
  })
889
- })
1170
+ ]
890
1171
  })]
891
1172
  }) }),
892
1173
  /* @__PURE__ */ require_jsx_runtime.jsx("td", {
@@ -980,7 +1261,7 @@ function podRequest(pod, metric) {
980
1261
  return "Unavailable";
981
1262
  }
982
1263
  }
983
- function NamespaceRows({ namespace, groups, clusterId, usage, open, toggleNamespace, expanded, toggleGroup }) {
1264
+ function NamespaceRows({ namespace, groups, clusterId, snapshotTime, usage, open, toggleNamespace, expanded, toggleGroup }) {
984
1265
  const sum = (metric) => groups.reduce((total, group) => {
985
1266
  const next = group[metric];
986
1267
  return {
@@ -1010,22 +1291,36 @@ function NamespaceRows({ namespace, groups, clusterId, usage, open, toggleNamesp
1010
1291
  className: "consumer-identity-main",
1011
1292
  children: [/* @__PURE__ */ require_jsx_runtime.jsxs("div", {
1012
1293
  className: "consumer-title-line",
1013
- children: [/* @__PURE__ */ require_jsx_runtime.jsxs("span", {
1014
- className: "namespace-identity",
1015
- children: [/* @__PURE__ */ require_jsx_runtime.jsx(require_ai_icon.AiIcon, {
1016
- material: "folder_open",
1017
- size: 17
1018
- }), /* @__PURE__ */ require_jsx_runtime.jsx("span", {
1019
- className: "consumer-identity-label",
1020
- children: namespace
1021
- })]
1022
- }), /* @__PURE__ */ require_jsx_runtime.jsx(ConsumerStatus, { pods: groups.flatMap((group) => group.pods) })]
1294
+ children: [
1295
+ /* @__PURE__ */ require_jsx_runtime.jsxs("span", {
1296
+ className: "namespace-identity",
1297
+ children: [/* @__PURE__ */ require_jsx_runtime.jsx(require_ai_icon.AiIcon, {
1298
+ material: "folder_open",
1299
+ size: 17
1300
+ }), /* @__PURE__ */ require_jsx_runtime.jsx("span", {
1301
+ className: "consumer-identity-label",
1302
+ children: namespace
1303
+ })]
1304
+ }),
1305
+ /* @__PURE__ */ require_jsx_runtime.jsx(ConsumerStatus, { pods: groups.flatMap((group) => group.pods) }),
1306
+ /* @__PURE__ */ require_jsx_runtime.jsxs("small", { children: [
1307
+ groups.filter((group) => consumerStatus(group.pods).tone === "healthy").length,
1308
+ " ready ·",
1309
+ " ",
1310
+ groups.filter((group) => ["warning", "error"].includes(consumerStatus(group.pods).tone)).length,
1311
+ " ",
1312
+ "need attention · ",
1313
+ groups.filter((group) => consumerStatus(group.pods).tone === "neutral").length,
1314
+ " ",
1315
+ "unknown"
1316
+ ] })
1317
+ ]
1023
1318
  }), /* @__PURE__ */ require_jsx_runtime.jsxs("small", {
1024
1319
  className: "consumer-subtitle",
1025
1320
  title: "Resource totals include only consumers shown by the current filters",
1026
1321
  children: [
1027
1322
  groups.length,
1028
- " shown consumers",
1323
+ " filtered consumers",
1029
1324
  /* @__PURE__ */ require_jsx_runtime.jsx("span", { children: "·" }),
1030
1325
  groups.reduce((total, group) => total + group.pods.length, 0),
1031
1326
  " pods"
@@ -1069,6 +1364,7 @@ function NamespaceRows({ namespace, groups, clusterId, usage, open, toggleNamesp
1069
1364
  }), open && groups.map((group) => /* @__PURE__ */ require_jsx_runtime.jsx(ConsumerRows, {
1070
1365
  group,
1071
1366
  depth: namespace ? 1 : 0,
1367
+ snapshotTime,
1072
1368
  clusterId,
1073
1369
  expanded: expanded.includes(groupKey(group)),
1074
1370
  toggle: () => toggleGroup(groupKey(group)),