@rapidmx/web-client 0.17.2 → 0.19.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 (96) hide show
  1. package/apps/admin/diagnostics/index.tsx +15 -0
  2. package/apps/shared/components/admin/diagnostics/Badge.tsx +37 -0
  3. package/apps/shared/components/admin/diagnostics/ComponentsTable.tsx +131 -0
  4. package/apps/shared/components/admin/diagnostics/DiagnosticsManager.tsx +173 -0
  5. package/apps/shared/components/admin/diagnostics/HostCard.tsx +108 -0
  6. package/apps/shared/components/admin/diagnostics/InstalledPlugins.tsx +136 -0
  7. package/apps/shared/components/admin/diagnostics/KubernetesNotice.tsx +25 -0
  8. package/apps/shared/components/admin/diagnostics/LogStore.ts +75 -0
  9. package/apps/shared/components/admin/diagnostics/LogViewer.tsx +87 -0
  10. package/apps/shared/components/admin/diagnostics/LogsPanel.tsx +235 -0
  11. package/apps/shared/components/admin/diagnostics/MetricTile.tsx +43 -0
  12. package/apps/shared/components/admin/diagnostics/NamespaceCard.tsx +95 -0
  13. package/apps/shared/components/admin/diagnostics/PackagesTable.tsx +107 -0
  14. package/apps/shared/components/admin/diagnostics/ProcessCard.tsx +70 -0
  15. package/apps/shared/components/admin/diagnostics/PvcTable.tsx +79 -0
  16. package/apps/shared/components/admin/diagnostics/RuntimePanel.tsx +110 -0
  17. package/apps/shared/components/admin/diagnostics/ServerVersionCard.tsx +37 -0
  18. package/apps/shared/components/admin/diagnostics/Sparkline.tsx +79 -0
  19. package/apps/shared/components/admin/diagnostics/SystemPanel.tsx +81 -0
  20. package/apps/shared/components/admin/diagnostics/UsageMeter.tsx +73 -0
  21. package/apps/shared/components/admin/diagnostics/VersionsPanel.tsx +36 -0
  22. package/apps/shared/components/admin/diagnostics/diagnosticsApi.ts +197 -0
  23. package/apps/shared/components/admin/diagnostics/download.ts +28 -0
  24. package/apps/shared/components/admin/diagnostics/format.ts +106 -0
  25. package/apps/shared/components/admin/diagnostics/logClient.ts +231 -0
  26. package/apps/shared/components/admin/diagnostics/logLines.ts +125 -0
  27. package/apps/shared/components/admin/diagnostics/metricsHistory.ts +28 -0
  28. package/apps/shared/components/admin/diagnostics/useDiagnosticsResource.ts +44 -0
  29. package/apps/shared/components/admin/diagnostics/useLogStream.ts +122 -0
  30. package/apps/shared/components/admin/diagnostics/useMetricsPolling.ts +84 -0
  31. package/apps/shared/components/admin/layout/AdminShell.tsx +3 -0
  32. package/apps/shared/components/admin/settings/PluginsManager.tsx +87 -12
  33. package/dist/apps/admin/diagnostics/index.d.ts +3 -0
  34. package/dist/apps/admin/diagnostics/index.js +6 -0
  35. package/dist/apps/shared/components/admin/diagnostics/Badge.d.ts +10 -0
  36. package/dist/apps/shared/components/admin/diagnostics/Badge.js +15 -0
  37. package/dist/apps/shared/components/admin/diagnostics/ComponentsTable.d.ts +15 -0
  38. package/dist/apps/shared/components/admin/diagnostics/ComponentsTable.js +35 -0
  39. package/dist/apps/shared/components/admin/diagnostics/DiagnosticsManager.d.ts +17 -0
  40. package/dist/apps/shared/components/admin/diagnostics/DiagnosticsManager.js +111 -0
  41. package/dist/apps/shared/components/admin/diagnostics/HostCard.d.ts +13 -0
  42. package/dist/apps/shared/components/admin/diagnostics/HostCard.js +33 -0
  43. package/dist/apps/shared/components/admin/diagnostics/InstalledPlugins.d.ts +26 -0
  44. package/dist/apps/shared/components/admin/diagnostics/InstalledPlugins.js +60 -0
  45. package/dist/apps/shared/components/admin/diagnostics/KubernetesNotice.d.ts +8 -0
  46. package/dist/apps/shared/components/admin/diagnostics/KubernetesNotice.js +9 -0
  47. package/dist/apps/shared/components/admin/diagnostics/LogStore.d.ts +37 -0
  48. package/dist/apps/shared/components/admin/diagnostics/LogStore.js +50 -0
  49. package/dist/apps/shared/components/admin/diagnostics/LogViewer.d.ts +24 -0
  50. package/dist/apps/shared/components/admin/diagnostics/LogViewer.js +47 -0
  51. package/dist/apps/shared/components/admin/diagnostics/LogsPanel.d.ts +13 -0
  52. package/dist/apps/shared/components/admin/diagnostics/LogsPanel.js +63 -0
  53. package/dist/apps/shared/components/admin/diagnostics/MetricTile.d.ts +21 -0
  54. package/dist/apps/shared/components/admin/diagnostics/MetricTile.js +7 -0
  55. package/dist/apps/shared/components/admin/diagnostics/NamespaceCard.d.ts +13 -0
  56. package/dist/apps/shared/components/admin/diagnostics/NamespaceCard.js +15 -0
  57. package/dist/apps/shared/components/admin/diagnostics/PackagesTable.d.ts +8 -0
  58. package/dist/apps/shared/components/admin/diagnostics/PackagesTable.js +31 -0
  59. package/dist/apps/shared/components/admin/diagnostics/ProcessCard.d.ts +8 -0
  60. package/dist/apps/shared/components/admin/diagnostics/ProcessCard.js +18 -0
  61. package/dist/apps/shared/components/admin/diagnostics/PvcTable.d.ts +6 -0
  62. package/dist/apps/shared/components/admin/diagnostics/PvcTable.js +18 -0
  63. package/dist/apps/shared/components/admin/diagnostics/RuntimePanel.d.ts +7 -0
  64. package/dist/apps/shared/components/admin/diagnostics/RuntimePanel.js +28 -0
  65. package/dist/apps/shared/components/admin/diagnostics/ServerVersionCard.d.ts +6 -0
  66. package/dist/apps/shared/components/admin/diagnostics/ServerVersionCard.js +17 -0
  67. package/dist/apps/shared/components/admin/diagnostics/Sparkline.d.ts +22 -0
  68. package/dist/apps/shared/components/admin/diagnostics/Sparkline.js +34 -0
  69. package/dist/apps/shared/components/admin/diagnostics/SystemPanel.d.ts +6 -0
  70. package/dist/apps/shared/components/admin/diagnostics/SystemPanel.js +26 -0
  71. package/dist/apps/shared/components/admin/diagnostics/UsageMeter.d.ts +26 -0
  72. package/dist/apps/shared/components/admin/diagnostics/UsageMeter.js +25 -0
  73. package/dist/apps/shared/components/admin/diagnostics/VersionsPanel.d.ts +10 -0
  74. package/dist/apps/shared/components/admin/diagnostics/VersionsPanel.js +11 -0
  75. package/dist/apps/shared/components/admin/diagnostics/diagnosticsApi.d.ts +168 -0
  76. package/dist/apps/shared/components/admin/diagnostics/diagnosticsApi.js +17 -0
  77. package/dist/apps/shared/components/admin/diagnostics/download.d.ts +9 -0
  78. package/dist/apps/shared/components/admin/diagnostics/download.js +23 -0
  79. package/dist/apps/shared/components/admin/diagnostics/format.d.ts +24 -0
  80. package/dist/apps/shared/components/admin/diagnostics/format.js +91 -0
  81. package/dist/apps/shared/components/admin/diagnostics/logClient.d.ts +85 -0
  82. package/dist/apps/shared/components/admin/diagnostics/logClient.js +173 -0
  83. package/dist/apps/shared/components/admin/diagnostics/logLines.d.ts +47 -0
  84. package/dist/apps/shared/components/admin/diagnostics/logLines.js +86 -0
  85. package/dist/apps/shared/components/admin/diagnostics/metricsHistory.d.ts +9 -0
  86. package/dist/apps/shared/components/admin/diagnostics/metricsHistory.js +19 -0
  87. package/dist/apps/shared/components/admin/diagnostics/useDiagnosticsResource.d.ts +12 -0
  88. package/dist/apps/shared/components/admin/diagnostics/useDiagnosticsResource.js +30 -0
  89. package/dist/apps/shared/components/admin/diagnostics/useLogStream.d.ts +37 -0
  90. package/dist/apps/shared/components/admin/diagnostics/useLogStream.js +79 -0
  91. package/dist/apps/shared/components/admin/diagnostics/useMetricsPolling.d.ts +20 -0
  92. package/dist/apps/shared/components/admin/diagnostics/useMetricsPolling.js +69 -0
  93. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +1 -1
  94. package/dist/apps/shared/components/admin/layout/AdminShell.js +2 -1
  95. package/dist/apps/shared/components/admin/settings/PluginsManager.js +61 -13
  96. package/package.json +2 -2
@@ -0,0 +1,21 @@
1
+ import React, { ReactNode } from "react";
2
+ import { SparklineProps } from "./Sparkline.js";
3
+ export interface MetricTileProps {
4
+ /** What is measured (`CPU`). */
5
+ label: string;
6
+ /** The current value, written out (`1.2 cores`). */
7
+ value: string;
8
+ /** Smaller text under the value. */
9
+ detail?: string;
10
+ /** When the value is a share of a capacity: the share (0 to 100) and the amounts it comes from. */
11
+ usage?: {
12
+ percent: number;
13
+ detail: string;
14
+ flag?: boolean;
15
+ };
16
+ /** The value's recent history, drawn under it. */
17
+ history?: Omit<SparklineProps, "label">;
18
+ children?: ReactNode;
19
+ }
20
+ /** One measurement: its label, the current value in numbers, a meter when it has a capacity, and a sparkline of its recent past. */
21
+ export default function MetricTile({ label, value, detail, usage, history, children }: MetricTileProps): React.JSX.Element;
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import Sparkline from "./Sparkline.js";
3
+ import UsageMeter from "./UsageMeter.js";
4
+ /** One measurement: its label, the current value in numbers, a meter when it has a capacity, and a sparkline of its recent past. */
5
+ export default function MetricTile({ label, value, detail, usage, history, children }) {
6
+ return (_jsxs("div", { className: "rounded-md border border-border bg-surface p-4", children: [_jsx("div", { className: "text-xs uppercase tracking-wide text-text-muted", children: label }), _jsx("div", { className: "mt-1 text-xl font-semibold", children: value }), detail && _jsx("div", { className: "text-xs text-text-muted", children: detail }), usage && (_jsx("div", { className: "mt-3", children: _jsx(UsageMeter, { label: `${label} usage`, percent: usage.percent, detail: usage.detail, flag: usage.flag }) })), history && (_jsx("div", { className: "mt-3", children: _jsx(Sparkline, { label: label, ...history }) })), children] }));
7
+ }
@@ -0,0 +1,13 @@
1
+ import React from "react";
2
+ import type { DiagnosticsMetrics } from "./diagnosticsApi.js";
3
+ type NamespaceMetrics = NonNullable<DiagnosticsMetrics["kubernetes"]["namespace"]>;
4
+ export interface NamespaceCardProps {
5
+ namespace: NamespaceMetrics | undefined;
6
+ history: DiagnosticsMetrics[];
7
+ }
8
+ /**
9
+ * The install's namespace: what all its pods use together, and what each pod uses. The figures come from metrics-server; without
10
+ * it there are none, and that is said as a hint (it is a choice of the cluster), not as a failure.
11
+ */
12
+ export default function NamespaceCard({ namespace, history }: NamespaceCardProps): React.JSX.Element;
13
+ export {};
@@ -0,0 +1,15 @@
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { HiOutlineInformationCircle } from "react-icons/hi2";
3
+ import { formatBytes, formatCores, NO_VALUE } from "./format.js";
4
+ import { seriesOf } from "./metricsHistory.js";
5
+ import MetricTile from "./MetricTile.js";
6
+ const POD_HEADINGS = ["Pod", "Component", "CPU", "Memory"];
7
+ /**
8
+ * The install's namespace: what all its pods use together, and what each pod uses. The figures come from metrics-server; without
9
+ * it there are none, and that is said as a hint (it is a choice of the cluster), not as a failure.
10
+ */
11
+ export default function NamespaceCard({ namespace, history }) {
12
+ // Biggest memory first; a pod with no figure last.
13
+ const pods = [...(namespace?.pods ?? [])].sort((a, b) => (b.memoryUsedBytes ?? -1) - (a.memoryUsedBytes ?? -1));
14
+ return (_jsxs("section", { "aria-labelledby": "diagnostics-namespace-heading", className: "rounded-md border border-border bg-surface p-4", children: [_jsxs("h2", { id: "diagnostics-namespace-heading", className: "text-base font-bold uppercase tracking-wide mb-3", children: ["Namespace", namespace?.name ? ` ${namespace.name}` : ""] }), !namespace ? (_jsx("p", { className: "text-sm text-text-muted", children: "The server did not report the namespace\u2019s figures." })) : !namespace.podMetricsAvailable ? (_jsxs("div", { role: "status", className: "flex items-start gap-3 rounded-md border border-border bg-surface-alt p-4 text-sm", children: [_jsx(HiOutlineInformationCircle, { size: 20, "aria-hidden": "true", className: "mt-0.5 shrink-0 text-text-muted" }), _jsxs("div", { children: [_jsx("p", { className: "font-medium", children: "Per-pod CPU and memory are not available." }), _jsxs("p", { className: "mt-1 text-text-muted", children: ["They come from metrics-server, which this cluster does not provide or the server may not read.", namespace.podMetricsReason ? ` ${namespace.podMetricsReason}` : ""] })] })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "grid grid-cols-1 gap-3 md:grid-cols-2", children: [_jsx(MetricTile, { label: "Namespace CPU", value: formatCores(namespace.cpuUsedCores), history: { values: seriesOf(history, (sample) => sample.kubernetes?.namespace?.cpuUsedCores), format: formatCores } }), _jsx(MetricTile, { label: "Namespace memory", value: formatBytes(namespace.memoryUsedBytes), history: { values: seriesOf(history, (sample) => sample.kubernetes?.namespace?.memoryUsedBytes), format: formatBytes } })] }), _jsx("h3", { className: "mt-4 mb-2 text-xs uppercase tracking-wide text-text-muted", children: "Pods" }), pods.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No pod figures were reported." })) : (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm border-collapse", children: [_jsx("thead", { children: _jsx("tr", { children: POD_HEADINGS.map((heading) => (_jsx("th", { className: "text-left text-xs uppercase tracking-wide text-text-muted py-2 px-2.5 border-b border-border", children: heading }, heading))) }) }), _jsx("tbody", { children: pods.map((pod) => (_jsxs("tr", { children: [_jsx("td", { className: "py-2 px-2.5 border-b border-border font-mono break-all", children: pod.name }), _jsx("td", { className: "py-2 px-2.5 border-b border-border", children: pod.component ?? NO_VALUE }), _jsx("td", { className: "py-2 px-2.5 border-b border-border tabular-nums", children: formatCores(pod.cpuUsedCores) }), _jsx("td", { className: "py-2 px-2.5 border-b border-border tabular-nums", children: formatBytes(pod.memoryUsedBytes) })] }, pod.name))) })] }) }))] }))] }));
15
+ }
@@ -0,0 +1,8 @@
1
+ import React from "react";
2
+ import type { DiagnosticsVersions } from "./diagnosticsApi.js";
3
+ /** How many packages are listed at first, and how many more each "Show more" adds: the list can be a thousand long. */
4
+ export declare const PACKAGES_PAGE_SIZE = 100;
5
+ /** Every package installed on the server, filtered by a name and paged, with a badge on the ones the server itself depends on. */
6
+ export default function PackagesTable({ packages }: {
7
+ packages: DiagnosticsVersions["packages"];
8
+ }): React.JSX.Element;
@@ -0,0 +1,31 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ ///////////////////////////////////////////////////////////////////////////////
3
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
4
+ // SPDX-License-Identifier: MPL-2.0
5
+ ///////////////////////////////////////////////////////////////////////////////
6
+ import { useMemo, useState } from "react";
7
+ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
8
+ import Badge from "./Badge.js";
9
+ /** How many packages are listed at first, and how many more each "Show more" adds: the list can be a thousand long. */
10
+ export const PACKAGES_PAGE_SIZE = 100;
11
+ const INPUT_CLASS = "w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
12
+ /** Every package installed on the server, filtered by a name and paged, with a badge on the ones the server itself depends on. */
13
+ export default function PackagesTable({ packages }) {
14
+ const [query, setQuery] = useState("");
15
+ const [directOnly, setDirectOnly] = useState(false);
16
+ const [limit, setLimit] = useState(PACKAGES_PAGE_SIZE);
17
+ const matching = useMemo(() => {
18
+ const needle = query.trim().toLowerCase();
19
+ return packages.filter((item) => (!directOnly || item.direct) && (needle === "" || `${item.name} ${item.version}`.toLowerCase().includes(needle)));
20
+ }, [packages, query, directOnly]);
21
+ const shown = matching.slice(0, limit);
22
+ return (_jsxs("section", { "aria-labelledby": "diagnostics-packages-heading", className: "rounded-md border border-border bg-surface p-4", children: [_jsx("h2", { id: "diagnostics-packages-heading", className: "text-base font-bold uppercase tracking-wide mb-3", children: "Installed packages" }), _jsxs("div", { className: "mb-3 flex flex-wrap items-center gap-3", children: [_jsx("div", { className: "min-w-48 flex-1", children: _jsx("input", { type: "search", "aria-label": "Filter packages", className: INPUT_CLASS, placeholder: "Filter by name or version", value: query, onChange: (event) => {
23
+ setQuery(event.target.value);
24
+ setLimit(PACKAGES_PAGE_SIZE);
25
+ } }) }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: directOnly, onChange: (event) => {
26
+ setDirectOnly(event.target.checked);
27
+ setLimit(PACKAGES_PAGE_SIZE);
28
+ } }), "Direct dependencies only"] })] }), _jsx("p", { role: "status", className: "mb-2 text-xs text-text-muted", children: matching.length === packages.length
29
+ ? `${packages.length} packages`
30
+ : `${matching.length} of ${packages.length} packages match` }), matching.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No packages match." })) : (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm border-collapse", children: [_jsx("thead", { children: _jsx("tr", { children: ["Package", "Version", ""].map((heading) => (_jsx("th", { className: "text-left text-xs uppercase tracking-wide text-text-muted py-2 px-2.5 border-b border-border", children: heading }, heading))) }) }), _jsx("tbody", { children: shown.map((item) => (_jsxs("tr", { children: [_jsx("td", { className: "py-2 px-2.5 border-b border-border font-mono break-all", children: item.name }), _jsx("td", { className: "py-2 px-2.5 border-b border-border font-mono", children: item.version }), _jsx("td", { className: "py-2 px-2.5 border-b border-border", children: item.direct && _jsx(Badge, { tone: "info", children: "direct" }) })] }, `${item.name}@${item.version}`))) })] }) })), shown.length < matching.length && (_jsxs("div", { className: "mt-3 flex items-center gap-3", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setLimit(limit + PACKAGES_PAGE_SIZE), children: "Show more" }), _jsxs("span", { className: "text-xs text-text-muted", children: ["Showing ", shown.length, " of ", matching.length] })] }))] }));
31
+ }
@@ -0,0 +1,8 @@
1
+ import React from "react";
2
+ import type { DiagnosticsMetrics } from "./diagnosticsApi.js";
3
+ export interface ProcessCardProps {
4
+ process: DiagnosticsMetrics["process"] | undefined;
5
+ history: DiagnosticsMetrics[];
6
+ }
7
+ /** The server's own Node.js process: its CPU, memory, and the memory and load its container sees. Shown with or without Kubernetes. */
8
+ export default function ProcessCard({ process, history }: ProcessCardProps): React.JSX.Element;
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { formatBytes, formatCores, formatPercent, percentOf } from "./format.js";
3
+ import { formatLoad } from "./HostCard.js";
4
+ import { seriesOf } from "./metricsHistory.js";
5
+ import MetricTile from "./MetricTile.js";
6
+ /** The server's own Node.js process: its CPU, memory, and the memory and load its container sees. Shown with or without Kubernetes. */
7
+ export default function ProcessCard({ process, history }) {
8
+ const heapPercent = percentOf(process?.heapUsedBytes, process?.heapTotalBytes);
9
+ const systemUsed = process?.systemMemoryTotalBytes !== undefined && process.systemMemoryFreeBytes !== undefined
10
+ ? process.systemMemoryTotalBytes - process.systemMemoryFreeBytes
11
+ : undefined;
12
+ const systemPercent = percentOf(systemUsed, process?.systemMemoryTotalBytes);
13
+ return (_jsxs("section", { "aria-labelledby": "diagnostics-process-heading", className: "rounded-md border border-border bg-surface p-4", children: [_jsx("h2", { id: "diagnostics-process-heading", className: "text-base font-bold uppercase tracking-wide mb-3", children: "This server process" }), !process ? (_jsx("p", { className: "text-sm text-text-muted", children: "The server did not report its own figures." })) : (_jsxs("div", { className: "grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-4", children: [_jsx(MetricTile, { label: "Process CPU", value: formatPercent(process.cpuPercent), detail: `${formatCores(process.cpuCount)} available`, history: { values: seriesOf(history, (sample) => sample.process?.cpuPercent), format: formatPercent } }), _jsx(MetricTile, { label: "Process memory (RSS)", value: formatBytes(process.rssBytes), history: { values: seriesOf(history, (sample) => sample.process?.rssBytes), format: formatBytes } }), _jsx(MetricTile, { label: "Heap", value: formatBytes(process.heapUsedBytes), detail: `of ${formatBytes(process.heapTotalBytes)} allocated`, usage: heapPercent === undefined
14
+ ? undefined
15
+ : { percent: heapPercent, detail: `${formatBytes(process.heapUsedBytes)} of ${formatBytes(process.heapTotalBytes)}`, flag: false } }), _jsx(MetricTile, { label: "Load average", value: formatLoad(process.loadAverage), detail: "1, 5 and 15 minutes" }), _jsx(MetricTile, { label: "Memory visible to the container", value: formatBytes(systemUsed), detail: `of ${formatBytes(process.systemMemoryTotalBytes)}`, usage: systemPercent === undefined
16
+ ? undefined
17
+ : { percent: systemPercent, detail: `${formatBytes(systemUsed)} of ${formatBytes(process.systemMemoryTotalBytes)}` } })] }))] }));
18
+ }
@@ -0,0 +1,6 @@
1
+ import React from "react";
2
+ import type { DiagnosticsPvcMetrics } from "./diagnosticsApi.js";
3
+ /** The install's persistent volume claims, with a usage bar for the ones the server can measure. */
4
+ export default function PvcTable({ pvcs }: {
5
+ pvcs: DiagnosticsPvcMetrics[];
6
+ }): React.JSX.Element;
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { formatBytes, NO_VALUE, percentOf } from "./format.js";
3
+ import UsageMeter from "./UsageMeter.js";
4
+ const HEADINGS = ["Volume", "Phase", "Storage class", "Size", "Usage"];
5
+ function Usage({ pvc }) {
6
+ if (!pvc.mountedByServer) {
7
+ return _jsx("span", { className: "text-text-muted", children: "Usage not available: not mounted in the server pod." });
8
+ }
9
+ const percent = percentOf(pvc.usedBytes, pvc.capacityBytes);
10
+ if (percent === undefined) {
11
+ return _jsx("span", { className: "text-text-muted", children: "The server has no usage figures for this volume." });
12
+ }
13
+ return (_jsxs("div", { children: [_jsx(UsageMeter, { label: `Usage of volume ${pvc.name}`, percent: percent, detail: `${formatBytes(pvc.usedBytes)} of ${formatBytes(pvc.capacityBytes)}`, flag: !pvc.sharesNodeDisk }), pvc.sharesNodeDisk && (_jsx("p", { className: "mt-1 text-xs text-text-muted", children: "Shares the node\u2019s disk; the volume\u2019s size is not enforced." }))] }));
14
+ }
15
+ /** The install's persistent volume claims, with a usage bar for the ones the server can measure. */
16
+ export default function PvcTable({ pvcs }) {
17
+ return (_jsxs("section", { "aria-labelledby": "diagnostics-pvc-heading", className: "rounded-md border border-border bg-surface p-4", children: [_jsx("h2", { id: "diagnostics-pvc-heading", className: "text-base font-bold uppercase tracking-wide mb-3", children: "Persistent volumes" }), pvcs.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "This install has no persistent volume claims." })) : (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm border-collapse", children: [_jsx("thead", { children: _jsx("tr", { children: HEADINGS.map((heading) => (_jsx("th", { className: "text-left text-xs uppercase tracking-wide text-text-muted py-2 px-2.5 border-b border-border", children: heading }, heading))) }) }), _jsx("tbody", { children: pvcs.map((pvc) => (_jsxs("tr", { className: "align-top", children: [_jsx("td", { className: "py-2 px-2.5 border-b border-border font-mono break-all", children: pvc.name }), _jsx("td", { className: "py-2 px-2.5 border-b border-border", children: pvc.phase }), _jsx("td", { className: "py-2 px-2.5 border-b border-border", children: pvc.storageClass ?? NO_VALUE }), _jsx("td", { className: "py-2 px-2.5 border-b border-border tabular-nums", children: formatBytes(pvc.capacityBytes ?? pvc.requestedBytes) }), _jsx("td", { className: "py-2 px-2.5 border-b border-border min-w-56", children: _jsx(Usage, { pvc: pvc }) })] }, pvc.name))) })] }) }))] }));
18
+ }
@@ -0,0 +1,7 @@
1
+ import React from "react";
2
+ import type { DiagnosticsRuntime } from "./diagnosticsApi.js";
3
+ import type { DiagnosticsResource } from "./useDiagnosticsResource.js";
4
+ /** The Runtime tab: the Kubernetes version and the nodes the install's pods run on. */
5
+ export default function RuntimePanel({ runtime }: {
6
+ runtime: DiagnosticsResource<DiagnosticsRuntime>;
7
+ }): React.JSX.Element;
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
3
+ import Badge from "./Badge.js";
4
+ import KubernetesNotice from "./KubernetesNotice.js";
5
+ import { formatDateTime, NO_VALUE } from "./format.js";
6
+ /** What a distribution's id is called on screen. */
7
+ const DISTRIBUTIONS = {
8
+ k3s: "k3s",
9
+ rke2: "RKE2",
10
+ eks: "Amazon EKS",
11
+ gke: "Google GKE",
12
+ aks: "Azure AKS",
13
+ kubernetes: "Kubernetes",
14
+ };
15
+ const NODE_HEADINGS = ["Node", "Internal IP", "Pods of this install"];
16
+ function Fact({ term, children }) {
17
+ return (_jsxs("div", { children: [_jsx("dt", { className: "text-xs uppercase tracking-wide text-text-muted", children: term }), _jsx("dd", { className: "break-words", children: children })] }));
18
+ }
19
+ function RuntimeDetails({ runtime }) {
20
+ const version = runtime.version;
21
+ const nodes = runtime.nodes ?? [];
22
+ return (_jsxs(_Fragment, { children: [_jsxs("section", { "aria-labelledby": "diagnostics-kubernetes-heading", className: "rounded-md border border-border bg-surface p-4", children: [_jsx("h2", { id: "diagnostics-kubernetes-heading", className: "text-base font-bold uppercase tracking-wide mb-3", children: "Kubernetes" }), _jsxs("dl", { className: "grid grid-cols-1 gap-x-6 gap-y-2 text-sm sm:grid-cols-2 lg:grid-cols-3", children: [_jsx(Fact, { term: "Version", children: _jsx("span", { className: "font-mono", children: version?.gitVersion ?? NO_VALUE }) }), _jsx(Fact, { term: "Distribution", children: version ? _jsx(Badge, { tone: "info", children: DISTRIBUTIONS[version.distribution] ?? version.distribution }) : NO_VALUE }), _jsx(Fact, { term: "Platform", children: version?.platform ?? NO_VALUE }), _jsx(Fact, { term: "Namespace", children: _jsx("span", { className: "font-mono", children: runtime.namespace ?? NO_VALUE }) }), _jsx(Fact, { term: "Go version", children: version?.goVersion ?? NO_VALUE }), _jsx(Fact, { term: "Built", children: formatDateTime(version?.buildDate) })] })] }), _jsxs("section", { "aria-labelledby": "diagnostics-nodes-heading", className: "rounded-md border border-border bg-surface p-4", children: [_jsx("h2", { id: "diagnostics-nodes-heading", className: "text-base font-bold uppercase tracking-wide mb-1", children: "Nodes" }), _jsx("p", { className: "mb-3 text-xs text-text-muted", children: "The nodes that run this install\u2019s pods. The server can only see its own namespace, so nothing about the nodes themselves is listed here." }), nodes.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No node is running any of this install\u2019s pods." })) : (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm border-collapse", children: [_jsx("thead", { children: _jsx("tr", { children: NODE_HEADINGS.map((heading) => (_jsx("th", { className: "text-left text-xs uppercase tracking-wide text-text-muted py-2 px-2.5 border-b border-border", children: heading }, heading))) }) }), _jsx("tbody", { children: nodes.map((node) => (_jsxs("tr", { children: [_jsx("td", { className: "py-2 px-2.5 border-b border-border font-mono break-all", children: node.name }), _jsx("td", { className: "py-2 px-2.5 border-b border-border font-mono", children: node.internalIP ?? NO_VALUE }), _jsx("td", { className: "py-2 px-2.5 border-b border-border tabular-nums", children: node.podCount })] }, node.name))) })] }) }))] })] }));
23
+ }
24
+ /** The Runtime tab: the Kubernetes version and the nodes the install's pods run on. */
25
+ export default function RuntimePanel({ runtime }) {
26
+ const { data, error, loading } = runtime;
27
+ return (_jsxs("div", { className: "space-y-6", children: [error && _jsx(Alert, { children: error }), !data && loading && _jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" }), data && (data.available ? _jsx(RuntimeDetails, { runtime: data }) : _jsx(KubernetesNotice, { reason: data.reason }))] }));
28
+ }
@@ -0,0 +1,6 @@
1
+ import React from "react";
2
+ import type { DiagnosticsVersions } from "./diagnosticsApi.js";
3
+ /** The server process: Node.js, the deployed package, the machine and how long it has been up. */
4
+ export default function ServerVersionCard({ server }: {
5
+ server: DiagnosticsVersions["server"];
6
+ }): React.JSX.Element;
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { formatDateTime, formatUptime, NO_VALUE } from "./format.js";
3
+ /** The server process: Node.js, the deployed package, the machine and how long it has been up. */
4
+ export default function ServerVersionCard({ server }) {
5
+ const rows = [
6
+ ["Node.js", server.nodeVersion || NO_VALUE],
7
+ ["V8", server.v8Version || NO_VALUE],
8
+ ["Package", server.packageName ? `${server.packageName} ${server.packageVersion ?? ""}`.trim() : NO_VALUE],
9
+ ["Environment", server.nodeEnv || NO_VALUE],
10
+ ["Platform", [server.platform, server.arch].filter(Boolean).join(" / ") || NO_VALUE],
11
+ ["Host name", server.hostname || NO_VALUE],
12
+ ["Process ID", server.pid === undefined ? NO_VALUE : String(server.pid)],
13
+ ["Started", formatDateTime(server.startedAt)],
14
+ ["Up for", formatUptime(server.uptimeSeconds)],
15
+ ];
16
+ return (_jsxs("section", { "aria-labelledby": "diagnostics-server-heading", className: "rounded-md border border-border bg-surface p-4", children: [_jsx("h2", { id: "diagnostics-server-heading", className: "text-base font-bold uppercase tracking-wide mb-3", children: "Server" }), _jsx("dl", { className: "grid grid-cols-1 gap-x-6 gap-y-2 text-sm sm:grid-cols-2 lg:grid-cols-3", children: rows.map(([term, description]) => (_jsxs("div", { children: [_jsx("dt", { className: "text-xs uppercase tracking-wide text-text-muted", children: term }), _jsx("dd", { className: "break-words", children: description })] }, term))) })] }));
17
+ }
@@ -0,0 +1,22 @@
1
+ import React from "react";
2
+ /**
3
+ * Where each value is drawn, as `[x, y]` in the plot's own coordinates. The x-axis always has room for a full
4
+ * history (`METRICS_HISTORY_SIZE` samples), with the newest sample at the right edge, so a line that has just started
5
+ * grows in from the right rather than being stretched across the plot. The y-axis runs from zero to `max` (the highest
6
+ * value when it is not given), so the shape is never a lie about how much the value moves.
7
+ */
8
+ export declare function sparklinePoints(values: number[], max?: number): [number, number][];
9
+ export interface SparklineProps {
10
+ values: number[];
11
+ /** What is drawn, for the text alternative (`CPU used`). */
12
+ label: string;
13
+ /** The value the plot's top stands for. Defaults to the highest value seen. */
14
+ max?: number;
15
+ /** Writes a value for the text alternative. */
16
+ format: (value: number) => string;
17
+ }
18
+ /**
19
+ * A line with a wash of the same colour under it, over the last few minutes of a value. It carries no numbers itself: the
20
+ * value beside it does, and the text alternative says the latest, lowest and highest.
21
+ */
22
+ export default function Sparkline({ values, label, max, format }: SparklineProps): React.JSX.Element;
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { METRICS_HISTORY_SIZE } from "./metricsHistory.js";
3
+ const WIDTH = 240;
4
+ const HEIGHT = 48;
5
+ /** Keeps a line at the very top or bottom of the plot from being clipped by its own 2px stroke. */
6
+ const PADDING = 2;
7
+ /**
8
+ * Where each value is drawn, as `[x, y]` in the plot's own coordinates. The x-axis always has room for a full
9
+ * history (`METRICS_HISTORY_SIZE` samples), with the newest sample at the right edge, so a line that has just started
10
+ * grows in from the right rather than being stretched across the plot. The y-axis runs from zero to `max` (the highest
11
+ * value when it is not given), so the shape is never a lie about how much the value moves.
12
+ */
13
+ export function sparklinePoints(values, max) {
14
+ const top = max !== undefined && max > 0 ? max : Math.max(...values, 0) || 1;
15
+ const step = WIDTH / (METRICS_HISTORY_SIZE - 1);
16
+ return values.map((value, index) => {
17
+ const x = WIDTH - (values.length - 1 - index) * step;
18
+ const y = HEIGHT - PADDING - (Math.min(Math.max(value, 0), top) / top) * (HEIGHT - 2 * PADDING);
19
+ return [x, y];
20
+ });
21
+ }
22
+ /**
23
+ * A line with a wash of the same colour under it, over the last few minutes of a value. It carries no numbers itself: the
24
+ * value beside it does, and the text alternative says the latest, lowest and highest.
25
+ */
26
+ export default function Sparkline({ values, label, max, format }) {
27
+ const points = sparklinePoints(values, max);
28
+ let description = `${label}: collecting samples`;
29
+ if (values.length > 1) {
30
+ description = `${label}: latest ${format(values[values.length - 1])}, lowest ${format(Math.min(...values))}, highest ${format(Math.max(...values))} over the last ${values.length} samples`;
31
+ }
32
+ const line = points.map(([x, y], index) => `${index === 0 ? "M" : "L"}${x.toFixed(1)} ${y.toFixed(1)}`).join(" ");
33
+ return (_jsxs("svg", { role: "img", "aria-label": description, viewBox: `0 0 ${WIDTH} ${HEIGHT}`, preserveAspectRatio: "none", className: "block h-12 w-full", children: [_jsx("title", { children: description }), _jsx("line", { x1: "0", y1: HEIGHT - 0.5, x2: WIDTH, y2: HEIGHT - 0.5, className: "stroke-border", strokeWidth: "1", vectorEffect: "non-scaling-stroke" }), points.length > 1 && (_jsxs(_Fragment, { children: [_jsx("path", { d: `${line} L${points[points.length - 1][0].toFixed(1)} ${HEIGHT} L${points[0][0].toFixed(1)} ${HEIGHT} Z`, className: "fill-primary/10" }), _jsx("path", { d: line, fill: "none", className: "stroke-primary", strokeWidth: "2", strokeLinejoin: "round", strokeLinecap: "round", vectorEffect: "non-scaling-stroke" })] }))] }));
34
+ }
@@ -0,0 +1,6 @@
1
+ import React from "react";
2
+ import type { MetricsPolling } from "./useMetricsPolling.js";
3
+ /** The System tab: live resource use, refreshed every few seconds, with the last few minutes drawn as sparklines. */
4
+ export default function SystemPanel({ polling }: {
5
+ polling: MetricsPolling;
6
+ }): React.JSX.Element;
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { HiOutlinePause, HiOutlinePlay } from "react-icons/hi2";
3
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
4
+ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
5
+ import { formatDateTime } from "./format.js";
6
+ import HostCard from "./HostCard.js";
7
+ import KubernetesNotice from "./KubernetesNotice.js";
8
+ import { METRICS_POLL_INTERVAL_MS } from "./metricsHistory.js";
9
+ import NamespaceCard from "./NamespaceCard.js";
10
+ import ProcessCard from "./ProcessCard.js";
11
+ import PvcTable from "./PvcTable.js";
12
+ /** The System tab: live resource use, refreshed every few seconds, with the last few minutes drawn as sparklines. */
13
+ export default function SystemPanel({ polling }) {
14
+ const { history, error, loading, paused, setPaused } = polling;
15
+ const latest = history[history.length - 1];
16
+ const kubernetes = latest?.kubernetes;
17
+ const errors = kubernetes?.errors ?? [];
18
+ let status = `Updates every ${METRICS_POLL_INTERVAL_MS / 1000} seconds while this page is visible.`;
19
+ if (paused) {
20
+ status = "Updates are paused.";
21
+ }
22
+ else if (loading) {
23
+ status = "Loading…";
24
+ }
25
+ return (_jsxs("div", { className: "space-y-6", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", "aria-pressed": paused, onClick: () => setPaused(!paused), children: paused ? (_jsxs(_Fragment, { children: [_jsx(HiOutlinePlay, { size: 16, "aria-hidden": "true", className: "mr-1 inline" }), "Resume"] })) : (_jsxs(_Fragment, { children: [_jsx(HiOutlinePause, { size: 16, "aria-hidden": "true", className: "mr-1 inline" }), "Pause"] })) }), _jsxs("p", { role: "status", className: "text-xs text-text-muted", children: [status, latest && ` Last sample ${formatDateTime(latest.collectedAt)}.`] })] }), error && _jsx(Alert, { children: error }), latest && (_jsxs(_Fragment, { children: [_jsx(HostCard, { host: latest.host, history: history }), _jsx(ProcessCard, { process: latest.process, history: history }), !kubernetes?.available ? (_jsx(KubernetesNotice, { reason: kubernetes?.reason })) : (_jsxs(_Fragment, { children: [errors.length > 0 && (_jsx(Alert, { children: errors.map((message) => (_jsx("div", { children: message }, message))) })), _jsx(NamespaceCard, { namespace: kubernetes.namespace, history: history }), _jsx(PvcTable, { pvcs: kubernetes.pvcs ?? [] })] }))] }))] }));
26
+ }
@@ -0,0 +1,26 @@
1
+ import React from "react";
2
+ /** Above this share of its capacity, a resource is called high. */
3
+ export declare const HIGH_USAGE_PERCENT = 80;
4
+ /** Above this share of its capacity, a resource is called critical. */
5
+ export declare const CRITICAL_USAGE_PERCENT = 90;
6
+ export type UsageSeverity = "normal" | "high" | "critical";
7
+ export declare function usageSeverity(percent: number): UsageSeverity;
8
+ export interface UsageMeterProps {
9
+ /** What the bar measures (`Data volume usage`), for its text alternative. */
10
+ label: string;
11
+ /** How much of the capacity is used, 0 to 100 (a value outside is clamped for the bar, and written as it is). */
12
+ percent: number;
13
+ /** The amounts the percentage comes from (`3.2 GiB of 8 GiB`). */
14
+ detail: string;
15
+ /**
16
+ * Whether a high value is flagged (default). Off for a figure that is not the fullness of something of its own, such as a
17
+ * volume that shares the node's disk, where "nearly full" would be a false alarm.
18
+ */
19
+ flag?: boolean;
20
+ }
21
+ /**
22
+ * A bar showing how full something is. The amounts and the percentage are always written beside it, and a bar over
23
+ * `HIGH_USAGE_PERCENT` or `CRITICAL_USAGE_PERCENT` is also worded (High, Critical) with an icon, so the state is never
24
+ * only a colour.
25
+ */
26
+ export default function UsageMeter({ label, percent, detail, flag }: UsageMeterProps): React.JSX.Element;
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { HiOutlineExclamationCircle, HiOutlineExclamationTriangle } from "react-icons/hi2";
3
+ import { formatPercent } from "./format.js";
4
+ /** Above this share of its capacity, a resource is called high. */
5
+ export const HIGH_USAGE_PERCENT = 80;
6
+ /** Above this share of its capacity, a resource is called critical. */
7
+ export const CRITICAL_USAGE_PERCENT = 90;
8
+ export function usageSeverity(percent) {
9
+ if (percent > CRITICAL_USAGE_PERCENT) {
10
+ return "critical";
11
+ }
12
+ return percent > HIGH_USAGE_PERCENT ? "high" : "normal";
13
+ }
14
+ /** The fill of the bar for each severity: the ordinary accent, then the warning and danger colours. */
15
+ const FILL = { normal: "bg-primary", high: "bg-warning", critical: "bg-danger" };
16
+ /**
17
+ * A bar showing how full something is. The amounts and the percentage are always written beside it, and a bar over
18
+ * `HIGH_USAGE_PERCENT` or `CRITICAL_USAGE_PERCENT` is also worded (High, Critical) with an icon, so the state is never
19
+ * only a colour.
20
+ */
21
+ export default function UsageMeter({ label, percent, detail, flag = true }) {
22
+ const severity = flag ? usageSeverity(percent) : "normal";
23
+ const width = Math.min(Math.max(percent, 0), 100);
24
+ return (_jsxs("div", { children: [_jsxs("div", { className: "flex items-center justify-between gap-2 text-sm", children: [_jsx("span", { className: "tabular-nums", children: detail }), _jsxs("span", { className: "flex items-center gap-1 tabular-nums", children: [severity === "high" && _jsx(HiOutlineExclamationTriangle, { size: 16, "aria-hidden": "true", className: "text-warning" }), severity === "critical" && _jsx(HiOutlineExclamationCircle, { size: 16, "aria-hidden": "true", className: "text-danger" }), formatPercent(percent), severity !== "normal" && _jsx("span", { className: "font-medium", children: severity === "high" ? "High" : "Critical" })] })] }), _jsx("div", { role: "meter", "aria-label": label, "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round(width), "aria-valuetext": `${detail}, ${formatPercent(percent)}${severity === "normal" ? "" : `, ${severity}`}`, className: "mt-1 h-2 overflow-hidden rounded-pill border border-border bg-surface-alt", children: _jsx("div", { className: `h-full rounded-pill ${FILL[severity]}`, style: { width: `${width}%` } }) })] }));
25
+ }
@@ -0,0 +1,10 @@
1
+ import React from "react";
2
+ import type { DiagnosticsVersions } from "./diagnosticsApi.js";
3
+ import type { DiagnosticsResource } from "./useDiagnosticsResource.js";
4
+ import { InstalledPluginsData } from "./InstalledPlugins.js";
5
+ export interface VersionsPanelProps {
6
+ versions: DiagnosticsResource<DiagnosticsVersions>;
7
+ plugins: DiagnosticsResource<InstalledPluginsData>;
8
+ }
9
+ /** The Versions tab: the server, its packages, the installed plugins and the install's other containers. */
10
+ export default function VersionsPanel({ versions, plugins }: VersionsPanelProps): React.JSX.Element;
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
3
+ import ComponentsTable from "./ComponentsTable.js";
4
+ import InstalledPlugins from "./InstalledPlugins.js";
5
+ import PackagesTable from "./PackagesTable.js";
6
+ import ServerVersionCard from "./ServerVersionCard.js";
7
+ /** The Versions tab: the server, its packages, the installed plugins and the install's other containers. */
8
+ export default function VersionsPanel({ versions, plugins }) {
9
+ const { data, error, loading } = versions;
10
+ return (_jsxs("div", { className: "space-y-6", children: [error && _jsx(Alert, { children: error }), !data && loading && _jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" }), data && (_jsxs(_Fragment, { children: [_jsx(ServerVersionCard, { server: data.server }), _jsx(ComponentsTable, { components: data.components ?? [], kubernetes: data.kubernetes ?? { available: false } }), _jsx(InstalledPlugins, { data: plugins.data, error: plugins.error, loading: plugins.loading }), _jsx(PackagesTable, { packages: data.packages ?? [] })] }))] }));
11
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * The client of the server's `/admin/diagnostics/*` endpoints (`versions`, `runtime`, `metrics`). All three are GET,
3
+ * trusted-administrator only and `@RequiresElevation()`: an elevated token is required (403 `api-104`, see
4
+ * `../elevation.ts`), and any other 403 means the caller is not an administrator. The server may leave any optional
5
+ * field out, so a caller reads them defensively.
6
+ *
7
+ * The server reads Kubernetes with namespace-scoped permissions only: it can list the install's own pods, but not the
8
+ * cluster's nodes or the kubelet's statistics. That is why a node here is only what the pods say of it, and why the
9
+ * machine figures are those of the node running the server, read from inside its own container.
10
+ */
11
+ /** The components of a RapidMX install that run beside the server itself, in the order the server lists them. */
12
+ export type DiagnosticsComponentName = "postfix" | "postfix-bridge" | "mongodb" | "postgresql" | "redis" | "coturn" | "rspamd" | "clamav";
13
+ export interface DiagnosticsContainer {
14
+ name: string;
15
+ image: string;
16
+ tag?: string;
17
+ digest?: string;
18
+ ready: boolean;
19
+ restartCount: number;
20
+ }
21
+ export interface DiagnosticsPod {
22
+ name: string;
23
+ phase: string;
24
+ ready: boolean;
25
+ restarts: number;
26
+ node?: string;
27
+ startedAt?: string;
28
+ containers: DiagnosticsContainer[];
29
+ }
30
+ export interface DiagnosticsComponent {
31
+ component: DiagnosticsComponentName;
32
+ /** `missing`: Kubernetes is reachable but no pod for it exists. `unknown`: Kubernetes is unavailable. */
33
+ status: "running" | "not-ready" | "missing" | "unknown";
34
+ /** The image tag of the component's main container. */
35
+ version?: string;
36
+ pods: DiagnosticsPod[];
37
+ }
38
+ export interface DiagnosticsVersions {
39
+ server: {
40
+ nodeVersion: string;
41
+ v8Version: string;
42
+ packageName: string;
43
+ packageVersion: string;
44
+ platform: string;
45
+ arch: string;
46
+ hostname: string;
47
+ pid: number;
48
+ startedAt: string;
49
+ uptimeSeconds: number;
50
+ nodeEnv: string;
51
+ };
52
+ /** Sorted by name. */
53
+ packages: {
54
+ name: string;
55
+ version: string;
56
+ direct: boolean;
57
+ }[];
58
+ /** Always all eight, in the order of `DiagnosticsComponentName`. */
59
+ components: DiagnosticsComponent[];
60
+ kubernetes: {
61
+ available: boolean;
62
+ reason?: string;
63
+ };
64
+ }
65
+ /** A node that runs some of this install's pods, derived from the pods: nothing else about the node is readable. */
66
+ export interface DiagnosticsNode {
67
+ name: string;
68
+ internalIP?: string;
69
+ /** How many of the install's pods run on it. */
70
+ podCount: number;
71
+ }
72
+ export type KubernetesDistribution = "k3s" | "rke2" | "eks" | "gke" | "aks" | "kubernetes";
73
+ export interface DiagnosticsRuntime {
74
+ available: boolean;
75
+ reason?: string;
76
+ namespace?: string;
77
+ version?: {
78
+ gitVersion: string;
79
+ major: string;
80
+ minor: string;
81
+ platform: string;
82
+ goVersion?: string;
83
+ buildDate?: string;
84
+ distribution: KubernetesDistribution;
85
+ };
86
+ nodes: DiagnosticsNode[];
87
+ }
88
+ /** One filesystem of the node that runs the server, as the server's own container sees it. */
89
+ export interface DiagnosticsDiskMetrics {
90
+ path: string;
91
+ /** The volume claim mounted there, when it is one. */
92
+ pvc?: string;
93
+ usedBytes: number;
94
+ capacityBytes: number;
95
+ availableBytes: number;
96
+ /** The figures are the whole node filesystem's, not a volume of its own (a hostPath-style volume, such as k3s local-path). */
97
+ sharesNodeDisk?: boolean;
98
+ }
99
+ /**
100
+ * The node the server pod runs on, as seen from inside the server's own container (on a single-node k3s that is the whole
101
+ * machine). In a cluster of several nodes it is only that one node.
102
+ */
103
+ export interface DiagnosticsHostMetrics {
104
+ /** The whole host, 0 to 100 across all cores. */
105
+ cpuPercent: number;
106
+ cpuCount: number;
107
+ memoryTotalBytes: number;
108
+ memoryUsedBytes: number;
109
+ loadAverage: [number, number, number];
110
+ disks: DiagnosticsDiskMetrics[];
111
+ }
112
+ /** From the metrics API (metrics-server): absent when it is not installed. */
113
+ export interface DiagnosticsPodMetrics {
114
+ name: string;
115
+ component?: DiagnosticsComponentName | "server";
116
+ cpuUsedCores?: number;
117
+ memoryUsedBytes?: number;
118
+ }
119
+ /**
120
+ * `usedBytes`/`availableBytes` exist only for a volume the server pod mounts (`mountedByServer`). `sharesNodeDisk` means the
121
+ * figures are the whole node filesystem's, and the volume's own size is not enforced.
122
+ */
123
+ export interface DiagnosticsPvcMetrics {
124
+ name: string;
125
+ phase: string;
126
+ storageClass?: string;
127
+ requestedBytes?: number;
128
+ capacityBytes?: number;
129
+ usedBytes?: number;
130
+ availableBytes?: number;
131
+ mountedByServer: boolean;
132
+ sharesNodeDisk?: boolean;
133
+ }
134
+ export interface DiagnosticsMetrics {
135
+ collectedAt: string;
136
+ process: {
137
+ cpuPercent: number;
138
+ rssBytes: number;
139
+ heapUsedBytes: number;
140
+ heapTotalBytes: number;
141
+ loadAverage: [number, number, number];
142
+ systemMemoryTotalBytes: number;
143
+ systemMemoryFreeBytes: number;
144
+ cpuCount: number;
145
+ };
146
+ host: DiagnosticsHostMetrics;
147
+ kubernetes: {
148
+ available: boolean;
149
+ reason?: string;
150
+ namespace?: {
151
+ name: string;
152
+ /** Whether the pod figures could be read: they need metrics-server. */
153
+ podMetricsAvailable: boolean;
154
+ podMetricsReason?: string;
155
+ cpuUsedCores?: number;
156
+ memoryUsedBytes?: number;
157
+ pods: DiagnosticsPodMetrics[];
158
+ };
159
+ pvcs: DiagnosticsPvcMetrics[];
160
+ errors: string[];
161
+ };
162
+ }
163
+ /** What is installed and running: the server process, its packages, and the other containers of the install. */
164
+ export declare function getDiagnosticsVersions(): Promise<DiagnosticsVersions>;
165
+ /** The Kubernetes version, and the nodes the install's pods run on. */
166
+ export declare function getDiagnosticsRuntime(): Promise<DiagnosticsRuntime>;
167
+ /** One sample of live resource use. Polled by the System tab. */
168
+ export declare function getDiagnosticsMetrics(): Promise<DiagnosticsMetrics>;
@@ -0,0 +1,17 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import { apiFetch } from "@rapidmx/react-shared/util/api.js";
6
+ /** What is installed and running: the server process, its packages, and the other containers of the install. */
7
+ export function getDiagnosticsVersions() {
8
+ return apiFetch("/admin/diagnostics/versions");
9
+ }
10
+ /** The Kubernetes version, and the nodes the install's pods run on. */
11
+ export function getDiagnosticsRuntime() {
12
+ return apiFetch("/admin/diagnostics/runtime");
13
+ }
14
+ /** One sample of live resource use. Polled by the System tab. */
15
+ export function getDiagnosticsMetrics() {
16
+ return apiFetch("/admin/diagnostics/metrics");
17
+ }