@crvouga/mockingbird-service-aws-speech 1.6.0 → 1.7.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-aws-speech
2
2
 
3
- ## 1.6.0 (2026-09-30)
3
+ ## 1.7.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.6.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.6.0 (2026-09-29)
4
22
 
5
23
  ### Features
6
24
 
@@ -1656,6 +1656,167 @@ function faultPresetList(value) {
1656
1656
  })
1657
1657
  };
1658
1658
  }
1659
+ function mountSqlExplorer(root, api) {
1660
+ const cell = (value) => {
1661
+ if (value === null || value === void 0)
1662
+ return "";
1663
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
1664
+ return String(value);
1665
+ }
1666
+ return JSON.stringify(value) ?? "";
1667
+ };
1668
+ root.replaceChildren();
1669
+ const layout = document.createElement("div");
1670
+ layout.className = "sql-layout";
1671
+ const tables = document.createElement("div");
1672
+ tables.className = "sql-tables";
1673
+ const stack = document.createElement("div");
1674
+ stack.className = "stack";
1675
+ const banner = document.createElement("p");
1676
+ banner.className = "banner";
1677
+ const grid = document.createElement("div");
1678
+ const input = document.createElement("textarea");
1679
+ input.className = "sql-input";
1680
+ input.spellcheck = false;
1681
+ input.placeholder = "SELECT * FROM ...";
1682
+ const actions = document.createElement("div");
1683
+ actions.className = "row-actions";
1684
+ const run = document.createElement("button");
1685
+ run.type = "button";
1686
+ run.textContent = "Run";
1687
+ actions.append(run);
1688
+ const result = document.createElement("pre");
1689
+ stack.append(banner, grid, input, actions, result);
1690
+ layout.append(tables, stack);
1691
+ root.append(layout);
1692
+ const showError = (error) => {
1693
+ banner.textContent = error instanceof Error ? error.message : String(error);
1694
+ banner.classList.add("show");
1695
+ };
1696
+ const clearError = () => {
1697
+ banner.textContent = "";
1698
+ banner.classList.remove("show");
1699
+ };
1700
+ const renderGrid = (columns, rows) => {
1701
+ grid.replaceChildren();
1702
+ if (columns.length === 0) {
1703
+ const empty = document.createElement("p");
1704
+ empty.className = "empty";
1705
+ empty.textContent = "No rows.";
1706
+ grid.append(empty);
1707
+ return;
1708
+ }
1709
+ const table = document.createElement("table");
1710
+ const head = document.createElement("thead");
1711
+ const headRow = document.createElement("tr");
1712
+ for (const column of columns) {
1713
+ const th = document.createElement("th");
1714
+ th.textContent = column;
1715
+ headRow.append(th);
1716
+ }
1717
+ head.append(headRow);
1718
+ const body = document.createElement("tbody");
1719
+ for (const row of rows) {
1720
+ const tr = document.createElement("tr");
1721
+ for (const column of columns) {
1722
+ const td = document.createElement("td");
1723
+ td.dataset.label = column;
1724
+ td.textContent = cell(row[column]);
1725
+ tr.append(td);
1726
+ }
1727
+ body.append(tr);
1728
+ }
1729
+ table.append(head, body);
1730
+ grid.append(table);
1731
+ };
1732
+ const rowsOf = (value) => {
1733
+ if (!isRecord2(value) || !Array.isArray(value.rows))
1734
+ return [];
1735
+ return value.rows.flatMap((row) => isRecord2(row) ? [row] : []);
1736
+ };
1737
+ const columnsOf = (value, rows) => {
1738
+ if (isRecord2(value) && Array.isArray(value.columns)) {
1739
+ const named = value.columns.flatMap((column) => typeof column === "string" ? [column] : []);
1740
+ if (named.length > 0)
1741
+ return named;
1742
+ }
1743
+ const first = rows[0];
1744
+ return first === void 0 ? [] : Object.keys(first);
1745
+ };
1746
+ let selected = "";
1747
+ const loadPage = async (schema, name) => {
1748
+ selected = `${schema}.${name}`;
1749
+ const page = await api.get(`/sql/tables/${encodeURIComponent(schema)}/${encodeURIComponent(name)}?limit=50`);
1750
+ const rows = rowsOf(page);
1751
+ renderGrid(columnsOf(page, rows), rows);
1752
+ const total = isRecord2(page) && typeof page.total === "number" ? page.total : rows.length;
1753
+ result.textContent = `${selected} \xB7 ${total} rows`;
1754
+ for (const button of tables.querySelectorAll("button")) {
1755
+ if (!(button instanceof HTMLButtonElement))
1756
+ continue;
1757
+ button.setAttribute("aria-current", button.dataset.schema === schema && button.dataset.table === name ? "true" : "false");
1758
+ }
1759
+ };
1760
+ const loadTables = async () => {
1761
+ const payload = await api.get("/sql/tables");
1762
+ const listed = isRecord2(payload) && Array.isArray(payload.tables) ? payload.tables : [];
1763
+ tables.replaceChildren();
1764
+ if (listed.length === 0) {
1765
+ const empty = document.createElement("p");
1766
+ empty.className = "empty";
1767
+ empty.textContent = "No tables.";
1768
+ tables.append(empty);
1769
+ return;
1770
+ }
1771
+ for (const item of listed) {
1772
+ if (!isRecord2(item) || typeof item.schema !== "string" || typeof item.name !== "string")
1773
+ continue;
1774
+ const button = document.createElement("button");
1775
+ button.type = "button";
1776
+ button.dataset.schema = item.schema;
1777
+ button.dataset.table = item.name;
1778
+ button.textContent = `${item.schema}.${item.name}`;
1779
+ tables.append(button);
1780
+ }
1781
+ };
1782
+ tables.addEventListener("click", (event) => {
1783
+ const target = event.target;
1784
+ if (!(target instanceof Element))
1785
+ return;
1786
+ const button = target.closest("button");
1787
+ if (!(button instanceof HTMLButtonElement))
1788
+ return;
1789
+ const schema = button.dataset.schema;
1790
+ const name = button.dataset.table;
1791
+ if (schema === void 0 || name === void 0)
1792
+ return;
1793
+ clearError();
1794
+ loadPage(schema, name).catch(showError);
1795
+ });
1796
+ run.addEventListener("click", () => {
1797
+ const sql = input.value.trim();
1798
+ if (sql === "") {
1799
+ showError(new Error("Enter a statement"));
1800
+ return;
1801
+ }
1802
+ clearError();
1803
+ api.send("POST", "/sql/query", { sql }).then((payload) => {
1804
+ const rows = rowsOf(payload);
1805
+ renderGrid(columnsOf(payload, rows), rows);
1806
+ const count = isRecord2(payload) && typeof payload.rowCount === "number" ? payload.rowCount : rows.length;
1807
+ const truncated = isRecord2(payload) && payload.truncated === true ? " (truncated)" : "";
1808
+ result.textContent = `${count} rows${truncated}`;
1809
+ }).catch(showError);
1810
+ });
1811
+ const namespace = document.getElementById("namespace");
1812
+ if (namespace) {
1813
+ namespace.addEventListener("change", () => {
1814
+ clearError();
1815
+ loadTables().catch(showError);
1816
+ });
1817
+ }
1818
+ loadTables().catch(showError);
1819
+ }
1659
1820
  function bootAdmin(config) {
1660
1821
  const fieldKinds = ["string", "number", "boolean", "null", "object", "array", "unknown"];
1661
1822
  const sources = ["declared", "inferred", "mixed"];
@@ -2096,6 +2257,50 @@ function bootAdmin(config) {
2096
2257
  if (render)
2097
2258
  await render();
2098
2259
  }
2260
+ const delay = (ms) => new Promise((resolve) => {
2261
+ window.setTimeout(resolve, ms);
2262
+ });
2263
+ const confirmAction = async (button, pending, done, work) => {
2264
+ if (button.dataset.pending === "1")
2265
+ return;
2266
+ const label = button.textContent ?? "";
2267
+ const width = button.offsetWidth;
2268
+ button.dataset.pending = "1";
2269
+ button.classList.add("pressed");
2270
+ button.setAttribute("aria-busy", "true");
2271
+ button.style.minWidth = `${width}px`;
2272
+ button.textContent = pending;
2273
+ try {
2274
+ await work();
2275
+ button.textContent = done;
2276
+ await delay(1e3);
2277
+ } catch (error) {
2278
+ showError(error);
2279
+ } finally {
2280
+ button.textContent = label;
2281
+ button.style.minWidth = "";
2282
+ button.classList.remove("pressed");
2283
+ button.removeAttribute("aria-busy");
2284
+ delete button.dataset.pending;
2285
+ }
2286
+ };
2287
+ document.addEventListener("click", (event) => {
2288
+ const target = event.target;
2289
+ if (!(target instanceof Element))
2290
+ return;
2291
+ const button = target.closest("button");
2292
+ if (!(button instanceof HTMLButtonElement) || button.dataset.pending === "1")
2293
+ return;
2294
+ button.classList.add("pressed");
2295
+ const token = String(Date.now());
2296
+ button.dataset.press = token;
2297
+ window.setTimeout(() => {
2298
+ if (button.dataset.press !== token || button.dataset.pending === "1")
2299
+ return;
2300
+ button.classList.remove("pressed");
2301
+ delete button.dataset.press;
2302
+ }, 450);
2303
+ });
2099
2304
  el("namespace", HTMLSelectElement).addEventListener("change", () => {
2100
2305
  state.namespace = el("namespace", HTMLSelectElement).value;
2101
2306
  refresh().catch(showError);
@@ -2106,7 +2311,7 @@ function bootAdmin(config) {
2106
2311
  refresh().catch(showError);
2107
2312
  });
2108
2313
  el("refresh", HTMLButtonElement).addEventListener("click", () => {
2109
- refresh().catch(showError);
2314
+ confirmAction(el("refresh", HTMLButtonElement), "Refreshing...", "Updated", () => refresh()).catch(showError);
2110
2315
  });
2111
2316
  el("collections", HTMLElement).addEventListener("click", (event) => {
2112
2317
  const target = event.target;
@@ -2229,10 +2434,10 @@ function bootAdmin(config) {
2229
2434
  }).then(() => renderFaults()).catch(showError);
2230
2435
  });
2231
2436
  el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
2232
- api("DELETE", "/faults").then(() => renderFaults()).catch(showError);
2437
+ confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
2233
2438
  });
2234
2439
  el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
2235
- api("DELETE", "/requests").then(() => renderJournal()).catch(showError);
2440
+ confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
2236
2441
  });
2237
2442
  el("route-list", HTMLElement).addEventListener("click", (event) => {
2238
2443
  const target = event.target;
@@ -2288,6 +2493,22 @@ function bootAdmin(config) {
2288
2493
  panel.script = value.script;
2289
2494
  return panel;
2290
2495
  };
2496
+ const decodeExtension = (value) => {
2497
+ if (!isRecord2(value))
2498
+ return void 0;
2499
+ if (value.kind === "sql") {
2500
+ return {
2501
+ kind: "sql",
2502
+ id: typeof value.id === "string" && value.id !== "" ? value.id : "sql",
2503
+ title: typeof value.title === "string" && value.title !== "" ? value.title : "SQL",
2504
+ description: typeof value.description === "string" ? value.description : "Browse tables and run queries."
2505
+ };
2506
+ }
2507
+ if (value.kind !== "panel")
2508
+ return void 0;
2509
+ const panel = decodePanel(value);
2510
+ return panel === void 0 ? void 0 : { kind: "panel", ...panel };
2511
+ };
2291
2512
  const mountPanels = (panels) => {
2292
2513
  const nav = el("nav", HTMLElement);
2293
2514
  const views = el("views", HTMLElement);
@@ -2341,15 +2562,57 @@ function bootAdmin(config) {
2341
2562
  return decoded === void 0 ? [] : [decoded];
2342
2563
  });
2343
2564
  mountPanels(panels);
2565
+ const extensions = requireList(isRecord2(manifest) ? manifest.extensions : void 0, "Extensions").flatMap((extension) => {
2566
+ const decoded = decodeExtension(extension);
2567
+ return decoded === void 0 ? [] : [decoded];
2568
+ });
2569
+ const extensionPanels = extensions.flatMap((extension) => extension.kind === "panel" ? [extension] : []);
2570
+ mountPanels(extensionPanels);
2571
+ for (const extension of extensions) {
2572
+ if (extension.kind !== "sql")
2573
+ continue;
2574
+ const nav = el("nav", HTMLElement);
2575
+ const views = el("views", HTMLElement);
2576
+ const button = document.createElement("button");
2577
+ button.type = "button";
2578
+ button.dataset.view = extension.id;
2579
+ button.textContent = extension.title;
2580
+ nav.append(button);
2581
+ const section = document.createElement("section");
2582
+ section.className = "view";
2583
+ section.id = `view-${extension.id}`;
2584
+ section.hidden = true;
2585
+ const title = document.createElement("h2");
2586
+ title.textContent = extension.title;
2587
+ const lede = document.createElement("p");
2588
+ lede.className = "lede";
2589
+ lede.textContent = extension.description;
2590
+ const body = document.createElement("div");
2591
+ body.className = "panel-body";
2592
+ section.append(title, lede, body);
2593
+ views.append(section);
2594
+ mountSqlExplorer(body, panelApi);
2595
+ }
2344
2596
  return refresh();
2345
2597
  }).catch(showError);
2346
2598
  }
2347
- var clientPrelude = [isRecord2, mountAdminBrand, startAdminBrand, faultPresetList, bootAdmin].map((fn) => fn.toString()).join("\n");
2599
+ var clientPrelude = [
2600
+ isRecord2,
2601
+ mountAdminBrand,
2602
+ startAdminBrand,
2603
+ faultPresetList,
2604
+ mountSqlExplorer,
2605
+ bootAdmin
2606
+ ].map((fn) => fn.toString()).join("\n");
2348
2607
  var adminClientSource = (config) => {
2349
2608
  const payload = JSON.stringify(config).replace(/</g, "\\u003c");
2609
+ const entry = bootAdmin.name;
2610
+ if (!/^[A-Za-z_$][\w$]*$/.test(entry)) {
2611
+ throw new Error("admin client cannot call its boot function");
2612
+ }
2350
2613
  return `<script>
2351
2614
  ${clientPrelude}
2352
- bootAdmin(${payload});
2615
+ ${entry}(${payload});
2353
2616
  </script>`;
2354
2617
  };
2355
2618
 
@@ -2390,19 +2653,33 @@ var STANDARD_ADMIN_ROUTES = [
2390
2653
  "GET /ui/manifest"
2391
2654
  ];
2392
2655
  var PANEL_ID = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
2656
+ var RESERVED_VIEWS = /* @__PURE__ */ new Set(["overview", "state", "clock", "faults", "journal", "routes"]);
2657
+ var checkPanel = (panel, seen, label) => {
2658
+ if (!PANEL_ID.test(panel.id)) {
2659
+ throw new RangeError(`${label} id must match ${PANEL_ID}: ${JSON.stringify(panel.id)}`);
2660
+ }
2661
+ if (RESERVED_VIEWS.has(panel.id)) {
2662
+ throw new RangeError(`${label} id ${panel.id} is reserved by the admin shell`);
2663
+ }
2664
+ if (seen.has(panel.id))
2665
+ throw new RangeError(`duplicate admin view id ${panel.id}`);
2666
+ seen.add(panel.id);
2667
+ if (panel.title.trim() === "")
2668
+ throw new RangeError(`${label} ${panel.id} needs a title`);
2669
+ };
2393
2670
  var assertAdminUi = (ui) => {
2394
- if (!ui?.panels)
2671
+ if (!ui)
2395
2672
  return;
2396
2673
  const seen = /* @__PURE__ */ new Set();
2397
- for (const panel of ui.panels) {
2398
- if (!PANEL_ID.test(panel.id)) {
2399
- throw new RangeError(`admin panel id must match ${PANEL_ID}: ${JSON.stringify(panel.id)}`);
2674
+ for (const panel of ui.panels ?? [])
2675
+ checkPanel(panel, seen, "admin panel");
2676
+ for (const extension of ui.extensions ?? []) {
2677
+ if (extension.kind === "panel") {
2678
+ checkPanel(extension, seen, "admin extension");
2679
+ continue;
2400
2680
  }
2401
- if (seen.has(panel.id))
2402
- throw new RangeError(`duplicate admin panel id ${panel.id}`);
2403
- seen.add(panel.id);
2404
- if (panel.title.trim() === "")
2405
- throw new RangeError(`admin panel ${panel.id} needs a title`);
2681
+ const id = extension.id ?? "sql";
2682
+ checkPanel({ id, title: extension.title ?? "SQL" }, seen, "admin extension");
2406
2683
  }
2407
2684
  };
2408
2685
 
@@ -2563,9 +2840,24 @@ var renderAdminDocument = (service) => {
2563
2840
  .controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
2564
2841
  label.field { display: flex; flex-direction: column; gap: 2px; font-size: 11px; color: var(--faint); }
2565
2842
  label.field input, label.field select, .controls input[type="password"] {
2566
- background: var(--bg-inset); border: 1px solid var(--line); border-radius: 8px;
2567
- padding: 7px 8px; min-height: 36px;
2568
- }
2843
+ background-color: var(--bg-inset); border: 1px solid var(--line); border-radius: 8px;
2844
+ padding: 7px 8px; min-height: 36px; line-height: 1.2;
2845
+ }
2846
+ /* A native menu ignores min-height, so Preset rendered about half as tall as
2847
+ the text fields in the same form. Paint it like those fields. */
2848
+ label.field select {
2849
+ -webkit-appearance: none; appearance: none;
2850
+ padding-right: 28px;
2851
+ 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");
2852
+ background-repeat: no-repeat; background-position: right 10px center; background-size: 12px 12px;
2853
+ }
2854
+ .stack > .field > input,
2855
+ .stack > .field > select,
2856
+ .stack > .field > textarea,
2857
+ dialog .field > input,
2858
+ dialog .field > select,
2859
+ dialog .field > textarea { width: 100%; }
2860
+ .icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
2569
2861
  .icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
2570
2862
  border-radius: 9px; min-height: 36px; padding: 0 12px; border: 1px solid var(--line);
2571
2863
  background: var(--bg-elev);
@@ -2573,6 +2865,14 @@ var renderAdminDocument = (service) => {
2573
2865
  .btn-primary { background: var(--accent); color: var(--accent-ink); border-color: transparent; }
2574
2866
  .btn-quiet { background: transparent; }
2575
2867
  .btn-danger { color: var(--err); }
2868
+ /* Held after mouseup. Clearing an empty journal changes nothing on screen,
2869
+ so the press itself has to stay visible. */
2870
+ .icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
2871
+ .icon-btn.pressed, .btn.pressed, .btn-quiet.pressed, .btn-danger.pressed, nav.side button.pressed {
2872
+ background: var(--bg-inset); border-color: var(--line-strong); color: var(--ink);
2873
+ }
2874
+ .btn-danger:active, .btn-danger.pressed { background: var(--err-soft); color: var(--err); }
2875
+ .btn-primary:active, .btn-primary.pressed { filter: brightness(0.88); color: var(--accent-ink); }
2576
2876
  /* minmax(0, 1fr): a 1fr column will not shrink below its content, so a long
2577
2877
  record or the full nav row would widen the page past the viewport. */
2578
2878
  .layout {
@@ -2613,6 +2913,12 @@ var renderAdminDocument = (service) => {
2613
2913
  border-radius: 999px; padding: 4px 8px;
2614
2914
  }
2615
2915
  .chip i { color: var(--faint); font-style: normal; }
2916
+ .sql-layout {
2917
+ display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
2918
+ }
2919
+ .sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
2920
+ .sql-tables button { text-align: left; }
2921
+ .sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
2616
2922
  table { width: 100%; border-collapse: collapse; font-size: 14px; }
2617
2923
  th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--line); vertical-align: top; }
2618
2924
  th { color: var(--faint); font-weight: 600; font-size: 12px; }
@@ -2661,14 +2967,22 @@ var renderAdminDocument = (service) => {
2661
2967
  input, select, textarea { font-size: 16px; }
2662
2968
  .brand, .vendor, .controls { width: 100%; }
2663
2969
  label.field, label.field select, label.field input { flex: 1; min-width: 0; }
2664
- .layout { grid-template-columns: minmax(0, 1fr); }
2970
+ /* The layout fills the screen. A single-column grid stretches every auto
2971
+ row, so the tab bar grew with the leftover height and the buttons floated
2972
+ in the middle of it. The bar stays one row; the page below takes the rest. */
2973
+ .layout {
2974
+ grid-template-columns: minmax(0, 1fr);
2975
+ grid-template-rows: auto minmax(0, 1fr);
2976
+ align-content: start;
2977
+ }
2665
2978
  nav.side {
2979
+ align-self: start; height: auto;
2666
2980
  flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
2667
2981
  border-right: 0; border-bottom: 1px solid var(--line);
2668
2982
  padding: 8px;
2669
2983
  }
2670
2984
  nav.side button { white-space: nowrap; flex: none; }
2671
- .split { grid-template-columns: minmax(0, 1fr); }
2985
+ .split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
2672
2986
  main { padding: 14px; }
2673
2987
  table, thead, tbody, tr, th, td { display: block; }
2674
2988
  thead { display: none; }
@@ -2839,6 +3153,24 @@ var json2 = (body) => new Response(JSON.stringify(body), {
2839
3153
  status: 200,
2840
3154
  headers: { "content-type": "application/json; charset=utf-8" }
2841
3155
  });
3156
+ var describeExtension = (extension) => {
3157
+ if (extension.kind === "sql") {
3158
+ return {
3159
+ kind: "sql",
3160
+ id: extension.id ?? "sql",
3161
+ title: extension.title ?? "SQL",
3162
+ ...extension.description !== void 0 ? { description: extension.description } : {}
3163
+ };
3164
+ }
3165
+ return {
3166
+ kind: "panel",
3167
+ id: extension.id,
3168
+ title: extension.title,
3169
+ ...extension.description !== void 0 ? { description: extension.description } : {},
3170
+ html: extension.html,
3171
+ ...extension.script !== void 0 ? { script: extension.script } : {}
3172
+ };
3173
+ };
2842
3174
  var adminUiRoutes = (service, ui) => {
2843
3175
  const shell = () => renderAdminDocument(service);
2844
3176
  const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
@@ -2853,7 +3185,8 @@ var adminUiRoutes = (service, ui) => {
2853
3185
  ...panel.description !== void 0 ? { description: panel.description } : {},
2854
3186
  html: panel.html,
2855
3187
  ...panel.script !== void 0 ? { script: panel.script } : {}
2856
- }))
3188
+ })),
3189
+ extensions: (ui?.extensions ?? []).map(describeExtension)
2857
3190
  })
2858
3191
  };
2859
3192
  };
@@ -3456,7 +3789,7 @@ var stateAdminRoutes = (routes) => {
3456
3789
  };
3457
3790
 
3458
3791
  // ../core/dist/version.js
3459
- var PACKAGE_VERSION = true ? "1.6.0" : UNRELEASED_VERSION;
3792
+ var PACKAGE_VERSION = true ? "1.7.0" : UNRELEASED_VERSION;
3460
3793
 
3461
3794
  // ../core/dist/signing.js
3462
3795
  var encoder = new TextEncoder();
@@ -5618,4 +5951,4 @@ export {
5618
5951
  accessKeyCredential,
5619
5952
  SpeechAPI
5620
5953
  };
5621
- //# sourceMappingURL=chunk-7C3XFQVJ.js.map
5954
+ //# sourceMappingURL=chunk-4VZZ5XPO.js.map