@rapidmx/web-client 0.17.0 → 0.18.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 (107) 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/calendar/layout/CalendarShell.tsx +1 -1
  33. package/apps/shared/components/contacts/layout/ContactsShell.tsx +1 -2
  34. package/apps/shared/components/mail/layout/MailShell.tsx +1 -2
  35. package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -2
  36. package/apps/shared/components/tasks/layout/TasksShell.tsx +1 -2
  37. package/apps/shared/navigation/index.ts +29 -2
  38. package/dist/apps/admin/diagnostics/index.d.ts +3 -0
  39. package/dist/apps/admin/diagnostics/index.js +6 -0
  40. package/dist/apps/shared/components/admin/diagnostics/Badge.d.ts +10 -0
  41. package/dist/apps/shared/components/admin/diagnostics/Badge.js +15 -0
  42. package/dist/apps/shared/components/admin/diagnostics/ComponentsTable.d.ts +15 -0
  43. package/dist/apps/shared/components/admin/diagnostics/ComponentsTable.js +35 -0
  44. package/dist/apps/shared/components/admin/diagnostics/DiagnosticsManager.d.ts +17 -0
  45. package/dist/apps/shared/components/admin/diagnostics/DiagnosticsManager.js +111 -0
  46. package/dist/apps/shared/components/admin/diagnostics/HostCard.d.ts +13 -0
  47. package/dist/apps/shared/components/admin/diagnostics/HostCard.js +33 -0
  48. package/dist/apps/shared/components/admin/diagnostics/InstalledPlugins.d.ts +26 -0
  49. package/dist/apps/shared/components/admin/diagnostics/InstalledPlugins.js +60 -0
  50. package/dist/apps/shared/components/admin/diagnostics/KubernetesNotice.d.ts +8 -0
  51. package/dist/apps/shared/components/admin/diagnostics/KubernetesNotice.js +9 -0
  52. package/dist/apps/shared/components/admin/diagnostics/LogStore.d.ts +37 -0
  53. package/dist/apps/shared/components/admin/diagnostics/LogStore.js +50 -0
  54. package/dist/apps/shared/components/admin/diagnostics/LogViewer.d.ts +24 -0
  55. package/dist/apps/shared/components/admin/diagnostics/LogViewer.js +47 -0
  56. package/dist/apps/shared/components/admin/diagnostics/LogsPanel.d.ts +13 -0
  57. package/dist/apps/shared/components/admin/diagnostics/LogsPanel.js +63 -0
  58. package/dist/apps/shared/components/admin/diagnostics/MetricTile.d.ts +21 -0
  59. package/dist/apps/shared/components/admin/diagnostics/MetricTile.js +7 -0
  60. package/dist/apps/shared/components/admin/diagnostics/NamespaceCard.d.ts +13 -0
  61. package/dist/apps/shared/components/admin/diagnostics/NamespaceCard.js +15 -0
  62. package/dist/apps/shared/components/admin/diagnostics/PackagesTable.d.ts +8 -0
  63. package/dist/apps/shared/components/admin/diagnostics/PackagesTable.js +31 -0
  64. package/dist/apps/shared/components/admin/diagnostics/ProcessCard.d.ts +8 -0
  65. package/dist/apps/shared/components/admin/diagnostics/ProcessCard.js +18 -0
  66. package/dist/apps/shared/components/admin/diagnostics/PvcTable.d.ts +6 -0
  67. package/dist/apps/shared/components/admin/diagnostics/PvcTable.js +18 -0
  68. package/dist/apps/shared/components/admin/diagnostics/RuntimePanel.d.ts +7 -0
  69. package/dist/apps/shared/components/admin/diagnostics/RuntimePanel.js +28 -0
  70. package/dist/apps/shared/components/admin/diagnostics/ServerVersionCard.d.ts +6 -0
  71. package/dist/apps/shared/components/admin/diagnostics/ServerVersionCard.js +17 -0
  72. package/dist/apps/shared/components/admin/diagnostics/Sparkline.d.ts +22 -0
  73. package/dist/apps/shared/components/admin/diagnostics/Sparkline.js +34 -0
  74. package/dist/apps/shared/components/admin/diagnostics/SystemPanel.d.ts +6 -0
  75. package/dist/apps/shared/components/admin/diagnostics/SystemPanel.js +26 -0
  76. package/dist/apps/shared/components/admin/diagnostics/UsageMeter.d.ts +26 -0
  77. package/dist/apps/shared/components/admin/diagnostics/UsageMeter.js +25 -0
  78. package/dist/apps/shared/components/admin/diagnostics/VersionsPanel.d.ts +10 -0
  79. package/dist/apps/shared/components/admin/diagnostics/VersionsPanel.js +11 -0
  80. package/dist/apps/shared/components/admin/diagnostics/diagnosticsApi.d.ts +168 -0
  81. package/dist/apps/shared/components/admin/diagnostics/diagnosticsApi.js +17 -0
  82. package/dist/apps/shared/components/admin/diagnostics/download.d.ts +9 -0
  83. package/dist/apps/shared/components/admin/diagnostics/download.js +23 -0
  84. package/dist/apps/shared/components/admin/diagnostics/format.d.ts +24 -0
  85. package/dist/apps/shared/components/admin/diagnostics/format.js +91 -0
  86. package/dist/apps/shared/components/admin/diagnostics/logClient.d.ts +85 -0
  87. package/dist/apps/shared/components/admin/diagnostics/logClient.js +173 -0
  88. package/dist/apps/shared/components/admin/diagnostics/logLines.d.ts +47 -0
  89. package/dist/apps/shared/components/admin/diagnostics/logLines.js +86 -0
  90. package/dist/apps/shared/components/admin/diagnostics/metricsHistory.d.ts +9 -0
  91. package/dist/apps/shared/components/admin/diagnostics/metricsHistory.js +19 -0
  92. package/dist/apps/shared/components/admin/diagnostics/useDiagnosticsResource.d.ts +12 -0
  93. package/dist/apps/shared/components/admin/diagnostics/useDiagnosticsResource.js +30 -0
  94. package/dist/apps/shared/components/admin/diagnostics/useLogStream.d.ts +37 -0
  95. package/dist/apps/shared/components/admin/diagnostics/useLogStream.js +79 -0
  96. package/dist/apps/shared/components/admin/diagnostics/useMetricsPolling.d.ts +20 -0
  97. package/dist/apps/shared/components/admin/diagnostics/useMetricsPolling.js +69 -0
  98. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +1 -1
  99. package/dist/apps/shared/components/admin/layout/AdminShell.js +2 -1
  100. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +1 -1
  101. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +1 -2
  102. package/dist/apps/shared/components/mail/layout/MailShell.js +1 -2
  103. package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -2
  104. package/dist/apps/shared/components/tasks/layout/TasksShell.js +1 -2
  105. package/dist/apps/shared/navigation/index.d.ts +11 -0
  106. package/dist/apps/shared/navigation/index.js +26 -2
  107. package/package.json +1 -1
@@ -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
+ }
@@ -0,0 +1,9 @@
1
+ /** Saves `content` to the viewer's computer as a file called `filename`. What the download buttons call, so a test can swap it. */
2
+ export type SaveFile = (filename: string, content: string, mimeType: string) => void;
3
+ /**
4
+ * The browser way to save text it made itself: a Blob behind an object URL, opened by a temporary `<a download>`. The URL is
5
+ * revoked once the click has been handled (not in the same turn: some browsers start the download after it).
6
+ */
7
+ export declare const saveTextFile: SaveFile;
8
+ /** `rapidmx-server-logs-2026-09-26T10-30-00.000Z.log`: the name, then the time with the colons a file name cannot hold replaced. */
9
+ export declare function timestampedFilename(name: string, extension: string, now?: Date): string;
@@ -0,0 +1,23 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ /**
6
+ * The browser way to save text it made itself: a Blob behind an object URL, opened by a temporary `<a download>`. The URL is
7
+ * revoked once the click has been handled (not in the same turn: some browsers start the download after it).
8
+ */
9
+ export const saveTextFile = (filename, content, mimeType) => {
10
+ const url = URL.createObjectURL(new Blob([content], { type: mimeType }));
11
+ const link = document.createElement("a");
12
+ link.href = url;
13
+ link.download = filename;
14
+ link.style.display = "none";
15
+ document.body.appendChild(link);
16
+ link.click();
17
+ link.remove();
18
+ setTimeout(() => URL.revokeObjectURL(url), 0);
19
+ };
20
+ /** `rapidmx-server-logs-2026-09-26T10-30-00.000Z.log`: the name, then the time with the colons a file name cannot hold replaced. */
21
+ export function timestampedFilename(name, extension, now = new Date()) {
22
+ return `${name}-${now.toISOString().replace(/:/g, "-")}.${extension}`;
23
+ }
@@ -0,0 +1,24 @@
1
+ /** What is shown where a number is missing or not a number. */
2
+ export declare const NO_VALUE = "\u2014";
3
+ /** `1536` as `1.5 KiB` (binary units, as Kubernetes reports them). */
4
+ export declare function formatBytes(bytes: number | undefined): string;
5
+ /** CPU as cores: `0.25 cores`, `1 core`, `4 cores`. */
6
+ export declare function formatCores(cores: number | undefined): string;
7
+ /** `42.34` as `42.3%`. */
8
+ export declare function formatPercent(percent: number | undefined): string;
9
+ /** `used` as a percentage of `capacity`, or `undefined` when either is missing or the capacity is not positive. */
10
+ export declare function percentOf(used: number | undefined, capacity: number | undefined): number | undefined;
11
+ /** Seconds as `3d 4h`, `4h 12m`, `12m 5s` or `5s`: the two largest units that are not zero. */
12
+ export declare function formatUptime(seconds: number | undefined): string;
13
+ /** A date as the viewer's local date and time, or a dash for a missing one (an unreadable one is shown as it came). */
14
+ export declare function formatDateTime(value: string | undefined): string;
15
+ /** `sha256:0123456789abcdef...` as `0123456789ab`. */
16
+ export declare function shortDigest(digest: string | undefined): string;
17
+ /** Shown when the server wants the administrator to have confirmed their identity recently (`api-104`). */
18
+ export declare const ELEVATION_MESSAGE = "Diagnostics needs you to have recently confirmed your identity. Reload this page, or sign in again, then try once more.";
19
+ /** Shown for a 403 that is not about elevation, or a 401. */
20
+ export declare const NOT_AUTHORISED_MESSAGE = "You are not authorised to view diagnostics.";
21
+ /** Whether the caller is not (or no longer) allowed: retrying the same request would fail the same way. */
22
+ export declare function isPermissionError(err: unknown): boolean;
23
+ /** Why a request failed, in words for the administrator. */
24
+ export declare function describeError(err: unknown, fallback: string): string;