@open-mercato/core 0.6.6-develop.6384.1.f06fc0b42c → 0.6.6-develop.6386.1.391a1afb9e
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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/bootstrap.js +6 -1
- package/dist/bootstrap.js.map +2 -2
- package/dist/modules/attachments/lib/scope.js +26 -0
- package/dist/modules/attachments/lib/scope.js.map +7 -0
- package/dist/modules/catalog/commands/variants.js +4 -2
- package/dist/modules/catalog/commands/variants.js.map +2 -2
- package/dist/modules/configs/api/module-telemetry/route.js +75 -0
- package/dist/modules/configs/api/module-telemetry/route.js.map +7 -0
- package/dist/modules/configs/api/openapi.js +110 -0
- package/dist/modules/configs/api/openapi.js.map +2 -2
- package/dist/modules/configs/backend/config/cache/page.meta.js +1 -1
- package/dist/modules/configs/backend/config/cache/page.meta.js.map +1 -1
- package/dist/modules/configs/backend/config/module-telemetry/page.js +10 -0
- package/dist/modules/configs/backend/config/module-telemetry/page.js.map +7 -0
- package/dist/modules/configs/backend/config/module-telemetry/page.meta.js +36 -0
- package/dist/modules/configs/backend/config/module-telemetry/page.meta.js.map +7 -0
- package/dist/modules/configs/components/ModuleTelemetryPanel.js +801 -0
- package/dist/modules/configs/components/ModuleTelemetryPanel.js.map +7 -0
- package/dist/modules/messages/commands/messages.js +0 -1
- package/dist/modules/messages/commands/messages.js.map +2 -2
- package/dist/modules/messages/lib/attachments.js +13 -7
- package/dist/modules/messages/lib/attachments.js.map +2 -2
- package/dist/modules/query_index/di.js +3 -3
- package/dist/modules/query_index/di.js.map +2 -2
- package/package.json +7 -7
- package/src/bootstrap.ts +7 -1
- package/src/modules/attachments/lib/scope.ts +42 -0
- package/src/modules/catalog/commands/variants.ts +7 -2
- package/src/modules/configs/api/module-telemetry/route.ts +77 -0
- package/src/modules/configs/api/openapi.ts +117 -0
- package/src/modules/configs/backend/config/cache/page.meta.ts +1 -1
- package/src/modules/configs/backend/config/module-telemetry/page.meta.ts +34 -0
- package/src/modules/configs/backend/config/module-telemetry/page.tsx +12 -0
- package/src/modules/configs/components/ModuleTelemetryPanel.tsx +1109 -0
- package/src/modules/configs/i18n/de.json +72 -0
- package/src/modules/configs/i18n/en.json +72 -0
- package/src/modules/configs/i18n/es.json +72 -0
- package/src/modules/configs/i18n/pl.json +72 -0
- package/src/modules/messages/commands/messages.ts +0 -1
- package/src/modules/messages/lib/attachments.ts +15 -7
- package/src/modules/query_index/di.ts +3 -3
|
@@ -0,0 +1,801 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|
5
|
+
import { Button } from "@open-mercato/ui/primitives/button";
|
|
6
|
+
import {
|
|
7
|
+
SegmentedControl,
|
|
8
|
+
SegmentedControlItem
|
|
9
|
+
} from "@open-mercato/ui/primitives/segmented-control";
|
|
10
|
+
import { Spinner } from "@open-mercato/ui/primitives/spinner";
|
|
11
|
+
import { StatusBadge } from "@open-mercato/ui/primitives/status-badge";
|
|
12
|
+
import { SimpleTooltip, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@open-mercato/ui/primitives/tooltip";
|
|
13
|
+
import { IconButton } from "@open-mercato/ui/primitives/icon-button";
|
|
14
|
+
import { formatAttachmentFileSize as formatBytes } from "@open-mercato/ui/backend/detail/AttachmentVisualPreview";
|
|
15
|
+
import { ErrorMessage } from "@open-mercato/ui/backend/detail";
|
|
16
|
+
import { readApiResultOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
|
|
17
|
+
import { flash } from "@open-mercato/ui/backend/FlashMessages";
|
|
18
|
+
import { useConfirmDialog } from "@open-mercato/ui/backend/confirm-dialog";
|
|
19
|
+
import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
|
|
20
|
+
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
21
|
+
import { Copy, Info, RefreshCw, Trash2 } from "lucide-react";
|
|
22
|
+
const API_PATH = "/api/configs/module-telemetry";
|
|
23
|
+
const HOUR_MS = 60 * 60 * 1e3;
|
|
24
|
+
const REASON_LABEL_KEYS = {
|
|
25
|
+
p95_duration: "configs.moduleTelemetry.reason.p95Duration",
|
|
26
|
+
cpu: "configs.moduleTelemetry.reason.cpu",
|
|
27
|
+
heap_allocations: "configs.moduleTelemetry.reason.heapAllocations",
|
|
28
|
+
rss_growth: "configs.moduleTelemetry.reason.rssGrowth",
|
|
29
|
+
errors: "configs.moduleTelemetry.reason.errors"
|
|
30
|
+
};
|
|
31
|
+
const REASON_FALLBACKS = {
|
|
32
|
+
p95_duration: "Slow p95",
|
|
33
|
+
cpu: "CPU",
|
|
34
|
+
heap_allocations: "Heap",
|
|
35
|
+
rss_growth: "RSS growth",
|
|
36
|
+
errors: "Errors"
|
|
37
|
+
};
|
|
38
|
+
const USAGE_RANGE_PRESETS = ["today", "last_1h", "last_6h", "last_24h", "all"];
|
|
39
|
+
function formatMs(ms) {
|
|
40
|
+
if (!Number.isFinite(ms) || ms <= 0) return "0 ms";
|
|
41
|
+
if (ms >= 1e3) return `${(ms / 1e3).toFixed(ms >= 1e4 ? 1 : 2)} s`;
|
|
42
|
+
return `${Math.round(ms)} ms`;
|
|
43
|
+
}
|
|
44
|
+
function formatCount(value) {
|
|
45
|
+
return new Intl.NumberFormat().format(value);
|
|
46
|
+
}
|
|
47
|
+
function formatBucketInterval(ms) {
|
|
48
|
+
if (!ms || !Number.isFinite(ms)) return "interval";
|
|
49
|
+
if (ms >= 60 * 60 * 1e3) {
|
|
50
|
+
const hours = Math.round(ms / (60 * 60 * 1e3));
|
|
51
|
+
return `${hours}h`;
|
|
52
|
+
}
|
|
53
|
+
const minutes = Math.round(ms / (60 * 1e3));
|
|
54
|
+
return `${minutes}m`;
|
|
55
|
+
}
|
|
56
|
+
function bucketIntervalLabel(buckets, fallbackMs, translate) {
|
|
57
|
+
const intervals = new Set(
|
|
58
|
+
buckets.map((bucket) => bucket.bucketIntervalMs).filter((value) => Number.isFinite(value) && value > 0)
|
|
59
|
+
);
|
|
60
|
+
if (intervals.size === 1) return formatBucketInterval(Array.from(intervals)[0]);
|
|
61
|
+
return formatBucketInterval(fallbackMs);
|
|
62
|
+
}
|
|
63
|
+
function bucketCountLabel(buckets, fallbackMs, translate) {
|
|
64
|
+
const intervals = new Set(
|
|
65
|
+
buckets.map((bucket) => bucket.bucketIntervalMs).filter((value) => Number.isFinite(value) && value > 0)
|
|
66
|
+
);
|
|
67
|
+
if (intervals.size > 1) {
|
|
68
|
+
return translate(
|
|
69
|
+
"configs.moduleTelemetry.overview.mixedBucketCount",
|
|
70
|
+
"{{count}} buckets \xB7 mixed intervals",
|
|
71
|
+
{ count: buckets.length }
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return translate(
|
|
75
|
+
"configs.moduleTelemetry.overview.bucketCount",
|
|
76
|
+
"{{count}} {{bucketWord}} ({{interval}} interval)",
|
|
77
|
+
{
|
|
78
|
+
count: buckets.length,
|
|
79
|
+
bucketWord: buckets.length === 1 ? translate("configs.moduleTelemetry.overview.bucketSingular", "bucket") : translate("configs.moduleTelemetry.overview.bucketPlural", "buckets"),
|
|
80
|
+
interval: bucketIntervalLabel(buckets, fallbackMs, translate)
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
function formatRangeBoundary(ms) {
|
|
85
|
+
return new Date(ms).toLocaleString(void 0, {
|
|
86
|
+
month: "short",
|
|
87
|
+
day: "numeric",
|
|
88
|
+
hour: "2-digit",
|
|
89
|
+
minute: "2-digit"
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function MetricTitle({
|
|
93
|
+
label,
|
|
94
|
+
tooltip
|
|
95
|
+
}) {
|
|
96
|
+
return /* @__PURE__ */ jsxs("div", { className: "inline-flex min-w-0 items-center gap-1.5 text-xs uppercase tracking-wide text-muted-foreground", children: [
|
|
97
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: label }),
|
|
98
|
+
/* @__PURE__ */ jsx(SimpleTooltip, { content: tooltip, side: "top", align: "center", variant: "light", size: "lg", children: /* @__PURE__ */ jsx(
|
|
99
|
+
"span",
|
|
100
|
+
{
|
|
101
|
+
className: "inline-flex size-4 shrink-0 items-center justify-center rounded-full text-muted-foreground hover:text-foreground",
|
|
102
|
+
"aria-label": tooltip,
|
|
103
|
+
children: /* @__PURE__ */ jsx(Info, { className: "h-3.5 w-3.5", "aria-hidden": "true" })
|
|
104
|
+
}
|
|
105
|
+
) })
|
|
106
|
+
] });
|
|
107
|
+
}
|
|
108
|
+
function MetricCard({
|
|
109
|
+
label,
|
|
110
|
+
tooltip,
|
|
111
|
+
value,
|
|
112
|
+
subValue
|
|
113
|
+
}) {
|
|
114
|
+
return /* @__PURE__ */ jsxs("div", { className: "rounded-lg border bg-card p-4", children: [
|
|
115
|
+
/* @__PURE__ */ jsx(MetricTitle, { label, tooltip }),
|
|
116
|
+
/* @__PURE__ */ jsx("div", { className: "mt-2 text-2xl font-semibold", children: value }),
|
|
117
|
+
subValue ? /* @__PURE__ */ jsx("div", { className: "mt-1 text-xs text-muted-foreground", children: subValue }) : null
|
|
118
|
+
] });
|
|
119
|
+
}
|
|
120
|
+
function firstBucketStartMs(buckets) {
|
|
121
|
+
const starts = buckets.map((bucket) => Date.parse(bucket.bucketStart)).filter((value) => Number.isFinite(value));
|
|
122
|
+
return starts.length ? Math.min(...starts) : null;
|
|
123
|
+
}
|
|
124
|
+
function firstBucketEndMs(buckets) {
|
|
125
|
+
const ends = buckets.map((bucket) => Date.parse(bucket.bucketEnd)).filter((value) => Number.isFinite(value));
|
|
126
|
+
return ends.length ? Math.min(...ends) : null;
|
|
127
|
+
}
|
|
128
|
+
function firstAvailableTelemetryMs(buckets, startedAtMs, nowMs) {
|
|
129
|
+
const firstBucketMs = firstBucketStartMs(buckets);
|
|
130
|
+
if (firstBucketMs === null) return Number.isFinite(startedAtMs) ? startedAtMs : nowMs;
|
|
131
|
+
if (!Number.isFinite(startedAtMs)) return firstBucketMs;
|
|
132
|
+
const firstBucketEnd = firstBucketEndMs(buckets);
|
|
133
|
+
if (firstBucketEnd !== null && firstBucketEnd > startedAtMs) return startedAtMs;
|
|
134
|
+
return firstBucketMs;
|
|
135
|
+
}
|
|
136
|
+
function startOfTodayMs(nowMs) {
|
|
137
|
+
const date = new Date(nowMs);
|
|
138
|
+
date.setHours(0, 0, 0, 0);
|
|
139
|
+
return date.getTime();
|
|
140
|
+
}
|
|
141
|
+
function resolveUsageRange(preset, buckets, startedAt, nowMs = Date.now()) {
|
|
142
|
+
const startedAtMs = Date.parse(startedAt);
|
|
143
|
+
const availableStartMs = firstAvailableTelemetryMs(buckets, startedAtMs, nowMs);
|
|
144
|
+
const rawStartMs = preset === "today" ? startOfTodayMs(nowMs) : preset === "last_1h" ? nowMs - HOUR_MS : preset === "last_6h" ? nowMs - 6 * HOUR_MS : preset === "last_24h" ? nowMs - 24 * HOUR_MS : availableStartMs;
|
|
145
|
+
const boundedStartMs = preset === "all" ? rawStartMs : Math.max(rawStartMs, availableStartMs);
|
|
146
|
+
return {
|
|
147
|
+
preset,
|
|
148
|
+
startMs: Math.min(boundedStartMs, nowMs),
|
|
149
|
+
endMs: nowMs
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function bucketsInRange(buckets, range) {
|
|
153
|
+
return buckets.filter((bucket) => {
|
|
154
|
+
const bucketStartMs = Date.parse(bucket.bucketStart);
|
|
155
|
+
const bucketEndMs = Date.parse(bucket.bucketEnd);
|
|
156
|
+
if (!Number.isFinite(bucketStartMs) || !Number.isFinite(bucketEndMs)) return false;
|
|
157
|
+
return bucketEndMs > range.startMs && bucketStartMs <= range.endMs;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function activeRangeHours(buckets, bucketIntervalMs) {
|
|
161
|
+
const fallbackIntervalMs = bucketIntervalMs && Number.isFinite(bucketIntervalMs) && bucketIntervalMs > 0 ? bucketIntervalMs : HOUR_MS;
|
|
162
|
+
if (!buckets.length) return fallbackIntervalMs / HOUR_MS;
|
|
163
|
+
const totalMs = buckets.reduce((sum, bucket) => {
|
|
164
|
+
const interval = Number.isFinite(bucket.bucketIntervalMs) && bucket.bucketIntervalMs > 0 ? bucket.bucketIntervalMs : fallbackIntervalMs;
|
|
165
|
+
return sum + interval;
|
|
166
|
+
}, 0);
|
|
167
|
+
return Math.max(fallbackIntervalMs / HOUR_MS, totalMs / HOUR_MS);
|
|
168
|
+
}
|
|
169
|
+
function formatActiveDuration(hours) {
|
|
170
|
+
if (!Number.isFinite(hours) || hours <= 0) return "0m";
|
|
171
|
+
if (hours < 1) return `${Math.max(Math.round(hours * 60), 1)}m`;
|
|
172
|
+
if (hours < 10) return `${hours.toFixed(1)}h`;
|
|
173
|
+
return `${Math.round(hours)}h`;
|
|
174
|
+
}
|
|
175
|
+
function usageRangeLabel(preset, translate) {
|
|
176
|
+
if (preset === "today") return translate("configs.moduleTelemetry.range.today", "Today");
|
|
177
|
+
if (preset === "last_1h") return translate("configs.moduleTelemetry.range.last1h", "Last hour");
|
|
178
|
+
if (preset === "last_6h") return translate("configs.moduleTelemetry.range.last6h", "Last 6 hours");
|
|
179
|
+
if (preset === "last_24h") return translate("configs.moduleTelemetry.range.last24h", "Last 24 hours");
|
|
180
|
+
return translate("configs.moduleTelemetry.range.all", "All available");
|
|
181
|
+
}
|
|
182
|
+
function isUsageRangePreset(value) {
|
|
183
|
+
return !!value && USAGE_RANGE_PRESETS.includes(value);
|
|
184
|
+
}
|
|
185
|
+
function readUsageRangePreset(searchParams) {
|
|
186
|
+
const value = searchParams?.get("range") ?? null;
|
|
187
|
+
return isUsageRangePreset(value) ? value : "today";
|
|
188
|
+
}
|
|
189
|
+
function usageStageLabel(stage, translate) {
|
|
190
|
+
if (stage === "startup") return translate("configs.moduleTelemetry.stage.startup", "Startup");
|
|
191
|
+
return translate("configs.moduleTelemetry.stage.running", "Running");
|
|
192
|
+
}
|
|
193
|
+
function sortStageRows(a, b) {
|
|
194
|
+
if (a.stage !== b.stage) return a.stage === "startup" ? -1 : 1;
|
|
195
|
+
return b.totalCpuMs - a.totalCpuMs;
|
|
196
|
+
}
|
|
197
|
+
function groupRangeModules(modules) {
|
|
198
|
+
const groups = /* @__PURE__ */ new Map();
|
|
199
|
+
for (const module of modules) {
|
|
200
|
+
const group = groups.get(module.moduleId) ?? [];
|
|
201
|
+
group.push(module);
|
|
202
|
+
groups.set(module.moduleId, group);
|
|
203
|
+
}
|
|
204
|
+
return Array.from(groups.entries()).map(([moduleId, stages]) => {
|
|
205
|
+
const candidateReasons = Array.from(new Set(stages.flatMap((stage) => stage.candidateReasons)));
|
|
206
|
+
return {
|
|
207
|
+
moduleId,
|
|
208
|
+
stages: [...stages].sort(sortStageRows),
|
|
209
|
+
candidateReasons,
|
|
210
|
+
calls: stages.reduce((sum, stage) => sum + stage.calls, 0),
|
|
211
|
+
totalCpuMs: stages.reduce((sum, stage) => sum + stage.totalCpuMs, 0),
|
|
212
|
+
positiveRssDeltaBytes: stages.reduce((sum, stage) => sum + stage.positiveRssDeltaBytes, 0)
|
|
213
|
+
};
|
|
214
|
+
}).sort(
|
|
215
|
+
(a, b) => b.candidateReasons.length - a.candidateReasons.length || b.totalCpuMs - a.totalCpuMs || b.positiveRssDeltaBytes - a.positiveRssDeltaBytes || b.calls - a.calls || a.moduleId.localeCompare(b.moduleId)
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
function buildCandidateReasonsForModule(module, thresholds, bucketCount) {
|
|
219
|
+
const divisor = Math.max(1, bucketCount);
|
|
220
|
+
const reasons = [];
|
|
221
|
+
if (module.p95DurationMs >= thresholds.p95DurationMs) reasons.push("p95_duration");
|
|
222
|
+
if (module.totalCpuMs / divisor >= thresholds.cpuMs) reasons.push("cpu");
|
|
223
|
+
if (module.positiveHeapDeltaBytes / divisor >= thresholds.positiveHeapDeltaBytes) reasons.push("heap_allocations");
|
|
224
|
+
if (module.positiveRssDeltaBytes / divisor >= thresholds.positiveRssDeltaBytes) reasons.push("rss_growth");
|
|
225
|
+
if (module.errors / divisor >= thresholds.errors) reasons.push("errors");
|
|
226
|
+
return reasons;
|
|
227
|
+
}
|
|
228
|
+
function mergeRangeOperations(modules) {
|
|
229
|
+
const operations = /* @__PURE__ */ new Map();
|
|
230
|
+
for (const module of modules) {
|
|
231
|
+
for (const operation of module.topOperations) {
|
|
232
|
+
const key = `${operation.surface}\0${operation.operation}\0${operation.resourceId ?? ""}`;
|
|
233
|
+
const existing = operations.get(key);
|
|
234
|
+
if (!existing) {
|
|
235
|
+
operations.set(key, { ...operation });
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
existing.calls += operation.calls;
|
|
239
|
+
existing.errors += operation.errors;
|
|
240
|
+
existing.totalDurationMs += operation.totalDurationMs;
|
|
241
|
+
existing.maxDurationMs = Math.max(existing.maxDurationMs, operation.maxDurationMs);
|
|
242
|
+
existing.p95DurationMs = Math.max(existing.p95DurationMs, operation.p95DurationMs);
|
|
243
|
+
existing.totalCpuUserMs += operation.totalCpuUserMs;
|
|
244
|
+
existing.totalCpuSystemMs += operation.totalCpuSystemMs;
|
|
245
|
+
existing.maxCpuMs = Math.max(existing.maxCpuMs, operation.maxCpuMs);
|
|
246
|
+
existing.totalHeapDeltaBytes += operation.totalHeapDeltaBytes;
|
|
247
|
+
existing.positiveHeapDeltaBytes += operation.positiveHeapDeltaBytes;
|
|
248
|
+
existing.maxHeapDeltaBytes = Math.max(existing.maxHeapDeltaBytes, operation.maxHeapDeltaBytes);
|
|
249
|
+
existing.totalRssDeltaBytes += operation.totalRssDeltaBytes;
|
|
250
|
+
existing.positiveRssDeltaBytes += operation.positiveRssDeltaBytes;
|
|
251
|
+
existing.maxRssDeltaBytes = Math.max(existing.maxRssDeltaBytes, operation.maxRssDeltaBytes);
|
|
252
|
+
existing.firstSeenAt = existing.firstSeenAt < operation.firstSeenAt ? existing.firstSeenAt : operation.firstSeenAt;
|
|
253
|
+
existing.lastSeenAt = existing.lastSeenAt > operation.lastSeenAt ? existing.lastSeenAt : operation.lastSeenAt;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return Array.from(operations.values()).sort(
|
|
257
|
+
(a, b) => b.totalCpuUserMs + b.totalCpuSystemMs - (a.totalCpuUserMs + a.totalCpuSystemMs) || b.totalDurationMs - a.totalDurationMs || b.calls - a.calls
|
|
258
|
+
).slice(0, 5);
|
|
259
|
+
}
|
|
260
|
+
function mergeRangeSurfaces(modules) {
|
|
261
|
+
const surfaces = /* @__PURE__ */ new Map();
|
|
262
|
+
for (const module of modules) {
|
|
263
|
+
for (const surface of module.surfaces) {
|
|
264
|
+
const existing = surfaces.get(surface.surface);
|
|
265
|
+
if (!existing) {
|
|
266
|
+
surfaces.set(surface.surface, { ...surface });
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
existing.calls += surface.calls;
|
|
270
|
+
existing.errors += surface.errors;
|
|
271
|
+
existing.totalDurationMs += surface.totalDurationMs;
|
|
272
|
+
existing.p95DurationMs = Math.max(existing.p95DurationMs, surface.p95DurationMs);
|
|
273
|
+
existing.totalCpuMs += surface.totalCpuMs;
|
|
274
|
+
existing.positiveHeapDeltaBytes += surface.positiveHeapDeltaBytes;
|
|
275
|
+
existing.positiveRssDeltaBytes += surface.positiveRssDeltaBytes;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return Array.from(surfaces.values()).sort((a, b) => b.totalCpuMs - a.totalCpuMs || b.totalDurationMs - a.totalDurationMs);
|
|
279
|
+
}
|
|
280
|
+
function aggregateRangeModules(buckets, thresholds) {
|
|
281
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
282
|
+
for (const bucket of buckets) {
|
|
283
|
+
for (const module of bucket.modules) {
|
|
284
|
+
const key = `${module.moduleId}\0${bucket.stage}`;
|
|
285
|
+
const group = grouped.get(key) ?? { moduleId: module.moduleId, stage: bucket.stage, modules: [], bucketKeys: /* @__PURE__ */ new Set() };
|
|
286
|
+
group.modules.push(module);
|
|
287
|
+
group.bucketKeys.add(bucket.bucketStart);
|
|
288
|
+
grouped.set(key, group);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return Array.from(grouped.values()).map(({ moduleId, stage, modules, bucketKeys }) => {
|
|
292
|
+
const summaryBase = {
|
|
293
|
+
moduleId,
|
|
294
|
+
stage,
|
|
295
|
+
calls: modules.reduce((sum, module) => sum + module.calls, 0),
|
|
296
|
+
errors: modules.reduce((sum, module) => sum + module.errors, 0),
|
|
297
|
+
totalDurationMs: modules.reduce((sum, module) => sum + module.totalDurationMs, 0),
|
|
298
|
+
p95DurationMs: Math.max(...modules.map((module) => module.p95DurationMs), 0),
|
|
299
|
+
totalCpuMs: modules.reduce((sum, module) => sum + module.totalCpuMs, 0),
|
|
300
|
+
positiveHeapDeltaBytes: modules.reduce((sum, module) => sum + module.positiveHeapDeltaBytes, 0),
|
|
301
|
+
positiveRssDeltaBytes: modules.reduce((sum, module) => sum + module.positiveRssDeltaBytes, 0),
|
|
302
|
+
surfaces: mergeRangeSurfaces(modules),
|
|
303
|
+
topOperations: mergeRangeOperations(modules)
|
|
304
|
+
};
|
|
305
|
+
return {
|
|
306
|
+
...summaryBase,
|
|
307
|
+
candidateReasons: buildCandidateReasonsForModule(summaryBase, thresholds, bucketKeys.size)
|
|
308
|
+
};
|
|
309
|
+
}).sort(
|
|
310
|
+
(a, b) => b.candidateReasons.length - a.candidateReasons.length || b.totalCpuMs - a.totalCpuMs || b.positiveRssDeltaBytes - a.positiveRssDeltaBytes || b.calls - a.calls
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
function RangeOverview({
|
|
314
|
+
buckets,
|
|
315
|
+
modules,
|
|
316
|
+
range,
|
|
317
|
+
rangePreset,
|
|
318
|
+
onRangePresetChange,
|
|
319
|
+
bucketIntervalMs,
|
|
320
|
+
translate
|
|
321
|
+
}) {
|
|
322
|
+
const selectedHours = activeRangeHours(buckets, bucketIntervalMs);
|
|
323
|
+
const activeDurationLabel = formatActiveDuration(selectedHours);
|
|
324
|
+
const bucketsLabel = bucketCountLabel(buckets, bucketIntervalMs, translate);
|
|
325
|
+
const totalHeapGrowthBytes = modules.reduce((sum, module) => sum + module.positiveHeapDeltaBytes, 0);
|
|
326
|
+
const totalRssGrowthBytes = modules.reduce((sum, module) => sum + module.positiveRssDeltaBytes, 0);
|
|
327
|
+
const totals = {
|
|
328
|
+
modules: new Set(modules.map((module) => module.moduleId)).size,
|
|
329
|
+
calls: modules.reduce((sum, module) => sum + module.calls, 0),
|
|
330
|
+
totalCpuMs: modules.reduce((sum, module) => sum + module.totalCpuMs, 0),
|
|
331
|
+
avgHeapGrowthPerHour: totalHeapGrowthBytes / selectedHours,
|
|
332
|
+
avgRssGrowthPerHour: totalRssGrowthBytes / selectedHours
|
|
333
|
+
};
|
|
334
|
+
const growthTooltip = translate(
|
|
335
|
+
"configs.moduleTelemetry.overview.growthTooltip",
|
|
336
|
+
"Average positive growth per hour of measured activity in the selected range: total growth divided by the hours actually covered by tracked buckets, not the full wall-clock span. Idle stretches with no tracked calls do not dilute this rate, so it stays comparable across range presets. This is allocation pressure over time, not the current live process memory."
|
|
337
|
+
);
|
|
338
|
+
const heapSubValue = translate(
|
|
339
|
+
"configs.moduleTelemetry.overview.growthSubValue",
|
|
340
|
+
"{{total}} total over {{duration}}",
|
|
341
|
+
{ total: formatBytes(totalHeapGrowthBytes), duration: activeDurationLabel }
|
|
342
|
+
);
|
|
343
|
+
const rssSubValue = translate(
|
|
344
|
+
"configs.moduleTelemetry.overview.growthSubValue",
|
|
345
|
+
"{{total}} total over {{duration}}",
|
|
346
|
+
{ total: formatBytes(totalRssGrowthBytes), duration: activeDurationLabel }
|
|
347
|
+
);
|
|
348
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-4 rounded-lg border bg-card p-4", children: [
|
|
349
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between", children: [
|
|
350
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
351
|
+
/* @__PURE__ */ jsx("h3", { className: "text-base font-semibold", children: translate("configs.moduleTelemetry.overview.title", "Usage overview") }),
|
|
352
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: translate(
|
|
353
|
+
"configs.moduleTelemetry.overview.description",
|
|
354
|
+
"From {{from}} to now \xB7 {{buckets}}",
|
|
355
|
+
{
|
|
356
|
+
from: formatRangeBoundary(range.startMs),
|
|
357
|
+
buckets: bucketsLabel
|
|
358
|
+
}
|
|
359
|
+
) })
|
|
360
|
+
] }),
|
|
361
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2 sm:flex-row", children: /* @__PURE__ */ jsx(
|
|
362
|
+
SegmentedControl,
|
|
363
|
+
{
|
|
364
|
+
value: rangePreset,
|
|
365
|
+
onValueChange: (value) => onRangePresetChange(value),
|
|
366
|
+
"aria-label": translate("configs.moduleTelemetry.range.label", "Time range"),
|
|
367
|
+
size: "sm",
|
|
368
|
+
className: "max-w-full flex-wrap overflow-x-auto rounded-lg",
|
|
369
|
+
children: USAGE_RANGE_PRESETS.map((option) => /* @__PURE__ */ jsx(SegmentedControlItem, { value: option, children: usageRangeLabel(option, translate) }, option))
|
|
370
|
+
}
|
|
371
|
+
) })
|
|
372
|
+
] }),
|
|
373
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-2 xl:grid-cols-5", children: [
|
|
374
|
+
/* @__PURE__ */ jsx(
|
|
375
|
+
MetricCard,
|
|
376
|
+
{
|
|
377
|
+
label: translate("configs.moduleTelemetry.overview.modules", "Modules"),
|
|
378
|
+
tooltip: translate("configs.moduleTelemetry.overview.modulesTooltip", "Unique modules with tracked activity in the selected range. Startup and running are shown as separate rows inside each module group."),
|
|
379
|
+
value: formatCount(totals.modules)
|
|
380
|
+
}
|
|
381
|
+
),
|
|
382
|
+
/* @__PURE__ */ jsx(
|
|
383
|
+
MetricCard,
|
|
384
|
+
{
|
|
385
|
+
label: translate("configs.moduleTelemetry.overview.calls", "Calls"),
|
|
386
|
+
tooltip: translate("configs.moduleTelemetry.overview.callsTooltip", "Total tracked operation calls in the selected range."),
|
|
387
|
+
value: formatCount(totals.calls)
|
|
388
|
+
}
|
|
389
|
+
),
|
|
390
|
+
/* @__PURE__ */ jsx(
|
|
391
|
+
MetricCard,
|
|
392
|
+
{
|
|
393
|
+
label: translate("configs.moduleTelemetry.overview.cpu", "CPU"),
|
|
394
|
+
tooltip: translate("configs.moduleTelemetry.overview.cpuTooltip", "Total CPU time attributed to tracked module operations in the selected range."),
|
|
395
|
+
value: formatMs(totals.totalCpuMs)
|
|
396
|
+
}
|
|
397
|
+
),
|
|
398
|
+
/* @__PURE__ */ jsx(
|
|
399
|
+
MetricCard,
|
|
400
|
+
{
|
|
401
|
+
label: translate("configs.moduleTelemetry.overview.heapPerHour", "Heap / hour"),
|
|
402
|
+
tooltip: growthTooltip,
|
|
403
|
+
value: formatBytes(totals.avgHeapGrowthPerHour),
|
|
404
|
+
subValue: heapSubValue
|
|
405
|
+
}
|
|
406
|
+
),
|
|
407
|
+
/* @__PURE__ */ jsx(
|
|
408
|
+
MetricCard,
|
|
409
|
+
{
|
|
410
|
+
label: translate("configs.moduleTelemetry.overview.rssPerHour", "RSS / hour"),
|
|
411
|
+
tooltip: growthTooltip,
|
|
412
|
+
value: formatBytes(totals.avgRssGrowthPerHour),
|
|
413
|
+
subValue: rssSubValue
|
|
414
|
+
}
|
|
415
|
+
)
|
|
416
|
+
] })
|
|
417
|
+
] });
|
|
418
|
+
}
|
|
419
|
+
function CandidateBadges({
|
|
420
|
+
module,
|
|
421
|
+
translate
|
|
422
|
+
}) {
|
|
423
|
+
if (!module.candidateReasons.length) {
|
|
424
|
+
return /* @__PURE__ */ jsx(StatusBadge, { variant: "neutral", dot: true, children: translate("configs.moduleTelemetry.table.normal", "Normal") });
|
|
425
|
+
}
|
|
426
|
+
return /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", children: module.candidateReasons.map((reason) => /* @__PURE__ */ jsx(StatusBadge, { variant: "warning", dot: true, children: translate(REASON_LABEL_KEYS[reason] ?? `configs.moduleTelemetry.reason.${reason}`, REASON_FALLBACKS[reason] ?? reason) }, reason)) });
|
|
427
|
+
}
|
|
428
|
+
function StageBadge({
|
|
429
|
+
stage,
|
|
430
|
+
translate
|
|
431
|
+
}) {
|
|
432
|
+
return /* @__PURE__ */ jsx(StatusBadge, { variant: stage === "startup" ? "info" : "neutral", dot: true, children: usageStageLabel(stage, translate) });
|
|
433
|
+
}
|
|
434
|
+
function copyRowStats(module, translate) {
|
|
435
|
+
const json = JSON.stringify(module, null, 2);
|
|
436
|
+
navigator.clipboard.writeText(json).then(() => {
|
|
437
|
+
flash(translate("configs.moduleTelemetry.table.copySuccess", "Row stats copied to clipboard."), "success");
|
|
438
|
+
}).catch((err) => {
|
|
439
|
+
console.warn("[ModuleTelemetryPanel] clipboard write failed", err);
|
|
440
|
+
flash(translate("configs.moduleTelemetry.table.copyError", "Failed to copy row stats."), "error");
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
function TopOperationsBreakdown({
|
|
444
|
+
operations,
|
|
445
|
+
translate
|
|
446
|
+
}) {
|
|
447
|
+
return /* @__PURE__ */ jsxs("div", { className: "w-[26rem] max-w-[80vw] space-y-2", children: [
|
|
448
|
+
/* @__PURE__ */ jsx("div", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: translate("configs.moduleTelemetry.table.topOperationsBreakdown", "Top operations in this range") }),
|
|
449
|
+
/* @__PURE__ */ jsxs("table", { className: "w-full text-xs", children: [
|
|
450
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: "text-muted-foreground", children: [
|
|
451
|
+
/* @__PURE__ */ jsx("th", { className: "pb-1 pr-2 text-left font-medium", children: translate("configs.moduleTelemetry.table.topOperation", "Top operation") }),
|
|
452
|
+
/* @__PURE__ */ jsx("th", { className: "pb-1 pr-2 text-right font-medium", children: translate("configs.moduleTelemetry.table.calls", "Calls") }),
|
|
453
|
+
/* @__PURE__ */ jsx("th", { className: "pb-1 pr-2 text-right font-medium", children: translate("configs.moduleTelemetry.table.worstP95", "Worst p95") }),
|
|
454
|
+
/* @__PURE__ */ jsx("th", { className: "pb-1 pr-2 text-right font-medium", children: translate("configs.moduleTelemetry.table.cpu", "CPU") }),
|
|
455
|
+
/* @__PURE__ */ jsx("th", { className: "pb-1 pr-2 text-right font-medium", children: translate("configs.moduleTelemetry.table.heap", "Heap growth") }),
|
|
456
|
+
/* @__PURE__ */ jsx("th", { className: "pb-1 text-right font-medium", children: translate("configs.moduleTelemetry.table.rss", "RSS growth") })
|
|
457
|
+
] }) }),
|
|
458
|
+
/* @__PURE__ */ jsx("tbody", { children: operations.map((operation, index) => /* @__PURE__ */ jsxs("tr", { className: "border-t border-border/50", children: [
|
|
459
|
+
/* @__PURE__ */ jsxs("td", { className: "py-1 pr-2 align-top", children: [
|
|
460
|
+
/* @__PURE__ */ jsx("div", { className: "font-medium text-foreground", children: operation.operation }),
|
|
461
|
+
/* @__PURE__ */ jsxs("div", { className: "text-muted-foreground", children: [
|
|
462
|
+
operation.surface,
|
|
463
|
+
operation.errors > 0 ? ` \xB7 ${translate("configs.moduleTelemetry.table.errorCount", "{{count}} errors", { count: operation.errors })}` : ""
|
|
464
|
+
] }),
|
|
465
|
+
operation.concurrentCalls > 0 ? /* @__PURE__ */ jsx("div", { className: "text-muted-foreground", children: translate(
|
|
466
|
+
"configs.moduleTelemetry.table.concurrentOverlap",
|
|
467
|
+
"{{percent}}% overlapped other calls \u2014 CPU/heap/RSS less certain",
|
|
468
|
+
{ percent: Math.round(operation.concurrentCalls / Math.max(operation.calls, 1) * 100) }
|
|
469
|
+
) }) : null
|
|
470
|
+
] }),
|
|
471
|
+
/* @__PURE__ */ jsx("td", { className: "py-1 pr-2 text-right align-top", children: formatCount(operation.calls) }),
|
|
472
|
+
/* @__PURE__ */ jsx("td", { className: "py-1 pr-2 text-right align-top", children: formatMs(operation.p95DurationMs) }),
|
|
473
|
+
/* @__PURE__ */ jsx("td", { className: "py-1 pr-2 text-right align-top", children: formatMs(operation.totalCpuUserMs + operation.totalCpuSystemMs) }),
|
|
474
|
+
/* @__PURE__ */ jsx("td", { className: "py-1 pr-2 text-right align-top", children: formatBytes(operation.positiveHeapDeltaBytes) }),
|
|
475
|
+
/* @__PURE__ */ jsx("td", { className: "py-1 text-right align-top", children: formatBytes(operation.positiveRssDeltaBytes) })
|
|
476
|
+
] }, `${operation.surface}\0${operation.operation}\0${index}`)) })
|
|
477
|
+
] })
|
|
478
|
+
] });
|
|
479
|
+
}
|
|
480
|
+
function RangeModuleTable({
|
|
481
|
+
modules,
|
|
482
|
+
translate
|
|
483
|
+
}) {
|
|
484
|
+
if (!modules.length) {
|
|
485
|
+
return /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: translate("configs.moduleTelemetry.modules.empty", "No module activity has been recorded for this range yet.") });
|
|
486
|
+
}
|
|
487
|
+
const groups = groupRangeModules(modules);
|
|
488
|
+
return /* @__PURE__ */ jsx(TooltipProvider, { delayDuration: 200, children: /* @__PURE__ */ jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxs("table", { className: "w-full min-w-[980px] text-sm", children: [
|
|
489
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: [
|
|
490
|
+
/* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-left", children: translate("configs.moduleTelemetry.table.module", "Module") }),
|
|
491
|
+
/* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-left", children: translate("configs.moduleTelemetry.table.stage", "Stage") }),
|
|
492
|
+
/* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-left", children: /* @__PURE__ */ jsx(
|
|
493
|
+
MetricTitle,
|
|
494
|
+
{
|
|
495
|
+
label: translate("configs.moduleTelemetry.table.signals", "Signals"),
|
|
496
|
+
tooltip: translate(
|
|
497
|
+
"configs.moduleTelemetry.table.signalsTooltip",
|
|
498
|
+
"Flags a module whose CPU, heap growth, RSS growth, or errors average at or above a heavy-usage threshold per 5-minute bucket it was active in during this range. This is a per-bucket average, not a raw range-wide total, so the same sustained workload looks the same regardless of the selected time range."
|
|
499
|
+
)
|
|
500
|
+
}
|
|
501
|
+
) }),
|
|
502
|
+
/* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-right", children: translate("configs.moduleTelemetry.table.calls", "Calls") }),
|
|
503
|
+
/* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-right", children: translate("configs.moduleTelemetry.table.worstP95", "Worst p95") }),
|
|
504
|
+
/* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-right", children: translate("configs.moduleTelemetry.table.cpu", "CPU") }),
|
|
505
|
+
/* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-right", children: translate("configs.moduleTelemetry.table.heap", "Heap growth") }),
|
|
506
|
+
/* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-right", children: translate("configs.moduleTelemetry.table.rss", "RSS growth") }),
|
|
507
|
+
/* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-left", children: translate("configs.moduleTelemetry.table.topOperation", "Top operation") }),
|
|
508
|
+
/* @__PURE__ */ jsx("th", { className: "px-3 py-2 text-right", children: translate("configs.moduleTelemetry.table.actions", "Actions") })
|
|
509
|
+
] }) }),
|
|
510
|
+
/* @__PURE__ */ jsx("tbody", { children: groups.map((group) => /* @__PURE__ */ jsx(React.Fragment, { children: group.stages.map((module, stageIndex) => {
|
|
511
|
+
const topOperation = module.topOperations[0];
|
|
512
|
+
const topSurface = module.surfaces[0];
|
|
513
|
+
return /* @__PURE__ */ jsxs(
|
|
514
|
+
"tr",
|
|
515
|
+
{
|
|
516
|
+
className: stageIndex === 0 ? "border-t" : "border-t border-border/50",
|
|
517
|
+
children: [
|
|
518
|
+
stageIndex === 0 ? /* @__PURE__ */ jsx("td", { className: "w-56 px-3 py-3 align-top", rowSpan: group.stages.length, children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
519
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium", children: group.moduleId }),
|
|
520
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: translate(
|
|
521
|
+
"configs.moduleTelemetry.table.stageCount",
|
|
522
|
+
"{{count}} stage rows",
|
|
523
|
+
{ count: group.stages.length }
|
|
524
|
+
) })
|
|
525
|
+
] }) }) : null,
|
|
526
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-3 align-top", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
527
|
+
/* @__PURE__ */ jsx(StageBadge, { stage: module.stage, translate }),
|
|
528
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: topSurface ? translate(
|
|
529
|
+
"configs.moduleTelemetry.table.surfaceSummary",
|
|
530
|
+
"{{surface}} \xB7 {{calls}} calls",
|
|
531
|
+
{ surface: topSurface.surface, calls: topSurface.calls }
|
|
532
|
+
) : translate("configs.moduleTelemetry.table.noSurface", "No surface data") })
|
|
533
|
+
] }) }),
|
|
534
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-3 align-top", children: /* @__PURE__ */ jsx(CandidateBadges, { module, translate }) }),
|
|
535
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-3 align-top text-right", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
536
|
+
/* @__PURE__ */ jsx("span", { children: formatCount(module.calls) }),
|
|
537
|
+
module.errors > 0 ? /* @__PURE__ */ jsx("span", { className: "text-xs text-destructive", children: translate("configs.moduleTelemetry.table.errorCount", "{{count}} errors", { count: module.errors }) }) : null
|
|
538
|
+
] }) }),
|
|
539
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-3 align-top text-right", children: formatMs(module.p95DurationMs) }),
|
|
540
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-3 align-top text-right", children: formatMs(module.totalCpuMs) }),
|
|
541
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-3 align-top text-right", children: formatBytes(module.positiveHeapDeltaBytes) }),
|
|
542
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-3 align-top text-right", children: formatBytes(module.positiveRssDeltaBytes) }),
|
|
543
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-3 align-top", children: topOperation ? /* @__PURE__ */ jsxs(Tooltip, { delayDuration: 200, children: [
|
|
544
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs("div", { className: "flex cursor-default flex-col gap-1", children: [
|
|
545
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium underline decoration-dotted decoration-muted-foreground underline-offset-2", children: topOperation.operation }),
|
|
546
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: translate(
|
|
547
|
+
"configs.moduleTelemetry.table.topOperationMeta",
|
|
548
|
+
"{{surface}} \xB7 {{cpu}} CPU \xB7 {{calls}} calls",
|
|
549
|
+
{
|
|
550
|
+
surface: topOperation.surface,
|
|
551
|
+
cpu: formatMs(topOperation.totalCpuUserMs + topOperation.totalCpuSystemMs),
|
|
552
|
+
calls: topOperation.calls
|
|
553
|
+
}
|
|
554
|
+
) })
|
|
555
|
+
] }) }),
|
|
556
|
+
/* @__PURE__ */ jsx(TooltipContent, { side: "left", align: "start", variant: "light", className: "max-w-none p-3", children: /* @__PURE__ */ jsx(TopOperationsBreakdown, { operations: module.topOperations, translate }) })
|
|
557
|
+
] }) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: translate("configs.moduleTelemetry.table.none", "None") }) }),
|
|
558
|
+
/* @__PURE__ */ jsx("td", { className: "px-3 py-3 align-top text-right", children: /* @__PURE__ */ jsx(
|
|
559
|
+
SimpleTooltip,
|
|
560
|
+
{
|
|
561
|
+
content: translate("configs.moduleTelemetry.table.copyStats", "Copy row stats for debugging"),
|
|
562
|
+
side: "top",
|
|
563
|
+
align: "center",
|
|
564
|
+
variant: "light",
|
|
565
|
+
children: /* @__PURE__ */ jsx(
|
|
566
|
+
IconButton,
|
|
567
|
+
{
|
|
568
|
+
type: "button",
|
|
569
|
+
variant: "ghost",
|
|
570
|
+
size: "sm",
|
|
571
|
+
"aria-label": translate("configs.moduleTelemetry.table.copyStats", "Copy row stats for debugging"),
|
|
572
|
+
onClick: () => copyRowStats(module, translate),
|
|
573
|
+
children: /* @__PURE__ */ jsx(Copy, { className: "h-4 w-4", "aria-hidden": "true" })
|
|
574
|
+
}
|
|
575
|
+
)
|
|
576
|
+
}
|
|
577
|
+
) })
|
|
578
|
+
]
|
|
579
|
+
},
|
|
580
|
+
`${module.moduleId}:${module.stage}`
|
|
581
|
+
);
|
|
582
|
+
}) }, group.moduleId)) })
|
|
583
|
+
] }) }) });
|
|
584
|
+
}
|
|
585
|
+
function RangeModuleSection({
|
|
586
|
+
modules,
|
|
587
|
+
range,
|
|
588
|
+
bucketIntervalMs,
|
|
589
|
+
bucketsLabel,
|
|
590
|
+
translate
|
|
591
|
+
}) {
|
|
592
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-4 rounded-lg border bg-card p-4", children: [
|
|
593
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
594
|
+
/* @__PURE__ */ jsx("h3", { className: "text-base font-semibold", children: translate("configs.moduleTelemetry.modules.title", "Modules") }),
|
|
595
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: translate(
|
|
596
|
+
"configs.moduleTelemetry.modules.description",
|
|
597
|
+
"Aggregated module stats from {{from}} to now across {{buckets}}. Startup and running rows are separated.",
|
|
598
|
+
{
|
|
599
|
+
from: formatRangeBoundary(range.startMs),
|
|
600
|
+
buckets: bucketsLabel
|
|
601
|
+
}
|
|
602
|
+
) }),
|
|
603
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: translate(
|
|
604
|
+
"configs.moduleTelemetry.modules.interval",
|
|
605
|
+
"Startup is the first {{interval}} bucket after telemetry starts. Later buckets are running.",
|
|
606
|
+
{ interval: formatBucketInterval(bucketIntervalMs) }
|
|
607
|
+
) })
|
|
608
|
+
] }),
|
|
609
|
+
/* @__PURE__ */ jsx(RangeModuleTable, { modules, translate })
|
|
610
|
+
] });
|
|
611
|
+
}
|
|
612
|
+
function ModuleTelemetryPanel() {
|
|
613
|
+
const t = useT();
|
|
614
|
+
const router = useRouter();
|
|
615
|
+
const pathname = usePathname();
|
|
616
|
+
const searchParams = useSearchParams();
|
|
617
|
+
const [state, setState] = React.useState({ loading: true, error: null, report: null });
|
|
618
|
+
const [clearingTelemetry, setClearingTelemetry] = React.useState(false);
|
|
619
|
+
const rangeParam = searchParams.get("range");
|
|
620
|
+
const [usageRangePreset, setUsageRangePreset] = React.useState(() => readUsageRangePreset(searchParams));
|
|
621
|
+
const { confirm, ConfirmDialogElement } = useConfirmDialog();
|
|
622
|
+
const { runMutation, retryLastMutation } = useGuardedMutation({
|
|
623
|
+
contextId: "configs-module-telemetry",
|
|
624
|
+
blockedMessage: t("ui.forms.flash.saveBlocked", "Save blocked by validation")
|
|
625
|
+
});
|
|
626
|
+
const clearMutationContext = React.useMemo(
|
|
627
|
+
() => ({
|
|
628
|
+
formId: "configs-module-telemetry",
|
|
629
|
+
resourceKind: "configs.moduleTelemetry",
|
|
630
|
+
retryLastMutation
|
|
631
|
+
}),
|
|
632
|
+
[retryLastMutation]
|
|
633
|
+
);
|
|
634
|
+
const loadReport = React.useCallback(async () => {
|
|
635
|
+
setState((current) => ({ ...current, loading: true, error: null }));
|
|
636
|
+
try {
|
|
637
|
+
const report2 = await readApiResultOrThrow(API_PATH, void 0, {
|
|
638
|
+
errorMessage: t("configs.moduleTelemetry.loadError", "Failed to load module telemetry.")
|
|
639
|
+
});
|
|
640
|
+
setState({ loading: false, error: null, report: report2 });
|
|
641
|
+
} catch (error) {
|
|
642
|
+
const message = error instanceof Error && error.message ? error.message : t("configs.moduleTelemetry.loadError", "Failed to load module telemetry.");
|
|
643
|
+
setState({ loading: false, error: message, report: null });
|
|
644
|
+
}
|
|
645
|
+
}, [t]);
|
|
646
|
+
React.useEffect(() => {
|
|
647
|
+
loadReport().catch(() => {
|
|
648
|
+
});
|
|
649
|
+
}, [loadReport]);
|
|
650
|
+
React.useEffect(() => {
|
|
651
|
+
const nextRange = readUsageRangePreset(searchParams);
|
|
652
|
+
setUsageRangePreset((current) => current === nextRange ? current : nextRange);
|
|
653
|
+
}, [rangeParam, searchParams]);
|
|
654
|
+
const handleUsageRangeChange = React.useCallback((value) => {
|
|
655
|
+
setUsageRangePreset(value);
|
|
656
|
+
const params = new URLSearchParams(searchParams.toString());
|
|
657
|
+
params.set("range", value);
|
|
658
|
+
const query = params.toString();
|
|
659
|
+
router.replace(query ? `${pathname}?${query}` : pathname, { scroll: false });
|
|
660
|
+
}, [pathname, router, searchParams]);
|
|
661
|
+
const handleRefresh = React.useCallback(() => {
|
|
662
|
+
loadReport().catch(() => {
|
|
663
|
+
});
|
|
664
|
+
}, [loadReport]);
|
|
665
|
+
const handleClearTelemetry = React.useCallback(async () => {
|
|
666
|
+
if (clearingTelemetry) return;
|
|
667
|
+
const confirmed = await confirm({
|
|
668
|
+
title: t("configs.moduleTelemetry.clear.confirmTitle", "Clear all telemetry data?"),
|
|
669
|
+
text: t(
|
|
670
|
+
"configs.moduleTelemetry.clear.confirmText",
|
|
671
|
+
"This removes current module telemetry and local process telemetry files for this development instance."
|
|
672
|
+
),
|
|
673
|
+
confirmText: t("configs.moduleTelemetry.clear.confirmButton", "Clear telemetry"),
|
|
674
|
+
variant: "destructive"
|
|
675
|
+
});
|
|
676
|
+
if (!confirmed) return;
|
|
677
|
+
setClearingTelemetry(true);
|
|
678
|
+
try {
|
|
679
|
+
await runMutation({
|
|
680
|
+
// optimistic-lock-exempt: clears in-process/dev-only telemetry buckets and
|
|
681
|
+
// local process telemetry files — there is no versioned entity (no updated_at)
|
|
682
|
+
// to lock against, so no expected-version header applies.
|
|
683
|
+
operation: () => readApiResultOrThrow(
|
|
684
|
+
API_PATH,
|
|
685
|
+
{ method: "DELETE" },
|
|
686
|
+
{
|
|
687
|
+
errorMessage: t("configs.moduleTelemetry.clear.error", "Failed to clear module telemetry."),
|
|
688
|
+
allowNullResult: true
|
|
689
|
+
}
|
|
690
|
+
),
|
|
691
|
+
context: clearMutationContext,
|
|
692
|
+
mutationPayload: {}
|
|
693
|
+
});
|
|
694
|
+
await loadReport();
|
|
695
|
+
flash(t("configs.moduleTelemetry.clear.success", "Module telemetry data cleared."), "success");
|
|
696
|
+
} catch (error) {
|
|
697
|
+
const message = error instanceof Error && error.message ? error.message : t("configs.moduleTelemetry.clear.error", "Failed to clear module telemetry.");
|
|
698
|
+
flash(message, "error");
|
|
699
|
+
} finally {
|
|
700
|
+
setClearingTelemetry(false);
|
|
701
|
+
}
|
|
702
|
+
}, [clearMutationContext, clearingTelemetry, confirm, loadReport, runMutation, t]);
|
|
703
|
+
if (state.loading) {
|
|
704
|
+
return /* @__PURE__ */ jsxs("section", { className: "space-y-3 rounded-lg border bg-background p-6", children: [
|
|
705
|
+
/* @__PURE__ */ jsxs("header", { className: "space-y-1", children: [
|
|
706
|
+
/* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: t("configs.moduleTelemetry.title", "Module telemetry") }),
|
|
707
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("configs.moduleTelemetry.description", "Preview module-level resource attribution collected in this process.") })
|
|
708
|
+
] }),
|
|
709
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [
|
|
710
|
+
/* @__PURE__ */ jsx(Spinner, { className: "h-4 w-4" }),
|
|
711
|
+
t("configs.moduleTelemetry.loading", "Loading module telemetry\u2026")
|
|
712
|
+
] })
|
|
713
|
+
] });
|
|
714
|
+
}
|
|
715
|
+
if (state.error) {
|
|
716
|
+
return /* @__PURE__ */ jsxs("section", { className: "space-y-3 rounded-lg border bg-background p-6", children: [
|
|
717
|
+
/* @__PURE__ */ jsxs("header", { className: "space-y-1", children: [
|
|
718
|
+
/* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: t("configs.moduleTelemetry.title", "Module telemetry") }),
|
|
719
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("configs.moduleTelemetry.description", "Preview module-level resource attribution collected in this process.") })
|
|
720
|
+
] }),
|
|
721
|
+
/* @__PURE__ */ jsx(ErrorMessage, { label: state.error }),
|
|
722
|
+
/* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", onClick: () => loadReport().catch(() => {
|
|
723
|
+
}), children: t("configs.moduleTelemetry.retry", "Retry") })
|
|
724
|
+
] });
|
|
725
|
+
}
|
|
726
|
+
const report = state.report;
|
|
727
|
+
if (!report) return null;
|
|
728
|
+
const usageRange = resolveUsageRange(usageRangePreset, report.buckets ?? [], report.startedAt);
|
|
729
|
+
const rangeBuckets = bucketsInRange(report.buckets ?? [], usageRange);
|
|
730
|
+
const rangeModules = aggregateRangeModules(rangeBuckets, report.thresholds);
|
|
731
|
+
const bucketsLabel = bucketCountLabel(rangeBuckets, report.bucketIntervalMs, t);
|
|
732
|
+
const telemetryStartedAtMs = firstAvailableTelemetryMs(report.buckets ?? [], Date.parse(report.startedAt), Date.now());
|
|
733
|
+
return /* @__PURE__ */ jsxs("section", { className: "space-y-6 rounded-lg border bg-background p-6", children: [
|
|
734
|
+
/* @__PURE__ */ jsxs("header", { className: "flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between", children: [
|
|
735
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
736
|
+
/* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: t("configs.moduleTelemetry.title", "Module telemetry") }),
|
|
737
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t("configs.moduleTelemetry.description", "Preview module-level resource attribution collected in this process.") }),
|
|
738
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t(
|
|
739
|
+
"configs.moduleTelemetry.generatedAt",
|
|
740
|
+
"Report generated {{timestamp}}",
|
|
741
|
+
{ timestamp: new Date(report.generatedAt).toLocaleString() }
|
|
742
|
+
) }),
|
|
743
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t(
|
|
744
|
+
"configs.moduleTelemetry.startedAt",
|
|
745
|
+
"Collecting since {{timestamp}}",
|
|
746
|
+
{ timestamp: new Date(telemetryStartedAtMs).toLocaleString() }
|
|
747
|
+
) })
|
|
748
|
+
] }),
|
|
749
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
|
|
750
|
+
report.canClearTelemetry ? /* @__PURE__ */ jsxs(
|
|
751
|
+
Button,
|
|
752
|
+
{
|
|
753
|
+
type: "button",
|
|
754
|
+
variant: "destructive-outline",
|
|
755
|
+
onClick: () => {
|
|
756
|
+
void handleClearTelemetry();
|
|
757
|
+
},
|
|
758
|
+
disabled: clearingTelemetry,
|
|
759
|
+
children: [
|
|
760
|
+
/* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4", "aria-hidden": "true" }),
|
|
761
|
+
clearingTelemetry ? t("configs.moduleTelemetry.clear.clearing", "Clearing...") : t("configs.moduleTelemetry.clear.button", "Clear all telemetry data")
|
|
762
|
+
]
|
|
763
|
+
}
|
|
764
|
+
) : null,
|
|
765
|
+
/* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", onClick: handleRefresh, children: [
|
|
766
|
+
/* @__PURE__ */ jsx(RefreshCw, { className: "h-4 w-4", "aria-hidden": "true" }),
|
|
767
|
+
t("configs.moduleTelemetry.refresh", "Refresh")
|
|
768
|
+
] })
|
|
769
|
+
] })
|
|
770
|
+
] }),
|
|
771
|
+
/* @__PURE__ */ jsx(
|
|
772
|
+
RangeOverview,
|
|
773
|
+
{
|
|
774
|
+
buckets: rangeBuckets,
|
|
775
|
+
modules: rangeModules,
|
|
776
|
+
range: usageRange,
|
|
777
|
+
rangePreset: usageRangePreset,
|
|
778
|
+
onRangePresetChange: handleUsageRangeChange,
|
|
779
|
+
bucketIntervalMs: report.bucketIntervalMs,
|
|
780
|
+
translate: t
|
|
781
|
+
}
|
|
782
|
+
),
|
|
783
|
+
/* @__PURE__ */ jsx(
|
|
784
|
+
RangeModuleSection,
|
|
785
|
+
{
|
|
786
|
+
modules: rangeModules,
|
|
787
|
+
range: usageRange,
|
|
788
|
+
bucketIntervalMs: report.bucketIntervalMs,
|
|
789
|
+
bucketsLabel,
|
|
790
|
+
translate: t
|
|
791
|
+
}
|
|
792
|
+
),
|
|
793
|
+
ConfirmDialogElement
|
|
794
|
+
] });
|
|
795
|
+
}
|
|
796
|
+
var ModuleTelemetryPanel_default = ModuleTelemetryPanel;
|
|
797
|
+
export {
|
|
798
|
+
ModuleTelemetryPanel,
|
|
799
|
+
ModuleTelemetryPanel_default as default
|
|
800
|
+
};
|
|
801
|
+
//# sourceMappingURL=ModuleTelemetryPanel.js.map
|