@crvouga/mockingbird-service-livekit 1.4.0 → 1.5.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.
@@ -533,10 +533,10 @@ var OpenAPIReferenceError = class extends Error {
533
533
  };
534
534
  var unescapePointer = (segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~");
535
535
  var isReference = (value) => typeof value === "object" && value !== null && typeof value.$ref === "string";
536
- var resolveRef = (document2, ref) => {
536
+ var resolveRef = (document3, ref) => {
537
537
  if (!ref.startsWith("#/"))
538
538
  throw new OpenAPIReferenceError(ref);
539
- let cursor = document2;
539
+ let cursor = document3;
540
540
  for (const raw of ref.slice(2).split("/")) {
541
541
  const segment = unescapePointer(raw);
542
542
  if (typeof cursor !== "object" || cursor === null || !(segment in cursor)) {
@@ -548,14 +548,14 @@ var resolveRef = (document2, ref) => {
548
548
  throw new OpenAPIReferenceError(ref);
549
549
  return cursor;
550
550
  };
551
- var deref = (document2, value) => {
551
+ var deref = (document3, value) => {
552
552
  let current = value;
553
553
  const seen = /* @__PURE__ */ new Set();
554
554
  while (isReference(current)) {
555
555
  if (seen.has(current.$ref))
556
556
  throw new OpenAPIReferenceError(`${current.$ref} (cycle)`);
557
557
  seen.add(current.$ref);
558
- current = resolveRef(document2, current.$ref);
558
+ current = resolveRef(document3, current.$ref);
559
559
  }
560
560
  return current;
561
561
  };
@@ -573,36 +573,36 @@ var HTTP_METHODS = [
573
573
  ];
574
574
 
575
575
  // ../../openapi/core/dist/document.js
576
- var mergeParameters = (document2, item, own) => {
576
+ var mergeParameters = (document3, item, own) => {
577
577
  const merged = /* @__PURE__ */ new Map();
578
578
  for (const raw of item.parameters ?? []) {
579
- const parameter = deref(document2, raw);
579
+ const parameter = deref(document3, raw);
580
580
  merged.set(`${parameter.in}:${parameter.name}`, parameter);
581
581
  }
582
582
  for (const raw of own ?? []) {
583
- const parameter = deref(document2, raw);
583
+ const parameter = deref(document3, raw);
584
584
  merged.set(`${parameter.in}:${parameter.name}`, parameter);
585
585
  }
586
586
  return [...merged.values()];
587
587
  };
588
- var listOperations = (document2) => {
588
+ var listOperations = (document3) => {
589
589
  const operations = [];
590
- for (const [path, item] of Object.entries(document2.paths)) {
590
+ for (const [path, item] of Object.entries(document3.paths)) {
591
591
  for (const method of HTTP_METHODS) {
592
592
  const operation = item[method];
593
593
  if (operation?.operationId === void 0)
594
594
  continue;
595
595
  const responses = {};
596
596
  for (const [status, response] of Object.entries(operation.responses)) {
597
- responses[status] = deref(document2, response);
597
+ responses[status] = deref(document3, response);
598
598
  }
599
599
  operations.push({
600
600
  operationId: operation.operationId,
601
601
  method,
602
602
  path,
603
603
  operation,
604
- parameters: mergeParameters(document2, item, operation.parameters),
605
- requestBody: operation.requestBody === void 0 ? void 0 : deref(document2, operation.requestBody),
604
+ parameters: mergeParameters(document3, item, operation.parameters),
605
+ requestBody: operation.requestBody === void 0 ? void 0 : deref(document3, operation.requestBody),
606
606
  responses
607
607
  });
608
608
  }
@@ -963,6 +963,1232 @@ var clearNamespace = (sqlite, namespace) => {
963
963
  });
964
964
  };
965
965
 
966
+ // ../../ui/dist/reset.js
967
+ var CSS_RESET = `*,
968
+ *::before,
969
+ *::after {
970
+ box-sizing: border-box;
971
+ margin: 0;
972
+ padding: 0;
973
+ }
974
+
975
+ * {
976
+ scrollbar-width: thin;
977
+ scrollbar-color: color-mix(in srgb, CanvasText 35%, Canvas) Canvas;
978
+ }
979
+
980
+ *::-webkit-scrollbar {
981
+ width: 12px;
982
+ height: 12px;
983
+ }
984
+
985
+ *::-webkit-scrollbar-track,
986
+ *::-webkit-scrollbar-corner {
987
+ background: Canvas;
988
+ }
989
+
990
+ *::-webkit-scrollbar-thumb {
991
+ background: color-mix(in srgb, CanvasText 35%, Canvas);
992
+ border: 3px solid Canvas;
993
+ border-radius: 999px;
994
+ }
995
+
996
+ html {
997
+ hanging-punctuation: first allow-end last;
998
+ -webkit-text-size-adjust: none;
999
+ text-size-adjust: none;
1000
+ tab-size: 4;
1001
+ font-family:
1002
+ ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI Variable", "Segoe UI",
1003
+ "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
1004
+ font-size: 16px;
1005
+ line-height: 1.5;
1006
+ -webkit-font-smoothing: antialiased;
1007
+ -moz-osx-font-smoothing: grayscale;
1008
+ text-rendering: optimizeLegibility;
1009
+ -webkit-tap-highlight-color: transparent;
1010
+ scroll-behavior: smooth;
1011
+ background: Canvas;
1012
+ color: CanvasText;
1013
+ }
1014
+
1015
+ body {
1016
+ min-height: 100svh;
1017
+ line-height: 1.5;
1018
+ background: Canvas;
1019
+ color: CanvasText;
1020
+ }
1021
+
1022
+ h1 {
1023
+ font-size: 2em;
1024
+ }
1025
+
1026
+ h2 {
1027
+ font-size: 1.5em;
1028
+ }
1029
+
1030
+ h3 {
1031
+ font-size: 1.17em;
1032
+ }
1033
+
1034
+ h4 {
1035
+ font-size: 1em;
1036
+ }
1037
+
1038
+ h5 {
1039
+ font-size: 0.83em;
1040
+ }
1041
+
1042
+ h6 {
1043
+ font-size: 0.67em;
1044
+ }
1045
+
1046
+ h1,
1047
+ h2,
1048
+ h3,
1049
+ h4,
1050
+ h5,
1051
+ h6 {
1052
+ font-weight: 700;
1053
+ line-height: 1.2;
1054
+ text-wrap: balance;
1055
+ overflow-wrap: break-word;
1056
+ }
1057
+
1058
+ p,
1059
+ li,
1060
+ figcaption,
1061
+ dd,
1062
+ dt,
1063
+ blockquote {
1064
+ overflow-wrap: break-word;
1065
+ }
1066
+
1067
+ p,
1068
+ li,
1069
+ figcaption,
1070
+ blockquote {
1071
+ text-wrap: pretty;
1072
+ }
1073
+
1074
+ ul,
1075
+ ol,
1076
+ menu {
1077
+ list-style: none;
1078
+ }
1079
+
1080
+ a {
1081
+ color: inherit;
1082
+ text-decoration: underline;
1083
+ text-decoration-thickness: 1px;
1084
+ text-underline-offset: 0.15em;
1085
+ text-decoration-skip-ink: auto;
1086
+ }
1087
+
1088
+ b,
1089
+ strong {
1090
+ font-weight: 700;
1091
+ }
1092
+
1093
+ small {
1094
+ font-size: 0.875em;
1095
+ }
1096
+
1097
+ code,
1098
+ kbd,
1099
+ samp,
1100
+ pre {
1101
+ font-family: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
1102
+ font-size: 1em;
1103
+ }
1104
+
1105
+ sub,
1106
+ sup {
1107
+ font-size: 75%;
1108
+ line-height: 0;
1109
+ position: relative;
1110
+ vertical-align: baseline;
1111
+ }
1112
+
1113
+ sub {
1114
+ bottom: -0.25em;
1115
+ }
1116
+
1117
+ sup {
1118
+ top: -0.5em;
1119
+ }
1120
+
1121
+ abbr[title] {
1122
+ text-decoration: underline dotted;
1123
+ text-underline-offset: 0.15em;
1124
+ }
1125
+
1126
+ mark {
1127
+ background: Mark;
1128
+ color: MarkText;
1129
+ }
1130
+
1131
+ hr {
1132
+ height: 0;
1133
+ border: 0;
1134
+ border-top: 1px solid color-mix(in srgb, CanvasText 20%, transparent);
1135
+ color: inherit;
1136
+ }
1137
+
1138
+ img,
1139
+ picture,
1140
+ video,
1141
+ canvas {
1142
+ display: block;
1143
+ max-width: 100%;
1144
+ }
1145
+
1146
+ img,
1147
+ video {
1148
+ height: auto;
1149
+ }
1150
+
1151
+ svg {
1152
+ overflow: hidden;
1153
+ }
1154
+
1155
+ iframe {
1156
+ border: 0;
1157
+ max-width: 100%;
1158
+ }
1159
+
1160
+ table {
1161
+ border-collapse: collapse;
1162
+ border-spacing: 0;
1163
+ }
1164
+
1165
+ fieldset {
1166
+ border: 0;
1167
+ min-inline-size: 0;
1168
+ }
1169
+
1170
+ legend {
1171
+ padding: 0;
1172
+ }
1173
+
1174
+ button,
1175
+ input,
1176
+ select,
1177
+ textarea,
1178
+ optgroup {
1179
+ font: inherit;
1180
+ letter-spacing: inherit;
1181
+ color: inherit;
1182
+ margin: 0;
1183
+ vertical-align: middle;
1184
+ }
1185
+
1186
+ button,
1187
+ input[type="button"],
1188
+ input[type="submit"],
1189
+ input[type="reset"] {
1190
+ appearance: none;
1191
+ background: none;
1192
+ border: 0;
1193
+ padding: 0;
1194
+ cursor: pointer;
1195
+ text-align: center;
1196
+ }
1197
+
1198
+ input,
1199
+ textarea,
1200
+ select {
1201
+ background-color: Canvas;
1202
+ border: 1px solid color-mix(in srgb, CanvasText 28%, transparent);
1203
+ border-radius: 0;
1204
+ padding: 0.5em 0.65em;
1205
+ }
1206
+
1207
+ input[type="checkbox"],
1208
+ input[type="radio"],
1209
+ input[type="range"],
1210
+ input[type="file"],
1211
+ input[type="color"],
1212
+ input[type="button"],
1213
+ input[type="submit"],
1214
+ input[type="reset"],
1215
+ input[type="image"],
1216
+ input[type="hidden"] {
1217
+ background: none;
1218
+ border: 0;
1219
+ padding: 0;
1220
+ border-radius: 0;
1221
+ }
1222
+
1223
+ input[type="checkbox"],
1224
+ input[type="radio"] {
1225
+ accent-color: CanvasText;
1226
+ }
1227
+
1228
+ input[type="search"] {
1229
+ appearance: none;
1230
+ background-color: Canvas;
1231
+ border: 1px solid color-mix(in srgb, CanvasText 28%, transparent);
1232
+ }
1233
+
1234
+ input[type="search"]::-webkit-search-decoration,
1235
+ input[type="search"]::-webkit-search-cancel-button,
1236
+ input[type="search"]::-webkit-search-results-button,
1237
+ input[type="search"]::-webkit-search-results-decoration {
1238
+ -webkit-appearance: none;
1239
+ display: none;
1240
+ }
1241
+
1242
+ input[type="number"] {
1243
+ appearance: textfield;
1244
+ background-color: Canvas;
1245
+ border: 1px solid color-mix(in srgb, CanvasText 28%, transparent);
1246
+ }
1247
+
1248
+ input[type="number"]::-webkit-inner-spin-button,
1249
+ input[type="number"]::-webkit-outer-spin-button {
1250
+ -webkit-appearance: none;
1251
+ margin: 0;
1252
+ }
1253
+
1254
+ textarea {
1255
+ resize: vertical;
1256
+ }
1257
+
1258
+ ::placeholder {
1259
+ opacity: 1;
1260
+ color: color-mix(in srgb, CanvasText 45%, transparent);
1261
+ }
1262
+
1263
+ :focus-visible {
1264
+ outline: 2px solid CanvasText;
1265
+ outline-offset: 2px;
1266
+ }
1267
+
1268
+ :focus:not(:focus-visible) {
1269
+ outline: none;
1270
+ }
1271
+
1272
+ :disabled,
1273
+ [disabled] {
1274
+ cursor: default;
1275
+ opacity: 0.6;
1276
+ }
1277
+
1278
+ label,
1279
+ summary,
1280
+ select,
1281
+ [role="button"] {
1282
+ cursor: pointer;
1283
+ }
1284
+
1285
+ summary {
1286
+ display: list-item;
1287
+ }
1288
+
1289
+ details {
1290
+ display: block;
1291
+ }
1292
+
1293
+ dialog {
1294
+ margin: auto;
1295
+ padding: 1rem;
1296
+ border: 1px solid color-mix(in srgb, CanvasText 20%, transparent);
1297
+ background: Canvas;
1298
+ color: CanvasText;
1299
+ max-width: calc(100vw - 2rem);
1300
+ max-height: calc(100svh - 2rem);
1301
+ }
1302
+
1303
+ dialog:not([open]) {
1304
+ display: none;
1305
+ }
1306
+
1307
+ ::backdrop {
1308
+ background: color-mix(in srgb, CanvasText 40%, transparent);
1309
+ }
1310
+
1311
+ [hidden] {
1312
+ display: none !important;
1313
+ }
1314
+
1315
+ ::selection {
1316
+ background: Highlight;
1317
+ color: HighlightText;
1318
+ }
1319
+
1320
+ @media (prefers-reduced-motion: reduce) {
1321
+ *,
1322
+ *::before,
1323
+ *::after {
1324
+ animation-duration: 0.01ms !important;
1325
+ animation-iteration-count: 1 !important;
1326
+ transition-duration: 0.01ms !important;
1327
+ scroll-behavior: auto !important;
1328
+ }
1329
+ }
1330
+
1331
+ @media (pointer: coarse) {
1332
+ input,
1333
+ select,
1334
+ textarea {
1335
+ font-size: max(16px, 1em);
1336
+ }
1337
+ }`;
1338
+
1339
+ // ../core/dist/admin-client.js
1340
+ function isRecord2(value) {
1341
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1342
+ }
1343
+ function mountAdminBrand(doc, host, service, catalog) {
1344
+ const clip = (value, max) => {
1345
+ if (typeof value !== "string")
1346
+ return null;
1347
+ const trimmed = value.trim();
1348
+ if (trimmed === "")
1349
+ return null;
1350
+ return trimmed.length > max ? trimmed.slice(0, max) : trimmed;
1351
+ };
1352
+ const httpUrl = (value) => {
1353
+ if (typeof value !== "string" || value.length > 2e3)
1354
+ return null;
1355
+ try {
1356
+ const url = new URL(value);
1357
+ if (url.protocol !== "https:" && url.protocol !== "http:")
1358
+ return null;
1359
+ return url.href;
1360
+ } catch {
1361
+ return null;
1362
+ }
1363
+ };
1364
+ const hexColor = (value) => {
1365
+ if (typeof value !== "string")
1366
+ return null;
1367
+ const raw = value.trim();
1368
+ return /^#[0-9a-fA-F]{3,8}$/.test(raw) ? raw : null;
1369
+ };
1370
+ const hostname = (value) => {
1371
+ try {
1372
+ return new URL(value).hostname.replace(/^www\./, "");
1373
+ } catch {
1374
+ return value;
1375
+ }
1376
+ };
1377
+ host.replaceChildren();
1378
+ host.hidden = true;
1379
+ if (!isRecord2(catalog) || !Object.hasOwn(catalog, service))
1380
+ return;
1381
+ const row = catalog[service];
1382
+ if (!isRecord2(row))
1383
+ return;
1384
+ const vendor = clip(row.vendor, 80);
1385
+ const website = httpUrl(row.website);
1386
+ const guide = httpUrl(row.guide);
1387
+ const docs = httpUrl(row.docs);
1388
+ const logo = httpUrl(row.logo);
1389
+ const color = hexColor(row.color);
1390
+ const description = clip(row.description, 280);
1391
+ if (vendor === null || website === null && guide === null && docs === null)
1392
+ return;
1393
+ const add = (node) => {
1394
+ host.append(node);
1395
+ return node;
1396
+ };
1397
+ if (logo !== null) {
1398
+ const img = doc.createElement("img");
1399
+ img.alt = "";
1400
+ img.src = logo;
1401
+ img.width = 16;
1402
+ img.height = 16;
1403
+ img.decoding = "async";
1404
+ img.referrerPolicy = "no-referrer";
1405
+ if (color !== null)
1406
+ img.style.borderColor = color;
1407
+ add(img);
1408
+ }
1409
+ const name = doc.createElement("span");
1410
+ name.className = "vendor-name";
1411
+ name.textContent = vendor;
1412
+ if (description !== null)
1413
+ name.title = description;
1414
+ add(name);
1415
+ const sep = () => {
1416
+ const node = doc.createElement("span");
1417
+ node.className = "vendor-sep";
1418
+ node.setAttribute("aria-hidden", "true");
1419
+ node.textContent = "\xB7";
1420
+ return node;
1421
+ };
1422
+ const link = (href, label, title) => {
1423
+ const node = doc.createElement("a");
1424
+ node.className = "vendor-link";
1425
+ node.href = href;
1426
+ node.target = "_blank";
1427
+ node.rel = "noopener noreferrer";
1428
+ node.referrerPolicy = "no-referrer";
1429
+ node.textContent = label;
1430
+ node.title = title;
1431
+ return node;
1432
+ };
1433
+ if (website !== null) {
1434
+ add(sep());
1435
+ add(link(website, hostname(website), `${vendor} website`));
1436
+ }
1437
+ if (guide !== null) {
1438
+ add(sep());
1439
+ add(link(guide, "Docs", `Docs for ${vendor}`));
1440
+ }
1441
+ if (docs !== null) {
1442
+ add(sep());
1443
+ add(link(docs, "API", `${vendor} API reference`));
1444
+ }
1445
+ host.removeAttribute("hidden");
1446
+ host.hidden = false;
1447
+ }
1448
+ function startAdminBrand(env) {
1449
+ const requested = env.params.get("brands");
1450
+ let brandsUrl = env.brandsUrl;
1451
+ if (requested) {
1452
+ try {
1453
+ const url = new URL(requested, env.location.href);
1454
+ const local = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "[::1]" || url.origin === env.location.origin;
1455
+ if ((url.protocol === "https:" || url.protocol === "http:") && local)
1456
+ brandsUrl = url.href;
1457
+ } catch {
1458
+ brandsUrl = env.brandsUrl;
1459
+ }
1460
+ }
1461
+ const vendorHost = env.$("vendor");
1462
+ const brandRoot = env.document.querySelector("[data-mockingbird-admin]");
1463
+ const brandService = brandRoot?.getAttribute("data-service") || "";
1464
+ if (vendorHost === null)
1465
+ return;
1466
+ env.fetch(brandsUrl, {
1467
+ headers: { accept: "application/json" },
1468
+ signal: AbortSignal.timeout(5e3)
1469
+ }).then(async (response) => {
1470
+ if (!response.ok)
1471
+ return null;
1472
+ const catalog = await response.json();
1473
+ return catalog;
1474
+ }).then((catalog) => {
1475
+ mountAdminBrand(env.document, vendorHost, brandService, catalog);
1476
+ }).catch(() => {
1477
+ });
1478
+ }
1479
+ function faultPresetList(value) {
1480
+ if (!isRecord2(value))
1481
+ throw new Error("Fault presets response was not an object");
1482
+ if (!Object.hasOwn(value, "presets") || value.presets === void 0 || value.presets === null) {
1483
+ return { presets: [] };
1484
+ }
1485
+ if (!Array.isArray(value.presets))
1486
+ throw new Error("Fault presets response was not a list");
1487
+ return {
1488
+ presets: value.presets.flatMap((item) => {
1489
+ if (!isRecord2(item) || typeof item.name !== "string")
1490
+ return [];
1491
+ return [{ name: item.name }];
1492
+ })
1493
+ };
1494
+ }
1495
+ function bootAdmin(config) {
1496
+ const fieldKinds = ["string", "number", "boolean", "null", "object", "array", "unknown"];
1497
+ const sources = ["declared", "inferred", "mixed"];
1498
+ const isFieldKind = (value) => typeof value === "string" && fieldKinds.some((kind) => kind === value);
1499
+ const isSource = (value) => typeof value === "string" && sources.some((source) => source === value);
1500
+ const readJson2 = (text) => {
1501
+ const value = JSON.parse(text);
1502
+ return value;
1503
+ };
1504
+ const esc = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => {
1505
+ switch (char) {
1506
+ case "&":
1507
+ return "&amp;";
1508
+ case "<":
1509
+ return "&lt;";
1510
+ case ">":
1511
+ return "&gt;";
1512
+ case '"':
1513
+ return "&quot;";
1514
+ default:
1515
+ return "&#39;";
1516
+ }
1517
+ });
1518
+ const el = (id, kind) => {
1519
+ const node = document.getElementById(id);
1520
+ if (!(node instanceof kind))
1521
+ throw new Error(`admin shell is missing #${id}`);
1522
+ return node;
1523
+ };
1524
+ const errorMessage = (data, fallback) => {
1525
+ if (!isRecord2(data))
1526
+ return fallback;
1527
+ const error2 = data.error;
1528
+ if (!isRecord2(error2) || typeof error2.message !== "string" || error2.message === "") {
1529
+ return fallback;
1530
+ }
1531
+ return error2.message;
1532
+ };
1533
+ const requireList = (value, label) => {
1534
+ if (value === void 0 || value === null)
1535
+ return [];
1536
+ if (!Array.isArray(value))
1537
+ throw new Error(`${label} was not a list`);
1538
+ return value;
1539
+ };
1540
+ const state = {
1541
+ namespace: "default",
1542
+ key: sessionStorage.getItem("mockingbird-admin-key") || "",
1543
+ view: "overview",
1544
+ collection: "",
1545
+ shape: null,
1546
+ route: null,
1547
+ editing: null
1548
+ };
1549
+ const standard = new Set(config.standardRoutes);
1550
+ const banner = el("banner", HTMLElement);
1551
+ const keyInput = el("admin-key", HTMLInputElement);
1552
+ keyInput.value = state.key;
1553
+ const params = new URLSearchParams(location.search);
1554
+ const queryKey = params.get("key");
1555
+ if (queryKey) {
1556
+ state.key = queryKey;
1557
+ keyInput.value = state.key;
1558
+ sessionStorage.setItem("mockingbird-admin-key", state.key);
1559
+ params.delete("key");
1560
+ const next = params.toString();
1561
+ history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
1562
+ }
1563
+ const queryNamespace = params.get("namespace");
1564
+ if (queryNamespace)
1565
+ state.namespace = queryNamespace;
1566
+ const showError = (error2) => {
1567
+ banner.textContent = error2 instanceof Error ? error2.message : String(error2);
1568
+ banner.classList.add("show");
1569
+ };
1570
+ const clearError = () => {
1571
+ banner.classList.remove("show");
1572
+ };
1573
+ const api = async (method, path, body) => {
1574
+ const headers = { accept: "application/json" };
1575
+ if (state.key !== "")
1576
+ headers[config.adminKeyHeader] = state.key;
1577
+ if (state.namespace !== "")
1578
+ headers["x-mockingbird-namespace"] = state.namespace;
1579
+ if (body !== void 0)
1580
+ headers["content-type"] = "application/json";
1581
+ const init = { method, headers };
1582
+ if (body !== void 0)
1583
+ init.body = JSON.stringify(body);
1584
+ const response = await fetch(config.adminPrefix + path, init);
1585
+ const text = await response.text();
1586
+ let data = null;
1587
+ if (text !== "") {
1588
+ try {
1589
+ data = readJson2(text);
1590
+ } catch {
1591
+ data = { raw: text };
1592
+ }
1593
+ }
1594
+ if (response.status === 401) {
1595
+ keyInput.focus();
1596
+ throw new Error(errorMessage(data, "Admin key required"));
1597
+ }
1598
+ if (!response.ok) {
1599
+ throw new Error(errorMessage(data, `${response.status} ${method} ${path}`));
1600
+ }
1601
+ return data;
1602
+ };
1603
+ const originGet = async (path) => {
1604
+ const response = await fetch(path, { headers: { accept: "application/json" } });
1605
+ if (!response.ok)
1606
+ throw new Error(`${response.status} ${path}`);
1607
+ const value = await response.json();
1608
+ return value;
1609
+ };
1610
+ const panelApi = {
1611
+ get: (path) => api("GET", path),
1612
+ send: (method, path, body) => api(method, path, body),
1613
+ namespace: () => state.namespace
1614
+ };
1615
+ const isPanelScript = (value) => typeof value === "function";
1616
+ const setTheme = (theme) => {
1617
+ if (theme)
1618
+ document.documentElement.dataset.theme = theme;
1619
+ else
1620
+ delete document.documentElement.dataset.theme;
1621
+ };
1622
+ const storedTheme = localStorage.getItem("mockingbird-admin-theme");
1623
+ if (storedTheme)
1624
+ setTheme(storedTheme);
1625
+ el("theme", HTMLButtonElement).addEventListener("click", () => {
1626
+ const current = document.documentElement.dataset.theme;
1627
+ const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
1628
+ const next = dark ? "light" : "dark";
1629
+ localStorage.setItem("mockingbird-admin-theme", next);
1630
+ setTheme(next);
1631
+ });
1632
+ const show = (view) => {
1633
+ state.view = view;
1634
+ for (const button of document.querySelectorAll("#nav button")) {
1635
+ if (!(button instanceof HTMLButtonElement))
1636
+ continue;
1637
+ button.setAttribute("aria-current", button.dataset.view === view ? "true" : "false");
1638
+ }
1639
+ for (const section of document.querySelectorAll("main .view")) {
1640
+ if (!(section instanceof HTMLElement))
1641
+ continue;
1642
+ section.hidden = section.id !== `view-${view}`;
1643
+ }
1644
+ };
1645
+ el("nav", HTMLElement).addEventListener("click", (event) => {
1646
+ const target = event.target;
1647
+ if (!(target instanceof Element))
1648
+ return;
1649
+ const button = target.closest("button[data-view]");
1650
+ if (!(button instanceof HTMLButtonElement))
1651
+ return;
1652
+ const view = button.dataset.view;
1653
+ if (view === void 0)
1654
+ return;
1655
+ show(view);
1656
+ refresh().catch(showError);
1657
+ });
1658
+ const cards = (items) => `<div class="grid">${items.map((item) => `<article class="card"><div class="k">${item.k}</div><div class="v">${item.v}</div></article>`).join("")}</div>`;
1659
+ const table = (columns, rows) => {
1660
+ if (rows.length === 0)
1661
+ return '<p class="empty">Nothing here yet.</p>';
1662
+ const head = `<tr>${columns.map((column) => `<th>${column.label}</th>`).join("")}</tr>`;
1663
+ const body = rows.map((row) => {
1664
+ const cells = columns.map((column) => `<td data-label="${column.label}">${column.cell(row)}</td>`).join("");
1665
+ const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
1666
+ return `<tr${attrs}>${cells}</tr>`;
1667
+ }).join("");
1668
+ return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
1669
+ };
1670
+ const preview = (value) => {
1671
+ const text = JSON.stringify(value) ?? "null";
1672
+ return esc(text.length > 180 ? `${text.slice(0, 180)}\u2026` : text);
1673
+ };
1674
+ const when = (epoch) => {
1675
+ const date = new Date(epoch);
1676
+ return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
1677
+ };
1678
+ const clockCard = (epoch) => {
1679
+ const date = new Date(epoch);
1680
+ if (Number.isNaN(date.getTime()))
1681
+ return esc(String(epoch));
1682
+ const iso = date.toISOString();
1683
+ return `${esc(iso.slice(0, 10))}<span class="clock-time">${esc(iso.slice(11, 19))} UTC</span>`;
1684
+ };
1685
+ const decodeClock = (value) => {
1686
+ if (!isRecord2(value) || typeof value.now !== "number" || typeof value.offsetMs !== "number" || typeof value.frozen !== "boolean") {
1687
+ throw new Error("Clock response was not a clock");
1688
+ }
1689
+ return { now: value.now, offsetMs: value.offsetMs, frozen: value.frozen };
1690
+ };
1691
+ const decodeField = (value) => {
1692
+ if (!isRecord2(value) || typeof value.name !== "string" || !isFieldKind(value.kind)) {
1693
+ return void 0;
1694
+ }
1695
+ const field = {
1696
+ name: value.name,
1697
+ kind: value.kind,
1698
+ optional: value.optional === true
1699
+ };
1700
+ if (typeof value.description === "string")
1701
+ field.description = value.description;
1702
+ if (Array.isArray(value.fields)) {
1703
+ field.fields = value.fields.flatMap((item) => {
1704
+ const child = decodeField(item);
1705
+ return child === void 0 ? [] : [child];
1706
+ });
1707
+ }
1708
+ return field;
1709
+ };
1710
+ const decodeCollection = (value) => {
1711
+ if (!isRecord2(value) || typeof value.name !== "string")
1712
+ return void 0;
1713
+ const fields = Array.isArray(value.fields) ? value.fields.flatMap((item) => {
1714
+ const field = decodeField(item);
1715
+ return field === void 0 ? [] : [field];
1716
+ }) : [];
1717
+ const collection = {
1718
+ name: value.name,
1719
+ label: typeof value.label === "string" ? value.label : value.name,
1720
+ count: typeof value.count === "number" ? value.count : 0,
1721
+ source: isSource(value.source) ? value.source : "inferred",
1722
+ fields
1723
+ };
1724
+ if (typeof value.description === "string")
1725
+ collection.description = value.description;
1726
+ return collection;
1727
+ };
1728
+ const decodeState = (value) => {
1729
+ if (!isRecord2(value) || !Array.isArray(value.collections)) {
1730
+ throw new Error("State response has no collection list");
1731
+ }
1732
+ return {
1733
+ namespace: typeof value.namespace === "string" ? value.namespace : state.namespace,
1734
+ storageNamespace: typeof value.storageNamespace === "string" ? value.storageNamespace : "",
1735
+ collections: value.collections.flatMap((item) => {
1736
+ const collection = decodeCollection(item);
1737
+ return collection === void 0 ? [] : [collection];
1738
+ })
1739
+ };
1740
+ };
1741
+ const decodeRecords = (value) => requireList(isRecord2(value) ? value.records : void 0, "Records").flatMap((item) => {
1742
+ if (!isRecord2(item) || typeof item.id !== "string")
1743
+ return [];
1744
+ return [
1745
+ {
1746
+ id: item.id,
1747
+ seq: typeof item.seq === "number" ? item.seq : 0,
1748
+ value: item.value,
1749
+ attrs: ` class="clickable" data-id="${esc(item.id)}"`
1750
+ }
1751
+ ];
1752
+ });
1753
+ const textOf = (value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? String(value) : "";
1754
+ const decodeJournal = (value) => requireList(isRecord2(value) ? value.requests : void 0, "Journal").flatMap((item) => {
1755
+ if (!isRecord2(item))
1756
+ return [];
1757
+ return [
1758
+ {
1759
+ at: textOf(item.at),
1760
+ method: textOf(item.method),
1761
+ path: textOf(item.path),
1762
+ status: textOf(item.status),
1763
+ durationMs: textOf(item.durationMs),
1764
+ operationId: textOf(item.operationId)
1765
+ }
1766
+ ];
1767
+ });
1768
+ const decodeFaults = (value) => requireList(isRecord2(value) ? value.faults : void 0, "Faults").flatMap((item) => {
1769
+ if (!isRecord2(item))
1770
+ return [];
1771
+ return [
1772
+ {
1773
+ id: textOf(item.id),
1774
+ method: typeof item.method === "string" ? item.method : void 0,
1775
+ pathPrefix: typeof item.pathPrefix === "string" ? item.pathPrefix : void 0,
1776
+ operationId: typeof item.operationId === "string" ? item.operationId : void 0,
1777
+ status: typeof item.status === "number" ? item.status : void 0,
1778
+ effect: typeof item.effect === "string" ? item.effect : void 0,
1779
+ drop: typeof item.drop === "boolean" ? item.drop : void 0,
1780
+ latencyMs: typeof item.latencyMs === "number" ? item.latencyMs : void 0
1781
+ }
1782
+ ];
1783
+ });
1784
+ const loadNamespaces = async () => {
1785
+ const data = await api("GET", "/namespaces");
1786
+ const select = el("namespace", HTMLSelectElement);
1787
+ const listed = requireList(isRecord2(data) ? data.namespaces : void 0, "Namespaces");
1788
+ const fallback = isRecord2(data) && typeof data.default === "string" && data.default !== "" ? data.default : "default";
1789
+ const names = (listed.length > 0 ? listed.map((name) => String(name)) : [fallback]).slice();
1790
+ if (!names.includes(state.namespace))
1791
+ names.unshift(state.namespace);
1792
+ select.innerHTML = names.map((name) => `<option value="${esc(name)}">${esc(name)}</option>`).join("");
1793
+ select.value = state.namespace;
1794
+ };
1795
+ const renderOverview = async () => {
1796
+ const [health, shape, clock, journal] = await Promise.all([
1797
+ originGet("/health"),
1798
+ api("GET", "/state"),
1799
+ api("GET", "/clock"),
1800
+ api("GET", "/requests?limit=8")
1801
+ ]);
1802
+ if (!isRecord2(health))
1803
+ throw new Error("Health response was not an object");
1804
+ const decoded = decodeState(shape);
1805
+ state.shape = decoded;
1806
+ const time = decodeClock(clock);
1807
+ el("overview-cards", HTMLElement).innerHTML = cards([
1808
+ {
1809
+ k: "Service",
1810
+ v: esc(typeof health.service === "string" && health.service !== "" ? health.service : config.service)
1811
+ },
1812
+ {
1813
+ k: "Status",
1814
+ v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
1815
+ },
1816
+ { k: "Namespace", v: esc(state.namespace) },
1817
+ { k: "Collections", v: String(decoded.collections.length) },
1818
+ {
1819
+ k: "Records",
1820
+ v: String(decoded.collections.reduce((sum, item) => sum + item.count, 0))
1821
+ },
1822
+ { k: "Clock", v: clockCard(time.now) }
1823
+ ]);
1824
+ el("overview-requests", HTMLElement).innerHTML = table([
1825
+ { label: "Method", cell: (row) => esc(row.method) },
1826
+ { label: "Path", cell: (row) => esc(row.path) },
1827
+ {
1828
+ label: "Status",
1829
+ cell: (row) => `<span class="status-${row.status.charAt(0)}">${esc(row.status)}</span>`
1830
+ },
1831
+ { label: "Operation", cell: (row) => esc(row.operationId) }
1832
+ ], decodeJournal(journal));
1833
+ };
1834
+ const schemaChips = (collection) => {
1835
+ if (collection.fields.length === 0) {
1836
+ return '<p class="empty">No fields yet. Add a record or declare the collection on the service.</p>';
1837
+ }
1838
+ return `<div class="chips">${collection.fields.map((field) => `<span class="chip">${esc(field.name)} <i>${esc(field.kind)}${field.optional ? "?" : ""}</i></span>`).join("")}</div>`;
1839
+ };
1840
+ const renderState = async () => {
1841
+ const shape = decodeState(await api("GET", "/state"));
1842
+ state.shape = shape;
1843
+ const host = el("collections", HTMLElement);
1844
+ if (shape.collections.length === 0) {
1845
+ host.innerHTML = '<p class="empty">This namespace has no collections yet.</p>';
1846
+ el("schema", HTMLElement).innerHTML = "";
1847
+ el("records", HTMLElement).innerHTML = "";
1848
+ return;
1849
+ }
1850
+ if (!shape.collections.some((item) => item.name === state.collection)) {
1851
+ const first = shape.collections[0];
1852
+ if (first === void 0)
1853
+ return;
1854
+ state.collection = first.name;
1855
+ }
1856
+ host.innerHTML = shape.collections.map((item) => `<button type="button" class="collection" data-collection="${esc(item.name)}" aria-current="${item.name === state.collection ? "true" : "false"}"><strong>${esc(item.label)}</strong><small>${esc(item.count)} \xB7 ${esc(item.source)}</small></button>`).join("");
1857
+ const selected = shape.collections.find((item) => item.name === state.collection);
1858
+ if (selected === void 0)
1859
+ return;
1860
+ const description = selected.description ? `<p class="lede">${esc(selected.description)}</p>` : "";
1861
+ el("schema", HTMLElement).innerHTML = `<h3>${esc(selected.label)}</h3>${description}${schemaChips(selected)}`;
1862
+ const page = await api("GET", `/state/${encodeURIComponent(state.collection)}?limit=50`);
1863
+ el("records", HTMLElement).innerHTML = table([
1864
+ { label: "Id", cell: (row) => `<code>${esc(row.id)}</code>` },
1865
+ { label: "Seq", cell: (row) => esc(row.seq) },
1866
+ { label: "Value", cell: (row) => preview(row.value) }
1867
+ ], decodeRecords(page));
1868
+ };
1869
+ const renderClock = async () => {
1870
+ const clock = decodeClock(await api("GET", "/clock"));
1871
+ el("clock-readout", HTMLElement).innerHTML = `<div class="k">Now</div><div class="v">${esc(when(clock.now))}</div><p>${esc(clock.now)} ms \xB7 offset ${esc(clock.offsetMs)} ms ${clock.frozen ? "<span class='badge warn'>frozen</span>" : "<span class='badge ok'>live</span>"}</p>`;
1872
+ const freeze = el("freeze", HTMLButtonElement);
1873
+ freeze.textContent = clock.frozen ? "Unfreeze" : "Freeze";
1874
+ freeze.dataset.frozen = clock.frozen ? "1" : "0";
1875
+ };
1876
+ const renderFaults = async () => {
1877
+ const [faults, presets] = await Promise.all([
1878
+ api("GET", "/faults"),
1879
+ api("GET", "/faults/presets").catch(() => ({ presets: [] }))
1880
+ ]);
1881
+ const select = el("fault-preset", HTMLSelectElement);
1882
+ const current = select.value;
1883
+ const list = faultPresetList(presets).presets;
1884
+ select.innerHTML = `<option value="">Custom rule</option>${list.map((preset) => `<option value="${esc(preset.name)}">${esc(preset.name)}</option>`).join("")}`;
1885
+ select.value = current;
1886
+ el("fault-list", HTMLElement).innerHTML = table([
1887
+ { label: "Id", cell: (row) => esc(row.id) },
1888
+ {
1889
+ label: "Match",
1890
+ cell: (row) => esc([row.method, row.pathPrefix, row.operationId].filter((part) => Boolean(part)).join(" ") || "all")
1891
+ },
1892
+ {
1893
+ label: "Effect",
1894
+ cell: (row) => esc(row.status ?? row.effect ?? (row.drop ? "drop" : row.latencyMs || ""))
1895
+ }
1896
+ ], decodeFaults(faults));
1897
+ };
1898
+ const renderJournal = async () => {
1899
+ const journal = await api("GET", "/requests?limit=100");
1900
+ el("journal", HTMLElement).innerHTML = table([
1901
+ { label: "When", cell: (row) => esc(row.at) },
1902
+ { label: "Method", cell: (row) => esc(row.method) },
1903
+ { label: "Path", cell: (row) => esc(row.path) },
1904
+ { label: "Status", cell: (row) => esc(row.status) },
1905
+ { label: "ms", cell: (row) => esc(row.durationMs) }
1906
+ ], decodeJournal(journal));
1907
+ };
1908
+ const renderRoutes = async () => {
1909
+ const data = await api("GET", "/");
1910
+ if (!isRecord2(data))
1911
+ throw new Error("Route list was not an object");
1912
+ const routes = requireList(data.routes, "Routes").map((route) => String(route));
1913
+ el("route-list", HTMLElement).innerHTML = routes.map((route) => {
1914
+ const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
1915
+ const parts = route.split(" ");
1916
+ const method = parts[0] ?? "";
1917
+ const path = parts.slice(1).join(" ");
1918
+ return `<div class="route"><span class="method">${esc(method)}</span><span>${esc(path)}${extra}</span><button class="btn" type="button" data-route="${esc(route)}">Use</button></div>`;
1919
+ }).join("");
1920
+ };
1921
+ const renderers = {
1922
+ overview: renderOverview,
1923
+ state: renderState,
1924
+ clock: renderClock,
1925
+ faults: renderFaults,
1926
+ journal: renderJournal,
1927
+ routes: renderRoutes
1928
+ };
1929
+ async function refresh() {
1930
+ clearError();
1931
+ const render = renderers[state.view];
1932
+ if (render)
1933
+ await render();
1934
+ }
1935
+ el("namespace", HTMLSelectElement).addEventListener("change", () => {
1936
+ state.namespace = el("namespace", HTMLSelectElement).value;
1937
+ refresh().catch(showError);
1938
+ });
1939
+ keyInput.addEventListener("change", () => {
1940
+ state.key = keyInput.value.trim();
1941
+ sessionStorage.setItem("mockingbird-admin-key", state.key);
1942
+ refresh().catch(showError);
1943
+ });
1944
+ el("refresh", HTMLButtonElement).addEventListener("click", () => {
1945
+ refresh().catch(showError);
1946
+ });
1947
+ el("collections", HTMLElement).addEventListener("click", (event) => {
1948
+ const target = event.target;
1949
+ if (!(target instanceof Element))
1950
+ return;
1951
+ const button = target.closest("[data-collection]");
1952
+ if (!(button instanceof HTMLElement))
1953
+ return;
1954
+ const collection = button.dataset.collection;
1955
+ if (collection === void 0)
1956
+ return;
1957
+ state.collection = collection;
1958
+ renderState().catch(showError);
1959
+ });
1960
+ el("records", HTMLElement).addEventListener("click", (event) => {
1961
+ const target = event.target;
1962
+ if (!(target instanceof Element))
1963
+ return;
1964
+ const row = target.closest("[data-id]");
1965
+ if (!(row instanceof HTMLElement))
1966
+ return;
1967
+ const id = row.dataset.id;
1968
+ if (id === void 0)
1969
+ return;
1970
+ openEditor(id).catch(showError);
1971
+ });
1972
+ const editor = el("editor", HTMLDialogElement);
1973
+ const editorId = el("editor-id", HTMLInputElement);
1974
+ const editorValue = el("editor-value", HTMLTextAreaElement);
1975
+ const editorDelete = el("editor-delete", HTMLButtonElement);
1976
+ async function openEditor(id) {
1977
+ state.editing = id;
1978
+ el("editor-title", HTMLElement).textContent = id ? "Edit record" : "New record";
1979
+ editorId.value = id ?? "";
1980
+ editorId.readOnly = id !== null;
1981
+ editorDelete.hidden = id === null;
1982
+ if (id) {
1983
+ const record2 = await api("GET", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`);
1984
+ if (!isRecord2(record2))
1985
+ throw new Error("Record response was not an object");
1986
+ editorValue.value = JSON.stringify(record2.value, null, 2) ?? "";
1987
+ } else {
1988
+ editorValue.value = "{\n \n}";
1989
+ }
1990
+ editor.showModal();
1991
+ }
1992
+ el("new-record", HTMLButtonElement).addEventListener("click", () => {
1993
+ openEditor(null).catch(showError);
1994
+ });
1995
+ el("editor-form", HTMLFormElement).addEventListener("submit", (event) => {
1996
+ const submitter = event.submitter;
1997
+ if (!(submitter instanceof HTMLButtonElement) || submitter.value !== "save")
1998
+ return;
1999
+ event.preventDefault();
2000
+ let value;
2001
+ try {
2002
+ value = readJson2(editorValue.value);
2003
+ } catch {
2004
+ showError(new Error("Value is not JSON"));
2005
+ return;
2006
+ }
2007
+ const id = editorId.value.trim();
2008
+ const path = `/state/${encodeURIComponent(state.collection)}`;
2009
+ const request = state.editing ? api("PUT", `${path}/${encodeURIComponent(id)}`, { value }) : api("POST", path, { id: id || void 0, value });
2010
+ request.then(() => {
2011
+ editor.close();
2012
+ return renderState();
2013
+ }).catch(showError);
2014
+ });
2015
+ editorDelete.addEventListener("click", () => {
2016
+ const id = state.editing;
2017
+ if (!id)
2018
+ return;
2019
+ api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
2020
+ editor.close();
2021
+ return renderState();
2022
+ }).catch(showError);
2023
+ });
2024
+ for (const button of document.querySelectorAll("[data-advance]")) {
2025
+ if (!(button instanceof HTMLButtonElement))
2026
+ continue;
2027
+ button.addEventListener("click", () => {
2028
+ const raw = button.dataset.advance;
2029
+ if (raw === void 0)
2030
+ return;
2031
+ api("POST", "/clock", { advance: Number(raw) }).then(() => renderClock()).catch(showError);
2032
+ });
2033
+ }
2034
+ el("freeze", HTMLButtonElement).addEventListener("click", () => {
2035
+ const frozen = el("freeze", HTMLButtonElement).dataset.frozen === "1";
2036
+ api("POST", "/clock", { freeze: !frozen }).then(() => renderClock()).catch(showError);
2037
+ });
2038
+ el("clock-reset", HTMLButtonElement).addEventListener("click", () => {
2039
+ api("POST", "/clock", { reset: true }).then(() => renderClock()).catch(showError);
2040
+ });
2041
+ el("clock-set", HTMLFormElement).addEventListener("submit", (event) => {
2042
+ event.preventDefault();
2043
+ const raw = el("clock-instant", HTMLInputElement).value.trim();
2044
+ const set = /^-?\d+$/.test(raw) ? Number(raw) : raw;
2045
+ api("POST", "/clock", { set }).then(() => renderClock()).catch(showError);
2046
+ });
2047
+ el("fault-form", HTMLFormElement).addEventListener("submit", (event) => {
2048
+ event.preventDefault();
2049
+ const preset = el("fault-preset", HTMLSelectElement).value;
2050
+ if (preset !== "") {
2051
+ api("POST", "/faults", { preset }).then(() => renderFaults()).catch(showError);
2052
+ return;
2053
+ }
2054
+ const status = Number(el("fault-status", HTMLInputElement).value);
2055
+ if (!Number.isFinite(status)) {
2056
+ showError(new Error("Status must be a number, or choose a preset"));
2057
+ return;
2058
+ }
2059
+ const path = el("fault-path", HTMLInputElement).value;
2060
+ const method = el("fault-method", HTMLInputElement).value;
2061
+ api("POST", "/faults", {
2062
+ status,
2063
+ ...path !== "" ? { pathPrefix: path } : {},
2064
+ ...method !== "" ? { method } : {}
2065
+ }).then(() => renderFaults()).catch(showError);
2066
+ });
2067
+ el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
2068
+ api("DELETE", "/faults").then(() => renderFaults()).catch(showError);
2069
+ });
2070
+ el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
2071
+ api("DELETE", "/requests").then(() => renderJournal()).catch(showError);
2072
+ });
2073
+ el("route-list", HTMLElement).addEventListener("click", (event) => {
2074
+ const target = event.target;
2075
+ if (!(target instanceof Element))
2076
+ return;
2077
+ const button = target.closest("[data-route]");
2078
+ if (!(button instanceof HTMLElement))
2079
+ return;
2080
+ const route = button.dataset.route;
2081
+ if (route === void 0)
2082
+ return;
2083
+ state.route = route;
2084
+ el("route-title", HTMLElement).textContent = route;
2085
+ const method = route.split(" ")[0] ?? "";
2086
+ el("route-body", HTMLTextAreaElement).disabled = method === "GET" || method === "DELETE";
2087
+ });
2088
+ el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
2089
+ event.preventDefault();
2090
+ const send = async () => {
2091
+ if (state.route === null)
2092
+ throw new Error("Choose a route first");
2093
+ const parts = state.route.split(" ");
2094
+ const method = parts[0];
2095
+ const path = parts.slice(1).join(" ");
2096
+ if (method === void 0)
2097
+ throw new Error("Choose a route first");
2098
+ const concrete = path.replace(/:([A-Za-z0-9_]+)/g, (_match, key) => prompt(`Value for ${key}`) || "");
2099
+ let body;
2100
+ const routeBody = el("route-body", HTMLTextAreaElement);
2101
+ if (method !== "GET" && method !== "DELETE" && routeBody.value.trim() !== "") {
2102
+ try {
2103
+ body = readJson2(routeBody.value);
2104
+ } catch {
2105
+ throw new Error("Body is not JSON");
2106
+ }
2107
+ }
2108
+ const data = await api(method, concrete, body);
2109
+ el("route-result", HTMLElement).textContent = JSON.stringify(data, null, 2);
2110
+ };
2111
+ send().catch(showError);
2112
+ });
2113
+ const decodePanel = (value) => {
2114
+ if (!isRecord2(value) || typeof value.id !== "string" || typeof value.title !== "string") {
2115
+ return void 0;
2116
+ }
2117
+ const panel = {
2118
+ id: value.id,
2119
+ title: value.title,
2120
+ description: typeof value.description === "string" ? value.description : "",
2121
+ html: typeof value.html === "string" ? value.html : ""
2122
+ };
2123
+ if (typeof value.script === "string")
2124
+ panel.script = value.script;
2125
+ return panel;
2126
+ };
2127
+ const mountPanels = (panels) => {
2128
+ const nav = el("nav", HTMLElement);
2129
+ const views = el("views", HTMLElement);
2130
+ for (const panel of panels) {
2131
+ const button = document.createElement("button");
2132
+ button.type = "button";
2133
+ button.dataset.view = panel.id;
2134
+ button.textContent = panel.title;
2135
+ nav.append(button);
2136
+ const section = document.createElement("section");
2137
+ section.className = "view";
2138
+ section.id = `view-${panel.id}`;
2139
+ section.hidden = true;
2140
+ const title = document.createElement("h2");
2141
+ title.textContent = panel.title;
2142
+ const lede = document.createElement("p");
2143
+ lede.className = "lede";
2144
+ lede.textContent = panel.description;
2145
+ const body = document.createElement("div");
2146
+ body.className = "panel-body";
2147
+ body.id = `panel-${panel.id}`;
2148
+ body.innerHTML = panel.html;
2149
+ section.append(title, lede, body);
2150
+ views.append(section);
2151
+ if (panel.script === void 0)
2152
+ continue;
2153
+ try {
2154
+ const compiled = new Function("root", "api", panel.script);
2155
+ if (!isPanelScript(compiled))
2156
+ throw new Error("panel script did not compile");
2157
+ compiled(body, panelApi);
2158
+ } catch (error2) {
2159
+ const note = document.createElement("p");
2160
+ note.className = "banner show";
2161
+ note.textContent = error2 instanceof Error ? error2.message : String(error2);
2162
+ body.append(note);
2163
+ }
2164
+ }
2165
+ };
2166
+ startAdminBrand({
2167
+ document,
2168
+ $: (id) => document.getElementById(id),
2169
+ params,
2170
+ location: { href: location.href, origin: location.origin },
2171
+ fetch,
2172
+ brandsUrl: config.brandsUrl
2173
+ });
2174
+ loadNamespaces().then(() => api("GET", "/ui/manifest")).then((manifest) => {
2175
+ const panels = requireList(isRecord2(manifest) ? manifest.panels : void 0, "Panels").flatMap((panel) => {
2176
+ const decoded = decodePanel(panel);
2177
+ return decoded === void 0 ? [] : [decoded];
2178
+ });
2179
+ mountPanels(panels);
2180
+ return refresh();
2181
+ }).catch(showError);
2182
+ }
2183
+ var clientPrelude = [isRecord2, mountAdminBrand, startAdminBrand, faultPresetList, bootAdmin].map((fn) => fn.toString()).join("\n");
2184
+ var adminClientSource = (config) => {
2185
+ const payload = JSON.stringify(config).replace(/</g, "\\u003c");
2186
+ return `<script>
2187
+ ${clientPrelude}
2188
+ bootAdmin(${payload});
2189
+ </script>`;
2190
+ };
2191
+
966
2192
  // ../core/dist/surface.js
967
2193
  var defineMock = (runtime) => runtime;
968
2194
  var STANDARD_ADMIN_ROUTES = [
@@ -1031,94 +2257,106 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
1031
2257
  return "&#39;";
1032
2258
  }
1033
2259
  });
2260
+ var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
1034
2261
  var renderAdminDocument = (service) => {
1035
2262
  const name = escapeHtml(service);
1036
- const standard = JSON.stringify(STANDARD_ADMIN_ROUTES);
2263
+ const script = adminClientSource({
2264
+ standardRoutes: STANDARD_ADMIN_ROUTES,
2265
+ adminPrefix: ADMIN_PREFIX,
2266
+ adminKeyHeader: ADMIN_KEY_HEADER,
2267
+ brandsUrl: ADMIN_BRANDS_URL,
2268
+ service
2269
+ });
1037
2270
  return `<!DOCTYPE html>
1038
2271
  <html lang="en">
1039
2272
  <head>
1040
2273
  <meta charset="utf-8">
1041
2274
  <meta name="viewport" content="width=device-width, initial-scale=1">
1042
2275
  <meta name="color-scheme" content="light dark">
1043
- <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%233d4f3a'/%3E%3C/svg%3E">
2276
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='2' fill='%2327272a'/%3E%3C/svg%3E">
1044
2277
  <title>${name} admin</title>
1045
2278
  <style>
2279
+ ${CSS_RESET}
1046
2280
  :root {
1047
2281
  color-scheme: light;
1048
- --bg: #f6f5f2;
2282
+ --bg: #fafafa;
1049
2283
  --bg-elev: #ffffff;
1050
- --bg-muted: #eeece6;
1051
- --bg-inset: #f3f1eb;
1052
- --line: #e2dfd6;
1053
- --line-strong: #d0ccc1;
1054
- --ink: #1c1915;
1055
- --muted: #5e584f;
1056
- --faint: #8a8378;
1057
- --accent: #3d4f3a;
1058
- --accent-ink: #f7f6f2;
1059
- --accent-soft: #e5ece2;
1060
- --ok: #1f7a45;
1061
- --ok-soft: #e5f4eb;
1062
- --warn: #9a6700;
1063
- --warn-soft: #fbf3df;
1064
- --err: #a33b32;
1065
- --err-soft: #fdecea;
1066
- --shadow: 0 1px 2px rgb(28 25 21 / 0.05), 0 12px 32px rgb(28 25 21 / 0.06);
1067
- --sans: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
2284
+ --bg-muted: #f4f4f5;
2285
+ --bg-inset: #f4f4f5;
2286
+ --line: #dedee3;
2287
+ --line-strong: #c4c4cc;
2288
+ --ink: #18181b;
2289
+ --muted: #62626b;
2290
+ --faint: #71717a;
2291
+ --accent: #27272a;
2292
+ --accent-ink: #ffffff;
2293
+ --accent-soft: #f4f4f5;
2294
+ --ok: #166534;
2295
+ --ok-soft: #f0fdf4;
2296
+ --warn: #a16207;
2297
+ --warn-soft: #fefce8;
2298
+ --err: #a82d32;
2299
+ --err-soft: #fef2f2;
2300
+ --shadow: none;
2301
+ --sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
1068
2302
  --mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
1069
- --radius: 14px;
1070
- --header: 60px;
1071
- --nav: 220px;
2303
+ --radius: 10px;
2304
+ --header: 56px;
2305
+ --nav: 200px;
1072
2306
  }
1073
2307
  :root[data-theme="dark"] {
1074
2308
  color-scheme: dark;
1075
- --bg: #141311;
1076
- --bg-elev: #1e1c19;
1077
- --bg-muted: #26231f;
1078
- --bg-inset: #181715;
1079
- --line: #34302b;
1080
- --line-strong: #4a453d;
1081
- --ink: #f4f1ea;
1082
- --muted: #c4bdb1;
1083
- --faint: #8f877c;
1084
- --accent: #c5d5b8;
1085
- --accent-ink: #1a2118;
1086
- --accent-soft: #2a3328;
1087
- --ok: #8fd6a8;
1088
- --ok-soft: #1a2e22;
1089
- --warn: #f0cc78;
1090
- --warn-soft: #332911;
1091
- --err: #f0a8a2;
1092
- --err-soft: #3a201e;
1093
- --shadow: 0 1px 2px rgb(0 0 0 / 0.3), 0 16px 40px rgb(0 0 0 / 0.28);
2309
+ --bg: #111113;
2310
+ --bg-elev: #19191c;
2311
+ --bg-muted: #242428;
2312
+ --bg-inset: #111113;
2313
+ --line: #36363c;
2314
+ --line-strong: #52525b;
2315
+ --ink: #f4f4f5;
2316
+ --muted: #a9a9b2;
2317
+ --faint: #71717a;
2318
+ --accent: #e4e4e7;
2319
+ --accent-ink: #18181b;
2320
+ --accent-soft: #242428;
2321
+ --ok: #86efac;
2322
+ --ok-soft: #14241b;
2323
+ --warn: #fde68a;
2324
+ --warn-soft: #2a2410;
2325
+ --err: #ffaaaa;
2326
+ --err-soft: #2c1516;
2327
+ --shadow: none;
1094
2328
  }
1095
2329
  @media (prefers-color-scheme: dark) {
1096
2330
  :root:not([data-theme="light"]) {
1097
2331
  color-scheme: dark;
1098
- --bg: #141311;
1099
- --bg-elev: #1e1c19;
1100
- --bg-muted: #26231f;
1101
- --bg-inset: #181715;
1102
- --line: #34302b;
1103
- --line-strong: #4a453d;
1104
- --ink: #f4f1ea;
1105
- --muted: #c4bdb1;
1106
- --faint: #8f877c;
1107
- --accent: #c5d5b8;
1108
- --accent-ink: #1a2118;
1109
- --accent-soft: #2a3328;
1110
- --ok: #8fd6a8;
1111
- --ok-soft: #1a2e22;
1112
- --warn: #f0cc78;
1113
- --warn-soft: #332911;
1114
- --err: #f0a8a2;
1115
- --err-soft: #3a201e;
1116
- --shadow: 0 1px 2px rgb(0 0 0 / 0.3), 0 16px 40px rgb(0 0 0 / 0.28);
2332
+ --bg: #111113;
2333
+ --bg-elev: #19191c;
2334
+ --bg-muted: #242428;
2335
+ --bg-inset: #111113;
2336
+ --line: #36363c;
2337
+ --line-strong: #52525b;
2338
+ --ink: #f4f4f5;
2339
+ --muted: #a9a9b2;
2340
+ --faint: #71717a;
2341
+ --accent: #e4e4e7;
2342
+ --accent-ink: #18181b;
2343
+ --accent-soft: #242428;
2344
+ --ok: #86efac;
2345
+ --ok-soft: #14241b;
2346
+ --warn: #fde68a;
2347
+ --warn-soft: #2a2410;
2348
+ --err: #ffaaaa;
2349
+ --err-soft: #2c1516;
2350
+ --shadow: none;
1117
2351
  }
1118
2352
  }
1119
- * { box-sizing: border-box; }
1120
- html, body { margin: 0; height: 100%; }
2353
+ /* Fill the viewport, then grow with the content so the background covers the scroll. */
2354
+ html { height: 100%; }
1121
2355
  body {
2356
+ margin: 0;
2357
+ min-height: 100%;
2358
+ display: flex;
2359
+ flex-direction: column;
1122
2360
  font-family: var(--sans);
1123
2361
  background: var(--bg);
1124
2362
  color: var(--ink);
@@ -1127,24 +2365,37 @@ var renderAdminDocument = (service) => {
1127
2365
  padding-right: env(safe-area-inset-right);
1128
2366
  -webkit-text-size-adjust: 100%;
1129
2367
  }
1130
- button, input, select, textarea { font: inherit; color: inherit; }
1131
- button { cursor: pointer; }
1132
2368
  :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
1133
- @media (prefers-reduced-motion: reduce) {
1134
- * { scroll-behavior: auto !important; transition: none !important; }
1135
- }
1136
- .app { min-height: 100%; display: flex; flex-direction: column; }
2369
+ .app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
1137
2370
  header.top {
1138
2371
  position: sticky; top: 0; z-index: 5;
1139
2372
  display: flex; align-items: center; gap: 12px;
1140
2373
  min-height: var(--header); padding: 10px 16px;
1141
- background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
2374
+ background: var(--bg-elev);
1142
2375
  border-bottom: 1px solid var(--line);
1143
- backdrop-filter: blur(10px);
1144
- }
1145
- .brand { display: flex; flex-direction: column; min-width: 0; margin-right: auto; }
1146
- .brand strong { font-size: 15px; letter-spacing: -0.01em; }
1147
- .brand span { color: var(--faint); font-size: 12px; }
2376
+ min-width: 0; max-width: 100%;
2377
+ }
2378
+ .brand {
2379
+ display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
2380
+ min-width: 0; max-width: 100%; margin-right: auto;
2381
+ }
2382
+ .brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; }
2383
+ .brand-id strong { font-size: 15px; letter-spacing: -0.01em; }
2384
+ .brand-id span { color: var(--faint); font-size: 12px; }
2385
+ .vendor {
2386
+ display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
2387
+ min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
2388
+ }
2389
+ .vendor[hidden] { display: none; }
2390
+ /* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
2391
+ .vendor img {
2392
+ width: 16px; height: 16px; padding: 2px; box-sizing: content-box;
2393
+ border-radius: 4px; border: 1px solid var(--line); background: #fff; object-fit: contain;
2394
+ }
2395
+ .vendor-name { color: var(--ink); font-weight: 600; }
2396
+ .vendor-sep { color: var(--faint); }
2397
+ .vendor-link { color: inherit; text-decoration: none; }
2398
+ .vendor-link:hover { color: var(--ink); text-decoration: underline; }
1148
2399
  .controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
1149
2400
  label.field { display: flex; flex-direction: column; gap: 2px; font-size: 11px; color: var(--faint); }
1150
2401
  label.field input, label.field select, .controls input[type="password"] {
@@ -1162,7 +2413,7 @@ var renderAdminDocument = (service) => {
1162
2413
  record or the full nav row would widen the page past the viewport. */
1163
2414
  .layout {
1164
2415
  display: grid; grid-template-columns: var(--nav) minmax(0, 1fr);
1165
- flex: 1; min-height: 0; min-width: 0;
2416
+ flex: 1 0 auto; min-width: 0;
1166
2417
  }
1167
2418
  nav.side {
1168
2419
  border-right: 1px solid var(--line); padding: 12px;
@@ -1177,7 +2428,7 @@ var renderAdminDocument = (service) => {
1177
2428
  main { padding: 20px; max-width: 1100px; width: 100%; min-width: 0; }
1178
2429
  .view h2 { margin: 0 0 4px; font-size: 22px; letter-spacing: -0.02em; }
1179
2430
  .lede { margin: 0 0 16px; color: var(--muted); }
1180
- .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; }
2431
+ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 180px), 1fr)); gap: 12px; }
1181
2432
  .card {
1182
2433
  background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
1183
2434
  padding: 14px 16px; box-shadow: var(--shadow);
@@ -1216,8 +2467,8 @@ var renderAdminDocument = (service) => {
1216
2467
  .banner.show { display: block; }
1217
2468
  .empty { color: var(--muted); padding: 24px 8px; }
1218
2469
  dialog {
1219
- border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--bg-elev);
1220
- color: var(--ink); width: min(640px, calc(100vw - 24px)); box-shadow: var(--shadow);
2470
+ border: 1px solid var(--line); border-radius: var(--radius); padding: 0; background: var(--bg-elev);
2471
+ color: var(--ink); width: min(640px, calc(100% - 24px)); max-width: calc(100vw - 24px); box-shadow: none;
1221
2472
  }
1222
2473
  dialog::backdrop { background: rgb(20 19 17 / 0.45); }
1223
2474
  dialog form, .dialog-body { padding: 16px; display: flex; flex-direction: column; gap: 10px; }
@@ -1244,7 +2495,7 @@ var renderAdminDocument = (service) => {
1244
2495
  padding-top: max(10px, env(safe-area-inset-top));
1245
2496
  }
1246
2497
  input, select, textarea { font-size: 16px; }
1247
- .controls { width: 100%; }
2498
+ .brand, .vendor, .controls { width: 100%; }
1248
2499
  label.field, label.field select, label.field input { flex: 1; min-width: 0; }
1249
2500
  .layout { grid-template-columns: minmax(0, 1fr); }
1250
2501
  nav.side {
@@ -1275,8 +2526,11 @@ var renderAdminDocument = (service) => {
1275
2526
  <div class="app" data-service="${name}" data-mockingbird-admin>
1276
2527
  <header class="top">
1277
2528
  <div class="brand">
1278
- <strong>${name}</strong>
1279
- <span>Mockingbird admin</span>
2529
+ <div class="brand-id">
2530
+ <strong>${name}</strong>
2531
+ <span>Admin</span>
2532
+ </div>
2533
+ <div class="vendor" id="vendor" hidden></div>
1280
2534
  </div>
1281
2535
  <div class="controls">
1282
2536
  <label class="field">Namespace
@@ -1406,460 +2660,7 @@ var renderAdminDocument = (service) => {
1406
2660
  </div>
1407
2661
  </form>
1408
2662
  </dialog>
1409
- <script>
1410
- (() => {
1411
- const STANDARD = new Set(${standard})
1412
- const state = {
1413
- namespace: "default",
1414
- key: sessionStorage.getItem("mockingbird-admin-key") || "",
1415
- view: "overview",
1416
- collection: "",
1417
- shape: null,
1418
- route: null,
1419
- editing: null
1420
- }
1421
- const $ = (id) => document.getElementById(id)
1422
- const banner = $("banner")
1423
- const keyInput = $("admin-key")
1424
- keyInput.value = state.key
1425
- const params = new URLSearchParams(location.search)
1426
- if (params.get("key")) {
1427
- state.key = params.get("key")
1428
- keyInput.value = state.key
1429
- sessionStorage.setItem("mockingbird-admin-key", state.key)
1430
- params.delete("key")
1431
- const next = params.toString()
1432
- history.replaceState(null, "", location.pathname + (next ? "?" + next : ""))
1433
- }
1434
- if (params.get("namespace")) state.namespace = params.get("namespace")
1435
-
1436
- const showError = (error) => {
1437
- banner.textContent = error instanceof Error ? error.message : String(error)
1438
- banner.classList.add("show")
1439
- }
1440
- const clearError = () => banner.classList.remove("show")
1441
-
1442
- async function originGet(path) {
1443
- const response = await fetch(path, { headers: { accept: "application/json" } })
1444
- if (!response.ok) throw new Error(response.status + " " + path)
1445
- return response.json()
1446
- }
1447
- async function api(method, path, body) {
1448
- const headers = { accept: "application/json" }
1449
- if (state.key) headers["${ADMIN_KEY_HEADER}"] = state.key
1450
- if (state.namespace) headers["x-mockingbird-namespace"] = state.namespace
1451
- if (body !== undefined) headers["content-type"] = "application/json"
1452
- const response = await fetch("${ADMIN_PREFIX}" + path, {
1453
- method,
1454
- headers,
1455
- body: body === undefined ? undefined : JSON.stringify(body)
1456
- })
1457
- const text = await response.text()
1458
- let data = null
1459
- if (text) {
1460
- try { data = JSON.parse(text) } catch { data = { raw: text } }
1461
- }
1462
- if (response.status === 401) {
1463
- keyInput.focus()
1464
- throw new Error((data && data.error && data.error.message) || "Admin key required")
1465
- }
1466
- if (!response.ok) {
1467
- throw new Error((data && data.error && data.error.message) || (response.status + " " + method + " " + path))
1468
- }
1469
- return data
1470
- }
1471
- const panelApi = {
1472
- get: (path) => api("GET", path),
1473
- send: (method, path, body) => api(method, path, body),
1474
- namespace: () => state.namespace
1475
- }
1476
-
1477
- function setTheme(theme) {
1478
- if (theme) document.documentElement.dataset.theme = theme
1479
- else delete document.documentElement.dataset.theme
1480
- }
1481
- const storedTheme = localStorage.getItem("mockingbird-admin-theme")
1482
- if (storedTheme) setTheme(storedTheme)
1483
- $("theme").addEventListener("click", () => {
1484
- const current = document.documentElement.dataset.theme
1485
- const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches
1486
- const next = dark ? "light" : "dark"
1487
- localStorage.setItem("mockingbird-admin-theme", next)
1488
- setTheme(next)
1489
- })
1490
-
1491
- function show(view) {
1492
- state.view = view
1493
- for (const button of document.querySelectorAll("#nav button")) {
1494
- button.setAttribute("aria-current", button.dataset.view === view ? "true" : "false")
1495
- }
1496
- for (const section of document.querySelectorAll("main .view")) {
1497
- section.hidden = section.id !== "view-" + view
1498
- }
1499
- }
1500
- document.getElementById("nav").addEventListener("click", (event) => {
1501
- const button = event.target.closest("button[data-view]")
1502
- if (!button) return
1503
- show(button.dataset.view)
1504
- refresh().catch(showError)
1505
- })
1506
-
1507
- function cards(items) {
1508
- return '<div class="grid">' + items.map((item) =>
1509
- '<article class="card"><div class="k">' + item.k + '</div><div class="v">' + item.v + '</div></article>'
1510
- ).join("") + "</div>"
1511
- }
1512
- function table(columns, rows) {
1513
- if (rows.length === 0) return '<p class="empty">Nothing here yet.</p>'
1514
- const head = "<tr>" + columns.map((column) => "<th>" + column.label + "</th>").join("") + "</tr>"
1515
- const body = rows.map((row) => "<tr" + (row.attrs || "") + ">" + columns.map((column) =>
1516
- '<td data-label="' + column.label + '">' + column.cell(row) + "</td>"
1517
- ).join("") + "</tr>").join("")
1518
- return "<table><thead>" + head + "</thead><tbody>" + body + "</tbody></table>"
1519
- }
1520
- const esc = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => ({
1521
- "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;"
1522
- }[char]))
1523
- const preview = (value) => {
1524
- const text = JSON.stringify(value)
1525
- return esc(text.length > 180 ? text.slice(0, 180) + "\u2026" : text)
1526
- }
1527
- const when = (epoch) => {
1528
- const date = new Date(epoch)
1529
- return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString()
1530
- }
1531
- const clockCard = (epoch) => {
1532
- const date = new Date(epoch)
1533
- if (Number.isNaN(date.getTime())) return esc(String(epoch))
1534
- const iso = date.toISOString()
1535
- return esc(iso.slice(0, 10)) + '<span class="clock-time">' + esc(iso.slice(11, 19)) + " UTC</span>"
1536
- }
1537
-
1538
- async function loadNamespaces() {
1539
- const data = await api("GET", "/namespaces")
1540
- const select = $("namespace")
1541
- const names = data.namespaces && data.namespaces.length ? data.namespaces : [data.default || "default"]
1542
- if (!names.includes(state.namespace)) names.unshift(state.namespace)
1543
- select.innerHTML = names.map((name) =>
1544
- '<option value="' + esc(name) + '">' + esc(name) + "</option>"
1545
- ).join("")
1546
- select.value = state.namespace
1547
- }
1548
-
1549
- async function renderOverview() {
1550
- const [health, shape, clock, journal] = await Promise.all([
1551
- originGet("/health"),
1552
- api("GET", "/state"),
1553
- api("GET", "/clock"),
1554
- api("GET", "/requests?limit=8")
1555
- ])
1556
- state.shape = shape
1557
- $("overview-cards").innerHTML = cards([
1558
- { k: "Service", v: esc(health.service || "${name}") },
1559
- { k: "Status", v: esc(health.status || "ok") },
1560
- { k: "Namespace", v: esc(state.namespace) },
1561
- { k: "Collections", v: String(shape.collections.length) },
1562
- { k: "Records", v: String(shape.collections.reduce((sum, item) => sum + item.count, 0)) },
1563
- { k: "Clock", v: clockCard(clock.now) }
1564
- ])
1565
- const requests = (journal && journal.requests) || []
1566
- $("overview-requests").innerHTML = table(
1567
- [
1568
- { label: "Method", cell: (row) => esc(row.method) },
1569
- { label: "Path", cell: (row) => esc(row.path) },
1570
- { label: "Status", cell: (row) => '<span class="status-' + String(row.status).charAt(0) + '">' + esc(row.status) + "</span>" },
1571
- { label: "Operation", cell: (row) => esc(row.operationId || "") }
1572
- ],
1573
- requests
1574
- )
1575
- }
1576
-
1577
- function schemaChips(collection) {
1578
- if (!collection || collection.fields.length === 0) {
1579
- return '<p class="empty">No fields yet. Add a record or declare the collection on the service.</p>'
1580
- }
1581
- return '<div class="chips">' + collection.fields.map((field) =>
1582
- '<span class="chip">' + esc(field.name) + ' <i>' + esc(field.kind) + (field.optional ? "?" : "") + "</i></span>"
1583
- ).join("") + "</div>"
1584
- }
1585
-
1586
- async function renderState() {
1587
- const shape = await api("GET", "/state")
1588
- state.shape = shape
1589
- const host = $("collections")
1590
- if (shape.collections.length === 0) {
1591
- host.innerHTML = '<p class="empty">This namespace has no collections yet.</p>'
1592
- $("schema").innerHTML = ""
1593
- $("records").innerHTML = ""
1594
- return
1595
- }
1596
- if (!shape.collections.some((item) => item.name === state.collection)) {
1597
- state.collection = shape.collections[0].name
1598
- }
1599
- host.innerHTML = shape.collections.map((item) =>
1600
- '<button type="button" class="collection" data-collection="' + esc(item.name) + '" aria-current="' +
1601
- (item.name === state.collection ? "true" : "false") + '"><strong>' + esc(item.label) +
1602
- '</strong><small>' + esc(item.count) + " \xB7 " + esc(item.source) + "</small></button>"
1603
- ).join("")
1604
- const selected = shape.collections.find((item) => item.name === state.collection)
1605
- $("schema").innerHTML = "<h3>" + esc(selected.label) + "</h3>" +
1606
- (selected.description ? "<p class=" + '"lede">' + esc(selected.description) + "</p>" : "") +
1607
- schemaChips(selected)
1608
- const page = await api("GET", "/state/" + encodeURIComponent(state.collection) + "?limit=50")
1609
- const rows = page.records.map((record) => ({
1610
- ...record,
1611
- attrs: ' class="clickable" data-id="' + esc(record.id) + '"'
1612
- }))
1613
- $("records").innerHTML = table(
1614
- [
1615
- { label: "Id", cell: (row) => "<code>" + esc(row.id) + "</code>" },
1616
- { label: "Seq", cell: (row) => esc(row.seq) },
1617
- { label: "Value", cell: (row) => preview(row.value) }
1618
- ],
1619
- rows
1620
- )
1621
- }
1622
-
1623
- async function renderClock() {
1624
- const clock = await api("GET", "/clock")
1625
- $("clock-readout").innerHTML = "<div class='k'>Now</div><div class='v'>" + esc(when(clock.now)) +
1626
- "</div><p>" + esc(clock.now) + " ms \xB7 offset " + esc(clock.offsetMs) + " ms " +
1627
- (clock.frozen ? "<span class='badge warn'>frozen</span>" : "<span class='badge ok'>live</span>") + "</p>"
1628
- $("freeze").textContent = clock.frozen ? "Unfreeze" : "Freeze"
1629
- $("freeze").dataset.frozen = clock.frozen ? "1" : "0"
1630
- }
1631
-
1632
- async function renderFaults() {
1633
- const [faults, presets] = await Promise.all([
1634
- api("GET", "/faults"),
1635
- api("GET", "/faults/presets").catch(() => ({ presets: [] }))
1636
- ])
1637
- const select = $("fault-preset")
1638
- const current = select.value
1639
- const list = presets.presets || []
1640
- select.innerHTML = '<option value="">Custom rule</option>' + list.map((preset) =>
1641
- '<option value="' + esc(preset.name) + '">' + esc(preset.name) + "</option>"
1642
- ).join("")
1643
- select.value = current
1644
- const rules = faults.faults || []
1645
- $("fault-list").innerHTML = table(
1646
- [
1647
- { label: "Id", cell: (row) => esc(row.id) },
1648
- { label: "Match", cell: (row) => esc([row.method, row.pathPrefix, row.operationId].filter(Boolean).join(" ") || "all") },
1649
- { label: "Effect", cell: (row) => esc(row.status ?? row.effect ?? (row.drop ? "drop" : row.latencyMs || "")) }
1650
- ],
1651
- rules
1652
- )
1653
- }
1654
-
1655
- async function renderJournal() {
1656
- const journal = await api("GET", "/requests?limit=100")
1657
- $("journal").innerHTML = table(
1658
- [
1659
- { label: "When", cell: (row) => esc(row.at || "") },
1660
- { label: "Method", cell: (row) => esc(row.method) },
1661
- { label: "Path", cell: (row) => esc(row.path) },
1662
- { label: "Status", cell: (row) => esc(row.status) },
1663
- { label: "ms", cell: (row) => esc(row.durationMs) }
1664
- ],
1665
- journal.requests || []
1666
- )
1667
- }
1668
-
1669
- async function renderRoutes() {
1670
- const data = await api("GET", "/")
1671
- const routes = data.routes || []
1672
- $("route-list").innerHTML = routes.map((route) => {
1673
- const extra = STANDARD.has(route) ? "" : " <span class='badge'>extension</span>"
1674
- return '<div class="route"><span class="method">' + esc(route.split(" ")[0]) +
1675
- '</span><span>' + esc(route.split(" ").slice(1).join(" ")) + extra +
1676
- '</span><button class="btn" type="button" data-route="' + esc(route) + '">Use</button></div>'
1677
- }).join("")
1678
- }
1679
-
1680
- const renderers = {
1681
- overview: renderOverview,
1682
- state: renderState,
1683
- clock: renderClock,
1684
- faults: renderFaults,
1685
- journal: renderJournal,
1686
- routes: renderRoutes
1687
- }
1688
-
1689
- async function refresh() {
1690
- clearError()
1691
- if (state.view in renderers) await renderers[state.view]()
1692
- }
1693
-
1694
- $("namespace").addEventListener("change", () => {
1695
- state.namespace = $("namespace").value
1696
- refresh().catch(showError)
1697
- })
1698
- keyInput.addEventListener("change", () => {
1699
- state.key = keyInput.value.trim()
1700
- sessionStorage.setItem("mockingbird-admin-key", state.key)
1701
- refresh().catch(showError)
1702
- })
1703
- $("refresh").addEventListener("click", () => refresh().catch(showError))
1704
-
1705
- $("collections").addEventListener("click", (event) => {
1706
- const button = event.target.closest("[data-collection]")
1707
- if (!button) return
1708
- state.collection = button.dataset.collection
1709
- renderState().catch(showError)
1710
- })
1711
- $("records").addEventListener("click", (event) => {
1712
- const row = event.target.closest("[data-id]")
1713
- if (!row) return
1714
- openEditor(row.dataset.id).catch(showError)
1715
- })
1716
-
1717
- const editor = $("editor")
1718
- async function openEditor(id) {
1719
- state.editing = id || null
1720
- $("editor-title").textContent = id ? "Edit record" : "New record"
1721
- $("editor-id").value = id || ""
1722
- $("editor-id").readOnly = Boolean(id)
1723
- $("editor-delete").hidden = !id
1724
- if (id) {
1725
- const record = await api("GET", "/state/" + encodeURIComponent(state.collection) + "/" + encodeURIComponent(id))
1726
- $("editor-value").value = JSON.stringify(record.value, null, 2)
1727
- } else {
1728
- $("editor-value").value = "{\\n \\n}"
1729
- }
1730
- editor.showModal()
1731
- }
1732
- $("new-record").addEventListener("click", () => openEditor(null).catch(showError))
1733
- $("editor-form").addEventListener("submit", (event) => {
1734
- const submitter = event.submitter
1735
- if (!submitter || submitter.value !== "save") return
1736
- event.preventDefault()
1737
- let value
1738
- try { value = JSON.parse($("editor-value").value) }
1739
- catch { showError(new Error("Value is not JSON")); return }
1740
- const id = $("editor-id").value.trim()
1741
- const path = "/state/" + encodeURIComponent(state.collection)
1742
- const request = state.editing
1743
- ? api("PUT", path + "/" + encodeURIComponent(id), { value })
1744
- : api("POST", path, { id: id || undefined, value })
1745
- request.then(() => { editor.close(); return renderState() }).catch(showError)
1746
- })
1747
- $("editor-delete").addEventListener("click", () => {
1748
- const id = state.editing
1749
- if (!id) return
1750
- api("DELETE", "/state/" + encodeURIComponent(state.collection) + "/" + encodeURIComponent(id))
1751
- .then(() => { editor.close(); return renderState() })
1752
- .catch(showError)
1753
- })
1754
-
1755
- document.querySelectorAll("[data-advance]").forEach((button) => {
1756
- button.addEventListener("click", () => {
1757
- api("POST", "/clock", { advance: Number(button.dataset.advance) }).then(renderClock).catch(showError)
1758
- })
1759
- })
1760
- $("freeze").addEventListener("click", () => {
1761
- const frozen = $("freeze").dataset.frozen === "1"
1762
- api("POST", "/clock", { freeze: !frozen }).then(renderClock).catch(showError)
1763
- })
1764
- $("clock-reset").addEventListener("click", () => {
1765
- api("POST", "/clock", { reset: true }).then(renderClock).catch(showError)
1766
- })
1767
- $("clock-set").addEventListener("submit", (event) => {
1768
- event.preventDefault()
1769
- const raw = $("clock-instant").value.trim()
1770
- const set = /^-?\\d+$/.test(raw) ? Number(raw) : raw
1771
- api("POST", "/clock", { set }).then(renderClock).catch(showError)
1772
- })
1773
-
1774
- $("fault-form").addEventListener("submit", (event) => {
1775
- event.preventDefault()
1776
- const preset = $("fault-preset").value
1777
- const body = preset
1778
- ? { preset }
1779
- : {
1780
- status: Number($("fault-status").value),
1781
- ...($("fault-path").value ? { pathPrefix: $("fault-path").value } : {}),
1782
- ...($("fault-method").value ? { method: $("fault-method").value } : {})
1783
- }
1784
- if (!preset && !Number.isFinite(body.status)) {
1785
- showError(new Error("Status must be a number, or choose a preset"))
1786
- return
1787
- }
1788
- api("POST", "/faults", body).then(renderFaults).catch(showError)
1789
- })
1790
- $("fault-clear").addEventListener("click", () => api("DELETE", "/faults").then(renderFaults).catch(showError))
1791
- $("journal-clear").addEventListener("click", () => api("DELETE", "/requests").then(renderJournal).catch(showError))
1792
-
1793
- $("route-list").addEventListener("click", (event) => {
1794
- const button = event.target.closest("[data-route]")
1795
- if (!button) return
1796
- state.route = button.dataset.route
1797
- $("route-title").textContent = state.route
1798
- const method = state.route.split(" ")[0]
1799
- $("route-body").disabled = method === "GET" || method === "DELETE"
1800
- })
1801
- $("route-form").addEventListener("submit", async (event) => {
1802
- event.preventDefault()
1803
- if (!state.route) return showError(new Error("Choose a route first"))
1804
- const [method, path] = [state.route.split(" ")[0], state.route.split(" ").slice(1).join(" ")]
1805
- const concrete = path.replace(/:([A-Za-z0-9_]+)/g, (_, key) => prompt("Value for " + key) || "")
1806
- let body
1807
- if (method !== "GET" && method !== "DELETE" && $("route-body").value.trim()) {
1808
- try { body = JSON.parse($("route-body").value) }
1809
- catch { return showError(new Error("Body is not JSON")) }
1810
- }
1811
- try {
1812
- const data = await api(method, concrete, body)
1813
- $("route-result").textContent = JSON.stringify(data, null, 2)
1814
- } catch (error) {
1815
- showError(error)
1816
- }
1817
- })
1818
-
1819
- function mountPanels(panels) {
1820
- const nav = $("nav")
1821
- const views = $("views")
1822
- for (const panel of panels) {
1823
- const button = document.createElement("button")
1824
- button.type = "button"
1825
- button.dataset.view = panel.id
1826
- button.textContent = panel.title
1827
- nav.append(button)
1828
- const section = document.createElement("section")
1829
- section.className = "view"
1830
- section.id = "view-" + panel.id
1831
- section.hidden = true
1832
- const title = document.createElement("h2")
1833
- title.textContent = panel.title
1834
- const lede = document.createElement("p")
1835
- lede.className = "lede"
1836
- lede.textContent = panel.description || ""
1837
- const body = document.createElement("div")
1838
- body.className = "panel-body"
1839
- body.id = "panel-" + panel.id
1840
- body.innerHTML = panel.html || ""
1841
- section.append(title, lede, body)
1842
- views.append(section)
1843
- if (panel.script) {
1844
- try {
1845
- const run = new Function("root", "api", panel.script)
1846
- run(body, panelApi)
1847
- } catch (error) {
1848
- const note = document.createElement("p")
1849
- note.className = "banner show"
1850
- note.textContent = error instanceof Error ? error.message : String(error)
1851
- body.append(note)
1852
- }
1853
- }
1854
- }
1855
- }
1856
-
1857
- loadNamespaces()
1858
- .then(() => api("GET", "/ui/manifest"))
1859
- .then((manifest) => { mountPanels((manifest && manifest.panels) || []); return refresh() })
1860
- .catch(showError)
1861
- })()
1862
- </script>
2663
+ ${script}
1863
2664
  </body>
1864
2665
  </html>`;
1865
2666
  };
@@ -1876,10 +2677,10 @@ var json2 = (body) => new Response(JSON.stringify(body), {
1876
2677
  });
1877
2678
  var adminUiRoutes = (service, ui) => {
1878
2679
  const shell = () => renderAdminDocument(service);
1879
- const document2 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
2680
+ const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
1880
2681
  return {
1881
- "GET /ui": () => htmlResponse(document2()),
1882
- "GET /ui/": () => htmlResponse(document2()),
2682
+ "GET /ui": () => htmlResponse(document3()),
2683
+ "GET /ui/": () => htmlResponse(document3()),
1883
2684
  "GET /ui/manifest": () => json2({
1884
2685
  service,
1885
2686
  panels: (ui?.panels ?? []).map((panel) => ({
@@ -2367,7 +3168,7 @@ var SAMPLE = 25;
2367
3168
  var WALK_LIMIT = 1e4;
2368
3169
  var SKIP_CTORS = /* @__PURE__ */ new Set(["Hono", "HonoRequest", "Router"]);
2369
3170
  var fail = (status, message) => ({ ok: false, status, message });
2370
- var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3171
+ var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2371
3172
  var kindOf = (value) => {
2372
3173
  if (value === null)
2373
3174
  return "null";
@@ -2466,7 +3267,7 @@ var absorb = (accum, value) => {
2466
3267
  const kind = kindOf(value);
2467
3268
  accum.kinds.add(kind);
2468
3269
  accum.present++;
2469
- if (!isRecord2(value))
3270
+ if (!isRecord3(value))
2470
3271
  return;
2471
3272
  for (const [key, child] of Object.entries(value)) {
2472
3273
  const nested = accum.nested.get(key) ?? blankAccum(key);
@@ -2491,7 +3292,7 @@ var inferFields = (records) => {
2491
3292
  if (records.length === 0)
2492
3293
  return [];
2493
3294
  const sample = records.slice(0, SAMPLE);
2494
- const objects = sample.filter((record2) => isRecord2(record2.value));
3295
+ const objects = sample.filter((record2) => isRecord3(record2.value));
2495
3296
  if (objects.length === 0) {
2496
3297
  const kinds = new Set(sample.map((record2) => kindOf(record2.value)));
2497
3298
  const list = [...kinds];
@@ -2678,7 +3479,7 @@ var patchStateRecord = (scope, name, id, value) => {
2678
3479
  const parsed = jsonValue(value);
2679
3480
  if (!parsed.ok)
2680
3481
  return parsed;
2681
- const next = isRecord2(current.value.value) && isRecord2(parsed.value) ? { ...current.value.value, ...parsed.value } : parsed.value;
3482
+ const next = isRecord3(current.value.value) && isRecord3(parsed.value) ? { ...current.value.value, ...parsed.value } : parsed.value;
2682
3483
  return replaceStateRecord(scope, name, id, next);
2683
3484
  };
2684
3485
  var deleteStateRecord = (scope, name, id) => {
@@ -2746,7 +3547,7 @@ var stateAdminRoutes = (routes) => {
2746
3547
  const scope = open(namespace);
2747
3548
  if (!scope.ok)
2748
3549
  return fromResult(scope, 201);
2749
- if (!isRecord2(body) || !("value" in body)) {
3550
+ if (!isRecord3(body) || !("value" in body)) {
2750
3551
  return adminError2(400, 'expected {"id?": "<id>", "value": <json>}');
2751
3552
  }
2752
3553
  const id = body.id;
@@ -2764,7 +3565,7 @@ var stateAdminRoutes = (routes) => {
2764
3565
  const scope = open(namespace);
2765
3566
  if (!scope.ok)
2766
3567
  return fromResult(scope, 200);
2767
- if (!isRecord2(body) || !("value" in body))
3568
+ if (!isRecord3(body) || !("value" in body))
2768
3569
  return adminError2(400, 'expected {"value": <json>}');
2769
3570
  const replaced = replaceStateRecord(scope.value, params.collection ?? "", params.id ?? "", body.value);
2770
3571
  if (replaced.ok)
@@ -2775,7 +3576,7 @@ var stateAdminRoutes = (routes) => {
2775
3576
  const scope = open(namespace);
2776
3577
  if (!scope.ok)
2777
3578
  return fromResult(scope, 200);
2778
- if (!isRecord2(body) || !("value" in body))
3579
+ if (!isRecord3(body) || !("value" in body))
2779
3580
  return adminError2(400, 'expected {"value": <json>}');
2780
3581
  const patched = patchStateRecord(scope.value, params.collection ?? "", params.id ?? "", body.value);
2781
3582
  if (patched.ok)
@@ -2795,7 +3596,7 @@ var stateAdminRoutes = (routes) => {
2795
3596
  };
2796
3597
 
2797
3598
  // ../core/dist/version.js
2798
- var PACKAGE_VERSION = true ? "1.4.0" : UNRELEASED_VERSION;
3599
+ var PACKAGE_VERSION = true ? "1.5.1" : UNRELEASED_VERSION;
2799
3600
 
2800
3601
  // ../core/dist/signing.js
2801
3602
  var encoder = new TextEncoder();
@@ -3132,9 +3933,9 @@ var createWebhookHub = (options) => {
3132
3933
  };
3133
3934
  var json4 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
3134
3935
  var adminError3 = (status, message) => json4(status, { error: { type: "mockingbird_admin", message } });
3135
- var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3936
+ var isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3136
3937
  var parseEndpoint = (value) => {
3137
- if (!isRecord3(value) || typeof value.url !== "string")
3938
+ if (!isRecord4(value) || typeof value.url !== "string")
3138
3939
  return "each endpoint needs a url";
3139
3940
  try {
3140
3941
  new URL(value.url);
@@ -3151,12 +3952,12 @@ var parseEndpoint = (value) => {
3151
3952
  const events = value.events ?? value.enabledEvents;
3152
3953
  if (Array.isArray(events))
3153
3954
  endpoint.events = events.map(String);
3154
- if (isRecord3(value.tags)) {
3955
+ if (isRecord4(value.tags)) {
3155
3956
  endpoint.tags = Object.fromEntries(Object.entries(value.tags).map(([k, v]) => [k, String(v)]));
3156
3957
  }
3157
3958
  if (typeof value.account === "string")
3158
3959
  endpoint.tags = { ...endpoint.tags, account: value.account };
3159
- if (isRecord3(value.headers)) {
3960
+ if (isRecord4(value.headers)) {
3160
3961
  endpoint.headers = Object.fromEntries(Object.entries(value.headers).map(([k, v]) => [k, String(v)]));
3161
3962
  }
3162
3963
  return endpoint;
@@ -3183,7 +3984,7 @@ var webhookAdminRoutes = (hub) => ({
3183
3984
  return json4(200, { status: "ok" });
3184
3985
  },
3185
3986
  "POST /webhooks/faults": ({ body, namespace }) => {
3186
- if (!isRecord3(body) || !["duplicate", "reorder", "drop"].includes(String(body.mode))) {
3987
+ if (!isRecord4(body) || !["duplicate", "reorder", "drop"].includes(String(body.mode))) {
3187
3988
  return adminError3(400, "mode must be duplicate, reorder or drop");
3188
3989
  }
3189
3990
  const fault = { mode: body.mode };
@@ -3199,7 +4000,7 @@ var webhookAdminRoutes = (hub) => ({
3199
4000
  }))
3200
4001
  }),
3201
4002
  "PUT /webhook-endpoints": ({ body, namespace }) => {
3202
- const list = Array.isArray(body) ? body : isRecord3(body) ? body.endpoints : void 0;
4003
+ const list = Array.isArray(body) ? body : isRecord4(body) ? body.endpoints : void 0;
3203
4004
  if (!Array.isArray(list))
3204
4005
  return adminError3(400, "expected [{url, secret?, events?}]");
3205
4006
  const parsed = [];
@@ -3267,8 +4068,8 @@ var DroppedConnectionError = class extends TypeError {
3267
4068
  this.name = "TypeError";
3268
4069
  }
3269
4070
  };
3270
- var operationMatcher = (document2) => {
3271
- const matchers = listOperations(document2).map((operation) => ({
4071
+ var operationMatcher = (document3) => {
4072
+ const matchers = listOperations(document3).map((operation) => ({
3272
4073
  operationId: operation.operationId,
3273
4074
  method: operation.method.toUpperCase(),
3274
4075
  pattern: new RegExp(`^${operation.path.split("/").map((segment) => segment.startsWith("{") ? "[^/]+" : segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("/")}/?$`),
@@ -3363,10 +4164,10 @@ var createRuntime = (options) => {
3363
4164
  const ensureBranch = (namespace, branch2, at) => {
3364
4165
  if (!BRANCH_PATTERN2.test(branch2))
3365
4166
  throw new RangeError(`branch must match ${BRANCH_PATTERN2}`);
3366
- const history = timeline(namespace);
4167
+ const history2 = timeline(namespace);
3367
4168
  if (branch2 === "main") {
3368
4169
  if (at !== void 0) {
3369
- const point = history.checkout("main", at);
4170
+ const point = history2.checkout("main", at);
3370
4171
  restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
3371
4172
  captured.set(namespace, point.value.snapshot);
3372
4173
  rng.setState(point.value.rngState);
@@ -3379,10 +4180,10 @@ var createRuntime = (options) => {
3379
4180
  return namespace;
3380
4181
  }
3381
4182
  const storage = physicalBranch(namespace, branch2);
3382
- if (!history.hasBranch(branch2)) {
4183
+ if (!history2.hasBranch(branch2)) {
3383
4184
  if (at === void 0)
3384
- history.commit(capture(namespace));
3385
- const point = history.fork(branch2, at === void 0 ? {} : { from: at });
4185
+ history2.commit(capture(namespace));
4186
+ const point = history2.fork(branch2, at === void 0 ? {} : { from: at });
3386
4187
  const branchRng = createRng(options.seed ?? 0);
3387
4188
  if (point)
3388
4189
  branchRng.setState(point.value.rngState);
@@ -3391,8 +4192,8 @@ var createRuntime = (options) => {
3391
4192
  restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
3392
4193
  if (point)
3393
4194
  captured.set(storage, point.value.snapshot);
3394
- } else if (at !== void 0 && history.head(branch2)?.id !== at) {
3395
- const point = history.checkout(branch2, at);
4195
+ } else if (at !== void 0 && history2.head(branch2)?.id !== at) {
4196
+ const point = history2.checkout(branch2, at);
3396
4197
  if (!instances.has(storage)) {
3397
4198
  const branchRng = createRng(options.seed ?? 0);
3398
4199
  branchRng.setState(point.value.rngState);
@@ -3403,7 +4204,7 @@ var createRuntime = (options) => {
3403
4204
  captured.set(storage, point.value.snapshot);
3404
4205
  } else {
3405
4206
  if (!instances.has(storage)) {
3406
- const point = history.head(branch2);
4207
+ const point = history2.head(branch2);
3407
4208
  const branchRng = createRng(options.seed ?? 0);
3408
4209
  if (point)
3409
4210
  branchRng.setState(point.value.rngState);
@@ -3427,8 +4228,8 @@ var createRuntime = (options) => {
3427
4228
  const checkout = (checkpointId, checkoutOptions = {}) => {
3428
4229
  const namespace = checkoutOptions.namespace ?? DEFAULT_NAMESPACE;
3429
4230
  const branchName = checkoutOptions.branch ?? "main";
3430
- const history = timeline(namespace);
3431
- const point = history.checkout(branchName, checkpointId);
4231
+ const history2 = timeline(namespace);
4232
+ const point = history2.checkout(branchName, checkpointId);
3432
4233
  const storage = ensureBranch(namespace, branchName);
3433
4234
  restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
3434
4235
  captured.set(storage, point.value.snapshot);
@@ -3478,9 +4279,9 @@ var createRuntime = (options) => {
3478
4279
  instance(name);
3479
4280
  restoreNamespace(sqlite, storageNamespace(name), from);
3480
4281
  captured.set(name, from);
3481
- const history = timelines.get(name);
3482
- if (history)
3483
- history.commit(capture(name), { branch: "main" });
4282
+ const history2 = timelines.get(name);
4283
+ if (history2)
4284
+ history2.commit(capture(name), { branch: "main" });
3484
4285
  else
3485
4286
  timeline(name);
3486
4287
  };
@@ -3709,10 +4510,10 @@ var createRuntime = (options) => {
3709
4510
  },
3710
4511
  release: (name, checkpointId) => timeline(name).release(checkpointId),
3711
4512
  inspect: (name) => {
3712
- const history = timeline(name);
4513
+ const history2 = timeline(name);
3713
4514
  return {
3714
- branches: history.branches(),
3715
- checkpoints: history.checkpoints().map(({ id, branch: branchName, parent, at }) => ({
4515
+ branches: history2.branches(),
4516
+ checkpoints: history2.checkpoints().map(({ id, branch: branchName, parent, at }) => ({
3716
4517
  id,
3717
4518
  branch: branchName,
3718
4519
  parent,
@@ -3727,11 +4528,11 @@ var createRuntime = (options) => {
3727
4528
  } : {},
3728
4529
  routes: {
3729
4530
  ...credentialRoutes(credentials),
3730
- ...options.presets ? presetRoutes(options.presets, runtime) : {},
3731
4531
  ...options.webhooks ? webhookAdminRoutes(options.webhooks) : {},
3732
4532
  ...options.admin?.(runtime) ?? {},
3733
- // Standard state and UI routes win a colliding key. A service adds its own keys
3734
- // beside these; it does not replace `/state` or `/ui`.
4533
+ // Shell routes win a colliding key. Fault presets are one of them: the admin page
4534
+ // maps `presets`, and a service route must not replace that list with a record.
4535
+ ...options.presets ? presetRoutes(options.presets, runtime) : {},
3735
4536
  ...stateAdminRoutes({
3736
4537
  open: stateScope,
3737
4538
  afterWrite: (namespace) => {
@@ -3806,9 +4607,12 @@ var credentialRoutes = (registry) => ({
3806
4607
  }
3807
4608
  });
3808
4609
  var presetRoutes = (presets, runtime) => ({
3809
- "GET /faults/presets": () => adminJson(200, {
3810
- presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
3811
- }),
4610
+ "GET /faults/presets": () => {
4611
+ const body = {
4612
+ presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
4613
+ };
4614
+ return adminJson(200, body);
4615
+ },
3812
4616
  "POST /faults/presets/:name": ({ params, body, namespace }) => {
3813
4617
  const name = params.name;
3814
4618
  if (!presets[name])
@@ -3866,7 +4670,7 @@ var verifyJwt = async (token, secrets, now) => {
3866
4670
  var bodySha256 = async (body) => bytesToBase64(new Uint8Array(await crypto.subtle.digest("SHA-256", encoder2.encode(body))));
3867
4671
 
3868
4672
  // src/generated/openapi.ts
3869
- var document = JSON.parse(`{"openapi":"3.1.0","info":{"title":"LiveKit Server API (Mockingbird subset)","version":"2.19.1","description":"Twirp JSON room, participant, egress, and SIP contract."},"servers":[{"url":"https://example.livekit.cloud"}],"paths":{"/twirp/livekit.RoomService/{method}":{"post":{"operationId":"RoomService","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"parameters":[{"name":"method","in":"path","required":true,"schema":{"type":"string"}},{"name":"Authorization","in":"header","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"responses":{"200":{"description":"Protobuf JSON response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"Twirp error","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/twirp/livekit.Egress/{method}":{"post":{"operationId":"EgressService","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"parameters":[{"name":"method","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"responses":{"200":{"description":"Protobuf JSON response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/twirp/livekit.SIP/{method}":{"post":{"operationId":"SipService","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"parameters":[{"name":"method","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"responses":{"200":{"description":"Protobuf JSON response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}}}}`);
4673
+ var document2 = JSON.parse(`{"openapi":"3.1.0","info":{"title":"LiveKit Server API (Mockingbird subset)","version":"2.19.1","description":"Twirp JSON room, participant, egress, and SIP contract."},"servers":[{"url":"https://example.livekit.cloud"}],"paths":{"/twirp/livekit.RoomService/{method}":{"post":{"operationId":"RoomService","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"parameters":[{"name":"method","in":"path","required":true,"schema":{"type":"string"}},{"name":"Authorization","in":"header","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"responses":{"200":{"description":"Protobuf JSON response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"Twirp error","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/twirp/livekit.Egress/{method}":{"post":{"operationId":"EgressService","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"parameters":[{"name":"method","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"responses":{"200":{"description":"Protobuf JSON response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/twirp/livekit.SIP/{method}":{"post":{"operationId":"SipService","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"parameters":[{"name":"method","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"responses":{"200":{"description":"Protobuf JSON response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}}}}`);
3870
4674
  var operationIds = ["RoomService", "EgressService", "SipService"];
3871
4675
  var supportedOperationIds = ["RoomService", "EgressService", "SipService"];
3872
4676
 
@@ -3984,7 +4788,7 @@ var createRuntime2 = (options = {}) => {
3984
4788
  });
3985
4789
  const runtime = createRuntime({
3986
4790
  name: LIVEKIT_NAMESPACE,
3987
- document,
4791
+ document: document2,
3988
4792
  presets: LIVEKIT_PRESETS,
3989
4793
  ...options.sqlite ? { sqlite: options.sqlite } : {},
3990
4794
  ...options.clock ? { clock: options.clock } : {},
@@ -4363,7 +5167,7 @@ var LiveKitAPI = class {
4363
5167
  };
4364
5168
 
4365
5169
  export {
4366
- document,
5170
+ document2 as document,
4367
5171
  operationIds,
4368
5172
  supportedOperationIds,
4369
5173
  LIVEKIT_PRESETS,
@@ -4371,4 +5175,4 @@ export {
4371
5175
  LIVEKIT_NAMESPACE,
4372
5176
  LiveKitAPI
4373
5177
  };
4374
- //# sourceMappingURL=chunk-EZZFTKWG.js.map
5178
+ //# sourceMappingURL=chunk-I2AJCCT7.js.map