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