@crvouga/mockingbird-service-aws-speech 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 +14 -0
- package/dist/{chunk-IQIS2V3E.js → chunk-DPBUA2NJ.js} +4 -3
- package/dist/{chunk-IQIS2V3E.js.map → chunk-DPBUA2NJ.js.map} +2 -2
- package/dist/{chunk-FLBEZUJF.js → chunk-RTZSXNQY.js} +622 -235
- package/dist/chunk-RTZSXNQY.js.map +7 -0
- package/dist/cli.js +51 -27
- package/dist/cli.js.map +2 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.js +2 -2
- package/openapi.yaml +334 -0
- package/package.json +29 -4
- package/dist/chunk-FLBEZUJF.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
|
};
|
|
@@ -1555,6 +1594,34 @@ dialog:not([open]) {
|
|
|
1555
1594
|
function isRecord2(value) {
|
|
1556
1595
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1557
1596
|
}
|
|
1597
|
+
function highlightJsonText(text2) {
|
|
1598
|
+
const escapeToken = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
1599
|
+
switch (char) {
|
|
1600
|
+
case "&":
|
|
1601
|
+
return "&";
|
|
1602
|
+
case "<":
|
|
1603
|
+
return "<";
|
|
1604
|
+
case ">":
|
|
1605
|
+
return ">";
|
|
1606
|
+
case '"':
|
|
1607
|
+
return """;
|
|
1608
|
+
default:
|
|
1609
|
+
return "'";
|
|
1610
|
+
}
|
|
1611
|
+
});
|
|
1612
|
+
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;
|
|
1613
|
+
let cursor = 0;
|
|
1614
|
+
let html = "";
|
|
1615
|
+
for (const match of text2.matchAll(token)) {
|
|
1616
|
+
const index = match.index ?? 0;
|
|
1617
|
+
const value = match[0];
|
|
1618
|
+
html += escapeToken(text2.slice(cursor, index));
|
|
1619
|
+
const kind = value.startsWith('"') ? value.trimEnd().endsWith(":") ? "key" : "string" : value === "true" || value === "false" ? "boolean" : value === "null" ? "null" : "number";
|
|
1620
|
+
html += `<span class="json-${kind}">${escapeToken(value)}</span>`;
|
|
1621
|
+
cursor = index + value.length;
|
|
1622
|
+
}
|
|
1623
|
+
return html + escapeToken(text2.slice(cursor));
|
|
1624
|
+
}
|
|
1558
1625
|
function mountAdminBrand(doc, host, service, catalog) {
|
|
1559
1626
|
const clip = (value, max) => {
|
|
1560
1627
|
if (typeof value !== "string")
|
|
@@ -1916,14 +1983,16 @@ function bootAdmin(config) {
|
|
|
1916
1983
|
const state = {
|
|
1917
1984
|
namespace: "default",
|
|
1918
1985
|
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
1919
|
-
view: "overview",
|
|
1986
|
+
view: ["overview", "state", "clock", "faults", "journal", "routes"].includes(location.hash.slice(1)) ? location.hash.slice(1) : "overview",
|
|
1920
1987
|
collection: "",
|
|
1921
1988
|
shape: null,
|
|
1922
1989
|
route: null,
|
|
1990
|
+
routes: [],
|
|
1923
1991
|
editing: null
|
|
1924
1992
|
};
|
|
1925
1993
|
const standard = new Set(config.standardRoutes);
|
|
1926
1994
|
const banner = el("banner", HTMLElement);
|
|
1995
|
+
const bannerMessage = el("banner-message", HTMLElement);
|
|
1927
1996
|
const keyInput = el("admin-key", HTMLInputElement);
|
|
1928
1997
|
keyInput.value = state.key;
|
|
1929
1998
|
const params = new URLSearchParams(location.search);
|
|
@@ -1934,18 +2003,21 @@ function bootAdmin(config) {
|
|
|
1934
2003
|
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
1935
2004
|
params.delete("key");
|
|
1936
2005
|
const next = params.toString();
|
|
1937
|
-
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
2006
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : "") + location.hash);
|
|
1938
2007
|
}
|
|
1939
2008
|
const queryNamespace = params.get("namespace");
|
|
1940
2009
|
if (queryNamespace)
|
|
1941
2010
|
state.namespace = queryNamespace;
|
|
1942
2011
|
const showError = (error) => {
|
|
1943
|
-
|
|
2012
|
+
bannerMessage.textContent = error instanceof Error ? error.message : String(error);
|
|
1944
2013
|
banner.classList.add("show");
|
|
2014
|
+
banner.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
1945
2015
|
};
|
|
1946
2016
|
const clearError = () => {
|
|
1947
2017
|
banner.classList.remove("show");
|
|
2018
|
+
bannerMessage.textContent = "";
|
|
1948
2019
|
};
|
|
2020
|
+
el("banner-dismiss", HTMLButtonElement).addEventListener("click", clearError);
|
|
1949
2021
|
const api = async (method, path, body) => {
|
|
1950
2022
|
const headers = { accept: "application/json" };
|
|
1951
2023
|
if (state.key !== "")
|
|
@@ -1989,16 +2061,21 @@ function bootAdmin(config) {
|
|
|
1989
2061
|
namespace: () => state.namespace
|
|
1990
2062
|
};
|
|
1991
2063
|
const isPanelScript = (value) => typeof value === "function";
|
|
2064
|
+
const themeButton = el("theme", HTMLButtonElement);
|
|
1992
2065
|
const setTheme = (theme) => {
|
|
1993
2066
|
if (theme)
|
|
1994
2067
|
document.documentElement.dataset.theme = theme;
|
|
1995
2068
|
else
|
|
1996
2069
|
delete document.documentElement.dataset.theme;
|
|
2070
|
+
const dark = theme ? theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2071
|
+
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>';
|
|
2072
|
+
const label = dark ? "Use light theme" : "Use dark theme";
|
|
2073
|
+
themeButton.setAttribute("aria-label", label);
|
|
2074
|
+
themeButton.title = label;
|
|
1997
2075
|
};
|
|
1998
2076
|
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
2077
|
+
setTheme(storedTheme === "dark" || storedTheme === "light" ? storedTheme : null);
|
|
2078
|
+
themeButton.addEventListener("click", () => {
|
|
2002
2079
|
const current = document.documentElement.dataset.theme;
|
|
2003
2080
|
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2004
2081
|
const next = dark ? "light" : "dark";
|
|
@@ -2017,6 +2094,7 @@ function bootAdmin(config) {
|
|
|
2017
2094
|
continue;
|
|
2018
2095
|
section.hidden = section.id !== `view-${view}`;
|
|
2019
2096
|
}
|
|
2097
|
+
history.replaceState(null, "", `${location.pathname}${location.search}#${encodeURIComponent(view)}`);
|
|
2020
2098
|
};
|
|
2021
2099
|
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
2022
2100
|
const target = event.target;
|
|
@@ -2031,7 +2109,7 @@ function bootAdmin(config) {
|
|
|
2031
2109
|
show(view);
|
|
2032
2110
|
refresh().catch(showError);
|
|
2033
2111
|
});
|
|
2034
|
-
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>`;
|
|
2112
|
+
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>`;
|
|
2035
2113
|
const table = (columns, rows) => {
|
|
2036
2114
|
if (rows.length === 0)
|
|
2037
2115
|
return '<p class="empty">Nothing here yet.</p>';
|
|
@@ -2041,16 +2119,22 @@ function bootAdmin(config) {
|
|
|
2041
2119
|
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
2042
2120
|
return `<tr${attrs}>${cells}</tr>`;
|
|
2043
2121
|
}).join("");
|
|
2044
|
-
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
2122
|
+
return `<div class="table-shell"><table><thead>${head}</thead><tbody>${body}</tbody></table></div>`;
|
|
2045
2123
|
};
|
|
2046
2124
|
const preview = (value) => {
|
|
2047
2125
|
const text2 = JSON.stringify(value) ?? "null";
|
|
2048
|
-
|
|
2126
|
+
const clipped = text2.length > 180 ? `${text2.slice(0, 180)}\u2026` : text2;
|
|
2127
|
+
return `<code class="json-inline">${highlightJsonText(clipped)}</code>`;
|
|
2049
2128
|
};
|
|
2050
2129
|
const when = (epoch) => {
|
|
2051
2130
|
const date = new Date(epoch);
|
|
2052
2131
|
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
2053
2132
|
};
|
|
2133
|
+
const journalWhen = (value) => {
|
|
2134
|
+
const numeric = value.trim() === "" ? Number.NaN : Number(value);
|
|
2135
|
+
const epoch = Number.isFinite(numeric) ? numeric : Date.parse(value);
|
|
2136
|
+
return Number.isFinite(epoch) ? when(epoch) : value;
|
|
2137
|
+
};
|
|
2054
2138
|
const clockCard = (epoch) => {
|
|
2055
2139
|
const date = new Date(epoch);
|
|
2056
2140
|
if (Number.isNaN(date.getTime()))
|
|
@@ -2170,7 +2254,7 @@ function bootAdmin(config) {
|
|
|
2170
2254
|
};
|
|
2171
2255
|
const renderOverview = async () => {
|
|
2172
2256
|
const [health, shape, clock, journal] = await Promise.all([
|
|
2173
|
-
originGet(
|
|
2257
|
+
originGet(`${config.adminPrefix}/health`),
|
|
2174
2258
|
api("GET", "/state"),
|
|
2175
2259
|
api("GET", "/clock"),
|
|
2176
2260
|
api("GET", "/requests?limit=8")
|
|
@@ -2187,7 +2271,7 @@ function bootAdmin(config) {
|
|
|
2187
2271
|
},
|
|
2188
2272
|
{
|
|
2189
2273
|
k: "Status",
|
|
2190
|
-
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
2274
|
+
v: `<span class="badge ok">${esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")}</span>`
|
|
2191
2275
|
},
|
|
2192
2276
|
{ k: "Namespace", v: esc(state.namespace) },
|
|
2193
2277
|
{ k: "Collections", v: String(decoded.collections.length) },
|
|
@@ -2274,26 +2358,46 @@ function bootAdmin(config) {
|
|
|
2274
2358
|
const renderJournal = async () => {
|
|
2275
2359
|
const journal = await api("GET", "/requests?limit=100");
|
|
2276
2360
|
el("journal", HTMLElement).innerHTML = table([
|
|
2277
|
-
{ label: "When", cell: (row) => esc(row.at) },
|
|
2278
|
-
{
|
|
2361
|
+
{ label: "When", cell: (row) => esc(journalWhen(row.at)) },
|
|
2362
|
+
{
|
|
2363
|
+
label: "Method",
|
|
2364
|
+
cell: (row) => `<span class="method">${esc(row.method)}</span>`
|
|
2365
|
+
},
|
|
2279
2366
|
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2280
|
-
{
|
|
2367
|
+
{
|
|
2368
|
+
label: "Status",
|
|
2369
|
+
cell: (row) => `<span class="badge ${row.status.startsWith("2") ? "ok" : row.status.startsWith("4") || row.status.startsWith("5") ? "warn" : ""}">${esc(row.status)}</span>`
|
|
2370
|
+
},
|
|
2281
2371
|
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
2282
2372
|
], decodeJournal(journal));
|
|
2283
2373
|
};
|
|
2284
|
-
const
|
|
2285
|
-
const
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2374
|
+
const paintRoutes = () => {
|
|
2375
|
+
const query = el("route-search", HTMLInputElement).value.trim().toLocaleLowerCase();
|
|
2376
|
+
const routes = state.routes.filter((route) => route.toLocaleLowerCase().includes(query));
|
|
2377
|
+
el("route-count", HTMLElement).textContent = `${routes.length} of ${state.routes.length}`;
|
|
2378
|
+
if (state.routes.length === 0) {
|
|
2379
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No admin routes are available.</p>';
|
|
2380
|
+
return;
|
|
2381
|
+
}
|
|
2382
|
+
if (routes.length === 0) {
|
|
2383
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No routes match this filter.</p>';
|
|
2384
|
+
return;
|
|
2385
|
+
}
|
|
2289
2386
|
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2290
2387
|
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2291
2388
|
const parts = route.split(" ");
|
|
2292
2389
|
const method = parts[0] ?? "";
|
|
2293
2390
|
const path = parts.slice(1).join(" ");
|
|
2294
|
-
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>`;
|
|
2391
|
+
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>`;
|
|
2295
2392
|
}).join("");
|
|
2296
2393
|
};
|
|
2394
|
+
const renderRoutes = async () => {
|
|
2395
|
+
const data = await api("GET", "/");
|
|
2396
|
+
if (!isRecord2(data))
|
|
2397
|
+
throw new Error("Route list was not an object");
|
|
2398
|
+
state.routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
2399
|
+
paintRoutes();
|
|
2400
|
+
};
|
|
2297
2401
|
const renderers = {
|
|
2298
2402
|
overview: renderOverview,
|
|
2299
2403
|
state: renderState,
|
|
@@ -2302,19 +2406,50 @@ function bootAdmin(config) {
|
|
|
2302
2406
|
journal: renderJournal,
|
|
2303
2407
|
routes: renderRoutes
|
|
2304
2408
|
};
|
|
2409
|
+
let refreshToken = 0;
|
|
2305
2410
|
async function refresh() {
|
|
2411
|
+
const token = ++refreshToken;
|
|
2306
2412
|
clearError();
|
|
2413
|
+
const main = el("views", HTMLElement);
|
|
2414
|
+
const sync = el("sync-status", HTMLElement);
|
|
2415
|
+
main.setAttribute("aria-busy", "true");
|
|
2416
|
+
sync.classList.add("busy");
|
|
2417
|
+
sync.textContent = "Updating";
|
|
2307
2418
|
const render = renderers[state.view];
|
|
2308
|
-
|
|
2309
|
-
|
|
2419
|
+
try {
|
|
2420
|
+
if (render)
|
|
2421
|
+
await render();
|
|
2422
|
+
if (token === refreshToken)
|
|
2423
|
+
sync.textContent = "Up to date";
|
|
2424
|
+
} finally {
|
|
2425
|
+
if (token === refreshToken) {
|
|
2426
|
+
main.removeAttribute("aria-busy");
|
|
2427
|
+
sync.classList.remove("busy");
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2310
2430
|
}
|
|
2311
2431
|
const delay = (ms) => new Promise((resolve) => {
|
|
2312
2432
|
window.setTimeout(resolve, ms);
|
|
2313
2433
|
});
|
|
2314
|
-
const confirmAction = async (button, pending, done, work) => {
|
|
2434
|
+
const confirmAction = async (button, pending, done, work, confirmLabel) => {
|
|
2315
2435
|
if (button.dataset.pending === "1")
|
|
2316
2436
|
return;
|
|
2317
2437
|
const label = button.textContent ?? "";
|
|
2438
|
+
if (confirmLabel !== void 0 && button.dataset.confirming !== "1") {
|
|
2439
|
+
button.dataset.confirming = "1";
|
|
2440
|
+
button.classList.add("confirming");
|
|
2441
|
+
button.textContent = confirmLabel;
|
|
2442
|
+
window.setTimeout(() => {
|
|
2443
|
+
if (button.dataset.confirming !== "1")
|
|
2444
|
+
return;
|
|
2445
|
+
button.textContent = label;
|
|
2446
|
+
button.classList.remove("confirming");
|
|
2447
|
+
delete button.dataset.confirming;
|
|
2448
|
+
}, 3e3);
|
|
2449
|
+
return;
|
|
2450
|
+
}
|
|
2451
|
+
button.classList.remove("confirming");
|
|
2452
|
+
delete button.dataset.confirming;
|
|
2318
2453
|
const width = button.offsetWidth;
|
|
2319
2454
|
button.dataset.pending = "1";
|
|
2320
2455
|
button.classList.add("pressed");
|
|
@@ -2335,6 +2470,65 @@ function bootAdmin(config) {
|
|
|
2335
2470
|
delete button.dataset.pending;
|
|
2336
2471
|
}
|
|
2337
2472
|
};
|
|
2473
|
+
const wireJsonEditor = (textareaId, highlightId, statusId, shellId, allowEmpty) => {
|
|
2474
|
+
const textarea = el(textareaId, HTMLTextAreaElement);
|
|
2475
|
+
const highlight = el(highlightId, HTMLElement);
|
|
2476
|
+
const status = el(statusId, HTMLElement);
|
|
2477
|
+
const shell = el(shellId, HTMLElement);
|
|
2478
|
+
const sync = () => {
|
|
2479
|
+
const text2 = textarea.value;
|
|
2480
|
+
highlight.innerHTML = `${highlightJsonText(text2)}
|
|
2481
|
+
`;
|
|
2482
|
+
let valid = true;
|
|
2483
|
+
let message = "Valid JSON";
|
|
2484
|
+
if (allowEmpty && text2.trim() === "")
|
|
2485
|
+
message = "No body";
|
|
2486
|
+
else {
|
|
2487
|
+
try {
|
|
2488
|
+
readJson2(text2);
|
|
2489
|
+
} catch {
|
|
2490
|
+
valid = false;
|
|
2491
|
+
message = "Invalid JSON";
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
shell.classList.toggle("invalid", !valid);
|
|
2495
|
+
status.classList.toggle("invalid", !valid);
|
|
2496
|
+
status.textContent = message;
|
|
2497
|
+
textarea.setAttribute("aria-invalid", valid ? "false" : "true");
|
|
2498
|
+
};
|
|
2499
|
+
const syncScroll = () => {
|
|
2500
|
+
const preview2 = highlight.parentElement;
|
|
2501
|
+
if (!(preview2 instanceof HTMLElement))
|
|
2502
|
+
return;
|
|
2503
|
+
preview2.scrollTop = textarea.scrollTop;
|
|
2504
|
+
preview2.scrollLeft = textarea.scrollLeft;
|
|
2505
|
+
};
|
|
2506
|
+
textarea.addEventListener("input", sync);
|
|
2507
|
+
textarea.addEventListener("scroll", syncScroll);
|
|
2508
|
+
sync();
|
|
2509
|
+
return sync;
|
|
2510
|
+
};
|
|
2511
|
+
const syncRouteEditor = wireJsonEditor("route-body", "route-highlight", "route-json-status", "route-editor", true);
|
|
2512
|
+
const syncRecordEditor = wireJsonEditor("editor-value", "editor-highlight", "editor-json-status", "record-editor", false);
|
|
2513
|
+
for (const button of document.querySelectorAll("[data-format]")) {
|
|
2514
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2515
|
+
continue;
|
|
2516
|
+
button.addEventListener("click", () => {
|
|
2517
|
+
const target = button.dataset.format;
|
|
2518
|
+
if (target === void 0)
|
|
2519
|
+
return;
|
|
2520
|
+
const textarea = document.getElementById(target);
|
|
2521
|
+
if (!(textarea instanceof HTMLTextAreaElement) || textarea.value.trim() === "")
|
|
2522
|
+
return;
|
|
2523
|
+
try {
|
|
2524
|
+
textarea.value = JSON.stringify(readJson2(textarea.value), null, 2) ?? "";
|
|
2525
|
+
textarea.dispatchEvent(new Event("input"));
|
|
2526
|
+
textarea.focus();
|
|
2527
|
+
} catch {
|
|
2528
|
+
showError(new Error("Fix the JSON before formatting it"));
|
|
2529
|
+
}
|
|
2530
|
+
});
|
|
2531
|
+
}
|
|
2338
2532
|
document.addEventListener("click", (event) => {
|
|
2339
2533
|
const target = event.target;
|
|
2340
2534
|
if (!(target instanceof Element))
|
|
@@ -2407,6 +2601,7 @@ function bootAdmin(config) {
|
|
|
2407
2601
|
} else {
|
|
2408
2602
|
editorValue.value = "{\n \n}";
|
|
2409
2603
|
}
|
|
2604
|
+
syncRecordEditor();
|
|
2410
2605
|
editor.showModal();
|
|
2411
2606
|
}
|
|
2412
2607
|
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -2436,10 +2631,10 @@ function bootAdmin(config) {
|
|
|
2436
2631
|
const id = state.editing;
|
|
2437
2632
|
if (!id)
|
|
2438
2633
|
return;
|
|
2439
|
-
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2634
|
+
confirmAction(editorDelete, "Deleting...", "Deleted", () => api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2440
2635
|
editor.close();
|
|
2441
2636
|
return renderState();
|
|
2442
|
-
}).catch(showError);
|
|
2637
|
+
}), "Confirm delete").catch(showError);
|
|
2443
2638
|
});
|
|
2444
2639
|
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2445
2640
|
if (!(button instanceof HTMLButtonElement))
|
|
@@ -2485,10 +2680,10 @@ function bootAdmin(config) {
|
|
|
2485
2680
|
}).then(() => renderFaults()).catch(showError);
|
|
2486
2681
|
});
|
|
2487
2682
|
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2488
|
-
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
|
|
2683
|
+
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults()), "Confirm clear").catch(showError);
|
|
2489
2684
|
});
|
|
2490
2685
|
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2491
|
-
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
|
|
2686
|
+
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal()), "Confirm clear").catch(showError);
|
|
2492
2687
|
});
|
|
2493
2688
|
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2494
2689
|
const target = event.target;
|
|
@@ -2502,9 +2697,15 @@ function bootAdmin(config) {
|
|
|
2502
2697
|
return;
|
|
2503
2698
|
state.route = route;
|
|
2504
2699
|
el("route-title", HTMLElement).textContent = route;
|
|
2700
|
+
el("route-send", HTMLButtonElement).disabled = false;
|
|
2505
2701
|
const method = route.split(" ")[0] ?? "";
|
|
2506
|
-
el("route-body", HTMLTextAreaElement)
|
|
2702
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
2703
|
+
routeBody.disabled = method === "GET" || method === "DELETE";
|
|
2704
|
+
el("route-editor", HTMLElement).hidden = routeBody.disabled;
|
|
2705
|
+
el("route-json-status", HTMLElement).parentElement?.toggleAttribute("hidden", routeBody.disabled);
|
|
2706
|
+
syncRouteEditor();
|
|
2507
2707
|
});
|
|
2708
|
+
el("route-search", HTMLInputElement).addEventListener("input", paintRoutes);
|
|
2508
2709
|
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2509
2710
|
event.preventDefault();
|
|
2510
2711
|
const send = async () => {
|
|
@@ -2526,7 +2727,8 @@ function bootAdmin(config) {
|
|
|
2526
2727
|
}
|
|
2527
2728
|
}
|
|
2528
2729
|
const data = await api(method, concrete, body);
|
|
2529
|
-
|
|
2730
|
+
const text2 = JSON.stringify(data, null, 2) ?? "null";
|
|
2731
|
+
el("route-result", HTMLElement).innerHTML = `<code>${highlightJsonText(text2)}</code>`;
|
|
2530
2732
|
};
|
|
2531
2733
|
send().catch(showError);
|
|
2532
2734
|
});
|
|
@@ -2644,11 +2846,13 @@ function bootAdmin(config) {
|
|
|
2644
2846
|
views.append(section);
|
|
2645
2847
|
mountSqlExplorer(body, panelApi);
|
|
2646
2848
|
}
|
|
2849
|
+
show(document.getElementById(`view-${state.view}`) === null ? "overview" : state.view);
|
|
2647
2850
|
return refresh();
|
|
2648
2851
|
}).catch(showError);
|
|
2649
2852
|
}
|
|
2650
2853
|
var clientPrelude = [
|
|
2651
2854
|
isRecord2,
|
|
2855
|
+
highlightJsonText,
|
|
2652
2856
|
mountAdminBrand,
|
|
2653
2857
|
startAdminBrand,
|
|
2654
2858
|
faultPresetList,
|
|
@@ -2671,6 +2875,7 @@ ${entry}(${payload});
|
|
|
2671
2875
|
var defineMock = (runtime) => runtime;
|
|
2672
2876
|
var STANDARD_ADMIN_ROUTES = [
|
|
2673
2877
|
"GET /",
|
|
2878
|
+
"GET /health",
|
|
2674
2879
|
"POST /reset",
|
|
2675
2880
|
"GET /namespaces",
|
|
2676
2881
|
"GET /clock",
|
|
@@ -2750,11 +2955,11 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
2750
2955
|
}
|
|
2751
2956
|
});
|
|
2752
2957
|
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
2753
|
-
var renderAdminDocument = (service) => {
|
|
2958
|
+
var renderAdminDocument = (service, adminPrefix = ADMIN_PREFIX) => {
|
|
2754
2959
|
const name = escapeHtml(service);
|
|
2755
2960
|
const script = adminClientSource({
|
|
2756
2961
|
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
2757
|
-
adminPrefix
|
|
2962
|
+
adminPrefix,
|
|
2758
2963
|
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
2759
2964
|
brandsUrl: ADMIN_BRANDS_URL,
|
|
2760
2965
|
service
|
|
@@ -2771,75 +2976,99 @@ var renderAdminDocument = (service) => {
|
|
|
2771
2976
|
${CSS_RESET}
|
|
2772
2977
|
:root {
|
|
2773
2978
|
color-scheme: light;
|
|
2774
|
-
--bg: #
|
|
2979
|
+
--bg: #f7f8fb;
|
|
2775
2980
|
--bg-elev: #ffffff;
|
|
2776
|
-
--bg-muted: #
|
|
2777
|
-
--bg-inset: #
|
|
2778
|
-
--line: #
|
|
2779
|
-
--line-strong: #
|
|
2780
|
-
--ink: #
|
|
2781
|
-
--muted: #
|
|
2782
|
-
--faint: #
|
|
2783
|
-
--accent: #
|
|
2981
|
+
--bg-muted: #f1f3f7;
|
|
2982
|
+
--bg-inset: #f8f9fc;
|
|
2983
|
+
--line: #e4e7ec;
|
|
2984
|
+
--line-strong: #cfd4dc;
|
|
2985
|
+
--ink: #101828;
|
|
2986
|
+
--muted: #475467;
|
|
2987
|
+
--faint: #667085;
|
|
2988
|
+
--accent: #6941c6;
|
|
2989
|
+
--accent-hover: #5933b4;
|
|
2784
2990
|
--accent-ink: #ffffff;
|
|
2785
|
-
--accent-soft: #
|
|
2786
|
-
--ok: #
|
|
2787
|
-
--ok-soft: #
|
|
2788
|
-
--warn: #
|
|
2789
|
-
--warn-soft: #
|
|
2790
|
-
--err: #
|
|
2791
|
-
--err-soft: #
|
|
2792
|
-
--
|
|
2991
|
+
--accent-soft: #f4f0ff;
|
|
2992
|
+
--ok: #067647;
|
|
2993
|
+
--ok-soft: #ecfdf3;
|
|
2994
|
+
--warn: #b54708;
|
|
2995
|
+
--warn-soft: #fffaeb;
|
|
2996
|
+
--err: #b42318;
|
|
2997
|
+
--err-soft: #fef3f2;
|
|
2998
|
+
--code-key: #6941c6;
|
|
2999
|
+
--code-string: #067647;
|
|
3000
|
+
--code-number: #175cd3;
|
|
3001
|
+
--code-boolean: #c11574;
|
|
3002
|
+
--code-null: #667085;
|
|
3003
|
+
--shadow-xs: 0 1px 2px rgb(16 24 40 / 0.05);
|
|
3004
|
+
--shadow-sm: 0 1px 3px rgb(16 24 40 / 0.08), 0 1px 2px rgb(16 24 40 / 0.04);
|
|
3005
|
+
--shadow-lg: 0 18px 48px rgb(16 24 40 / 0.16), 0 4px 12px rgb(16 24 40 / 0.08);
|
|
2793
3006
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2794
3007
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
2795
|
-
--radius:
|
|
2796
|
-
--header:
|
|
2797
|
-
--nav:
|
|
3008
|
+
--radius: 12px;
|
|
3009
|
+
--header: 72px;
|
|
3010
|
+
--nav: 224px;
|
|
2798
3011
|
}
|
|
2799
3012
|
:root[data-theme="dark"] {
|
|
2800
3013
|
color-scheme: dark;
|
|
2801
|
-
--bg: #
|
|
2802
|
-
--bg-elev: #
|
|
2803
|
-
--bg-muted: #
|
|
2804
|
-
--bg-inset: #
|
|
2805
|
-
--line: #
|
|
2806
|
-
--line-strong: #
|
|
2807
|
-
--ink: #
|
|
2808
|
-
--muted: #
|
|
2809
|
-
--faint: #
|
|
2810
|
-
--accent: #
|
|
2811
|
-
--accent-
|
|
2812
|
-
--accent-
|
|
2813
|
-
--
|
|
2814
|
-
--ok
|
|
2815
|
-
--
|
|
2816
|
-
--warn
|
|
2817
|
-
--
|
|
2818
|
-
--err
|
|
2819
|
-
--
|
|
3014
|
+
--bg: #0c0e14;
|
|
3015
|
+
--bg-elev: #151821;
|
|
3016
|
+
--bg-muted: #20232e;
|
|
3017
|
+
--bg-inset: #11141c;
|
|
3018
|
+
--line: #2b303d;
|
|
3019
|
+
--line-strong: #414858;
|
|
3020
|
+
--ink: #f5f7fa;
|
|
3021
|
+
--muted: #b6bdc9;
|
|
3022
|
+
--faint: #8d96a7;
|
|
3023
|
+
--accent: #9e77ed;
|
|
3024
|
+
--accent-hover: #b692f6;
|
|
3025
|
+
--accent-ink: #ffffff;
|
|
3026
|
+
--accent-soft: #2b2142;
|
|
3027
|
+
--ok: #75e0a7;
|
|
3028
|
+
--ok-soft: #102a20;
|
|
3029
|
+
--warn: #fec84b;
|
|
3030
|
+
--warn-soft: #302611;
|
|
3031
|
+
--err: #fda29b;
|
|
3032
|
+
--err-soft: #321817;
|
|
3033
|
+
--code-key: #c3a6ff;
|
|
3034
|
+
--code-string: #75e0a7;
|
|
3035
|
+
--code-number: #84adff;
|
|
3036
|
+
--code-boolean: #f9a8d4;
|
|
3037
|
+
--code-null: #98a2b3;
|
|
3038
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3039
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3040
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2820
3041
|
}
|
|
2821
3042
|
@media (prefers-color-scheme: dark) {
|
|
2822
3043
|
:root:not([data-theme="light"]) {
|
|
2823
3044
|
color-scheme: dark;
|
|
2824
|
-
--bg: #
|
|
2825
|
-
--bg-elev: #
|
|
2826
|
-
--bg-muted: #
|
|
2827
|
-
--bg-inset: #
|
|
2828
|
-
--line: #
|
|
2829
|
-
--line-strong: #
|
|
2830
|
-
--ink: #
|
|
2831
|
-
--muted: #
|
|
2832
|
-
--faint: #
|
|
2833
|
-
--accent: #
|
|
2834
|
-
--accent-
|
|
2835
|
-
--accent-
|
|
2836
|
-
--
|
|
2837
|
-
--ok
|
|
2838
|
-
--
|
|
2839
|
-
--warn
|
|
2840
|
-
--
|
|
2841
|
-
--err
|
|
2842
|
-
--
|
|
3045
|
+
--bg: #0c0e14;
|
|
3046
|
+
--bg-elev: #151821;
|
|
3047
|
+
--bg-muted: #20232e;
|
|
3048
|
+
--bg-inset: #11141c;
|
|
3049
|
+
--line: #2b303d;
|
|
3050
|
+
--line-strong: #414858;
|
|
3051
|
+
--ink: #f5f7fa;
|
|
3052
|
+
--muted: #b6bdc9;
|
|
3053
|
+
--faint: #8d96a7;
|
|
3054
|
+
--accent: #9e77ed;
|
|
3055
|
+
--accent-hover: #b692f6;
|
|
3056
|
+
--accent-ink: #ffffff;
|
|
3057
|
+
--accent-soft: #2b2142;
|
|
3058
|
+
--ok: #75e0a7;
|
|
3059
|
+
--ok-soft: #102a20;
|
|
3060
|
+
--warn: #fec84b;
|
|
3061
|
+
--warn-soft: #302611;
|
|
3062
|
+
--err: #fda29b;
|
|
3063
|
+
--err-soft: #321817;
|
|
3064
|
+
--code-key: #c3a6ff;
|
|
3065
|
+
--code-string: #75e0a7;
|
|
3066
|
+
--code-number: #84adff;
|
|
3067
|
+
--code-boolean: #f9a8d4;
|
|
3068
|
+
--code-null: #98a2b3;
|
|
3069
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3070
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3071
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2843
3072
|
}
|
|
2844
3073
|
}
|
|
2845
3074
|
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
@@ -2857,13 +3086,17 @@ var renderAdminDocument = (service) => {
|
|
|
2857
3086
|
padding-right: env(safe-area-inset-right);
|
|
2858
3087
|
-webkit-text-size-adjust: 100%;
|
|
2859
3088
|
}
|
|
2860
|
-
|
|
3089
|
+
::selection { background: color-mix(in srgb, var(--accent) 28%, transparent); }
|
|
3090
|
+
:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px; }
|
|
3091
|
+
button, input, select, textarea { transition: border-color 140ms ease, box-shadow 140ms ease, transform 100ms ease; }
|
|
3092
|
+
button:disabled, input:disabled, select:disabled, textarea:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
2861
3093
|
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
2862
3094
|
header.top {
|
|
2863
|
-
position: sticky; top: 0; z-index:
|
|
3095
|
+
position: sticky; top: 0; z-index: 20;
|
|
2864
3096
|
display: flex; align-items: center; gap: 12px;
|
|
2865
|
-
min-height: var(--header); padding:
|
|
2866
|
-
background: var(--bg-elev);
|
|
3097
|
+
min-height: var(--header); padding: 12px 20px;
|
|
3098
|
+
background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
|
|
3099
|
+
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
2867
3100
|
border-bottom: 1px solid var(--line);
|
|
2868
3101
|
min-width: 0; max-width: 100%;
|
|
2869
3102
|
}
|
|
@@ -2871,12 +3104,21 @@ var renderAdminDocument = (service) => {
|
|
|
2871
3104
|
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
2872
3105
|
min-width: 0; max-width: 100%; margin-right: auto;
|
|
2873
3106
|
}
|
|
2874
|
-
.brand-
|
|
2875
|
-
.brand-
|
|
3107
|
+
.brand-lockup { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
3108
|
+
.brand-mark {
|
|
3109
|
+
width: 38px; height: 38px; display: grid; place-items: center; flex: none;
|
|
3110
|
+
border-radius: 11px; color: #fff;
|
|
3111
|
+
background: linear-gradient(145deg, #7f56d9 0%, #53389e 100%);
|
|
3112
|
+
box-shadow: 0 6px 16px rgb(105 65 198 / 0.24), inset 0 1px 0 rgb(255 255 255 / 0.25);
|
|
3113
|
+
}
|
|
3114
|
+
.brand-mark svg { width: 22px; height: 22px; }
|
|
3115
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; line-height: 1.25; }
|
|
3116
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
2876
3117
|
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
2877
3118
|
.vendor {
|
|
2878
3119
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
2879
3120
|
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
3121
|
+
padding: 5px 8px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-inset);
|
|
2880
3122
|
}
|
|
2881
3123
|
.vendor[hidden] { display: none; }
|
|
2882
3124
|
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
@@ -2888,12 +3130,17 @@ var renderAdminDocument = (service) => {
|
|
|
2888
3130
|
.vendor-sep { color: var(--faint); }
|
|
2889
3131
|
.vendor-link { color: inherit; text-decoration: none; }
|
|
2890
3132
|
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
2891
|
-
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items:
|
|
2892
|
-
|
|
3133
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
|
|
3134
|
+
.sync-status { align-self: center; display: flex; align-items: center; gap: 6px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3135
|
+
.sync-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
|
|
3136
|
+
.sync-status.busy::before { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); animation: pulse 1.2s ease-in-out infinite; }
|
|
3137
|
+
label.field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; font-weight: 600; color: var(--faint); }
|
|
2893
3138
|
label.field input, label.field select, .controls input[type="password"] {
|
|
2894
|
-
background-color: var(--bg-
|
|
2895
|
-
padding:
|
|
3139
|
+
background-color: var(--bg-elev); border: 1px solid var(--line-strong); border-radius: 9px;
|
|
3140
|
+
padding: 8px 10px; min-height: 38px; line-height: 1.2; box-shadow: var(--shadow-xs);
|
|
2896
3141
|
}
|
|
3142
|
+
label.field input:hover, label.field select:hover, textarea:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--line-strong)); }
|
|
3143
|
+
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; }
|
|
2897
3144
|
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
2898
3145
|
the text fields in the same form. Paint it like those fields. */
|
|
2899
3146
|
label.field select {
|
|
@@ -2910,12 +3157,20 @@ var renderAdminDocument = (service) => {
|
|
|
2910
3157
|
dialog .field > textarea { width: 100%; }
|
|
2911
3158
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
2912
3159
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
.btn
|
|
3160
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
|
3161
|
+
border-radius: 9px; min-height: 38px; padding: 0 13px; border: 1px solid var(--line-strong);
|
|
3162
|
+
background: var(--bg-elev); color: var(--ink); font-size: 13px; font-weight: 600; line-height: 1;
|
|
3163
|
+
box-shadow: var(--shadow-xs);
|
|
3164
|
+
}
|
|
3165
|
+
.icon-btn { width: 38px; padding: 0; }
|
|
3166
|
+
.icon-btn svg, .btn svg, .btn-primary svg { width: 16px; height: 16px; }
|
|
3167
|
+
.icon-btn:hover, .btn:hover { background: var(--bg-muted); border-color: var(--line-strong); }
|
|
3168
|
+
.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; }
|
|
3169
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
3170
|
+
.btn-quiet { background: transparent; border-color: transparent; box-shadow: none; }
|
|
3171
|
+
.btn-quiet:hover { background: var(--bg-muted); }
|
|
3172
|
+
.btn-danger { color: var(--err); border-color: color-mix(in srgb, var(--err) 25%, var(--line)); }
|
|
3173
|
+
.btn-danger:hover, .btn-danger.confirming { background: var(--err-soft); border-color: color-mix(in srgb, var(--err) 45%, var(--line)); }
|
|
2919
3174
|
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
2920
3175
|
so the press itself has to stay visible. */
|
|
2921
3176
|
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
@@ -2931,93 +3186,159 @@ var renderAdminDocument = (service) => {
|
|
|
2931
3186
|
flex: 1 0 auto; min-width: 0;
|
|
2932
3187
|
}
|
|
2933
3188
|
nav.side {
|
|
2934
|
-
|
|
3189
|
+
position: sticky; top: var(--header); align-self: start; height: calc(100vh - var(--header));
|
|
3190
|
+
border-right: 1px solid var(--line); padding: 18px 12px;
|
|
2935
3191
|
display: flex; flex-direction: column; gap: 4px;
|
|
2936
|
-
background: var(--bg-elev);
|
|
3192
|
+
background: var(--bg-elev); overflow-y: auto;
|
|
2937
3193
|
}
|
|
3194
|
+
.nav-label { padding: 0 10px 8px; color: var(--faint); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
|
|
2938
3195
|
nav.side button {
|
|
3196
|
+
position: relative; display: flex; align-items: center; gap: 10px; width: 100%;
|
|
2939
3197
|
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
2940
|
-
padding: 9px 10px; min-height:
|
|
2941
|
-
}
|
|
2942
|
-
nav.side button
|
|
2943
|
-
|
|
2944
|
-
.
|
|
2945
|
-
.
|
|
2946
|
-
.
|
|
3198
|
+
padding: 9px 10px; min-height: 42px; color: var(--muted); font-size: 13px; font-weight: 550;
|
|
3199
|
+
}
|
|
3200
|
+
nav.side button:hover { background: var(--bg-muted); color: var(--ink); }
|
|
3201
|
+
nav.side button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 650; }
|
|
3202
|
+
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); }
|
|
3203
|
+
.nav-icon { width: 18px; height: 18px; display: grid; place-items: center; flex: none; }
|
|
3204
|
+
.nav-icon svg { width: 18px; height: 18px; }
|
|
3205
|
+
main { padding: 32px clamp(20px, 4vw, 48px) 64px; max-width: 1320px; width: 100%; min-width: 0; }
|
|
3206
|
+
main[aria-busy="true"] .view { opacity: 0.62; }
|
|
3207
|
+
.view { animation: view-in 180ms ease-out; }
|
|
3208
|
+
.view[hidden] { display: none; }
|
|
3209
|
+
.view-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
|
|
3210
|
+
.view h2 { margin: 0 0 5px; font-size: clamp(24px, 3vw, 30px); line-height: 1.2; letter-spacing: -0.035em; }
|
|
3211
|
+
.view h3 { margin: 24px 0 10px; font-size: 15px; letter-spacing: -0.01em; }
|
|
3212
|
+
.lede { margin: 0; color: var(--muted); max-width: 72ch; font-size: 14px; }
|
|
3213
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 175px), 1fr)); gap: 14px; }
|
|
2947
3214
|
.card {
|
|
2948
3215
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
2949
|
-
padding:
|
|
3216
|
+
padding: 18px; box-shadow: var(--shadow-sm);
|
|
2950
3217
|
}
|
|
2951
|
-
.card
|
|
2952
|
-
.card
|
|
3218
|
+
.stat-card { position: relative; overflow: hidden; }
|
|
3219
|
+
.stat-card::after { content: ""; position: absolute; inset: 0 0 auto; height: 2px; background: linear-gradient(90deg, var(--accent), transparent 78%); opacity: 0.7; }
|
|
3220
|
+
.card .k { color: var(--faint); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
3221
|
+
.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; }
|
|
2953
3222
|
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
2954
|
-
.
|
|
3223
|
+
.section-card { background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-xs); overflow: hidden; }
|
|
3224
|
+
.section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
|
|
3225
|
+
.section-title h3 { margin: 0; }
|
|
3226
|
+
.split { display: grid; grid-template-columns: minmax(0, 250px) minmax(0, 1fr); gap: 18px; align-items: start; }
|
|
3227
|
+
#collections { padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); box-shadow: var(--shadow-xs); }
|
|
2955
3228
|
.collection {
|
|
2956
3229
|
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
2957
|
-
border-radius:
|
|
3230
|
+
border-radius: 9px; padding: 10px 11px; min-height: 48px; color: var(--ink); cursor: pointer;
|
|
2958
3231
|
}
|
|
2959
|
-
.collection
|
|
3232
|
+
.collection:hover { background: var(--bg-muted); }
|
|
3233
|
+
.collection[aria-current="true"] { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 18%, var(--line)); color: var(--accent); }
|
|
2960
3234
|
.collection small { display: block; color: var(--faint); }
|
|
2961
3235
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
2962
|
-
.chip {
|
|
2963
|
-
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
2964
|
-
border-radius: 999px; padding: 4px 8px;
|
|
2965
|
-
}
|
|
3236
|
+
.chip { font-family: var(--mono); font-size: 11px; background: var(--bg-muted); border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; }
|
|
2966
3237
|
.chip i { color: var(--faint); font-style: normal; }
|
|
2967
3238
|
.sql-layout {
|
|
2968
3239
|
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
2969
3240
|
}
|
|
2970
|
-
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
2971
|
-
.sql-tables button { text-align: left; }
|
|
3241
|
+
.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); }
|
|
3242
|
+
.sql-tables button { text-align: left; border: 0; border-radius: 8px; padding: 9px 10px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
3243
|
+
.sql-tables button:hover, .sql-tables button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); }
|
|
2972
3244
|
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
2973
|
-
table { width: 100%; border-
|
|
2974
|
-
|
|
2975
|
-
th {
|
|
3245
|
+
.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); }
|
|
3246
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
3247
|
+
th, td { text-align: left; padding: 11px 13px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
3248
|
+
th { color: var(--faint); background: var(--bg-inset); font-weight: 650; font-size: 11px; text-transform: uppercase; letter-spacing: 0.045em; }
|
|
3249
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
2976
3250
|
tr.clickable { cursor: pointer; }
|
|
2977
|
-
tr.clickable:hover { background: var(--
|
|
3251
|
+
tr.clickable:hover { background: var(--accent-soft); }
|
|
2978
3252
|
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
2979
3253
|
pre {
|
|
2980
3254
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
2981
|
-
background: var(--bg-inset); border-radius: 10px; padding:
|
|
2982
|
-
}
|
|
2983
|
-
.
|
|
3255
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 10px; padding: 14px; font-size: 12px; line-height: 1.65;
|
|
3256
|
+
}
|
|
3257
|
+
.json-key { color: var(--code-key); }
|
|
3258
|
+
.json-string { color: var(--code-string); }
|
|
3259
|
+
.json-number { color: var(--code-number); }
|
|
3260
|
+
.json-boolean { color: var(--code-boolean); }
|
|
3261
|
+
.json-null { color: var(--code-null); font-style: italic; }
|
|
3262
|
+
.json-inline { display: block; max-width: 44rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
3263
|
+
.json-view { position: relative; max-height: 26rem; overflow: auto; }
|
|
3264
|
+
.json-editor { position: relative; min-height: 190px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--bg-inset); overflow: hidden; }
|
|
3265
|
+
.json-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); }
|
|
3266
|
+
.json-editor.invalid { border-color: var(--err); }
|
|
3267
|
+
.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; }
|
|
3268
|
+
.json-editor pre { position: absolute; inset: 0; pointer-events: none; background: transparent; }
|
|
3269
|
+
.json-editor textarea { position: relative; z-index: 1; resize: vertical; background: transparent; color: transparent; caret-color: var(--ink); -webkit-text-fill-color: transparent; }
|
|
3270
|
+
.json-editor textarea::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); }
|
|
3271
|
+
.editor-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; }
|
|
3272
|
+
.json-status { display: inline-flex; align-items: center; gap: 6px; color: var(--ok); font-size: 11px; }
|
|
3273
|
+
.json-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
3274
|
+
.json-status.invalid { color: var(--err); }
|
|
3275
|
+
.format-json { border: 0; padding: 3px 5px; background: transparent; color: var(--accent); font-size: 11px; font-weight: 650; cursor: pointer; }
|
|
3276
|
+
.row-actions { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 14px 0; }
|
|
2984
3277
|
.banner {
|
|
2985
|
-
display: none;
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
3278
|
+
display: none; position: sticky; top: calc(var(--header) + 12px); z-index: 15;
|
|
3279
|
+
align-items: center; gap: 10px; margin: 0 0 16px; padding: 11px 12px 11px 14px;
|
|
3280
|
+
border: 1px solid color-mix(in srgb, var(--err) 30%, var(--line)); border-radius: 10px;
|
|
3281
|
+
background: var(--err-soft); color: var(--err); box-shadow: var(--shadow-sm); font-size: 13px;
|
|
3282
|
+
}
|
|
3283
|
+
.banner.show { display: flex; }
|
|
3284
|
+
.banner span { flex: 1; }
|
|
3285
|
+
.banner button { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 7px; background: transparent; color: currentColor; cursor: pointer; }
|
|
3286
|
+
.banner button:hover { background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
3287
|
+
.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); }
|
|
2990
3288
|
dialog {
|
|
2991
|
-
border: 1px solid var(--line); border-radius:
|
|
2992
|
-
color: var(--ink); width: min(
|
|
2993
|
-
}
|
|
2994
|
-
dialog
|
|
2995
|
-
dialog
|
|
3289
|
+
border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--bg-elev);
|
|
3290
|
+
color: var(--ink); width: min(720px, calc(100% - 24px)); max-width: calc(100vw - 24px); box-shadow: var(--shadow-lg);
|
|
3291
|
+
}
|
|
3292
|
+
dialog[open] { animation: dialog-in 160ms ease-out; }
|
|
3293
|
+
dialog::backdrop { background: rgb(12 14 20 / 0.58); backdrop-filter: blur(3px); }
|
|
3294
|
+
dialog form, .dialog-body { padding: 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
3295
|
+
.dialog-heading { display: flex; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
|
3296
|
+
.dialog-heading strong { font-size: 18px; letter-spacing: -0.02em; }
|
|
3297
|
+
.dialog-heading span { color: var(--muted); font-size: 12px; }
|
|
2996
3298
|
textarea {
|
|
2997
3299
|
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
2998
3300
|
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
2999
3301
|
font-family: var(--mono); font-size: 12px;
|
|
3000
3302
|
}
|
|
3001
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
3002
|
-
.
|
|
3303
|
+
.dialog-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding-top: 4px; }
|
|
3304
|
+
.dialog-actions .btn-danger { margin-right: auto; }
|
|
3305
|
+
.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); }
|
|
3003
3306
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
3004
3307
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
3005
3308
|
.status-2 { color: var(--ok); }
|
|
3006
3309
|
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
3007
3310
|
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
3311
|
+
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
3312
|
+
.form-grid .wide { grid-column: span 2; }
|
|
3008
3313
|
.route {
|
|
3009
|
-
display: grid; grid-template-columns:
|
|
3010
|
-
padding:
|
|
3011
|
-
}
|
|
3012
|
-
.
|
|
3314
|
+
display: grid; grid-template-columns: 84px minmax(0, 1fr) auto; gap: 10px; align-items: center;
|
|
3315
|
+
padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--bg-elev);
|
|
3316
|
+
}
|
|
3317
|
+
.route:first-child { border-radius: var(--radius) var(--radius) 0 0; }
|
|
3318
|
+
.route:last-child { border-bottom: 0; border-radius: 0 0 var(--radius) var(--radius); }
|
|
3319
|
+
.route-tools { display: flex; align-items: flex-end; gap: 12px; margin-bottom: 12px; }
|
|
3320
|
+
.route-tools .field { flex: 1; }
|
|
3321
|
+
.route-tools input { width: 100%; }
|
|
3322
|
+
.route-count { padding-bottom: 10px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3323
|
+
#route-list { max-height: 34rem; border: 1px solid var(--line); border-radius: var(--radius); overflow: auto; box-shadow: var(--shadow-xs); }
|
|
3324
|
+
.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; }
|
|
3325
|
+
@keyframes view-in { from { transform: translateY(3px); } to { transform: translateY(0); } }
|
|
3326
|
+
@keyframes dialog-in { from { transform: translateY(8px) scale(0.99); } to { transform: translateY(0) scale(1); } }
|
|
3327
|
+
@keyframes pulse { 50% { opacity: 0.45; } }
|
|
3328
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
|
|
3329
|
+
@media (forced-colors: active) { .json-editor pre { display: none; } .json-editor textarea { color: CanvasText; -webkit-text-fill-color: CanvasText; } }
|
|
3013
3330
|
@media (max-width: 800px) {
|
|
3331
|
+
:root { --header: 0px; }
|
|
3014
3332
|
header.top {
|
|
3015
|
-
align-items: stretch; flex-direction: column;
|
|
3016
|
-
padding
|
|
3333
|
+
position: relative; align-items: stretch; flex-direction: column;
|
|
3334
|
+
padding: max(12px, env(safe-area-inset-top)) 14px 12px;
|
|
3017
3335
|
}
|
|
3018
3336
|
input, select, textarea { font-size: 16px; }
|
|
3019
3337
|
.brand, .vendor, .controls { width: 100%; }
|
|
3338
|
+
.brand { gap: 10px; }
|
|
3339
|
+
.vendor { order: 2; border-radius: 9px; }
|
|
3020
3340
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
3341
|
+
.sync-status { display: none; }
|
|
3021
3342
|
/* The layout fills the screen. A single-column grid stretches every auto
|
|
3022
3343
|
row, so the tab bar grew with the leftover height and the buttons floated
|
|
3023
3344
|
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
@@ -3027,14 +3348,23 @@ var renderAdminDocument = (service) => {
|
|
|
3027
3348
|
align-content: start;
|
|
3028
3349
|
}
|
|
3029
3350
|
nav.side {
|
|
3030
|
-
align-self: start; height: auto;
|
|
3351
|
+
position: sticky; top: 0; z-index: 12; align-self: start; height: auto;
|
|
3031
3352
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
3032
3353
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
3033
|
-
padding: 8px;
|
|
3354
|
+
padding: 8px; box-shadow: var(--shadow-xs);
|
|
3034
3355
|
}
|
|
3035
|
-
nav
|
|
3356
|
+
.nav-label { display: none; }
|
|
3357
|
+
nav.side button { width: auto; white-space: nowrap; flex: none; }
|
|
3358
|
+
nav.side button[aria-current="true"]::before { inset: auto 9px -8px; width: auto; height: 3px; border-radius: 3px 3px 0 0; }
|
|
3036
3359
|
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
3037
|
-
|
|
3360
|
+
.form-grid { grid-template-columns: minmax(0, 1fr); }
|
|
3361
|
+
.form-grid .wide { grid-column: auto; }
|
|
3362
|
+
main { padding: 22px 14px 48px; }
|
|
3363
|
+
.view-head { margin-bottom: 18px; }
|
|
3364
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
3365
|
+
.card { padding: 14px; }
|
|
3366
|
+
.card .v { font-size: 20px; }
|
|
3367
|
+
.table-shell { border: 0; background: transparent; box-shadow: none; overflow: visible; }
|
|
3038
3368
|
table, thead, tbody, tr, th, td { display: block; }
|
|
3039
3369
|
thead { display: none; }
|
|
3040
3370
|
tr {
|
|
@@ -3049,50 +3379,63 @@ var renderAdminDocument = (service) => {
|
|
|
3049
3379
|
.route { grid-template-columns: 72px 1fr; }
|
|
3050
3380
|
.route button { grid-column: 1 / -1; }
|
|
3051
3381
|
}
|
|
3382
|
+
@media (max-width: 460px) {
|
|
3383
|
+
.controls label.field { min-width: calc(50% - 4px); }
|
|
3384
|
+
.controls .icon-btn, .controls .btn { flex: 1; }
|
|
3385
|
+
.grid { grid-template-columns: minmax(0, 1fr); }
|
|
3386
|
+
.dialog-actions { flex-wrap: wrap; }
|
|
3387
|
+
.dialog-actions button { flex: 1; }
|
|
3388
|
+
.dialog-actions .btn-danger { flex-basis: 100%; margin-right: 0; }
|
|
3389
|
+
}
|
|
3052
3390
|
</style>
|
|
3053
3391
|
</head>
|
|
3054
3392
|
<body>
|
|
3055
3393
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
3056
3394
|
<header class="top">
|
|
3057
3395
|
<div class="brand">
|
|
3058
|
-
<div class="brand-
|
|
3059
|
-
<
|
|
3060
|
-
|
|
3396
|
+
<div class="brand-lockup">
|
|
3397
|
+
<div class="brand-mark" aria-hidden="true">
|
|
3398
|
+
<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>
|
|
3399
|
+
</div>
|
|
3400
|
+
<div class="brand-id">
|
|
3401
|
+
<strong>${name}</strong>
|
|
3402
|
+
<span>Mock admin</span>
|
|
3403
|
+
</div>
|
|
3061
3404
|
</div>
|
|
3062
3405
|
<div class="vendor" id="vendor" hidden></div>
|
|
3063
3406
|
</div>
|
|
3064
3407
|
<div class="controls">
|
|
3408
|
+
<span class="sync-status" id="sync-status" role="status" aria-live="polite">Ready</span>
|
|
3065
3409
|
<label class="field">Namespace
|
|
3066
3410
|
<select id="namespace" aria-label="Namespace"></select>
|
|
3067
3411
|
</label>
|
|
3068
3412
|
<label class="field">Admin key
|
|
3069
3413
|
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
3070
3414
|
</label>
|
|
3071
|
-
<button class="icon-btn" id="theme" type="button" aria-label="
|
|
3072
|
-
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
3415
|
+
<button class="icon-btn" id="theme" type="button" aria-label="Use dark theme" title="Use dark theme"></button>
|
|
3416
|
+
<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>
|
|
3073
3417
|
</div>
|
|
3074
3418
|
</header>
|
|
3075
3419
|
<div class="layout">
|
|
3076
3420
|
<nav class="side" id="nav" aria-label="Admin sections">
|
|
3077
|
-
<
|
|
3078
|
-
<button type="button" data-view="
|
|
3079
|
-
<button type="button" data-view="
|
|
3080
|
-
<button type="button" data-view="
|
|
3081
|
-
<button type="button" data-view="
|
|
3082
|
-
<button type="button" data-view="
|
|
3421
|
+
<span class="nav-label">Workspace</span>
|
|
3422
|
+
<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>
|
|
3423
|
+
<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>
|
|
3424
|
+
<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>
|
|
3425
|
+
<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>
|
|
3426
|
+
<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>
|
|
3427
|
+
<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>
|
|
3083
3428
|
</nav>
|
|
3084
3429
|
<main id="views">
|
|
3085
|
-
<
|
|
3430
|
+
<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>
|
|
3086
3431
|
<section class="view" id="view-overview">
|
|
3087
|
-
<h2>Overview</h2>
|
|
3088
|
-
<p class="lede">Health, clock, and the collections this namespace is holding.</p>
|
|
3432
|
+
<div class="view-head"><div><h2>Overview</h2><p class="lede">Health, clock, and the collections this namespace is holding.</p></div></div>
|
|
3089
3433
|
<div class="grid" id="overview-cards"></div>
|
|
3090
3434
|
<h3>Recent requests</h3>
|
|
3091
3435
|
<div id="overview-requests"></div>
|
|
3092
3436
|
</section>
|
|
3093
3437
|
<section class="view" id="view-state" hidden>
|
|
3094
|
-
<h2>State</h2>
|
|
3095
|
-
<p class="lede">Every collection in this namespace. Declared fields stay visible before any row exists; stored rows add whatever else they contain.</p>
|
|
3438
|
+
<div class="view-head"><div><h2>State</h2><p class="lede">Inspect and edit every collection in this namespace.</p></div></div>
|
|
3096
3439
|
<div class="split">
|
|
3097
3440
|
<div id="collections"></div>
|
|
3098
3441
|
<div>
|
|
@@ -3105,8 +3448,7 @@ var renderAdminDocument = (service) => {
|
|
|
3105
3448
|
</div>
|
|
3106
3449
|
</section>
|
|
3107
3450
|
<section class="view" id="view-clock" hidden>
|
|
3108
|
-
<h2>Clock</h2>
|
|
3109
|
-
<p class="lede">The clock every timestamp in this mock reads.</p>
|
|
3451
|
+
<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>
|
|
3110
3452
|
<div class="card" id="clock-readout"></div>
|
|
3111
3453
|
<div class="row-actions">
|
|
3112
3454
|
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
@@ -3125,21 +3467,22 @@ var renderAdminDocument = (service) => {
|
|
|
3125
3467
|
</form>
|
|
3126
3468
|
</section>
|
|
3127
3469
|
<section class="view" id="view-faults" hidden>
|
|
3128
|
-
<h2>Faults</h2>
|
|
3129
|
-
<p class="lede">Inject a response, a delay, or a named preset. Rules apply to this namespace.</p>
|
|
3470
|
+
<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>
|
|
3130
3471
|
<form id="fault-form" class="card stack">
|
|
3131
|
-
<
|
|
3132
|
-
<
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
<
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
<
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
<
|
|
3142
|
-
|
|
3472
|
+
<div class="form-grid">
|
|
3473
|
+
<label class="field wide">Preset
|
|
3474
|
+
<select id="fault-preset"><option value="">Custom rule</option></select>
|
|
3475
|
+
</label>
|
|
3476
|
+
<label class="field">Status
|
|
3477
|
+
<input id="fault-status" inputmode="numeric" placeholder="503">
|
|
3478
|
+
</label>
|
|
3479
|
+
<label class="field wide">Path prefix
|
|
3480
|
+
<input id="fault-path" placeholder="/v1">
|
|
3481
|
+
</label>
|
|
3482
|
+
<label class="field">Method
|
|
3483
|
+
<input id="fault-method" placeholder="GET">
|
|
3484
|
+
</label>
|
|
3485
|
+
</div>
|
|
3143
3486
|
<div class="dialog-actions">
|
|
3144
3487
|
<button class="btn-primary" type="submit">Add fault</button>
|
|
3145
3488
|
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
@@ -3148,26 +3491,31 @@ var renderAdminDocument = (service) => {
|
|
|
3148
3491
|
<div id="fault-list"></div>
|
|
3149
3492
|
</section>
|
|
3150
3493
|
<section class="view" id="view-journal" hidden>
|
|
3151
|
-
<h2>Journal</h2>
|
|
3152
|
-
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
3153
|
-
<div class="row-actions">
|
|
3154
|
-
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
3155
|
-
</div>
|
|
3494
|
+
<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>
|
|
3156
3495
|
<div id="journal"></div>
|
|
3157
3496
|
</section>
|
|
3158
3497
|
<section class="view" id="view-routes" hidden>
|
|
3159
|
-
<h2>Routes</h2>
|
|
3160
|
-
<
|
|
3498
|
+
<div class="view-head"><div><h2>Routes</h2><p class="lede">Explore shared controls and service-specific admin endpoints.</p></div></div>
|
|
3499
|
+
<div class="route-tools">
|
|
3500
|
+
<label class="field">Filter routes
|
|
3501
|
+
<input id="route-search" type="search" placeholder="Method, path, or extension" autocomplete="off">
|
|
3502
|
+
</label>
|
|
3503
|
+
<span class="route-count" id="route-count"></span>
|
|
3504
|
+
</div>
|
|
3161
3505
|
<div id="route-list"></div>
|
|
3162
|
-
<form id="route-form" class="card stack">
|
|
3506
|
+
<form id="route-form" class="card stack" style="margin-top: 18px">
|
|
3163
3507
|
<strong id="route-title">Select a route</strong>
|
|
3164
3508
|
<label class="field">JSON body
|
|
3165
|
-
<
|
|
3509
|
+
<div class="json-editor" id="route-editor" hidden>
|
|
3510
|
+
<pre aria-hidden="true"><code id="route-highlight"></code></pre>
|
|
3511
|
+
<textarea id="route-body" placeholder="{ }" spellcheck="false" aria-describedby="route-json-status" disabled></textarea>
|
|
3512
|
+
</div>
|
|
3513
|
+
<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>
|
|
3166
3514
|
</label>
|
|
3167
3515
|
<div class="dialog-actions">
|
|
3168
|
-
<button class="btn-primary" type="submit">Send</button>
|
|
3516
|
+
<button class="btn-primary" id="route-send" type="submit" disabled>Send request</button>
|
|
3169
3517
|
</div>
|
|
3170
|
-
<pre id="route-result">Response will show here.</pre>
|
|
3518
|
+
<pre class="json-view" id="route-result"><code>Response will show here.</code></pre>
|
|
3171
3519
|
</form>
|
|
3172
3520
|
</section>
|
|
3173
3521
|
</main>
|
|
@@ -3175,12 +3523,16 @@ var renderAdminDocument = (service) => {
|
|
|
3175
3523
|
</div>
|
|
3176
3524
|
<dialog id="editor">
|
|
3177
3525
|
<form method="dialog" id="editor-form">
|
|
3178
|
-
<strong id="editor-title">Record</strong>
|
|
3526
|
+
<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>
|
|
3179
3527
|
<label class="field">Id
|
|
3180
3528
|
<input id="editor-id" required>
|
|
3181
3529
|
</label>
|
|
3182
3530
|
<label class="field">JSON value
|
|
3183
|
-
<
|
|
3531
|
+
<div class="json-editor" id="record-editor">
|
|
3532
|
+
<pre aria-hidden="true"><code id="editor-highlight"></code></pre>
|
|
3533
|
+
<textarea id="editor-value" spellcheck="false" aria-describedby="editor-json-status"></textarea>
|
|
3534
|
+
</div>
|
|
3535
|
+
<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>
|
|
3184
3536
|
</label>
|
|
3185
3537
|
<div class="dialog-actions">
|
|
3186
3538
|
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
@@ -3222,8 +3574,8 @@ var describeExtension = (extension) => {
|
|
|
3222
3574
|
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
3223
3575
|
};
|
|
3224
3576
|
};
|
|
3225
|
-
var adminUiRoutes = (service, ui) => {
|
|
3226
|
-
const shell = () => renderAdminDocument(service);
|
|
3577
|
+
var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
3578
|
+
const shell = () => renderAdminDocument(service, adminPrefix);
|
|
3227
3579
|
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
3228
3580
|
return {
|
|
3229
3581
|
"GET /ui": () => htmlResponse(document3()),
|
|
@@ -3840,14 +4192,17 @@ var stateAdminRoutes = (routes) => {
|
|
|
3840
4192
|
};
|
|
3841
4193
|
|
|
3842
4194
|
// ../core/dist/version.js
|
|
3843
|
-
var PACKAGE_VERSION = true ? "
|
|
4195
|
+
var PACKAGE_VERSION = true ? "2.0.0" : UNRELEASED_VERSION;
|
|
3844
4196
|
|
|
3845
4197
|
// ../core/dist/signing.js
|
|
3846
4198
|
var encoder = new TextEncoder();
|
|
3847
4199
|
var toHex = (bytes) => [...bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
3848
4200
|
|
|
3849
4201
|
// ../core/dist/webhooks.js
|
|
3850
|
-
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4202
|
+
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4203
|
+
status,
|
|
4204
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4205
|
+
});
|
|
3851
4206
|
var adminError3 = (status, message) => json4(status, { error: { type: "mockingbird_admin", message } });
|
|
3852
4207
|
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3853
4208
|
var parseEndpoint = (value) => {
|
|
@@ -3955,7 +4310,6 @@ var AT_HEADER = "x-mockingbird-at";
|
|
|
3955
4310
|
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
3956
4311
|
var DEFAULT_NAMESPACE = "default";
|
|
3957
4312
|
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
3958
|
-
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
3959
4313
|
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
3960
4314
|
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
3961
4315
|
var effects = /* @__PURE__ */ new WeakMap();
|
|
@@ -3996,6 +4350,22 @@ var operationMatcher = (document3) => {
|
|
|
3996
4350
|
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
3997
4351
|
};
|
|
3998
4352
|
var createRuntime = (options) => {
|
|
4353
|
+
const adminPrefix = resolveAdminPrefix(options.adminPrefix);
|
|
4354
|
+
const internalPaths = Object.entries(options.document?.paths ?? {}).filter(([, item]) => item["x-mockingbird-internal"] === true).map(([path]) => path);
|
|
4355
|
+
for (const path of internalPaths) {
|
|
4356
|
+
if (!path.startsWith(`${ADMIN_PREFIX}/blobs/`))
|
|
4357
|
+
throw new Error(`internal transport ${path} must live under ${ADMIN_PREFIX}/blobs/`);
|
|
4358
|
+
}
|
|
4359
|
+
assertAdminPrefixAvailable(adminPrefix, Object.keys(options.document?.paths ?? {}).filter((path) => !internalPaths.includes(path)));
|
|
4360
|
+
const internalMatch = (path) => {
|
|
4361
|
+
for (const pattern of internalPaths) {
|
|
4362
|
+
const actual = path.split("/");
|
|
4363
|
+
const expected = pattern.split("/");
|
|
4364
|
+
if (actual.length === expected.length && expected.every((part, i) => part.startsWith("{") || part === actual[i]))
|
|
4365
|
+
return pattern;
|
|
4366
|
+
}
|
|
4367
|
+
return void 0;
|
|
4368
|
+
};
|
|
3999
4369
|
assertAdminUi(options.adminUi);
|
|
4000
4370
|
const sqlite = bootSqlite(options.sqlite);
|
|
4001
4371
|
const clock = options.clock ?? createClock();
|
|
@@ -4041,6 +4411,7 @@ var createRuntime = (options) => {
|
|
|
4041
4411
|
const created = options.create({
|
|
4042
4412
|
namespace: storageNamespace(key),
|
|
4043
4413
|
publicNamespace,
|
|
4414
|
+
adminPrefix,
|
|
4044
4415
|
sqlite,
|
|
4045
4416
|
clock: namespaceClock(publicNamespace),
|
|
4046
4417
|
rng: createdContextRng
|
|
@@ -4273,10 +4644,10 @@ var createRuntime = (options) => {
|
|
|
4273
4644
|
namespaceClock,
|
|
4274
4645
|
namespaceOf(request) {
|
|
4275
4646
|
const url = new URL(request.url);
|
|
4276
|
-
const prefixed =
|
|
4647
|
+
const prefixed = matchNamespacePath(url.pathname, adminPrefix);
|
|
4277
4648
|
const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
|
|
4278
4649
|
const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
|
|
4279
|
-
if (path
|
|
4650
|
+
if (isAdminPath(path, adminPrefix))
|
|
4280
4651
|
return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
|
|
4281
4652
|
if (selected !== void 0)
|
|
4282
4653
|
return selected;
|
|
@@ -4342,7 +4713,7 @@ var createRuntime = (options) => {
|
|
|
4342
4713
|
state: (namespace = DEFAULT_NAMESPACE) => inspectState(stateScope(namespace)),
|
|
4343
4714
|
fetch: async (incoming) => {
|
|
4344
4715
|
let request = incoming;
|
|
4345
|
-
const prefixed =
|
|
4716
|
+
const prefixed = matchNamespacePath(new URL(request.url).pathname, adminPrefix);
|
|
4346
4717
|
if (prefixed) {
|
|
4347
4718
|
const url2 = new URL(request.url);
|
|
4348
4719
|
url2.pathname = prefixed[2] ?? "/";
|
|
@@ -4358,7 +4729,15 @@ var createRuntime = (options) => {
|
|
|
4358
4729
|
signal: request.signal
|
|
4359
4730
|
});
|
|
4360
4731
|
}
|
|
4361
|
-
|
|
4732
|
+
const internalUrl = new URL(request.url);
|
|
4733
|
+
const internalPath = isAdminPath(internalUrl.pathname, adminPrefix) ? `${ADMIN_PREFIX}${internalUrl.pathname.slice(adminPrefix.length)}` : "";
|
|
4734
|
+
const internal = internalMatch(internalPath);
|
|
4735
|
+
const obsoleteInternalPath = adminPrefix !== ADMIN_PREFIX && internalMatch(internalUrl.pathname) && !internal;
|
|
4736
|
+
if (internal) {
|
|
4737
|
+
internalUrl.pathname = internalPath;
|
|
4738
|
+
request = new Request(internalUrl, request);
|
|
4739
|
+
}
|
|
4740
|
+
let namespace = internal ? internalUrl.searchParams.get("namespace") ?? control.namespaceOf(request) : control.namespaceOf(request);
|
|
4362
4741
|
if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
4363
4742
|
const credential = options.credential(request);
|
|
4364
4743
|
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
@@ -4378,7 +4757,9 @@ var createRuntime = (options) => {
|
|
|
4378
4757
|
return copy;
|
|
4379
4758
|
}
|
|
4380
4759
|
};
|
|
4381
|
-
|
|
4760
|
+
if (obsoleteInternalPath)
|
|
4761
|
+
return stamp(adminFail(404, "internal transport is only available under adminPrefix"));
|
|
4762
|
+
const handled = internal ? void 0 : await control.handle(request);
|
|
4382
4763
|
if (handled)
|
|
4383
4764
|
return stamp(handled);
|
|
4384
4765
|
const started = monotonicNow();
|
|
@@ -4419,7 +4800,7 @@ var createRuntime = (options) => {
|
|
|
4419
4800
|
type: "mockingbird_admin",
|
|
4420
4801
|
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
4421
4802
|
}
|
|
4422
|
-
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
4803
|
+
}), { status: 400, headers: { "content-type": "application/json; charset=utf-8" } }));
|
|
4423
4804
|
}
|
|
4424
4805
|
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
4425
4806
|
log(400);
|
|
@@ -4536,15 +4917,16 @@ var createRuntime = (options) => {
|
|
|
4536
4917
|
...options.admin?.(runtime) ?? {},
|
|
4537
4918
|
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
4538
4919
|
// maps `presets`, and a service route must not replace that list with a record.
|
|
4539
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
4920
|
+
...options.presets ? presetRoutes(options.presets, runtime, adminPrefix) : {},
|
|
4540
4921
|
...stateAdminRoutes({
|
|
4541
4922
|
open: stateScope,
|
|
4542
4923
|
afterWrite: (namespace) => {
|
|
4543
4924
|
checkpoint(namespace, "main");
|
|
4544
4925
|
}
|
|
4545
4926
|
}),
|
|
4546
|
-
...adminUiRoutes(options.name, options.adminUi)
|
|
4927
|
+
...adminUiRoutes(options.name, options.adminUi, adminPrefix)
|
|
4547
4928
|
},
|
|
4929
|
+
adminPrefix,
|
|
4548
4930
|
adminKey: options.adminKey
|
|
4549
4931
|
});
|
|
4550
4932
|
return defineMock(runtime);
|
|
@@ -4558,7 +4940,10 @@ var mutableResponse = (response) => {
|
|
|
4558
4940
|
return new Response(response.body, response);
|
|
4559
4941
|
}
|
|
4560
4942
|
};
|
|
4561
|
-
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
4943
|
+
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
4944
|
+
status,
|
|
4945
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4946
|
+
});
|
|
4562
4947
|
var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
|
|
4563
4948
|
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4564
4949
|
var credentialRoutes = (registry) => ({
|
|
@@ -4610,7 +4995,7 @@ var credentialRoutes = (registry) => ({
|
|
|
4610
4995
|
return adminJson(200, { status: "ok" });
|
|
4611
4996
|
}
|
|
4612
4997
|
});
|
|
4613
|
-
var presetRoutes = (presets, runtime) => ({
|
|
4998
|
+
var presetRoutes = (presets, runtime, adminPrefix) => ({
|
|
4614
4999
|
"GET /faults/presets": () => {
|
|
4615
5000
|
const body = {
|
|
4616
5001
|
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
@@ -4620,7 +5005,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
4620
5005
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
4621
5006
|
const name = params.name;
|
|
4622
5007
|
if (!presets[name])
|
|
4623
|
-
return adminFail(404, `no fault preset ${name}; GET /
|
|
5008
|
+
return adminFail(404, `no fault preset ${name}; GET ${adminPrefix}/faults/presets`);
|
|
4624
5009
|
const overrides = isObject(body) ? body : {};
|
|
4625
5010
|
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
4626
5011
|
}
|
|
@@ -5266,6 +5651,7 @@ var createRuntime2 = (options = {}) => createRuntime({
|
|
|
5266
5651
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
5267
5652
|
...options.clock ? { clock: options.clock } : {},
|
|
5268
5653
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
5654
|
+
...options.adminPrefix !== void 0 ? { adminPrefix: options.adminPrefix } : {},
|
|
5269
5655
|
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
5270
5656
|
...options.onLog ? { onLog: options.onLog } : {},
|
|
5271
5657
|
credential: accessKeyCredential,
|
|
@@ -6061,6 +6447,7 @@ var SpeechAPI = class {
|
|
|
6061
6447
|
|
|
6062
6448
|
export {
|
|
6063
6449
|
createClock,
|
|
6450
|
+
resolveAdminPrefix,
|
|
6064
6451
|
parseDuration,
|
|
6065
6452
|
MS_PER_CHARACTER,
|
|
6066
6453
|
durationFor,
|
|
@@ -6090,4 +6477,4 @@ export {
|
|
|
6090
6477
|
accessKeyCredential,
|
|
6091
6478
|
SpeechAPI
|
|
6092
6479
|
};
|
|
6093
|
-
//# sourceMappingURL=chunk-
|
|
6480
|
+
//# sourceMappingURL=chunk-RTZSXNQY.js.map
|