@crvouga/mockingbird-service-junction 1.7.0 → 1.8.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.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,24 @@
1
1
  # Changelog — @crvouga/mockingbird-service-junction
2
2
 
3
- ## 1.7.0 (2026-09-30)
3
+ ## 1.8.0 (2026-10-01)
4
+
5
+ ### Features
6
+
7
+ - record SQL timelines and serve the admin explorer ([8ced17a](https://github.com/crvouga/mockingbird/commit/8ced17ac398e386ef4e9d7e01fc7744c34b95d07))
8
+
9
+ ### Dependencies
10
+
11
+ - `@crvouga/mockingbird-service-sqlite`
12
+
13
+ ## 1.7.1 (2026-09-30)
14
+
15
+ ### Fixes and improvements
16
+
17
+ - keep admin buttons pressed until the action finishes ([0aabfcb](https://github.com/crvouga/mockingbird/commit/0aabfcb5f392aa51e193f9c3f817d5877d92bc6a))
18
+ - size admin select fields like the text inputs beside them ([0fb3ef2](https://github.com/crvouga/mockingbird/commit/0fb3ef2ddbf43376c93a2ae6d87c6c9bf890dafc))
19
+ - keep admin section tabs wired when boot is renamed ([4eda25e](https://github.com/crvouga/mockingbird/commit/4eda25eacd59da048d196f7cc8369d7a02b47bfc))
20
+
21
+ ## 1.7.0 (2026-09-29)
4
22
 
5
23
  ### Features
6
24
 
@@ -3,7 +3,7 @@ import {
3
3
  createRuntime,
4
4
  defaultBillingTypesFrom,
5
5
  pullCorpus
6
- } from "./chunk-LUHSOIDO.js";
6
+ } from "./chunk-WDRX5POF.js";
7
7
  import {
8
8
  defaultCorpus
9
9
  } from "./chunk-C2UGIPPM.js";
@@ -537,4 +537,4 @@ export {
537
537
  createServer2 as createServer,
538
538
  serveTarget
539
539
  };
540
- //# sourceMappingURL=chunk-WGRRZGPF.js.map
540
+ //# sourceMappingURL=chunk-OW7EOZRN.js.map
@@ -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"];
@@ -2111,6 +2272,50 @@ function bootAdmin(config) {
2111
2272
  if (render2)
2112
2273
  await render2();
2113
2274
  }
2275
+ const delay = (ms) => new Promise((resolve) => {
2276
+ window.setTimeout(resolve, ms);
2277
+ });
2278
+ const confirmAction = async (button, pending, done, work) => {
2279
+ if (button.dataset.pending === "1")
2280
+ return;
2281
+ const label = button.textContent ?? "";
2282
+ const width = button.offsetWidth;
2283
+ button.dataset.pending = "1";
2284
+ button.classList.add("pressed");
2285
+ button.setAttribute("aria-busy", "true");
2286
+ button.style.minWidth = `${width}px`;
2287
+ button.textContent = pending;
2288
+ try {
2289
+ await work();
2290
+ button.textContent = done;
2291
+ await delay(1e3);
2292
+ } catch (error) {
2293
+ showError(error);
2294
+ } finally {
2295
+ button.textContent = label;
2296
+ button.style.minWidth = "";
2297
+ button.classList.remove("pressed");
2298
+ button.removeAttribute("aria-busy");
2299
+ delete button.dataset.pending;
2300
+ }
2301
+ };
2302
+ document.addEventListener("click", (event) => {
2303
+ const target = event.target;
2304
+ if (!(target instanceof Element))
2305
+ return;
2306
+ const button = target.closest("button");
2307
+ if (!(button instanceof HTMLButtonElement) || button.dataset.pending === "1")
2308
+ return;
2309
+ button.classList.add("pressed");
2310
+ const token = String(Date.now());
2311
+ button.dataset.press = token;
2312
+ window.setTimeout(() => {
2313
+ if (button.dataset.press !== token || button.dataset.pending === "1")
2314
+ return;
2315
+ button.classList.remove("pressed");
2316
+ delete button.dataset.press;
2317
+ }, 450);
2318
+ });
2114
2319
  el("namespace", HTMLSelectElement).addEventListener("change", () => {
2115
2320
  state.namespace = el("namespace", HTMLSelectElement).value;
2116
2321
  refresh().catch(showError);
@@ -2121,7 +2326,7 @@ function bootAdmin(config) {
2121
2326
  refresh().catch(showError);
2122
2327
  });
2123
2328
  el("refresh", HTMLButtonElement).addEventListener("click", () => {
2124
- refresh().catch(showError);
2329
+ confirmAction(el("refresh", HTMLButtonElement), "Refreshing...", "Updated", () => refresh()).catch(showError);
2125
2330
  });
2126
2331
  el("collections", HTMLElement).addEventListener("click", (event) => {
2127
2332
  const target = event.target;
@@ -2244,10 +2449,10 @@ function bootAdmin(config) {
2244
2449
  }).then(() => renderFaults()).catch(showError);
2245
2450
  });
2246
2451
  el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
2247
- api("DELETE", "/faults").then(() => renderFaults()).catch(showError);
2452
+ confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
2248
2453
  });
2249
2454
  el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
2250
- api("DELETE", "/requests").then(() => renderJournal()).catch(showError);
2455
+ confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
2251
2456
  });
2252
2457
  el("route-list", HTMLElement).addEventListener("click", (event) => {
2253
2458
  const target = event.target;
@@ -2303,6 +2508,22 @@ function bootAdmin(config) {
2303
2508
  panel.script = value.script;
2304
2509
  return panel;
2305
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
+ };
2306
2527
  const mountPanels = (panels) => {
2307
2528
  const nav = el("nav", HTMLElement);
2308
2529
  const views = el("views", HTMLElement);
@@ -2356,15 +2577,57 @@ function bootAdmin(config) {
2356
2577
  return decoded === void 0 ? [] : [decoded];
2357
2578
  });
2358
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
+ }
2359
2611
  return refresh();
2360
2612
  }).catch(showError);
2361
2613
  }
2362
- 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");
2363
2622
  var adminClientSource = (config) => {
2364
2623
  const payload = JSON.stringify(config).replace(/</g, "\\u003c");
2624
+ const entry = bootAdmin.name;
2625
+ if (!/^[A-Za-z_$][\w$]*$/.test(entry)) {
2626
+ throw new Error("admin client cannot call its boot function");
2627
+ }
2365
2628
  return `<script>
2366
2629
  ${clientPrelude}
2367
- bootAdmin(${payload});
2630
+ ${entry}(${payload});
2368
2631
  </script>`;
2369
2632
  };
2370
2633
 
@@ -2405,19 +2668,33 @@ var STANDARD_ADMIN_ROUTES = [
2405
2668
  "GET /ui/manifest"
2406
2669
  ];
2407
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
+ };
2408
2685
  var assertAdminUi = (ui) => {
2409
- if (!ui?.panels)
2686
+ if (!ui)
2410
2687
  return;
2411
2688
  const seen = /* @__PURE__ */ new Set();
2412
- for (const panel of ui.panels) {
2413
- if (!PANEL_ID.test(panel.id)) {
2414
- 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;
2415
2695
  }
2416
- if (seen.has(panel.id))
2417
- throw new RangeError(`duplicate admin panel id ${panel.id}`);
2418
- seen.add(panel.id);
2419
- if (panel.title.trim() === "")
2420
- 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");
2421
2698
  }
2422
2699
  };
2423
2700
 
@@ -2578,9 +2855,24 @@ var renderAdminDocument = (service) => {
2578
2855
  .controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
2579
2856
  label.field { display: flex; flex-direction: column; gap: 2px; font-size: 11px; color: var(--faint); }
2580
2857
  label.field input, label.field select, .controls input[type="password"] {
2581
- background: var(--bg-inset); border: 1px solid var(--line); border-radius: 8px;
2582
- padding: 7px 8px; min-height: 36px;
2583
- }
2858
+ background-color: var(--bg-inset); border: 1px solid var(--line); border-radius: 8px;
2859
+ padding: 7px 8px; min-height: 36px; line-height: 1.2;
2860
+ }
2861
+ /* A native menu ignores min-height, so Preset rendered about half as tall as
2862
+ the text fields in the same form. Paint it like those fields. */
2863
+ label.field select {
2864
+ -webkit-appearance: none; appearance: none;
2865
+ padding-right: 28px;
2866
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='none' stroke='%2371717a' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' d='M2.5 4.5 6 8l3.5-3.5'/%3E%3C/svg%3E");
2867
+ background-repeat: no-repeat; background-position: right 10px center; background-size: 12px 12px;
2868
+ }
2869
+ .stack > .field > input,
2870
+ .stack > .field > select,
2871
+ .stack > .field > textarea,
2872
+ dialog .field > input,
2873
+ dialog .field > select,
2874
+ dialog .field > textarea { width: 100%; }
2875
+ .icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
2584
2876
  .icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
2585
2877
  border-radius: 9px; min-height: 36px; padding: 0 12px; border: 1px solid var(--line);
2586
2878
  background: var(--bg-elev);
@@ -2588,6 +2880,14 @@ var renderAdminDocument = (service) => {
2588
2880
  .btn-primary { background: var(--accent); color: var(--accent-ink); border-color: transparent; }
2589
2881
  .btn-quiet { background: transparent; }
2590
2882
  .btn-danger { color: var(--err); }
2883
+ /* Held after mouseup. Clearing an empty journal changes nothing on screen,
2884
+ so the press itself has to stay visible. */
2885
+ .icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
2886
+ .icon-btn.pressed, .btn.pressed, .btn-quiet.pressed, .btn-danger.pressed, nav.side button.pressed {
2887
+ background: var(--bg-inset); border-color: var(--line-strong); color: var(--ink);
2888
+ }
2889
+ .btn-danger:active, .btn-danger.pressed { background: var(--err-soft); color: var(--err); }
2890
+ .btn-primary:active, .btn-primary.pressed { filter: brightness(0.88); color: var(--accent-ink); }
2591
2891
  /* minmax(0, 1fr): a 1fr column will not shrink below its content, so a long
2592
2892
  record or the full nav row would widen the page past the viewport. */
2593
2893
  .layout {
@@ -2628,6 +2928,12 @@ var renderAdminDocument = (service) => {
2628
2928
  border-radius: 999px; padding: 4px 8px;
2629
2929
  }
2630
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%; }
2631
2937
  table { width: 100%; border-collapse: collapse; font-size: 14px; }
2632
2938
  th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--line); vertical-align: top; }
2633
2939
  th { color: var(--faint); font-weight: 600; font-size: 12px; }
@@ -2676,14 +2982,22 @@ var renderAdminDocument = (service) => {
2676
2982
  input, select, textarea { font-size: 16px; }
2677
2983
  .brand, .vendor, .controls { width: 100%; }
2678
2984
  label.field, label.field select, label.field input { flex: 1; min-width: 0; }
2679
- .layout { grid-template-columns: minmax(0, 1fr); }
2985
+ /* The layout fills the screen. A single-column grid stretches every auto
2986
+ row, so the tab bar grew with the leftover height and the buttons floated
2987
+ in the middle of it. The bar stays one row; the page below takes the rest. */
2988
+ .layout {
2989
+ grid-template-columns: minmax(0, 1fr);
2990
+ grid-template-rows: auto minmax(0, 1fr);
2991
+ align-content: start;
2992
+ }
2680
2993
  nav.side {
2994
+ align-self: start; height: auto;
2681
2995
  flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
2682
2996
  border-right: 0; border-bottom: 1px solid var(--line);
2683
2997
  padding: 8px;
2684
2998
  }
2685
2999
  nav.side button { white-space: nowrap; flex: none; }
2686
- .split { grid-template-columns: minmax(0, 1fr); }
3000
+ .split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
2687
3001
  main { padding: 14px; }
2688
3002
  table, thead, tbody, tr, th, td { display: block; }
2689
3003
  thead { display: none; }
@@ -2854,6 +3168,24 @@ var json2 = (body) => new Response(JSON.stringify(body), {
2854
3168
  status: 200,
2855
3169
  headers: { "content-type": "application/json; charset=utf-8" }
2856
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
+ };
2857
3189
  var adminUiRoutes = (service, ui) => {
2858
3190
  const shell = () => renderAdminDocument(service);
2859
3191
  const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
@@ -2868,7 +3200,8 @@ var adminUiRoutes = (service, ui) => {
2868
3200
  ...panel.description !== void 0 ? { description: panel.description } : {},
2869
3201
  html: panel.html,
2870
3202
  ...panel.script !== void 0 ? { script: panel.script } : {}
2871
- }))
3203
+ })),
3204
+ extensions: (ui?.extensions ?? []).map(describeExtension)
2872
3205
  })
2873
3206
  };
2874
3207
  };
@@ -3471,7 +3804,7 @@ var stateAdminRoutes = (routes) => {
3471
3804
  };
3472
3805
 
3473
3806
  // ../core/dist/version.js
3474
- var PACKAGE_VERSION = true ? "1.7.0" : UNRELEASED_VERSION;
3807
+ var PACKAGE_VERSION = true ? "1.8.0" : UNRELEASED_VERSION;
3475
3808
 
3476
3809
  // ../core/dist/webhooks.js
3477
3810
  var json4 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
@@ -11990,4 +12323,4 @@ export {
11990
12323
  JunctionAPI,
11991
12324
  LabAccountConflict
11992
12325
  };
11993
- //# sourceMappingURL=chunk-LUHSOIDO.js.map
12326
+ //# sourceMappingURL=chunk-WDRX5POF.js.map