@crvouga/mockingbird-service-livekit 1.8.0 → 2.0.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 +23 -1
- package/DISCOVERY.md +55 -0
- package/README.md +1 -1
- package/SUPPORT.md +13 -0
- package/dist/{chunk-3VTAPQLB.js → chunk-DLPZY3HQ.js} +43 -23
- package/dist/chunk-DLPZY3HQ.js.map +7 -0
- package/dist/{chunk-OYDPGLNO.js → chunk-QIRIAV7T.js} +622 -235
- package/dist/chunk-QIRIAV7T.js.map +7 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.js +2 -2
- package/openapi.yaml +37 -0
- package/package.json +29 -4
- package/dist/chunk-3VTAPQLB.js.map +0 -7
- package/dist/chunk-OYDPGLNO.js.map +0 -7
|
@@ -110,13 +110,31 @@ var Collection = class {
|
|
|
110
110
|
};
|
|
111
111
|
|
|
112
112
|
// ../core/dist/control.js
|
|
113
|
-
var HEALTH_PATH = "/health";
|
|
114
113
|
var ADMIN_PREFIX = "/__admin";
|
|
114
|
+
var resolveAdminPrefix = (prefix = ADMIN_PREFIX) => {
|
|
115
|
+
if (!/^\/[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*$/.test(prefix)) {
|
|
116
|
+
throw new Error("adminPrefix must be an absolute non-root path of literal segments, without a trailing slash");
|
|
117
|
+
}
|
|
118
|
+
return prefix;
|
|
119
|
+
};
|
|
120
|
+
var isAdminPath = (path, prefix) => path === prefix || path.startsWith(`${prefix}/`);
|
|
121
|
+
var matchNamespacePath = (path, prefix) => /^\/ns\/([^/]+)(\/.*)?$/.exec(isAdminPath(path, prefix) ? path.slice(prefix.length) : "");
|
|
122
|
+
var assertAdminPrefixAvailable = (prefix, paths) => {
|
|
123
|
+
const reserved = prefix.split("/").slice(1);
|
|
124
|
+
for (const path of paths) {
|
|
125
|
+
const segments = path.split("/").slice(1);
|
|
126
|
+
if (segments.length < reserved.length || /^\{[^}]+\}$/.test(segments[0] ?? ""))
|
|
127
|
+
continue;
|
|
128
|
+
if (reserved.every((part, i) => segments[i] === part || /^\{[^}]+\}$/.test(segments[i] ?? ""))) {
|
|
129
|
+
throw new Error(`vendor route ${path} collides with adminPrefix ${prefix}; choose a different adminPrefix`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
115
133
|
var ADMIN_KEY_HEADER = "x-mockingbird-admin-key";
|
|
116
134
|
var NAMESPACE_HEADER = "x-mockingbird-namespace";
|
|
117
135
|
var json = (status, body) => new Response(JSON.stringify(body), {
|
|
118
136
|
status,
|
|
119
|
-
headers: { "content-type": "application/json" }
|
|
137
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
120
138
|
});
|
|
121
139
|
var adminError = (status, message) => json(status, { error: { type: "mockingbird_admin", message } });
|
|
122
140
|
var UNITS = {
|
|
@@ -168,11 +186,25 @@ var readJson = async (request) => {
|
|
|
168
186
|
};
|
|
169
187
|
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
170
188
|
var createControlPlane = (context) => {
|
|
189
|
+
const adminPrefix = resolveAdminPrefix(context.adminPrefix);
|
|
171
190
|
const snapshots = /* @__PURE__ */ new Map();
|
|
172
191
|
let snapshotCounter = 0;
|
|
173
192
|
const headerNamespace = (request) => request.headers.get(NAMESPACE_HEADER) ?? context.defaultNamespace;
|
|
174
193
|
const adminNamespace = (request, url) => url.searchParams.get("namespace") ?? headerNamespace(request);
|
|
194
|
+
const health = () => {
|
|
195
|
+
return json(200, {
|
|
196
|
+
...context.describe(),
|
|
197
|
+
status: "ok",
|
|
198
|
+
service: context.name,
|
|
199
|
+
uptimeMs: context.wallNow() - context.startedAt,
|
|
200
|
+
clock: context.clock.state(),
|
|
201
|
+
namespaces: context.namespaces().length,
|
|
202
|
+
adminPrefix,
|
|
203
|
+
adminUi: `${adminPrefix}/ui`
|
|
204
|
+
});
|
|
205
|
+
};
|
|
175
206
|
const builtin = {
|
|
207
|
+
"GET /health": health,
|
|
176
208
|
"GET /": () => json(200, {
|
|
177
209
|
service: context.name,
|
|
178
210
|
routes: [...Object.keys(builtin), ...Object.keys(context.routes)].sort()
|
|
@@ -339,7 +371,14 @@ var createControlPlane = (context) => {
|
|
|
339
371
|
return json(200, { status: "ok" });
|
|
340
372
|
}
|
|
341
373
|
};
|
|
342
|
-
const
|
|
374
|
+
for (const key of Object.keys(context.routes)) {
|
|
375
|
+
if (Object.hasOwn(builtin, key))
|
|
376
|
+
throw new Error(`admin route ${key} overrides a shared route`);
|
|
377
|
+
const path = key.slice(key.indexOf(" ") + 1);
|
|
378
|
+
if (isAdminPath(path, "/ns") || isAdminPath(path, "/blobs"))
|
|
379
|
+
throw new Error(`admin route ${key} overlaps an internal transport`);
|
|
380
|
+
}
|
|
381
|
+
const routes = [...Object.entries(builtin), ...Object.entries(context.routes)].map(([key, handler]) => {
|
|
343
382
|
const space = key.indexOf(" ");
|
|
344
383
|
return { method: key.slice(0, space), pattern: key.slice(space + 1), handler };
|
|
345
384
|
});
|
|
@@ -347,21 +386,13 @@ var createControlPlane = (context) => {
|
|
|
347
386
|
namespaceOf: headerNamespace,
|
|
348
387
|
async handle(request) {
|
|
349
388
|
const url = new URL(request.url);
|
|
350
|
-
if (url.pathname ===
|
|
351
|
-
return
|
|
352
|
-
status: "ok",
|
|
353
|
-
service: context.name,
|
|
354
|
-
uptimeMs: context.wallNow() - context.startedAt,
|
|
355
|
-
clock: context.clock.state(),
|
|
356
|
-
namespaces: context.namespaces().length,
|
|
357
|
-
adminUi: `${ADMIN_PREFIX}/ui`,
|
|
358
|
-
...context.describe()
|
|
359
|
-
});
|
|
389
|
+
if (url.pathname === `${adminPrefix}/health` && request.method === "GET") {
|
|
390
|
+
return health();
|
|
360
391
|
}
|
|
361
|
-
if (url.pathname
|
|
392
|
+
if (!isAdminPath(url.pathname, adminPrefix)) {
|
|
362
393
|
return void 0;
|
|
363
394
|
}
|
|
364
|
-
const path = url.pathname.slice(
|
|
395
|
+
const path = url.pathname.slice(adminPrefix.length) || "/";
|
|
365
396
|
const shell = request.method === "GET" && (path === "/ui" || path === "/ui/");
|
|
366
397
|
if (!shell && context.adminKey !== void 0 && request.headers.get(ADMIN_KEY_HEADER) !== context.adminKey) {
|
|
367
398
|
return adminError(401, `missing or wrong ${ADMIN_KEY_HEADER}`);
|
|
@@ -369,7 +400,12 @@ var createControlPlane = (context) => {
|
|
|
369
400
|
for (const route of routes) {
|
|
370
401
|
if (route.method !== request.method)
|
|
371
402
|
continue;
|
|
372
|
-
|
|
403
|
+
let params;
|
|
404
|
+
try {
|
|
405
|
+
params = matchRoute(route.pattern, path);
|
|
406
|
+
} catch {
|
|
407
|
+
return adminError(400, "invalid path encoding");
|
|
408
|
+
}
|
|
373
409
|
if (!params)
|
|
374
410
|
continue;
|
|
375
411
|
let body;
|
|
@@ -378,15 +414,18 @@ var createControlPlane = (context) => {
|
|
|
378
414
|
} catch {
|
|
379
415
|
return adminError(400, "request body is not valid JSON");
|
|
380
416
|
}
|
|
417
|
+
const namespace = adminNamespace(request, url);
|
|
418
|
+
if (!/^[A-Za-z0-9_.-]{1,64}$/.test(namespace))
|
|
419
|
+
return adminError(400, "invalid namespace");
|
|
381
420
|
return route.handler({
|
|
382
421
|
request,
|
|
383
422
|
url,
|
|
384
423
|
params,
|
|
385
|
-
namespace
|
|
424
|
+
namespace,
|
|
386
425
|
body
|
|
387
426
|
});
|
|
388
427
|
}
|
|
389
|
-
return adminError(404, `no admin route ${request.method} ${path}; GET ${
|
|
428
|
+
return adminError(404, `no admin route ${request.method} ${path}; GET ${adminPrefix} lists them`);
|
|
390
429
|
}
|
|
391
430
|
};
|
|
392
431
|
};
|
|
@@ -1391,6 +1430,34 @@ dialog:not([open]) {
|
|
|
1391
1430
|
function isRecord2(value) {
|
|
1392
1431
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1393
1432
|
}
|
|
1433
|
+
function highlightJsonText(text) {
|
|
1434
|
+
const escapeToken = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
1435
|
+
switch (char) {
|
|
1436
|
+
case "&":
|
|
1437
|
+
return "&";
|
|
1438
|
+
case "<":
|
|
1439
|
+
return "<";
|
|
1440
|
+
case ">":
|
|
1441
|
+
return ">";
|
|
1442
|
+
case '"':
|
|
1443
|
+
return """;
|
|
1444
|
+
default:
|
|
1445
|
+
return "'";
|
|
1446
|
+
}
|
|
1447
|
+
});
|
|
1448
|
+
const token = /("(?:\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*"\s*:|"(?:\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*"|\b(?:true|false)\b|\bnull\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g;
|
|
1449
|
+
let cursor = 0;
|
|
1450
|
+
let html = "";
|
|
1451
|
+
for (const match2 of text.matchAll(token)) {
|
|
1452
|
+
const index = match2.index ?? 0;
|
|
1453
|
+
const value = match2[0];
|
|
1454
|
+
html += escapeToken(text.slice(cursor, index));
|
|
1455
|
+
const kind = value.startsWith('"') ? value.trimEnd().endsWith(":") ? "key" : "string" : value === "true" || value === "false" ? "boolean" : value === "null" ? "null" : "number";
|
|
1456
|
+
html += `<span class="json-${kind}">${escapeToken(value)}</span>`;
|
|
1457
|
+
cursor = index + value.length;
|
|
1458
|
+
}
|
|
1459
|
+
return html + escapeToken(text.slice(cursor));
|
|
1460
|
+
}
|
|
1394
1461
|
function mountAdminBrand(doc, host, service, catalog) {
|
|
1395
1462
|
const clip = (value, max) => {
|
|
1396
1463
|
if (typeof value !== "string")
|
|
@@ -1752,14 +1819,16 @@ function bootAdmin(config) {
|
|
|
1752
1819
|
const state = {
|
|
1753
1820
|
namespace: "default",
|
|
1754
1821
|
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
1755
|
-
view: "overview",
|
|
1822
|
+
view: ["overview", "state", "clock", "faults", "journal", "routes"].includes(location.hash.slice(1)) ? location.hash.slice(1) : "overview",
|
|
1756
1823
|
collection: "",
|
|
1757
1824
|
shape: null,
|
|
1758
1825
|
route: null,
|
|
1826
|
+
routes: [],
|
|
1759
1827
|
editing: null
|
|
1760
1828
|
};
|
|
1761
1829
|
const standard = new Set(config.standardRoutes);
|
|
1762
1830
|
const banner = el("banner", HTMLElement);
|
|
1831
|
+
const bannerMessage = el("banner-message", HTMLElement);
|
|
1763
1832
|
const keyInput = el("admin-key", HTMLInputElement);
|
|
1764
1833
|
keyInput.value = state.key;
|
|
1765
1834
|
const params = new URLSearchParams(location.search);
|
|
@@ -1770,18 +1839,21 @@ function bootAdmin(config) {
|
|
|
1770
1839
|
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
1771
1840
|
params.delete("key");
|
|
1772
1841
|
const next = params.toString();
|
|
1773
|
-
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
1842
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : "") + location.hash);
|
|
1774
1843
|
}
|
|
1775
1844
|
const queryNamespace = params.get("namespace");
|
|
1776
1845
|
if (queryNamespace)
|
|
1777
1846
|
state.namespace = queryNamespace;
|
|
1778
1847
|
const showError = (error2) => {
|
|
1779
|
-
|
|
1848
|
+
bannerMessage.textContent = error2 instanceof Error ? error2.message : String(error2);
|
|
1780
1849
|
banner.classList.add("show");
|
|
1850
|
+
banner.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
1781
1851
|
};
|
|
1782
1852
|
const clearError = () => {
|
|
1783
1853
|
banner.classList.remove("show");
|
|
1854
|
+
bannerMessage.textContent = "";
|
|
1784
1855
|
};
|
|
1856
|
+
el("banner-dismiss", HTMLButtonElement).addEventListener("click", clearError);
|
|
1785
1857
|
const api = async (method, path, body) => {
|
|
1786
1858
|
const headers = { accept: "application/json" };
|
|
1787
1859
|
if (state.key !== "")
|
|
@@ -1825,16 +1897,21 @@ function bootAdmin(config) {
|
|
|
1825
1897
|
namespace: () => state.namespace
|
|
1826
1898
|
};
|
|
1827
1899
|
const isPanelScript = (value) => typeof value === "function";
|
|
1900
|
+
const themeButton = el("theme", HTMLButtonElement);
|
|
1828
1901
|
const setTheme = (theme) => {
|
|
1829
1902
|
if (theme)
|
|
1830
1903
|
document.documentElement.dataset.theme = theme;
|
|
1831
1904
|
else
|
|
1832
1905
|
delete document.documentElement.dataset.theme;
|
|
1906
|
+
const dark = theme ? theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
1907
|
+
themeButton.innerHTML = dark ? '<svg viewBox="0 0 20 20" fill="none" aria-hidden="true"><circle cx="10" cy="10" r="3.2" stroke="currentColor" stroke-width="1.5"/><path d="M10 2v1.5M10 16.5V18M18 10h-1.5M3.5 10H2M15.7 4.3l-1 1M5.3 14.7l-1 1M15.7 15.7l-1-1M5.3 5.3l-1-1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>' : '<svg viewBox="0 0 20 20" fill="none" aria-hidden="true"><path d="M16.8 12.2A7 7 0 0 1 7.8 3.2a7 7 0 1 0 9 9Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>';
|
|
1908
|
+
const label = dark ? "Use light theme" : "Use dark theme";
|
|
1909
|
+
themeButton.setAttribute("aria-label", label);
|
|
1910
|
+
themeButton.title = label;
|
|
1833
1911
|
};
|
|
1834
1912
|
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
1913
|
+
setTheme(storedTheme === "dark" || storedTheme === "light" ? storedTheme : null);
|
|
1914
|
+
themeButton.addEventListener("click", () => {
|
|
1838
1915
|
const current = document.documentElement.dataset.theme;
|
|
1839
1916
|
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
1840
1917
|
const next = dark ? "light" : "dark";
|
|
@@ -1853,6 +1930,7 @@ function bootAdmin(config) {
|
|
|
1853
1930
|
continue;
|
|
1854
1931
|
section.hidden = section.id !== `view-${view}`;
|
|
1855
1932
|
}
|
|
1933
|
+
history.replaceState(null, "", `${location.pathname}${location.search}#${encodeURIComponent(view)}`);
|
|
1856
1934
|
};
|
|
1857
1935
|
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
1858
1936
|
const target = event.target;
|
|
@@ -1867,7 +1945,7 @@ function bootAdmin(config) {
|
|
|
1867
1945
|
show(view);
|
|
1868
1946
|
refresh().catch(showError);
|
|
1869
1947
|
});
|
|
1870
|
-
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>`;
|
|
1948
|
+
const cards = (items) => `<div class="grid">${items.map((item) => `<article class="card stat-card"><div class="k">${item.k}</div><div class="v">${item.v}</div></article>`).join("")}</div>`;
|
|
1871
1949
|
const table = (columns, rows) => {
|
|
1872
1950
|
if (rows.length === 0)
|
|
1873
1951
|
return '<p class="empty">Nothing here yet.</p>';
|
|
@@ -1877,16 +1955,22 @@ function bootAdmin(config) {
|
|
|
1877
1955
|
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
1878
1956
|
return `<tr${attrs}>${cells}</tr>`;
|
|
1879
1957
|
}).join("");
|
|
1880
|
-
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
1958
|
+
return `<div class="table-shell"><table><thead>${head}</thead><tbody>${body}</tbody></table></div>`;
|
|
1881
1959
|
};
|
|
1882
1960
|
const preview = (value) => {
|
|
1883
1961
|
const text = JSON.stringify(value) ?? "null";
|
|
1884
|
-
|
|
1962
|
+
const clipped = text.length > 180 ? `${text.slice(0, 180)}\u2026` : text;
|
|
1963
|
+
return `<code class="json-inline">${highlightJsonText(clipped)}</code>`;
|
|
1885
1964
|
};
|
|
1886
1965
|
const when = (epoch) => {
|
|
1887
1966
|
const date = new Date(epoch);
|
|
1888
1967
|
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
1889
1968
|
};
|
|
1969
|
+
const journalWhen = (value) => {
|
|
1970
|
+
const numeric = value.trim() === "" ? Number.NaN : Number(value);
|
|
1971
|
+
const epoch = Number.isFinite(numeric) ? numeric : Date.parse(value);
|
|
1972
|
+
return Number.isFinite(epoch) ? when(epoch) : value;
|
|
1973
|
+
};
|
|
1890
1974
|
const clockCard = (epoch) => {
|
|
1891
1975
|
const date = new Date(epoch);
|
|
1892
1976
|
if (Number.isNaN(date.getTime()))
|
|
@@ -2006,7 +2090,7 @@ function bootAdmin(config) {
|
|
|
2006
2090
|
};
|
|
2007
2091
|
const renderOverview = async () => {
|
|
2008
2092
|
const [health, shape, clock, journal] = await Promise.all([
|
|
2009
|
-
originGet(
|
|
2093
|
+
originGet(`${config.adminPrefix}/health`),
|
|
2010
2094
|
api("GET", "/state"),
|
|
2011
2095
|
api("GET", "/clock"),
|
|
2012
2096
|
api("GET", "/requests?limit=8")
|
|
@@ -2023,7 +2107,7 @@ function bootAdmin(config) {
|
|
|
2023
2107
|
},
|
|
2024
2108
|
{
|
|
2025
2109
|
k: "Status",
|
|
2026
|
-
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
2110
|
+
v: `<span class="badge ok">${esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")}</span>`
|
|
2027
2111
|
},
|
|
2028
2112
|
{ k: "Namespace", v: esc(state.namespace) },
|
|
2029
2113
|
{ k: "Collections", v: String(decoded.collections.length) },
|
|
@@ -2110,26 +2194,46 @@ function bootAdmin(config) {
|
|
|
2110
2194
|
const renderJournal = async () => {
|
|
2111
2195
|
const journal = await api("GET", "/requests?limit=100");
|
|
2112
2196
|
el("journal", HTMLElement).innerHTML = table([
|
|
2113
|
-
{ label: "When", cell: (row) => esc(row.at) },
|
|
2114
|
-
{
|
|
2197
|
+
{ label: "When", cell: (row) => esc(journalWhen(row.at)) },
|
|
2198
|
+
{
|
|
2199
|
+
label: "Method",
|
|
2200
|
+
cell: (row) => `<span class="method">${esc(row.method)}</span>`
|
|
2201
|
+
},
|
|
2115
2202
|
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2116
|
-
{
|
|
2203
|
+
{
|
|
2204
|
+
label: "Status",
|
|
2205
|
+
cell: (row) => `<span class="badge ${row.status.startsWith("2") ? "ok" : row.status.startsWith("4") || row.status.startsWith("5") ? "warn" : ""}">${esc(row.status)}</span>`
|
|
2206
|
+
},
|
|
2117
2207
|
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
2118
2208
|
], decodeJournal(journal));
|
|
2119
2209
|
};
|
|
2120
|
-
const
|
|
2121
|
-
const
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2210
|
+
const paintRoutes = () => {
|
|
2211
|
+
const query = el("route-search", HTMLInputElement).value.trim().toLocaleLowerCase();
|
|
2212
|
+
const routes = state.routes.filter((route) => route.toLocaleLowerCase().includes(query));
|
|
2213
|
+
el("route-count", HTMLElement).textContent = `${routes.length} of ${state.routes.length}`;
|
|
2214
|
+
if (state.routes.length === 0) {
|
|
2215
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No admin routes are available.</p>';
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2218
|
+
if (routes.length === 0) {
|
|
2219
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No routes match this filter.</p>';
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2125
2222
|
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2126
2223
|
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2127
2224
|
const parts = route.split(" ");
|
|
2128
2225
|
const method = parts[0] ?? "";
|
|
2129
2226
|
const path = parts.slice(1).join(" ");
|
|
2130
|
-
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>`;
|
|
2227
|
+
return `<div class="route"><span class="method">${esc(method)}</span><span>${esc(path)}${extra}</span><button class="btn" type="button" data-route="${esc(route)}" aria-label="Use ${esc(route)}">Use</button></div>`;
|
|
2131
2228
|
}).join("");
|
|
2132
2229
|
};
|
|
2230
|
+
const renderRoutes = async () => {
|
|
2231
|
+
const data = await api("GET", "/");
|
|
2232
|
+
if (!isRecord2(data))
|
|
2233
|
+
throw new Error("Route list was not an object");
|
|
2234
|
+
state.routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
2235
|
+
paintRoutes();
|
|
2236
|
+
};
|
|
2133
2237
|
const renderers = {
|
|
2134
2238
|
overview: renderOverview,
|
|
2135
2239
|
state: renderState,
|
|
@@ -2138,19 +2242,50 @@ function bootAdmin(config) {
|
|
|
2138
2242
|
journal: renderJournal,
|
|
2139
2243
|
routes: renderRoutes
|
|
2140
2244
|
};
|
|
2245
|
+
let refreshToken = 0;
|
|
2141
2246
|
async function refresh() {
|
|
2247
|
+
const token = ++refreshToken;
|
|
2142
2248
|
clearError();
|
|
2249
|
+
const main = el("views", HTMLElement);
|
|
2250
|
+
const sync = el("sync-status", HTMLElement);
|
|
2251
|
+
main.setAttribute("aria-busy", "true");
|
|
2252
|
+
sync.classList.add("busy");
|
|
2253
|
+
sync.textContent = "Updating";
|
|
2143
2254
|
const render = renderers[state.view];
|
|
2144
|
-
|
|
2145
|
-
|
|
2255
|
+
try {
|
|
2256
|
+
if (render)
|
|
2257
|
+
await render();
|
|
2258
|
+
if (token === refreshToken)
|
|
2259
|
+
sync.textContent = "Up to date";
|
|
2260
|
+
} finally {
|
|
2261
|
+
if (token === refreshToken) {
|
|
2262
|
+
main.removeAttribute("aria-busy");
|
|
2263
|
+
sync.classList.remove("busy");
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2146
2266
|
}
|
|
2147
2267
|
const delay = (ms) => new Promise((resolve) => {
|
|
2148
2268
|
window.setTimeout(resolve, ms);
|
|
2149
2269
|
});
|
|
2150
|
-
const confirmAction = async (button, pending, done, work) => {
|
|
2270
|
+
const confirmAction = async (button, pending, done, work, confirmLabel) => {
|
|
2151
2271
|
if (button.dataset.pending === "1")
|
|
2152
2272
|
return;
|
|
2153
2273
|
const label = button.textContent ?? "";
|
|
2274
|
+
if (confirmLabel !== void 0 && button.dataset.confirming !== "1") {
|
|
2275
|
+
button.dataset.confirming = "1";
|
|
2276
|
+
button.classList.add("confirming");
|
|
2277
|
+
button.textContent = confirmLabel;
|
|
2278
|
+
window.setTimeout(() => {
|
|
2279
|
+
if (button.dataset.confirming !== "1")
|
|
2280
|
+
return;
|
|
2281
|
+
button.textContent = label;
|
|
2282
|
+
button.classList.remove("confirming");
|
|
2283
|
+
delete button.dataset.confirming;
|
|
2284
|
+
}, 3e3);
|
|
2285
|
+
return;
|
|
2286
|
+
}
|
|
2287
|
+
button.classList.remove("confirming");
|
|
2288
|
+
delete button.dataset.confirming;
|
|
2154
2289
|
const width = button.offsetWidth;
|
|
2155
2290
|
button.dataset.pending = "1";
|
|
2156
2291
|
button.classList.add("pressed");
|
|
@@ -2171,6 +2306,65 @@ function bootAdmin(config) {
|
|
|
2171
2306
|
delete button.dataset.pending;
|
|
2172
2307
|
}
|
|
2173
2308
|
};
|
|
2309
|
+
const wireJsonEditor = (textareaId, highlightId, statusId, shellId, allowEmpty) => {
|
|
2310
|
+
const textarea = el(textareaId, HTMLTextAreaElement);
|
|
2311
|
+
const highlight = el(highlightId, HTMLElement);
|
|
2312
|
+
const status = el(statusId, HTMLElement);
|
|
2313
|
+
const shell = el(shellId, HTMLElement);
|
|
2314
|
+
const sync = () => {
|
|
2315
|
+
const text = textarea.value;
|
|
2316
|
+
highlight.innerHTML = `${highlightJsonText(text)}
|
|
2317
|
+
`;
|
|
2318
|
+
let valid = true;
|
|
2319
|
+
let message = "Valid JSON";
|
|
2320
|
+
if (allowEmpty && text.trim() === "")
|
|
2321
|
+
message = "No body";
|
|
2322
|
+
else {
|
|
2323
|
+
try {
|
|
2324
|
+
readJson2(text);
|
|
2325
|
+
} catch {
|
|
2326
|
+
valid = false;
|
|
2327
|
+
message = "Invalid JSON";
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
shell.classList.toggle("invalid", !valid);
|
|
2331
|
+
status.classList.toggle("invalid", !valid);
|
|
2332
|
+
status.textContent = message;
|
|
2333
|
+
textarea.setAttribute("aria-invalid", valid ? "false" : "true");
|
|
2334
|
+
};
|
|
2335
|
+
const syncScroll = () => {
|
|
2336
|
+
const preview2 = highlight.parentElement;
|
|
2337
|
+
if (!(preview2 instanceof HTMLElement))
|
|
2338
|
+
return;
|
|
2339
|
+
preview2.scrollTop = textarea.scrollTop;
|
|
2340
|
+
preview2.scrollLeft = textarea.scrollLeft;
|
|
2341
|
+
};
|
|
2342
|
+
textarea.addEventListener("input", sync);
|
|
2343
|
+
textarea.addEventListener("scroll", syncScroll);
|
|
2344
|
+
sync();
|
|
2345
|
+
return sync;
|
|
2346
|
+
};
|
|
2347
|
+
const syncRouteEditor = wireJsonEditor("route-body", "route-highlight", "route-json-status", "route-editor", true);
|
|
2348
|
+
const syncRecordEditor = wireJsonEditor("editor-value", "editor-highlight", "editor-json-status", "record-editor", false);
|
|
2349
|
+
for (const button of document.querySelectorAll("[data-format]")) {
|
|
2350
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2351
|
+
continue;
|
|
2352
|
+
button.addEventListener("click", () => {
|
|
2353
|
+
const target = button.dataset.format;
|
|
2354
|
+
if (target === void 0)
|
|
2355
|
+
return;
|
|
2356
|
+
const textarea = document.getElementById(target);
|
|
2357
|
+
if (!(textarea instanceof HTMLTextAreaElement) || textarea.value.trim() === "")
|
|
2358
|
+
return;
|
|
2359
|
+
try {
|
|
2360
|
+
textarea.value = JSON.stringify(readJson2(textarea.value), null, 2) ?? "";
|
|
2361
|
+
textarea.dispatchEvent(new Event("input"));
|
|
2362
|
+
textarea.focus();
|
|
2363
|
+
} catch {
|
|
2364
|
+
showError(new Error("Fix the JSON before formatting it"));
|
|
2365
|
+
}
|
|
2366
|
+
});
|
|
2367
|
+
}
|
|
2174
2368
|
document.addEventListener("click", (event) => {
|
|
2175
2369
|
const target = event.target;
|
|
2176
2370
|
if (!(target instanceof Element))
|
|
@@ -2243,6 +2437,7 @@ function bootAdmin(config) {
|
|
|
2243
2437
|
} else {
|
|
2244
2438
|
editorValue.value = "{\n \n}";
|
|
2245
2439
|
}
|
|
2440
|
+
syncRecordEditor();
|
|
2246
2441
|
editor.showModal();
|
|
2247
2442
|
}
|
|
2248
2443
|
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -2272,10 +2467,10 @@ function bootAdmin(config) {
|
|
|
2272
2467
|
const id = state.editing;
|
|
2273
2468
|
if (!id)
|
|
2274
2469
|
return;
|
|
2275
|
-
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2470
|
+
confirmAction(editorDelete, "Deleting...", "Deleted", () => api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2276
2471
|
editor.close();
|
|
2277
2472
|
return renderState();
|
|
2278
|
-
}).catch(showError);
|
|
2473
|
+
}), "Confirm delete").catch(showError);
|
|
2279
2474
|
});
|
|
2280
2475
|
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2281
2476
|
if (!(button instanceof HTMLButtonElement))
|
|
@@ -2321,10 +2516,10 @@ function bootAdmin(config) {
|
|
|
2321
2516
|
}).then(() => renderFaults()).catch(showError);
|
|
2322
2517
|
});
|
|
2323
2518
|
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2324
|
-
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
|
|
2519
|
+
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults()), "Confirm clear").catch(showError);
|
|
2325
2520
|
});
|
|
2326
2521
|
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2327
|
-
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
|
|
2522
|
+
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal()), "Confirm clear").catch(showError);
|
|
2328
2523
|
});
|
|
2329
2524
|
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2330
2525
|
const target = event.target;
|
|
@@ -2338,9 +2533,15 @@ function bootAdmin(config) {
|
|
|
2338
2533
|
return;
|
|
2339
2534
|
state.route = route;
|
|
2340
2535
|
el("route-title", HTMLElement).textContent = route;
|
|
2536
|
+
el("route-send", HTMLButtonElement).disabled = false;
|
|
2341
2537
|
const method = route.split(" ")[0] ?? "";
|
|
2342
|
-
el("route-body", HTMLTextAreaElement)
|
|
2538
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
2539
|
+
routeBody.disabled = method === "GET" || method === "DELETE";
|
|
2540
|
+
el("route-editor", HTMLElement).hidden = routeBody.disabled;
|
|
2541
|
+
el("route-json-status", HTMLElement).parentElement?.toggleAttribute("hidden", routeBody.disabled);
|
|
2542
|
+
syncRouteEditor();
|
|
2343
2543
|
});
|
|
2544
|
+
el("route-search", HTMLInputElement).addEventListener("input", paintRoutes);
|
|
2344
2545
|
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2345
2546
|
event.preventDefault();
|
|
2346
2547
|
const send = async () => {
|
|
@@ -2362,7 +2563,8 @@ function bootAdmin(config) {
|
|
|
2362
2563
|
}
|
|
2363
2564
|
}
|
|
2364
2565
|
const data = await api(method, concrete, body);
|
|
2365
|
-
|
|
2566
|
+
const text = JSON.stringify(data, null, 2) ?? "null";
|
|
2567
|
+
el("route-result", HTMLElement).innerHTML = `<code>${highlightJsonText(text)}</code>`;
|
|
2366
2568
|
};
|
|
2367
2569
|
send().catch(showError);
|
|
2368
2570
|
});
|
|
@@ -2480,11 +2682,13 @@ function bootAdmin(config) {
|
|
|
2480
2682
|
views.append(section);
|
|
2481
2683
|
mountSqlExplorer(body, panelApi);
|
|
2482
2684
|
}
|
|
2685
|
+
show(document.getElementById(`view-${state.view}`) === null ? "overview" : state.view);
|
|
2483
2686
|
return refresh();
|
|
2484
2687
|
}).catch(showError);
|
|
2485
2688
|
}
|
|
2486
2689
|
var clientPrelude = [
|
|
2487
2690
|
isRecord2,
|
|
2691
|
+
highlightJsonText,
|
|
2488
2692
|
mountAdminBrand,
|
|
2489
2693
|
startAdminBrand,
|
|
2490
2694
|
faultPresetList,
|
|
@@ -2507,6 +2711,7 @@ ${entry}(${payload});
|
|
|
2507
2711
|
var defineMock = (runtime) => runtime;
|
|
2508
2712
|
var STANDARD_ADMIN_ROUTES = [
|
|
2509
2713
|
"GET /",
|
|
2714
|
+
"GET /health",
|
|
2510
2715
|
"POST /reset",
|
|
2511
2716
|
"GET /namespaces",
|
|
2512
2717
|
"GET /clock",
|
|
@@ -2586,11 +2791,11 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
2586
2791
|
}
|
|
2587
2792
|
});
|
|
2588
2793
|
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
2589
|
-
var renderAdminDocument = (service) => {
|
|
2794
|
+
var renderAdminDocument = (service, adminPrefix = ADMIN_PREFIX) => {
|
|
2590
2795
|
const name = escapeHtml(service);
|
|
2591
2796
|
const script = adminClientSource({
|
|
2592
2797
|
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
2593
|
-
adminPrefix
|
|
2798
|
+
adminPrefix,
|
|
2594
2799
|
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
2595
2800
|
brandsUrl: ADMIN_BRANDS_URL,
|
|
2596
2801
|
service
|
|
@@ -2607,75 +2812,99 @@ var renderAdminDocument = (service) => {
|
|
|
2607
2812
|
${CSS_RESET}
|
|
2608
2813
|
:root {
|
|
2609
2814
|
color-scheme: light;
|
|
2610
|
-
--bg: #
|
|
2815
|
+
--bg: #f7f8fb;
|
|
2611
2816
|
--bg-elev: #ffffff;
|
|
2612
|
-
--bg-muted: #
|
|
2613
|
-
--bg-inset: #
|
|
2614
|
-
--line: #
|
|
2615
|
-
--line-strong: #
|
|
2616
|
-
--ink: #
|
|
2617
|
-
--muted: #
|
|
2618
|
-
--faint: #
|
|
2619
|
-
--accent: #
|
|
2817
|
+
--bg-muted: #f1f3f7;
|
|
2818
|
+
--bg-inset: #f8f9fc;
|
|
2819
|
+
--line: #e4e7ec;
|
|
2820
|
+
--line-strong: #cfd4dc;
|
|
2821
|
+
--ink: #101828;
|
|
2822
|
+
--muted: #475467;
|
|
2823
|
+
--faint: #667085;
|
|
2824
|
+
--accent: #6941c6;
|
|
2825
|
+
--accent-hover: #5933b4;
|
|
2620
2826
|
--accent-ink: #ffffff;
|
|
2621
|
-
--accent-soft: #
|
|
2622
|
-
--ok: #
|
|
2623
|
-
--ok-soft: #
|
|
2624
|
-
--warn: #
|
|
2625
|
-
--warn-soft: #
|
|
2626
|
-
--err: #
|
|
2627
|
-
--err-soft: #
|
|
2628
|
-
--
|
|
2827
|
+
--accent-soft: #f4f0ff;
|
|
2828
|
+
--ok: #067647;
|
|
2829
|
+
--ok-soft: #ecfdf3;
|
|
2830
|
+
--warn: #b54708;
|
|
2831
|
+
--warn-soft: #fffaeb;
|
|
2832
|
+
--err: #b42318;
|
|
2833
|
+
--err-soft: #fef3f2;
|
|
2834
|
+
--code-key: #6941c6;
|
|
2835
|
+
--code-string: #067647;
|
|
2836
|
+
--code-number: #175cd3;
|
|
2837
|
+
--code-boolean: #c11574;
|
|
2838
|
+
--code-null: #667085;
|
|
2839
|
+
--shadow-xs: 0 1px 2px rgb(16 24 40 / 0.05);
|
|
2840
|
+
--shadow-sm: 0 1px 3px rgb(16 24 40 / 0.08), 0 1px 2px rgb(16 24 40 / 0.04);
|
|
2841
|
+
--shadow-lg: 0 18px 48px rgb(16 24 40 / 0.16), 0 4px 12px rgb(16 24 40 / 0.08);
|
|
2629
2842
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2630
2843
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
2631
|
-
--radius:
|
|
2632
|
-
--header:
|
|
2633
|
-
--nav:
|
|
2844
|
+
--radius: 12px;
|
|
2845
|
+
--header: 72px;
|
|
2846
|
+
--nav: 224px;
|
|
2634
2847
|
}
|
|
2635
2848
|
:root[data-theme="dark"] {
|
|
2636
2849
|
color-scheme: dark;
|
|
2637
|
-
--bg: #
|
|
2638
|
-
--bg-elev: #
|
|
2639
|
-
--bg-muted: #
|
|
2640
|
-
--bg-inset: #
|
|
2641
|
-
--line: #
|
|
2642
|
-
--line-strong: #
|
|
2643
|
-
--ink: #
|
|
2644
|
-
--muted: #
|
|
2645
|
-
--faint: #
|
|
2646
|
-
--accent: #
|
|
2647
|
-
--accent-
|
|
2648
|
-
--accent-
|
|
2649
|
-
--
|
|
2650
|
-
--ok
|
|
2651
|
-
--
|
|
2652
|
-
--warn
|
|
2653
|
-
--
|
|
2654
|
-
--err
|
|
2655
|
-
--
|
|
2850
|
+
--bg: #0c0e14;
|
|
2851
|
+
--bg-elev: #151821;
|
|
2852
|
+
--bg-muted: #20232e;
|
|
2853
|
+
--bg-inset: #11141c;
|
|
2854
|
+
--line: #2b303d;
|
|
2855
|
+
--line-strong: #414858;
|
|
2856
|
+
--ink: #f5f7fa;
|
|
2857
|
+
--muted: #b6bdc9;
|
|
2858
|
+
--faint: #8d96a7;
|
|
2859
|
+
--accent: #9e77ed;
|
|
2860
|
+
--accent-hover: #b692f6;
|
|
2861
|
+
--accent-ink: #ffffff;
|
|
2862
|
+
--accent-soft: #2b2142;
|
|
2863
|
+
--ok: #75e0a7;
|
|
2864
|
+
--ok-soft: #102a20;
|
|
2865
|
+
--warn: #fec84b;
|
|
2866
|
+
--warn-soft: #302611;
|
|
2867
|
+
--err: #fda29b;
|
|
2868
|
+
--err-soft: #321817;
|
|
2869
|
+
--code-key: #c3a6ff;
|
|
2870
|
+
--code-string: #75e0a7;
|
|
2871
|
+
--code-number: #84adff;
|
|
2872
|
+
--code-boolean: #f9a8d4;
|
|
2873
|
+
--code-null: #98a2b3;
|
|
2874
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
2875
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
2876
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2656
2877
|
}
|
|
2657
2878
|
@media (prefers-color-scheme: dark) {
|
|
2658
2879
|
:root:not([data-theme="light"]) {
|
|
2659
2880
|
color-scheme: dark;
|
|
2660
|
-
--bg: #
|
|
2661
|
-
--bg-elev: #
|
|
2662
|
-
--bg-muted: #
|
|
2663
|
-
--bg-inset: #
|
|
2664
|
-
--line: #
|
|
2665
|
-
--line-strong: #
|
|
2666
|
-
--ink: #
|
|
2667
|
-
--muted: #
|
|
2668
|
-
--faint: #
|
|
2669
|
-
--accent: #
|
|
2670
|
-
--accent-
|
|
2671
|
-
--accent-
|
|
2672
|
-
--
|
|
2673
|
-
--ok
|
|
2674
|
-
--
|
|
2675
|
-
--warn
|
|
2676
|
-
--
|
|
2677
|
-
--err
|
|
2678
|
-
--
|
|
2881
|
+
--bg: #0c0e14;
|
|
2882
|
+
--bg-elev: #151821;
|
|
2883
|
+
--bg-muted: #20232e;
|
|
2884
|
+
--bg-inset: #11141c;
|
|
2885
|
+
--line: #2b303d;
|
|
2886
|
+
--line-strong: #414858;
|
|
2887
|
+
--ink: #f5f7fa;
|
|
2888
|
+
--muted: #b6bdc9;
|
|
2889
|
+
--faint: #8d96a7;
|
|
2890
|
+
--accent: #9e77ed;
|
|
2891
|
+
--accent-hover: #b692f6;
|
|
2892
|
+
--accent-ink: #ffffff;
|
|
2893
|
+
--accent-soft: #2b2142;
|
|
2894
|
+
--ok: #75e0a7;
|
|
2895
|
+
--ok-soft: #102a20;
|
|
2896
|
+
--warn: #fec84b;
|
|
2897
|
+
--warn-soft: #302611;
|
|
2898
|
+
--err: #fda29b;
|
|
2899
|
+
--err-soft: #321817;
|
|
2900
|
+
--code-key: #c3a6ff;
|
|
2901
|
+
--code-string: #75e0a7;
|
|
2902
|
+
--code-number: #84adff;
|
|
2903
|
+
--code-boolean: #f9a8d4;
|
|
2904
|
+
--code-null: #98a2b3;
|
|
2905
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
2906
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
2907
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2679
2908
|
}
|
|
2680
2909
|
}
|
|
2681
2910
|
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
@@ -2693,13 +2922,17 @@ var renderAdminDocument = (service) => {
|
|
|
2693
2922
|
padding-right: env(safe-area-inset-right);
|
|
2694
2923
|
-webkit-text-size-adjust: 100%;
|
|
2695
2924
|
}
|
|
2696
|
-
|
|
2925
|
+
::selection { background: color-mix(in srgb, var(--accent) 28%, transparent); }
|
|
2926
|
+
:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px; }
|
|
2927
|
+
button, input, select, textarea { transition: border-color 140ms ease, box-shadow 140ms ease, transform 100ms ease; }
|
|
2928
|
+
button:disabled, input:disabled, select:disabled, textarea:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
2697
2929
|
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
2698
2930
|
header.top {
|
|
2699
|
-
position: sticky; top: 0; z-index:
|
|
2931
|
+
position: sticky; top: 0; z-index: 20;
|
|
2700
2932
|
display: flex; align-items: center; gap: 12px;
|
|
2701
|
-
min-height: var(--header); padding:
|
|
2702
|
-
background: var(--bg-elev);
|
|
2933
|
+
min-height: var(--header); padding: 12px 20px;
|
|
2934
|
+
background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
|
|
2935
|
+
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
2703
2936
|
border-bottom: 1px solid var(--line);
|
|
2704
2937
|
min-width: 0; max-width: 100%;
|
|
2705
2938
|
}
|
|
@@ -2707,12 +2940,21 @@ var renderAdminDocument = (service) => {
|
|
|
2707
2940
|
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
2708
2941
|
min-width: 0; max-width: 100%; margin-right: auto;
|
|
2709
2942
|
}
|
|
2710
|
-
.brand-
|
|
2711
|
-
.brand-
|
|
2943
|
+
.brand-lockup { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
2944
|
+
.brand-mark {
|
|
2945
|
+
width: 38px; height: 38px; display: grid; place-items: center; flex: none;
|
|
2946
|
+
border-radius: 11px; color: #fff;
|
|
2947
|
+
background: linear-gradient(145deg, #7f56d9 0%, #53389e 100%);
|
|
2948
|
+
box-shadow: 0 6px 16px rgb(105 65 198 / 0.24), inset 0 1px 0 rgb(255 255 255 / 0.25);
|
|
2949
|
+
}
|
|
2950
|
+
.brand-mark svg { width: 22px; height: 22px; }
|
|
2951
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; line-height: 1.25; }
|
|
2952
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
2712
2953
|
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
2713
2954
|
.vendor {
|
|
2714
2955
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
2715
2956
|
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
2957
|
+
padding: 5px 8px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-inset);
|
|
2716
2958
|
}
|
|
2717
2959
|
.vendor[hidden] { display: none; }
|
|
2718
2960
|
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
@@ -2724,12 +2966,17 @@ var renderAdminDocument = (service) => {
|
|
|
2724
2966
|
.vendor-sep { color: var(--faint); }
|
|
2725
2967
|
.vendor-link { color: inherit; text-decoration: none; }
|
|
2726
2968
|
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
2727
|
-
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items:
|
|
2728
|
-
|
|
2969
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
|
|
2970
|
+
.sync-status { align-self: center; display: flex; align-items: center; gap: 6px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
2971
|
+
.sync-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
|
|
2972
|
+
.sync-status.busy::before { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); animation: pulse 1.2s ease-in-out infinite; }
|
|
2973
|
+
label.field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; font-weight: 600; color: var(--faint); }
|
|
2729
2974
|
label.field input, label.field select, .controls input[type="password"] {
|
|
2730
|
-
background-color: var(--bg-
|
|
2731
|
-
padding:
|
|
2975
|
+
background-color: var(--bg-elev); border: 1px solid var(--line-strong); border-radius: 9px;
|
|
2976
|
+
padding: 8px 10px; min-height: 38px; line-height: 1.2; box-shadow: var(--shadow-xs);
|
|
2732
2977
|
}
|
|
2978
|
+
label.field input:hover, label.field select:hover, textarea:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--line-strong)); }
|
|
2979
|
+
label.field input:focus, label.field select:focus, textarea:focus { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); outline: none; }
|
|
2733
2980
|
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
2734
2981
|
the text fields in the same form. Paint it like those fields. */
|
|
2735
2982
|
label.field select {
|
|
@@ -2746,12 +2993,20 @@ var renderAdminDocument = (service) => {
|
|
|
2746
2993
|
dialog .field > textarea { width: 100%; }
|
|
2747
2994
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
2748
2995
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
.btn
|
|
2996
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
|
2997
|
+
border-radius: 9px; min-height: 38px; padding: 0 13px; border: 1px solid var(--line-strong);
|
|
2998
|
+
background: var(--bg-elev); color: var(--ink); font-size: 13px; font-weight: 600; line-height: 1;
|
|
2999
|
+
box-shadow: var(--shadow-xs);
|
|
3000
|
+
}
|
|
3001
|
+
.icon-btn { width: 38px; padding: 0; }
|
|
3002
|
+
.icon-btn svg, .btn svg, .btn-primary svg { width: 16px; height: 16px; }
|
|
3003
|
+
.icon-btn:hover, .btn:hover { background: var(--bg-muted); border-color: var(--line-strong); }
|
|
3004
|
+
.btn-primary { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); box-shadow: 0 1px 2px rgb(16 24 40 / 0.08), 0 0 0 1px rgb(255 255 255 / 0.08) inset; }
|
|
3005
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
3006
|
+
.btn-quiet { background: transparent; border-color: transparent; box-shadow: none; }
|
|
3007
|
+
.btn-quiet:hover { background: var(--bg-muted); }
|
|
3008
|
+
.btn-danger { color: var(--err); border-color: color-mix(in srgb, var(--err) 25%, var(--line)); }
|
|
3009
|
+
.btn-danger:hover, .btn-danger.confirming { background: var(--err-soft); border-color: color-mix(in srgb, var(--err) 45%, var(--line)); }
|
|
2755
3010
|
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
2756
3011
|
so the press itself has to stay visible. */
|
|
2757
3012
|
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
@@ -2767,93 +3022,159 @@ var renderAdminDocument = (service) => {
|
|
|
2767
3022
|
flex: 1 0 auto; min-width: 0;
|
|
2768
3023
|
}
|
|
2769
3024
|
nav.side {
|
|
2770
|
-
|
|
3025
|
+
position: sticky; top: var(--header); align-self: start; height: calc(100vh - var(--header));
|
|
3026
|
+
border-right: 1px solid var(--line); padding: 18px 12px;
|
|
2771
3027
|
display: flex; flex-direction: column; gap: 4px;
|
|
2772
|
-
background: var(--bg-elev);
|
|
3028
|
+
background: var(--bg-elev); overflow-y: auto;
|
|
2773
3029
|
}
|
|
3030
|
+
.nav-label { padding: 0 10px 8px; color: var(--faint); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
|
|
2774
3031
|
nav.side button {
|
|
3032
|
+
position: relative; display: flex; align-items: center; gap: 10px; width: 100%;
|
|
2775
3033
|
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
2776
|
-
padding: 9px 10px; min-height:
|
|
2777
|
-
}
|
|
2778
|
-
nav.side button
|
|
2779
|
-
|
|
2780
|
-
.
|
|
2781
|
-
.
|
|
2782
|
-
.
|
|
3034
|
+
padding: 9px 10px; min-height: 42px; color: var(--muted); font-size: 13px; font-weight: 550;
|
|
3035
|
+
}
|
|
3036
|
+
nav.side button:hover { background: var(--bg-muted); color: var(--ink); }
|
|
3037
|
+
nav.side button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 650; }
|
|
3038
|
+
nav.side button[aria-current="true"]::before { content: ""; position: absolute; left: -12px; width: 3px; height: 22px; border-radius: 0 4px 4px 0; background: var(--accent); }
|
|
3039
|
+
.nav-icon { width: 18px; height: 18px; display: grid; place-items: center; flex: none; }
|
|
3040
|
+
.nav-icon svg { width: 18px; height: 18px; }
|
|
3041
|
+
main { padding: 32px clamp(20px, 4vw, 48px) 64px; max-width: 1320px; width: 100%; min-width: 0; }
|
|
3042
|
+
main[aria-busy="true"] .view { opacity: 0.62; }
|
|
3043
|
+
.view { animation: view-in 180ms ease-out; }
|
|
3044
|
+
.view[hidden] { display: none; }
|
|
3045
|
+
.view-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
|
|
3046
|
+
.view h2 { margin: 0 0 5px; font-size: clamp(24px, 3vw, 30px); line-height: 1.2; letter-spacing: -0.035em; }
|
|
3047
|
+
.view h3 { margin: 24px 0 10px; font-size: 15px; letter-spacing: -0.01em; }
|
|
3048
|
+
.lede { margin: 0; color: var(--muted); max-width: 72ch; font-size: 14px; }
|
|
3049
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 175px), 1fr)); gap: 14px; }
|
|
2783
3050
|
.card {
|
|
2784
3051
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
2785
|
-
padding:
|
|
3052
|
+
padding: 18px; box-shadow: var(--shadow-sm);
|
|
2786
3053
|
}
|
|
2787
|
-
.card
|
|
2788
|
-
.card
|
|
3054
|
+
.stat-card { position: relative; overflow: hidden; }
|
|
3055
|
+
.stat-card::after { content: ""; position: absolute; inset: 0 0 auto; height: 2px; background: linear-gradient(90deg, var(--accent), transparent 78%); opacity: 0.7; }
|
|
3056
|
+
.card .k { color: var(--faint); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
3057
|
+
.card .v { display: flex; flex-direction: column; gap: 2px; font-size: 24px; font-weight: 650; margin-top: 7px; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; letter-spacing: -0.025em; }
|
|
2789
3058
|
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
2790
|
-
.
|
|
3059
|
+
.section-card { background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-xs); overflow: hidden; }
|
|
3060
|
+
.section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
|
|
3061
|
+
.section-title h3 { margin: 0; }
|
|
3062
|
+
.split { display: grid; grid-template-columns: minmax(0, 250px) minmax(0, 1fr); gap: 18px; align-items: start; }
|
|
3063
|
+
#collections { padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); box-shadow: var(--shadow-xs); }
|
|
2791
3064
|
.collection {
|
|
2792
3065
|
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
2793
|
-
border-radius:
|
|
3066
|
+
border-radius: 9px; padding: 10px 11px; min-height: 48px; color: var(--ink); cursor: pointer;
|
|
2794
3067
|
}
|
|
2795
|
-
.collection
|
|
3068
|
+
.collection:hover { background: var(--bg-muted); }
|
|
3069
|
+
.collection[aria-current="true"] { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 18%, var(--line)); color: var(--accent); }
|
|
2796
3070
|
.collection small { display: block; color: var(--faint); }
|
|
2797
3071
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
2798
|
-
.chip {
|
|
2799
|
-
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
2800
|
-
border-radius: 999px; padding: 4px 8px;
|
|
2801
|
-
}
|
|
3072
|
+
.chip { font-family: var(--mono); font-size: 11px; background: var(--bg-muted); border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; }
|
|
2802
3073
|
.chip i { color: var(--faint); font-style: normal; }
|
|
2803
3074
|
.sql-layout {
|
|
2804
3075
|
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
2805
3076
|
}
|
|
2806
|
-
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
2807
|
-
.sql-tables button { text-align: left; }
|
|
3077
|
+
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); }
|
|
3078
|
+
.sql-tables button { text-align: left; border: 0; border-radius: 8px; padding: 9px 10px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
3079
|
+
.sql-tables button:hover, .sql-tables button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); }
|
|
2808
3080
|
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
2809
|
-
table { width: 100%; border-
|
|
2810
|
-
|
|
2811
|
-
th {
|
|
3081
|
+
.table-shell { width: 100%; overflow-x: auto; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); box-shadow: var(--shadow-xs); }
|
|
3082
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
3083
|
+
th, td { text-align: left; padding: 11px 13px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
3084
|
+
th { color: var(--faint); background: var(--bg-inset); font-weight: 650; font-size: 11px; text-transform: uppercase; letter-spacing: 0.045em; }
|
|
3085
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
2812
3086
|
tr.clickable { cursor: pointer; }
|
|
2813
|
-
tr.clickable:hover { background: var(--
|
|
3087
|
+
tr.clickable:hover { background: var(--accent-soft); }
|
|
2814
3088
|
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
2815
3089
|
pre {
|
|
2816
3090
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
2817
|
-
background: var(--bg-inset); border-radius: 10px; padding:
|
|
2818
|
-
}
|
|
2819
|
-
.
|
|
3091
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 10px; padding: 14px; font-size: 12px; line-height: 1.65;
|
|
3092
|
+
}
|
|
3093
|
+
.json-key { color: var(--code-key); }
|
|
3094
|
+
.json-string { color: var(--code-string); }
|
|
3095
|
+
.json-number { color: var(--code-number); }
|
|
3096
|
+
.json-boolean { color: var(--code-boolean); }
|
|
3097
|
+
.json-null { color: var(--code-null); font-style: italic; }
|
|
3098
|
+
.json-inline { display: block; max-width: 44rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
3099
|
+
.json-view { position: relative; max-height: 26rem; overflow: auto; }
|
|
3100
|
+
.json-editor { position: relative; min-height: 190px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--bg-inset); overflow: hidden; }
|
|
3101
|
+
.json-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); }
|
|
3102
|
+
.json-editor.invalid { border-color: var(--err); }
|
|
3103
|
+
.json-editor pre, .json-editor textarea { margin: 0; padding: 12px; border: 0; border-radius: 0; min-height: 190px; width: 100%; font: 12px/1.65 var(--mono); tab-size: 2; white-space: pre; overflow: auto; }
|
|
3104
|
+
.json-editor pre { position: absolute; inset: 0; pointer-events: none; background: transparent; }
|
|
3105
|
+
.json-editor textarea { position: relative; z-index: 1; resize: vertical; background: transparent; color: transparent; caret-color: var(--ink); -webkit-text-fill-color: transparent; }
|
|
3106
|
+
.json-editor textarea::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); }
|
|
3107
|
+
.editor-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; }
|
|
3108
|
+
.json-status { display: inline-flex; align-items: center; gap: 6px; color: var(--ok); font-size: 11px; }
|
|
3109
|
+
.json-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
3110
|
+
.json-status.invalid { color: var(--err); }
|
|
3111
|
+
.format-json { border: 0; padding: 3px 5px; background: transparent; color: var(--accent); font-size: 11px; font-weight: 650; cursor: pointer; }
|
|
3112
|
+
.row-actions { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 14px 0; }
|
|
2820
3113
|
.banner {
|
|
2821
|
-
display: none;
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
3114
|
+
display: none; position: sticky; top: calc(var(--header) + 12px); z-index: 15;
|
|
3115
|
+
align-items: center; gap: 10px; margin: 0 0 16px; padding: 11px 12px 11px 14px;
|
|
3116
|
+
border: 1px solid color-mix(in srgb, var(--err) 30%, var(--line)); border-radius: 10px;
|
|
3117
|
+
background: var(--err-soft); color: var(--err); box-shadow: var(--shadow-sm); font-size: 13px;
|
|
3118
|
+
}
|
|
3119
|
+
.banner.show { display: flex; }
|
|
3120
|
+
.banner span { flex: 1; }
|
|
3121
|
+
.banner button { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 7px; background: transparent; color: currentColor; cursor: pointer; }
|
|
3122
|
+
.banner button:hover { background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
3123
|
+
.empty { color: var(--muted); padding: 34px 18px; text-align: center; border: 1px dashed var(--line-strong); border-radius: var(--radius); background: color-mix(in srgb, var(--bg-elev) 55%, transparent); }
|
|
2826
3124
|
dialog {
|
|
2827
|
-
border: 1px solid var(--line); border-radius:
|
|
2828
|
-
color: var(--ink); width: min(
|
|
2829
|
-
}
|
|
2830
|
-
dialog
|
|
2831
|
-
dialog
|
|
3125
|
+
border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--bg-elev);
|
|
3126
|
+
color: var(--ink); width: min(720px, calc(100% - 24px)); max-width: calc(100vw - 24px); box-shadow: var(--shadow-lg);
|
|
3127
|
+
}
|
|
3128
|
+
dialog[open] { animation: dialog-in 160ms ease-out; }
|
|
3129
|
+
dialog::backdrop { background: rgb(12 14 20 / 0.58); backdrop-filter: blur(3px); }
|
|
3130
|
+
dialog form, .dialog-body { padding: 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
3131
|
+
.dialog-heading { display: flex; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
|
3132
|
+
.dialog-heading strong { font-size: 18px; letter-spacing: -0.02em; }
|
|
3133
|
+
.dialog-heading span { color: var(--muted); font-size: 12px; }
|
|
2832
3134
|
textarea {
|
|
2833
3135
|
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
2834
3136
|
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
2835
3137
|
font-family: var(--mono); font-size: 12px;
|
|
2836
3138
|
}
|
|
2837
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
2838
|
-
.
|
|
3139
|
+
.dialog-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding-top: 4px; }
|
|
3140
|
+
.dialog-actions .btn-danger { margin-right: auto; }
|
|
3141
|
+
.badge { display: inline-flex; align-items: center; font-size: 10px; font-weight: 700; letter-spacing: 0.03em; border: 1px solid var(--line); border-radius: 999px; padding: 3px 7px; background: var(--bg-muted); color: var(--muted); }
|
|
2839
3142
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
2840
3143
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
2841
3144
|
.status-2 { color: var(--ok); }
|
|
2842
3145
|
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
2843
3146
|
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
3147
|
+
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
3148
|
+
.form-grid .wide { grid-column: span 2; }
|
|
2844
3149
|
.route {
|
|
2845
|
-
display: grid; grid-template-columns:
|
|
2846
|
-
padding:
|
|
2847
|
-
}
|
|
2848
|
-
.
|
|
3150
|
+
display: grid; grid-template-columns: 84px minmax(0, 1fr) auto; gap: 10px; align-items: center;
|
|
3151
|
+
padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--bg-elev);
|
|
3152
|
+
}
|
|
3153
|
+
.route:first-child { border-radius: var(--radius) var(--radius) 0 0; }
|
|
3154
|
+
.route:last-child { border-bottom: 0; border-radius: 0 0 var(--radius) var(--radius); }
|
|
3155
|
+
.route-tools { display: flex; align-items: flex-end; gap: 12px; margin-bottom: 12px; }
|
|
3156
|
+
.route-tools .field { flex: 1; }
|
|
3157
|
+
.route-tools input { width: 100%; }
|
|
3158
|
+
.route-count { padding-bottom: 10px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3159
|
+
#route-list { max-height: 34rem; border: 1px solid var(--line); border-radius: var(--radius); overflow: auto; box-shadow: var(--shadow-xs); }
|
|
3160
|
+
.method { width: fit-content; font-family: var(--mono); font-size: 11px; font-weight: 750; color: var(--accent); background: var(--accent-soft); border-radius: 6px; padding: 4px 6px; }
|
|
3161
|
+
@keyframes view-in { from { transform: translateY(3px); } to { transform: translateY(0); } }
|
|
3162
|
+
@keyframes dialog-in { from { transform: translateY(8px) scale(0.99); } to { transform: translateY(0) scale(1); } }
|
|
3163
|
+
@keyframes pulse { 50% { opacity: 0.45; } }
|
|
3164
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
|
|
3165
|
+
@media (forced-colors: active) { .json-editor pre { display: none; } .json-editor textarea { color: CanvasText; -webkit-text-fill-color: CanvasText; } }
|
|
2849
3166
|
@media (max-width: 800px) {
|
|
3167
|
+
:root { --header: 0px; }
|
|
2850
3168
|
header.top {
|
|
2851
|
-
align-items: stretch; flex-direction: column;
|
|
2852
|
-
padding
|
|
3169
|
+
position: relative; align-items: stretch; flex-direction: column;
|
|
3170
|
+
padding: max(12px, env(safe-area-inset-top)) 14px 12px;
|
|
2853
3171
|
}
|
|
2854
3172
|
input, select, textarea { font-size: 16px; }
|
|
2855
3173
|
.brand, .vendor, .controls { width: 100%; }
|
|
3174
|
+
.brand { gap: 10px; }
|
|
3175
|
+
.vendor { order: 2; border-radius: 9px; }
|
|
2856
3176
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
3177
|
+
.sync-status { display: none; }
|
|
2857
3178
|
/* The layout fills the screen. A single-column grid stretches every auto
|
|
2858
3179
|
row, so the tab bar grew with the leftover height and the buttons floated
|
|
2859
3180
|
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
@@ -2863,14 +3184,23 @@ var renderAdminDocument = (service) => {
|
|
|
2863
3184
|
align-content: start;
|
|
2864
3185
|
}
|
|
2865
3186
|
nav.side {
|
|
2866
|
-
align-self: start; height: auto;
|
|
3187
|
+
position: sticky; top: 0; z-index: 12; align-self: start; height: auto;
|
|
2867
3188
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
2868
3189
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
2869
|
-
padding: 8px;
|
|
3190
|
+
padding: 8px; box-shadow: var(--shadow-xs);
|
|
2870
3191
|
}
|
|
2871
|
-
nav
|
|
3192
|
+
.nav-label { display: none; }
|
|
3193
|
+
nav.side button { width: auto; white-space: nowrap; flex: none; }
|
|
3194
|
+
nav.side button[aria-current="true"]::before { inset: auto 9px -8px; width: auto; height: 3px; border-radius: 3px 3px 0 0; }
|
|
2872
3195
|
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
2873
|
-
|
|
3196
|
+
.form-grid { grid-template-columns: minmax(0, 1fr); }
|
|
3197
|
+
.form-grid .wide { grid-column: auto; }
|
|
3198
|
+
main { padding: 22px 14px 48px; }
|
|
3199
|
+
.view-head { margin-bottom: 18px; }
|
|
3200
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
3201
|
+
.card { padding: 14px; }
|
|
3202
|
+
.card .v { font-size: 20px; }
|
|
3203
|
+
.table-shell { border: 0; background: transparent; box-shadow: none; overflow: visible; }
|
|
2874
3204
|
table, thead, tbody, tr, th, td { display: block; }
|
|
2875
3205
|
thead { display: none; }
|
|
2876
3206
|
tr {
|
|
@@ -2885,50 +3215,63 @@ var renderAdminDocument = (service) => {
|
|
|
2885
3215
|
.route { grid-template-columns: 72px 1fr; }
|
|
2886
3216
|
.route button { grid-column: 1 / -1; }
|
|
2887
3217
|
}
|
|
3218
|
+
@media (max-width: 460px) {
|
|
3219
|
+
.controls label.field { min-width: calc(50% - 4px); }
|
|
3220
|
+
.controls .icon-btn, .controls .btn { flex: 1; }
|
|
3221
|
+
.grid { grid-template-columns: minmax(0, 1fr); }
|
|
3222
|
+
.dialog-actions { flex-wrap: wrap; }
|
|
3223
|
+
.dialog-actions button { flex: 1; }
|
|
3224
|
+
.dialog-actions .btn-danger { flex-basis: 100%; margin-right: 0; }
|
|
3225
|
+
}
|
|
2888
3226
|
</style>
|
|
2889
3227
|
</head>
|
|
2890
3228
|
<body>
|
|
2891
3229
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
2892
3230
|
<header class="top">
|
|
2893
3231
|
<div class="brand">
|
|
2894
|
-
<div class="brand-
|
|
2895
|
-
<
|
|
2896
|
-
|
|
3232
|
+
<div class="brand-lockup">
|
|
3233
|
+
<div class="brand-mark" aria-hidden="true">
|
|
3234
|
+
<svg viewBox="0 0 24 24" fill="none"><path d="M5 17.5V7l7 5 7-5v10.5" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/><path d="M8 15.5 12 18l4-2.5" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
3235
|
+
</div>
|
|
3236
|
+
<div class="brand-id">
|
|
3237
|
+
<strong>${name}</strong>
|
|
3238
|
+
<span>Mock admin</span>
|
|
3239
|
+
</div>
|
|
2897
3240
|
</div>
|
|
2898
3241
|
<div class="vendor" id="vendor" hidden></div>
|
|
2899
3242
|
</div>
|
|
2900
3243
|
<div class="controls">
|
|
3244
|
+
<span class="sync-status" id="sync-status" role="status" aria-live="polite">Ready</span>
|
|
2901
3245
|
<label class="field">Namespace
|
|
2902
3246
|
<select id="namespace" aria-label="Namespace"></select>
|
|
2903
3247
|
</label>
|
|
2904
3248
|
<label class="field">Admin key
|
|
2905
3249
|
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
2906
3250
|
</label>
|
|
2907
|
-
<button class="icon-btn" id="theme" type="button" aria-label="
|
|
2908
|
-
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
3251
|
+
<button class="icon-btn" id="theme" type="button" aria-label="Use dark theme" title="Use dark theme"></button>
|
|
3252
|
+
<button class="btn" id="refresh" type="button"><svg viewBox="0 0 20 20" fill="none" aria-hidden="true"><path d="M15.8 7.2A6.2 6.2 0 1 0 16 12" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/><path d="M12.8 7.2h3v-3" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>Refresh</button>
|
|
2909
3253
|
</div>
|
|
2910
3254
|
</header>
|
|
2911
3255
|
<div class="layout">
|
|
2912
3256
|
<nav class="side" id="nav" aria-label="Admin sections">
|
|
2913
|
-
<
|
|
2914
|
-
<button type="button" data-view="
|
|
2915
|
-
<button type="button" data-view="
|
|
2916
|
-
<button type="button" data-view="
|
|
2917
|
-
<button type="button" data-view="
|
|
2918
|
-
<button type="button" data-view="
|
|
3257
|
+
<span class="nav-label">Workspace</span>
|
|
3258
|
+
<button type="button" data-view="overview" aria-current="true"><span class="nav-icon" aria-hidden="true"><svg viewBox="0 0 20 20" fill="none"><rect x="2.5" y="2.5" width="6" height="6" rx="1.5" stroke="currentColor" stroke-width="1.5"/><rect x="11.5" y="2.5" width="6" height="6" rx="1.5" stroke="currentColor" stroke-width="1.5"/><rect x="2.5" y="11.5" width="6" height="6" rx="1.5" stroke="currentColor" stroke-width="1.5"/><rect x="11.5" y="11.5" width="6" height="6" rx="1.5" stroke="currentColor" stroke-width="1.5"/></svg></span>Overview</button>
|
|
3259
|
+
<button type="button" data-view="state"><span class="nav-icon" aria-hidden="true"><svg viewBox="0 0 20 20" fill="none"><ellipse cx="10" cy="5" rx="6.5" ry="2.5" stroke="currentColor" stroke-width="1.5"/><path d="M3.5 5v5c0 1.4 2.9 2.5 6.5 2.5s6.5-1.1 6.5-2.5V5M3.5 10v5c0 1.4 2.9 2.5 6.5 2.5s6.5-1.1 6.5-2.5v-5" stroke="currentColor" stroke-width="1.5"/></svg></span>State</button>
|
|
3260
|
+
<button type="button" data-view="clock"><span class="nav-icon" aria-hidden="true"><svg viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="7.5" stroke="currentColor" stroke-width="1.5"/><path d="M10 5.8v4.6l3 1.8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></span>Clock</button>
|
|
3261
|
+
<button type="button" data-view="faults"><span class="nav-icon" aria-hidden="true"><svg viewBox="0 0 20 20" fill="none"><path d="m11.2 2.8-6.5 9h5.1l-1 5.4 6.5-9h-5.1l1-5.4Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg></span>Faults</button>
|
|
3262
|
+
<button type="button" data-view="journal"><span class="nav-icon" aria-hidden="true"><svg viewBox="0 0 20 20" fill="none"><path d="M5 3.5h10a1.5 1.5 0 0 1 1.5 1.5v12l-2.2-1.4-2.2 1.4-2.1-1.4L7.8 17l-2.2-1.4L3.5 17V5A1.5 1.5 0 0 1 5 3.5Z" stroke="currentColor" stroke-width="1.5"/><path d="M6.7 7h6.6M6.7 10h6.6M6.7 13h3.8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></span>Journal</button>
|
|
3263
|
+
<button type="button" data-view="routes"><span class="nav-icon" aria-hidden="true"><svg viewBox="0 0 20 20" fill="none"><path d="M6.5 4.5h7a2 2 0 0 1 2 2v7M13 11l2.5 2.5L18 11M13.5 15.5h-7a2 2 0 0 1-2-2v-7M7 9 4.5 6.5 2 9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></span>Routes</button>
|
|
2919
3264
|
</nav>
|
|
2920
3265
|
<main id="views">
|
|
2921
|
-
<
|
|
3266
|
+
<div class="banner" id="banner" role="alert"><span id="banner-message"></span><button id="banner-dismiss" type="button" aria-label="Dismiss error">\xD7</button></div>
|
|
2922
3267
|
<section class="view" id="view-overview">
|
|
2923
|
-
<h2>Overview</h2>
|
|
2924
|
-
<p class="lede">Health, clock, and the collections this namespace is holding.</p>
|
|
3268
|
+
<div class="view-head"><div><h2>Overview</h2><p class="lede">Health, clock, and the collections this namespace is holding.</p></div></div>
|
|
2925
3269
|
<div class="grid" id="overview-cards"></div>
|
|
2926
3270
|
<h3>Recent requests</h3>
|
|
2927
3271
|
<div id="overview-requests"></div>
|
|
2928
3272
|
</section>
|
|
2929
3273
|
<section class="view" id="view-state" hidden>
|
|
2930
|
-
<h2>State</h2>
|
|
2931
|
-
<p class="lede">Every collection in this namespace. Declared fields stay visible before any row exists; stored rows add whatever else they contain.</p>
|
|
3274
|
+
<div class="view-head"><div><h2>State</h2><p class="lede">Inspect and edit every collection in this namespace.</p></div></div>
|
|
2932
3275
|
<div class="split">
|
|
2933
3276
|
<div id="collections"></div>
|
|
2934
3277
|
<div>
|
|
@@ -2941,8 +3284,7 @@ var renderAdminDocument = (service) => {
|
|
|
2941
3284
|
</div>
|
|
2942
3285
|
</section>
|
|
2943
3286
|
<section class="view" id="view-clock" hidden>
|
|
2944
|
-
<h2>Clock</h2>
|
|
2945
|
-
<p class="lede">The clock every timestamp in this mock reads.</p>
|
|
3287
|
+
<div class="view-head"><div><h2>Clock</h2><p class="lede">Control the source of time used by every timestamp in this mock.</p></div></div>
|
|
2946
3288
|
<div class="card" id="clock-readout"></div>
|
|
2947
3289
|
<div class="row-actions">
|
|
2948
3290
|
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
@@ -2961,21 +3303,22 @@ var renderAdminDocument = (service) => {
|
|
|
2961
3303
|
</form>
|
|
2962
3304
|
</section>
|
|
2963
3305
|
<section class="view" id="view-faults" hidden>
|
|
2964
|
-
<h2>Faults</h2>
|
|
2965
|
-
<p class="lede">Inject a response, a delay, or a named preset. Rules apply to this namespace.</p>
|
|
3306
|
+
<div class="view-head"><div><h2>Faults</h2><p class="lede">Test unhappy paths with a response, delay, or named preset scoped to this namespace.</p></div></div>
|
|
2966
3307
|
<form id="fault-form" class="card stack">
|
|
2967
|
-
<
|
|
2968
|
-
<
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
<
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
<
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
<
|
|
2978
|
-
|
|
3308
|
+
<div class="form-grid">
|
|
3309
|
+
<label class="field wide">Preset
|
|
3310
|
+
<select id="fault-preset"><option value="">Custom rule</option></select>
|
|
3311
|
+
</label>
|
|
3312
|
+
<label class="field">Status
|
|
3313
|
+
<input id="fault-status" inputmode="numeric" placeholder="503">
|
|
3314
|
+
</label>
|
|
3315
|
+
<label class="field wide">Path prefix
|
|
3316
|
+
<input id="fault-path" placeholder="/v1">
|
|
3317
|
+
</label>
|
|
3318
|
+
<label class="field">Method
|
|
3319
|
+
<input id="fault-method" placeholder="GET">
|
|
3320
|
+
</label>
|
|
3321
|
+
</div>
|
|
2979
3322
|
<div class="dialog-actions">
|
|
2980
3323
|
<button class="btn-primary" type="submit">Add fault</button>
|
|
2981
3324
|
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
@@ -2984,26 +3327,31 @@ var renderAdminDocument = (service) => {
|
|
|
2984
3327
|
<div id="fault-list"></div>
|
|
2985
3328
|
</section>
|
|
2986
3329
|
<section class="view" id="view-journal" hidden>
|
|
2987
|
-
<h2>Journal</h2>
|
|
2988
|
-
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
2989
|
-
<div class="row-actions">
|
|
2990
|
-
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
2991
|
-
</div>
|
|
3330
|
+
<div class="view-head"><div><h2>Journal</h2><p class="lede">Requests this namespace has seen. Bodies are never stored.</p></div><button class="btn-danger" id="journal-clear" type="button">Clear journal</button></div>
|
|
2992
3331
|
<div id="journal"></div>
|
|
2993
3332
|
</section>
|
|
2994
3333
|
<section class="view" id="view-routes" hidden>
|
|
2995
|
-
<h2>Routes</h2>
|
|
2996
|
-
<
|
|
3334
|
+
<div class="view-head"><div><h2>Routes</h2><p class="lede">Explore shared controls and service-specific admin endpoints.</p></div></div>
|
|
3335
|
+
<div class="route-tools">
|
|
3336
|
+
<label class="field">Filter routes
|
|
3337
|
+
<input id="route-search" type="search" placeholder="Method, path, or extension" autocomplete="off">
|
|
3338
|
+
</label>
|
|
3339
|
+
<span class="route-count" id="route-count"></span>
|
|
3340
|
+
</div>
|
|
2997
3341
|
<div id="route-list"></div>
|
|
2998
|
-
<form id="route-form" class="card stack">
|
|
3342
|
+
<form id="route-form" class="card stack" style="margin-top: 18px">
|
|
2999
3343
|
<strong id="route-title">Select a route</strong>
|
|
3000
3344
|
<label class="field">JSON body
|
|
3001
|
-
<
|
|
3345
|
+
<div class="json-editor" id="route-editor" hidden>
|
|
3346
|
+
<pre aria-hidden="true"><code id="route-highlight"></code></pre>
|
|
3347
|
+
<textarea id="route-body" placeholder="{ }" spellcheck="false" aria-describedby="route-json-status" disabled></textarea>
|
|
3348
|
+
</div>
|
|
3349
|
+
<span class="editor-meta" hidden><span class="json-status" id="route-json-status">Valid JSON</span><button class="format-json" type="button" data-format="route-body">Format JSON</button></span>
|
|
3002
3350
|
</label>
|
|
3003
3351
|
<div class="dialog-actions">
|
|
3004
|
-
<button class="btn-primary" type="submit">Send</button>
|
|
3352
|
+
<button class="btn-primary" id="route-send" type="submit" disabled>Send request</button>
|
|
3005
3353
|
</div>
|
|
3006
|
-
<pre id="route-result">Response will show here.</pre>
|
|
3354
|
+
<pre class="json-view" id="route-result"><code>Response will show here.</code></pre>
|
|
3007
3355
|
</form>
|
|
3008
3356
|
</section>
|
|
3009
3357
|
</main>
|
|
@@ -3011,12 +3359,16 @@ var renderAdminDocument = (service) => {
|
|
|
3011
3359
|
</div>
|
|
3012
3360
|
<dialog id="editor">
|
|
3013
3361
|
<form method="dialog" id="editor-form">
|
|
3014
|
-
<strong id="editor-title">Record</strong>
|
|
3362
|
+
<div class="dialog-heading"><div><strong id="editor-title">Record</strong><br><span>Edit the ID and JSON value stored in this collection.</span></div></div>
|
|
3015
3363
|
<label class="field">Id
|
|
3016
3364
|
<input id="editor-id" required>
|
|
3017
3365
|
</label>
|
|
3018
3366
|
<label class="field">JSON value
|
|
3019
|
-
<
|
|
3367
|
+
<div class="json-editor" id="record-editor">
|
|
3368
|
+
<pre aria-hidden="true"><code id="editor-highlight"></code></pre>
|
|
3369
|
+
<textarea id="editor-value" spellcheck="false" aria-describedby="editor-json-status"></textarea>
|
|
3370
|
+
</div>
|
|
3371
|
+
<span class="editor-meta"><span class="json-status" id="editor-json-status">Valid JSON</span><button class="format-json" type="button" data-format="editor-value">Format JSON</button></span>
|
|
3020
3372
|
</label>
|
|
3021
3373
|
<div class="dialog-actions">
|
|
3022
3374
|
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
@@ -3058,8 +3410,8 @@ var describeExtension = (extension) => {
|
|
|
3058
3410
|
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
3059
3411
|
};
|
|
3060
3412
|
};
|
|
3061
|
-
var adminUiRoutes = (service, ui) => {
|
|
3062
|
-
const shell = () => renderAdminDocument(service);
|
|
3413
|
+
var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
3414
|
+
const shell = () => renderAdminDocument(service, adminPrefix);
|
|
3063
3415
|
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
3064
3416
|
return {
|
|
3065
3417
|
"GET /ui": () => htmlResponse(document3()),
|
|
@@ -3980,7 +4332,7 @@ var stateAdminRoutes = (routes) => {
|
|
|
3980
4332
|
};
|
|
3981
4333
|
|
|
3982
4334
|
// ../core/dist/version.js
|
|
3983
|
-
var PACKAGE_VERSION = true ? "
|
|
4335
|
+
var PACKAGE_VERSION = true ? "2.0.0" : UNRELEASED_VERSION;
|
|
3984
4336
|
|
|
3985
4337
|
// ../core/dist/signing.js
|
|
3986
4338
|
var encoder = new TextEncoder();
|
|
@@ -4353,7 +4705,10 @@ var createWebhookHub = (options) => {
|
|
|
4353
4705
|
};
|
|
4354
4706
|
return hub;
|
|
4355
4707
|
};
|
|
4356
|
-
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4708
|
+
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4709
|
+
status,
|
|
4710
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4711
|
+
});
|
|
4357
4712
|
var adminError3 = (status, message) => json4(status, { error: { type: "mockingbird_admin", message } });
|
|
4358
4713
|
var isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4359
4714
|
var parseEndpoint = (value) => {
|
|
@@ -4461,7 +4816,6 @@ var AT_HEADER = "x-mockingbird-at";
|
|
|
4461
4816
|
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
4462
4817
|
var DEFAULT_NAMESPACE = "default";
|
|
4463
4818
|
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4464
|
-
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
4465
4819
|
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4466
4820
|
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
4467
4821
|
var effects = /* @__PURE__ */ new WeakMap();
|
|
@@ -4500,6 +4854,22 @@ var operationMatcher = (document3) => {
|
|
|
4500
4854
|
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
4501
4855
|
};
|
|
4502
4856
|
var createRuntime = (options) => {
|
|
4857
|
+
const adminPrefix = resolveAdminPrefix(options.adminPrefix);
|
|
4858
|
+
const internalPaths = Object.entries(options.document?.paths ?? {}).filter(([, item]) => item["x-mockingbird-internal"] === true).map(([path]) => path);
|
|
4859
|
+
for (const path of internalPaths) {
|
|
4860
|
+
if (!path.startsWith(`${ADMIN_PREFIX}/blobs/`))
|
|
4861
|
+
throw new Error(`internal transport ${path} must live under ${ADMIN_PREFIX}/blobs/`);
|
|
4862
|
+
}
|
|
4863
|
+
assertAdminPrefixAvailable(adminPrefix, Object.keys(options.document?.paths ?? {}).filter((path) => !internalPaths.includes(path)));
|
|
4864
|
+
const internalMatch = (path) => {
|
|
4865
|
+
for (const pattern of internalPaths) {
|
|
4866
|
+
const actual = path.split("/");
|
|
4867
|
+
const expected = pattern.split("/");
|
|
4868
|
+
if (actual.length === expected.length && expected.every((part, i) => part.startsWith("{") || part === actual[i]))
|
|
4869
|
+
return pattern;
|
|
4870
|
+
}
|
|
4871
|
+
return void 0;
|
|
4872
|
+
};
|
|
4503
4873
|
assertAdminUi(options.adminUi);
|
|
4504
4874
|
const sqlite = bootSqlite(options.sqlite);
|
|
4505
4875
|
const clock = options.clock ?? createClock();
|
|
@@ -4545,6 +4915,7 @@ var createRuntime = (options) => {
|
|
|
4545
4915
|
const created = options.create({
|
|
4546
4916
|
namespace: storageNamespace(key),
|
|
4547
4917
|
publicNamespace,
|
|
4918
|
+
adminPrefix,
|
|
4548
4919
|
sqlite,
|
|
4549
4920
|
clock: namespaceClock(publicNamespace),
|
|
4550
4921
|
rng: createdContextRng
|
|
@@ -4777,10 +5148,10 @@ var createRuntime = (options) => {
|
|
|
4777
5148
|
namespaceClock,
|
|
4778
5149
|
namespaceOf(request) {
|
|
4779
5150
|
const url = new URL(request.url);
|
|
4780
|
-
const prefixed =
|
|
5151
|
+
const prefixed = matchNamespacePath(url.pathname, adminPrefix);
|
|
4781
5152
|
const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
|
|
4782
5153
|
const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
|
|
4783
|
-
if (path
|
|
5154
|
+
if (isAdminPath(path, adminPrefix))
|
|
4784
5155
|
return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
|
|
4785
5156
|
if (selected !== void 0)
|
|
4786
5157
|
return selected;
|
|
@@ -4846,7 +5217,7 @@ var createRuntime = (options) => {
|
|
|
4846
5217
|
state: (namespace = DEFAULT_NAMESPACE) => inspectState(stateScope(namespace)),
|
|
4847
5218
|
fetch: async (incoming) => {
|
|
4848
5219
|
let request = incoming;
|
|
4849
|
-
const prefixed =
|
|
5220
|
+
const prefixed = matchNamespacePath(new URL(request.url).pathname, adminPrefix);
|
|
4850
5221
|
if (prefixed) {
|
|
4851
5222
|
const url2 = new URL(request.url);
|
|
4852
5223
|
url2.pathname = prefixed[2] ?? "/";
|
|
@@ -4862,7 +5233,15 @@ var createRuntime = (options) => {
|
|
|
4862
5233
|
signal: request.signal
|
|
4863
5234
|
});
|
|
4864
5235
|
}
|
|
4865
|
-
|
|
5236
|
+
const internalUrl = new URL(request.url);
|
|
5237
|
+
const internalPath = isAdminPath(internalUrl.pathname, adminPrefix) ? `${ADMIN_PREFIX}${internalUrl.pathname.slice(adminPrefix.length)}` : "";
|
|
5238
|
+
const internal = internalMatch(internalPath);
|
|
5239
|
+
const obsoleteInternalPath = adminPrefix !== ADMIN_PREFIX && internalMatch(internalUrl.pathname) && !internal;
|
|
5240
|
+
if (internal) {
|
|
5241
|
+
internalUrl.pathname = internalPath;
|
|
5242
|
+
request = new Request(internalUrl, request);
|
|
5243
|
+
}
|
|
5244
|
+
let namespace = internal ? internalUrl.searchParams.get("namespace") ?? control.namespaceOf(request) : control.namespaceOf(request);
|
|
4866
5245
|
if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
4867
5246
|
const credential = options.credential(request);
|
|
4868
5247
|
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
@@ -4882,7 +5261,9 @@ var createRuntime = (options) => {
|
|
|
4882
5261
|
return copy;
|
|
4883
5262
|
}
|
|
4884
5263
|
};
|
|
4885
|
-
|
|
5264
|
+
if (obsoleteInternalPath)
|
|
5265
|
+
return stamp(adminFail(404, "internal transport is only available under adminPrefix"));
|
|
5266
|
+
const handled = internal ? void 0 : await control.handle(request);
|
|
4886
5267
|
if (handled)
|
|
4887
5268
|
return stamp(handled);
|
|
4888
5269
|
const started = monotonicNow();
|
|
@@ -4923,7 +5304,7 @@ var createRuntime = (options) => {
|
|
|
4923
5304
|
type: "mockingbird_admin",
|
|
4924
5305
|
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
4925
5306
|
}
|
|
4926
|
-
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
5307
|
+
}), { status: 400, headers: { "content-type": "application/json; charset=utf-8" } }));
|
|
4927
5308
|
}
|
|
4928
5309
|
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
4929
5310
|
log(400);
|
|
@@ -5040,15 +5421,16 @@ var createRuntime = (options) => {
|
|
|
5040
5421
|
...options.admin?.(runtime) ?? {},
|
|
5041
5422
|
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
5042
5423
|
// maps `presets`, and a service route must not replace that list with a record.
|
|
5043
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
5424
|
+
...options.presets ? presetRoutes(options.presets, runtime, adminPrefix) : {},
|
|
5044
5425
|
...stateAdminRoutes({
|
|
5045
5426
|
open: stateScope,
|
|
5046
5427
|
afterWrite: (namespace) => {
|
|
5047
5428
|
checkpoint(namespace, "main");
|
|
5048
5429
|
}
|
|
5049
5430
|
}),
|
|
5050
|
-
...adminUiRoutes(options.name, options.adminUi)
|
|
5431
|
+
...adminUiRoutes(options.name, options.adminUi, adminPrefix)
|
|
5051
5432
|
},
|
|
5433
|
+
adminPrefix,
|
|
5052
5434
|
adminKey: options.adminKey
|
|
5053
5435
|
});
|
|
5054
5436
|
return defineMock(runtime);
|
|
@@ -5062,7 +5444,10 @@ var mutableResponse = (response) => {
|
|
|
5062
5444
|
return new Response(response.body, response);
|
|
5063
5445
|
}
|
|
5064
5446
|
};
|
|
5065
|
-
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5447
|
+
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5448
|
+
status,
|
|
5449
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
5450
|
+
});
|
|
5066
5451
|
var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
|
|
5067
5452
|
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5068
5453
|
var credentialRoutes = (registry) => ({
|
|
@@ -5114,7 +5499,7 @@ var credentialRoutes = (registry) => ({
|
|
|
5114
5499
|
return adminJson(200, { status: "ok" });
|
|
5115
5500
|
}
|
|
5116
5501
|
});
|
|
5117
|
-
var presetRoutes = (presets, runtime) => ({
|
|
5502
|
+
var presetRoutes = (presets, runtime, adminPrefix) => ({
|
|
5118
5503
|
"GET /faults/presets": () => {
|
|
5119
5504
|
const body = {
|
|
5120
5505
|
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
@@ -5124,7 +5509,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
5124
5509
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
5125
5510
|
const name = params.name;
|
|
5126
5511
|
if (!presets[name])
|
|
5127
|
-
return adminFail(404, `no fault preset ${name}; GET /
|
|
5512
|
+
return adminFail(404, `no fault preset ${name}; GET ${adminPrefix}/faults/presets`);
|
|
5128
5513
|
const overrides = isObject(body) ? body : {};
|
|
5129
5514
|
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
5130
5515
|
}
|
|
@@ -5301,6 +5686,7 @@ var createRuntime2 = (options = {}) => {
|
|
|
5301
5686
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
5302
5687
|
...options.clock ? { clock: options.clock } : {},
|
|
5303
5688
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
5689
|
+
...options.adminPrefix !== void 0 ? { adminPrefix: options.adminPrefix } : {},
|
|
5304
5690
|
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
5305
5691
|
...options.onLog ? { onLog: options.onLog } : {},
|
|
5306
5692
|
create: ({ sqlite, namespace, clock }) => new LiveKitAPI({
|
|
@@ -5676,6 +6062,7 @@ var LiveKitAPI = class {
|
|
|
5676
6062
|
|
|
5677
6063
|
export {
|
|
5678
6064
|
createClock,
|
|
6065
|
+
resolveAdminPrefix,
|
|
5679
6066
|
parseDuration,
|
|
5680
6067
|
document2 as document,
|
|
5681
6068
|
operationIds,
|
|
@@ -5685,4 +6072,4 @@ export {
|
|
|
5685
6072
|
LIVEKIT_NAMESPACE,
|
|
5686
6073
|
LiveKitAPI
|
|
5687
6074
|
};
|
|
5688
|
-
//# sourceMappingURL=chunk-
|
|
6075
|
+
//# sourceMappingURL=chunk-QIRIAV7T.js.map
|