@crvouga/mockingbird-service-junction 1.7.1 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog — @crvouga/mockingbird-service-junction
2
2
 
3
+ ## 1.8.1 (2026-10-01)
4
+
5
+ ### Fixes and improvements
6
+
7
+ - freeze clock before setting time ([a054a1d](https://github.com/crvouga/mockingbird/commit/a054a1defad62539d719b1d31c15107afea623bd))
8
+
9
+ ## 1.8.0 (2026-09-30)
10
+
11
+ ### Features
12
+
13
+ - record SQL timelines and serve the admin explorer ([8ced17a](https://github.com/crvouga/mockingbird/commit/8ced17ac398e386ef4e9d7e01fc7744c34b95d07))
14
+
3
15
  ## 1.7.1 (2026-09-30)
4
16
 
5
17
  ### Fixes and improvements
@@ -193,6 +193,8 @@ var createControlPlane = (context) => {
193
193
  return adminError(400, "expected a JSON object");
194
194
  if (body.reset === true)
195
195
  context.clock.reset();
196
+ if (body.freeze === true)
197
+ context.clock.freeze();
196
198
  if (body.set !== void 0) {
197
199
  const instant2 = parseInstant(body.set);
198
200
  if (instant2 === void 0)
@@ -205,8 +207,6 @@ var createControlPlane = (context) => {
205
207
  return adminError(400, 'advance: expected ms or "15m"-style');
206
208
  context.clock.advance(delta);
207
209
  }
208
- if (body.freeze === true)
209
- context.clock.freeze();
210
210
  if (body.freeze === false)
211
211
  context.clock.unfreeze();
212
212
  return json(200, context.clock.state());
@@ -1671,6 +1671,167 @@ function faultPresetList(value) {
1671
1671
  })
1672
1672
  };
1673
1673
  }
1674
+ function mountSqlExplorer(root, api) {
1675
+ const cell = (value) => {
1676
+ if (value === null || value === void 0)
1677
+ return "";
1678
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
1679
+ return String(value);
1680
+ }
1681
+ return JSON.stringify(value) ?? "";
1682
+ };
1683
+ root.replaceChildren();
1684
+ const layout = document.createElement("div");
1685
+ layout.className = "sql-layout";
1686
+ const tables = document.createElement("div");
1687
+ tables.className = "sql-tables";
1688
+ const stack = document.createElement("div");
1689
+ stack.className = "stack";
1690
+ const banner = document.createElement("p");
1691
+ banner.className = "banner";
1692
+ const grid = document.createElement("div");
1693
+ const input = document.createElement("textarea");
1694
+ input.className = "sql-input";
1695
+ input.spellcheck = false;
1696
+ input.placeholder = "SELECT * FROM ...";
1697
+ const actions = document.createElement("div");
1698
+ actions.className = "row-actions";
1699
+ const run = document.createElement("button");
1700
+ run.type = "button";
1701
+ run.textContent = "Run";
1702
+ actions.append(run);
1703
+ const result = document.createElement("pre");
1704
+ stack.append(banner, grid, input, actions, result);
1705
+ layout.append(tables, stack);
1706
+ root.append(layout);
1707
+ const showError = (error) => {
1708
+ banner.textContent = error instanceof Error ? error.message : String(error);
1709
+ banner.classList.add("show");
1710
+ };
1711
+ const clearError = () => {
1712
+ banner.textContent = "";
1713
+ banner.classList.remove("show");
1714
+ };
1715
+ const renderGrid = (columns, rows) => {
1716
+ grid.replaceChildren();
1717
+ if (columns.length === 0) {
1718
+ const empty = document.createElement("p");
1719
+ empty.className = "empty";
1720
+ empty.textContent = "No rows.";
1721
+ grid.append(empty);
1722
+ return;
1723
+ }
1724
+ const table = document.createElement("table");
1725
+ const head = document.createElement("thead");
1726
+ const headRow = document.createElement("tr");
1727
+ for (const column of columns) {
1728
+ const th = document.createElement("th");
1729
+ th.textContent = column;
1730
+ headRow.append(th);
1731
+ }
1732
+ head.append(headRow);
1733
+ const body = document.createElement("tbody");
1734
+ for (const row of rows) {
1735
+ const tr = document.createElement("tr");
1736
+ for (const column of columns) {
1737
+ const td = document.createElement("td");
1738
+ td.dataset.label = column;
1739
+ td.textContent = cell(row[column]);
1740
+ tr.append(td);
1741
+ }
1742
+ body.append(tr);
1743
+ }
1744
+ table.append(head, body);
1745
+ grid.append(table);
1746
+ };
1747
+ const rowsOf = (value) => {
1748
+ if (!isRecord2(value) || !Array.isArray(value.rows))
1749
+ return [];
1750
+ return value.rows.flatMap((row) => isRecord2(row) ? [row] : []);
1751
+ };
1752
+ const columnsOf = (value, rows) => {
1753
+ if (isRecord2(value) && Array.isArray(value.columns)) {
1754
+ const named = value.columns.flatMap((column) => typeof column === "string" ? [column] : []);
1755
+ if (named.length > 0)
1756
+ return named;
1757
+ }
1758
+ const first = rows[0];
1759
+ return first === void 0 ? [] : Object.keys(first);
1760
+ };
1761
+ let selected = "";
1762
+ const loadPage = async (schema, name) => {
1763
+ selected = `${schema}.${name}`;
1764
+ const page = await api.get(`/sql/tables/${encodeURIComponent(schema)}/${encodeURIComponent(name)}?limit=50`);
1765
+ const rows = rowsOf(page);
1766
+ renderGrid(columnsOf(page, rows), rows);
1767
+ const total = isRecord2(page) && typeof page.total === "number" ? page.total : rows.length;
1768
+ result.textContent = `${selected} \xB7 ${total} rows`;
1769
+ for (const button of tables.querySelectorAll("button")) {
1770
+ if (!(button instanceof HTMLButtonElement))
1771
+ continue;
1772
+ button.setAttribute("aria-current", button.dataset.schema === schema && button.dataset.table === name ? "true" : "false");
1773
+ }
1774
+ };
1775
+ const loadTables = async () => {
1776
+ const payload = await api.get("/sql/tables");
1777
+ const listed = isRecord2(payload) && Array.isArray(payload.tables) ? payload.tables : [];
1778
+ tables.replaceChildren();
1779
+ if (listed.length === 0) {
1780
+ const empty = document.createElement("p");
1781
+ empty.className = "empty";
1782
+ empty.textContent = "No tables.";
1783
+ tables.append(empty);
1784
+ return;
1785
+ }
1786
+ for (const item of listed) {
1787
+ if (!isRecord2(item) || typeof item.schema !== "string" || typeof item.name !== "string")
1788
+ continue;
1789
+ const button = document.createElement("button");
1790
+ button.type = "button";
1791
+ button.dataset.schema = item.schema;
1792
+ button.dataset.table = item.name;
1793
+ button.textContent = `${item.schema}.${item.name}`;
1794
+ tables.append(button);
1795
+ }
1796
+ };
1797
+ tables.addEventListener("click", (event) => {
1798
+ const target = event.target;
1799
+ if (!(target instanceof Element))
1800
+ return;
1801
+ const button = target.closest("button");
1802
+ if (!(button instanceof HTMLButtonElement))
1803
+ return;
1804
+ const schema = button.dataset.schema;
1805
+ const name = button.dataset.table;
1806
+ if (schema === void 0 || name === void 0)
1807
+ return;
1808
+ clearError();
1809
+ loadPage(schema, name).catch(showError);
1810
+ });
1811
+ run.addEventListener("click", () => {
1812
+ const sql = input.value.trim();
1813
+ if (sql === "") {
1814
+ showError(new Error("Enter a statement"));
1815
+ return;
1816
+ }
1817
+ clearError();
1818
+ api.send("POST", "/sql/query", { sql }).then((payload) => {
1819
+ const rows = rowsOf(payload);
1820
+ renderGrid(columnsOf(payload, rows), rows);
1821
+ const count = isRecord2(payload) && typeof payload.rowCount === "number" ? payload.rowCount : rows.length;
1822
+ const truncated = isRecord2(payload) && payload.truncated === true ? " (truncated)" : "";
1823
+ result.textContent = `${count} rows${truncated}`;
1824
+ }).catch(showError);
1825
+ });
1826
+ const namespace = document.getElementById("namespace");
1827
+ if (namespace) {
1828
+ namespace.addEventListener("change", () => {
1829
+ clearError();
1830
+ loadTables().catch(showError);
1831
+ });
1832
+ }
1833
+ loadTables().catch(showError);
1834
+ }
1674
1835
  function bootAdmin(config) {
1675
1836
  const fieldKinds = ["string", "number", "boolean", "null", "object", "array", "unknown"];
1676
1837
  const sources = ["declared", "inferred", "mixed"];
@@ -2347,6 +2508,22 @@ function bootAdmin(config) {
2347
2508
  panel.script = value.script;
2348
2509
  return panel;
2349
2510
  };
2511
+ const decodeExtension = (value) => {
2512
+ if (!isRecord2(value))
2513
+ return void 0;
2514
+ if (value.kind === "sql") {
2515
+ return {
2516
+ kind: "sql",
2517
+ id: typeof value.id === "string" && value.id !== "" ? value.id : "sql",
2518
+ title: typeof value.title === "string" && value.title !== "" ? value.title : "SQL",
2519
+ description: typeof value.description === "string" ? value.description : "Browse tables and run queries."
2520
+ };
2521
+ }
2522
+ if (value.kind !== "panel")
2523
+ return void 0;
2524
+ const panel = decodePanel(value);
2525
+ return panel === void 0 ? void 0 : { kind: "panel", ...panel };
2526
+ };
2350
2527
  const mountPanels = (panels) => {
2351
2528
  const nav = el("nav", HTMLElement);
2352
2529
  const views = el("views", HTMLElement);
@@ -2400,10 +2577,48 @@ function bootAdmin(config) {
2400
2577
  return decoded === void 0 ? [] : [decoded];
2401
2578
  });
2402
2579
  mountPanels(panels);
2580
+ const extensions = requireList(isRecord2(manifest) ? manifest.extensions : void 0, "Extensions").flatMap((extension) => {
2581
+ const decoded = decodeExtension(extension);
2582
+ return decoded === void 0 ? [] : [decoded];
2583
+ });
2584
+ const extensionPanels = extensions.flatMap((extension) => extension.kind === "panel" ? [extension] : []);
2585
+ mountPanels(extensionPanels);
2586
+ for (const extension of extensions) {
2587
+ if (extension.kind !== "sql")
2588
+ continue;
2589
+ const nav = el("nav", HTMLElement);
2590
+ const views = el("views", HTMLElement);
2591
+ const button = document.createElement("button");
2592
+ button.type = "button";
2593
+ button.dataset.view = extension.id;
2594
+ button.textContent = extension.title;
2595
+ nav.append(button);
2596
+ const section = document.createElement("section");
2597
+ section.className = "view";
2598
+ section.id = `view-${extension.id}`;
2599
+ section.hidden = true;
2600
+ const title = document.createElement("h2");
2601
+ title.textContent = extension.title;
2602
+ const lede = document.createElement("p");
2603
+ lede.className = "lede";
2604
+ lede.textContent = extension.description;
2605
+ const body = document.createElement("div");
2606
+ body.className = "panel-body";
2607
+ section.append(title, lede, body);
2608
+ views.append(section);
2609
+ mountSqlExplorer(body, panelApi);
2610
+ }
2403
2611
  return refresh();
2404
2612
  }).catch(showError);
2405
2613
  }
2406
- var clientPrelude = [isRecord2, mountAdminBrand, startAdminBrand, faultPresetList, bootAdmin].map((fn) => fn.toString()).join("\n");
2614
+ var clientPrelude = [
2615
+ isRecord2,
2616
+ mountAdminBrand,
2617
+ startAdminBrand,
2618
+ faultPresetList,
2619
+ mountSqlExplorer,
2620
+ bootAdmin
2621
+ ].map((fn) => fn.toString()).join("\n");
2407
2622
  var adminClientSource = (config) => {
2408
2623
  const payload = JSON.stringify(config).replace(/</g, "\\u003c");
2409
2624
  const entry = bootAdmin.name;
@@ -2453,19 +2668,33 @@ var STANDARD_ADMIN_ROUTES = [
2453
2668
  "GET /ui/manifest"
2454
2669
  ];
2455
2670
  var PANEL_ID = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
2671
+ var RESERVED_VIEWS = /* @__PURE__ */ new Set(["overview", "state", "clock", "faults", "journal", "routes"]);
2672
+ var checkPanel = (panel, seen, label) => {
2673
+ if (!PANEL_ID.test(panel.id)) {
2674
+ throw new RangeError(`${label} id must match ${PANEL_ID}: ${JSON.stringify(panel.id)}`);
2675
+ }
2676
+ if (RESERVED_VIEWS.has(panel.id)) {
2677
+ throw new RangeError(`${label} id ${panel.id} is reserved by the admin shell`);
2678
+ }
2679
+ if (seen.has(panel.id))
2680
+ throw new RangeError(`duplicate admin view id ${panel.id}`);
2681
+ seen.add(panel.id);
2682
+ if (panel.title.trim() === "")
2683
+ throw new RangeError(`${label} ${panel.id} needs a title`);
2684
+ };
2456
2685
  var assertAdminUi = (ui) => {
2457
- if (!ui?.panels)
2686
+ if (!ui)
2458
2687
  return;
2459
2688
  const seen = /* @__PURE__ */ new Set();
2460
- for (const panel of ui.panels) {
2461
- if (!PANEL_ID.test(panel.id)) {
2462
- throw new RangeError(`admin panel id must match ${PANEL_ID}: ${JSON.stringify(panel.id)}`);
2689
+ for (const panel of ui.panels ?? [])
2690
+ checkPanel(panel, seen, "admin panel");
2691
+ for (const extension of ui.extensions ?? []) {
2692
+ if (extension.kind === "panel") {
2693
+ checkPanel(extension, seen, "admin extension");
2694
+ continue;
2463
2695
  }
2464
- if (seen.has(panel.id))
2465
- throw new RangeError(`duplicate admin panel id ${panel.id}`);
2466
- seen.add(panel.id);
2467
- if (panel.title.trim() === "")
2468
- throw new RangeError(`admin panel ${panel.id} needs a title`);
2696
+ const id = extension.id ?? "sql";
2697
+ checkPanel({ id, title: extension.title ?? "SQL" }, seen, "admin extension");
2469
2698
  }
2470
2699
  };
2471
2700
 
@@ -2699,6 +2928,12 @@ var renderAdminDocument = (service) => {
2699
2928
  border-radius: 999px; padding: 4px 8px;
2700
2929
  }
2701
2930
  .chip i { color: var(--faint); font-style: normal; }
2931
+ .sql-layout {
2932
+ display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
2933
+ }
2934
+ .sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
2935
+ .sql-tables button { text-align: left; }
2936
+ .sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
2702
2937
  table { width: 100%; border-collapse: collapse; font-size: 14px; }
2703
2938
  th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--line); vertical-align: top; }
2704
2939
  th { color: var(--faint); font-weight: 600; font-size: 12px; }
@@ -2762,7 +2997,7 @@ var renderAdminDocument = (service) => {
2762
2997
  padding: 8px;
2763
2998
  }
2764
2999
  nav.side button { white-space: nowrap; flex: none; }
2765
- .split { grid-template-columns: minmax(0, 1fr); }
3000
+ .split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
2766
3001
  main { padding: 14px; }
2767
3002
  table, thead, tbody, tr, th, td { display: block; }
2768
3003
  thead { display: none; }
@@ -2933,6 +3168,24 @@ var json2 = (body) => new Response(JSON.stringify(body), {
2933
3168
  status: 200,
2934
3169
  headers: { "content-type": "application/json; charset=utf-8" }
2935
3170
  });
3171
+ var describeExtension = (extension) => {
3172
+ if (extension.kind === "sql") {
3173
+ return {
3174
+ kind: "sql",
3175
+ id: extension.id ?? "sql",
3176
+ title: extension.title ?? "SQL",
3177
+ ...extension.description !== void 0 ? { description: extension.description } : {}
3178
+ };
3179
+ }
3180
+ return {
3181
+ kind: "panel",
3182
+ id: extension.id,
3183
+ title: extension.title,
3184
+ ...extension.description !== void 0 ? { description: extension.description } : {},
3185
+ html: extension.html,
3186
+ ...extension.script !== void 0 ? { script: extension.script } : {}
3187
+ };
3188
+ };
2936
3189
  var adminUiRoutes = (service, ui) => {
2937
3190
  const shell = () => renderAdminDocument(service);
2938
3191
  const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
@@ -2947,7 +3200,8 @@ var adminUiRoutes = (service, ui) => {
2947
3200
  ...panel.description !== void 0 ? { description: panel.description } : {},
2948
3201
  html: panel.html,
2949
3202
  ...panel.script !== void 0 ? { script: panel.script } : {}
2950
- }))
3203
+ })),
3204
+ extensions: (ui?.extensions ?? []).map(describeExtension)
2951
3205
  })
2952
3206
  };
2953
3207
  };
@@ -3550,7 +3804,7 @@ var stateAdminRoutes = (routes) => {
3550
3804
  };
3551
3805
 
3552
3806
  // ../core/dist/version.js
3553
- var PACKAGE_VERSION = true ? "1.7.1" : UNRELEASED_VERSION;
3807
+ var PACKAGE_VERSION = true ? "1.8.1" : UNRELEASED_VERSION;
3554
3808
 
3555
3809
  // ../core/dist/webhooks.js
3556
3810
  var json4 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
@@ -12069,4 +12323,4 @@ export {
12069
12323
  JunctionAPI,
12070
12324
  LabAccountConflict
12071
12325
  };
12072
- //# sourceMappingURL=chunk-KWBRKBUI.js.map
12326
+ //# sourceMappingURL=chunk-GDU23FW6.js.map