@lunora/studio 0.0.0 → 1.0.0-alpha.1
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/LICENSE.md +105 -0
- package/README.md +123 -9
- package/__assets__/package-og.svg +14 -0
- package/dist/index.d.ts +1402 -0
- package/dist/index.js +41 -0
- package/dist/mount.d.ts +21 -0
- package/dist/mount.js +26 -0
- package/dist/packem_shared/ADMIN_FUNCTION_PREFIX-DmBqMZ-z.js +45 -0
- package/dist/packem_shared/ApiDocsPanel-DpRjJhG5.js +842 -0
- package/dist/packem_shared/ApiReferencePanel-DMIUp-kK.js +229 -0
- package/dist/packem_shared/ApiTab-DURGU15e.js +251 -0
- package/dist/packem_shared/AuditPanel-BC59Nhst.js +212 -0
- package/dist/packem_shared/CommandPalette-Dx_CoB9i.js +373 -0
- package/dist/packem_shared/ConfirmButton-WQVUoGFb.js +59 -0
- package/dist/packem_shared/ConnectionBadge-Bxagrip8.js +111 -0
- package/dist/packem_shared/DEFAULT_AUTO_REFRESH_MS-Vxwaxx51.js +50 -0
- package/dist/packem_shared/DEFAULT_INSIGHT_THRESHOLDS-DjF0h-gA.js +89 -0
- package/dist/packem_shared/DataBrowser-Coz6jJE6.js +4542 -0
- package/dist/packem_shared/DataFilters-FNquMaiu.js +249 -0
- package/dist/packem_shared/ErrorBoundary-BzAApI7J.js +66 -0
- package/dist/packem_shared/ExportImportPanel-WO34fJxy.js +193 -0
- package/dist/packem_shared/FileBrowser-Zcr-Qgxo.js +2932 -0
- package/dist/packem_shared/FunctionRunner-j0Rd5m9t.js +343 -0
- package/dist/packem_shared/FunctionStatsPanel-DboBl-XL.js +432 -0
- package/dist/packem_shared/GlobalDataBrowser-9MhPEfgN.js +318 -0
- package/dist/packem_shared/HealthPanel-DOIgbUtx.js +640 -0
- package/dist/packem_shared/HomePanel-bdOCNA-p.js +1273 -0
- package/dist/packem_shared/InsightsPanel-DaZPnSgt.js +423 -0
- package/dist/packem_shared/LogsPanel-CWdqAGpQ.js +839 -0
- package/dist/packem_shared/MailPanel-D_EGtDnS.js +447 -0
- package/dist/packem_shared/MetricsPanel-E4Gv6wTO.js +1625 -0
- package/dist/packem_shared/MigrationsPanel-DQdPY9io.js +246 -0
- package/dist/packem_shared/OpenRpcReferencePanel-j2p3HB0s.js +191 -0
- package/dist/packem_shared/PitrPanel-BbBkQR6t.js +252 -0
- package/dist/packem_shared/STUDIO_ROOT_CLASS-D12gX2dV.js +3 -0
- package/dist/packem_shared/ScheduledJobs-Ok1CYYwI.js +159 -0
- package/dist/packem_shared/SchemaViewer-D8XGnp-X.js +2512 -0
- package/dist/packem_shared/SecurityAdvisorPanel-Cdm2IxLW.js +79 -0
- package/dist/packem_shared/SettingsPanel-D3WF2mBU.js +176 -0
- package/dist/packem_shared/ShardInput-DNCsT1KW.js +107 -0
- package/dist/packem_shared/SqlEditorPanel-BuQ7f2Hs.js +13 -0
- package/dist/packem_shared/Studio-D36od9Oz.js +33 -0
- package/dist/packem_shared/StudioApp-dvywkJ8I.js +383 -0
- package/dist/packem_shared/StudioI18nProvider-Dcajsznk.js +48 -0
- package/dist/packem_shared/TableEditor-DIVDk3vT.js +371 -0
- package/dist/packem_shared/advisor-view-DBlzJi6C.js +159 -0
- package/dist/packem_shared/aggregateMetrics-D4nUHEKU.js +108 -0
- package/dist/packem_shared/app.d-CCmwDEVs.d.ts +300 -0
- package/dist/packem_shared/badge-B2PKA1-5.js +49 -0
- package/dist/packem_shared/bar-chart-CzJAgqkp.js +3245 -0
- package/dist/packem_shared/button-BhsN2uZH.js +49 -0
- package/dist/packem_shared/card-DURq3ElK.js +175 -0
- package/dist/packem_shared/cf-links-BZfRdxSE.js +8 -0
- package/dist/packem_shared/checkbox-UNkzAxl-.js +63 -0
- package/dist/packem_shared/createStudioI18n-CgvlmDkN.js +27 -0
- package/dist/packem_shared/data-grid-CCh2Couo.js +183 -0
- package/dist/packem_shared/dropdown-menu-WY4B_eJO.js +280 -0
- package/dist/packem_shared/empty-state-DY_oe0k6.js +98 -0
- package/dist/packem_shared/grid-features-DTjG6Sex.js +840 -0
- package/dist/packem_shared/input-XH4r1Pt1.js +53 -0
- package/dist/packem_shared/internal-BBZYexre.js +68 -0
- package/dist/packem_shared/label-D8ykjn5J.js +46 -0
- package/dist/packem_shared/live-status-bPff1O7Y.js +44 -0
- package/dist/packem_shared/reference-view-BCKIoai7.js +2180 -0
- package/dist/packem_shared/shard-history-DyebH1R5.js +38 -0
- package/dist/packem_shared/sparkline-10dG-_f0.js +93 -0
- package/dist/packem_shared/sql-editor-panel-CW2y2x9h.js +2562 -0
- package/dist/packem_shared/storage-tier-CL98eOvn.js +85 -0
- package/dist/packem_shared/studio-BDVd7rIV.js +10303 -0
- package/dist/packem_shared/table-_RzNvy3R.js +246 -0
- package/dist/packem_shared/table-list-sidebar-aZHLq70w.js +832 -0
- package/dist/packem_shared/textarea-D3gaCU_-.js +46 -0
- package/dist/packem_shared/use-live-admin-D1h1Fzsd.js +73 -0
- package/dist/packem_shared/use-live-shard-seed-B74RYcOy.js +76 -0
- package/dist/packem_shared/useDebounced-Dxncpg6z.js +32 -0
- package/dist/packem_shared/utils-B05Dmz_H.js +8 -0
- package/dist/packem_shared/virtual-rect-CVMUskSm.js +10 -0
- package/dist/standalone/studio.js +356 -0
- package/dist/styles.css +2 -0
- package/package.json +77 -17
- package/src/theme.css +59 -0
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
import { c } from 'react/compiler-runtime';
|
|
2
|
+
import { useLunora } from '@lunora/react';
|
|
3
|
+
import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
|
4
|
+
import ConnectionBadge from './ConnectionBadge-Bxagrip8.js';
|
|
5
|
+
import { L as LiveError } from './live-status-bPff1O7Y.js';
|
|
6
|
+
import { B as Badge } from './badge-B2PKA1-5.js';
|
|
7
|
+
import { C as Card } from './card-DURq3ElK.js';
|
|
8
|
+
import { u as useLiveAdmin } from './use-live-admin-D1h1Fzsd.js';
|
|
9
|
+
import { useT } from './createStudioI18n-CgvlmDkN.js';
|
|
10
|
+
import { ADMIN_FUNCTIONS } from './ADMIN_FUNCTION_PREFIX-DmBqMZ-z.js';
|
|
11
|
+
import { f as fireAndForget, d as formatTimestamp, c as callOptions, e as errorMessage, a as adminRef } from './internal-BBZYexre.js';
|
|
12
|
+
import { l as loadRecentShards } from './shard-history-DyebH1R5.js';
|
|
13
|
+
import { c as cn } from './utils-B05Dmz_H.js';
|
|
14
|
+
import { shardsToAggregate } from './aggregateMetrics-D4nUHEKU.js';
|
|
15
|
+
import { S as Sparkline } from './sparkline-10dG-_f0.js';
|
|
16
|
+
import { jsxDEV } from 'react/jsx-dev-runtime';
|
|
17
|
+
|
|
18
|
+
const STATUS_RANK = {
|
|
19
|
+
completed: 0,
|
|
20
|
+
failed: 2,
|
|
21
|
+
in_progress: 1
|
|
22
|
+
};
|
|
23
|
+
const sumShardMetrics = (results) => {
|
|
24
|
+
let requests = 0;
|
|
25
|
+
let errors = 0;
|
|
26
|
+
let reachable = 0;
|
|
27
|
+
let failed = 0;
|
|
28
|
+
const history = [];
|
|
29
|
+
for (const {
|
|
30
|
+
metrics
|
|
31
|
+
} of results) {
|
|
32
|
+
if (metrics === null) {
|
|
33
|
+
failed += 1;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
reachable += 1;
|
|
37
|
+
requests += metrics.requests;
|
|
38
|
+
errors += metrics.errors;
|
|
39
|
+
history.push(...metrics.history ?? []);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
errors,
|
|
43
|
+
failed,
|
|
44
|
+
history,
|
|
45
|
+
reachable,
|
|
46
|
+
requests
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
const mergeFunctionStats = (perShard) => {
|
|
50
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
51
|
+
for (const shard of perShard) {
|
|
52
|
+
for (const stat of shard) {
|
|
53
|
+
const existing = byPath.get(stat.path);
|
|
54
|
+
if (existing === void 0) {
|
|
55
|
+
byPath.set(stat.path, {
|
|
56
|
+
...stat
|
|
57
|
+
});
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
existing.calls += stat.calls;
|
|
61
|
+
existing.errors += stat.errors;
|
|
62
|
+
existing.totalDurationMs += stat.totalDurationMs;
|
|
63
|
+
existing.scans = (existing.scans ?? 0) + (stat.scans ?? 0);
|
|
64
|
+
existing.maxDurationMs = Math.max(existing.maxDurationMs, stat.maxDurationMs);
|
|
65
|
+
existing.lastCalledAt = Math.max(existing.lastCalledAt, stat.lastCalledAt);
|
|
66
|
+
if ((stat.lastErrorAt ?? 0) > (existing.lastErrorAt ?? 0)) {
|
|
67
|
+
existing.lastErrorAt = stat.lastErrorAt;
|
|
68
|
+
existing.lastErrorMessage = stat.lastErrorMessage;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return [...byPath.values()];
|
|
73
|
+
};
|
|
74
|
+
const dedupeMigrations = (perShard) => {
|
|
75
|
+
const byId = /* @__PURE__ */ new Map();
|
|
76
|
+
for (const shard of perShard) {
|
|
77
|
+
for (const row of shard) {
|
|
78
|
+
const existing = byId.get(row.id);
|
|
79
|
+
if (existing === void 0) {
|
|
80
|
+
byId.set(row.id, row);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const rankDelta = STATUS_RANK[row.status] - STATUS_RANK[existing.status];
|
|
84
|
+
if (rankDelta > 0 || rankDelta === 0 && (row.updatedAt ?? 0) > (existing.updatedAt ?? 0)) {
|
|
85
|
+
byId.set(row.id, row);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return [...byId.values()];
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const GET_AUTH_METRICS = adminRef(ADMIN_FUNCTIONS.getAuthMetrics);
|
|
93
|
+
const GET_FUNCTION_STATS = adminRef(ADMIN_FUNCTIONS.getFunctionStats);
|
|
94
|
+
const GET_LOGS = adminRef(ADMIN_FUNCTIONS.getLogs);
|
|
95
|
+
const GET_METRICS = adminRef(ADMIN_FUNCTIONS.getMetrics);
|
|
96
|
+
const MIGRATION_STATUS = adminRef(ADMIN_FUNCTIONS.migrationStatus);
|
|
97
|
+
const RECENT_ERROR_LIMIT = 5;
|
|
98
|
+
const MIN_FANOUT_INTERVAL_MS = 2e3;
|
|
99
|
+
const TOP_FUNCTION_LIMIT = 5;
|
|
100
|
+
const REQUEST_ERROR_WARN = 0.01;
|
|
101
|
+
const REQUEST_ERROR_CRIT = 0.05;
|
|
102
|
+
const AUTH_FAIL_WARN = 0.1;
|
|
103
|
+
const AUTH_FAIL_CRIT = 0.3;
|
|
104
|
+
const BACKLOG_WARN = 1;
|
|
105
|
+
const BACKLOG_CRIT = 50;
|
|
106
|
+
const rateLevel = (rate, warn, crit) => {
|
|
107
|
+
if (rate >= crit) {
|
|
108
|
+
return "crit";
|
|
109
|
+
}
|
|
110
|
+
return rate >= warn ? "warn" : "ok";
|
|
111
|
+
};
|
|
112
|
+
const countLevel = (count, warn, crit) => {
|
|
113
|
+
if (count >= crit) {
|
|
114
|
+
return "crit";
|
|
115
|
+
}
|
|
116
|
+
return count >= warn ? "warn" : "ok";
|
|
117
|
+
};
|
|
118
|
+
const LEVEL_VARIANT = {
|
|
119
|
+
crit: "destructive",
|
|
120
|
+
ok: "secondary",
|
|
121
|
+
warn: "default"
|
|
122
|
+
};
|
|
123
|
+
const ratePercent = (numerator, denominator) => {
|
|
124
|
+
if (denominator === 0) {
|
|
125
|
+
return "—";
|
|
126
|
+
}
|
|
127
|
+
return `${(numerator / denominator * 100).toFixed(1)}%`;
|
|
128
|
+
};
|
|
129
|
+
const requestErrorSeries = (history) => {
|
|
130
|
+
const byBucket = /* @__PURE__ */ new Map();
|
|
131
|
+
for (const bucket of history ?? []) {
|
|
132
|
+
const slot = byBucket.get(bucket.bucketMs) ?? {
|
|
133
|
+
calls: 0,
|
|
134
|
+
errors: 0
|
|
135
|
+
};
|
|
136
|
+
slot.calls += bucket.calls;
|
|
137
|
+
slot.errors += bucket.errors;
|
|
138
|
+
byBucket.set(bucket.bucketMs, slot);
|
|
139
|
+
}
|
|
140
|
+
const ordered = [...byBucket.entries()].toSorted((a, b) => a[0] - b[0]);
|
|
141
|
+
return {
|
|
142
|
+
errors: ordered.map(([, slot]) => slot.errors),
|
|
143
|
+
requests: ordered.map(([, slot]) => slot.calls)
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
const migrationSummary = (rows) => {
|
|
147
|
+
let failed = 0;
|
|
148
|
+
let running = 0;
|
|
149
|
+
for (const row of rows) {
|
|
150
|
+
if (row.status === "failed") {
|
|
151
|
+
failed += 1;
|
|
152
|
+
} else if (row.status === "in_progress") {
|
|
153
|
+
running += 1;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
let level = "ok";
|
|
157
|
+
if (failed > 0) {
|
|
158
|
+
level = "crit";
|
|
159
|
+
} else if (running > 0) {
|
|
160
|
+
level = "warn";
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
failed,
|
|
164
|
+
level,
|
|
165
|
+
running
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
const migrationTileValue = (summary, t) => {
|
|
169
|
+
if (summary.failed > 0) {
|
|
170
|
+
return t("{count} failed", {
|
|
171
|
+
count: summary.failed.toString()
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
if (summary.running > 0) {
|
|
175
|
+
return t("{count} running", {
|
|
176
|
+
count: summary.running.toString()
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return t("OK");
|
|
180
|
+
};
|
|
181
|
+
const LEVEL_DOT = {
|
|
182
|
+
crit: "bg-destructive",
|
|
183
|
+
ok: "bg-success",
|
|
184
|
+
warn: "bg-warning"
|
|
185
|
+
};
|
|
186
|
+
const LEVEL_TEXT = {
|
|
187
|
+
crit: "text-destructive",
|
|
188
|
+
ok: "text-foreground",
|
|
189
|
+
warn: "text-warning"
|
|
190
|
+
};
|
|
191
|
+
const LEVEL_RING = {
|
|
192
|
+
crit: "bg-destructive/10 text-destructive",
|
|
193
|
+
ok: "bg-success/10 text-success",
|
|
194
|
+
warn: "bg-warning/10 text-warning"
|
|
195
|
+
};
|
|
196
|
+
const worstLevel = (levels) => {
|
|
197
|
+
if (levels.includes("crit")) {
|
|
198
|
+
return "crit";
|
|
199
|
+
}
|
|
200
|
+
return levels.includes("warn") ? "warn" : "ok";
|
|
201
|
+
};
|
|
202
|
+
const SloCard = (t0) => {
|
|
203
|
+
const $ = c(19);
|
|
204
|
+
const {
|
|
205
|
+
chart,
|
|
206
|
+
label,
|
|
207
|
+
level,
|
|
208
|
+
testId,
|
|
209
|
+
value
|
|
210
|
+
} = t0;
|
|
211
|
+
const t1 = LEVEL_DOT[level];
|
|
212
|
+
let t2;
|
|
213
|
+
if ($[0] !== t1) {
|
|
214
|
+
t2 = cn("size-1.5 shrink-0 rounded-full", t1);
|
|
215
|
+
$[0] = t1;
|
|
216
|
+
$[1] = t2;
|
|
217
|
+
} else {
|
|
218
|
+
t2 = $[1];
|
|
219
|
+
}
|
|
220
|
+
let t3;
|
|
221
|
+
if ($[2] !== t2) {
|
|
222
|
+
t3 = /* @__PURE__ */ jsxDEV("span", {
|
|
223
|
+
"aria-hidden": "true",
|
|
224
|
+
className: t2
|
|
225
|
+
}, void 0, false);
|
|
226
|
+
$[2] = t2;
|
|
227
|
+
$[3] = t3;
|
|
228
|
+
} else {
|
|
229
|
+
t3 = $[3];
|
|
230
|
+
}
|
|
231
|
+
let t4;
|
|
232
|
+
if ($[4] !== label || $[5] !== t3) {
|
|
233
|
+
t4 = /* @__PURE__ */ jsxDEV("span", {
|
|
234
|
+
className: "flex items-center gap-1.5 font-mono text-[11px] tracking-wide text-muted-foreground uppercase",
|
|
235
|
+
children: [t3, label]
|
|
236
|
+
}, void 0, true);
|
|
237
|
+
$[4] = label;
|
|
238
|
+
$[5] = t3;
|
|
239
|
+
$[6] = t4;
|
|
240
|
+
} else {
|
|
241
|
+
t4 = $[6];
|
|
242
|
+
}
|
|
243
|
+
const t5 = LEVEL_TEXT[level];
|
|
244
|
+
let t6;
|
|
245
|
+
if ($[7] !== t5) {
|
|
246
|
+
t6 = cn("truncate text-2xl font-semibold tabular-nums", t5);
|
|
247
|
+
$[7] = t5;
|
|
248
|
+
$[8] = t6;
|
|
249
|
+
} else {
|
|
250
|
+
t6 = $[8];
|
|
251
|
+
}
|
|
252
|
+
let t7;
|
|
253
|
+
if ($[9] !== t6 || $[10] !== testId || $[11] !== value) {
|
|
254
|
+
t7 = /* @__PURE__ */ jsxDEV("span", {
|
|
255
|
+
className: t6,
|
|
256
|
+
"data-testid": testId,
|
|
257
|
+
children: value
|
|
258
|
+
}, void 0, false);
|
|
259
|
+
$[9] = t6;
|
|
260
|
+
$[10] = testId;
|
|
261
|
+
$[11] = value;
|
|
262
|
+
$[12] = t7;
|
|
263
|
+
} else {
|
|
264
|
+
t7 = $[12];
|
|
265
|
+
}
|
|
266
|
+
let t8;
|
|
267
|
+
if ($[13] !== chart || $[14] !== t7) {
|
|
268
|
+
t8 = /* @__PURE__ */ jsxDEV("div", {
|
|
269
|
+
className: "flex items-center justify-between gap-3",
|
|
270
|
+
children: [t7, chart]
|
|
271
|
+
}, void 0, true);
|
|
272
|
+
$[13] = chart;
|
|
273
|
+
$[14] = t7;
|
|
274
|
+
$[15] = t8;
|
|
275
|
+
} else {
|
|
276
|
+
t8 = $[15];
|
|
277
|
+
}
|
|
278
|
+
let t9;
|
|
279
|
+
if ($[16] !== t4 || $[17] !== t8) {
|
|
280
|
+
t9 = /* @__PURE__ */ jsxDEV(Card, {
|
|
281
|
+
className: "gap-0 py-0",
|
|
282
|
+
children: /* @__PURE__ */ jsxDEV("div", {
|
|
283
|
+
className: "flex flex-col gap-2.5 p-4",
|
|
284
|
+
children: [t4, t8]
|
|
285
|
+
}, void 0, true)
|
|
286
|
+
}, void 0, false);
|
|
287
|
+
$[16] = t4;
|
|
288
|
+
$[17] = t8;
|
|
289
|
+
$[18] = t9;
|
|
290
|
+
} else {
|
|
291
|
+
t9 = $[18];
|
|
292
|
+
}
|
|
293
|
+
return t9;
|
|
294
|
+
};
|
|
295
|
+
const loadSloData = async (client, rootShard, shards) => {
|
|
296
|
+
const perShardSettled = await Promise.all(shards.map((shard) => {
|
|
297
|
+
const shardOptions = callOptions(shard);
|
|
298
|
+
return Promise.allSettled([client.query(GET_METRICS, {}, shardOptions), client.query(GET_FUNCTION_STATS, {}, shardOptions), client.query(MIGRATION_STATUS, {}, shardOptions)]);
|
|
299
|
+
}));
|
|
300
|
+
const rootOptions = callOptions(rootShard);
|
|
301
|
+
const schedulerStatus = typeof client.schedulerStatus === "function" ? client.schedulerStatus() : Promise.reject(new Error("scheduler status unavailable"));
|
|
302
|
+
const [logs, authMetrics, schedulerState] = await Promise.allSettled([client.query(GET_LOGS, {}, rootOptions), client.query(GET_AUTH_METRICS, {}, rootOptions), schedulerStatus]);
|
|
303
|
+
const perShard = perShardSettled.map(([m, f, mig]) => {
|
|
304
|
+
return {
|
|
305
|
+
functions: f.status === "fulfilled" ? f.value.functions : [],
|
|
306
|
+
metrics: m.status === "fulfilled" ? m.value : null,
|
|
307
|
+
migrations: mig.status === "fulfilled" ? mig.value.migrations : []
|
|
308
|
+
};
|
|
309
|
+
});
|
|
310
|
+
const totals = sumShardMetrics(perShard);
|
|
311
|
+
const rootMetrics = perShardSettled[0]?.[0];
|
|
312
|
+
return {
|
|
313
|
+
auth: authMetrics.status === "fulfilled" ? authMetrics.value : null,
|
|
314
|
+
entries: logs.status === "fulfilled" ? logs.value.entries : [],
|
|
315
|
+
functions: mergeFunctionStats(perShard.map((shardResult) => shardResult.functions)),
|
|
316
|
+
logsError: logs.status === "rejected" ? errorMessage(logs.reason) : null,
|
|
317
|
+
metricsError: totals.reachable === 0 && rootMetrics?.status === "rejected" ? errorMessage(rootMetrics.reason) : null,
|
|
318
|
+
migrations: dedupeMigrations(perShard.map((shardResult) => shardResult.migrations)),
|
|
319
|
+
scheduler: schedulerState.status === "fulfilled" ? schedulerState.value : null,
|
|
320
|
+
totals
|
|
321
|
+
};
|
|
322
|
+
};
|
|
323
|
+
const HealthPanel = ({
|
|
324
|
+
initialShardKey
|
|
325
|
+
}) => {
|
|
326
|
+
const client = useLunora();
|
|
327
|
+
const t = useT();
|
|
328
|
+
const [entries, setEntries] = useState([]);
|
|
329
|
+
const [logsError, setLogsError] = useState(null);
|
|
330
|
+
const [totals, setTotals] = useState(null);
|
|
331
|
+
const [metricsError, setMetricsError] = useState(null);
|
|
332
|
+
const [functions, setFunctions] = useState([]);
|
|
333
|
+
const [auth, setAuth] = useState(null);
|
|
334
|
+
const [scheduler, setScheduler] = useState(null);
|
|
335
|
+
const [migrations, setMigrations] = useState([]);
|
|
336
|
+
const [liveError, setLiveError] = useState(void 0);
|
|
337
|
+
const [recentShards] = useState(loadRecentShards);
|
|
338
|
+
const inFlightRef = useRef(false);
|
|
339
|
+
const mountedRef = useRef(true);
|
|
340
|
+
useEffect(() => {
|
|
341
|
+
mountedRef.current = true;
|
|
342
|
+
return () => {
|
|
343
|
+
mountedRef.current = false;
|
|
344
|
+
};
|
|
345
|
+
}, []);
|
|
346
|
+
const rootShard = initialShardKey ?? "";
|
|
347
|
+
const refresh = useCallback(async () => {
|
|
348
|
+
if (inFlightRef.current) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
inFlightRef.current = true;
|
|
352
|
+
const result = await loadSloData(client, rootShard, shardsToAggregate(rootShard, recentShards));
|
|
353
|
+
if (!mountedRef.current) {
|
|
354
|
+
inFlightRef.current = false;
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
setTotals(result.totals);
|
|
358
|
+
setMetricsError(result.metricsError);
|
|
359
|
+
setFunctions(result.functions);
|
|
360
|
+
setMigrations(result.migrations);
|
|
361
|
+
setEntries(result.entries);
|
|
362
|
+
setLogsError(result.logsError);
|
|
363
|
+
setAuth(result.auth);
|
|
364
|
+
setScheduler(result.scheduler);
|
|
365
|
+
inFlightRef.current = false;
|
|
366
|
+
}, [client, recentShards, rootShard]);
|
|
367
|
+
useEffect(() => {
|
|
368
|
+
fireAndForget(refresh());
|
|
369
|
+
}, [refresh]);
|
|
370
|
+
const lastFanOutRef = useRef(0);
|
|
371
|
+
const trailingTimerRef = useRef(null);
|
|
372
|
+
const scheduleFanOut = () => {
|
|
373
|
+
const elapsed = Date.now() - lastFanOutRef.current;
|
|
374
|
+
if (elapsed >= MIN_FANOUT_INTERVAL_MS) {
|
|
375
|
+
lastFanOutRef.current = Date.now();
|
|
376
|
+
fireAndForget(refresh());
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
trailingTimerRef.current ??= setTimeout(() => {
|
|
380
|
+
trailingTimerRef.current = null;
|
|
381
|
+
lastFanOutRef.current = Date.now();
|
|
382
|
+
fireAndForget(refresh());
|
|
383
|
+
}, MIN_FANOUT_INTERVAL_MS - elapsed);
|
|
384
|
+
};
|
|
385
|
+
useEffect(() => () => {
|
|
386
|
+
if (trailingTimerRef.current !== null) {
|
|
387
|
+
clearTimeout(trailingTimerRef.current);
|
|
388
|
+
}
|
|
389
|
+
}, []);
|
|
390
|
+
useLiveAdmin(ADMIN_FUNCTIONS.getMetrics, {}, rootShard, () => {
|
|
391
|
+
setLiveError(void 0);
|
|
392
|
+
scheduleFanOut();
|
|
393
|
+
}, true, setLiveError);
|
|
394
|
+
const recentErrors = entries.filter((entry) => entry.level === "error");
|
|
395
|
+
const topErrors = recentErrors.slice(0, RECENT_ERROR_LIMIT);
|
|
396
|
+
const trend = requestErrorSeries(totals?.history);
|
|
397
|
+
const authTrend = useMemo(() => {
|
|
398
|
+
const buckets = auth?.history ?? [];
|
|
399
|
+
return {
|
|
400
|
+
attempts: buckets.map((bucket) => bucket.attempts),
|
|
401
|
+
failures: buckets.map((bucket_0) => bucket_0.failures)
|
|
402
|
+
};
|
|
403
|
+
}, [auth?.history]);
|
|
404
|
+
const worstFunctions = functions.filter((stat) => stat.calls > 0).toSorted((a, b) => b.errors / b.calls - a.errors / a.calls || b.calls - a.calls).slice(0, TOP_FUNCTION_LIMIT);
|
|
405
|
+
const migration = migrationSummary(migrations);
|
|
406
|
+
const appErrorRate = totals === null || totals.requests === 0 ? 0 : totals.errors / totals.requests;
|
|
407
|
+
const errorLevel = rateLevel(appErrorRate, REQUEST_ERROR_WARN, REQUEST_ERROR_CRIT);
|
|
408
|
+
const authLevel = auth === null ? "ok" : rateLevel(auth.failureRate, AUTH_FAIL_WARN, AUTH_FAIL_CRIT);
|
|
409
|
+
const backlogLevel = scheduler === null ? "ok" : countLevel(scheduler.backlog, BACKLOG_WARN, BACKLOG_CRIT);
|
|
410
|
+
const overall = worstLevel([errorLevel, authLevel, backlogLevel, migration.level]);
|
|
411
|
+
let statusLabel;
|
|
412
|
+
let statusDescription;
|
|
413
|
+
if (overall === "crit") {
|
|
414
|
+
statusLabel = t("Critical");
|
|
415
|
+
statusDescription = t("One or more service levels are breached.");
|
|
416
|
+
} else if (overall === "warn") {
|
|
417
|
+
statusLabel = t("Degraded");
|
|
418
|
+
statusDescription = t("Some service levels need attention.");
|
|
419
|
+
} else {
|
|
420
|
+
statusLabel = t("All systems healthy");
|
|
421
|
+
statusDescription = t("All service levels are within target.");
|
|
422
|
+
}
|
|
423
|
+
return /* @__PURE__ */ jsxDEV("div", {
|
|
424
|
+
className: "flex flex-col gap-6",
|
|
425
|
+
"data-testid": "lunora-health",
|
|
426
|
+
children: [/* @__PURE__ */ jsxDEV(Card, {
|
|
427
|
+
className: "gap-0 py-0",
|
|
428
|
+
children: [/* @__PURE__ */ jsxDEV("div", {
|
|
429
|
+
className: "flex flex-wrap items-center justify-between gap-4 p-4",
|
|
430
|
+
children: [/* @__PURE__ */ jsxDEV("div", {
|
|
431
|
+
className: "flex items-center gap-3",
|
|
432
|
+
children: [/* @__PURE__ */ jsxDEV("span", {
|
|
433
|
+
"aria-hidden": "true",
|
|
434
|
+
className: cn("flex size-10 shrink-0 items-center justify-center rounded-full", LEVEL_RING[overall]),
|
|
435
|
+
children: /* @__PURE__ */ jsxDEV("span", {
|
|
436
|
+
className: cn("size-3 rounded-full", LEVEL_DOT[overall])
|
|
437
|
+
}, void 0, false)
|
|
438
|
+
}, void 0, false), /* @__PURE__ */ jsxDEV("div", {
|
|
439
|
+
className: "grid leading-tight",
|
|
440
|
+
children: [/* @__PURE__ */ jsxDEV("span", {
|
|
441
|
+
className: "text-sm font-semibold text-foreground",
|
|
442
|
+
"data-testid": "hl-status",
|
|
443
|
+
children: statusLabel
|
|
444
|
+
}, void 0, false), /* @__PURE__ */ jsxDEV("span", {
|
|
445
|
+
className: "text-[13px] text-muted-foreground",
|
|
446
|
+
children: statusDescription
|
|
447
|
+
}, void 0, false)]
|
|
448
|
+
}, void 0, true)]
|
|
449
|
+
}, void 0, true), /* @__PURE__ */ jsxDEV("div", {
|
|
450
|
+
className: "flex flex-wrap items-center gap-4",
|
|
451
|
+
children: [/* @__PURE__ */ jsxDEV("div", {
|
|
452
|
+
className: "text-end",
|
|
453
|
+
children: [/* @__PURE__ */ jsxDEV("div", {
|
|
454
|
+
className: "font-mono text-[10px] tracking-wide text-muted-foreground uppercase",
|
|
455
|
+
children: t("Requests")
|
|
456
|
+
}, void 0, false), /* @__PURE__ */ jsxDEV("div", {
|
|
457
|
+
className: "flex items-center justify-end gap-2",
|
|
458
|
+
children: [trend.requests.length >= 2 && /* @__PURE__ */ jsxDEV(Sparkline, {
|
|
459
|
+
ariaLabel: t("Requests over time"),
|
|
460
|
+
className: "h-6 w-20 text-foreground",
|
|
461
|
+
series: trend.requests,
|
|
462
|
+
testId: "hl-spark-requests"
|
|
463
|
+
}, void 0, false), /* @__PURE__ */ jsxDEV("span", {
|
|
464
|
+
className: "text-lg font-semibold tabular-nums text-foreground",
|
|
465
|
+
"data-testid": "hl-requests",
|
|
466
|
+
children: (totals?.requests ?? 0).toString()
|
|
467
|
+
}, void 0, false)]
|
|
468
|
+
}, void 0, true)]
|
|
469
|
+
}, void 0, true), /* @__PURE__ */ jsxDEV("span", {
|
|
470
|
+
"aria-hidden": "true",
|
|
471
|
+
className: "h-9 w-px bg-border"
|
|
472
|
+
}, void 0, false), /* @__PURE__ */ jsxDEV("div", {
|
|
473
|
+
className: "text-end",
|
|
474
|
+
children: [/* @__PURE__ */ jsxDEV("div", {
|
|
475
|
+
className: "font-mono text-[10px] tracking-wide text-muted-foreground uppercase",
|
|
476
|
+
children: t("Error rate")
|
|
477
|
+
}, void 0, false), /* @__PURE__ */ jsxDEV("div", {
|
|
478
|
+
className: cn("text-lg font-semibold tabular-nums", LEVEL_TEXT[errorLevel]),
|
|
479
|
+
"data-testid": "hl-error-rate",
|
|
480
|
+
children: totals === null ? "—" : ratePercent(totals.errors, totals.requests)
|
|
481
|
+
}, void 0, false)]
|
|
482
|
+
}, void 0, true), /* @__PURE__ */ jsxDEV("span", {
|
|
483
|
+
"aria-hidden": "true",
|
|
484
|
+
className: "h-9 w-px bg-border"
|
|
485
|
+
}, void 0, false), /* @__PURE__ */ jsxDEV("div", {
|
|
486
|
+
className: "flex items-center gap-2",
|
|
487
|
+
children: [/* @__PURE__ */ jsxDEV(ConnectionBadge, {}, void 0, false), /* @__PURE__ */ jsxDEV(LiveError, {
|
|
488
|
+
message: liveError,
|
|
489
|
+
prefix: "hl"
|
|
490
|
+
}, void 0, false)]
|
|
491
|
+
}, void 0, true)]
|
|
492
|
+
}, void 0, true)]
|
|
493
|
+
}, void 0, true), metricsError !== null && /* @__PURE__ */ jsxDEV("div", {
|
|
494
|
+
className: "border-t border-border bg-destructive/5 px-4 py-2 text-sm text-destructive",
|
|
495
|
+
"data-testid": "hl-metrics-error",
|
|
496
|
+
role: "alert",
|
|
497
|
+
children: metricsError
|
|
498
|
+
}, void 0, false)]
|
|
499
|
+
}, void 0, true), /* @__PURE__ */ jsxDEV("div", {
|
|
500
|
+
className: "grid gap-3 sm:grid-cols-2 xl:grid-cols-4",
|
|
501
|
+
"data-testid": "hl-slo",
|
|
502
|
+
children: [/* @__PURE__ */ jsxDEV(SloCard, {
|
|
503
|
+
chart: trend.errors.length >= 2 ? /* @__PURE__ */ jsxDEV(Sparkline, {
|
|
504
|
+
ariaLabel: t("Errors over time"),
|
|
505
|
+
className: "h-7 w-20 text-destructive",
|
|
506
|
+
series: trend.errors,
|
|
507
|
+
testId: "hl-spark-errors"
|
|
508
|
+
}, void 0, false) : void 0,
|
|
509
|
+
label: t("Error rate"),
|
|
510
|
+
level: errorLevel,
|
|
511
|
+
testId: "hl-slo-errorrate",
|
|
512
|
+
value: totals === null ? "—" : ratePercent(totals.errors, totals.requests)
|
|
513
|
+
}, void 0, false), /* @__PURE__ */ jsxDEV(SloCard, {
|
|
514
|
+
chart: authTrend.failures.length >= 2 ? /* @__PURE__ */ jsxDEV(Sparkline, {
|
|
515
|
+
ariaLabel: t("Auth failures over time"),
|
|
516
|
+
className: "h-7 w-20 text-destructive",
|
|
517
|
+
series: authTrend.failures,
|
|
518
|
+
testId: "hl-spark-auth"
|
|
519
|
+
}, void 0, false) : void 0,
|
|
520
|
+
label: t("Auth failures"),
|
|
521
|
+
level: authLevel,
|
|
522
|
+
testId: "hl-slo-auth",
|
|
523
|
+
value: auth === null ? "—" : ratePercent(auth.failures, auth.attempts)
|
|
524
|
+
}, void 0, false), /* @__PURE__ */ jsxDEV(SloCard, {
|
|
525
|
+
label: t("Scheduler backlog"),
|
|
526
|
+
level: backlogLevel,
|
|
527
|
+
testId: "hl-slo-backlog",
|
|
528
|
+
value: scheduler === null ? "—" : scheduler.backlog.toString()
|
|
529
|
+
}, void 0, false), /* @__PURE__ */ jsxDEV(SloCard, {
|
|
530
|
+
label: t("Migrations"),
|
|
531
|
+
level: migration.level,
|
|
532
|
+
testId: "hl-slo-migrations",
|
|
533
|
+
value: migrationTileValue(migration, t)
|
|
534
|
+
}, void 0, false)]
|
|
535
|
+
}, void 0, true), /* @__PURE__ */ jsxDEV("div", {
|
|
536
|
+
className: "grid gap-3 lg:grid-cols-2",
|
|
537
|
+
children: [/* @__PURE__ */ jsxDEV(Card, {
|
|
538
|
+
className: "gap-0 py-0",
|
|
539
|
+
"data-testid": "hl-functions",
|
|
540
|
+
children: [/* @__PURE__ */ jsxDEV("header", {
|
|
541
|
+
className: "border-b border-border px-4 py-3",
|
|
542
|
+
children: /* @__PURE__ */ jsxDEV("span", {
|
|
543
|
+
className: "font-mono text-[11px] tracking-wide text-muted-foreground uppercase",
|
|
544
|
+
children: t("Functions by error rate")
|
|
545
|
+
}, void 0, false)
|
|
546
|
+
}, void 0, false), worstFunctions.length === 0 ? /* @__PURE__ */ jsxDEV("p", {
|
|
547
|
+
className: "px-4 py-8 text-center text-sm text-muted-foreground",
|
|
548
|
+
"data-testid": "hl-functions-empty",
|
|
549
|
+
children: t("No function activity yet.")
|
|
550
|
+
}, void 0, false) : /* @__PURE__ */ jsxDEV("ul", {
|
|
551
|
+
className: "divide-y divide-border",
|
|
552
|
+
children: worstFunctions.map((stat_0) => /* @__PURE__ */ jsxDEV("li", {
|
|
553
|
+
className: "flex flex-wrap items-center justify-between gap-2 px-4 py-2 text-xs",
|
|
554
|
+
"data-testid": "hl-fn-row",
|
|
555
|
+
children: [/* @__PURE__ */ jsxDEV("span", {
|
|
556
|
+
className: "truncate font-mono text-foreground",
|
|
557
|
+
children: stat_0.path
|
|
558
|
+
}, void 0, false), /* @__PURE__ */ jsxDEV("span", {
|
|
559
|
+
className: "flex shrink-0 items-center gap-2",
|
|
560
|
+
children: [/* @__PURE__ */ jsxDEV("span", {
|
|
561
|
+
className: "tabular-nums text-muted-foreground",
|
|
562
|
+
children: t("{count} calls", {
|
|
563
|
+
count: stat_0.calls.toString()
|
|
564
|
+
})
|
|
565
|
+
}, void 0, false), /* @__PURE__ */ jsxDEV(Badge, {
|
|
566
|
+
variant: LEVEL_VARIANT[rateLevel(stat_0.errors / stat_0.calls, REQUEST_ERROR_WARN, REQUEST_ERROR_CRIT)],
|
|
567
|
+
children: ratePercent(stat_0.errors, stat_0.calls)
|
|
568
|
+
}, void 0, false)]
|
|
569
|
+
}, void 0, true)]
|
|
570
|
+
}, stat_0.path, true))
|
|
571
|
+
}, void 0, false)]
|
|
572
|
+
}, void 0, true), /* @__PURE__ */ jsxDEV(Card, {
|
|
573
|
+
className: "gap-0 py-0",
|
|
574
|
+
children: [/* @__PURE__ */ jsxDEV("header", {
|
|
575
|
+
className: "flex items-center justify-between gap-2 border-b border-border px-4 py-3",
|
|
576
|
+
children: [/* @__PURE__ */ jsxDEV("span", {
|
|
577
|
+
className: "font-mono text-[11px] tracking-wide text-muted-foreground uppercase",
|
|
578
|
+
children: t("Recent errors")
|
|
579
|
+
}, void 0, false), /* @__PURE__ */ jsxDEV(Badge, {
|
|
580
|
+
"data-testid": "hl-error-count",
|
|
581
|
+
variant: recentErrors.length > 0 ? "destructive" : "outline",
|
|
582
|
+
children: recentErrors.length
|
|
583
|
+
}, void 0, false)]
|
|
584
|
+
}, void 0, true), logsError !== null && /* @__PURE__ */ jsxDEV("p", {
|
|
585
|
+
className: "px-4 py-8 text-center text-sm text-destructive",
|
|
586
|
+
"data-testid": "hl-logs-error",
|
|
587
|
+
role: "alert",
|
|
588
|
+
children: logsError
|
|
589
|
+
}, void 0, false), logsError === null && topErrors.length === 0 && /* @__PURE__ */ jsxDEV("p", {
|
|
590
|
+
className: "px-4 py-8 text-center text-sm text-muted-foreground",
|
|
591
|
+
"data-testid": "hl-errors-empty",
|
|
592
|
+
children: t("No recent errors.")
|
|
593
|
+
}, void 0, false), topErrors.length > 0 && /* @__PURE__ */ jsxDEV("ul", {
|
|
594
|
+
className: "divide-y divide-border",
|
|
595
|
+
children: topErrors.map((entry_0, index) => /* @__PURE__ */ jsxDEV("li", {
|
|
596
|
+
className: "flex flex-col gap-0.5 px-4 py-2 text-xs",
|
|
597
|
+
"data-testid": "hl-error-row",
|
|
598
|
+
children: [/* @__PURE__ */ jsxDEV("span", {
|
|
599
|
+
className: "flex items-center gap-2",
|
|
600
|
+
children: [/* @__PURE__ */ jsxDEV("time", {
|
|
601
|
+
className: "shrink-0 text-muted-foreground",
|
|
602
|
+
children: formatTimestamp(entry_0.timestamp)
|
|
603
|
+
}, void 0, false), entry_0.functionPath !== void 0 && /* @__PURE__ */ jsxDEV("span", {
|
|
604
|
+
className: "truncate font-mono text-foreground",
|
|
605
|
+
children: entry_0.functionPath
|
|
606
|
+
}, void 0, false)]
|
|
607
|
+
}, void 0, true), /* @__PURE__ */ jsxDEV("span", {
|
|
608
|
+
className: "text-destructive",
|
|
609
|
+
children: entry_0.message
|
|
610
|
+
}, void 0, false)]
|
|
611
|
+
}, `${entry_0.timestamp.toString()}-${index.toString()}`, true))
|
|
612
|
+
}, void 0, false)]
|
|
613
|
+
}, void 0, true)]
|
|
614
|
+
}, void 0, true), /* @__PURE__ */ jsxDEV(Card, {
|
|
615
|
+
className: "gap-0 py-0",
|
|
616
|
+
children: [/* @__PURE__ */ jsxDEV("header", {
|
|
617
|
+
className: "flex items-center justify-between gap-2 border-b border-border px-4 py-3",
|
|
618
|
+
children: [/* @__PURE__ */ jsxDEV("span", {
|
|
619
|
+
className: "font-mono text-[11px] tracking-wide text-muted-foreground uppercase",
|
|
620
|
+
children: t("Shards seen")
|
|
621
|
+
}, void 0, false), /* @__PURE__ */ jsxDEV(Badge, {
|
|
622
|
+
"data-testid": "hl-shard-count",
|
|
623
|
+
variant: "outline",
|
|
624
|
+
children: recentShards.length
|
|
625
|
+
}, void 0, false)]
|
|
626
|
+
}, void 0, true), recentShards.length > 0 && /* @__PURE__ */ jsxDEV("ul", {
|
|
627
|
+
className: "flex flex-wrap gap-1.5 p-4",
|
|
628
|
+
children: recentShards.map((shard) => /* @__PURE__ */ jsxDEV("li", {
|
|
629
|
+
"data-testid": "hl-shard",
|
|
630
|
+
children: /* @__PURE__ */ jsxDEV(Badge, {
|
|
631
|
+
variant: "secondary",
|
|
632
|
+
children: shard
|
|
633
|
+
}, void 0, false)
|
|
634
|
+
}, shard, false))
|
|
635
|
+
}, void 0, false)]
|
|
636
|
+
}, void 0, true)]
|
|
637
|
+
}, void 0, true);
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
export { HealthPanel };
|