@crvouga/mockingbird-service-payload-cms 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 +2 -2
- package/SUPPORT.md +12 -0
- package/dist/{chunk-COTVMNU6.js → chunk-JSNSLQM4.js} +622 -235
- package/dist/chunk-JSNSLQM4.js.map +7 -0
- package/dist/{chunk-FBRZZAKE.js → chunk-XIEZRTGW.js} +44 -24
- package/dist/chunk-XIEZRTGW.js.map +7 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +4 -3
- package/dist/index.js +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.js +2 -2
- package/openapi.yaml +206 -0
- package/package.json +29 -4
- package/dist/chunk-COTVMNU6.js.map +0 -7
- package/dist/chunk-FBRZZAKE.js.map +0 -7
|
@@ -248,13 +248,31 @@ var Collection = class {
|
|
|
248
248
|
};
|
|
249
249
|
|
|
250
250
|
// ../core/dist/control.js
|
|
251
|
-
var HEALTH_PATH = "/health";
|
|
252
251
|
var ADMIN_PREFIX = "/__admin";
|
|
252
|
+
var resolveAdminPrefix = (prefix = ADMIN_PREFIX) => {
|
|
253
|
+
if (!/^\/[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*$/.test(prefix)) {
|
|
254
|
+
throw new Error("adminPrefix must be an absolute non-root path of literal segments, without a trailing slash");
|
|
255
|
+
}
|
|
256
|
+
return prefix;
|
|
257
|
+
};
|
|
258
|
+
var isAdminPath = (path, prefix) => path === prefix || path.startsWith(`${prefix}/`);
|
|
259
|
+
var matchNamespacePath = (path, prefix) => /^\/ns\/([^/]+)(\/.*)?$/.exec(isAdminPath(path, prefix) ? path.slice(prefix.length) : "");
|
|
260
|
+
var assertAdminPrefixAvailable = (prefix, paths) => {
|
|
261
|
+
const reserved = prefix.split("/").slice(1);
|
|
262
|
+
for (const path of paths) {
|
|
263
|
+
const segments = path.split("/").slice(1);
|
|
264
|
+
if (segments.length < reserved.length || /^\{[^}]+\}$/.test(segments[0] ?? ""))
|
|
265
|
+
continue;
|
|
266
|
+
if (reserved.every((part, i) => segments[i] === part || /^\{[^}]+\}$/.test(segments[i] ?? ""))) {
|
|
267
|
+
throw new Error(`vendor route ${path} collides with adminPrefix ${prefix}; choose a different adminPrefix`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
};
|
|
253
271
|
var ADMIN_KEY_HEADER = "x-mockingbird-admin-key";
|
|
254
272
|
var NAMESPACE_HEADER = "x-mockingbird-namespace";
|
|
255
273
|
var json = (status, body) => new Response(JSON.stringify(body), {
|
|
256
274
|
status,
|
|
257
|
-
headers: { "content-type": "application/json" }
|
|
275
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
258
276
|
});
|
|
259
277
|
var adminError = (status, message) => json(status, { error: { type: "mockingbird_admin", message } });
|
|
260
278
|
var UNITS = {
|
|
@@ -306,11 +324,25 @@ var readJson = async (request) => {
|
|
|
306
324
|
};
|
|
307
325
|
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
308
326
|
var createControlPlane = (context) => {
|
|
327
|
+
const adminPrefix = resolveAdminPrefix(context.adminPrefix);
|
|
309
328
|
const snapshots = /* @__PURE__ */ new Map();
|
|
310
329
|
let snapshotCounter = 0;
|
|
311
330
|
const headerNamespace = (request) => request.headers.get(NAMESPACE_HEADER) ?? context.defaultNamespace;
|
|
312
331
|
const adminNamespace = (request, url) => url.searchParams.get("namespace") ?? headerNamespace(request);
|
|
332
|
+
const health = () => {
|
|
333
|
+
return json(200, {
|
|
334
|
+
...context.describe(),
|
|
335
|
+
status: "ok",
|
|
336
|
+
service: context.name,
|
|
337
|
+
uptimeMs: context.wallNow() - context.startedAt,
|
|
338
|
+
clock: context.clock.state(),
|
|
339
|
+
namespaces: context.namespaces().length,
|
|
340
|
+
adminPrefix,
|
|
341
|
+
adminUi: `${adminPrefix}/ui`
|
|
342
|
+
});
|
|
343
|
+
};
|
|
313
344
|
const builtin = {
|
|
345
|
+
"GET /health": health,
|
|
314
346
|
"GET /": () => json(200, {
|
|
315
347
|
service: context.name,
|
|
316
348
|
routes: [...Object.keys(builtin), ...Object.keys(context.routes)].sort()
|
|
@@ -477,7 +509,14 @@ var createControlPlane = (context) => {
|
|
|
477
509
|
return json(200, { status: "ok" });
|
|
478
510
|
}
|
|
479
511
|
};
|
|
480
|
-
const
|
|
512
|
+
for (const key of Object.keys(context.routes)) {
|
|
513
|
+
if (Object.hasOwn(builtin, key))
|
|
514
|
+
throw new Error(`admin route ${key} overrides a shared route`);
|
|
515
|
+
const path = key.slice(key.indexOf(" ") + 1);
|
|
516
|
+
if (isAdminPath(path, "/ns") || isAdminPath(path, "/blobs"))
|
|
517
|
+
throw new Error(`admin route ${key} overlaps an internal transport`);
|
|
518
|
+
}
|
|
519
|
+
const routes = [...Object.entries(builtin), ...Object.entries(context.routes)].map(([key, handler]) => {
|
|
481
520
|
const space = key.indexOf(" ");
|
|
482
521
|
return { method: key.slice(0, space), pattern: key.slice(space + 1), handler };
|
|
483
522
|
});
|
|
@@ -485,21 +524,13 @@ var createControlPlane = (context) => {
|
|
|
485
524
|
namespaceOf: headerNamespace,
|
|
486
525
|
async handle(request) {
|
|
487
526
|
const url = new URL(request.url);
|
|
488
|
-
if (url.pathname ===
|
|
489
|
-
return
|
|
490
|
-
status: "ok",
|
|
491
|
-
service: context.name,
|
|
492
|
-
uptimeMs: context.wallNow() - context.startedAt,
|
|
493
|
-
clock: context.clock.state(),
|
|
494
|
-
namespaces: context.namespaces().length,
|
|
495
|
-
adminUi: `${ADMIN_PREFIX}/ui`,
|
|
496
|
-
...context.describe()
|
|
497
|
-
});
|
|
527
|
+
if (url.pathname === `${adminPrefix}/health` && request.method === "GET") {
|
|
528
|
+
return health();
|
|
498
529
|
}
|
|
499
|
-
if (url.pathname
|
|
530
|
+
if (!isAdminPath(url.pathname, adminPrefix)) {
|
|
500
531
|
return void 0;
|
|
501
532
|
}
|
|
502
|
-
const path = url.pathname.slice(
|
|
533
|
+
const path = url.pathname.slice(adminPrefix.length) || "/";
|
|
503
534
|
const shell = request.method === "GET" && (path === "/ui" || path === "/ui/");
|
|
504
535
|
if (!shell && context.adminKey !== void 0 && request.headers.get(ADMIN_KEY_HEADER) !== context.adminKey) {
|
|
505
536
|
return adminError(401, `missing or wrong ${ADMIN_KEY_HEADER}`);
|
|
@@ -507,7 +538,12 @@ var createControlPlane = (context) => {
|
|
|
507
538
|
for (const route of routes) {
|
|
508
539
|
if (route.method !== request.method)
|
|
509
540
|
continue;
|
|
510
|
-
|
|
541
|
+
let params;
|
|
542
|
+
try {
|
|
543
|
+
params = matchRoute(route.pattern, path);
|
|
544
|
+
} catch {
|
|
545
|
+
return adminError(400, "invalid path encoding");
|
|
546
|
+
}
|
|
511
547
|
if (!params)
|
|
512
548
|
continue;
|
|
513
549
|
let body;
|
|
@@ -516,15 +552,18 @@ var createControlPlane = (context) => {
|
|
|
516
552
|
} catch {
|
|
517
553
|
return adminError(400, "request body is not valid JSON");
|
|
518
554
|
}
|
|
555
|
+
const namespace = adminNamespace(request, url);
|
|
556
|
+
if (!/^[A-Za-z0-9_.-]{1,64}$/.test(namespace))
|
|
557
|
+
return adminError(400, "invalid namespace");
|
|
519
558
|
return route.handler({
|
|
520
559
|
request,
|
|
521
560
|
url,
|
|
522
561
|
params,
|
|
523
|
-
namespace
|
|
562
|
+
namespace,
|
|
524
563
|
body
|
|
525
564
|
});
|
|
526
565
|
}
|
|
527
|
-
return adminError(404, `no admin route ${request.method} ${path}; GET ${
|
|
566
|
+
return adminError(404, `no admin route ${request.method} ${path}; GET ${adminPrefix} lists them`);
|
|
528
567
|
}
|
|
529
568
|
};
|
|
530
569
|
};
|
|
@@ -1569,6 +1608,34 @@ dialog:not([open]) {
|
|
|
1569
1608
|
function isRecord2(value) {
|
|
1570
1609
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1571
1610
|
}
|
|
1611
|
+
function highlightJsonText(text) {
|
|
1612
|
+
const escapeToken = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
1613
|
+
switch (char) {
|
|
1614
|
+
case "&":
|
|
1615
|
+
return "&";
|
|
1616
|
+
case "<":
|
|
1617
|
+
return "<";
|
|
1618
|
+
case ">":
|
|
1619
|
+
return ">";
|
|
1620
|
+
case '"':
|
|
1621
|
+
return """;
|
|
1622
|
+
default:
|
|
1623
|
+
return "'";
|
|
1624
|
+
}
|
|
1625
|
+
});
|
|
1626
|
+
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;
|
|
1627
|
+
let cursor = 0;
|
|
1628
|
+
let html = "";
|
|
1629
|
+
for (const match of text.matchAll(token)) {
|
|
1630
|
+
const index = match.index ?? 0;
|
|
1631
|
+
const value = match[0];
|
|
1632
|
+
html += escapeToken(text.slice(cursor, index));
|
|
1633
|
+
const kind = value.startsWith('"') ? value.trimEnd().endsWith(":") ? "key" : "string" : value === "true" || value === "false" ? "boolean" : value === "null" ? "null" : "number";
|
|
1634
|
+
html += `<span class="json-${kind}">${escapeToken(value)}</span>`;
|
|
1635
|
+
cursor = index + value.length;
|
|
1636
|
+
}
|
|
1637
|
+
return html + escapeToken(text.slice(cursor));
|
|
1638
|
+
}
|
|
1572
1639
|
function mountAdminBrand(doc, host, service, catalog) {
|
|
1573
1640
|
const clip = (value, max) => {
|
|
1574
1641
|
if (typeof value !== "string")
|
|
@@ -1930,14 +1997,16 @@ function bootAdmin(config) {
|
|
|
1930
1997
|
const state = {
|
|
1931
1998
|
namespace: "default",
|
|
1932
1999
|
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
1933
|
-
view: "overview",
|
|
2000
|
+
view: ["overview", "state", "clock", "faults", "journal", "routes"].includes(location.hash.slice(1)) ? location.hash.slice(1) : "overview",
|
|
1934
2001
|
collection: "",
|
|
1935
2002
|
shape: null,
|
|
1936
2003
|
route: null,
|
|
2004
|
+
routes: [],
|
|
1937
2005
|
editing: null
|
|
1938
2006
|
};
|
|
1939
2007
|
const standard = new Set(config.standardRoutes);
|
|
1940
2008
|
const banner = el("banner", HTMLElement);
|
|
2009
|
+
const bannerMessage = el("banner-message", HTMLElement);
|
|
1941
2010
|
const keyInput = el("admin-key", HTMLInputElement);
|
|
1942
2011
|
keyInput.value = state.key;
|
|
1943
2012
|
const params = new URLSearchParams(location.search);
|
|
@@ -1948,18 +2017,21 @@ function bootAdmin(config) {
|
|
|
1948
2017
|
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
1949
2018
|
params.delete("key");
|
|
1950
2019
|
const next = params.toString();
|
|
1951
|
-
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
2020
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : "") + location.hash);
|
|
1952
2021
|
}
|
|
1953
2022
|
const queryNamespace = params.get("namespace");
|
|
1954
2023
|
if (queryNamespace)
|
|
1955
2024
|
state.namespace = queryNamespace;
|
|
1956
2025
|
const showError = (error) => {
|
|
1957
|
-
|
|
2026
|
+
bannerMessage.textContent = error instanceof Error ? error.message : String(error);
|
|
1958
2027
|
banner.classList.add("show");
|
|
2028
|
+
banner.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
1959
2029
|
};
|
|
1960
2030
|
const clearError = () => {
|
|
1961
2031
|
banner.classList.remove("show");
|
|
2032
|
+
bannerMessage.textContent = "";
|
|
1962
2033
|
};
|
|
2034
|
+
el("banner-dismiss", HTMLButtonElement).addEventListener("click", clearError);
|
|
1963
2035
|
const api = async (method, path, body) => {
|
|
1964
2036
|
const headers = { accept: "application/json" };
|
|
1965
2037
|
if (state.key !== "")
|
|
@@ -2003,16 +2075,21 @@ function bootAdmin(config) {
|
|
|
2003
2075
|
namespace: () => state.namespace
|
|
2004
2076
|
};
|
|
2005
2077
|
const isPanelScript = (value) => typeof value === "function";
|
|
2078
|
+
const themeButton = el("theme", HTMLButtonElement);
|
|
2006
2079
|
const setTheme = (theme) => {
|
|
2007
2080
|
if (theme)
|
|
2008
2081
|
document.documentElement.dataset.theme = theme;
|
|
2009
2082
|
else
|
|
2010
2083
|
delete document.documentElement.dataset.theme;
|
|
2084
|
+
const dark = theme ? theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2085
|
+
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>';
|
|
2086
|
+
const label = dark ? "Use light theme" : "Use dark theme";
|
|
2087
|
+
themeButton.setAttribute("aria-label", label);
|
|
2088
|
+
themeButton.title = label;
|
|
2011
2089
|
};
|
|
2012
2090
|
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
2091
|
+
setTheme(storedTheme === "dark" || storedTheme === "light" ? storedTheme : null);
|
|
2092
|
+
themeButton.addEventListener("click", () => {
|
|
2016
2093
|
const current = document.documentElement.dataset.theme;
|
|
2017
2094
|
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2018
2095
|
const next = dark ? "light" : "dark";
|
|
@@ -2031,6 +2108,7 @@ function bootAdmin(config) {
|
|
|
2031
2108
|
continue;
|
|
2032
2109
|
section.hidden = section.id !== `view-${view}`;
|
|
2033
2110
|
}
|
|
2111
|
+
history.replaceState(null, "", `${location.pathname}${location.search}#${encodeURIComponent(view)}`);
|
|
2034
2112
|
};
|
|
2035
2113
|
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
2036
2114
|
const target = event.target;
|
|
@@ -2045,7 +2123,7 @@ function bootAdmin(config) {
|
|
|
2045
2123
|
show(view);
|
|
2046
2124
|
refresh().catch(showError);
|
|
2047
2125
|
});
|
|
2048
|
-
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>`;
|
|
2126
|
+
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>`;
|
|
2049
2127
|
const table = (columns, rows) => {
|
|
2050
2128
|
if (rows.length === 0)
|
|
2051
2129
|
return '<p class="empty">Nothing here yet.</p>';
|
|
@@ -2055,16 +2133,22 @@ function bootAdmin(config) {
|
|
|
2055
2133
|
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
2056
2134
|
return `<tr${attrs}>${cells}</tr>`;
|
|
2057
2135
|
}).join("");
|
|
2058
|
-
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
2136
|
+
return `<div class="table-shell"><table><thead>${head}</thead><tbody>${body}</tbody></table></div>`;
|
|
2059
2137
|
};
|
|
2060
2138
|
const preview = (value) => {
|
|
2061
2139
|
const text = JSON.stringify(value) ?? "null";
|
|
2062
|
-
|
|
2140
|
+
const clipped = text.length > 180 ? `${text.slice(0, 180)}\u2026` : text;
|
|
2141
|
+
return `<code class="json-inline">${highlightJsonText(clipped)}</code>`;
|
|
2063
2142
|
};
|
|
2064
2143
|
const when = (epoch) => {
|
|
2065
2144
|
const date = new Date(epoch);
|
|
2066
2145
|
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
2067
2146
|
};
|
|
2147
|
+
const journalWhen = (value) => {
|
|
2148
|
+
const numeric = value.trim() === "" ? Number.NaN : Number(value);
|
|
2149
|
+
const epoch = Number.isFinite(numeric) ? numeric : Date.parse(value);
|
|
2150
|
+
return Number.isFinite(epoch) ? when(epoch) : value;
|
|
2151
|
+
};
|
|
2068
2152
|
const clockCard = (epoch) => {
|
|
2069
2153
|
const date = new Date(epoch);
|
|
2070
2154
|
if (Number.isNaN(date.getTime()))
|
|
@@ -2184,7 +2268,7 @@ function bootAdmin(config) {
|
|
|
2184
2268
|
};
|
|
2185
2269
|
const renderOverview = async () => {
|
|
2186
2270
|
const [health, shape, clock, journal] = await Promise.all([
|
|
2187
|
-
originGet(
|
|
2271
|
+
originGet(`${config.adminPrefix}/health`),
|
|
2188
2272
|
api("GET", "/state"),
|
|
2189
2273
|
api("GET", "/clock"),
|
|
2190
2274
|
api("GET", "/requests?limit=8")
|
|
@@ -2201,7 +2285,7 @@ function bootAdmin(config) {
|
|
|
2201
2285
|
},
|
|
2202
2286
|
{
|
|
2203
2287
|
k: "Status",
|
|
2204
|
-
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
2288
|
+
v: `<span class="badge ok">${esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")}</span>`
|
|
2205
2289
|
},
|
|
2206
2290
|
{ k: "Namespace", v: esc(state.namespace) },
|
|
2207
2291
|
{ k: "Collections", v: String(decoded.collections.length) },
|
|
@@ -2288,26 +2372,46 @@ function bootAdmin(config) {
|
|
|
2288
2372
|
const renderJournal = async () => {
|
|
2289
2373
|
const journal = await api("GET", "/requests?limit=100");
|
|
2290
2374
|
el("journal", HTMLElement).innerHTML = table([
|
|
2291
|
-
{ label: "When", cell: (row) => esc(row.at) },
|
|
2292
|
-
{
|
|
2375
|
+
{ label: "When", cell: (row) => esc(journalWhen(row.at)) },
|
|
2376
|
+
{
|
|
2377
|
+
label: "Method",
|
|
2378
|
+
cell: (row) => `<span class="method">${esc(row.method)}</span>`
|
|
2379
|
+
},
|
|
2293
2380
|
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2294
|
-
{
|
|
2381
|
+
{
|
|
2382
|
+
label: "Status",
|
|
2383
|
+
cell: (row) => `<span class="badge ${row.status.startsWith("2") ? "ok" : row.status.startsWith("4") || row.status.startsWith("5") ? "warn" : ""}">${esc(row.status)}</span>`
|
|
2384
|
+
},
|
|
2295
2385
|
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
2296
2386
|
], decodeJournal(journal));
|
|
2297
2387
|
};
|
|
2298
|
-
const
|
|
2299
|
-
const
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2388
|
+
const paintRoutes = () => {
|
|
2389
|
+
const query = el("route-search", HTMLInputElement).value.trim().toLocaleLowerCase();
|
|
2390
|
+
const routes = state.routes.filter((route) => route.toLocaleLowerCase().includes(query));
|
|
2391
|
+
el("route-count", HTMLElement).textContent = `${routes.length} of ${state.routes.length}`;
|
|
2392
|
+
if (state.routes.length === 0) {
|
|
2393
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No admin routes are available.</p>';
|
|
2394
|
+
return;
|
|
2395
|
+
}
|
|
2396
|
+
if (routes.length === 0) {
|
|
2397
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No routes match this filter.</p>';
|
|
2398
|
+
return;
|
|
2399
|
+
}
|
|
2303
2400
|
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2304
2401
|
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2305
2402
|
const parts = route.split(" ");
|
|
2306
2403
|
const method = parts[0] ?? "";
|
|
2307
2404
|
const path = parts.slice(1).join(" ");
|
|
2308
|
-
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>`;
|
|
2405
|
+
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>`;
|
|
2309
2406
|
}).join("");
|
|
2310
2407
|
};
|
|
2408
|
+
const renderRoutes = async () => {
|
|
2409
|
+
const data = await api("GET", "/");
|
|
2410
|
+
if (!isRecord2(data))
|
|
2411
|
+
throw new Error("Route list was not an object");
|
|
2412
|
+
state.routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
2413
|
+
paintRoutes();
|
|
2414
|
+
};
|
|
2311
2415
|
const renderers = {
|
|
2312
2416
|
overview: renderOverview,
|
|
2313
2417
|
state: renderState,
|
|
@@ -2316,19 +2420,50 @@ function bootAdmin(config) {
|
|
|
2316
2420
|
journal: renderJournal,
|
|
2317
2421
|
routes: renderRoutes
|
|
2318
2422
|
};
|
|
2423
|
+
let refreshToken = 0;
|
|
2319
2424
|
async function refresh() {
|
|
2425
|
+
const token = ++refreshToken;
|
|
2320
2426
|
clearError();
|
|
2427
|
+
const main = el("views", HTMLElement);
|
|
2428
|
+
const sync = el("sync-status", HTMLElement);
|
|
2429
|
+
main.setAttribute("aria-busy", "true");
|
|
2430
|
+
sync.classList.add("busy");
|
|
2431
|
+
sync.textContent = "Updating";
|
|
2321
2432
|
const render = renderers[state.view];
|
|
2322
|
-
|
|
2323
|
-
|
|
2433
|
+
try {
|
|
2434
|
+
if (render)
|
|
2435
|
+
await render();
|
|
2436
|
+
if (token === refreshToken)
|
|
2437
|
+
sync.textContent = "Up to date";
|
|
2438
|
+
} finally {
|
|
2439
|
+
if (token === refreshToken) {
|
|
2440
|
+
main.removeAttribute("aria-busy");
|
|
2441
|
+
sync.classList.remove("busy");
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2324
2444
|
}
|
|
2325
2445
|
const delay = (ms) => new Promise((resolve) => {
|
|
2326
2446
|
window.setTimeout(resolve, ms);
|
|
2327
2447
|
});
|
|
2328
|
-
const confirmAction = async (button, pending, done, work) => {
|
|
2448
|
+
const confirmAction = async (button, pending, done, work, confirmLabel) => {
|
|
2329
2449
|
if (button.dataset.pending === "1")
|
|
2330
2450
|
return;
|
|
2331
2451
|
const label = button.textContent ?? "";
|
|
2452
|
+
if (confirmLabel !== void 0 && button.dataset.confirming !== "1") {
|
|
2453
|
+
button.dataset.confirming = "1";
|
|
2454
|
+
button.classList.add("confirming");
|
|
2455
|
+
button.textContent = confirmLabel;
|
|
2456
|
+
window.setTimeout(() => {
|
|
2457
|
+
if (button.dataset.confirming !== "1")
|
|
2458
|
+
return;
|
|
2459
|
+
button.textContent = label;
|
|
2460
|
+
button.classList.remove("confirming");
|
|
2461
|
+
delete button.dataset.confirming;
|
|
2462
|
+
}, 3e3);
|
|
2463
|
+
return;
|
|
2464
|
+
}
|
|
2465
|
+
button.classList.remove("confirming");
|
|
2466
|
+
delete button.dataset.confirming;
|
|
2332
2467
|
const width = button.offsetWidth;
|
|
2333
2468
|
button.dataset.pending = "1";
|
|
2334
2469
|
button.classList.add("pressed");
|
|
@@ -2349,6 +2484,65 @@ function bootAdmin(config) {
|
|
|
2349
2484
|
delete button.dataset.pending;
|
|
2350
2485
|
}
|
|
2351
2486
|
};
|
|
2487
|
+
const wireJsonEditor = (textareaId, highlightId, statusId, shellId, allowEmpty) => {
|
|
2488
|
+
const textarea = el(textareaId, HTMLTextAreaElement);
|
|
2489
|
+
const highlight = el(highlightId, HTMLElement);
|
|
2490
|
+
const status = el(statusId, HTMLElement);
|
|
2491
|
+
const shell = el(shellId, HTMLElement);
|
|
2492
|
+
const sync = () => {
|
|
2493
|
+
const text = textarea.value;
|
|
2494
|
+
highlight.innerHTML = `${highlightJsonText(text)}
|
|
2495
|
+
`;
|
|
2496
|
+
let valid = true;
|
|
2497
|
+
let message = "Valid JSON";
|
|
2498
|
+
if (allowEmpty && text.trim() === "")
|
|
2499
|
+
message = "No body";
|
|
2500
|
+
else {
|
|
2501
|
+
try {
|
|
2502
|
+
readJson2(text);
|
|
2503
|
+
} catch {
|
|
2504
|
+
valid = false;
|
|
2505
|
+
message = "Invalid JSON";
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
shell.classList.toggle("invalid", !valid);
|
|
2509
|
+
status.classList.toggle("invalid", !valid);
|
|
2510
|
+
status.textContent = message;
|
|
2511
|
+
textarea.setAttribute("aria-invalid", valid ? "false" : "true");
|
|
2512
|
+
};
|
|
2513
|
+
const syncScroll = () => {
|
|
2514
|
+
const preview2 = highlight.parentElement;
|
|
2515
|
+
if (!(preview2 instanceof HTMLElement))
|
|
2516
|
+
return;
|
|
2517
|
+
preview2.scrollTop = textarea.scrollTop;
|
|
2518
|
+
preview2.scrollLeft = textarea.scrollLeft;
|
|
2519
|
+
};
|
|
2520
|
+
textarea.addEventListener("input", sync);
|
|
2521
|
+
textarea.addEventListener("scroll", syncScroll);
|
|
2522
|
+
sync();
|
|
2523
|
+
return sync;
|
|
2524
|
+
};
|
|
2525
|
+
const syncRouteEditor = wireJsonEditor("route-body", "route-highlight", "route-json-status", "route-editor", true);
|
|
2526
|
+
const syncRecordEditor = wireJsonEditor("editor-value", "editor-highlight", "editor-json-status", "record-editor", false);
|
|
2527
|
+
for (const button of document.querySelectorAll("[data-format]")) {
|
|
2528
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2529
|
+
continue;
|
|
2530
|
+
button.addEventListener("click", () => {
|
|
2531
|
+
const target = button.dataset.format;
|
|
2532
|
+
if (target === void 0)
|
|
2533
|
+
return;
|
|
2534
|
+
const textarea = document.getElementById(target);
|
|
2535
|
+
if (!(textarea instanceof HTMLTextAreaElement) || textarea.value.trim() === "")
|
|
2536
|
+
return;
|
|
2537
|
+
try {
|
|
2538
|
+
textarea.value = JSON.stringify(readJson2(textarea.value), null, 2) ?? "";
|
|
2539
|
+
textarea.dispatchEvent(new Event("input"));
|
|
2540
|
+
textarea.focus();
|
|
2541
|
+
} catch {
|
|
2542
|
+
showError(new Error("Fix the JSON before formatting it"));
|
|
2543
|
+
}
|
|
2544
|
+
});
|
|
2545
|
+
}
|
|
2352
2546
|
document.addEventListener("click", (event) => {
|
|
2353
2547
|
const target = event.target;
|
|
2354
2548
|
if (!(target instanceof Element))
|
|
@@ -2421,6 +2615,7 @@ function bootAdmin(config) {
|
|
|
2421
2615
|
} else {
|
|
2422
2616
|
editorValue.value = "{\n \n}";
|
|
2423
2617
|
}
|
|
2618
|
+
syncRecordEditor();
|
|
2424
2619
|
editor.showModal();
|
|
2425
2620
|
}
|
|
2426
2621
|
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -2450,10 +2645,10 @@ function bootAdmin(config) {
|
|
|
2450
2645
|
const id = state.editing;
|
|
2451
2646
|
if (!id)
|
|
2452
2647
|
return;
|
|
2453
|
-
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2648
|
+
confirmAction(editorDelete, "Deleting...", "Deleted", () => api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2454
2649
|
editor.close();
|
|
2455
2650
|
return renderState();
|
|
2456
|
-
}).catch(showError);
|
|
2651
|
+
}), "Confirm delete").catch(showError);
|
|
2457
2652
|
});
|
|
2458
2653
|
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2459
2654
|
if (!(button instanceof HTMLButtonElement))
|
|
@@ -2499,10 +2694,10 @@ function bootAdmin(config) {
|
|
|
2499
2694
|
}).then(() => renderFaults()).catch(showError);
|
|
2500
2695
|
});
|
|
2501
2696
|
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2502
|
-
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
|
|
2697
|
+
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults()), "Confirm clear").catch(showError);
|
|
2503
2698
|
});
|
|
2504
2699
|
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2505
|
-
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
|
|
2700
|
+
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal()), "Confirm clear").catch(showError);
|
|
2506
2701
|
});
|
|
2507
2702
|
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2508
2703
|
const target = event.target;
|
|
@@ -2516,9 +2711,15 @@ function bootAdmin(config) {
|
|
|
2516
2711
|
return;
|
|
2517
2712
|
state.route = route;
|
|
2518
2713
|
el("route-title", HTMLElement).textContent = route;
|
|
2714
|
+
el("route-send", HTMLButtonElement).disabled = false;
|
|
2519
2715
|
const method = route.split(" ")[0] ?? "";
|
|
2520
|
-
el("route-body", HTMLTextAreaElement)
|
|
2716
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
2717
|
+
routeBody.disabled = method === "GET" || method === "DELETE";
|
|
2718
|
+
el("route-editor", HTMLElement).hidden = routeBody.disabled;
|
|
2719
|
+
el("route-json-status", HTMLElement).parentElement?.toggleAttribute("hidden", routeBody.disabled);
|
|
2720
|
+
syncRouteEditor();
|
|
2521
2721
|
});
|
|
2722
|
+
el("route-search", HTMLInputElement).addEventListener("input", paintRoutes);
|
|
2522
2723
|
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2523
2724
|
event.preventDefault();
|
|
2524
2725
|
const send = async () => {
|
|
@@ -2540,7 +2741,8 @@ function bootAdmin(config) {
|
|
|
2540
2741
|
}
|
|
2541
2742
|
}
|
|
2542
2743
|
const data = await api(method, concrete, body);
|
|
2543
|
-
|
|
2744
|
+
const text = JSON.stringify(data, null, 2) ?? "null";
|
|
2745
|
+
el("route-result", HTMLElement).innerHTML = `<code>${highlightJsonText(text)}</code>`;
|
|
2544
2746
|
};
|
|
2545
2747
|
send().catch(showError);
|
|
2546
2748
|
});
|
|
@@ -2658,11 +2860,13 @@ function bootAdmin(config) {
|
|
|
2658
2860
|
views.append(section);
|
|
2659
2861
|
mountSqlExplorer(body, panelApi);
|
|
2660
2862
|
}
|
|
2863
|
+
show(document.getElementById(`view-${state.view}`) === null ? "overview" : state.view);
|
|
2661
2864
|
return refresh();
|
|
2662
2865
|
}).catch(showError);
|
|
2663
2866
|
}
|
|
2664
2867
|
var clientPrelude = [
|
|
2665
2868
|
isRecord2,
|
|
2869
|
+
highlightJsonText,
|
|
2666
2870
|
mountAdminBrand,
|
|
2667
2871
|
startAdminBrand,
|
|
2668
2872
|
faultPresetList,
|
|
@@ -2685,6 +2889,7 @@ ${entry}(${payload});
|
|
|
2685
2889
|
var defineMock = (runtime) => runtime;
|
|
2686
2890
|
var STANDARD_ADMIN_ROUTES = [
|
|
2687
2891
|
"GET /",
|
|
2892
|
+
"GET /health",
|
|
2688
2893
|
"POST /reset",
|
|
2689
2894
|
"GET /namespaces",
|
|
2690
2895
|
"GET /clock",
|
|
@@ -2764,11 +2969,11 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
2764
2969
|
}
|
|
2765
2970
|
});
|
|
2766
2971
|
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
2767
|
-
var renderAdminDocument = (service) => {
|
|
2972
|
+
var renderAdminDocument = (service, adminPrefix = ADMIN_PREFIX) => {
|
|
2768
2973
|
const name = escapeHtml(service);
|
|
2769
2974
|
const script = adminClientSource({
|
|
2770
2975
|
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
2771
|
-
adminPrefix
|
|
2976
|
+
adminPrefix,
|
|
2772
2977
|
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
2773
2978
|
brandsUrl: ADMIN_BRANDS_URL,
|
|
2774
2979
|
service
|
|
@@ -2785,75 +2990,99 @@ var renderAdminDocument = (service) => {
|
|
|
2785
2990
|
${CSS_RESET}
|
|
2786
2991
|
:root {
|
|
2787
2992
|
color-scheme: light;
|
|
2788
|
-
--bg: #
|
|
2993
|
+
--bg: #f7f8fb;
|
|
2789
2994
|
--bg-elev: #ffffff;
|
|
2790
|
-
--bg-muted: #
|
|
2791
|
-
--bg-inset: #
|
|
2792
|
-
--line: #
|
|
2793
|
-
--line-strong: #
|
|
2794
|
-
--ink: #
|
|
2795
|
-
--muted: #
|
|
2796
|
-
--faint: #
|
|
2797
|
-
--accent: #
|
|
2995
|
+
--bg-muted: #f1f3f7;
|
|
2996
|
+
--bg-inset: #f8f9fc;
|
|
2997
|
+
--line: #e4e7ec;
|
|
2998
|
+
--line-strong: #cfd4dc;
|
|
2999
|
+
--ink: #101828;
|
|
3000
|
+
--muted: #475467;
|
|
3001
|
+
--faint: #667085;
|
|
3002
|
+
--accent: #6941c6;
|
|
3003
|
+
--accent-hover: #5933b4;
|
|
2798
3004
|
--accent-ink: #ffffff;
|
|
2799
|
-
--accent-soft: #
|
|
2800
|
-
--ok: #
|
|
2801
|
-
--ok-soft: #
|
|
2802
|
-
--warn: #
|
|
2803
|
-
--warn-soft: #
|
|
2804
|
-
--err: #
|
|
2805
|
-
--err-soft: #
|
|
2806
|
-
--
|
|
3005
|
+
--accent-soft: #f4f0ff;
|
|
3006
|
+
--ok: #067647;
|
|
3007
|
+
--ok-soft: #ecfdf3;
|
|
3008
|
+
--warn: #b54708;
|
|
3009
|
+
--warn-soft: #fffaeb;
|
|
3010
|
+
--err: #b42318;
|
|
3011
|
+
--err-soft: #fef3f2;
|
|
3012
|
+
--code-key: #6941c6;
|
|
3013
|
+
--code-string: #067647;
|
|
3014
|
+
--code-number: #175cd3;
|
|
3015
|
+
--code-boolean: #c11574;
|
|
3016
|
+
--code-null: #667085;
|
|
3017
|
+
--shadow-xs: 0 1px 2px rgb(16 24 40 / 0.05);
|
|
3018
|
+
--shadow-sm: 0 1px 3px rgb(16 24 40 / 0.08), 0 1px 2px rgb(16 24 40 / 0.04);
|
|
3019
|
+
--shadow-lg: 0 18px 48px rgb(16 24 40 / 0.16), 0 4px 12px rgb(16 24 40 / 0.08);
|
|
2807
3020
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2808
3021
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
2809
|
-
--radius:
|
|
2810
|
-
--header:
|
|
2811
|
-
--nav:
|
|
3022
|
+
--radius: 12px;
|
|
3023
|
+
--header: 72px;
|
|
3024
|
+
--nav: 224px;
|
|
2812
3025
|
}
|
|
2813
3026
|
:root[data-theme="dark"] {
|
|
2814
3027
|
color-scheme: dark;
|
|
2815
|
-
--bg: #
|
|
2816
|
-
--bg-elev: #
|
|
2817
|
-
--bg-muted: #
|
|
2818
|
-
--bg-inset: #
|
|
2819
|
-
--line: #
|
|
2820
|
-
--line-strong: #
|
|
2821
|
-
--ink: #
|
|
2822
|
-
--muted: #
|
|
2823
|
-
--faint: #
|
|
2824
|
-
--accent: #
|
|
2825
|
-
--accent-
|
|
2826
|
-
--accent-
|
|
2827
|
-
--
|
|
2828
|
-
--ok
|
|
2829
|
-
--
|
|
2830
|
-
--warn
|
|
2831
|
-
--
|
|
2832
|
-
--err
|
|
2833
|
-
--
|
|
3028
|
+
--bg: #0c0e14;
|
|
3029
|
+
--bg-elev: #151821;
|
|
3030
|
+
--bg-muted: #20232e;
|
|
3031
|
+
--bg-inset: #11141c;
|
|
3032
|
+
--line: #2b303d;
|
|
3033
|
+
--line-strong: #414858;
|
|
3034
|
+
--ink: #f5f7fa;
|
|
3035
|
+
--muted: #b6bdc9;
|
|
3036
|
+
--faint: #8d96a7;
|
|
3037
|
+
--accent: #9e77ed;
|
|
3038
|
+
--accent-hover: #b692f6;
|
|
3039
|
+
--accent-ink: #ffffff;
|
|
3040
|
+
--accent-soft: #2b2142;
|
|
3041
|
+
--ok: #75e0a7;
|
|
3042
|
+
--ok-soft: #102a20;
|
|
3043
|
+
--warn: #fec84b;
|
|
3044
|
+
--warn-soft: #302611;
|
|
3045
|
+
--err: #fda29b;
|
|
3046
|
+
--err-soft: #321817;
|
|
3047
|
+
--code-key: #c3a6ff;
|
|
3048
|
+
--code-string: #75e0a7;
|
|
3049
|
+
--code-number: #84adff;
|
|
3050
|
+
--code-boolean: #f9a8d4;
|
|
3051
|
+
--code-null: #98a2b3;
|
|
3052
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3053
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3054
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2834
3055
|
}
|
|
2835
3056
|
@media (prefers-color-scheme: dark) {
|
|
2836
3057
|
:root:not([data-theme="light"]) {
|
|
2837
3058
|
color-scheme: dark;
|
|
2838
|
-
--bg: #
|
|
2839
|
-
--bg-elev: #
|
|
2840
|
-
--bg-muted: #
|
|
2841
|
-
--bg-inset: #
|
|
2842
|
-
--line: #
|
|
2843
|
-
--line-strong: #
|
|
2844
|
-
--ink: #
|
|
2845
|
-
--muted: #
|
|
2846
|
-
--faint: #
|
|
2847
|
-
--accent: #
|
|
2848
|
-
--accent-
|
|
2849
|
-
--accent-
|
|
2850
|
-
--
|
|
2851
|
-
--ok
|
|
2852
|
-
--
|
|
2853
|
-
--warn
|
|
2854
|
-
--
|
|
2855
|
-
--err
|
|
2856
|
-
--
|
|
3059
|
+
--bg: #0c0e14;
|
|
3060
|
+
--bg-elev: #151821;
|
|
3061
|
+
--bg-muted: #20232e;
|
|
3062
|
+
--bg-inset: #11141c;
|
|
3063
|
+
--line: #2b303d;
|
|
3064
|
+
--line-strong: #414858;
|
|
3065
|
+
--ink: #f5f7fa;
|
|
3066
|
+
--muted: #b6bdc9;
|
|
3067
|
+
--faint: #8d96a7;
|
|
3068
|
+
--accent: #9e77ed;
|
|
3069
|
+
--accent-hover: #b692f6;
|
|
3070
|
+
--accent-ink: #ffffff;
|
|
3071
|
+
--accent-soft: #2b2142;
|
|
3072
|
+
--ok: #75e0a7;
|
|
3073
|
+
--ok-soft: #102a20;
|
|
3074
|
+
--warn: #fec84b;
|
|
3075
|
+
--warn-soft: #302611;
|
|
3076
|
+
--err: #fda29b;
|
|
3077
|
+
--err-soft: #321817;
|
|
3078
|
+
--code-key: #c3a6ff;
|
|
3079
|
+
--code-string: #75e0a7;
|
|
3080
|
+
--code-number: #84adff;
|
|
3081
|
+
--code-boolean: #f9a8d4;
|
|
3082
|
+
--code-null: #98a2b3;
|
|
3083
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3084
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3085
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2857
3086
|
}
|
|
2858
3087
|
}
|
|
2859
3088
|
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
@@ -2871,13 +3100,17 @@ var renderAdminDocument = (service) => {
|
|
|
2871
3100
|
padding-right: env(safe-area-inset-right);
|
|
2872
3101
|
-webkit-text-size-adjust: 100%;
|
|
2873
3102
|
}
|
|
2874
|
-
|
|
3103
|
+
::selection { background: color-mix(in srgb, var(--accent) 28%, transparent); }
|
|
3104
|
+
:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px; }
|
|
3105
|
+
button, input, select, textarea { transition: border-color 140ms ease, box-shadow 140ms ease, transform 100ms ease; }
|
|
3106
|
+
button:disabled, input:disabled, select:disabled, textarea:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
2875
3107
|
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
2876
3108
|
header.top {
|
|
2877
|
-
position: sticky; top: 0; z-index:
|
|
3109
|
+
position: sticky; top: 0; z-index: 20;
|
|
2878
3110
|
display: flex; align-items: center; gap: 12px;
|
|
2879
|
-
min-height: var(--header); padding:
|
|
2880
|
-
background: var(--bg-elev);
|
|
3111
|
+
min-height: var(--header); padding: 12px 20px;
|
|
3112
|
+
background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
|
|
3113
|
+
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
2881
3114
|
border-bottom: 1px solid var(--line);
|
|
2882
3115
|
min-width: 0; max-width: 100%;
|
|
2883
3116
|
}
|
|
@@ -2885,12 +3118,21 @@ var renderAdminDocument = (service) => {
|
|
|
2885
3118
|
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
2886
3119
|
min-width: 0; max-width: 100%; margin-right: auto;
|
|
2887
3120
|
}
|
|
2888
|
-
.brand-
|
|
2889
|
-
.brand-
|
|
3121
|
+
.brand-lockup { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
3122
|
+
.brand-mark {
|
|
3123
|
+
width: 38px; height: 38px; display: grid; place-items: center; flex: none;
|
|
3124
|
+
border-radius: 11px; color: #fff;
|
|
3125
|
+
background: linear-gradient(145deg, #7f56d9 0%, #53389e 100%);
|
|
3126
|
+
box-shadow: 0 6px 16px rgb(105 65 198 / 0.24), inset 0 1px 0 rgb(255 255 255 / 0.25);
|
|
3127
|
+
}
|
|
3128
|
+
.brand-mark svg { width: 22px; height: 22px; }
|
|
3129
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; line-height: 1.25; }
|
|
3130
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
2890
3131
|
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
2891
3132
|
.vendor {
|
|
2892
3133
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
2893
3134
|
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
3135
|
+
padding: 5px 8px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-inset);
|
|
2894
3136
|
}
|
|
2895
3137
|
.vendor[hidden] { display: none; }
|
|
2896
3138
|
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
@@ -2902,12 +3144,17 @@ var renderAdminDocument = (service) => {
|
|
|
2902
3144
|
.vendor-sep { color: var(--faint); }
|
|
2903
3145
|
.vendor-link { color: inherit; text-decoration: none; }
|
|
2904
3146
|
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
2905
|
-
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items:
|
|
2906
|
-
|
|
3147
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
|
|
3148
|
+
.sync-status { align-self: center; display: flex; align-items: center; gap: 6px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3149
|
+
.sync-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
|
|
3150
|
+
.sync-status.busy::before { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); animation: pulse 1.2s ease-in-out infinite; }
|
|
3151
|
+
label.field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; font-weight: 600; color: var(--faint); }
|
|
2907
3152
|
label.field input, label.field select, .controls input[type="password"] {
|
|
2908
|
-
background-color: var(--bg-
|
|
2909
|
-
padding:
|
|
3153
|
+
background-color: var(--bg-elev); border: 1px solid var(--line-strong); border-radius: 9px;
|
|
3154
|
+
padding: 8px 10px; min-height: 38px; line-height: 1.2; box-shadow: var(--shadow-xs);
|
|
2910
3155
|
}
|
|
3156
|
+
label.field input:hover, label.field select:hover, textarea:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--line-strong)); }
|
|
3157
|
+
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; }
|
|
2911
3158
|
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
2912
3159
|
the text fields in the same form. Paint it like those fields. */
|
|
2913
3160
|
label.field select {
|
|
@@ -2924,12 +3171,20 @@ var renderAdminDocument = (service) => {
|
|
|
2924
3171
|
dialog .field > textarea { width: 100%; }
|
|
2925
3172
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
2926
3173
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
.btn
|
|
3174
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
|
3175
|
+
border-radius: 9px; min-height: 38px; padding: 0 13px; border: 1px solid var(--line-strong);
|
|
3176
|
+
background: var(--bg-elev); color: var(--ink); font-size: 13px; font-weight: 600; line-height: 1;
|
|
3177
|
+
box-shadow: var(--shadow-xs);
|
|
3178
|
+
}
|
|
3179
|
+
.icon-btn { width: 38px; padding: 0; }
|
|
3180
|
+
.icon-btn svg, .btn svg, .btn-primary svg { width: 16px; height: 16px; }
|
|
3181
|
+
.icon-btn:hover, .btn:hover { background: var(--bg-muted); border-color: var(--line-strong); }
|
|
3182
|
+
.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; }
|
|
3183
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
3184
|
+
.btn-quiet { background: transparent; border-color: transparent; box-shadow: none; }
|
|
3185
|
+
.btn-quiet:hover { background: var(--bg-muted); }
|
|
3186
|
+
.btn-danger { color: var(--err); border-color: color-mix(in srgb, var(--err) 25%, var(--line)); }
|
|
3187
|
+
.btn-danger:hover, .btn-danger.confirming { background: var(--err-soft); border-color: color-mix(in srgb, var(--err) 45%, var(--line)); }
|
|
2933
3188
|
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
2934
3189
|
so the press itself has to stay visible. */
|
|
2935
3190
|
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
@@ -2945,93 +3200,159 @@ var renderAdminDocument = (service) => {
|
|
|
2945
3200
|
flex: 1 0 auto; min-width: 0;
|
|
2946
3201
|
}
|
|
2947
3202
|
nav.side {
|
|
2948
|
-
|
|
3203
|
+
position: sticky; top: var(--header); align-self: start; height: calc(100vh - var(--header));
|
|
3204
|
+
border-right: 1px solid var(--line); padding: 18px 12px;
|
|
2949
3205
|
display: flex; flex-direction: column; gap: 4px;
|
|
2950
|
-
background: var(--bg-elev);
|
|
3206
|
+
background: var(--bg-elev); overflow-y: auto;
|
|
2951
3207
|
}
|
|
3208
|
+
.nav-label { padding: 0 10px 8px; color: var(--faint); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
|
|
2952
3209
|
nav.side button {
|
|
3210
|
+
position: relative; display: flex; align-items: center; gap: 10px; width: 100%;
|
|
2953
3211
|
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
2954
|
-
padding: 9px 10px; min-height:
|
|
2955
|
-
}
|
|
2956
|
-
nav.side button
|
|
2957
|
-
|
|
2958
|
-
.
|
|
2959
|
-
.
|
|
2960
|
-
.
|
|
3212
|
+
padding: 9px 10px; min-height: 42px; color: var(--muted); font-size: 13px; font-weight: 550;
|
|
3213
|
+
}
|
|
3214
|
+
nav.side button:hover { background: var(--bg-muted); color: var(--ink); }
|
|
3215
|
+
nav.side button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 650; }
|
|
3216
|
+
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); }
|
|
3217
|
+
.nav-icon { width: 18px; height: 18px; display: grid; place-items: center; flex: none; }
|
|
3218
|
+
.nav-icon svg { width: 18px; height: 18px; }
|
|
3219
|
+
main { padding: 32px clamp(20px, 4vw, 48px) 64px; max-width: 1320px; width: 100%; min-width: 0; }
|
|
3220
|
+
main[aria-busy="true"] .view { opacity: 0.62; }
|
|
3221
|
+
.view { animation: view-in 180ms ease-out; }
|
|
3222
|
+
.view[hidden] { display: none; }
|
|
3223
|
+
.view-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
|
|
3224
|
+
.view h2 { margin: 0 0 5px; font-size: clamp(24px, 3vw, 30px); line-height: 1.2; letter-spacing: -0.035em; }
|
|
3225
|
+
.view h3 { margin: 24px 0 10px; font-size: 15px; letter-spacing: -0.01em; }
|
|
3226
|
+
.lede { margin: 0; color: var(--muted); max-width: 72ch; font-size: 14px; }
|
|
3227
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 175px), 1fr)); gap: 14px; }
|
|
2961
3228
|
.card {
|
|
2962
3229
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
2963
|
-
padding:
|
|
3230
|
+
padding: 18px; box-shadow: var(--shadow-sm);
|
|
2964
3231
|
}
|
|
2965
|
-
.card
|
|
2966
|
-
.card
|
|
3232
|
+
.stat-card { position: relative; overflow: hidden; }
|
|
3233
|
+
.stat-card::after { content: ""; position: absolute; inset: 0 0 auto; height: 2px; background: linear-gradient(90deg, var(--accent), transparent 78%); opacity: 0.7; }
|
|
3234
|
+
.card .k { color: var(--faint); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
3235
|
+
.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; }
|
|
2967
3236
|
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
2968
|
-
.
|
|
3237
|
+
.section-card { background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-xs); overflow: hidden; }
|
|
3238
|
+
.section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
|
|
3239
|
+
.section-title h3 { margin: 0; }
|
|
3240
|
+
.split { display: grid; grid-template-columns: minmax(0, 250px) minmax(0, 1fr); gap: 18px; align-items: start; }
|
|
3241
|
+
#collections { padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); box-shadow: var(--shadow-xs); }
|
|
2969
3242
|
.collection {
|
|
2970
3243
|
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
2971
|
-
border-radius:
|
|
3244
|
+
border-radius: 9px; padding: 10px 11px; min-height: 48px; color: var(--ink); cursor: pointer;
|
|
2972
3245
|
}
|
|
2973
|
-
.collection
|
|
3246
|
+
.collection:hover { background: var(--bg-muted); }
|
|
3247
|
+
.collection[aria-current="true"] { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 18%, var(--line)); color: var(--accent); }
|
|
2974
3248
|
.collection small { display: block; color: var(--faint); }
|
|
2975
3249
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
2976
|
-
.chip {
|
|
2977
|
-
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
2978
|
-
border-radius: 999px; padding: 4px 8px;
|
|
2979
|
-
}
|
|
3250
|
+
.chip { font-family: var(--mono); font-size: 11px; background: var(--bg-muted); border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; }
|
|
2980
3251
|
.chip i { color: var(--faint); font-style: normal; }
|
|
2981
3252
|
.sql-layout {
|
|
2982
3253
|
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
2983
3254
|
}
|
|
2984
|
-
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
2985
|
-
.sql-tables button { text-align: left; }
|
|
3255
|
+
.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); }
|
|
3256
|
+
.sql-tables button { text-align: left; border: 0; border-radius: 8px; padding: 9px 10px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
3257
|
+
.sql-tables button:hover, .sql-tables button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); }
|
|
2986
3258
|
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
2987
|
-
table { width: 100%; border-
|
|
2988
|
-
|
|
2989
|
-
th {
|
|
3259
|
+
.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); }
|
|
3260
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
3261
|
+
th, td { text-align: left; padding: 11px 13px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
3262
|
+
th { color: var(--faint); background: var(--bg-inset); font-weight: 650; font-size: 11px; text-transform: uppercase; letter-spacing: 0.045em; }
|
|
3263
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
2990
3264
|
tr.clickable { cursor: pointer; }
|
|
2991
|
-
tr.clickable:hover { background: var(--
|
|
3265
|
+
tr.clickable:hover { background: var(--accent-soft); }
|
|
2992
3266
|
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
2993
3267
|
pre {
|
|
2994
3268
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
2995
|
-
background: var(--bg-inset); border-radius: 10px; padding:
|
|
2996
|
-
}
|
|
2997
|
-
.
|
|
3269
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 10px; padding: 14px; font-size: 12px; line-height: 1.65;
|
|
3270
|
+
}
|
|
3271
|
+
.json-key { color: var(--code-key); }
|
|
3272
|
+
.json-string { color: var(--code-string); }
|
|
3273
|
+
.json-number { color: var(--code-number); }
|
|
3274
|
+
.json-boolean { color: var(--code-boolean); }
|
|
3275
|
+
.json-null { color: var(--code-null); font-style: italic; }
|
|
3276
|
+
.json-inline { display: block; max-width: 44rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
3277
|
+
.json-view { position: relative; max-height: 26rem; overflow: auto; }
|
|
3278
|
+
.json-editor { position: relative; min-height: 190px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--bg-inset); overflow: hidden; }
|
|
3279
|
+
.json-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); }
|
|
3280
|
+
.json-editor.invalid { border-color: var(--err); }
|
|
3281
|
+
.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; }
|
|
3282
|
+
.json-editor pre { position: absolute; inset: 0; pointer-events: none; background: transparent; }
|
|
3283
|
+
.json-editor textarea { position: relative; z-index: 1; resize: vertical; background: transparent; color: transparent; caret-color: var(--ink); -webkit-text-fill-color: transparent; }
|
|
3284
|
+
.json-editor textarea::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); }
|
|
3285
|
+
.editor-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; }
|
|
3286
|
+
.json-status { display: inline-flex; align-items: center; gap: 6px; color: var(--ok); font-size: 11px; }
|
|
3287
|
+
.json-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
3288
|
+
.json-status.invalid { color: var(--err); }
|
|
3289
|
+
.format-json { border: 0; padding: 3px 5px; background: transparent; color: var(--accent); font-size: 11px; font-weight: 650; cursor: pointer; }
|
|
3290
|
+
.row-actions { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 14px 0; }
|
|
2998
3291
|
.banner {
|
|
2999
|
-
display: none;
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3292
|
+
display: none; position: sticky; top: calc(var(--header) + 12px); z-index: 15;
|
|
3293
|
+
align-items: center; gap: 10px; margin: 0 0 16px; padding: 11px 12px 11px 14px;
|
|
3294
|
+
border: 1px solid color-mix(in srgb, var(--err) 30%, var(--line)); border-radius: 10px;
|
|
3295
|
+
background: var(--err-soft); color: var(--err); box-shadow: var(--shadow-sm); font-size: 13px;
|
|
3296
|
+
}
|
|
3297
|
+
.banner.show { display: flex; }
|
|
3298
|
+
.banner span { flex: 1; }
|
|
3299
|
+
.banner button { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 7px; background: transparent; color: currentColor; cursor: pointer; }
|
|
3300
|
+
.banner button:hover { background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
3301
|
+
.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); }
|
|
3004
3302
|
dialog {
|
|
3005
|
-
border: 1px solid var(--line); border-radius:
|
|
3006
|
-
color: var(--ink); width: min(
|
|
3007
|
-
}
|
|
3008
|
-
dialog
|
|
3009
|
-
dialog
|
|
3303
|
+
border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--bg-elev);
|
|
3304
|
+
color: var(--ink); width: min(720px, calc(100% - 24px)); max-width: calc(100vw - 24px); box-shadow: var(--shadow-lg);
|
|
3305
|
+
}
|
|
3306
|
+
dialog[open] { animation: dialog-in 160ms ease-out; }
|
|
3307
|
+
dialog::backdrop { background: rgb(12 14 20 / 0.58); backdrop-filter: blur(3px); }
|
|
3308
|
+
dialog form, .dialog-body { padding: 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
3309
|
+
.dialog-heading { display: flex; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
|
3310
|
+
.dialog-heading strong { font-size: 18px; letter-spacing: -0.02em; }
|
|
3311
|
+
.dialog-heading span { color: var(--muted); font-size: 12px; }
|
|
3010
3312
|
textarea {
|
|
3011
3313
|
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
3012
3314
|
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
3013
3315
|
font-family: var(--mono); font-size: 12px;
|
|
3014
3316
|
}
|
|
3015
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
3016
|
-
.
|
|
3317
|
+
.dialog-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding-top: 4px; }
|
|
3318
|
+
.dialog-actions .btn-danger { margin-right: auto; }
|
|
3319
|
+
.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); }
|
|
3017
3320
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
3018
3321
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
3019
3322
|
.status-2 { color: var(--ok); }
|
|
3020
3323
|
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
3021
3324
|
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
3325
|
+
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
3326
|
+
.form-grid .wide { grid-column: span 2; }
|
|
3022
3327
|
.route {
|
|
3023
|
-
display: grid; grid-template-columns:
|
|
3024
|
-
padding:
|
|
3025
|
-
}
|
|
3026
|
-
.
|
|
3328
|
+
display: grid; grid-template-columns: 84px minmax(0, 1fr) auto; gap: 10px; align-items: center;
|
|
3329
|
+
padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--bg-elev);
|
|
3330
|
+
}
|
|
3331
|
+
.route:first-child { border-radius: var(--radius) var(--radius) 0 0; }
|
|
3332
|
+
.route:last-child { border-bottom: 0; border-radius: 0 0 var(--radius) var(--radius); }
|
|
3333
|
+
.route-tools { display: flex; align-items: flex-end; gap: 12px; margin-bottom: 12px; }
|
|
3334
|
+
.route-tools .field { flex: 1; }
|
|
3335
|
+
.route-tools input { width: 100%; }
|
|
3336
|
+
.route-count { padding-bottom: 10px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3337
|
+
#route-list { max-height: 34rem; border: 1px solid var(--line); border-radius: var(--radius); overflow: auto; box-shadow: var(--shadow-xs); }
|
|
3338
|
+
.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; }
|
|
3339
|
+
@keyframes view-in { from { transform: translateY(3px); } to { transform: translateY(0); } }
|
|
3340
|
+
@keyframes dialog-in { from { transform: translateY(8px) scale(0.99); } to { transform: translateY(0) scale(1); } }
|
|
3341
|
+
@keyframes pulse { 50% { opacity: 0.45; } }
|
|
3342
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
|
|
3343
|
+
@media (forced-colors: active) { .json-editor pre { display: none; } .json-editor textarea { color: CanvasText; -webkit-text-fill-color: CanvasText; } }
|
|
3027
3344
|
@media (max-width: 800px) {
|
|
3345
|
+
:root { --header: 0px; }
|
|
3028
3346
|
header.top {
|
|
3029
|
-
align-items: stretch; flex-direction: column;
|
|
3030
|
-
padding
|
|
3347
|
+
position: relative; align-items: stretch; flex-direction: column;
|
|
3348
|
+
padding: max(12px, env(safe-area-inset-top)) 14px 12px;
|
|
3031
3349
|
}
|
|
3032
3350
|
input, select, textarea { font-size: 16px; }
|
|
3033
3351
|
.brand, .vendor, .controls { width: 100%; }
|
|
3352
|
+
.brand { gap: 10px; }
|
|
3353
|
+
.vendor { order: 2; border-radius: 9px; }
|
|
3034
3354
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
3355
|
+
.sync-status { display: none; }
|
|
3035
3356
|
/* The layout fills the screen. A single-column grid stretches every auto
|
|
3036
3357
|
row, so the tab bar grew with the leftover height and the buttons floated
|
|
3037
3358
|
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
@@ -3041,14 +3362,23 @@ var renderAdminDocument = (service) => {
|
|
|
3041
3362
|
align-content: start;
|
|
3042
3363
|
}
|
|
3043
3364
|
nav.side {
|
|
3044
|
-
align-self: start; height: auto;
|
|
3365
|
+
position: sticky; top: 0; z-index: 12; align-self: start; height: auto;
|
|
3045
3366
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
3046
3367
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
3047
|
-
padding: 8px;
|
|
3368
|
+
padding: 8px; box-shadow: var(--shadow-xs);
|
|
3048
3369
|
}
|
|
3049
|
-
nav
|
|
3370
|
+
.nav-label { display: none; }
|
|
3371
|
+
nav.side button { width: auto; white-space: nowrap; flex: none; }
|
|
3372
|
+
nav.side button[aria-current="true"]::before { inset: auto 9px -8px; width: auto; height: 3px; border-radius: 3px 3px 0 0; }
|
|
3050
3373
|
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
3051
|
-
|
|
3374
|
+
.form-grid { grid-template-columns: minmax(0, 1fr); }
|
|
3375
|
+
.form-grid .wide { grid-column: auto; }
|
|
3376
|
+
main { padding: 22px 14px 48px; }
|
|
3377
|
+
.view-head { margin-bottom: 18px; }
|
|
3378
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
3379
|
+
.card { padding: 14px; }
|
|
3380
|
+
.card .v { font-size: 20px; }
|
|
3381
|
+
.table-shell { border: 0; background: transparent; box-shadow: none; overflow: visible; }
|
|
3052
3382
|
table, thead, tbody, tr, th, td { display: block; }
|
|
3053
3383
|
thead { display: none; }
|
|
3054
3384
|
tr {
|
|
@@ -3063,50 +3393,63 @@ var renderAdminDocument = (service) => {
|
|
|
3063
3393
|
.route { grid-template-columns: 72px 1fr; }
|
|
3064
3394
|
.route button { grid-column: 1 / -1; }
|
|
3065
3395
|
}
|
|
3396
|
+
@media (max-width: 460px) {
|
|
3397
|
+
.controls label.field { min-width: calc(50% - 4px); }
|
|
3398
|
+
.controls .icon-btn, .controls .btn { flex: 1; }
|
|
3399
|
+
.grid { grid-template-columns: minmax(0, 1fr); }
|
|
3400
|
+
.dialog-actions { flex-wrap: wrap; }
|
|
3401
|
+
.dialog-actions button { flex: 1; }
|
|
3402
|
+
.dialog-actions .btn-danger { flex-basis: 100%; margin-right: 0; }
|
|
3403
|
+
}
|
|
3066
3404
|
</style>
|
|
3067
3405
|
</head>
|
|
3068
3406
|
<body>
|
|
3069
3407
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
3070
3408
|
<header class="top">
|
|
3071
3409
|
<div class="brand">
|
|
3072
|
-
<div class="brand-
|
|
3073
|
-
<
|
|
3074
|
-
|
|
3410
|
+
<div class="brand-lockup">
|
|
3411
|
+
<div class="brand-mark" aria-hidden="true">
|
|
3412
|
+
<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>
|
|
3413
|
+
</div>
|
|
3414
|
+
<div class="brand-id">
|
|
3415
|
+
<strong>${name}</strong>
|
|
3416
|
+
<span>Mock admin</span>
|
|
3417
|
+
</div>
|
|
3075
3418
|
</div>
|
|
3076
3419
|
<div class="vendor" id="vendor" hidden></div>
|
|
3077
3420
|
</div>
|
|
3078
3421
|
<div class="controls">
|
|
3422
|
+
<span class="sync-status" id="sync-status" role="status" aria-live="polite">Ready</span>
|
|
3079
3423
|
<label class="field">Namespace
|
|
3080
3424
|
<select id="namespace" aria-label="Namespace"></select>
|
|
3081
3425
|
</label>
|
|
3082
3426
|
<label class="field">Admin key
|
|
3083
3427
|
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
3084
3428
|
</label>
|
|
3085
|
-
<button class="icon-btn" id="theme" type="button" aria-label="
|
|
3086
|
-
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
3429
|
+
<button class="icon-btn" id="theme" type="button" aria-label="Use dark theme" title="Use dark theme"></button>
|
|
3430
|
+
<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>
|
|
3087
3431
|
</div>
|
|
3088
3432
|
</header>
|
|
3089
3433
|
<div class="layout">
|
|
3090
3434
|
<nav class="side" id="nav" aria-label="Admin sections">
|
|
3091
|
-
<
|
|
3092
|
-
<button type="button" data-view="
|
|
3093
|
-
<button type="button" data-view="
|
|
3094
|
-
<button type="button" data-view="
|
|
3095
|
-
<button type="button" data-view="
|
|
3096
|
-
<button type="button" data-view="
|
|
3435
|
+
<span class="nav-label">Workspace</span>
|
|
3436
|
+
<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>
|
|
3437
|
+
<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>
|
|
3438
|
+
<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>
|
|
3439
|
+
<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>
|
|
3440
|
+
<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>
|
|
3441
|
+
<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>
|
|
3097
3442
|
</nav>
|
|
3098
3443
|
<main id="views">
|
|
3099
|
-
<
|
|
3444
|
+
<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>
|
|
3100
3445
|
<section class="view" id="view-overview">
|
|
3101
|
-
<h2>Overview</h2>
|
|
3102
|
-
<p class="lede">Health, clock, and the collections this namespace is holding.</p>
|
|
3446
|
+
<div class="view-head"><div><h2>Overview</h2><p class="lede">Health, clock, and the collections this namespace is holding.</p></div></div>
|
|
3103
3447
|
<div class="grid" id="overview-cards"></div>
|
|
3104
3448
|
<h3>Recent requests</h3>
|
|
3105
3449
|
<div id="overview-requests"></div>
|
|
3106
3450
|
</section>
|
|
3107
3451
|
<section class="view" id="view-state" hidden>
|
|
3108
|
-
<h2>State</h2>
|
|
3109
|
-
<p class="lede">Every collection in this namespace. Declared fields stay visible before any row exists; stored rows add whatever else they contain.</p>
|
|
3452
|
+
<div class="view-head"><div><h2>State</h2><p class="lede">Inspect and edit every collection in this namespace.</p></div></div>
|
|
3110
3453
|
<div class="split">
|
|
3111
3454
|
<div id="collections"></div>
|
|
3112
3455
|
<div>
|
|
@@ -3119,8 +3462,7 @@ var renderAdminDocument = (service) => {
|
|
|
3119
3462
|
</div>
|
|
3120
3463
|
</section>
|
|
3121
3464
|
<section class="view" id="view-clock" hidden>
|
|
3122
|
-
<h2>Clock</h2>
|
|
3123
|
-
<p class="lede">The clock every timestamp in this mock reads.</p>
|
|
3465
|
+
<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>
|
|
3124
3466
|
<div class="card" id="clock-readout"></div>
|
|
3125
3467
|
<div class="row-actions">
|
|
3126
3468
|
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
@@ -3139,21 +3481,22 @@ var renderAdminDocument = (service) => {
|
|
|
3139
3481
|
</form>
|
|
3140
3482
|
</section>
|
|
3141
3483
|
<section class="view" id="view-faults" hidden>
|
|
3142
|
-
<h2>Faults</h2>
|
|
3143
|
-
<p class="lede">Inject a response, a delay, or a named preset. Rules apply to this namespace.</p>
|
|
3484
|
+
<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>
|
|
3144
3485
|
<form id="fault-form" class="card stack">
|
|
3145
|
-
<
|
|
3146
|
-
<
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
<
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
<
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
<
|
|
3156
|
-
|
|
3486
|
+
<div class="form-grid">
|
|
3487
|
+
<label class="field wide">Preset
|
|
3488
|
+
<select id="fault-preset"><option value="">Custom rule</option></select>
|
|
3489
|
+
</label>
|
|
3490
|
+
<label class="field">Status
|
|
3491
|
+
<input id="fault-status" inputmode="numeric" placeholder="503">
|
|
3492
|
+
</label>
|
|
3493
|
+
<label class="field wide">Path prefix
|
|
3494
|
+
<input id="fault-path" placeholder="/v1">
|
|
3495
|
+
</label>
|
|
3496
|
+
<label class="field">Method
|
|
3497
|
+
<input id="fault-method" placeholder="GET">
|
|
3498
|
+
</label>
|
|
3499
|
+
</div>
|
|
3157
3500
|
<div class="dialog-actions">
|
|
3158
3501
|
<button class="btn-primary" type="submit">Add fault</button>
|
|
3159
3502
|
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
@@ -3162,26 +3505,31 @@ var renderAdminDocument = (service) => {
|
|
|
3162
3505
|
<div id="fault-list"></div>
|
|
3163
3506
|
</section>
|
|
3164
3507
|
<section class="view" id="view-journal" hidden>
|
|
3165
|
-
<h2>Journal</h2>
|
|
3166
|
-
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
3167
|
-
<div class="row-actions">
|
|
3168
|
-
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
3169
|
-
</div>
|
|
3508
|
+
<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>
|
|
3170
3509
|
<div id="journal"></div>
|
|
3171
3510
|
</section>
|
|
3172
3511
|
<section class="view" id="view-routes" hidden>
|
|
3173
|
-
<h2>Routes</h2>
|
|
3174
|
-
<
|
|
3512
|
+
<div class="view-head"><div><h2>Routes</h2><p class="lede">Explore shared controls and service-specific admin endpoints.</p></div></div>
|
|
3513
|
+
<div class="route-tools">
|
|
3514
|
+
<label class="field">Filter routes
|
|
3515
|
+
<input id="route-search" type="search" placeholder="Method, path, or extension" autocomplete="off">
|
|
3516
|
+
</label>
|
|
3517
|
+
<span class="route-count" id="route-count"></span>
|
|
3518
|
+
</div>
|
|
3175
3519
|
<div id="route-list"></div>
|
|
3176
|
-
<form id="route-form" class="card stack">
|
|
3520
|
+
<form id="route-form" class="card stack" style="margin-top: 18px">
|
|
3177
3521
|
<strong id="route-title">Select a route</strong>
|
|
3178
3522
|
<label class="field">JSON body
|
|
3179
|
-
<
|
|
3523
|
+
<div class="json-editor" id="route-editor" hidden>
|
|
3524
|
+
<pre aria-hidden="true"><code id="route-highlight"></code></pre>
|
|
3525
|
+
<textarea id="route-body" placeholder="{ }" spellcheck="false" aria-describedby="route-json-status" disabled></textarea>
|
|
3526
|
+
</div>
|
|
3527
|
+
<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>
|
|
3180
3528
|
</label>
|
|
3181
3529
|
<div class="dialog-actions">
|
|
3182
|
-
<button class="btn-primary" type="submit">Send</button>
|
|
3530
|
+
<button class="btn-primary" id="route-send" type="submit" disabled>Send request</button>
|
|
3183
3531
|
</div>
|
|
3184
|
-
<pre id="route-result">Response will show here.</pre>
|
|
3532
|
+
<pre class="json-view" id="route-result"><code>Response will show here.</code></pre>
|
|
3185
3533
|
</form>
|
|
3186
3534
|
</section>
|
|
3187
3535
|
</main>
|
|
@@ -3189,12 +3537,16 @@ var renderAdminDocument = (service) => {
|
|
|
3189
3537
|
</div>
|
|
3190
3538
|
<dialog id="editor">
|
|
3191
3539
|
<form method="dialog" id="editor-form">
|
|
3192
|
-
<strong id="editor-title">Record</strong>
|
|
3540
|
+
<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>
|
|
3193
3541
|
<label class="field">Id
|
|
3194
3542
|
<input id="editor-id" required>
|
|
3195
3543
|
</label>
|
|
3196
3544
|
<label class="field">JSON value
|
|
3197
|
-
<
|
|
3545
|
+
<div class="json-editor" id="record-editor">
|
|
3546
|
+
<pre aria-hidden="true"><code id="editor-highlight"></code></pre>
|
|
3547
|
+
<textarea id="editor-value" spellcheck="false" aria-describedby="editor-json-status"></textarea>
|
|
3548
|
+
</div>
|
|
3549
|
+
<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>
|
|
3198
3550
|
</label>
|
|
3199
3551
|
<div class="dialog-actions">
|
|
3200
3552
|
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
@@ -3236,8 +3588,8 @@ var describeExtension = (extension) => {
|
|
|
3236
3588
|
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
3237
3589
|
};
|
|
3238
3590
|
};
|
|
3239
|
-
var adminUiRoutes = (service, ui) => {
|
|
3240
|
-
const shell = () => renderAdminDocument(service);
|
|
3591
|
+
var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
3592
|
+
const shell = () => renderAdminDocument(service, adminPrefix);
|
|
3241
3593
|
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
3242
3594
|
return {
|
|
3243
3595
|
"GET /ui": () => htmlResponse(document3()),
|
|
@@ -3854,10 +4206,13 @@ var stateAdminRoutes = (routes) => {
|
|
|
3854
4206
|
};
|
|
3855
4207
|
|
|
3856
4208
|
// ../core/dist/version.js
|
|
3857
|
-
var PACKAGE_VERSION = true ? "
|
|
4209
|
+
var PACKAGE_VERSION = true ? "2.0.0" : UNRELEASED_VERSION;
|
|
3858
4210
|
|
|
3859
4211
|
// ../core/dist/webhooks.js
|
|
3860
|
-
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4212
|
+
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4213
|
+
status,
|
|
4214
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4215
|
+
});
|
|
3861
4216
|
var adminError3 = (status, message) => json4(status, { error: { type: "mockingbird_admin", message } });
|
|
3862
4217
|
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3863
4218
|
var parseEndpoint = (value) => {
|
|
@@ -3965,7 +4320,6 @@ var AT_HEADER = "x-mockingbird-at";
|
|
|
3965
4320
|
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
3966
4321
|
var DEFAULT_NAMESPACE = "default";
|
|
3967
4322
|
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
3968
|
-
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
3969
4323
|
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
3970
4324
|
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
3971
4325
|
var effects = /* @__PURE__ */ new WeakMap();
|
|
@@ -4006,6 +4360,22 @@ var operationMatcher = (document3) => {
|
|
|
4006
4360
|
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
4007
4361
|
};
|
|
4008
4362
|
var createRuntime = (options) => {
|
|
4363
|
+
const adminPrefix = resolveAdminPrefix(options.adminPrefix);
|
|
4364
|
+
const internalPaths = Object.entries(options.document?.paths ?? {}).filter(([, item]) => item["x-mockingbird-internal"] === true).map(([path]) => path);
|
|
4365
|
+
for (const path of internalPaths) {
|
|
4366
|
+
if (!path.startsWith(`${ADMIN_PREFIX}/blobs/`))
|
|
4367
|
+
throw new Error(`internal transport ${path} must live under ${ADMIN_PREFIX}/blobs/`);
|
|
4368
|
+
}
|
|
4369
|
+
assertAdminPrefixAvailable(adminPrefix, Object.keys(options.document?.paths ?? {}).filter((path) => !internalPaths.includes(path)));
|
|
4370
|
+
const internalMatch = (path) => {
|
|
4371
|
+
for (const pattern of internalPaths) {
|
|
4372
|
+
const actual = path.split("/");
|
|
4373
|
+
const expected = pattern.split("/");
|
|
4374
|
+
if (actual.length === expected.length && expected.every((part, i) => part.startsWith("{") || part === actual[i]))
|
|
4375
|
+
return pattern;
|
|
4376
|
+
}
|
|
4377
|
+
return void 0;
|
|
4378
|
+
};
|
|
4009
4379
|
assertAdminUi(options.adminUi);
|
|
4010
4380
|
const sqlite = bootSqlite(options.sqlite);
|
|
4011
4381
|
const clock = options.clock ?? createClock();
|
|
@@ -4051,6 +4421,7 @@ var createRuntime = (options) => {
|
|
|
4051
4421
|
const created = options.create({
|
|
4052
4422
|
namespace: storageNamespace(key),
|
|
4053
4423
|
publicNamespace,
|
|
4424
|
+
adminPrefix,
|
|
4054
4425
|
sqlite,
|
|
4055
4426
|
clock: namespaceClock(publicNamespace),
|
|
4056
4427
|
rng: createdContextRng
|
|
@@ -4283,10 +4654,10 @@ var createRuntime = (options) => {
|
|
|
4283
4654
|
namespaceClock,
|
|
4284
4655
|
namespaceOf(request) {
|
|
4285
4656
|
const url = new URL(request.url);
|
|
4286
|
-
const prefixed =
|
|
4657
|
+
const prefixed = matchNamespacePath(url.pathname, adminPrefix);
|
|
4287
4658
|
const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
|
|
4288
4659
|
const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
|
|
4289
|
-
if (path
|
|
4660
|
+
if (isAdminPath(path, adminPrefix))
|
|
4290
4661
|
return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
|
|
4291
4662
|
if (selected !== void 0)
|
|
4292
4663
|
return selected;
|
|
@@ -4352,7 +4723,7 @@ var createRuntime = (options) => {
|
|
|
4352
4723
|
state: (namespace = DEFAULT_NAMESPACE) => inspectState(stateScope(namespace)),
|
|
4353
4724
|
fetch: async (incoming) => {
|
|
4354
4725
|
let request = incoming;
|
|
4355
|
-
const prefixed =
|
|
4726
|
+
const prefixed = matchNamespacePath(new URL(request.url).pathname, adminPrefix);
|
|
4356
4727
|
if (prefixed) {
|
|
4357
4728
|
const url2 = new URL(request.url);
|
|
4358
4729
|
url2.pathname = prefixed[2] ?? "/";
|
|
@@ -4368,7 +4739,15 @@ var createRuntime = (options) => {
|
|
|
4368
4739
|
signal: request.signal
|
|
4369
4740
|
});
|
|
4370
4741
|
}
|
|
4371
|
-
|
|
4742
|
+
const internalUrl = new URL(request.url);
|
|
4743
|
+
const internalPath = isAdminPath(internalUrl.pathname, adminPrefix) ? `${ADMIN_PREFIX}${internalUrl.pathname.slice(adminPrefix.length)}` : "";
|
|
4744
|
+
const internal = internalMatch(internalPath);
|
|
4745
|
+
const obsoleteInternalPath = adminPrefix !== ADMIN_PREFIX && internalMatch(internalUrl.pathname) && !internal;
|
|
4746
|
+
if (internal) {
|
|
4747
|
+
internalUrl.pathname = internalPath;
|
|
4748
|
+
request = new Request(internalUrl, request);
|
|
4749
|
+
}
|
|
4750
|
+
let namespace = internal ? internalUrl.searchParams.get("namespace") ?? control.namespaceOf(request) : control.namespaceOf(request);
|
|
4372
4751
|
if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
4373
4752
|
const credential = options.credential(request);
|
|
4374
4753
|
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
@@ -4388,7 +4767,9 @@ var createRuntime = (options) => {
|
|
|
4388
4767
|
return copy;
|
|
4389
4768
|
}
|
|
4390
4769
|
};
|
|
4391
|
-
|
|
4770
|
+
if (obsoleteInternalPath)
|
|
4771
|
+
return stamp(adminFail(404, "internal transport is only available under adminPrefix"));
|
|
4772
|
+
const handled = internal ? void 0 : await control.handle(request);
|
|
4392
4773
|
if (handled)
|
|
4393
4774
|
return stamp(handled);
|
|
4394
4775
|
const started = monotonicNow();
|
|
@@ -4429,7 +4810,7 @@ var createRuntime = (options) => {
|
|
|
4429
4810
|
type: "mockingbird_admin",
|
|
4430
4811
|
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
4431
4812
|
}
|
|
4432
|
-
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
4813
|
+
}), { status: 400, headers: { "content-type": "application/json; charset=utf-8" } }));
|
|
4433
4814
|
}
|
|
4434
4815
|
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
4435
4816
|
log(400);
|
|
@@ -4546,15 +4927,16 @@ var createRuntime = (options) => {
|
|
|
4546
4927
|
...options.admin?.(runtime) ?? {},
|
|
4547
4928
|
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
4548
4929
|
// maps `presets`, and a service route must not replace that list with a record.
|
|
4549
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
4930
|
+
...options.presets ? presetRoutes(options.presets, runtime, adminPrefix) : {},
|
|
4550
4931
|
...stateAdminRoutes({
|
|
4551
4932
|
open: stateScope,
|
|
4552
4933
|
afterWrite: (namespace) => {
|
|
4553
4934
|
checkpoint(namespace, "main");
|
|
4554
4935
|
}
|
|
4555
4936
|
}),
|
|
4556
|
-
...adminUiRoutes(options.name, options.adminUi)
|
|
4937
|
+
...adminUiRoutes(options.name, options.adminUi, adminPrefix)
|
|
4557
4938
|
},
|
|
4939
|
+
adminPrefix,
|
|
4558
4940
|
adminKey: options.adminKey
|
|
4559
4941
|
});
|
|
4560
4942
|
return defineMock(runtime);
|
|
@@ -4568,7 +4950,10 @@ var mutableResponse = (response) => {
|
|
|
4568
4950
|
return new Response(response.body, response);
|
|
4569
4951
|
}
|
|
4570
4952
|
};
|
|
4571
|
-
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
4953
|
+
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
4954
|
+
status,
|
|
4955
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4956
|
+
});
|
|
4572
4957
|
var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
|
|
4573
4958
|
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4574
4959
|
var credentialRoutes = (registry) => ({
|
|
@@ -4620,7 +5005,7 @@ var credentialRoutes = (registry) => ({
|
|
|
4620
5005
|
return adminJson(200, { status: "ok" });
|
|
4621
5006
|
}
|
|
4622
5007
|
});
|
|
4623
|
-
var presetRoutes = (presets, runtime) => ({
|
|
5008
|
+
var presetRoutes = (presets, runtime, adminPrefix) => ({
|
|
4624
5009
|
"GET /faults/presets": () => {
|
|
4625
5010
|
const body = {
|
|
4626
5011
|
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
@@ -4630,7 +5015,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
4630
5015
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
4631
5016
|
const name = params.name;
|
|
4632
5017
|
if (!presets[name])
|
|
4633
|
-
return adminFail(404, `no fault preset ${name}; GET /
|
|
5018
|
+
return adminFail(404, `no fault preset ${name}; GET ${adminPrefix}/faults/presets`);
|
|
4634
5019
|
const overrides = isObject(body) ? body : {};
|
|
4635
5020
|
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
4636
5021
|
}
|
|
@@ -4839,6 +5224,7 @@ var createRuntime2 = (options = {}) => createRuntime({
|
|
|
4839
5224
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
4840
5225
|
...options.clock ? { clock: options.clock } : {},
|
|
4841
5226
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
5227
|
+
...options.adminPrefix !== void 0 ? { adminPrefix: options.adminPrefix } : {},
|
|
4842
5228
|
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
4843
5229
|
...options.onLog ? { onLog: options.onLog } : {},
|
|
4844
5230
|
credential: payloadCredential,
|
|
@@ -5095,6 +5481,7 @@ var PayloadCmsAPI = class {
|
|
|
5095
5481
|
|
|
5096
5482
|
export {
|
|
5097
5483
|
createClock,
|
|
5484
|
+
resolveAdminPrefix,
|
|
5098
5485
|
parseDuration,
|
|
5099
5486
|
document2 as document,
|
|
5100
5487
|
operationIds,
|
|
@@ -5109,4 +5496,4 @@ export {
|
|
|
5109
5496
|
compileWhere,
|
|
5110
5497
|
PayloadCmsAPI
|
|
5111
5498
|
};
|
|
5112
|
-
//# sourceMappingURL=chunk-
|
|
5499
|
+
//# sourceMappingURL=chunk-JSNSLQM4.js.map
|