@crvouga/mockingbird-service-payload-cms 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -678,10 +678,10 @@ var OpenAPIReferenceError = class extends Error {
678
678
  };
679
679
  var unescapePointer = (segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~");
680
680
  var isReference = (value) => typeof value === "object" && value !== null && typeof value.$ref === "string";
681
- var resolveRef = (document2, ref) => {
681
+ var resolveRef = (document3, ref) => {
682
682
  if (!ref.startsWith("#/"))
683
683
  throw new OpenAPIReferenceError(ref);
684
- let cursor = document2;
684
+ let cursor = document3;
685
685
  for (const raw of ref.slice(2).split("/")) {
686
686
  const segment = unescapePointer(raw);
687
687
  if (typeof cursor !== "object" || cursor === null || !(segment in cursor)) {
@@ -693,14 +693,14 @@ var resolveRef = (document2, ref) => {
693
693
  throw new OpenAPIReferenceError(ref);
694
694
  return cursor;
695
695
  };
696
- var deref = (document2, value) => {
696
+ var deref = (document3, value) => {
697
697
  let current = value;
698
698
  const seen = /* @__PURE__ */ new Set();
699
699
  while (isReference(current)) {
700
700
  if (seen.has(current.$ref))
701
701
  throw new OpenAPIReferenceError(`${current.$ref} (cycle)`);
702
702
  seen.add(current.$ref);
703
- current = resolveRef(document2, current.$ref);
703
+ current = resolveRef(document3, current.$ref);
704
704
  }
705
705
  return current;
706
706
  };
@@ -718,36 +718,36 @@ var HTTP_METHODS = [
718
718
  ];
719
719
 
720
720
  // ../../openapi/core/dist/document.js
721
- var mergeParameters = (document2, item, own) => {
721
+ var mergeParameters = (document3, item, own) => {
722
722
  const merged = /* @__PURE__ */ new Map();
723
723
  for (const raw of item.parameters ?? []) {
724
- const parameter = deref(document2, raw);
724
+ const parameter = deref(document3, raw);
725
725
  merged.set(`${parameter.in}:${parameter.name}`, parameter);
726
726
  }
727
727
  for (const raw of own ?? []) {
728
- const parameter = deref(document2, raw);
728
+ const parameter = deref(document3, raw);
729
729
  merged.set(`${parameter.in}:${parameter.name}`, parameter);
730
730
  }
731
731
  return [...merged.values()];
732
732
  };
733
- var listOperations = (document2) => {
733
+ var listOperations = (document3) => {
734
734
  const operations = [];
735
- for (const [path, item] of Object.entries(document2.paths)) {
735
+ for (const [path, item] of Object.entries(document3.paths)) {
736
736
  for (const method of HTTP_METHODS) {
737
737
  const operation = item[method];
738
738
  if (operation?.operationId === void 0)
739
739
  continue;
740
740
  const responses = {};
741
741
  for (const [status, response] of Object.entries(operation.responses)) {
742
- responses[status] = deref(document2, response);
742
+ responses[status] = deref(document3, response);
743
743
  }
744
744
  operations.push({
745
745
  operationId: operation.operationId,
746
746
  method,
747
747
  path,
748
748
  operation,
749
- parameters: mergeParameters(document2, item, operation.parameters),
750
- requestBody: operation.requestBody === void 0 ? void 0 : deref(document2, operation.requestBody),
749
+ parameters: mergeParameters(document3, item, operation.parameters),
750
+ requestBody: operation.requestBody === void 0 ? void 0 : deref(document3, operation.requestBody),
751
751
  responses
752
752
  });
753
753
  }
@@ -1141,6 +1141,1232 @@ var clearNamespace = (sqlite, namespace) => {
1141
1141
  });
1142
1142
  };
1143
1143
 
1144
+ // ../../ui/dist/reset.js
1145
+ var CSS_RESET = `*,
1146
+ *::before,
1147
+ *::after {
1148
+ box-sizing: border-box;
1149
+ margin: 0;
1150
+ padding: 0;
1151
+ }
1152
+
1153
+ * {
1154
+ scrollbar-width: thin;
1155
+ scrollbar-color: color-mix(in srgb, CanvasText 35%, Canvas) Canvas;
1156
+ }
1157
+
1158
+ *::-webkit-scrollbar {
1159
+ width: 12px;
1160
+ height: 12px;
1161
+ }
1162
+
1163
+ *::-webkit-scrollbar-track,
1164
+ *::-webkit-scrollbar-corner {
1165
+ background: Canvas;
1166
+ }
1167
+
1168
+ *::-webkit-scrollbar-thumb {
1169
+ background: color-mix(in srgb, CanvasText 35%, Canvas);
1170
+ border: 3px solid Canvas;
1171
+ border-radius: 999px;
1172
+ }
1173
+
1174
+ html {
1175
+ hanging-punctuation: first allow-end last;
1176
+ -webkit-text-size-adjust: none;
1177
+ text-size-adjust: none;
1178
+ tab-size: 4;
1179
+ font-family:
1180
+ ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI Variable", "Segoe UI",
1181
+ "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
1182
+ font-size: 16px;
1183
+ line-height: 1.5;
1184
+ -webkit-font-smoothing: antialiased;
1185
+ -moz-osx-font-smoothing: grayscale;
1186
+ text-rendering: optimizeLegibility;
1187
+ -webkit-tap-highlight-color: transparent;
1188
+ scroll-behavior: smooth;
1189
+ background: Canvas;
1190
+ color: CanvasText;
1191
+ }
1192
+
1193
+ body {
1194
+ min-height: 100svh;
1195
+ line-height: 1.5;
1196
+ background: Canvas;
1197
+ color: CanvasText;
1198
+ }
1199
+
1200
+ h1 {
1201
+ font-size: 2em;
1202
+ }
1203
+
1204
+ h2 {
1205
+ font-size: 1.5em;
1206
+ }
1207
+
1208
+ h3 {
1209
+ font-size: 1.17em;
1210
+ }
1211
+
1212
+ h4 {
1213
+ font-size: 1em;
1214
+ }
1215
+
1216
+ h5 {
1217
+ font-size: 0.83em;
1218
+ }
1219
+
1220
+ h6 {
1221
+ font-size: 0.67em;
1222
+ }
1223
+
1224
+ h1,
1225
+ h2,
1226
+ h3,
1227
+ h4,
1228
+ h5,
1229
+ h6 {
1230
+ font-weight: 700;
1231
+ line-height: 1.2;
1232
+ text-wrap: balance;
1233
+ overflow-wrap: break-word;
1234
+ }
1235
+
1236
+ p,
1237
+ li,
1238
+ figcaption,
1239
+ dd,
1240
+ dt,
1241
+ blockquote {
1242
+ overflow-wrap: break-word;
1243
+ }
1244
+
1245
+ p,
1246
+ li,
1247
+ figcaption,
1248
+ blockquote {
1249
+ text-wrap: pretty;
1250
+ }
1251
+
1252
+ ul,
1253
+ ol,
1254
+ menu {
1255
+ list-style: none;
1256
+ }
1257
+
1258
+ a {
1259
+ color: inherit;
1260
+ text-decoration: underline;
1261
+ text-decoration-thickness: 1px;
1262
+ text-underline-offset: 0.15em;
1263
+ text-decoration-skip-ink: auto;
1264
+ }
1265
+
1266
+ b,
1267
+ strong {
1268
+ font-weight: 700;
1269
+ }
1270
+
1271
+ small {
1272
+ font-size: 0.875em;
1273
+ }
1274
+
1275
+ code,
1276
+ kbd,
1277
+ samp,
1278
+ pre {
1279
+ font-family: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
1280
+ font-size: 1em;
1281
+ }
1282
+
1283
+ sub,
1284
+ sup {
1285
+ font-size: 75%;
1286
+ line-height: 0;
1287
+ position: relative;
1288
+ vertical-align: baseline;
1289
+ }
1290
+
1291
+ sub {
1292
+ bottom: -0.25em;
1293
+ }
1294
+
1295
+ sup {
1296
+ top: -0.5em;
1297
+ }
1298
+
1299
+ abbr[title] {
1300
+ text-decoration: underline dotted;
1301
+ text-underline-offset: 0.15em;
1302
+ }
1303
+
1304
+ mark {
1305
+ background: Mark;
1306
+ color: MarkText;
1307
+ }
1308
+
1309
+ hr {
1310
+ height: 0;
1311
+ border: 0;
1312
+ border-top: 1px solid color-mix(in srgb, CanvasText 20%, transparent);
1313
+ color: inherit;
1314
+ }
1315
+
1316
+ img,
1317
+ picture,
1318
+ video,
1319
+ canvas {
1320
+ display: block;
1321
+ max-width: 100%;
1322
+ }
1323
+
1324
+ img,
1325
+ video {
1326
+ height: auto;
1327
+ }
1328
+
1329
+ svg {
1330
+ overflow: hidden;
1331
+ }
1332
+
1333
+ iframe {
1334
+ border: 0;
1335
+ max-width: 100%;
1336
+ }
1337
+
1338
+ table {
1339
+ border-collapse: collapse;
1340
+ border-spacing: 0;
1341
+ }
1342
+
1343
+ fieldset {
1344
+ border: 0;
1345
+ min-inline-size: 0;
1346
+ }
1347
+
1348
+ legend {
1349
+ padding: 0;
1350
+ }
1351
+
1352
+ button,
1353
+ input,
1354
+ select,
1355
+ textarea,
1356
+ optgroup {
1357
+ font: inherit;
1358
+ letter-spacing: inherit;
1359
+ color: inherit;
1360
+ margin: 0;
1361
+ vertical-align: middle;
1362
+ }
1363
+
1364
+ button,
1365
+ input[type="button"],
1366
+ input[type="submit"],
1367
+ input[type="reset"] {
1368
+ appearance: none;
1369
+ background: none;
1370
+ border: 0;
1371
+ padding: 0;
1372
+ cursor: pointer;
1373
+ text-align: center;
1374
+ }
1375
+
1376
+ input,
1377
+ textarea,
1378
+ select {
1379
+ background-color: Canvas;
1380
+ border: 1px solid color-mix(in srgb, CanvasText 28%, transparent);
1381
+ border-radius: 0;
1382
+ padding: 0.5em 0.65em;
1383
+ }
1384
+
1385
+ input[type="checkbox"],
1386
+ input[type="radio"],
1387
+ input[type="range"],
1388
+ input[type="file"],
1389
+ input[type="color"],
1390
+ input[type="button"],
1391
+ input[type="submit"],
1392
+ input[type="reset"],
1393
+ input[type="image"],
1394
+ input[type="hidden"] {
1395
+ background: none;
1396
+ border: 0;
1397
+ padding: 0;
1398
+ border-radius: 0;
1399
+ }
1400
+
1401
+ input[type="checkbox"],
1402
+ input[type="radio"] {
1403
+ accent-color: CanvasText;
1404
+ }
1405
+
1406
+ input[type="search"] {
1407
+ appearance: none;
1408
+ background-color: Canvas;
1409
+ border: 1px solid color-mix(in srgb, CanvasText 28%, transparent);
1410
+ }
1411
+
1412
+ input[type="search"]::-webkit-search-decoration,
1413
+ input[type="search"]::-webkit-search-cancel-button,
1414
+ input[type="search"]::-webkit-search-results-button,
1415
+ input[type="search"]::-webkit-search-results-decoration {
1416
+ -webkit-appearance: none;
1417
+ display: none;
1418
+ }
1419
+
1420
+ input[type="number"] {
1421
+ appearance: textfield;
1422
+ background-color: Canvas;
1423
+ border: 1px solid color-mix(in srgb, CanvasText 28%, transparent);
1424
+ }
1425
+
1426
+ input[type="number"]::-webkit-inner-spin-button,
1427
+ input[type="number"]::-webkit-outer-spin-button {
1428
+ -webkit-appearance: none;
1429
+ margin: 0;
1430
+ }
1431
+
1432
+ textarea {
1433
+ resize: vertical;
1434
+ }
1435
+
1436
+ ::placeholder {
1437
+ opacity: 1;
1438
+ color: color-mix(in srgb, CanvasText 45%, transparent);
1439
+ }
1440
+
1441
+ :focus-visible {
1442
+ outline: 2px solid CanvasText;
1443
+ outline-offset: 2px;
1444
+ }
1445
+
1446
+ :focus:not(:focus-visible) {
1447
+ outline: none;
1448
+ }
1449
+
1450
+ :disabled,
1451
+ [disabled] {
1452
+ cursor: default;
1453
+ opacity: 0.6;
1454
+ }
1455
+
1456
+ label,
1457
+ summary,
1458
+ select,
1459
+ [role="button"] {
1460
+ cursor: pointer;
1461
+ }
1462
+
1463
+ summary {
1464
+ display: list-item;
1465
+ }
1466
+
1467
+ details {
1468
+ display: block;
1469
+ }
1470
+
1471
+ dialog {
1472
+ margin: auto;
1473
+ padding: 1rem;
1474
+ border: 1px solid color-mix(in srgb, CanvasText 20%, transparent);
1475
+ background: Canvas;
1476
+ color: CanvasText;
1477
+ max-width: calc(100vw - 2rem);
1478
+ max-height: calc(100svh - 2rem);
1479
+ }
1480
+
1481
+ dialog:not([open]) {
1482
+ display: none;
1483
+ }
1484
+
1485
+ ::backdrop {
1486
+ background: color-mix(in srgb, CanvasText 40%, transparent);
1487
+ }
1488
+
1489
+ [hidden] {
1490
+ display: none !important;
1491
+ }
1492
+
1493
+ ::selection {
1494
+ background: Highlight;
1495
+ color: HighlightText;
1496
+ }
1497
+
1498
+ @media (prefers-reduced-motion: reduce) {
1499
+ *,
1500
+ *::before,
1501
+ *::after {
1502
+ animation-duration: 0.01ms !important;
1503
+ animation-iteration-count: 1 !important;
1504
+ transition-duration: 0.01ms !important;
1505
+ scroll-behavior: auto !important;
1506
+ }
1507
+ }
1508
+
1509
+ @media (pointer: coarse) {
1510
+ input,
1511
+ select,
1512
+ textarea {
1513
+ font-size: max(16px, 1em);
1514
+ }
1515
+ }`;
1516
+
1517
+ // ../core/dist/admin-client.js
1518
+ function isRecord2(value) {
1519
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1520
+ }
1521
+ function mountAdminBrand(doc, host, service, catalog) {
1522
+ const clip = (value, max) => {
1523
+ if (typeof value !== "string")
1524
+ return null;
1525
+ const trimmed = value.trim();
1526
+ if (trimmed === "")
1527
+ return null;
1528
+ return trimmed.length > max ? trimmed.slice(0, max) : trimmed;
1529
+ };
1530
+ const httpUrl = (value) => {
1531
+ if (typeof value !== "string" || value.length > 2e3)
1532
+ return null;
1533
+ try {
1534
+ const url = new URL(value);
1535
+ if (url.protocol !== "https:" && url.protocol !== "http:")
1536
+ return null;
1537
+ return url.href;
1538
+ } catch {
1539
+ return null;
1540
+ }
1541
+ };
1542
+ const hexColor = (value) => {
1543
+ if (typeof value !== "string")
1544
+ return null;
1545
+ const raw = value.trim();
1546
+ return /^#[0-9a-fA-F]{3,8}$/.test(raw) ? raw : null;
1547
+ };
1548
+ const hostname = (value) => {
1549
+ try {
1550
+ return new URL(value).hostname.replace(/^www\./, "");
1551
+ } catch {
1552
+ return value;
1553
+ }
1554
+ };
1555
+ host.replaceChildren();
1556
+ host.hidden = true;
1557
+ if (!isRecord2(catalog) || !Object.hasOwn(catalog, service))
1558
+ return;
1559
+ const row = catalog[service];
1560
+ if (!isRecord2(row))
1561
+ return;
1562
+ const vendor = clip(row.vendor, 80);
1563
+ const website = httpUrl(row.website);
1564
+ const guide = httpUrl(row.guide);
1565
+ const docs = httpUrl(row.docs);
1566
+ const logo = httpUrl(row.logo);
1567
+ const color = hexColor(row.color);
1568
+ const description = clip(row.description, 280);
1569
+ if (vendor === null || website === null && guide === null && docs === null)
1570
+ return;
1571
+ const add = (node) => {
1572
+ host.append(node);
1573
+ return node;
1574
+ };
1575
+ if (logo !== null) {
1576
+ const img = doc.createElement("img");
1577
+ img.alt = "";
1578
+ img.src = logo;
1579
+ img.width = 16;
1580
+ img.height = 16;
1581
+ img.decoding = "async";
1582
+ img.referrerPolicy = "no-referrer";
1583
+ if (color !== null)
1584
+ img.style.borderColor = color;
1585
+ add(img);
1586
+ }
1587
+ const name = doc.createElement("span");
1588
+ name.className = "vendor-name";
1589
+ name.textContent = vendor;
1590
+ if (description !== null)
1591
+ name.title = description;
1592
+ add(name);
1593
+ const sep = () => {
1594
+ const node = doc.createElement("span");
1595
+ node.className = "vendor-sep";
1596
+ node.setAttribute("aria-hidden", "true");
1597
+ node.textContent = "\xB7";
1598
+ return node;
1599
+ };
1600
+ const link = (href, label, title) => {
1601
+ const node = doc.createElement("a");
1602
+ node.className = "vendor-link";
1603
+ node.href = href;
1604
+ node.target = "_blank";
1605
+ node.rel = "noopener noreferrer";
1606
+ node.referrerPolicy = "no-referrer";
1607
+ node.textContent = label;
1608
+ node.title = title;
1609
+ return node;
1610
+ };
1611
+ if (website !== null) {
1612
+ add(sep());
1613
+ add(link(website, hostname(website), `${vendor} website`));
1614
+ }
1615
+ if (guide !== null) {
1616
+ add(sep());
1617
+ add(link(guide, "Docs", `Docs for ${vendor}`));
1618
+ }
1619
+ if (docs !== null) {
1620
+ add(sep());
1621
+ add(link(docs, "API", `${vendor} API reference`));
1622
+ }
1623
+ host.removeAttribute("hidden");
1624
+ host.hidden = false;
1625
+ }
1626
+ function startAdminBrand(env) {
1627
+ const requested = env.params.get("brands");
1628
+ let brandsUrl = env.brandsUrl;
1629
+ if (requested) {
1630
+ try {
1631
+ const url = new URL(requested, env.location.href);
1632
+ const local = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "[::1]" || url.origin === env.location.origin;
1633
+ if ((url.protocol === "https:" || url.protocol === "http:") && local)
1634
+ brandsUrl = url.href;
1635
+ } catch {
1636
+ brandsUrl = env.brandsUrl;
1637
+ }
1638
+ }
1639
+ const vendorHost = env.$("vendor");
1640
+ const brandRoot = env.document.querySelector("[data-mockingbird-admin]");
1641
+ const brandService = brandRoot?.getAttribute("data-service") || "";
1642
+ if (vendorHost === null)
1643
+ return;
1644
+ env.fetch(brandsUrl, {
1645
+ headers: { accept: "application/json" },
1646
+ signal: AbortSignal.timeout(5e3)
1647
+ }).then(async (response) => {
1648
+ if (!response.ok)
1649
+ return null;
1650
+ const catalog = await response.json();
1651
+ return catalog;
1652
+ }).then((catalog) => {
1653
+ mountAdminBrand(env.document, vendorHost, brandService, catalog);
1654
+ }).catch(() => {
1655
+ });
1656
+ }
1657
+ function faultPresetList(value) {
1658
+ if (!isRecord2(value))
1659
+ throw new Error("Fault presets response was not an object");
1660
+ if (!Object.hasOwn(value, "presets") || value.presets === void 0 || value.presets === null) {
1661
+ return { presets: [] };
1662
+ }
1663
+ if (!Array.isArray(value.presets))
1664
+ throw new Error("Fault presets response was not a list");
1665
+ return {
1666
+ presets: value.presets.flatMap((item) => {
1667
+ if (!isRecord2(item) || typeof item.name !== "string")
1668
+ return [];
1669
+ return [{ name: item.name }];
1670
+ })
1671
+ };
1672
+ }
1673
+ function bootAdmin(config) {
1674
+ const fieldKinds = ["string", "number", "boolean", "null", "object", "array", "unknown"];
1675
+ const sources = ["declared", "inferred", "mixed"];
1676
+ const isFieldKind = (value) => typeof value === "string" && fieldKinds.some((kind) => kind === value);
1677
+ const isSource = (value) => typeof value === "string" && sources.some((source) => source === value);
1678
+ const readJson2 = (text) => {
1679
+ const value = JSON.parse(text);
1680
+ return value;
1681
+ };
1682
+ const esc = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => {
1683
+ switch (char) {
1684
+ case "&":
1685
+ return "&amp;";
1686
+ case "<":
1687
+ return "&lt;";
1688
+ case ">":
1689
+ return "&gt;";
1690
+ case '"':
1691
+ return "&quot;";
1692
+ default:
1693
+ return "&#39;";
1694
+ }
1695
+ });
1696
+ const el = (id, kind) => {
1697
+ const node = document.getElementById(id);
1698
+ if (!(node instanceof kind))
1699
+ throw new Error(`admin shell is missing #${id}`);
1700
+ return node;
1701
+ };
1702
+ const errorMessage = (data, fallback) => {
1703
+ if (!isRecord2(data))
1704
+ return fallback;
1705
+ const error = data.error;
1706
+ if (!isRecord2(error) || typeof error.message !== "string" || error.message === "") {
1707
+ return fallback;
1708
+ }
1709
+ return error.message;
1710
+ };
1711
+ const requireList = (value, label) => {
1712
+ if (value === void 0 || value === null)
1713
+ return [];
1714
+ if (!Array.isArray(value))
1715
+ throw new Error(`${label} was not a list`);
1716
+ return value;
1717
+ };
1718
+ const state = {
1719
+ namespace: "default",
1720
+ key: sessionStorage.getItem("mockingbird-admin-key") || "",
1721
+ view: "overview",
1722
+ collection: "",
1723
+ shape: null,
1724
+ route: null,
1725
+ editing: null
1726
+ };
1727
+ const standard = new Set(config.standardRoutes);
1728
+ const banner = el("banner", HTMLElement);
1729
+ const keyInput = el("admin-key", HTMLInputElement);
1730
+ keyInput.value = state.key;
1731
+ const params = new URLSearchParams(location.search);
1732
+ const queryKey = params.get("key");
1733
+ if (queryKey) {
1734
+ state.key = queryKey;
1735
+ keyInput.value = state.key;
1736
+ sessionStorage.setItem("mockingbird-admin-key", state.key);
1737
+ params.delete("key");
1738
+ const next = params.toString();
1739
+ history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
1740
+ }
1741
+ const queryNamespace = params.get("namespace");
1742
+ if (queryNamespace)
1743
+ state.namespace = queryNamespace;
1744
+ const showError = (error) => {
1745
+ banner.textContent = error instanceof Error ? error.message : String(error);
1746
+ banner.classList.add("show");
1747
+ };
1748
+ const clearError = () => {
1749
+ banner.classList.remove("show");
1750
+ };
1751
+ const api = async (method, path, body) => {
1752
+ const headers = { accept: "application/json" };
1753
+ if (state.key !== "")
1754
+ headers[config.adminKeyHeader] = state.key;
1755
+ if (state.namespace !== "")
1756
+ headers["x-mockingbird-namespace"] = state.namespace;
1757
+ if (body !== void 0)
1758
+ headers["content-type"] = "application/json";
1759
+ const init = { method, headers };
1760
+ if (body !== void 0)
1761
+ init.body = JSON.stringify(body);
1762
+ const response = await fetch(config.adminPrefix + path, init);
1763
+ const text = await response.text();
1764
+ let data = null;
1765
+ if (text !== "") {
1766
+ try {
1767
+ data = readJson2(text);
1768
+ } catch {
1769
+ data = { raw: text };
1770
+ }
1771
+ }
1772
+ if (response.status === 401) {
1773
+ keyInput.focus();
1774
+ throw new Error(errorMessage(data, "Admin key required"));
1775
+ }
1776
+ if (!response.ok) {
1777
+ throw new Error(errorMessage(data, `${response.status} ${method} ${path}`));
1778
+ }
1779
+ return data;
1780
+ };
1781
+ const originGet = async (path) => {
1782
+ const response = await fetch(path, { headers: { accept: "application/json" } });
1783
+ if (!response.ok)
1784
+ throw new Error(`${response.status} ${path}`);
1785
+ const value = await response.json();
1786
+ return value;
1787
+ };
1788
+ const panelApi = {
1789
+ get: (path) => api("GET", path),
1790
+ send: (method, path, body) => api(method, path, body),
1791
+ namespace: () => state.namespace
1792
+ };
1793
+ const isPanelScript = (value) => typeof value === "function";
1794
+ const setTheme = (theme) => {
1795
+ if (theme)
1796
+ document.documentElement.dataset.theme = theme;
1797
+ else
1798
+ delete document.documentElement.dataset.theme;
1799
+ };
1800
+ const storedTheme = localStorage.getItem("mockingbird-admin-theme");
1801
+ if (storedTheme)
1802
+ setTheme(storedTheme);
1803
+ el("theme", HTMLButtonElement).addEventListener("click", () => {
1804
+ const current = document.documentElement.dataset.theme;
1805
+ const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
1806
+ const next = dark ? "light" : "dark";
1807
+ localStorage.setItem("mockingbird-admin-theme", next);
1808
+ setTheme(next);
1809
+ });
1810
+ const show = (view) => {
1811
+ state.view = view;
1812
+ for (const button of document.querySelectorAll("#nav button")) {
1813
+ if (!(button instanceof HTMLButtonElement))
1814
+ continue;
1815
+ button.setAttribute("aria-current", button.dataset.view === view ? "true" : "false");
1816
+ }
1817
+ for (const section of document.querySelectorAll("main .view")) {
1818
+ if (!(section instanceof HTMLElement))
1819
+ continue;
1820
+ section.hidden = section.id !== `view-${view}`;
1821
+ }
1822
+ };
1823
+ el("nav", HTMLElement).addEventListener("click", (event) => {
1824
+ const target = event.target;
1825
+ if (!(target instanceof Element))
1826
+ return;
1827
+ const button = target.closest("button[data-view]");
1828
+ if (!(button instanceof HTMLButtonElement))
1829
+ return;
1830
+ const view = button.dataset.view;
1831
+ if (view === void 0)
1832
+ return;
1833
+ show(view);
1834
+ refresh().catch(showError);
1835
+ });
1836
+ 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>`;
1837
+ const table = (columns, rows) => {
1838
+ if (rows.length === 0)
1839
+ return '<p class="empty">Nothing here yet.</p>';
1840
+ const head = `<tr>${columns.map((column) => `<th>${column.label}</th>`).join("")}</tr>`;
1841
+ const body = rows.map((row) => {
1842
+ const cells = columns.map((column) => `<td data-label="${column.label}">${column.cell(row)}</td>`).join("");
1843
+ const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
1844
+ return `<tr${attrs}>${cells}</tr>`;
1845
+ }).join("");
1846
+ return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
1847
+ };
1848
+ const preview = (value) => {
1849
+ const text = JSON.stringify(value) ?? "null";
1850
+ return esc(text.length > 180 ? `${text.slice(0, 180)}\u2026` : text);
1851
+ };
1852
+ const when = (epoch) => {
1853
+ const date = new Date(epoch);
1854
+ return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
1855
+ };
1856
+ const clockCard = (epoch) => {
1857
+ const date = new Date(epoch);
1858
+ if (Number.isNaN(date.getTime()))
1859
+ return esc(String(epoch));
1860
+ const iso = date.toISOString();
1861
+ return `${esc(iso.slice(0, 10))}<span class="clock-time">${esc(iso.slice(11, 19))} UTC</span>`;
1862
+ };
1863
+ const decodeClock = (value) => {
1864
+ if (!isRecord2(value) || typeof value.now !== "number" || typeof value.offsetMs !== "number" || typeof value.frozen !== "boolean") {
1865
+ throw new Error("Clock response was not a clock");
1866
+ }
1867
+ return { now: value.now, offsetMs: value.offsetMs, frozen: value.frozen };
1868
+ };
1869
+ const decodeField = (value) => {
1870
+ if (!isRecord2(value) || typeof value.name !== "string" || !isFieldKind(value.kind)) {
1871
+ return void 0;
1872
+ }
1873
+ const field = {
1874
+ name: value.name,
1875
+ kind: value.kind,
1876
+ optional: value.optional === true
1877
+ };
1878
+ if (typeof value.description === "string")
1879
+ field.description = value.description;
1880
+ if (Array.isArray(value.fields)) {
1881
+ field.fields = value.fields.flatMap((item) => {
1882
+ const child = decodeField(item);
1883
+ return child === void 0 ? [] : [child];
1884
+ });
1885
+ }
1886
+ return field;
1887
+ };
1888
+ const decodeCollection = (value) => {
1889
+ if (!isRecord2(value) || typeof value.name !== "string")
1890
+ return void 0;
1891
+ const fields = Array.isArray(value.fields) ? value.fields.flatMap((item) => {
1892
+ const field = decodeField(item);
1893
+ return field === void 0 ? [] : [field];
1894
+ }) : [];
1895
+ const collection = {
1896
+ name: value.name,
1897
+ label: typeof value.label === "string" ? value.label : value.name,
1898
+ count: typeof value.count === "number" ? value.count : 0,
1899
+ source: isSource(value.source) ? value.source : "inferred",
1900
+ fields
1901
+ };
1902
+ if (typeof value.description === "string")
1903
+ collection.description = value.description;
1904
+ return collection;
1905
+ };
1906
+ const decodeState = (value) => {
1907
+ if (!isRecord2(value) || !Array.isArray(value.collections)) {
1908
+ throw new Error("State response has no collection list");
1909
+ }
1910
+ return {
1911
+ namespace: typeof value.namespace === "string" ? value.namespace : state.namespace,
1912
+ storageNamespace: typeof value.storageNamespace === "string" ? value.storageNamespace : "",
1913
+ collections: value.collections.flatMap((item) => {
1914
+ const collection = decodeCollection(item);
1915
+ return collection === void 0 ? [] : [collection];
1916
+ })
1917
+ };
1918
+ };
1919
+ const decodeRecords = (value) => requireList(isRecord2(value) ? value.records : void 0, "Records").flatMap((item) => {
1920
+ if (!isRecord2(item) || typeof item.id !== "string")
1921
+ return [];
1922
+ return [
1923
+ {
1924
+ id: item.id,
1925
+ seq: typeof item.seq === "number" ? item.seq : 0,
1926
+ value: item.value,
1927
+ attrs: ` class="clickable" data-id="${esc(item.id)}"`
1928
+ }
1929
+ ];
1930
+ });
1931
+ const textOf = (value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? String(value) : "";
1932
+ const decodeJournal = (value) => requireList(isRecord2(value) ? value.requests : void 0, "Journal").flatMap((item) => {
1933
+ if (!isRecord2(item))
1934
+ return [];
1935
+ return [
1936
+ {
1937
+ at: textOf(item.at),
1938
+ method: textOf(item.method),
1939
+ path: textOf(item.path),
1940
+ status: textOf(item.status),
1941
+ durationMs: textOf(item.durationMs),
1942
+ operationId: textOf(item.operationId)
1943
+ }
1944
+ ];
1945
+ });
1946
+ const decodeFaults = (value) => requireList(isRecord2(value) ? value.faults : void 0, "Faults").flatMap((item) => {
1947
+ if (!isRecord2(item))
1948
+ return [];
1949
+ return [
1950
+ {
1951
+ id: textOf(item.id),
1952
+ method: typeof item.method === "string" ? item.method : void 0,
1953
+ pathPrefix: typeof item.pathPrefix === "string" ? item.pathPrefix : void 0,
1954
+ operationId: typeof item.operationId === "string" ? item.operationId : void 0,
1955
+ status: typeof item.status === "number" ? item.status : void 0,
1956
+ effect: typeof item.effect === "string" ? item.effect : void 0,
1957
+ drop: typeof item.drop === "boolean" ? item.drop : void 0,
1958
+ latencyMs: typeof item.latencyMs === "number" ? item.latencyMs : void 0
1959
+ }
1960
+ ];
1961
+ });
1962
+ const loadNamespaces = async () => {
1963
+ const data = await api("GET", "/namespaces");
1964
+ const select = el("namespace", HTMLSelectElement);
1965
+ const listed = requireList(isRecord2(data) ? data.namespaces : void 0, "Namespaces");
1966
+ const fallback = isRecord2(data) && typeof data.default === "string" && data.default !== "" ? data.default : "default";
1967
+ const names = (listed.length > 0 ? listed.map((name) => String(name)) : [fallback]).slice();
1968
+ if (!names.includes(state.namespace))
1969
+ names.unshift(state.namespace);
1970
+ select.innerHTML = names.map((name) => `<option value="${esc(name)}">${esc(name)}</option>`).join("");
1971
+ select.value = state.namespace;
1972
+ };
1973
+ const renderOverview = async () => {
1974
+ const [health, shape, clock, journal] = await Promise.all([
1975
+ originGet("/health"),
1976
+ api("GET", "/state"),
1977
+ api("GET", "/clock"),
1978
+ api("GET", "/requests?limit=8")
1979
+ ]);
1980
+ if (!isRecord2(health))
1981
+ throw new Error("Health response was not an object");
1982
+ const decoded = decodeState(shape);
1983
+ state.shape = decoded;
1984
+ const time = decodeClock(clock);
1985
+ el("overview-cards", HTMLElement).innerHTML = cards([
1986
+ {
1987
+ k: "Service",
1988
+ v: esc(typeof health.service === "string" && health.service !== "" ? health.service : config.service)
1989
+ },
1990
+ {
1991
+ k: "Status",
1992
+ v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
1993
+ },
1994
+ { k: "Namespace", v: esc(state.namespace) },
1995
+ { k: "Collections", v: String(decoded.collections.length) },
1996
+ {
1997
+ k: "Records",
1998
+ v: String(decoded.collections.reduce((sum, item) => sum + item.count, 0))
1999
+ },
2000
+ { k: "Clock", v: clockCard(time.now) }
2001
+ ]);
2002
+ el("overview-requests", HTMLElement).innerHTML = table([
2003
+ { label: "Method", cell: (row) => esc(row.method) },
2004
+ { label: "Path", cell: (row) => esc(row.path) },
2005
+ {
2006
+ label: "Status",
2007
+ cell: (row) => `<span class="status-${row.status.charAt(0)}">${esc(row.status)}</span>`
2008
+ },
2009
+ { label: "Operation", cell: (row) => esc(row.operationId) }
2010
+ ], decodeJournal(journal));
2011
+ };
2012
+ const schemaChips = (collection) => {
2013
+ if (collection.fields.length === 0) {
2014
+ return '<p class="empty">No fields yet. Add a record or declare the collection on the service.</p>';
2015
+ }
2016
+ return `<div class="chips">${collection.fields.map((field) => `<span class="chip">${esc(field.name)} <i>${esc(field.kind)}${field.optional ? "?" : ""}</i></span>`).join("")}</div>`;
2017
+ };
2018
+ const renderState = async () => {
2019
+ const shape = decodeState(await api("GET", "/state"));
2020
+ state.shape = shape;
2021
+ const host = el("collections", HTMLElement);
2022
+ if (shape.collections.length === 0) {
2023
+ host.innerHTML = '<p class="empty">This namespace has no collections yet.</p>';
2024
+ el("schema", HTMLElement).innerHTML = "";
2025
+ el("records", HTMLElement).innerHTML = "";
2026
+ return;
2027
+ }
2028
+ if (!shape.collections.some((item) => item.name === state.collection)) {
2029
+ const first = shape.collections[0];
2030
+ if (first === void 0)
2031
+ return;
2032
+ state.collection = first.name;
2033
+ }
2034
+ 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("");
2035
+ const selected = shape.collections.find((item) => item.name === state.collection);
2036
+ if (selected === void 0)
2037
+ return;
2038
+ const description = selected.description ? `<p class="lede">${esc(selected.description)}</p>` : "";
2039
+ el("schema", HTMLElement).innerHTML = `<h3>${esc(selected.label)}</h3>${description}${schemaChips(selected)}`;
2040
+ const page = await api("GET", `/state/${encodeURIComponent(state.collection)}?limit=50`);
2041
+ el("records", HTMLElement).innerHTML = table([
2042
+ { label: "Id", cell: (row) => `<code>${esc(row.id)}</code>` },
2043
+ { label: "Seq", cell: (row) => esc(row.seq) },
2044
+ { label: "Value", cell: (row) => preview(row.value) }
2045
+ ], decodeRecords(page));
2046
+ };
2047
+ const renderClock = async () => {
2048
+ const clock = decodeClock(await api("GET", "/clock"));
2049
+ 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>`;
2050
+ const freeze = el("freeze", HTMLButtonElement);
2051
+ freeze.textContent = clock.frozen ? "Unfreeze" : "Freeze";
2052
+ freeze.dataset.frozen = clock.frozen ? "1" : "0";
2053
+ };
2054
+ const renderFaults = async () => {
2055
+ const [faults, presets] = await Promise.all([
2056
+ api("GET", "/faults"),
2057
+ api("GET", "/faults/presets").catch(() => ({ presets: [] }))
2058
+ ]);
2059
+ const select = el("fault-preset", HTMLSelectElement);
2060
+ const current = select.value;
2061
+ const list = faultPresetList(presets).presets;
2062
+ select.innerHTML = `<option value="">Custom rule</option>${list.map((preset) => `<option value="${esc(preset.name)}">${esc(preset.name)}</option>`).join("")}`;
2063
+ select.value = current;
2064
+ el("fault-list", HTMLElement).innerHTML = table([
2065
+ { label: "Id", cell: (row) => esc(row.id) },
2066
+ {
2067
+ label: "Match",
2068
+ cell: (row) => esc([row.method, row.pathPrefix, row.operationId].filter((part) => Boolean(part)).join(" ") || "all")
2069
+ },
2070
+ {
2071
+ label: "Effect",
2072
+ cell: (row) => esc(row.status ?? row.effect ?? (row.drop ? "drop" : row.latencyMs || ""))
2073
+ }
2074
+ ], decodeFaults(faults));
2075
+ };
2076
+ const renderJournal = async () => {
2077
+ const journal = await api("GET", "/requests?limit=100");
2078
+ el("journal", HTMLElement).innerHTML = table([
2079
+ { label: "When", cell: (row) => esc(row.at) },
2080
+ { label: "Method", cell: (row) => esc(row.method) },
2081
+ { label: "Path", cell: (row) => esc(row.path) },
2082
+ { label: "Status", cell: (row) => esc(row.status) },
2083
+ { label: "ms", cell: (row) => esc(row.durationMs) }
2084
+ ], decodeJournal(journal));
2085
+ };
2086
+ const renderRoutes = async () => {
2087
+ const data = await api("GET", "/");
2088
+ if (!isRecord2(data))
2089
+ throw new Error("Route list was not an object");
2090
+ const routes = requireList(data.routes, "Routes").map((route) => String(route));
2091
+ el("route-list", HTMLElement).innerHTML = routes.map((route) => {
2092
+ const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
2093
+ const parts = route.split(" ");
2094
+ const method = parts[0] ?? "";
2095
+ const path = parts.slice(1).join(" ");
2096
+ 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>`;
2097
+ }).join("");
2098
+ };
2099
+ const renderers = {
2100
+ overview: renderOverview,
2101
+ state: renderState,
2102
+ clock: renderClock,
2103
+ faults: renderFaults,
2104
+ journal: renderJournal,
2105
+ routes: renderRoutes
2106
+ };
2107
+ async function refresh() {
2108
+ clearError();
2109
+ const render = renderers[state.view];
2110
+ if (render)
2111
+ await render();
2112
+ }
2113
+ el("namespace", HTMLSelectElement).addEventListener("change", () => {
2114
+ state.namespace = el("namespace", HTMLSelectElement).value;
2115
+ refresh().catch(showError);
2116
+ });
2117
+ keyInput.addEventListener("change", () => {
2118
+ state.key = keyInput.value.trim();
2119
+ sessionStorage.setItem("mockingbird-admin-key", state.key);
2120
+ refresh().catch(showError);
2121
+ });
2122
+ el("refresh", HTMLButtonElement).addEventListener("click", () => {
2123
+ refresh().catch(showError);
2124
+ });
2125
+ el("collections", HTMLElement).addEventListener("click", (event) => {
2126
+ const target = event.target;
2127
+ if (!(target instanceof Element))
2128
+ return;
2129
+ const button = target.closest("[data-collection]");
2130
+ if (!(button instanceof HTMLElement))
2131
+ return;
2132
+ const collection = button.dataset.collection;
2133
+ if (collection === void 0)
2134
+ return;
2135
+ state.collection = collection;
2136
+ renderState().catch(showError);
2137
+ });
2138
+ el("records", HTMLElement).addEventListener("click", (event) => {
2139
+ const target = event.target;
2140
+ if (!(target instanceof Element))
2141
+ return;
2142
+ const row = target.closest("[data-id]");
2143
+ if (!(row instanceof HTMLElement))
2144
+ return;
2145
+ const id = row.dataset.id;
2146
+ if (id === void 0)
2147
+ return;
2148
+ openEditor(id).catch(showError);
2149
+ });
2150
+ const editor = el("editor", HTMLDialogElement);
2151
+ const editorId = el("editor-id", HTMLInputElement);
2152
+ const editorValue = el("editor-value", HTMLTextAreaElement);
2153
+ const editorDelete = el("editor-delete", HTMLButtonElement);
2154
+ async function openEditor(id) {
2155
+ state.editing = id;
2156
+ el("editor-title", HTMLElement).textContent = id ? "Edit record" : "New record";
2157
+ editorId.value = id ?? "";
2158
+ editorId.readOnly = id !== null;
2159
+ editorDelete.hidden = id === null;
2160
+ if (id) {
2161
+ const record = await api("GET", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`);
2162
+ if (!isRecord2(record))
2163
+ throw new Error("Record response was not an object");
2164
+ editorValue.value = JSON.stringify(record.value, null, 2) ?? "";
2165
+ } else {
2166
+ editorValue.value = "{\n \n}";
2167
+ }
2168
+ editor.showModal();
2169
+ }
2170
+ el("new-record", HTMLButtonElement).addEventListener("click", () => {
2171
+ openEditor(null).catch(showError);
2172
+ });
2173
+ el("editor-form", HTMLFormElement).addEventListener("submit", (event) => {
2174
+ const submitter = event.submitter;
2175
+ if (!(submitter instanceof HTMLButtonElement) || submitter.value !== "save")
2176
+ return;
2177
+ event.preventDefault();
2178
+ let value;
2179
+ try {
2180
+ value = readJson2(editorValue.value);
2181
+ } catch {
2182
+ showError(new Error("Value is not JSON"));
2183
+ return;
2184
+ }
2185
+ const id = editorId.value.trim();
2186
+ const path = `/state/${encodeURIComponent(state.collection)}`;
2187
+ const request = state.editing ? api("PUT", `${path}/${encodeURIComponent(id)}`, { value }) : api("POST", path, { id: id || void 0, value });
2188
+ request.then(() => {
2189
+ editor.close();
2190
+ return renderState();
2191
+ }).catch(showError);
2192
+ });
2193
+ editorDelete.addEventListener("click", () => {
2194
+ const id = state.editing;
2195
+ if (!id)
2196
+ return;
2197
+ api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
2198
+ editor.close();
2199
+ return renderState();
2200
+ }).catch(showError);
2201
+ });
2202
+ for (const button of document.querySelectorAll("[data-advance]")) {
2203
+ if (!(button instanceof HTMLButtonElement))
2204
+ continue;
2205
+ button.addEventListener("click", () => {
2206
+ const raw = button.dataset.advance;
2207
+ if (raw === void 0)
2208
+ return;
2209
+ api("POST", "/clock", { advance: Number(raw) }).then(() => renderClock()).catch(showError);
2210
+ });
2211
+ }
2212
+ el("freeze", HTMLButtonElement).addEventListener("click", () => {
2213
+ const frozen = el("freeze", HTMLButtonElement).dataset.frozen === "1";
2214
+ api("POST", "/clock", { freeze: !frozen }).then(() => renderClock()).catch(showError);
2215
+ });
2216
+ el("clock-reset", HTMLButtonElement).addEventListener("click", () => {
2217
+ api("POST", "/clock", { reset: true }).then(() => renderClock()).catch(showError);
2218
+ });
2219
+ el("clock-set", HTMLFormElement).addEventListener("submit", (event) => {
2220
+ event.preventDefault();
2221
+ const raw = el("clock-instant", HTMLInputElement).value.trim();
2222
+ const set = /^-?\d+$/.test(raw) ? Number(raw) : raw;
2223
+ api("POST", "/clock", { set }).then(() => renderClock()).catch(showError);
2224
+ });
2225
+ el("fault-form", HTMLFormElement).addEventListener("submit", (event) => {
2226
+ event.preventDefault();
2227
+ const preset = el("fault-preset", HTMLSelectElement).value;
2228
+ if (preset !== "") {
2229
+ api("POST", "/faults", { preset }).then(() => renderFaults()).catch(showError);
2230
+ return;
2231
+ }
2232
+ const status = Number(el("fault-status", HTMLInputElement).value);
2233
+ if (!Number.isFinite(status)) {
2234
+ showError(new Error("Status must be a number, or choose a preset"));
2235
+ return;
2236
+ }
2237
+ const path = el("fault-path", HTMLInputElement).value;
2238
+ const method = el("fault-method", HTMLInputElement).value;
2239
+ api("POST", "/faults", {
2240
+ status,
2241
+ ...path !== "" ? { pathPrefix: path } : {},
2242
+ ...method !== "" ? { method } : {}
2243
+ }).then(() => renderFaults()).catch(showError);
2244
+ });
2245
+ el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
2246
+ api("DELETE", "/faults").then(() => renderFaults()).catch(showError);
2247
+ });
2248
+ el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
2249
+ api("DELETE", "/requests").then(() => renderJournal()).catch(showError);
2250
+ });
2251
+ el("route-list", HTMLElement).addEventListener("click", (event) => {
2252
+ const target = event.target;
2253
+ if (!(target instanceof Element))
2254
+ return;
2255
+ const button = target.closest("[data-route]");
2256
+ if (!(button instanceof HTMLElement))
2257
+ return;
2258
+ const route = button.dataset.route;
2259
+ if (route === void 0)
2260
+ return;
2261
+ state.route = route;
2262
+ el("route-title", HTMLElement).textContent = route;
2263
+ const method = route.split(" ")[0] ?? "";
2264
+ el("route-body", HTMLTextAreaElement).disabled = method === "GET" || method === "DELETE";
2265
+ });
2266
+ el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
2267
+ event.preventDefault();
2268
+ const send = async () => {
2269
+ if (state.route === null)
2270
+ throw new Error("Choose a route first");
2271
+ const parts = state.route.split(" ");
2272
+ const method = parts[0];
2273
+ const path = parts.slice(1).join(" ");
2274
+ if (method === void 0)
2275
+ throw new Error("Choose a route first");
2276
+ const concrete = path.replace(/:([A-Za-z0-9_]+)/g, (_match, key) => prompt(`Value for ${key}`) || "");
2277
+ let body;
2278
+ const routeBody = el("route-body", HTMLTextAreaElement);
2279
+ if (method !== "GET" && method !== "DELETE" && routeBody.value.trim() !== "") {
2280
+ try {
2281
+ body = readJson2(routeBody.value);
2282
+ } catch {
2283
+ throw new Error("Body is not JSON");
2284
+ }
2285
+ }
2286
+ const data = await api(method, concrete, body);
2287
+ el("route-result", HTMLElement).textContent = JSON.stringify(data, null, 2);
2288
+ };
2289
+ send().catch(showError);
2290
+ });
2291
+ const decodePanel = (value) => {
2292
+ if (!isRecord2(value) || typeof value.id !== "string" || typeof value.title !== "string") {
2293
+ return void 0;
2294
+ }
2295
+ const panel = {
2296
+ id: value.id,
2297
+ title: value.title,
2298
+ description: typeof value.description === "string" ? value.description : "",
2299
+ html: typeof value.html === "string" ? value.html : ""
2300
+ };
2301
+ if (typeof value.script === "string")
2302
+ panel.script = value.script;
2303
+ return panel;
2304
+ };
2305
+ const mountPanels = (panels) => {
2306
+ const nav = el("nav", HTMLElement);
2307
+ const views = el("views", HTMLElement);
2308
+ for (const panel of panels) {
2309
+ const button = document.createElement("button");
2310
+ button.type = "button";
2311
+ button.dataset.view = panel.id;
2312
+ button.textContent = panel.title;
2313
+ nav.append(button);
2314
+ const section = document.createElement("section");
2315
+ section.className = "view";
2316
+ section.id = `view-${panel.id}`;
2317
+ section.hidden = true;
2318
+ const title = document.createElement("h2");
2319
+ title.textContent = panel.title;
2320
+ const lede = document.createElement("p");
2321
+ lede.className = "lede";
2322
+ lede.textContent = panel.description;
2323
+ const body = document.createElement("div");
2324
+ body.className = "panel-body";
2325
+ body.id = `panel-${panel.id}`;
2326
+ body.innerHTML = panel.html;
2327
+ section.append(title, lede, body);
2328
+ views.append(section);
2329
+ if (panel.script === void 0)
2330
+ continue;
2331
+ try {
2332
+ const compiled = new Function("root", "api", panel.script);
2333
+ if (!isPanelScript(compiled))
2334
+ throw new Error("panel script did not compile");
2335
+ compiled(body, panelApi);
2336
+ } catch (error) {
2337
+ const note = document.createElement("p");
2338
+ note.className = "banner show";
2339
+ note.textContent = error instanceof Error ? error.message : String(error);
2340
+ body.append(note);
2341
+ }
2342
+ }
2343
+ };
2344
+ startAdminBrand({
2345
+ document,
2346
+ $: (id) => document.getElementById(id),
2347
+ params,
2348
+ location: { href: location.href, origin: location.origin },
2349
+ fetch,
2350
+ brandsUrl: config.brandsUrl
2351
+ });
2352
+ loadNamespaces().then(() => api("GET", "/ui/manifest")).then((manifest) => {
2353
+ const panels = requireList(isRecord2(manifest) ? manifest.panels : void 0, "Panels").flatMap((panel) => {
2354
+ const decoded = decodePanel(panel);
2355
+ return decoded === void 0 ? [] : [decoded];
2356
+ });
2357
+ mountPanels(panels);
2358
+ return refresh();
2359
+ }).catch(showError);
2360
+ }
2361
+ var clientPrelude = [isRecord2, mountAdminBrand, startAdminBrand, faultPresetList, bootAdmin].map((fn) => fn.toString()).join("\n");
2362
+ var adminClientSource = (config) => {
2363
+ const payload = JSON.stringify(config).replace(/</g, "\\u003c");
2364
+ return `<script>
2365
+ ${clientPrelude}
2366
+ bootAdmin(${payload});
2367
+ </script>`;
2368
+ };
2369
+
1144
2370
  // ../core/dist/surface.js
1145
2371
  var defineMock = (runtime) => runtime;
1146
2372
  var STANDARD_ADMIN_ROUTES = [
@@ -1209,94 +2435,106 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
1209
2435
  return "&#39;";
1210
2436
  }
1211
2437
  });
2438
+ var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
1212
2439
  var renderAdminDocument = (service) => {
1213
2440
  const name = escapeHtml(service);
1214
- const standard = JSON.stringify(STANDARD_ADMIN_ROUTES);
2441
+ const script = adminClientSource({
2442
+ standardRoutes: STANDARD_ADMIN_ROUTES,
2443
+ adminPrefix: ADMIN_PREFIX,
2444
+ adminKeyHeader: ADMIN_KEY_HEADER,
2445
+ brandsUrl: ADMIN_BRANDS_URL,
2446
+ service
2447
+ });
1215
2448
  return `<!DOCTYPE html>
1216
2449
  <html lang="en">
1217
2450
  <head>
1218
2451
  <meta charset="utf-8">
1219
2452
  <meta name="viewport" content="width=device-width, initial-scale=1">
1220
2453
  <meta name="color-scheme" content="light dark">
1221
- <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">
2454
+ <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">
1222
2455
  <title>${name} admin</title>
1223
2456
  <style>
2457
+ ${CSS_RESET}
1224
2458
  :root {
1225
2459
  color-scheme: light;
1226
- --bg: #f6f5f2;
2460
+ --bg: #fafafa;
1227
2461
  --bg-elev: #ffffff;
1228
- --bg-muted: #eeece6;
1229
- --bg-inset: #f3f1eb;
1230
- --line: #e2dfd6;
1231
- --line-strong: #d0ccc1;
1232
- --ink: #1c1915;
1233
- --muted: #5e584f;
1234
- --faint: #8a8378;
1235
- --accent: #3d4f3a;
1236
- --accent-ink: #f7f6f2;
1237
- --accent-soft: #e5ece2;
1238
- --ok: #1f7a45;
1239
- --ok-soft: #e5f4eb;
1240
- --warn: #9a6700;
1241
- --warn-soft: #fbf3df;
1242
- --err: #a33b32;
1243
- --err-soft: #fdecea;
1244
- --shadow: 0 1px 2px rgb(28 25 21 / 0.05), 0 12px 32px rgb(28 25 21 / 0.06);
1245
- --sans: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
2462
+ --bg-muted: #f4f4f5;
2463
+ --bg-inset: #f4f4f5;
2464
+ --line: #dedee3;
2465
+ --line-strong: #c4c4cc;
2466
+ --ink: #18181b;
2467
+ --muted: #62626b;
2468
+ --faint: #71717a;
2469
+ --accent: #27272a;
2470
+ --accent-ink: #ffffff;
2471
+ --accent-soft: #f4f4f5;
2472
+ --ok: #166534;
2473
+ --ok-soft: #f0fdf4;
2474
+ --warn: #a16207;
2475
+ --warn-soft: #fefce8;
2476
+ --err: #a82d32;
2477
+ --err-soft: #fef2f2;
2478
+ --shadow: none;
2479
+ --sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
1246
2480
  --mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
1247
- --radius: 14px;
1248
- --header: 60px;
1249
- --nav: 220px;
2481
+ --radius: 10px;
2482
+ --header: 56px;
2483
+ --nav: 200px;
1250
2484
  }
1251
2485
  :root[data-theme="dark"] {
1252
2486
  color-scheme: dark;
1253
- --bg: #141311;
1254
- --bg-elev: #1e1c19;
1255
- --bg-muted: #26231f;
1256
- --bg-inset: #181715;
1257
- --line: #34302b;
1258
- --line-strong: #4a453d;
1259
- --ink: #f4f1ea;
1260
- --muted: #c4bdb1;
1261
- --faint: #8f877c;
1262
- --accent: #c5d5b8;
1263
- --accent-ink: #1a2118;
1264
- --accent-soft: #2a3328;
1265
- --ok: #8fd6a8;
1266
- --ok-soft: #1a2e22;
1267
- --warn: #f0cc78;
1268
- --warn-soft: #332911;
1269
- --err: #f0a8a2;
1270
- --err-soft: #3a201e;
1271
- --shadow: 0 1px 2px rgb(0 0 0 / 0.3), 0 16px 40px rgb(0 0 0 / 0.28);
2487
+ --bg: #111113;
2488
+ --bg-elev: #19191c;
2489
+ --bg-muted: #242428;
2490
+ --bg-inset: #111113;
2491
+ --line: #36363c;
2492
+ --line-strong: #52525b;
2493
+ --ink: #f4f4f5;
2494
+ --muted: #a9a9b2;
2495
+ --faint: #71717a;
2496
+ --accent: #e4e4e7;
2497
+ --accent-ink: #18181b;
2498
+ --accent-soft: #242428;
2499
+ --ok: #86efac;
2500
+ --ok-soft: #14241b;
2501
+ --warn: #fde68a;
2502
+ --warn-soft: #2a2410;
2503
+ --err: #ffaaaa;
2504
+ --err-soft: #2c1516;
2505
+ --shadow: none;
1272
2506
  }
1273
2507
  @media (prefers-color-scheme: dark) {
1274
2508
  :root:not([data-theme="light"]) {
1275
2509
  color-scheme: dark;
1276
- --bg: #141311;
1277
- --bg-elev: #1e1c19;
1278
- --bg-muted: #26231f;
1279
- --bg-inset: #181715;
1280
- --line: #34302b;
1281
- --line-strong: #4a453d;
1282
- --ink: #f4f1ea;
1283
- --muted: #c4bdb1;
1284
- --faint: #8f877c;
1285
- --accent: #c5d5b8;
1286
- --accent-ink: #1a2118;
1287
- --accent-soft: #2a3328;
1288
- --ok: #8fd6a8;
1289
- --ok-soft: #1a2e22;
1290
- --warn: #f0cc78;
1291
- --warn-soft: #332911;
1292
- --err: #f0a8a2;
1293
- --err-soft: #3a201e;
1294
- --shadow: 0 1px 2px rgb(0 0 0 / 0.3), 0 16px 40px rgb(0 0 0 / 0.28);
1295
- }
1296
- }
1297
- * { box-sizing: border-box; }
1298
- html, body { margin: 0; height: 100%; }
2510
+ --bg: #111113;
2511
+ --bg-elev: #19191c;
2512
+ --bg-muted: #242428;
2513
+ --bg-inset: #111113;
2514
+ --line: #36363c;
2515
+ --line-strong: #52525b;
2516
+ --ink: #f4f4f5;
2517
+ --muted: #a9a9b2;
2518
+ --faint: #71717a;
2519
+ --accent: #e4e4e7;
2520
+ --accent-ink: #18181b;
2521
+ --accent-soft: #242428;
2522
+ --ok: #86efac;
2523
+ --ok-soft: #14241b;
2524
+ --warn: #fde68a;
2525
+ --warn-soft: #2a2410;
2526
+ --err: #ffaaaa;
2527
+ --err-soft: #2c1516;
2528
+ --shadow: none;
2529
+ }
2530
+ }
2531
+ /* Fill the viewport, then grow with the content so the background covers the scroll. */
2532
+ html { height: 100%; }
1299
2533
  body {
2534
+ margin: 0;
2535
+ min-height: 100%;
2536
+ display: flex;
2537
+ flex-direction: column;
1300
2538
  font-family: var(--sans);
1301
2539
  background: var(--bg);
1302
2540
  color: var(--ink);
@@ -1305,24 +2543,37 @@ var renderAdminDocument = (service) => {
1305
2543
  padding-right: env(safe-area-inset-right);
1306
2544
  -webkit-text-size-adjust: 100%;
1307
2545
  }
1308
- button, input, select, textarea { font: inherit; color: inherit; }
1309
- button { cursor: pointer; }
1310
2546
  :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
1311
- @media (prefers-reduced-motion: reduce) {
1312
- * { scroll-behavior: auto !important; transition: none !important; }
1313
- }
1314
- .app { min-height: 100%; display: flex; flex-direction: column; }
2547
+ .app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
1315
2548
  header.top {
1316
2549
  position: sticky; top: 0; z-index: 5;
1317
2550
  display: flex; align-items: center; gap: 12px;
1318
2551
  min-height: var(--header); padding: 10px 16px;
1319
- background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
2552
+ background: var(--bg-elev);
1320
2553
  border-bottom: 1px solid var(--line);
1321
- backdrop-filter: blur(10px);
1322
- }
1323
- .brand { display: flex; flex-direction: column; min-width: 0; margin-right: auto; }
1324
- .brand strong { font-size: 15px; letter-spacing: -0.01em; }
1325
- .brand span { color: var(--faint); font-size: 12px; }
2554
+ min-width: 0; max-width: 100%;
2555
+ }
2556
+ .brand {
2557
+ display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
2558
+ min-width: 0; max-width: 100%; margin-right: auto;
2559
+ }
2560
+ .brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; }
2561
+ .brand-id strong { font-size: 15px; letter-spacing: -0.01em; }
2562
+ .brand-id span { color: var(--faint); font-size: 12px; }
2563
+ .vendor {
2564
+ display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
2565
+ min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
2566
+ }
2567
+ .vendor[hidden] { display: none; }
2568
+ /* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
2569
+ .vendor img {
2570
+ width: 16px; height: 16px; padding: 2px; box-sizing: content-box;
2571
+ border-radius: 4px; border: 1px solid var(--line); background: #fff; object-fit: contain;
2572
+ }
2573
+ .vendor-name { color: var(--ink); font-weight: 600; }
2574
+ .vendor-sep { color: var(--faint); }
2575
+ .vendor-link { color: inherit; text-decoration: none; }
2576
+ .vendor-link:hover { color: var(--ink); text-decoration: underline; }
1326
2577
  .controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
1327
2578
  label.field { display: flex; flex-direction: column; gap: 2px; font-size: 11px; color: var(--faint); }
1328
2579
  label.field input, label.field select, .controls input[type="password"] {
@@ -1340,7 +2591,7 @@ var renderAdminDocument = (service) => {
1340
2591
  record or the full nav row would widen the page past the viewport. */
1341
2592
  .layout {
1342
2593
  display: grid; grid-template-columns: var(--nav) minmax(0, 1fr);
1343
- flex: 1; min-height: 0; min-width: 0;
2594
+ flex: 1 0 auto; min-width: 0;
1344
2595
  }
1345
2596
  nav.side {
1346
2597
  border-right: 1px solid var(--line); padding: 12px;
@@ -1394,8 +2645,8 @@ var renderAdminDocument = (service) => {
1394
2645
  .banner.show { display: block; }
1395
2646
  .empty { color: var(--muted); padding: 24px 8px; }
1396
2647
  dialog {
1397
- border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--bg-elev);
1398
- color: var(--ink); width: min(640px, calc(100vw - 24px)); box-shadow: var(--shadow);
2648
+ border: 1px solid var(--line); border-radius: var(--radius); padding: 0; background: var(--bg-elev);
2649
+ color: var(--ink); width: min(640px, calc(100vw - 24px)); box-shadow: none;
1399
2650
  }
1400
2651
  dialog::backdrop { background: rgb(20 19 17 / 0.45); }
1401
2652
  dialog form, .dialog-body { padding: 16px; display: flex; flex-direction: column; gap: 10px; }
@@ -1422,7 +2673,7 @@ var renderAdminDocument = (service) => {
1422
2673
  padding-top: max(10px, env(safe-area-inset-top));
1423
2674
  }
1424
2675
  input, select, textarea { font-size: 16px; }
1425
- .controls { width: 100%; }
2676
+ .brand, .vendor, .controls { width: 100%; }
1426
2677
  label.field, label.field select, label.field input { flex: 1; min-width: 0; }
1427
2678
  .layout { grid-template-columns: minmax(0, 1fr); }
1428
2679
  nav.side {
@@ -1453,8 +2704,11 @@ var renderAdminDocument = (service) => {
1453
2704
  <div class="app" data-service="${name}" data-mockingbird-admin>
1454
2705
  <header class="top">
1455
2706
  <div class="brand">
1456
- <strong>${name}</strong>
1457
- <span>Mockingbird admin</span>
2707
+ <div class="brand-id">
2708
+ <strong>${name}</strong>
2709
+ <span>Admin</span>
2710
+ </div>
2711
+ <div class="vendor" id="vendor" hidden></div>
1458
2712
  </div>
1459
2713
  <div class="controls">
1460
2714
  <label class="field">Namespace
@@ -1584,460 +2838,7 @@ var renderAdminDocument = (service) => {
1584
2838
  </div>
1585
2839
  </form>
1586
2840
  </dialog>
1587
- <script>
1588
- (() => {
1589
- const STANDARD = new Set(${standard})
1590
- const state = {
1591
- namespace: "default",
1592
- key: sessionStorage.getItem("mockingbird-admin-key") || "",
1593
- view: "overview",
1594
- collection: "",
1595
- shape: null,
1596
- route: null,
1597
- editing: null
1598
- }
1599
- const $ = (id) => document.getElementById(id)
1600
- const banner = $("banner")
1601
- const keyInput = $("admin-key")
1602
- keyInput.value = state.key
1603
- const params = new URLSearchParams(location.search)
1604
- if (params.get("key")) {
1605
- state.key = params.get("key")
1606
- keyInput.value = state.key
1607
- sessionStorage.setItem("mockingbird-admin-key", state.key)
1608
- params.delete("key")
1609
- const next = params.toString()
1610
- history.replaceState(null, "", location.pathname + (next ? "?" + next : ""))
1611
- }
1612
- if (params.get("namespace")) state.namespace = params.get("namespace")
1613
-
1614
- const showError = (error) => {
1615
- banner.textContent = error instanceof Error ? error.message : String(error)
1616
- banner.classList.add("show")
1617
- }
1618
- const clearError = () => banner.classList.remove("show")
1619
-
1620
- async function originGet(path) {
1621
- const response = await fetch(path, { headers: { accept: "application/json" } })
1622
- if (!response.ok) throw new Error(response.status + " " + path)
1623
- return response.json()
1624
- }
1625
- async function api(method, path, body) {
1626
- const headers = { accept: "application/json" }
1627
- if (state.key) headers["${ADMIN_KEY_HEADER}"] = state.key
1628
- if (state.namespace) headers["x-mockingbird-namespace"] = state.namespace
1629
- if (body !== undefined) headers["content-type"] = "application/json"
1630
- const response = await fetch("${ADMIN_PREFIX}" + path, {
1631
- method,
1632
- headers,
1633
- body: body === undefined ? undefined : JSON.stringify(body)
1634
- })
1635
- const text = await response.text()
1636
- let data = null
1637
- if (text) {
1638
- try { data = JSON.parse(text) } catch { data = { raw: text } }
1639
- }
1640
- if (response.status === 401) {
1641
- keyInput.focus()
1642
- throw new Error((data && data.error && data.error.message) || "Admin key required")
1643
- }
1644
- if (!response.ok) {
1645
- throw new Error((data && data.error && data.error.message) || (response.status + " " + method + " " + path))
1646
- }
1647
- return data
1648
- }
1649
- const panelApi = {
1650
- get: (path) => api("GET", path),
1651
- send: (method, path, body) => api(method, path, body),
1652
- namespace: () => state.namespace
1653
- }
1654
-
1655
- function setTheme(theme) {
1656
- if (theme) document.documentElement.dataset.theme = theme
1657
- else delete document.documentElement.dataset.theme
1658
- }
1659
- const storedTheme = localStorage.getItem("mockingbird-admin-theme")
1660
- if (storedTheme) setTheme(storedTheme)
1661
- $("theme").addEventListener("click", () => {
1662
- const current = document.documentElement.dataset.theme
1663
- const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches
1664
- const next = dark ? "light" : "dark"
1665
- localStorage.setItem("mockingbird-admin-theme", next)
1666
- setTheme(next)
1667
- })
1668
-
1669
- function show(view) {
1670
- state.view = view
1671
- for (const button of document.querySelectorAll("#nav button")) {
1672
- button.setAttribute("aria-current", button.dataset.view === view ? "true" : "false")
1673
- }
1674
- for (const section of document.querySelectorAll("main .view")) {
1675
- section.hidden = section.id !== "view-" + view
1676
- }
1677
- }
1678
- document.getElementById("nav").addEventListener("click", (event) => {
1679
- const button = event.target.closest("button[data-view]")
1680
- if (!button) return
1681
- show(button.dataset.view)
1682
- refresh().catch(showError)
1683
- })
1684
-
1685
- function cards(items) {
1686
- return '<div class="grid">' + items.map((item) =>
1687
- '<article class="card"><div class="k">' + item.k + '</div><div class="v">' + item.v + '</div></article>'
1688
- ).join("") + "</div>"
1689
- }
1690
- function table(columns, rows) {
1691
- if (rows.length === 0) return '<p class="empty">Nothing here yet.</p>'
1692
- const head = "<tr>" + columns.map((column) => "<th>" + column.label + "</th>").join("") + "</tr>"
1693
- const body = rows.map((row) => "<tr" + (row.attrs || "") + ">" + columns.map((column) =>
1694
- '<td data-label="' + column.label + '">' + column.cell(row) + "</td>"
1695
- ).join("") + "</tr>").join("")
1696
- return "<table><thead>" + head + "</thead><tbody>" + body + "</tbody></table>"
1697
- }
1698
- const esc = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => ({
1699
- "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;"
1700
- }[char]))
1701
- const preview = (value) => {
1702
- const text = JSON.stringify(value)
1703
- return esc(text.length > 180 ? text.slice(0, 180) + "\u2026" : text)
1704
- }
1705
- const when = (epoch) => {
1706
- const date = new Date(epoch)
1707
- return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString()
1708
- }
1709
- const clockCard = (epoch) => {
1710
- const date = new Date(epoch)
1711
- if (Number.isNaN(date.getTime())) return esc(String(epoch))
1712
- const iso = date.toISOString()
1713
- return esc(iso.slice(0, 10)) + '<span class="clock-time">' + esc(iso.slice(11, 19)) + " UTC</span>"
1714
- }
1715
-
1716
- async function loadNamespaces() {
1717
- const data = await api("GET", "/namespaces")
1718
- const select = $("namespace")
1719
- const names = data.namespaces && data.namespaces.length ? data.namespaces : [data.default || "default"]
1720
- if (!names.includes(state.namespace)) names.unshift(state.namespace)
1721
- select.innerHTML = names.map((name) =>
1722
- '<option value="' + esc(name) + '">' + esc(name) + "</option>"
1723
- ).join("")
1724
- select.value = state.namespace
1725
- }
1726
-
1727
- async function renderOverview() {
1728
- const [health, shape, clock, journal] = await Promise.all([
1729
- originGet("/health"),
1730
- api("GET", "/state"),
1731
- api("GET", "/clock"),
1732
- api("GET", "/requests?limit=8")
1733
- ])
1734
- state.shape = shape
1735
- $("overview-cards").innerHTML = cards([
1736
- { k: "Service", v: esc(health.service || "${name}") },
1737
- { k: "Status", v: esc(health.status || "ok") },
1738
- { k: "Namespace", v: esc(state.namespace) },
1739
- { k: "Collections", v: String(shape.collections.length) },
1740
- { k: "Records", v: String(shape.collections.reduce((sum, item) => sum + item.count, 0)) },
1741
- { k: "Clock", v: clockCard(clock.now) }
1742
- ])
1743
- const requests = (journal && journal.requests) || []
1744
- $("overview-requests").innerHTML = table(
1745
- [
1746
- { label: "Method", cell: (row) => esc(row.method) },
1747
- { label: "Path", cell: (row) => esc(row.path) },
1748
- { label: "Status", cell: (row) => '<span class="status-' + String(row.status).charAt(0) + '">' + esc(row.status) + "</span>" },
1749
- { label: "Operation", cell: (row) => esc(row.operationId || "") }
1750
- ],
1751
- requests
1752
- )
1753
- }
1754
-
1755
- function schemaChips(collection) {
1756
- if (!collection || collection.fields.length === 0) {
1757
- return '<p class="empty">No fields yet. Add a record or declare the collection on the service.</p>'
1758
- }
1759
- return '<div class="chips">' + collection.fields.map((field) =>
1760
- '<span class="chip">' + esc(field.name) + ' <i>' + esc(field.kind) + (field.optional ? "?" : "") + "</i></span>"
1761
- ).join("") + "</div>"
1762
- }
1763
-
1764
- async function renderState() {
1765
- const shape = await api("GET", "/state")
1766
- state.shape = shape
1767
- const host = $("collections")
1768
- if (shape.collections.length === 0) {
1769
- host.innerHTML = '<p class="empty">This namespace has no collections yet.</p>'
1770
- $("schema").innerHTML = ""
1771
- $("records").innerHTML = ""
1772
- return
1773
- }
1774
- if (!shape.collections.some((item) => item.name === state.collection)) {
1775
- state.collection = shape.collections[0].name
1776
- }
1777
- host.innerHTML = shape.collections.map((item) =>
1778
- '<button type="button" class="collection" data-collection="' + esc(item.name) + '" aria-current="' +
1779
- (item.name === state.collection ? "true" : "false") + '"><strong>' + esc(item.label) +
1780
- '</strong><small>' + esc(item.count) + " \xB7 " + esc(item.source) + "</small></button>"
1781
- ).join("")
1782
- const selected = shape.collections.find((item) => item.name === state.collection)
1783
- $("schema").innerHTML = "<h3>" + esc(selected.label) + "</h3>" +
1784
- (selected.description ? "<p class=" + '"lede">' + esc(selected.description) + "</p>" : "") +
1785
- schemaChips(selected)
1786
- const page = await api("GET", "/state/" + encodeURIComponent(state.collection) + "?limit=50")
1787
- const rows = page.records.map((record) => ({
1788
- ...record,
1789
- attrs: ' class="clickable" data-id="' + esc(record.id) + '"'
1790
- }))
1791
- $("records").innerHTML = table(
1792
- [
1793
- { label: "Id", cell: (row) => "<code>" + esc(row.id) + "</code>" },
1794
- { label: "Seq", cell: (row) => esc(row.seq) },
1795
- { label: "Value", cell: (row) => preview(row.value) }
1796
- ],
1797
- rows
1798
- )
1799
- }
1800
-
1801
- async function renderClock() {
1802
- const clock = await api("GET", "/clock")
1803
- $("clock-readout").innerHTML = "<div class='k'>Now</div><div class='v'>" + esc(when(clock.now)) +
1804
- "</div><p>" + esc(clock.now) + " ms \xB7 offset " + esc(clock.offsetMs) + " ms " +
1805
- (clock.frozen ? "<span class='badge warn'>frozen</span>" : "<span class='badge ok'>live</span>") + "</p>"
1806
- $("freeze").textContent = clock.frozen ? "Unfreeze" : "Freeze"
1807
- $("freeze").dataset.frozen = clock.frozen ? "1" : "0"
1808
- }
1809
-
1810
- async function renderFaults() {
1811
- const [faults, presets] = await Promise.all([
1812
- api("GET", "/faults"),
1813
- api("GET", "/faults/presets").catch(() => ({ presets: [] }))
1814
- ])
1815
- const select = $("fault-preset")
1816
- const current = select.value
1817
- const list = presets.presets || []
1818
- select.innerHTML = '<option value="">Custom rule</option>' + list.map((preset) =>
1819
- '<option value="' + esc(preset.name) + '">' + esc(preset.name) + "</option>"
1820
- ).join("")
1821
- select.value = current
1822
- const rules = faults.faults || []
1823
- $("fault-list").innerHTML = table(
1824
- [
1825
- { label: "Id", cell: (row) => esc(row.id) },
1826
- { label: "Match", cell: (row) => esc([row.method, row.pathPrefix, row.operationId].filter(Boolean).join(" ") || "all") },
1827
- { label: "Effect", cell: (row) => esc(row.status ?? row.effect ?? (row.drop ? "drop" : row.latencyMs || "")) }
1828
- ],
1829
- rules
1830
- )
1831
- }
1832
-
1833
- async function renderJournal() {
1834
- const journal = await api("GET", "/requests?limit=100")
1835
- $("journal").innerHTML = table(
1836
- [
1837
- { label: "When", cell: (row) => esc(row.at || "") },
1838
- { label: "Method", cell: (row) => esc(row.method) },
1839
- { label: "Path", cell: (row) => esc(row.path) },
1840
- { label: "Status", cell: (row) => esc(row.status) },
1841
- { label: "ms", cell: (row) => esc(row.durationMs) }
1842
- ],
1843
- journal.requests || []
1844
- )
1845
- }
1846
-
1847
- async function renderRoutes() {
1848
- const data = await api("GET", "/")
1849
- const routes = data.routes || []
1850
- $("route-list").innerHTML = routes.map((route) => {
1851
- const extra = STANDARD.has(route) ? "" : " <span class='badge'>extension</span>"
1852
- return '<div class="route"><span class="method">' + esc(route.split(" ")[0]) +
1853
- '</span><span>' + esc(route.split(" ").slice(1).join(" ")) + extra +
1854
- '</span><button class="btn" type="button" data-route="' + esc(route) + '">Use</button></div>'
1855
- }).join("")
1856
- }
1857
-
1858
- const renderers = {
1859
- overview: renderOverview,
1860
- state: renderState,
1861
- clock: renderClock,
1862
- faults: renderFaults,
1863
- journal: renderJournal,
1864
- routes: renderRoutes
1865
- }
1866
-
1867
- async function refresh() {
1868
- clearError()
1869
- if (state.view in renderers) await renderers[state.view]()
1870
- }
1871
-
1872
- $("namespace").addEventListener("change", () => {
1873
- state.namespace = $("namespace").value
1874
- refresh().catch(showError)
1875
- })
1876
- keyInput.addEventListener("change", () => {
1877
- state.key = keyInput.value.trim()
1878
- sessionStorage.setItem("mockingbird-admin-key", state.key)
1879
- refresh().catch(showError)
1880
- })
1881
- $("refresh").addEventListener("click", () => refresh().catch(showError))
1882
-
1883
- $("collections").addEventListener("click", (event) => {
1884
- const button = event.target.closest("[data-collection]")
1885
- if (!button) return
1886
- state.collection = button.dataset.collection
1887
- renderState().catch(showError)
1888
- })
1889
- $("records").addEventListener("click", (event) => {
1890
- const row = event.target.closest("[data-id]")
1891
- if (!row) return
1892
- openEditor(row.dataset.id).catch(showError)
1893
- })
1894
-
1895
- const editor = $("editor")
1896
- async function openEditor(id) {
1897
- state.editing = id || null
1898
- $("editor-title").textContent = id ? "Edit record" : "New record"
1899
- $("editor-id").value = id || ""
1900
- $("editor-id").readOnly = Boolean(id)
1901
- $("editor-delete").hidden = !id
1902
- if (id) {
1903
- const record = await api("GET", "/state/" + encodeURIComponent(state.collection) + "/" + encodeURIComponent(id))
1904
- $("editor-value").value = JSON.stringify(record.value, null, 2)
1905
- } else {
1906
- $("editor-value").value = "{\\n \\n}"
1907
- }
1908
- editor.showModal()
1909
- }
1910
- $("new-record").addEventListener("click", () => openEditor(null).catch(showError))
1911
- $("editor-form").addEventListener("submit", (event) => {
1912
- const submitter = event.submitter
1913
- if (!submitter || submitter.value !== "save") return
1914
- event.preventDefault()
1915
- let value
1916
- try { value = JSON.parse($("editor-value").value) }
1917
- catch { showError(new Error("Value is not JSON")); return }
1918
- const id = $("editor-id").value.trim()
1919
- const path = "/state/" + encodeURIComponent(state.collection)
1920
- const request = state.editing
1921
- ? api("PUT", path + "/" + encodeURIComponent(id), { value })
1922
- : api("POST", path, { id: id || undefined, value })
1923
- request.then(() => { editor.close(); return renderState() }).catch(showError)
1924
- })
1925
- $("editor-delete").addEventListener("click", () => {
1926
- const id = state.editing
1927
- if (!id) return
1928
- api("DELETE", "/state/" + encodeURIComponent(state.collection) + "/" + encodeURIComponent(id))
1929
- .then(() => { editor.close(); return renderState() })
1930
- .catch(showError)
1931
- })
1932
-
1933
- document.querySelectorAll("[data-advance]").forEach((button) => {
1934
- button.addEventListener("click", () => {
1935
- api("POST", "/clock", { advance: Number(button.dataset.advance) }).then(renderClock).catch(showError)
1936
- })
1937
- })
1938
- $("freeze").addEventListener("click", () => {
1939
- const frozen = $("freeze").dataset.frozen === "1"
1940
- api("POST", "/clock", { freeze: !frozen }).then(renderClock).catch(showError)
1941
- })
1942
- $("clock-reset").addEventListener("click", () => {
1943
- api("POST", "/clock", { reset: true }).then(renderClock).catch(showError)
1944
- })
1945
- $("clock-set").addEventListener("submit", (event) => {
1946
- event.preventDefault()
1947
- const raw = $("clock-instant").value.trim()
1948
- const set = /^-?\\d+$/.test(raw) ? Number(raw) : raw
1949
- api("POST", "/clock", { set }).then(renderClock).catch(showError)
1950
- })
1951
-
1952
- $("fault-form").addEventListener("submit", (event) => {
1953
- event.preventDefault()
1954
- const preset = $("fault-preset").value
1955
- const body = preset
1956
- ? { preset }
1957
- : {
1958
- status: Number($("fault-status").value),
1959
- ...($("fault-path").value ? { pathPrefix: $("fault-path").value } : {}),
1960
- ...($("fault-method").value ? { method: $("fault-method").value } : {})
1961
- }
1962
- if (!preset && !Number.isFinite(body.status)) {
1963
- showError(new Error("Status must be a number, or choose a preset"))
1964
- return
1965
- }
1966
- api("POST", "/faults", body).then(renderFaults).catch(showError)
1967
- })
1968
- $("fault-clear").addEventListener("click", () => api("DELETE", "/faults").then(renderFaults).catch(showError))
1969
- $("journal-clear").addEventListener("click", () => api("DELETE", "/requests").then(renderJournal).catch(showError))
1970
-
1971
- $("route-list").addEventListener("click", (event) => {
1972
- const button = event.target.closest("[data-route]")
1973
- if (!button) return
1974
- state.route = button.dataset.route
1975
- $("route-title").textContent = state.route
1976
- const method = state.route.split(" ")[0]
1977
- $("route-body").disabled = method === "GET" || method === "DELETE"
1978
- })
1979
- $("route-form").addEventListener("submit", async (event) => {
1980
- event.preventDefault()
1981
- if (!state.route) return showError(new Error("Choose a route first"))
1982
- const [method, path] = [state.route.split(" ")[0], state.route.split(" ").slice(1).join(" ")]
1983
- const concrete = path.replace(/:([A-Za-z0-9_]+)/g, (_, key) => prompt("Value for " + key) || "")
1984
- let body
1985
- if (method !== "GET" && method !== "DELETE" && $("route-body").value.trim()) {
1986
- try { body = JSON.parse($("route-body").value) }
1987
- catch { return showError(new Error("Body is not JSON")) }
1988
- }
1989
- try {
1990
- const data = await api(method, concrete, body)
1991
- $("route-result").textContent = JSON.stringify(data, null, 2)
1992
- } catch (error) {
1993
- showError(error)
1994
- }
1995
- })
1996
-
1997
- function mountPanels(panels) {
1998
- const nav = $("nav")
1999
- const views = $("views")
2000
- for (const panel of panels) {
2001
- const button = document.createElement("button")
2002
- button.type = "button"
2003
- button.dataset.view = panel.id
2004
- button.textContent = panel.title
2005
- nav.append(button)
2006
- const section = document.createElement("section")
2007
- section.className = "view"
2008
- section.id = "view-" + panel.id
2009
- section.hidden = true
2010
- const title = document.createElement("h2")
2011
- title.textContent = panel.title
2012
- const lede = document.createElement("p")
2013
- lede.className = "lede"
2014
- lede.textContent = panel.description || ""
2015
- const body = document.createElement("div")
2016
- body.className = "panel-body"
2017
- body.id = "panel-" + panel.id
2018
- body.innerHTML = panel.html || ""
2019
- section.append(title, lede, body)
2020
- views.append(section)
2021
- if (panel.script) {
2022
- try {
2023
- const run = new Function("root", "api", panel.script)
2024
- run(body, panelApi)
2025
- } catch (error) {
2026
- const note = document.createElement("p")
2027
- note.className = "banner show"
2028
- note.textContent = error instanceof Error ? error.message : String(error)
2029
- body.append(note)
2030
- }
2031
- }
2032
- }
2033
- }
2034
-
2035
- loadNamespaces()
2036
- .then(() => api("GET", "/ui/manifest"))
2037
- .then((manifest) => { mountPanels((manifest && manifest.panels) || []); return refresh() })
2038
- .catch(showError)
2039
- })()
2040
- </script>
2841
+ ${script}
2041
2842
  </body>
2042
2843
  </html>`;
2043
2844
  };
@@ -2054,10 +2855,10 @@ var json2 = (body) => new Response(JSON.stringify(body), {
2054
2855
  });
2055
2856
  var adminUiRoutes = (service, ui) => {
2056
2857
  const shell = () => renderAdminDocument(service);
2057
- const document2 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
2858
+ const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
2058
2859
  return {
2059
- "GET /ui": () => htmlResponse(document2()),
2060
- "GET /ui/": () => htmlResponse(document2()),
2860
+ "GET /ui": () => htmlResponse(document3()),
2861
+ "GET /ui/": () => htmlResponse(document3()),
2061
2862
  "GET /ui/manifest": () => json2({
2062
2863
  service,
2063
2864
  panels: (ui?.panels ?? []).map((panel) => ({
@@ -2083,11 +2884,11 @@ var EXTENSION_KEYS = {
2083
2884
  };
2084
2885
 
2085
2886
  // ../../openapi/metadata/dist/read.js
2086
- var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2887
+ var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2087
2888
  var extensionOf = (holder, key) => holder[key];
2088
2889
  var operationMetadata = (operation) => {
2089
2890
  const raw = extensionOf(operation, EXTENSION_KEYS.operation);
2090
- const ext = isRecord2(raw) ? raw : {};
2891
+ const ext = isRecord3(raw) ? raw : {};
2091
2892
  const supported = ext.supported ?? true;
2092
2893
  const parity = ext.parity ?? {};
2093
2894
  return {
@@ -2123,9 +2924,9 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
2123
2924
  this.name = "OperationRegistryError";
2124
2925
  }
2125
2926
  };
2126
- var verifyOperations = (document2, handlers) => {
2927
+ var verifyOperations = (document3, handlers) => {
2127
2928
  const problems = [];
2128
- const operations = listOperations(document2);
2929
+ const operations = listOperations(document3);
2129
2930
  const seen = /* @__PURE__ */ new Set();
2130
2931
  for (const operation of operations) {
2131
2932
  if (seen.has(operation.operationId))
@@ -2241,7 +3042,7 @@ var SAMPLE = 25;
2241
3042
  var WALK_LIMIT = 1e4;
2242
3043
  var SKIP_CTORS = /* @__PURE__ */ new Set(["Hono", "HonoRequest", "Router"]);
2243
3044
  var fail2 = (status, message) => ({ ok: false, status, message });
2244
- var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3045
+ var isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2245
3046
  var kindOf = (value) => {
2246
3047
  if (value === null)
2247
3048
  return "null";
@@ -2340,7 +3141,7 @@ var absorb = (accum, value) => {
2340
3141
  const kind = kindOf(value);
2341
3142
  accum.kinds.add(kind);
2342
3143
  accum.present++;
2343
- if (!isRecord3(value))
3144
+ if (!isRecord4(value))
2344
3145
  return;
2345
3146
  for (const [key, child] of Object.entries(value)) {
2346
3147
  const nested = accum.nested.get(key) ?? blankAccum(key);
@@ -2365,7 +3166,7 @@ var inferFields = (records) => {
2365
3166
  if (records.length === 0)
2366
3167
  return [];
2367
3168
  const sample = records.slice(0, SAMPLE);
2368
- const objects = sample.filter((record) => isRecord3(record.value));
3169
+ const objects = sample.filter((record) => isRecord4(record.value));
2369
3170
  if (objects.length === 0) {
2370
3171
  const kinds = new Set(sample.map((record) => kindOf(record.value)));
2371
3172
  const list = [...kinds];
@@ -2552,7 +3353,7 @@ var patchStateRecord = (scope, name, id, value) => {
2552
3353
  const parsed = jsonValue(value);
2553
3354
  if (!parsed.ok)
2554
3355
  return parsed;
2555
- const next = isRecord3(current.value.value) && isRecord3(parsed.value) ? { ...current.value.value, ...parsed.value } : parsed.value;
3356
+ const next = isRecord4(current.value.value) && isRecord4(parsed.value) ? { ...current.value.value, ...parsed.value } : parsed.value;
2556
3357
  return replaceStateRecord(scope, name, id, next);
2557
3358
  };
2558
3359
  var deleteStateRecord = (scope, name, id) => {
@@ -2620,7 +3421,7 @@ var stateAdminRoutes = (routes) => {
2620
3421
  const scope = open(namespace);
2621
3422
  if (!scope.ok)
2622
3423
  return fromResult(scope, 201);
2623
- if (!isRecord3(body) || !("value" in body)) {
3424
+ if (!isRecord4(body) || !("value" in body)) {
2624
3425
  return adminError2(400, 'expected {"id?": "<id>", "value": <json>}');
2625
3426
  }
2626
3427
  const id = body.id;
@@ -2638,7 +3439,7 @@ var stateAdminRoutes = (routes) => {
2638
3439
  const scope = open(namespace);
2639
3440
  if (!scope.ok)
2640
3441
  return fromResult(scope, 200);
2641
- if (!isRecord3(body) || !("value" in body))
3442
+ if (!isRecord4(body) || !("value" in body))
2642
3443
  return adminError2(400, 'expected {"value": <json>}');
2643
3444
  const replaced = replaceStateRecord(scope.value, params.collection ?? "", params.id ?? "", body.value);
2644
3445
  if (replaced.ok)
@@ -2649,7 +3450,7 @@ var stateAdminRoutes = (routes) => {
2649
3450
  const scope = open(namespace);
2650
3451
  if (!scope.ok)
2651
3452
  return fromResult(scope, 200);
2652
- if (!isRecord3(body) || !("value" in body))
3453
+ if (!isRecord4(body) || !("value" in body))
2653
3454
  return adminError2(400, 'expected {"value": <json>}');
2654
3455
  const patched = patchStateRecord(scope.value, params.collection ?? "", params.id ?? "", body.value);
2655
3456
  if (patched.ok)
@@ -2669,14 +3470,14 @@ var stateAdminRoutes = (routes) => {
2669
3470
  };
2670
3471
 
2671
3472
  // ../core/dist/version.js
2672
- var PACKAGE_VERSION = true ? "1.4.0" : UNRELEASED_VERSION;
3473
+ var PACKAGE_VERSION = true ? "1.5.0" : UNRELEASED_VERSION;
2673
3474
 
2674
3475
  // ../core/dist/webhooks.js
2675
3476
  var json4 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
2676
3477
  var adminError3 = (status, message) => json4(status, { error: { type: "mockingbird_admin", message } });
2677
- var isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3478
+ var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2678
3479
  var parseEndpoint = (value) => {
2679
- if (!isRecord4(value) || typeof value.url !== "string")
3480
+ if (!isRecord5(value) || typeof value.url !== "string")
2680
3481
  return "each endpoint needs a url";
2681
3482
  try {
2682
3483
  new URL(value.url);
@@ -2693,12 +3494,12 @@ var parseEndpoint = (value) => {
2693
3494
  const events = value.events ?? value.enabledEvents;
2694
3495
  if (Array.isArray(events))
2695
3496
  endpoint.events = events.map(String);
2696
- if (isRecord4(value.tags)) {
3497
+ if (isRecord5(value.tags)) {
2697
3498
  endpoint.tags = Object.fromEntries(Object.entries(value.tags).map(([k, v]) => [k, String(v)]));
2698
3499
  }
2699
3500
  if (typeof value.account === "string")
2700
3501
  endpoint.tags = { ...endpoint.tags, account: value.account };
2701
- if (isRecord4(value.headers)) {
3502
+ if (isRecord5(value.headers)) {
2702
3503
  endpoint.headers = Object.fromEntries(Object.entries(value.headers).map(([k, v]) => [k, String(v)]));
2703
3504
  }
2704
3505
  return endpoint;
@@ -2725,7 +3526,7 @@ var webhookAdminRoutes = (hub) => ({
2725
3526
  return json4(200, { status: "ok" });
2726
3527
  },
2727
3528
  "POST /webhooks/faults": ({ body, namespace }) => {
2728
- if (!isRecord4(body) || !["duplicate", "reorder", "drop"].includes(String(body.mode))) {
3529
+ if (!isRecord5(body) || !["duplicate", "reorder", "drop"].includes(String(body.mode))) {
2729
3530
  return adminError3(400, "mode must be duplicate, reorder or drop");
2730
3531
  }
2731
3532
  const fault = { mode: body.mode };
@@ -2741,7 +3542,7 @@ var webhookAdminRoutes = (hub) => ({
2741
3542
  }))
2742
3543
  }),
2743
3544
  "PUT /webhook-endpoints": ({ body, namespace }) => {
2744
- const list = Array.isArray(body) ? body : isRecord4(body) ? body.endpoints : void 0;
3545
+ const list = Array.isArray(body) ? body : isRecord5(body) ? body.endpoints : void 0;
2745
3546
  if (!Array.isArray(list))
2746
3547
  return adminError3(400, "expected [{url, secret?, events?}]");
2747
3548
  const parsed = [];
@@ -2811,8 +3612,8 @@ var DroppedConnectionError = class extends TypeError {
2811
3612
  this.name = "TypeError";
2812
3613
  }
2813
3614
  };
2814
- var operationMatcher = (document2) => {
2815
- const matchers = listOperations(document2).map((operation) => ({
3615
+ var operationMatcher = (document3) => {
3616
+ const matchers = listOperations(document3).map((operation) => ({
2816
3617
  operationId: operation.operationId,
2817
3618
  method: operation.method.toUpperCase(),
2818
3619
  pattern: new RegExp(`^${operation.path.split("/").map((segment) => segment.startsWith("{") ? "[^/]+" : segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("/")}/?$`),
@@ -2907,10 +3708,10 @@ var createRuntime = (options) => {
2907
3708
  const ensureBranch = (namespace, branch2, at) => {
2908
3709
  if (!BRANCH_PATTERN2.test(branch2))
2909
3710
  throw new RangeError(`branch must match ${BRANCH_PATTERN2}`);
2910
- const history = timeline(namespace);
3711
+ const history2 = timeline(namespace);
2911
3712
  if (branch2 === "main") {
2912
3713
  if (at !== void 0) {
2913
- const point = history.checkout("main", at);
3714
+ const point = history2.checkout("main", at);
2914
3715
  restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
2915
3716
  captured.set(namespace, point.value.snapshot);
2916
3717
  rng.setState(point.value.rngState);
@@ -2923,10 +3724,10 @@ var createRuntime = (options) => {
2923
3724
  return namespace;
2924
3725
  }
2925
3726
  const storage = physicalBranch(namespace, branch2);
2926
- if (!history.hasBranch(branch2)) {
3727
+ if (!history2.hasBranch(branch2)) {
2927
3728
  if (at === void 0)
2928
- history.commit(capture(namespace));
2929
- const point = history.fork(branch2, at === void 0 ? {} : { from: at });
3729
+ history2.commit(capture(namespace));
3730
+ const point = history2.fork(branch2, at === void 0 ? {} : { from: at });
2930
3731
  const branchRng = createRng(options.seed ?? 0);
2931
3732
  if (point)
2932
3733
  branchRng.setState(point.value.rngState);
@@ -2935,8 +3736,8 @@ var createRuntime = (options) => {
2935
3736
  restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
2936
3737
  if (point)
2937
3738
  captured.set(storage, point.value.snapshot);
2938
- } else if (at !== void 0 && history.head(branch2)?.id !== at) {
2939
- const point = history.checkout(branch2, at);
3739
+ } else if (at !== void 0 && history2.head(branch2)?.id !== at) {
3740
+ const point = history2.checkout(branch2, at);
2940
3741
  if (!instances.has(storage)) {
2941
3742
  const branchRng = createRng(options.seed ?? 0);
2942
3743
  branchRng.setState(point.value.rngState);
@@ -2947,7 +3748,7 @@ var createRuntime = (options) => {
2947
3748
  captured.set(storage, point.value.snapshot);
2948
3749
  } else {
2949
3750
  if (!instances.has(storage)) {
2950
- const point = history.head(branch2);
3751
+ const point = history2.head(branch2);
2951
3752
  const branchRng = createRng(options.seed ?? 0);
2952
3753
  if (point)
2953
3754
  branchRng.setState(point.value.rngState);
@@ -2971,8 +3772,8 @@ var createRuntime = (options) => {
2971
3772
  const checkout = (checkpointId, checkoutOptions = {}) => {
2972
3773
  const namespace = checkoutOptions.namespace ?? DEFAULT_NAMESPACE;
2973
3774
  const branchName = checkoutOptions.branch ?? "main";
2974
- const history = timeline(namespace);
2975
- const point = history.checkout(branchName, checkpointId);
3775
+ const history2 = timeline(namespace);
3776
+ const point = history2.checkout(branchName, checkpointId);
2976
3777
  const storage = ensureBranch(namespace, branchName);
2977
3778
  restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
2978
3779
  captured.set(storage, point.value.snapshot);
@@ -3022,9 +3823,9 @@ var createRuntime = (options) => {
3022
3823
  instance(name);
3023
3824
  restoreNamespace(sqlite, storageNamespace(name), from);
3024
3825
  captured.set(name, from);
3025
- const history = timelines.get(name);
3026
- if (history)
3027
- history.commit(capture(name), { branch: "main" });
3826
+ const history2 = timelines.get(name);
3827
+ if (history2)
3828
+ history2.commit(capture(name), { branch: "main" });
3028
3829
  else
3029
3830
  timeline(name);
3030
3831
  };
@@ -3253,10 +4054,10 @@ var createRuntime = (options) => {
3253
4054
  },
3254
4055
  release: (name, checkpointId) => timeline(name).release(checkpointId),
3255
4056
  inspect: (name) => {
3256
- const history = timeline(name);
4057
+ const history2 = timeline(name);
3257
4058
  return {
3258
- branches: history.branches(),
3259
- checkpoints: history.checkpoints().map(({ id, branch: branchName, parent, at }) => ({
4059
+ branches: history2.branches(),
4060
+ checkpoints: history2.checkpoints().map(({ id, branch: branchName, parent, at }) => ({
3260
4061
  id,
3261
4062
  branch: branchName,
3262
4063
  parent,
@@ -3271,11 +4072,11 @@ var createRuntime = (options) => {
3271
4072
  } : {},
3272
4073
  routes: {
3273
4074
  ...credentialRoutes(credentials),
3274
- ...options.presets ? presetRoutes(options.presets, runtime) : {},
3275
4075
  ...options.webhooks ? webhookAdminRoutes(options.webhooks) : {},
3276
4076
  ...options.admin?.(runtime) ?? {},
3277
- // Standard state and UI routes win a colliding key. A service adds its own keys
3278
- // beside these; it does not replace `/state` or `/ui`.
4077
+ // Shell routes win a colliding key. Fault presets are one of them: the admin page
4078
+ // maps `presets`, and a service route must not replace that list with a record.
4079
+ ...options.presets ? presetRoutes(options.presets, runtime) : {},
3279
4080
  ...stateAdminRoutes({
3280
4081
  open: stateScope,
3281
4082
  afterWrite: (namespace) => {
@@ -3350,9 +4151,12 @@ var credentialRoutes = (registry) => ({
3350
4151
  }
3351
4152
  });
3352
4153
  var presetRoutes = (presets, runtime) => ({
3353
- "GET /faults/presets": () => adminJson(200, {
3354
- presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
3355
- }),
4154
+ "GET /faults/presets": () => {
4155
+ const body = {
4156
+ presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
4157
+ };
4158
+ return adminJson(200, body);
4159
+ },
3356
4160
  "POST /faults/presets/:name": ({ params, body, namespace }) => {
3357
4161
  const name = params.name;
3358
4162
  if (!presets[name])
@@ -3363,7 +4167,7 @@ var presetRoutes = (presets, runtime) => ({
3363
4167
  });
3364
4168
 
3365
4169
  // src/generated/openapi.ts
3366
- var document = JSON.parse(`{"openapi":"3.1.0","info":{"title":"Payload CMS REST API (Mockingbird subset)","description":"Stateful mock subset of Payload CMS's auto-generated collection REST API: find documents\\n(with a \`where\` query subset, \`limit\`, \`page\`, \`sort\`) and find one by id. Hand-authored\\nfrom Payload's REST API docs and the consumer's response type (payload-cms-response.type.ts).\\n","version":"2","x-mockingbird-upstream":{"note":"Shapes from https://payloadcms.com/docs/rest-api/overview and https://payloadcms.com/docs/queries/overview; the consumer is apps/backend/src/modules/cms/payload-cms/payload-cms.service.ts."}},"servers":[{"url":"https://payload.acme.example"}],"paths":{"/api/{collection}":{"parameters":[{"name":"collection","in":"path","required":true,"schema":{"type":"string","enum":["marketing"]}}],"get":{"operationId":"FindDocuments","description":"Paginated find. \`where[<field>][<operator>]=<value>\` supports equals, not_equals, in, not_in, exists, greater_than(_equal), less_than(_equal), like, contains, and nested \`where[and|or][<i>]\u2026\`. Only the consumer's filters are declared here for parity walks.","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"name":"where[type][equals]","in":"query","required":false,"schema":{"type":"string","enum":["referral","banner","email"]}},{"name":"where[isActive][equals]","in":"query","required":false,"schema":{"type":"string","enum":["true","false"]}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":0,"maximum":100}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":20}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","enum":["createdAt","-createdAt","name","-name","id","-id"]}}],"responses":{"200":{"description":"A page of documents","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedDocs"}}}},"400":{"description":"Invalid query","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Errors"}}}},"404":{"description":"Unknown collection","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Errors"}}}}}}},"/api/{collection}/{id}":{"parameters":[{"name":"collection","in":"path","required":true,"schema":{"type":"string","enum":["marketing"]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","description":"The seeded documents are 1-3; anything else is a 404.","pattern":"^[1-5]$"}}],"get":{"operationId":"FindDocumentById","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"The document","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketingDoc"}}}},"404":{"description":"Unknown document or collection","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Errors"}}}}}}}},"components":{"schemas":{"MarketingDoc":{"type":"object","required":["id","name","type","createdAt","updatedAt"],"properties":{"id":{"type":"integer","description":"Integer ids, as Payload's Postgres adapter issues them (the consumer types \`id\` as a number)."},"name":{"type":"string"},"type":{"type":"string","enum":["referral","banner","email"]},"isActive":{"type":"boolean"},"cardTitle":{"type":["string","null"]},"cardSubtitle":{"type":["string","null"]},"cardDescription":{"type":["string","null"]},"ctaTitle":{"type":["string","null"]},"ctaActionText":{"type":["string","null"]},"shareMessage":{"type":["string","null"]},"createdAt":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"updatedAt":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}},"PaginatedDocs":{"type":"object","required":["docs","totalDocs","limit","totalPages","page","pagingCounter","hasPrevPage","hasNextPage","prevPage","nextPage"],"properties":{"docs":{"type":"array","items":{"$ref":"#/components/schemas/MarketingDoc"}},"totalDocs":{"type":"integer"},"limit":{"type":"integer"},"totalPages":{"type":"integer"},"page":{"type":"integer"},"pagingCounter":{"type":"integer"},"hasPrevPage":{"type":"boolean"},"hasNextPage":{"type":"boolean"},"prevPage":{"type":["integer","null"]},"nextPage":{"type":["integer","null"]}}},"Errors":{"type":"object","required":["errors"],"properties":{"errors":{"type":"array","items":{"type":"object","required":["message"],"properties":{"message":{"type":"string"}}}}}}}}}`);
4170
+ var document2 = JSON.parse(`{"openapi":"3.1.0","info":{"title":"Payload CMS REST API (Mockingbird subset)","description":"Stateful mock subset of Payload CMS's auto-generated collection REST API: find documents\\n(with a \`where\` query subset, \`limit\`, \`page\`, \`sort\`) and find one by id. Hand-authored\\nfrom Payload's REST API docs and the consumer's response type (payload-cms-response.type.ts).\\n","version":"2","x-mockingbird-upstream":{"note":"Shapes from https://payloadcms.com/docs/rest-api/overview and https://payloadcms.com/docs/queries/overview; the consumer is apps/backend/src/modules/cms/payload-cms/payload-cms.service.ts."}},"servers":[{"url":"https://payload.acme.example"}],"paths":{"/api/{collection}":{"parameters":[{"name":"collection","in":"path","required":true,"schema":{"type":"string","enum":["marketing"]}}],"get":{"operationId":"FindDocuments","description":"Paginated find. \`where[<field>][<operator>]=<value>\` supports equals, not_equals, in, not_in, exists, greater_than(_equal), less_than(_equal), like, contains, and nested \`where[and|or][<i>]\u2026\`. Only the consumer's filters are declared here for parity walks.","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"name":"where[type][equals]","in":"query","required":false,"schema":{"type":"string","enum":["referral","banner","email"]}},{"name":"where[isActive][equals]","in":"query","required":false,"schema":{"type":"string","enum":["true","false"]}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":0,"maximum":100}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":20}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","enum":["createdAt","-createdAt","name","-name","id","-id"]}}],"responses":{"200":{"description":"A page of documents","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaginatedDocs"}}}},"400":{"description":"Invalid query","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Errors"}}}},"404":{"description":"Unknown collection","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Errors"}}}}}}},"/api/{collection}/{id}":{"parameters":[{"name":"collection","in":"path","required":true,"schema":{"type":"string","enum":["marketing"]}},{"name":"id","in":"path","required":true,"schema":{"type":"string","description":"The seeded documents are 1-3; anything else is a 404.","pattern":"^[1-5]$"}}],"get":{"operationId":"FindDocumentById","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"The document","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketingDoc"}}}},"404":{"description":"Unknown document or collection","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Errors"}}}}}}}},"components":{"schemas":{"MarketingDoc":{"type":"object","required":["id","name","type","createdAt","updatedAt"],"properties":{"id":{"type":"integer","description":"Integer ids, as Payload's Postgres adapter issues them (the consumer types \`id\` as a number)."},"name":{"type":"string"},"type":{"type":"string","enum":["referral","banner","email"]},"isActive":{"type":"boolean"},"cardTitle":{"type":["string","null"]},"cardSubtitle":{"type":["string","null"]},"cardDescription":{"type":["string","null"]},"ctaTitle":{"type":["string","null"]},"ctaActionText":{"type":["string","null"]},"shareMessage":{"type":["string","null"]},"createdAt":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"updatedAt":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}},"PaginatedDocs":{"type":"object","required":["docs","totalDocs","limit","totalPages","page","pagingCounter","hasPrevPage","hasNextPage","prevPage","nextPage"],"properties":{"docs":{"type":"array","items":{"$ref":"#/components/schemas/MarketingDoc"}},"totalDocs":{"type":"integer"},"limit":{"type":"integer"},"totalPages":{"type":"integer"},"page":{"type":"integer"},"pagingCounter":{"type":"integer"},"hasPrevPage":{"type":"boolean"},"hasNextPage":{"type":"boolean"},"prevPage":{"type":["integer","null"]},"nextPage":{"type":["integer","null"]}}},"Errors":{"type":"object","required":["errors"],"properties":{"errors":{"type":"array","items":{"type":"object","required":["message"],"properties":{"message":{"type":"string"}}}}}}}}}`);
3367
4171
  var operationIds = ["FindDocuments", "FindDocumentById"];
3368
4172
  var supportedOperationIds = ["FindDocuments", "FindDocumentById"];
3369
4173
 
@@ -3524,9 +4328,9 @@ var PAYLOAD_CMS_PRESETS = {
3524
4328
  };
3525
4329
  var json5 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
3526
4330
  var adminError4 = (status, message) => json5(status, { error: { type: "mockingbird_admin", message } });
3527
- var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4331
+ var isRecord6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3528
4332
  var toDocs = (value, now) => {
3529
- if (!Array.isArray(value) || !value.every(isRecord5)) return "expected {docs: [{\u2026}]}";
4333
+ if (!Array.isArray(value) || !value.every(isRecord6)) return "expected {docs: [{\u2026}]}";
3530
4334
  return value.map((doc, index) => ({
3531
4335
  createdAt: now,
3532
4336
  updatedAt: now,
@@ -3542,18 +4346,18 @@ var adminRoutes = (runtime) => ({
3542
4346
  },
3543
4347
  "PUT /collections/:slug": ({ params, body, namespace }) => {
3544
4348
  const docs = toDocs(
3545
- isRecord5(body) ? body.docs : void 0,
4349
+ isRecord6(body) ? body.docs : void 0,
3546
4350
  new Date(runtime.clock.now()).toISOString()
3547
4351
  );
3548
4352
  if (typeof docs === "string") return adminError4(400, docs);
3549
4353
  return json5(200, runtime.instance(namespace).state.replace(params.slug, docs));
3550
4354
  },
3551
4355
  "POST /collections/:slug/docs": ({ params, body, namespace }) => {
3552
- if (!isRecord5(body)) return adminError4(400, "expected a document object");
4356
+ if (!isRecord6(body)) return adminError4(400, "expected a document object");
3553
4357
  return json5(201, runtime.instance(namespace).addDoc(params.slug, body));
3554
4358
  },
3555
4359
  "PATCH /collections/:slug/docs/:id": ({ params, body, namespace }) => {
3556
- if (!isRecord5(body)) return adminError4(400, "expected a partial document object");
4360
+ if (!isRecord6(body)) return adminError4(400, "expected a partial document object");
3557
4361
  const doc = runtime.instance(namespace).updateDoc(params.slug, Number(params.id), body);
3558
4362
  return doc ? json5(200, doc) : adminError4(404, `no document ${params.slug}/${params.id}`);
3559
4363
  },
@@ -3561,7 +4365,7 @@ var adminRoutes = (runtime) => ({
3561
4365
  });
3562
4366
  var createRuntime2 = (options = {}) => createRuntime({
3563
4367
  name: PAYLOAD_CMS_NAMESPACE,
3564
- document,
4368
+ document: document2,
3565
4369
  ...options.sqlite ? { sqlite: options.sqlite } : {},
3566
4370
  ...options.clock ? { clock: options.clock } : {},
3567
4371
  ...options.seed !== void 0 ? { seed: options.seed } : {},
@@ -3600,8 +4404,8 @@ var OPERATORS = [
3600
4404
  "like",
3601
4405
  "contains"
3602
4406
  ];
3603
- var isRecord6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3604
- var fieldValue = (doc, path) => path.split(".").reduce((value, key) => isRecord6(value) ? value[key] : void 0, doc);
4407
+ var isRecord7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4408
+ var fieldValue = (doc, path) => path.split(".").reduce((value, key) => isRecord7(value) ? value[key] : void 0, doc);
3605
4409
  var cast = (raw, sample) => {
3606
4410
  if (typeof raw !== "string") return raw;
3607
4411
  if (typeof sample === "boolean") return raw === "true" ? true : raw === "false" ? false : raw;
@@ -3647,11 +4451,11 @@ var test = (operator, actual, raw) => {
3647
4451
  };
3648
4452
  var compileWhere = (where, fields) => {
3649
4453
  if (where === void 0) return { ok: true, match: () => true };
3650
- if (!isRecord6(where)) return { ok: false, message: "The where query is malformed." };
4454
+ if (!isRecord7(where)) return { ok: false, message: "The where query is malformed." };
3651
4455
  const parts = [];
3652
4456
  for (const [key, condition] of Object.entries(where)) {
3653
4457
  if (key === "and" || key === "or") {
3654
- const nested = (Array.isArray(condition) ? condition : isRecord6(condition) ? Object.values(condition) : []).map((item) => compileWhere(item, fields));
4458
+ const nested = (Array.isArray(condition) ? condition : isRecord7(condition) ? Object.values(condition) : []).map((item) => compileWhere(item, fields));
3655
4459
  const failed = nested.find((c) => !c.ok);
3656
4460
  if (failed) return failed;
3657
4461
  const matchers = nested.map((c) => c.match);
@@ -3664,7 +4468,7 @@ var compileWhere = (where, fields) => {
3664
4468
  if (!fields.has(root)) {
3665
4469
  return { ok: false, message: `The following path cannot be queried: ${key}` };
3666
4470
  }
3667
- if (!isRecord6(condition))
4471
+ if (!isRecord7(condition))
3668
4472
  return { ok: false, message: `The following path cannot be queried: ${key}` };
3669
4473
  for (const [operator, value] of Object.entries(condition)) {
3670
4474
  if (!OPERATORS.includes(operator)) {
@@ -3711,7 +4515,7 @@ var PayloadCmsAPI = class {
3711
4515
  }
3712
4516
  });
3713
4517
  this.service = createService({
3714
- document,
4518
+ document: document2,
3715
4519
  handlers,
3716
4520
  sqlite,
3717
4521
  namespace,
@@ -3820,7 +4624,7 @@ var PayloadCmsAPI = class {
3820
4624
  };
3821
4625
 
3822
4626
  export {
3823
- document,
4627
+ document2 as document,
3824
4628
  operationIds,
3825
4629
  supportedOperationIds,
3826
4630
  MARKETING_FIELDS,
@@ -3833,4 +4637,4 @@ export {
3833
4637
  compileWhere,
3834
4638
  PayloadCmsAPI
3835
4639
  };
3836
- //# sourceMappingURL=chunk-2ZHNDZ7B.js.map
4640
+ //# sourceMappingURL=chunk-7HS4CTPK.js.map