@crvouga/mockingbird-service-plane 1.8.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -1
- package/DISCOVERY.md +55 -0
- package/README.md +1 -1
- package/SUPPORT.md +21 -0
- package/dist/{chunk-LRGQXAHN.js → chunk-C6XLQ2US.js} +622 -235
- package/dist/chunk-C6XLQ2US.js.map +7 -0
- package/dist/{chunk-QT6V22QX.js → chunk-IKIADEZM.js} +44 -24
- package/dist/chunk-IKIADEZM.js.map +7 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.js +2 -2
- package/openapi.yaml +1014 -0
- package/package.json +29 -4
- package/dist/chunk-LRGQXAHN.js.map +0 -7
- package/dist/chunk-QT6V22QX.js.map +0 -7
|
@@ -110,13 +110,31 @@ var Collection = class {
|
|
|
110
110
|
};
|
|
111
111
|
|
|
112
112
|
// ../core/dist/control.js
|
|
113
|
-
var HEALTH_PATH = "/health";
|
|
114
113
|
var ADMIN_PREFIX = "/__admin";
|
|
114
|
+
var resolveAdminPrefix = (prefix = ADMIN_PREFIX) => {
|
|
115
|
+
if (!/^\/[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*$/.test(prefix)) {
|
|
116
|
+
throw new Error("adminPrefix must be an absolute non-root path of literal segments, without a trailing slash");
|
|
117
|
+
}
|
|
118
|
+
return prefix;
|
|
119
|
+
};
|
|
120
|
+
var isAdminPath = (path, prefix) => path === prefix || path.startsWith(`${prefix}/`);
|
|
121
|
+
var matchNamespacePath = (path, prefix) => /^\/ns\/([^/]+)(\/.*)?$/.exec(isAdminPath(path, prefix) ? path.slice(prefix.length) : "");
|
|
122
|
+
var assertAdminPrefixAvailable = (prefix, paths) => {
|
|
123
|
+
const reserved = prefix.split("/").slice(1);
|
|
124
|
+
for (const path of paths) {
|
|
125
|
+
const segments = path.split("/").slice(1);
|
|
126
|
+
if (segments.length < reserved.length || /^\{[^}]+\}$/.test(segments[0] ?? ""))
|
|
127
|
+
continue;
|
|
128
|
+
if (reserved.every((part, i) => segments[i] === part || /^\{[^}]+\}$/.test(segments[i] ?? ""))) {
|
|
129
|
+
throw new Error(`vendor route ${path} collides with adminPrefix ${prefix}; choose a different adminPrefix`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
115
133
|
var ADMIN_KEY_HEADER = "x-mockingbird-admin-key";
|
|
116
134
|
var NAMESPACE_HEADER = "x-mockingbird-namespace";
|
|
117
135
|
var json = (status, body) => new Response(JSON.stringify(body), {
|
|
118
136
|
status,
|
|
119
|
-
headers: { "content-type": "application/json" }
|
|
137
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
120
138
|
});
|
|
121
139
|
var adminError = (status, message) => json(status, { error: { type: "mockingbird_admin", message } });
|
|
122
140
|
var UNITS = {
|
|
@@ -168,11 +186,25 @@ var readJson = async (request) => {
|
|
|
168
186
|
};
|
|
169
187
|
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
170
188
|
var createControlPlane = (context) => {
|
|
189
|
+
const adminPrefix = resolveAdminPrefix(context.adminPrefix);
|
|
171
190
|
const snapshots = /* @__PURE__ */ new Map();
|
|
172
191
|
let snapshotCounter = 0;
|
|
173
192
|
const headerNamespace = (request) => request.headers.get(NAMESPACE_HEADER) ?? context.defaultNamespace;
|
|
174
193
|
const adminNamespace = (request, url) => url.searchParams.get("namespace") ?? headerNamespace(request);
|
|
194
|
+
const health = () => {
|
|
195
|
+
return json(200, {
|
|
196
|
+
...context.describe(),
|
|
197
|
+
status: "ok",
|
|
198
|
+
service: context.name,
|
|
199
|
+
uptimeMs: context.wallNow() - context.startedAt,
|
|
200
|
+
clock: context.clock.state(),
|
|
201
|
+
namespaces: context.namespaces().length,
|
|
202
|
+
adminPrefix,
|
|
203
|
+
adminUi: `${adminPrefix}/ui`
|
|
204
|
+
});
|
|
205
|
+
};
|
|
175
206
|
const builtin = {
|
|
207
|
+
"GET /health": health,
|
|
176
208
|
"GET /": () => json(200, {
|
|
177
209
|
service: context.name,
|
|
178
210
|
routes: [...Object.keys(builtin), ...Object.keys(context.routes)].sort()
|
|
@@ -339,7 +371,14 @@ var createControlPlane = (context) => {
|
|
|
339
371
|
return json(200, { status: "ok" });
|
|
340
372
|
}
|
|
341
373
|
};
|
|
342
|
-
const
|
|
374
|
+
for (const key of Object.keys(context.routes)) {
|
|
375
|
+
if (Object.hasOwn(builtin, key))
|
|
376
|
+
throw new Error(`admin route ${key} overrides a shared route`);
|
|
377
|
+
const path = key.slice(key.indexOf(" ") + 1);
|
|
378
|
+
if (isAdminPath(path, "/ns") || isAdminPath(path, "/blobs"))
|
|
379
|
+
throw new Error(`admin route ${key} overlaps an internal transport`);
|
|
380
|
+
}
|
|
381
|
+
const routes = [...Object.entries(builtin), ...Object.entries(context.routes)].map(([key, handler]) => {
|
|
343
382
|
const space = key.indexOf(" ");
|
|
344
383
|
return { method: key.slice(0, space), pattern: key.slice(space + 1), handler };
|
|
345
384
|
});
|
|
@@ -347,21 +386,13 @@ var createControlPlane = (context) => {
|
|
|
347
386
|
namespaceOf: headerNamespace,
|
|
348
387
|
async handle(request) {
|
|
349
388
|
const url = new URL(request.url);
|
|
350
|
-
if (url.pathname ===
|
|
351
|
-
return
|
|
352
|
-
status: "ok",
|
|
353
|
-
service: context.name,
|
|
354
|
-
uptimeMs: context.wallNow() - context.startedAt,
|
|
355
|
-
clock: context.clock.state(),
|
|
356
|
-
namespaces: context.namespaces().length,
|
|
357
|
-
adminUi: `${ADMIN_PREFIX}/ui`,
|
|
358
|
-
...context.describe()
|
|
359
|
-
});
|
|
389
|
+
if (url.pathname === `${adminPrefix}/health` && request.method === "GET") {
|
|
390
|
+
return health();
|
|
360
391
|
}
|
|
361
|
-
if (url.pathname
|
|
392
|
+
if (!isAdminPath(url.pathname, adminPrefix)) {
|
|
362
393
|
return void 0;
|
|
363
394
|
}
|
|
364
|
-
const path = url.pathname.slice(
|
|
395
|
+
const path = url.pathname.slice(adminPrefix.length) || "/";
|
|
365
396
|
const shell = request.method === "GET" && (path === "/ui" || path === "/ui/");
|
|
366
397
|
if (!shell && context.adminKey !== void 0 && request.headers.get(ADMIN_KEY_HEADER) !== context.adminKey) {
|
|
367
398
|
return adminError(401, `missing or wrong ${ADMIN_KEY_HEADER}`);
|
|
@@ -369,7 +400,12 @@ var createControlPlane = (context) => {
|
|
|
369
400
|
for (const route of routes) {
|
|
370
401
|
if (route.method !== request.method)
|
|
371
402
|
continue;
|
|
372
|
-
|
|
403
|
+
let params;
|
|
404
|
+
try {
|
|
405
|
+
params = matchRoute(route.pattern, path);
|
|
406
|
+
} catch {
|
|
407
|
+
return adminError(400, "invalid path encoding");
|
|
408
|
+
}
|
|
373
409
|
if (!params)
|
|
374
410
|
continue;
|
|
375
411
|
let body;
|
|
@@ -378,15 +414,18 @@ var createControlPlane = (context) => {
|
|
|
378
414
|
} catch {
|
|
379
415
|
return adminError(400, "request body is not valid JSON");
|
|
380
416
|
}
|
|
417
|
+
const namespace = adminNamespace(request, url);
|
|
418
|
+
if (!/^[A-Za-z0-9_.-]{1,64}$/.test(namespace))
|
|
419
|
+
return adminError(400, "invalid namespace");
|
|
381
420
|
return route.handler({
|
|
382
421
|
request,
|
|
383
422
|
url,
|
|
384
423
|
params,
|
|
385
|
-
namespace
|
|
424
|
+
namespace,
|
|
386
425
|
body
|
|
387
426
|
});
|
|
388
427
|
}
|
|
389
|
-
return adminError(404, `no admin route ${request.method} ${path}; GET ${
|
|
428
|
+
return adminError(404, `no admin route ${request.method} ${path}; GET ${adminPrefix} lists them`);
|
|
390
429
|
}
|
|
391
430
|
};
|
|
392
431
|
};
|
|
@@ -1798,6 +1837,34 @@ dialog:not([open]) {
|
|
|
1798
1837
|
function isRecord2(value) {
|
|
1799
1838
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1800
1839
|
}
|
|
1840
|
+
function highlightJsonText(text) {
|
|
1841
|
+
const escapeToken = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
1842
|
+
switch (char) {
|
|
1843
|
+
case "&":
|
|
1844
|
+
return "&";
|
|
1845
|
+
case "<":
|
|
1846
|
+
return "<";
|
|
1847
|
+
case ">":
|
|
1848
|
+
return ">";
|
|
1849
|
+
case '"':
|
|
1850
|
+
return """;
|
|
1851
|
+
default:
|
|
1852
|
+
return "'";
|
|
1853
|
+
}
|
|
1854
|
+
});
|
|
1855
|
+
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;
|
|
1856
|
+
let cursor = 0;
|
|
1857
|
+
let html = "";
|
|
1858
|
+
for (const match of text.matchAll(token)) {
|
|
1859
|
+
const index = match.index ?? 0;
|
|
1860
|
+
const value = match[0];
|
|
1861
|
+
html += escapeToken(text.slice(cursor, index));
|
|
1862
|
+
const kind = value.startsWith('"') ? value.trimEnd().endsWith(":") ? "key" : "string" : value === "true" || value === "false" ? "boolean" : value === "null" ? "null" : "number";
|
|
1863
|
+
html += `<span class="json-${kind}">${escapeToken(value)}</span>`;
|
|
1864
|
+
cursor = index + value.length;
|
|
1865
|
+
}
|
|
1866
|
+
return html + escapeToken(text.slice(cursor));
|
|
1867
|
+
}
|
|
1801
1868
|
function mountAdminBrand(doc, host, service, catalog) {
|
|
1802
1869
|
const clip = (value, max) => {
|
|
1803
1870
|
if (typeof value !== "string")
|
|
@@ -2159,14 +2226,16 @@ function bootAdmin(config) {
|
|
|
2159
2226
|
const state = {
|
|
2160
2227
|
namespace: "default",
|
|
2161
2228
|
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
2162
|
-
view: "overview",
|
|
2229
|
+
view: ["overview", "state", "clock", "faults", "journal", "routes"].includes(location.hash.slice(1)) ? location.hash.slice(1) : "overview",
|
|
2163
2230
|
collection: "",
|
|
2164
2231
|
shape: null,
|
|
2165
2232
|
route: null,
|
|
2233
|
+
routes: [],
|
|
2166
2234
|
editing: null
|
|
2167
2235
|
};
|
|
2168
2236
|
const standard = new Set(config.standardRoutes);
|
|
2169
2237
|
const banner = el("banner", HTMLElement);
|
|
2238
|
+
const bannerMessage = el("banner-message", HTMLElement);
|
|
2170
2239
|
const keyInput = el("admin-key", HTMLInputElement);
|
|
2171
2240
|
keyInput.value = state.key;
|
|
2172
2241
|
const params = new URLSearchParams(location.search);
|
|
@@ -2177,18 +2246,21 @@ function bootAdmin(config) {
|
|
|
2177
2246
|
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
2178
2247
|
params.delete("key");
|
|
2179
2248
|
const next = params.toString();
|
|
2180
|
-
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
2249
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : "") + location.hash);
|
|
2181
2250
|
}
|
|
2182
2251
|
const queryNamespace = params.get("namespace");
|
|
2183
2252
|
if (queryNamespace)
|
|
2184
2253
|
state.namespace = queryNamespace;
|
|
2185
2254
|
const showError = (error) => {
|
|
2186
|
-
|
|
2255
|
+
bannerMessage.textContent = error instanceof Error ? error.message : String(error);
|
|
2187
2256
|
banner.classList.add("show");
|
|
2257
|
+
banner.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
2188
2258
|
};
|
|
2189
2259
|
const clearError = () => {
|
|
2190
2260
|
banner.classList.remove("show");
|
|
2261
|
+
bannerMessage.textContent = "";
|
|
2191
2262
|
};
|
|
2263
|
+
el("banner-dismiss", HTMLButtonElement).addEventListener("click", clearError);
|
|
2192
2264
|
const api = async (method, path, body) => {
|
|
2193
2265
|
const headers = { accept: "application/json" };
|
|
2194
2266
|
if (state.key !== "")
|
|
@@ -2232,16 +2304,21 @@ function bootAdmin(config) {
|
|
|
2232
2304
|
namespace: () => state.namespace
|
|
2233
2305
|
};
|
|
2234
2306
|
const isPanelScript = (value) => typeof value === "function";
|
|
2307
|
+
const themeButton = el("theme", HTMLButtonElement);
|
|
2235
2308
|
const setTheme = (theme) => {
|
|
2236
2309
|
if (theme)
|
|
2237
2310
|
document.documentElement.dataset.theme = theme;
|
|
2238
2311
|
else
|
|
2239
2312
|
delete document.documentElement.dataset.theme;
|
|
2313
|
+
const dark = theme ? theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2314
|
+
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>';
|
|
2315
|
+
const label = dark ? "Use light theme" : "Use dark theme";
|
|
2316
|
+
themeButton.setAttribute("aria-label", label);
|
|
2317
|
+
themeButton.title = label;
|
|
2240
2318
|
};
|
|
2241
2319
|
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
2320
|
+
setTheme(storedTheme === "dark" || storedTheme === "light" ? storedTheme : null);
|
|
2321
|
+
themeButton.addEventListener("click", () => {
|
|
2245
2322
|
const current = document.documentElement.dataset.theme;
|
|
2246
2323
|
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2247
2324
|
const next = dark ? "light" : "dark";
|
|
@@ -2260,6 +2337,7 @@ function bootAdmin(config) {
|
|
|
2260
2337
|
continue;
|
|
2261
2338
|
section.hidden = section.id !== `view-${view}`;
|
|
2262
2339
|
}
|
|
2340
|
+
history.replaceState(null, "", `${location.pathname}${location.search}#${encodeURIComponent(view)}`);
|
|
2263
2341
|
};
|
|
2264
2342
|
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
2265
2343
|
const target = event.target;
|
|
@@ -2274,7 +2352,7 @@ function bootAdmin(config) {
|
|
|
2274
2352
|
show(view);
|
|
2275
2353
|
refresh().catch(showError);
|
|
2276
2354
|
});
|
|
2277
|
-
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>`;
|
|
2355
|
+
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>`;
|
|
2278
2356
|
const table = (columns, rows) => {
|
|
2279
2357
|
if (rows.length === 0)
|
|
2280
2358
|
return '<p class="empty">Nothing here yet.</p>';
|
|
@@ -2284,16 +2362,22 @@ function bootAdmin(config) {
|
|
|
2284
2362
|
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
2285
2363
|
return `<tr${attrs}>${cells}</tr>`;
|
|
2286
2364
|
}).join("");
|
|
2287
|
-
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
2365
|
+
return `<div class="table-shell"><table><thead>${head}</thead><tbody>${body}</tbody></table></div>`;
|
|
2288
2366
|
};
|
|
2289
2367
|
const preview = (value) => {
|
|
2290
2368
|
const text = JSON.stringify(value) ?? "null";
|
|
2291
|
-
|
|
2369
|
+
const clipped = text.length > 180 ? `${text.slice(0, 180)}\u2026` : text;
|
|
2370
|
+
return `<code class="json-inline">${highlightJsonText(clipped)}</code>`;
|
|
2292
2371
|
};
|
|
2293
2372
|
const when = (epoch) => {
|
|
2294
2373
|
const date = new Date(epoch);
|
|
2295
2374
|
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
2296
2375
|
};
|
|
2376
|
+
const journalWhen = (value) => {
|
|
2377
|
+
const numeric = value.trim() === "" ? Number.NaN : Number(value);
|
|
2378
|
+
const epoch = Number.isFinite(numeric) ? numeric : Date.parse(value);
|
|
2379
|
+
return Number.isFinite(epoch) ? when(epoch) : value;
|
|
2380
|
+
};
|
|
2297
2381
|
const clockCard = (epoch) => {
|
|
2298
2382
|
const date = new Date(epoch);
|
|
2299
2383
|
if (Number.isNaN(date.getTime()))
|
|
@@ -2413,7 +2497,7 @@ function bootAdmin(config) {
|
|
|
2413
2497
|
};
|
|
2414
2498
|
const renderOverview = async () => {
|
|
2415
2499
|
const [health, shape, clock, journal] = await Promise.all([
|
|
2416
|
-
originGet(
|
|
2500
|
+
originGet(`${config.adminPrefix}/health`),
|
|
2417
2501
|
api("GET", "/state"),
|
|
2418
2502
|
api("GET", "/clock"),
|
|
2419
2503
|
api("GET", "/requests?limit=8")
|
|
@@ -2430,7 +2514,7 @@ function bootAdmin(config) {
|
|
|
2430
2514
|
},
|
|
2431
2515
|
{
|
|
2432
2516
|
k: "Status",
|
|
2433
|
-
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
2517
|
+
v: `<span class="badge ok">${esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")}</span>`
|
|
2434
2518
|
},
|
|
2435
2519
|
{ k: "Namespace", v: esc(state.namespace) },
|
|
2436
2520
|
{ k: "Collections", v: String(decoded.collections.length) },
|
|
@@ -2517,26 +2601,46 @@ function bootAdmin(config) {
|
|
|
2517
2601
|
const renderJournal = async () => {
|
|
2518
2602
|
const journal = await api("GET", "/requests?limit=100");
|
|
2519
2603
|
el("journal", HTMLElement).innerHTML = table([
|
|
2520
|
-
{ label: "When", cell: (row) => esc(row.at) },
|
|
2521
|
-
{
|
|
2604
|
+
{ label: "When", cell: (row) => esc(journalWhen(row.at)) },
|
|
2605
|
+
{
|
|
2606
|
+
label: "Method",
|
|
2607
|
+
cell: (row) => `<span class="method">${esc(row.method)}</span>`
|
|
2608
|
+
},
|
|
2522
2609
|
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2523
|
-
{
|
|
2610
|
+
{
|
|
2611
|
+
label: "Status",
|
|
2612
|
+
cell: (row) => `<span class="badge ${row.status.startsWith("2") ? "ok" : row.status.startsWith("4") || row.status.startsWith("5") ? "warn" : ""}">${esc(row.status)}</span>`
|
|
2613
|
+
},
|
|
2524
2614
|
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
2525
2615
|
], decodeJournal(journal));
|
|
2526
2616
|
};
|
|
2527
|
-
const
|
|
2528
|
-
const
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2617
|
+
const paintRoutes = () => {
|
|
2618
|
+
const query = el("route-search", HTMLInputElement).value.trim().toLocaleLowerCase();
|
|
2619
|
+
const routes = state.routes.filter((route) => route.toLocaleLowerCase().includes(query));
|
|
2620
|
+
el("route-count", HTMLElement).textContent = `${routes.length} of ${state.routes.length}`;
|
|
2621
|
+
if (state.routes.length === 0) {
|
|
2622
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No admin routes are available.</p>';
|
|
2623
|
+
return;
|
|
2624
|
+
}
|
|
2625
|
+
if (routes.length === 0) {
|
|
2626
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No routes match this filter.</p>';
|
|
2627
|
+
return;
|
|
2628
|
+
}
|
|
2532
2629
|
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2533
2630
|
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2534
2631
|
const parts = route.split(" ");
|
|
2535
2632
|
const method = parts[0] ?? "";
|
|
2536
2633
|
const path = parts.slice(1).join(" ");
|
|
2537
|
-
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>`;
|
|
2634
|
+
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>`;
|
|
2538
2635
|
}).join("");
|
|
2539
2636
|
};
|
|
2637
|
+
const renderRoutes = async () => {
|
|
2638
|
+
const data = await api("GET", "/");
|
|
2639
|
+
if (!isRecord2(data))
|
|
2640
|
+
throw new Error("Route list was not an object");
|
|
2641
|
+
state.routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
2642
|
+
paintRoutes();
|
|
2643
|
+
};
|
|
2540
2644
|
const renderers = {
|
|
2541
2645
|
overview: renderOverview,
|
|
2542
2646
|
state: renderState,
|
|
@@ -2545,19 +2649,50 @@ function bootAdmin(config) {
|
|
|
2545
2649
|
journal: renderJournal,
|
|
2546
2650
|
routes: renderRoutes
|
|
2547
2651
|
};
|
|
2652
|
+
let refreshToken = 0;
|
|
2548
2653
|
async function refresh() {
|
|
2654
|
+
const token = ++refreshToken;
|
|
2549
2655
|
clearError();
|
|
2656
|
+
const main = el("views", HTMLElement);
|
|
2657
|
+
const sync = el("sync-status", HTMLElement);
|
|
2658
|
+
main.setAttribute("aria-busy", "true");
|
|
2659
|
+
sync.classList.add("busy");
|
|
2660
|
+
sync.textContent = "Updating";
|
|
2550
2661
|
const render = renderers[state.view];
|
|
2551
|
-
|
|
2552
|
-
|
|
2662
|
+
try {
|
|
2663
|
+
if (render)
|
|
2664
|
+
await render();
|
|
2665
|
+
if (token === refreshToken)
|
|
2666
|
+
sync.textContent = "Up to date";
|
|
2667
|
+
} finally {
|
|
2668
|
+
if (token === refreshToken) {
|
|
2669
|
+
main.removeAttribute("aria-busy");
|
|
2670
|
+
sync.classList.remove("busy");
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2553
2673
|
}
|
|
2554
2674
|
const delay = (ms) => new Promise((resolve) => {
|
|
2555
2675
|
window.setTimeout(resolve, ms);
|
|
2556
2676
|
});
|
|
2557
|
-
const confirmAction = async (button, pending, done, work) => {
|
|
2677
|
+
const confirmAction = async (button, pending, done, work, confirmLabel) => {
|
|
2558
2678
|
if (button.dataset.pending === "1")
|
|
2559
2679
|
return;
|
|
2560
2680
|
const label = button.textContent ?? "";
|
|
2681
|
+
if (confirmLabel !== void 0 && button.dataset.confirming !== "1") {
|
|
2682
|
+
button.dataset.confirming = "1";
|
|
2683
|
+
button.classList.add("confirming");
|
|
2684
|
+
button.textContent = confirmLabel;
|
|
2685
|
+
window.setTimeout(() => {
|
|
2686
|
+
if (button.dataset.confirming !== "1")
|
|
2687
|
+
return;
|
|
2688
|
+
button.textContent = label;
|
|
2689
|
+
button.classList.remove("confirming");
|
|
2690
|
+
delete button.dataset.confirming;
|
|
2691
|
+
}, 3e3);
|
|
2692
|
+
return;
|
|
2693
|
+
}
|
|
2694
|
+
button.classList.remove("confirming");
|
|
2695
|
+
delete button.dataset.confirming;
|
|
2561
2696
|
const width = button.offsetWidth;
|
|
2562
2697
|
button.dataset.pending = "1";
|
|
2563
2698
|
button.classList.add("pressed");
|
|
@@ -2578,6 +2713,65 @@ function bootAdmin(config) {
|
|
|
2578
2713
|
delete button.dataset.pending;
|
|
2579
2714
|
}
|
|
2580
2715
|
};
|
|
2716
|
+
const wireJsonEditor = (textareaId, highlightId, statusId, shellId, allowEmpty) => {
|
|
2717
|
+
const textarea = el(textareaId, HTMLTextAreaElement);
|
|
2718
|
+
const highlight = el(highlightId, HTMLElement);
|
|
2719
|
+
const status = el(statusId, HTMLElement);
|
|
2720
|
+
const shell = el(shellId, HTMLElement);
|
|
2721
|
+
const sync = () => {
|
|
2722
|
+
const text = textarea.value;
|
|
2723
|
+
highlight.innerHTML = `${highlightJsonText(text)}
|
|
2724
|
+
`;
|
|
2725
|
+
let valid = true;
|
|
2726
|
+
let message = "Valid JSON";
|
|
2727
|
+
if (allowEmpty && text.trim() === "")
|
|
2728
|
+
message = "No body";
|
|
2729
|
+
else {
|
|
2730
|
+
try {
|
|
2731
|
+
readJson2(text);
|
|
2732
|
+
} catch {
|
|
2733
|
+
valid = false;
|
|
2734
|
+
message = "Invalid JSON";
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
shell.classList.toggle("invalid", !valid);
|
|
2738
|
+
status.classList.toggle("invalid", !valid);
|
|
2739
|
+
status.textContent = message;
|
|
2740
|
+
textarea.setAttribute("aria-invalid", valid ? "false" : "true");
|
|
2741
|
+
};
|
|
2742
|
+
const syncScroll = () => {
|
|
2743
|
+
const preview2 = highlight.parentElement;
|
|
2744
|
+
if (!(preview2 instanceof HTMLElement))
|
|
2745
|
+
return;
|
|
2746
|
+
preview2.scrollTop = textarea.scrollTop;
|
|
2747
|
+
preview2.scrollLeft = textarea.scrollLeft;
|
|
2748
|
+
};
|
|
2749
|
+
textarea.addEventListener("input", sync);
|
|
2750
|
+
textarea.addEventListener("scroll", syncScroll);
|
|
2751
|
+
sync();
|
|
2752
|
+
return sync;
|
|
2753
|
+
};
|
|
2754
|
+
const syncRouteEditor = wireJsonEditor("route-body", "route-highlight", "route-json-status", "route-editor", true);
|
|
2755
|
+
const syncRecordEditor = wireJsonEditor("editor-value", "editor-highlight", "editor-json-status", "record-editor", false);
|
|
2756
|
+
for (const button of document.querySelectorAll("[data-format]")) {
|
|
2757
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2758
|
+
continue;
|
|
2759
|
+
button.addEventListener("click", () => {
|
|
2760
|
+
const target = button.dataset.format;
|
|
2761
|
+
if (target === void 0)
|
|
2762
|
+
return;
|
|
2763
|
+
const textarea = document.getElementById(target);
|
|
2764
|
+
if (!(textarea instanceof HTMLTextAreaElement) || textarea.value.trim() === "")
|
|
2765
|
+
return;
|
|
2766
|
+
try {
|
|
2767
|
+
textarea.value = JSON.stringify(readJson2(textarea.value), null, 2) ?? "";
|
|
2768
|
+
textarea.dispatchEvent(new Event("input"));
|
|
2769
|
+
textarea.focus();
|
|
2770
|
+
} catch {
|
|
2771
|
+
showError(new Error("Fix the JSON before formatting it"));
|
|
2772
|
+
}
|
|
2773
|
+
});
|
|
2774
|
+
}
|
|
2581
2775
|
document.addEventListener("click", (event) => {
|
|
2582
2776
|
const target = event.target;
|
|
2583
2777
|
if (!(target instanceof Element))
|
|
@@ -2650,6 +2844,7 @@ function bootAdmin(config) {
|
|
|
2650
2844
|
} else {
|
|
2651
2845
|
editorValue.value = "{\n \n}";
|
|
2652
2846
|
}
|
|
2847
|
+
syncRecordEditor();
|
|
2653
2848
|
editor.showModal();
|
|
2654
2849
|
}
|
|
2655
2850
|
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -2679,10 +2874,10 @@ function bootAdmin(config) {
|
|
|
2679
2874
|
const id = state.editing;
|
|
2680
2875
|
if (!id)
|
|
2681
2876
|
return;
|
|
2682
|
-
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2877
|
+
confirmAction(editorDelete, "Deleting...", "Deleted", () => api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2683
2878
|
editor.close();
|
|
2684
2879
|
return renderState();
|
|
2685
|
-
}).catch(showError);
|
|
2880
|
+
}), "Confirm delete").catch(showError);
|
|
2686
2881
|
});
|
|
2687
2882
|
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2688
2883
|
if (!(button instanceof HTMLButtonElement))
|
|
@@ -2728,10 +2923,10 @@ function bootAdmin(config) {
|
|
|
2728
2923
|
}).then(() => renderFaults()).catch(showError);
|
|
2729
2924
|
});
|
|
2730
2925
|
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2731
|
-
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
|
|
2926
|
+
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults()), "Confirm clear").catch(showError);
|
|
2732
2927
|
});
|
|
2733
2928
|
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2734
|
-
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
|
|
2929
|
+
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal()), "Confirm clear").catch(showError);
|
|
2735
2930
|
});
|
|
2736
2931
|
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2737
2932
|
const target = event.target;
|
|
@@ -2745,9 +2940,15 @@ function bootAdmin(config) {
|
|
|
2745
2940
|
return;
|
|
2746
2941
|
state.route = route;
|
|
2747
2942
|
el("route-title", HTMLElement).textContent = route;
|
|
2943
|
+
el("route-send", HTMLButtonElement).disabled = false;
|
|
2748
2944
|
const method = route.split(" ")[0] ?? "";
|
|
2749
|
-
el("route-body", HTMLTextAreaElement)
|
|
2945
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
2946
|
+
routeBody.disabled = method === "GET" || method === "DELETE";
|
|
2947
|
+
el("route-editor", HTMLElement).hidden = routeBody.disabled;
|
|
2948
|
+
el("route-json-status", HTMLElement).parentElement?.toggleAttribute("hidden", routeBody.disabled);
|
|
2949
|
+
syncRouteEditor();
|
|
2750
2950
|
});
|
|
2951
|
+
el("route-search", HTMLInputElement).addEventListener("input", paintRoutes);
|
|
2751
2952
|
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2752
2953
|
event.preventDefault();
|
|
2753
2954
|
const send = async () => {
|
|
@@ -2769,7 +2970,8 @@ function bootAdmin(config) {
|
|
|
2769
2970
|
}
|
|
2770
2971
|
}
|
|
2771
2972
|
const data = await api(method, concrete, body);
|
|
2772
|
-
|
|
2973
|
+
const text = JSON.stringify(data, null, 2) ?? "null";
|
|
2974
|
+
el("route-result", HTMLElement).innerHTML = `<code>${highlightJsonText(text)}</code>`;
|
|
2773
2975
|
};
|
|
2774
2976
|
send().catch(showError);
|
|
2775
2977
|
});
|
|
@@ -2887,11 +3089,13 @@ function bootAdmin(config) {
|
|
|
2887
3089
|
views.append(section);
|
|
2888
3090
|
mountSqlExplorer(body, panelApi);
|
|
2889
3091
|
}
|
|
3092
|
+
show(document.getElementById(`view-${state.view}`) === null ? "overview" : state.view);
|
|
2890
3093
|
return refresh();
|
|
2891
3094
|
}).catch(showError);
|
|
2892
3095
|
}
|
|
2893
3096
|
var clientPrelude = [
|
|
2894
3097
|
isRecord2,
|
|
3098
|
+
highlightJsonText,
|
|
2895
3099
|
mountAdminBrand,
|
|
2896
3100
|
startAdminBrand,
|
|
2897
3101
|
faultPresetList,
|
|
@@ -2914,6 +3118,7 @@ ${entry}(${payload});
|
|
|
2914
3118
|
var defineMock = (runtime) => runtime;
|
|
2915
3119
|
var STANDARD_ADMIN_ROUTES = [
|
|
2916
3120
|
"GET /",
|
|
3121
|
+
"GET /health",
|
|
2917
3122
|
"POST /reset",
|
|
2918
3123
|
"GET /namespaces",
|
|
2919
3124
|
"GET /clock",
|
|
@@ -2993,11 +3198,11 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
2993
3198
|
}
|
|
2994
3199
|
});
|
|
2995
3200
|
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
2996
|
-
var renderAdminDocument = (service) => {
|
|
3201
|
+
var renderAdminDocument = (service, adminPrefix = ADMIN_PREFIX) => {
|
|
2997
3202
|
const name = escapeHtml(service);
|
|
2998
3203
|
const script = adminClientSource({
|
|
2999
3204
|
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
3000
|
-
adminPrefix
|
|
3205
|
+
adminPrefix,
|
|
3001
3206
|
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
3002
3207
|
brandsUrl: ADMIN_BRANDS_URL,
|
|
3003
3208
|
service
|
|
@@ -3014,75 +3219,99 @@ var renderAdminDocument = (service) => {
|
|
|
3014
3219
|
${CSS_RESET}
|
|
3015
3220
|
:root {
|
|
3016
3221
|
color-scheme: light;
|
|
3017
|
-
--bg: #
|
|
3222
|
+
--bg: #f7f8fb;
|
|
3018
3223
|
--bg-elev: #ffffff;
|
|
3019
|
-
--bg-muted: #
|
|
3020
|
-
--bg-inset: #
|
|
3021
|
-
--line: #
|
|
3022
|
-
--line-strong: #
|
|
3023
|
-
--ink: #
|
|
3024
|
-
--muted: #
|
|
3025
|
-
--faint: #
|
|
3026
|
-
--accent: #
|
|
3224
|
+
--bg-muted: #f1f3f7;
|
|
3225
|
+
--bg-inset: #f8f9fc;
|
|
3226
|
+
--line: #e4e7ec;
|
|
3227
|
+
--line-strong: #cfd4dc;
|
|
3228
|
+
--ink: #101828;
|
|
3229
|
+
--muted: #475467;
|
|
3230
|
+
--faint: #667085;
|
|
3231
|
+
--accent: #6941c6;
|
|
3232
|
+
--accent-hover: #5933b4;
|
|
3027
3233
|
--accent-ink: #ffffff;
|
|
3028
|
-
--accent-soft: #
|
|
3029
|
-
--ok: #
|
|
3030
|
-
--ok-soft: #
|
|
3031
|
-
--warn: #
|
|
3032
|
-
--warn-soft: #
|
|
3033
|
-
--err: #
|
|
3034
|
-
--err-soft: #
|
|
3035
|
-
--
|
|
3234
|
+
--accent-soft: #f4f0ff;
|
|
3235
|
+
--ok: #067647;
|
|
3236
|
+
--ok-soft: #ecfdf3;
|
|
3237
|
+
--warn: #b54708;
|
|
3238
|
+
--warn-soft: #fffaeb;
|
|
3239
|
+
--err: #b42318;
|
|
3240
|
+
--err-soft: #fef3f2;
|
|
3241
|
+
--code-key: #6941c6;
|
|
3242
|
+
--code-string: #067647;
|
|
3243
|
+
--code-number: #175cd3;
|
|
3244
|
+
--code-boolean: #c11574;
|
|
3245
|
+
--code-null: #667085;
|
|
3246
|
+
--shadow-xs: 0 1px 2px rgb(16 24 40 / 0.05);
|
|
3247
|
+
--shadow-sm: 0 1px 3px rgb(16 24 40 / 0.08), 0 1px 2px rgb(16 24 40 / 0.04);
|
|
3248
|
+
--shadow-lg: 0 18px 48px rgb(16 24 40 / 0.16), 0 4px 12px rgb(16 24 40 / 0.08);
|
|
3036
3249
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
3037
3250
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
3038
|
-
--radius:
|
|
3039
|
-
--header:
|
|
3040
|
-
--nav:
|
|
3251
|
+
--radius: 12px;
|
|
3252
|
+
--header: 72px;
|
|
3253
|
+
--nav: 224px;
|
|
3041
3254
|
}
|
|
3042
3255
|
:root[data-theme="dark"] {
|
|
3043
3256
|
color-scheme: dark;
|
|
3044
|
-
--bg: #
|
|
3045
|
-
--bg-elev: #
|
|
3046
|
-
--bg-muted: #
|
|
3047
|
-
--bg-inset: #
|
|
3048
|
-
--line: #
|
|
3049
|
-
--line-strong: #
|
|
3050
|
-
--ink: #
|
|
3051
|
-
--muted: #
|
|
3052
|
-
--faint: #
|
|
3053
|
-
--accent: #
|
|
3054
|
-
--accent-
|
|
3055
|
-
--accent-
|
|
3056
|
-
--
|
|
3057
|
-
--ok
|
|
3058
|
-
--
|
|
3059
|
-
--warn
|
|
3060
|
-
--
|
|
3061
|
-
--err
|
|
3062
|
-
--
|
|
3257
|
+
--bg: #0c0e14;
|
|
3258
|
+
--bg-elev: #151821;
|
|
3259
|
+
--bg-muted: #20232e;
|
|
3260
|
+
--bg-inset: #11141c;
|
|
3261
|
+
--line: #2b303d;
|
|
3262
|
+
--line-strong: #414858;
|
|
3263
|
+
--ink: #f5f7fa;
|
|
3264
|
+
--muted: #b6bdc9;
|
|
3265
|
+
--faint: #8d96a7;
|
|
3266
|
+
--accent: #9e77ed;
|
|
3267
|
+
--accent-hover: #b692f6;
|
|
3268
|
+
--accent-ink: #ffffff;
|
|
3269
|
+
--accent-soft: #2b2142;
|
|
3270
|
+
--ok: #75e0a7;
|
|
3271
|
+
--ok-soft: #102a20;
|
|
3272
|
+
--warn: #fec84b;
|
|
3273
|
+
--warn-soft: #302611;
|
|
3274
|
+
--err: #fda29b;
|
|
3275
|
+
--err-soft: #321817;
|
|
3276
|
+
--code-key: #c3a6ff;
|
|
3277
|
+
--code-string: #75e0a7;
|
|
3278
|
+
--code-number: #84adff;
|
|
3279
|
+
--code-boolean: #f9a8d4;
|
|
3280
|
+
--code-null: #98a2b3;
|
|
3281
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3282
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3283
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
3063
3284
|
}
|
|
3064
3285
|
@media (prefers-color-scheme: dark) {
|
|
3065
3286
|
:root:not([data-theme="light"]) {
|
|
3066
3287
|
color-scheme: dark;
|
|
3067
|
-
--bg: #
|
|
3068
|
-
--bg-elev: #
|
|
3069
|
-
--bg-muted: #
|
|
3070
|
-
--bg-inset: #
|
|
3071
|
-
--line: #
|
|
3072
|
-
--line-strong: #
|
|
3073
|
-
--ink: #
|
|
3074
|
-
--muted: #
|
|
3075
|
-
--faint: #
|
|
3076
|
-
--accent: #
|
|
3077
|
-
--accent-
|
|
3078
|
-
--accent-
|
|
3079
|
-
--
|
|
3080
|
-
--ok
|
|
3081
|
-
--
|
|
3082
|
-
--warn
|
|
3083
|
-
--
|
|
3084
|
-
--err
|
|
3085
|
-
--
|
|
3288
|
+
--bg: #0c0e14;
|
|
3289
|
+
--bg-elev: #151821;
|
|
3290
|
+
--bg-muted: #20232e;
|
|
3291
|
+
--bg-inset: #11141c;
|
|
3292
|
+
--line: #2b303d;
|
|
3293
|
+
--line-strong: #414858;
|
|
3294
|
+
--ink: #f5f7fa;
|
|
3295
|
+
--muted: #b6bdc9;
|
|
3296
|
+
--faint: #8d96a7;
|
|
3297
|
+
--accent: #9e77ed;
|
|
3298
|
+
--accent-hover: #b692f6;
|
|
3299
|
+
--accent-ink: #ffffff;
|
|
3300
|
+
--accent-soft: #2b2142;
|
|
3301
|
+
--ok: #75e0a7;
|
|
3302
|
+
--ok-soft: #102a20;
|
|
3303
|
+
--warn: #fec84b;
|
|
3304
|
+
--warn-soft: #302611;
|
|
3305
|
+
--err: #fda29b;
|
|
3306
|
+
--err-soft: #321817;
|
|
3307
|
+
--code-key: #c3a6ff;
|
|
3308
|
+
--code-string: #75e0a7;
|
|
3309
|
+
--code-number: #84adff;
|
|
3310
|
+
--code-boolean: #f9a8d4;
|
|
3311
|
+
--code-null: #98a2b3;
|
|
3312
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3313
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3314
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
3086
3315
|
}
|
|
3087
3316
|
}
|
|
3088
3317
|
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
@@ -3100,13 +3329,17 @@ var renderAdminDocument = (service) => {
|
|
|
3100
3329
|
padding-right: env(safe-area-inset-right);
|
|
3101
3330
|
-webkit-text-size-adjust: 100%;
|
|
3102
3331
|
}
|
|
3103
|
-
|
|
3332
|
+
::selection { background: color-mix(in srgb, var(--accent) 28%, transparent); }
|
|
3333
|
+
:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px; }
|
|
3334
|
+
button, input, select, textarea { transition: border-color 140ms ease, box-shadow 140ms ease, transform 100ms ease; }
|
|
3335
|
+
button:disabled, input:disabled, select:disabled, textarea:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
3104
3336
|
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
3105
3337
|
header.top {
|
|
3106
|
-
position: sticky; top: 0; z-index:
|
|
3338
|
+
position: sticky; top: 0; z-index: 20;
|
|
3107
3339
|
display: flex; align-items: center; gap: 12px;
|
|
3108
|
-
min-height: var(--header); padding:
|
|
3109
|
-
background: var(--bg-elev);
|
|
3340
|
+
min-height: var(--header); padding: 12px 20px;
|
|
3341
|
+
background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
|
|
3342
|
+
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
3110
3343
|
border-bottom: 1px solid var(--line);
|
|
3111
3344
|
min-width: 0; max-width: 100%;
|
|
3112
3345
|
}
|
|
@@ -3114,12 +3347,21 @@ var renderAdminDocument = (service) => {
|
|
|
3114
3347
|
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
3115
3348
|
min-width: 0; max-width: 100%; margin-right: auto;
|
|
3116
3349
|
}
|
|
3117
|
-
.brand-
|
|
3118
|
-
.brand-
|
|
3350
|
+
.brand-lockup { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
3351
|
+
.brand-mark {
|
|
3352
|
+
width: 38px; height: 38px; display: grid; place-items: center; flex: none;
|
|
3353
|
+
border-radius: 11px; color: #fff;
|
|
3354
|
+
background: linear-gradient(145deg, #7f56d9 0%, #53389e 100%);
|
|
3355
|
+
box-shadow: 0 6px 16px rgb(105 65 198 / 0.24), inset 0 1px 0 rgb(255 255 255 / 0.25);
|
|
3356
|
+
}
|
|
3357
|
+
.brand-mark svg { width: 22px; height: 22px; }
|
|
3358
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; line-height: 1.25; }
|
|
3359
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
3119
3360
|
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
3120
3361
|
.vendor {
|
|
3121
3362
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
3122
3363
|
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
3364
|
+
padding: 5px 8px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-inset);
|
|
3123
3365
|
}
|
|
3124
3366
|
.vendor[hidden] { display: none; }
|
|
3125
3367
|
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
@@ -3131,12 +3373,17 @@ var renderAdminDocument = (service) => {
|
|
|
3131
3373
|
.vendor-sep { color: var(--faint); }
|
|
3132
3374
|
.vendor-link { color: inherit; text-decoration: none; }
|
|
3133
3375
|
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
3134
|
-
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items:
|
|
3135
|
-
|
|
3376
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
|
|
3377
|
+
.sync-status { align-self: center; display: flex; align-items: center; gap: 6px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3378
|
+
.sync-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
|
|
3379
|
+
.sync-status.busy::before { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); animation: pulse 1.2s ease-in-out infinite; }
|
|
3380
|
+
label.field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; font-weight: 600; color: var(--faint); }
|
|
3136
3381
|
label.field input, label.field select, .controls input[type="password"] {
|
|
3137
|
-
background-color: var(--bg-
|
|
3138
|
-
padding:
|
|
3382
|
+
background-color: var(--bg-elev); border: 1px solid var(--line-strong); border-radius: 9px;
|
|
3383
|
+
padding: 8px 10px; min-height: 38px; line-height: 1.2; box-shadow: var(--shadow-xs);
|
|
3139
3384
|
}
|
|
3385
|
+
label.field input:hover, label.field select:hover, textarea:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--line-strong)); }
|
|
3386
|
+
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; }
|
|
3140
3387
|
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
3141
3388
|
the text fields in the same form. Paint it like those fields. */
|
|
3142
3389
|
label.field select {
|
|
@@ -3153,12 +3400,20 @@ var renderAdminDocument = (service) => {
|
|
|
3153
3400
|
dialog .field > textarea { width: 100%; }
|
|
3154
3401
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
3155
3402
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
.btn
|
|
3403
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
|
3404
|
+
border-radius: 9px; min-height: 38px; padding: 0 13px; border: 1px solid var(--line-strong);
|
|
3405
|
+
background: var(--bg-elev); color: var(--ink); font-size: 13px; font-weight: 600; line-height: 1;
|
|
3406
|
+
box-shadow: var(--shadow-xs);
|
|
3407
|
+
}
|
|
3408
|
+
.icon-btn { width: 38px; padding: 0; }
|
|
3409
|
+
.icon-btn svg, .btn svg, .btn-primary svg { width: 16px; height: 16px; }
|
|
3410
|
+
.icon-btn:hover, .btn:hover { background: var(--bg-muted); border-color: var(--line-strong); }
|
|
3411
|
+
.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; }
|
|
3412
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
3413
|
+
.btn-quiet { background: transparent; border-color: transparent; box-shadow: none; }
|
|
3414
|
+
.btn-quiet:hover { background: var(--bg-muted); }
|
|
3415
|
+
.btn-danger { color: var(--err); border-color: color-mix(in srgb, var(--err) 25%, var(--line)); }
|
|
3416
|
+
.btn-danger:hover, .btn-danger.confirming { background: var(--err-soft); border-color: color-mix(in srgb, var(--err) 45%, var(--line)); }
|
|
3162
3417
|
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
3163
3418
|
so the press itself has to stay visible. */
|
|
3164
3419
|
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
@@ -3174,93 +3429,159 @@ var renderAdminDocument = (service) => {
|
|
|
3174
3429
|
flex: 1 0 auto; min-width: 0;
|
|
3175
3430
|
}
|
|
3176
3431
|
nav.side {
|
|
3177
|
-
|
|
3432
|
+
position: sticky; top: var(--header); align-self: start; height: calc(100vh - var(--header));
|
|
3433
|
+
border-right: 1px solid var(--line); padding: 18px 12px;
|
|
3178
3434
|
display: flex; flex-direction: column; gap: 4px;
|
|
3179
|
-
background: var(--bg-elev);
|
|
3435
|
+
background: var(--bg-elev); overflow-y: auto;
|
|
3180
3436
|
}
|
|
3437
|
+
.nav-label { padding: 0 10px 8px; color: var(--faint); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
|
|
3181
3438
|
nav.side button {
|
|
3439
|
+
position: relative; display: flex; align-items: center; gap: 10px; width: 100%;
|
|
3182
3440
|
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
3183
|
-
padding: 9px 10px; min-height:
|
|
3184
|
-
}
|
|
3185
|
-
nav.side button
|
|
3186
|
-
|
|
3187
|
-
.
|
|
3188
|
-
.
|
|
3189
|
-
.
|
|
3441
|
+
padding: 9px 10px; min-height: 42px; color: var(--muted); font-size: 13px; font-weight: 550;
|
|
3442
|
+
}
|
|
3443
|
+
nav.side button:hover { background: var(--bg-muted); color: var(--ink); }
|
|
3444
|
+
nav.side button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 650; }
|
|
3445
|
+
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); }
|
|
3446
|
+
.nav-icon { width: 18px; height: 18px; display: grid; place-items: center; flex: none; }
|
|
3447
|
+
.nav-icon svg { width: 18px; height: 18px; }
|
|
3448
|
+
main { padding: 32px clamp(20px, 4vw, 48px) 64px; max-width: 1320px; width: 100%; min-width: 0; }
|
|
3449
|
+
main[aria-busy="true"] .view { opacity: 0.62; }
|
|
3450
|
+
.view { animation: view-in 180ms ease-out; }
|
|
3451
|
+
.view[hidden] { display: none; }
|
|
3452
|
+
.view-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
|
|
3453
|
+
.view h2 { margin: 0 0 5px; font-size: clamp(24px, 3vw, 30px); line-height: 1.2; letter-spacing: -0.035em; }
|
|
3454
|
+
.view h3 { margin: 24px 0 10px; font-size: 15px; letter-spacing: -0.01em; }
|
|
3455
|
+
.lede { margin: 0; color: var(--muted); max-width: 72ch; font-size: 14px; }
|
|
3456
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 175px), 1fr)); gap: 14px; }
|
|
3190
3457
|
.card {
|
|
3191
3458
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
3192
|
-
padding:
|
|
3459
|
+
padding: 18px; box-shadow: var(--shadow-sm);
|
|
3193
3460
|
}
|
|
3194
|
-
.card
|
|
3195
|
-
.card
|
|
3461
|
+
.stat-card { position: relative; overflow: hidden; }
|
|
3462
|
+
.stat-card::after { content: ""; position: absolute; inset: 0 0 auto; height: 2px; background: linear-gradient(90deg, var(--accent), transparent 78%); opacity: 0.7; }
|
|
3463
|
+
.card .k { color: var(--faint); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
3464
|
+
.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; }
|
|
3196
3465
|
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
3197
|
-
.
|
|
3466
|
+
.section-card { background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-xs); overflow: hidden; }
|
|
3467
|
+
.section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
|
|
3468
|
+
.section-title h3 { margin: 0; }
|
|
3469
|
+
.split { display: grid; grid-template-columns: minmax(0, 250px) minmax(0, 1fr); gap: 18px; align-items: start; }
|
|
3470
|
+
#collections { padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); box-shadow: var(--shadow-xs); }
|
|
3198
3471
|
.collection {
|
|
3199
3472
|
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
3200
|
-
border-radius:
|
|
3473
|
+
border-radius: 9px; padding: 10px 11px; min-height: 48px; color: var(--ink); cursor: pointer;
|
|
3201
3474
|
}
|
|
3202
|
-
.collection
|
|
3475
|
+
.collection:hover { background: var(--bg-muted); }
|
|
3476
|
+
.collection[aria-current="true"] { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 18%, var(--line)); color: var(--accent); }
|
|
3203
3477
|
.collection small { display: block; color: var(--faint); }
|
|
3204
3478
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
3205
|
-
.chip {
|
|
3206
|
-
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
3207
|
-
border-radius: 999px; padding: 4px 8px;
|
|
3208
|
-
}
|
|
3479
|
+
.chip { font-family: var(--mono); font-size: 11px; background: var(--bg-muted); border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; }
|
|
3209
3480
|
.chip i { color: var(--faint); font-style: normal; }
|
|
3210
3481
|
.sql-layout {
|
|
3211
3482
|
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
3212
3483
|
}
|
|
3213
|
-
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
3214
|
-
.sql-tables button { text-align: left; }
|
|
3484
|
+
.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); }
|
|
3485
|
+
.sql-tables button { text-align: left; border: 0; border-radius: 8px; padding: 9px 10px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
3486
|
+
.sql-tables button:hover, .sql-tables button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); }
|
|
3215
3487
|
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
3216
|
-
table { width: 100%; border-
|
|
3217
|
-
|
|
3218
|
-
th {
|
|
3488
|
+
.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); }
|
|
3489
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
3490
|
+
th, td { text-align: left; padding: 11px 13px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
3491
|
+
th { color: var(--faint); background: var(--bg-inset); font-weight: 650; font-size: 11px; text-transform: uppercase; letter-spacing: 0.045em; }
|
|
3492
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
3219
3493
|
tr.clickable { cursor: pointer; }
|
|
3220
|
-
tr.clickable:hover { background: var(--
|
|
3494
|
+
tr.clickable:hover { background: var(--accent-soft); }
|
|
3221
3495
|
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
3222
3496
|
pre {
|
|
3223
3497
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
3224
|
-
background: var(--bg-inset); border-radius: 10px; padding:
|
|
3225
|
-
}
|
|
3226
|
-
.
|
|
3498
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 10px; padding: 14px; font-size: 12px; line-height: 1.65;
|
|
3499
|
+
}
|
|
3500
|
+
.json-key { color: var(--code-key); }
|
|
3501
|
+
.json-string { color: var(--code-string); }
|
|
3502
|
+
.json-number { color: var(--code-number); }
|
|
3503
|
+
.json-boolean { color: var(--code-boolean); }
|
|
3504
|
+
.json-null { color: var(--code-null); font-style: italic; }
|
|
3505
|
+
.json-inline { display: block; max-width: 44rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
3506
|
+
.json-view { position: relative; max-height: 26rem; overflow: auto; }
|
|
3507
|
+
.json-editor { position: relative; min-height: 190px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--bg-inset); overflow: hidden; }
|
|
3508
|
+
.json-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); }
|
|
3509
|
+
.json-editor.invalid { border-color: var(--err); }
|
|
3510
|
+
.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; }
|
|
3511
|
+
.json-editor pre { position: absolute; inset: 0; pointer-events: none; background: transparent; }
|
|
3512
|
+
.json-editor textarea { position: relative; z-index: 1; resize: vertical; background: transparent; color: transparent; caret-color: var(--ink); -webkit-text-fill-color: transparent; }
|
|
3513
|
+
.json-editor textarea::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); }
|
|
3514
|
+
.editor-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; }
|
|
3515
|
+
.json-status { display: inline-flex; align-items: center; gap: 6px; color: var(--ok); font-size: 11px; }
|
|
3516
|
+
.json-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
3517
|
+
.json-status.invalid { color: var(--err); }
|
|
3518
|
+
.format-json { border: 0; padding: 3px 5px; background: transparent; color: var(--accent); font-size: 11px; font-weight: 650; cursor: pointer; }
|
|
3519
|
+
.row-actions { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 14px 0; }
|
|
3227
3520
|
.banner {
|
|
3228
|
-
display: none;
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3521
|
+
display: none; position: sticky; top: calc(var(--header) + 12px); z-index: 15;
|
|
3522
|
+
align-items: center; gap: 10px; margin: 0 0 16px; padding: 11px 12px 11px 14px;
|
|
3523
|
+
border: 1px solid color-mix(in srgb, var(--err) 30%, var(--line)); border-radius: 10px;
|
|
3524
|
+
background: var(--err-soft); color: var(--err); box-shadow: var(--shadow-sm); font-size: 13px;
|
|
3525
|
+
}
|
|
3526
|
+
.banner.show { display: flex; }
|
|
3527
|
+
.banner span { flex: 1; }
|
|
3528
|
+
.banner button { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 7px; background: transparent; color: currentColor; cursor: pointer; }
|
|
3529
|
+
.banner button:hover { background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
3530
|
+
.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); }
|
|
3233
3531
|
dialog {
|
|
3234
|
-
border: 1px solid var(--line); border-radius:
|
|
3235
|
-
color: var(--ink); width: min(
|
|
3236
|
-
}
|
|
3237
|
-
dialog
|
|
3238
|
-
dialog
|
|
3532
|
+
border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--bg-elev);
|
|
3533
|
+
color: var(--ink); width: min(720px, calc(100% - 24px)); max-width: calc(100vw - 24px); box-shadow: var(--shadow-lg);
|
|
3534
|
+
}
|
|
3535
|
+
dialog[open] { animation: dialog-in 160ms ease-out; }
|
|
3536
|
+
dialog::backdrop { background: rgb(12 14 20 / 0.58); backdrop-filter: blur(3px); }
|
|
3537
|
+
dialog form, .dialog-body { padding: 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
3538
|
+
.dialog-heading { display: flex; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
|
3539
|
+
.dialog-heading strong { font-size: 18px; letter-spacing: -0.02em; }
|
|
3540
|
+
.dialog-heading span { color: var(--muted); font-size: 12px; }
|
|
3239
3541
|
textarea {
|
|
3240
3542
|
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
3241
3543
|
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
3242
3544
|
font-family: var(--mono); font-size: 12px;
|
|
3243
3545
|
}
|
|
3244
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
3245
|
-
.
|
|
3546
|
+
.dialog-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding-top: 4px; }
|
|
3547
|
+
.dialog-actions .btn-danger { margin-right: auto; }
|
|
3548
|
+
.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); }
|
|
3246
3549
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
3247
3550
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
3248
3551
|
.status-2 { color: var(--ok); }
|
|
3249
3552
|
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
3250
3553
|
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
3554
|
+
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
3555
|
+
.form-grid .wide { grid-column: span 2; }
|
|
3251
3556
|
.route {
|
|
3252
|
-
display: grid; grid-template-columns:
|
|
3253
|
-
padding:
|
|
3254
|
-
}
|
|
3255
|
-
.
|
|
3557
|
+
display: grid; grid-template-columns: 84px minmax(0, 1fr) auto; gap: 10px; align-items: center;
|
|
3558
|
+
padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--bg-elev);
|
|
3559
|
+
}
|
|
3560
|
+
.route:first-child { border-radius: var(--radius) var(--radius) 0 0; }
|
|
3561
|
+
.route:last-child { border-bottom: 0; border-radius: 0 0 var(--radius) var(--radius); }
|
|
3562
|
+
.route-tools { display: flex; align-items: flex-end; gap: 12px; margin-bottom: 12px; }
|
|
3563
|
+
.route-tools .field { flex: 1; }
|
|
3564
|
+
.route-tools input { width: 100%; }
|
|
3565
|
+
.route-count { padding-bottom: 10px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3566
|
+
#route-list { max-height: 34rem; border: 1px solid var(--line); border-radius: var(--radius); overflow: auto; box-shadow: var(--shadow-xs); }
|
|
3567
|
+
.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; }
|
|
3568
|
+
@keyframes view-in { from { transform: translateY(3px); } to { transform: translateY(0); } }
|
|
3569
|
+
@keyframes dialog-in { from { transform: translateY(8px) scale(0.99); } to { transform: translateY(0) scale(1); } }
|
|
3570
|
+
@keyframes pulse { 50% { opacity: 0.45; } }
|
|
3571
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
|
|
3572
|
+
@media (forced-colors: active) { .json-editor pre { display: none; } .json-editor textarea { color: CanvasText; -webkit-text-fill-color: CanvasText; } }
|
|
3256
3573
|
@media (max-width: 800px) {
|
|
3574
|
+
:root { --header: 0px; }
|
|
3257
3575
|
header.top {
|
|
3258
|
-
align-items: stretch; flex-direction: column;
|
|
3259
|
-
padding
|
|
3576
|
+
position: relative; align-items: stretch; flex-direction: column;
|
|
3577
|
+
padding: max(12px, env(safe-area-inset-top)) 14px 12px;
|
|
3260
3578
|
}
|
|
3261
3579
|
input, select, textarea { font-size: 16px; }
|
|
3262
3580
|
.brand, .vendor, .controls { width: 100%; }
|
|
3581
|
+
.brand { gap: 10px; }
|
|
3582
|
+
.vendor { order: 2; border-radius: 9px; }
|
|
3263
3583
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
3584
|
+
.sync-status { display: none; }
|
|
3264
3585
|
/* The layout fills the screen. A single-column grid stretches every auto
|
|
3265
3586
|
row, so the tab bar grew with the leftover height and the buttons floated
|
|
3266
3587
|
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
@@ -3270,14 +3591,23 @@ var renderAdminDocument = (service) => {
|
|
|
3270
3591
|
align-content: start;
|
|
3271
3592
|
}
|
|
3272
3593
|
nav.side {
|
|
3273
|
-
align-self: start; height: auto;
|
|
3594
|
+
position: sticky; top: 0; z-index: 12; align-self: start; height: auto;
|
|
3274
3595
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
3275
3596
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
3276
|
-
padding: 8px;
|
|
3597
|
+
padding: 8px; box-shadow: var(--shadow-xs);
|
|
3277
3598
|
}
|
|
3278
|
-
nav
|
|
3599
|
+
.nav-label { display: none; }
|
|
3600
|
+
nav.side button { width: auto; white-space: nowrap; flex: none; }
|
|
3601
|
+
nav.side button[aria-current="true"]::before { inset: auto 9px -8px; width: auto; height: 3px; border-radius: 3px 3px 0 0; }
|
|
3279
3602
|
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
3280
|
-
|
|
3603
|
+
.form-grid { grid-template-columns: minmax(0, 1fr); }
|
|
3604
|
+
.form-grid .wide { grid-column: auto; }
|
|
3605
|
+
main { padding: 22px 14px 48px; }
|
|
3606
|
+
.view-head { margin-bottom: 18px; }
|
|
3607
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
3608
|
+
.card { padding: 14px; }
|
|
3609
|
+
.card .v { font-size: 20px; }
|
|
3610
|
+
.table-shell { border: 0; background: transparent; box-shadow: none; overflow: visible; }
|
|
3281
3611
|
table, thead, tbody, tr, th, td { display: block; }
|
|
3282
3612
|
thead { display: none; }
|
|
3283
3613
|
tr {
|
|
@@ -3292,50 +3622,63 @@ var renderAdminDocument = (service) => {
|
|
|
3292
3622
|
.route { grid-template-columns: 72px 1fr; }
|
|
3293
3623
|
.route button { grid-column: 1 / -1; }
|
|
3294
3624
|
}
|
|
3625
|
+
@media (max-width: 460px) {
|
|
3626
|
+
.controls label.field { min-width: calc(50% - 4px); }
|
|
3627
|
+
.controls .icon-btn, .controls .btn { flex: 1; }
|
|
3628
|
+
.grid { grid-template-columns: minmax(0, 1fr); }
|
|
3629
|
+
.dialog-actions { flex-wrap: wrap; }
|
|
3630
|
+
.dialog-actions button { flex: 1; }
|
|
3631
|
+
.dialog-actions .btn-danger { flex-basis: 100%; margin-right: 0; }
|
|
3632
|
+
}
|
|
3295
3633
|
</style>
|
|
3296
3634
|
</head>
|
|
3297
3635
|
<body>
|
|
3298
3636
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
3299
3637
|
<header class="top">
|
|
3300
3638
|
<div class="brand">
|
|
3301
|
-
<div class="brand-
|
|
3302
|
-
<
|
|
3303
|
-
|
|
3639
|
+
<div class="brand-lockup">
|
|
3640
|
+
<div class="brand-mark" aria-hidden="true">
|
|
3641
|
+
<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>
|
|
3642
|
+
</div>
|
|
3643
|
+
<div class="brand-id">
|
|
3644
|
+
<strong>${name}</strong>
|
|
3645
|
+
<span>Mock admin</span>
|
|
3646
|
+
</div>
|
|
3304
3647
|
</div>
|
|
3305
3648
|
<div class="vendor" id="vendor" hidden></div>
|
|
3306
3649
|
</div>
|
|
3307
3650
|
<div class="controls">
|
|
3651
|
+
<span class="sync-status" id="sync-status" role="status" aria-live="polite">Ready</span>
|
|
3308
3652
|
<label class="field">Namespace
|
|
3309
3653
|
<select id="namespace" aria-label="Namespace"></select>
|
|
3310
3654
|
</label>
|
|
3311
3655
|
<label class="field">Admin key
|
|
3312
3656
|
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
3313
3657
|
</label>
|
|
3314
|
-
<button class="icon-btn" id="theme" type="button" aria-label="
|
|
3315
|
-
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
3658
|
+
<button class="icon-btn" id="theme" type="button" aria-label="Use dark theme" title="Use dark theme"></button>
|
|
3659
|
+
<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>
|
|
3316
3660
|
</div>
|
|
3317
3661
|
</header>
|
|
3318
3662
|
<div class="layout">
|
|
3319
3663
|
<nav class="side" id="nav" aria-label="Admin sections">
|
|
3320
|
-
<
|
|
3321
|
-
<button type="button" data-view="
|
|
3322
|
-
<button type="button" data-view="
|
|
3323
|
-
<button type="button" data-view="
|
|
3324
|
-
<button type="button" data-view="
|
|
3325
|
-
<button type="button" data-view="
|
|
3664
|
+
<span class="nav-label">Workspace</span>
|
|
3665
|
+
<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>
|
|
3666
|
+
<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>
|
|
3667
|
+
<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>
|
|
3668
|
+
<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>
|
|
3669
|
+
<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>
|
|
3670
|
+
<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>
|
|
3326
3671
|
</nav>
|
|
3327
3672
|
<main id="views">
|
|
3328
|
-
<
|
|
3673
|
+
<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>
|
|
3329
3674
|
<section class="view" id="view-overview">
|
|
3330
|
-
<h2>Overview</h2>
|
|
3331
|
-
<p class="lede">Health, clock, and the collections this namespace is holding.</p>
|
|
3675
|
+
<div class="view-head"><div><h2>Overview</h2><p class="lede">Health, clock, and the collections this namespace is holding.</p></div></div>
|
|
3332
3676
|
<div class="grid" id="overview-cards"></div>
|
|
3333
3677
|
<h3>Recent requests</h3>
|
|
3334
3678
|
<div id="overview-requests"></div>
|
|
3335
3679
|
</section>
|
|
3336
3680
|
<section class="view" id="view-state" hidden>
|
|
3337
|
-
<h2>State</h2>
|
|
3338
|
-
<p class="lede">Every collection in this namespace. Declared fields stay visible before any row exists; stored rows add whatever else they contain.</p>
|
|
3681
|
+
<div class="view-head"><div><h2>State</h2><p class="lede">Inspect and edit every collection in this namespace.</p></div></div>
|
|
3339
3682
|
<div class="split">
|
|
3340
3683
|
<div id="collections"></div>
|
|
3341
3684
|
<div>
|
|
@@ -3348,8 +3691,7 @@ var renderAdminDocument = (service) => {
|
|
|
3348
3691
|
</div>
|
|
3349
3692
|
</section>
|
|
3350
3693
|
<section class="view" id="view-clock" hidden>
|
|
3351
|
-
<h2>Clock</h2>
|
|
3352
|
-
<p class="lede">The clock every timestamp in this mock reads.</p>
|
|
3694
|
+
<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>
|
|
3353
3695
|
<div class="card" id="clock-readout"></div>
|
|
3354
3696
|
<div class="row-actions">
|
|
3355
3697
|
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
@@ -3368,21 +3710,22 @@ var renderAdminDocument = (service) => {
|
|
|
3368
3710
|
</form>
|
|
3369
3711
|
</section>
|
|
3370
3712
|
<section class="view" id="view-faults" hidden>
|
|
3371
|
-
<h2>Faults</h2>
|
|
3372
|
-
<p class="lede">Inject a response, a delay, or a named preset. Rules apply to this namespace.</p>
|
|
3713
|
+
<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>
|
|
3373
3714
|
<form id="fault-form" class="card stack">
|
|
3374
|
-
<
|
|
3375
|
-
<
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
<
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
<
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
<
|
|
3385
|
-
|
|
3715
|
+
<div class="form-grid">
|
|
3716
|
+
<label class="field wide">Preset
|
|
3717
|
+
<select id="fault-preset"><option value="">Custom rule</option></select>
|
|
3718
|
+
</label>
|
|
3719
|
+
<label class="field">Status
|
|
3720
|
+
<input id="fault-status" inputmode="numeric" placeholder="503">
|
|
3721
|
+
</label>
|
|
3722
|
+
<label class="field wide">Path prefix
|
|
3723
|
+
<input id="fault-path" placeholder="/v1">
|
|
3724
|
+
</label>
|
|
3725
|
+
<label class="field">Method
|
|
3726
|
+
<input id="fault-method" placeholder="GET">
|
|
3727
|
+
</label>
|
|
3728
|
+
</div>
|
|
3386
3729
|
<div class="dialog-actions">
|
|
3387
3730
|
<button class="btn-primary" type="submit">Add fault</button>
|
|
3388
3731
|
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
@@ -3391,26 +3734,31 @@ var renderAdminDocument = (service) => {
|
|
|
3391
3734
|
<div id="fault-list"></div>
|
|
3392
3735
|
</section>
|
|
3393
3736
|
<section class="view" id="view-journal" hidden>
|
|
3394
|
-
<h2>Journal</h2>
|
|
3395
|
-
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
3396
|
-
<div class="row-actions">
|
|
3397
|
-
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
3398
|
-
</div>
|
|
3737
|
+
<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>
|
|
3399
3738
|
<div id="journal"></div>
|
|
3400
3739
|
</section>
|
|
3401
3740
|
<section class="view" id="view-routes" hidden>
|
|
3402
|
-
<h2>Routes</h2>
|
|
3403
|
-
<
|
|
3741
|
+
<div class="view-head"><div><h2>Routes</h2><p class="lede">Explore shared controls and service-specific admin endpoints.</p></div></div>
|
|
3742
|
+
<div class="route-tools">
|
|
3743
|
+
<label class="field">Filter routes
|
|
3744
|
+
<input id="route-search" type="search" placeholder="Method, path, or extension" autocomplete="off">
|
|
3745
|
+
</label>
|
|
3746
|
+
<span class="route-count" id="route-count"></span>
|
|
3747
|
+
</div>
|
|
3404
3748
|
<div id="route-list"></div>
|
|
3405
|
-
<form id="route-form" class="card stack">
|
|
3749
|
+
<form id="route-form" class="card stack" style="margin-top: 18px">
|
|
3406
3750
|
<strong id="route-title">Select a route</strong>
|
|
3407
3751
|
<label class="field">JSON body
|
|
3408
|
-
<
|
|
3752
|
+
<div class="json-editor" id="route-editor" hidden>
|
|
3753
|
+
<pre aria-hidden="true"><code id="route-highlight"></code></pre>
|
|
3754
|
+
<textarea id="route-body" placeholder="{ }" spellcheck="false" aria-describedby="route-json-status" disabled></textarea>
|
|
3755
|
+
</div>
|
|
3756
|
+
<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>
|
|
3409
3757
|
</label>
|
|
3410
3758
|
<div class="dialog-actions">
|
|
3411
|
-
<button class="btn-primary" type="submit">Send</button>
|
|
3759
|
+
<button class="btn-primary" id="route-send" type="submit" disabled>Send request</button>
|
|
3412
3760
|
</div>
|
|
3413
|
-
<pre id="route-result">Response will show here.</pre>
|
|
3761
|
+
<pre class="json-view" id="route-result"><code>Response will show here.</code></pre>
|
|
3414
3762
|
</form>
|
|
3415
3763
|
</section>
|
|
3416
3764
|
</main>
|
|
@@ -3418,12 +3766,16 @@ var renderAdminDocument = (service) => {
|
|
|
3418
3766
|
</div>
|
|
3419
3767
|
<dialog id="editor">
|
|
3420
3768
|
<form method="dialog" id="editor-form">
|
|
3421
|
-
<strong id="editor-title">Record</strong>
|
|
3769
|
+
<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>
|
|
3422
3770
|
<label class="field">Id
|
|
3423
3771
|
<input id="editor-id" required>
|
|
3424
3772
|
</label>
|
|
3425
3773
|
<label class="field">JSON value
|
|
3426
|
-
<
|
|
3774
|
+
<div class="json-editor" id="record-editor">
|
|
3775
|
+
<pre aria-hidden="true"><code id="editor-highlight"></code></pre>
|
|
3776
|
+
<textarea id="editor-value" spellcheck="false" aria-describedby="editor-json-status"></textarea>
|
|
3777
|
+
</div>
|
|
3778
|
+
<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>
|
|
3427
3779
|
</label>
|
|
3428
3780
|
<div class="dialog-actions">
|
|
3429
3781
|
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
@@ -3465,8 +3817,8 @@ var describeExtension = (extension) => {
|
|
|
3465
3817
|
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
3466
3818
|
};
|
|
3467
3819
|
};
|
|
3468
|
-
var adminUiRoutes = (service, ui) => {
|
|
3469
|
-
const shell = () => renderAdminDocument(service);
|
|
3820
|
+
var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
3821
|
+
const shell = () => renderAdminDocument(service, adminPrefix);
|
|
3470
3822
|
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
3471
3823
|
return {
|
|
3472
3824
|
"GET /ui": () => htmlResponse(document3()),
|
|
@@ -4157,10 +4509,13 @@ var stateAdminRoutes = (routes) => {
|
|
|
4157
4509
|
};
|
|
4158
4510
|
|
|
4159
4511
|
// ../core/dist/version.js
|
|
4160
|
-
var PACKAGE_VERSION = true ? "
|
|
4512
|
+
var PACKAGE_VERSION = true ? "2.0.0" : UNRELEASED_VERSION;
|
|
4161
4513
|
|
|
4162
4514
|
// ../core/dist/webhooks.js
|
|
4163
|
-
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4515
|
+
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4516
|
+
status,
|
|
4517
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4518
|
+
});
|
|
4164
4519
|
var adminError3 = (status, message) => json4(status, { error: { type: "mockingbird_admin", message } });
|
|
4165
4520
|
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4166
4521
|
var parseEndpoint = (value) => {
|
|
@@ -4268,7 +4623,6 @@ var AT_HEADER = "x-mockingbird-at";
|
|
|
4268
4623
|
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
4269
4624
|
var DEFAULT_NAMESPACE = "default";
|
|
4270
4625
|
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4271
|
-
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
4272
4626
|
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4273
4627
|
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
4274
4628
|
var effects = /* @__PURE__ */ new WeakMap();
|
|
@@ -4309,6 +4663,22 @@ var operationMatcher = (document3) => {
|
|
|
4309
4663
|
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
4310
4664
|
};
|
|
4311
4665
|
var createRuntime = (options) => {
|
|
4666
|
+
const adminPrefix = resolveAdminPrefix(options.adminPrefix);
|
|
4667
|
+
const internalPaths = Object.entries(options.document?.paths ?? {}).filter(([, item]) => item["x-mockingbird-internal"] === true).map(([path]) => path);
|
|
4668
|
+
for (const path of internalPaths) {
|
|
4669
|
+
if (!path.startsWith(`${ADMIN_PREFIX}/blobs/`))
|
|
4670
|
+
throw new Error(`internal transport ${path} must live under ${ADMIN_PREFIX}/blobs/`);
|
|
4671
|
+
}
|
|
4672
|
+
assertAdminPrefixAvailable(adminPrefix, Object.keys(options.document?.paths ?? {}).filter((path) => !internalPaths.includes(path)));
|
|
4673
|
+
const internalMatch = (path) => {
|
|
4674
|
+
for (const pattern of internalPaths) {
|
|
4675
|
+
const actual = path.split("/");
|
|
4676
|
+
const expected = pattern.split("/");
|
|
4677
|
+
if (actual.length === expected.length && expected.every((part, i) => part.startsWith("{") || part === actual[i]))
|
|
4678
|
+
return pattern;
|
|
4679
|
+
}
|
|
4680
|
+
return void 0;
|
|
4681
|
+
};
|
|
4312
4682
|
assertAdminUi(options.adminUi);
|
|
4313
4683
|
const sqlite = bootSqlite(options.sqlite);
|
|
4314
4684
|
const clock = options.clock ?? createClock();
|
|
@@ -4354,6 +4724,7 @@ var createRuntime = (options) => {
|
|
|
4354
4724
|
const created = options.create({
|
|
4355
4725
|
namespace: storageNamespace(key),
|
|
4356
4726
|
publicNamespace,
|
|
4727
|
+
adminPrefix,
|
|
4357
4728
|
sqlite,
|
|
4358
4729
|
clock: namespaceClock(publicNamespace),
|
|
4359
4730
|
rng: createdContextRng
|
|
@@ -4586,10 +4957,10 @@ var createRuntime = (options) => {
|
|
|
4586
4957
|
namespaceClock,
|
|
4587
4958
|
namespaceOf(request) {
|
|
4588
4959
|
const url = new URL(request.url);
|
|
4589
|
-
const prefixed =
|
|
4960
|
+
const prefixed = matchNamespacePath(url.pathname, adminPrefix);
|
|
4590
4961
|
const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
|
|
4591
4962
|
const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
|
|
4592
|
-
if (path
|
|
4963
|
+
if (isAdminPath(path, adminPrefix))
|
|
4593
4964
|
return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
|
|
4594
4965
|
if (selected !== void 0)
|
|
4595
4966
|
return selected;
|
|
@@ -4655,7 +5026,7 @@ var createRuntime = (options) => {
|
|
|
4655
5026
|
state: (namespace = DEFAULT_NAMESPACE) => inspectState(stateScope(namespace)),
|
|
4656
5027
|
fetch: async (incoming) => {
|
|
4657
5028
|
let request = incoming;
|
|
4658
|
-
const prefixed =
|
|
5029
|
+
const prefixed = matchNamespacePath(new URL(request.url).pathname, adminPrefix);
|
|
4659
5030
|
if (prefixed) {
|
|
4660
5031
|
const url2 = new URL(request.url);
|
|
4661
5032
|
url2.pathname = prefixed[2] ?? "/";
|
|
@@ -4671,7 +5042,15 @@ var createRuntime = (options) => {
|
|
|
4671
5042
|
signal: request.signal
|
|
4672
5043
|
});
|
|
4673
5044
|
}
|
|
4674
|
-
|
|
5045
|
+
const internalUrl = new URL(request.url);
|
|
5046
|
+
const internalPath = isAdminPath(internalUrl.pathname, adminPrefix) ? `${ADMIN_PREFIX}${internalUrl.pathname.slice(adminPrefix.length)}` : "";
|
|
5047
|
+
const internal = internalMatch(internalPath);
|
|
5048
|
+
const obsoleteInternalPath = adminPrefix !== ADMIN_PREFIX && internalMatch(internalUrl.pathname) && !internal;
|
|
5049
|
+
if (internal) {
|
|
5050
|
+
internalUrl.pathname = internalPath;
|
|
5051
|
+
request = new Request(internalUrl, request);
|
|
5052
|
+
}
|
|
5053
|
+
let namespace = internal ? internalUrl.searchParams.get("namespace") ?? control.namespaceOf(request) : control.namespaceOf(request);
|
|
4675
5054
|
if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
4676
5055
|
const credential = options.credential(request);
|
|
4677
5056
|
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
@@ -4691,7 +5070,9 @@ var createRuntime = (options) => {
|
|
|
4691
5070
|
return copy;
|
|
4692
5071
|
}
|
|
4693
5072
|
};
|
|
4694
|
-
|
|
5073
|
+
if (obsoleteInternalPath)
|
|
5074
|
+
return stamp(adminFail(404, "internal transport is only available under adminPrefix"));
|
|
5075
|
+
const handled = internal ? void 0 : await control.handle(request);
|
|
4695
5076
|
if (handled)
|
|
4696
5077
|
return stamp(handled);
|
|
4697
5078
|
const started = monotonicNow();
|
|
@@ -4732,7 +5113,7 @@ var createRuntime = (options) => {
|
|
|
4732
5113
|
type: "mockingbird_admin",
|
|
4733
5114
|
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
4734
5115
|
}
|
|
4735
|
-
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
5116
|
+
}), { status: 400, headers: { "content-type": "application/json; charset=utf-8" } }));
|
|
4736
5117
|
}
|
|
4737
5118
|
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
4738
5119
|
log(400);
|
|
@@ -4849,15 +5230,16 @@ var createRuntime = (options) => {
|
|
|
4849
5230
|
...options.admin?.(runtime) ?? {},
|
|
4850
5231
|
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
4851
5232
|
// maps `presets`, and a service route must not replace that list with a record.
|
|
4852
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
5233
|
+
...options.presets ? presetRoutes(options.presets, runtime, adminPrefix) : {},
|
|
4853
5234
|
...stateAdminRoutes({
|
|
4854
5235
|
open: stateScope,
|
|
4855
5236
|
afterWrite: (namespace) => {
|
|
4856
5237
|
checkpoint(namespace, "main");
|
|
4857
5238
|
}
|
|
4858
5239
|
}),
|
|
4859
|
-
...adminUiRoutes(options.name, options.adminUi)
|
|
5240
|
+
...adminUiRoutes(options.name, options.adminUi, adminPrefix)
|
|
4860
5241
|
},
|
|
5242
|
+
adminPrefix,
|
|
4861
5243
|
adminKey: options.adminKey
|
|
4862
5244
|
});
|
|
4863
5245
|
return defineMock(runtime);
|
|
@@ -4871,7 +5253,10 @@ var mutableResponse = (response) => {
|
|
|
4871
5253
|
return new Response(response.body, response);
|
|
4872
5254
|
}
|
|
4873
5255
|
};
|
|
4874
|
-
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5256
|
+
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5257
|
+
status,
|
|
5258
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
5259
|
+
});
|
|
4875
5260
|
var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
|
|
4876
5261
|
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4877
5262
|
var credentialRoutes = (registry) => ({
|
|
@@ -4923,7 +5308,7 @@ var credentialRoutes = (registry) => ({
|
|
|
4923
5308
|
return adminJson(200, { status: "ok" });
|
|
4924
5309
|
}
|
|
4925
5310
|
});
|
|
4926
|
-
var presetRoutes = (presets, runtime) => ({
|
|
5311
|
+
var presetRoutes = (presets, runtime, adminPrefix) => ({
|
|
4927
5312
|
"GET /faults/presets": () => {
|
|
4928
5313
|
const body = {
|
|
4929
5314
|
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
@@ -4933,7 +5318,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
4933
5318
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
4934
5319
|
const name = params.name;
|
|
4935
5320
|
if (!presets[name])
|
|
4936
|
-
return adminFail(404, `no fault preset ${name}; GET /
|
|
5321
|
+
return adminFail(404, `no fault preset ${name}; GET ${adminPrefix}/faults/presets`);
|
|
4937
5322
|
const overrides = isObject(body) ? body : {};
|
|
4938
5323
|
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
4939
5324
|
}
|
|
@@ -5181,6 +5566,7 @@ var createRuntime2 = (options = {}) => createRuntime({
|
|
|
5181
5566
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
5182
5567
|
...options.clock ? { clock: options.clock } : {},
|
|
5183
5568
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
5569
|
+
...options.adminPrefix !== void 0 ? { adminPrefix: options.adminPrefix } : {},
|
|
5184
5570
|
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
5185
5571
|
...options.onLog ? { onLog: options.onLog } : {},
|
|
5186
5572
|
credential: apiKeyCredential,
|
|
@@ -5603,6 +5989,7 @@ var PlaneAPI = class {
|
|
|
5603
5989
|
|
|
5604
5990
|
export {
|
|
5605
5991
|
createClock,
|
|
5992
|
+
resolveAdminPrefix,
|
|
5606
5993
|
parseDuration,
|
|
5607
5994
|
document2 as document,
|
|
5608
5995
|
operationIds,
|
|
@@ -5615,4 +6002,4 @@ export {
|
|
|
5615
6002
|
apiKeyCredential,
|
|
5616
6003
|
PlaneAPI
|
|
5617
6004
|
};
|
|
5618
|
-
//# sourceMappingURL=chunk-
|
|
6005
|
+
//# sourceMappingURL=chunk-C6XLQ2US.js.map
|