@crvouga/mockingbird-service-flex 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 +3 -3
- package/SUPPORT.md +24 -0
- package/dist/{chunk-O2G3Z532.js → chunk-E3W2D3EY.js} +627 -237
- package/dist/chunk-E3W2D3EY.js.map +7 -0
- package/dist/{chunk-Y75DCYJC.js → chunk-PWSGISMP.js} +44 -24
- package/dist/chunk-PWSGISMP.js.map +7 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +6 -3
- package/dist/index.js +1 -1
- package/dist/server.d.ts +5 -1
- package/dist/server.js +2 -2
- package/openapi.yaml +1180 -0
- package/package.json +28 -3
- package/dist/chunk-O2G3Z532.js.map +0 -7
- package/dist/chunk-Y75DCYJC.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
|
};
|
|
@@ -1824,6 +1863,34 @@ dialog:not([open]) {
|
|
|
1824
1863
|
function isRecord2(value) {
|
|
1825
1864
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1826
1865
|
}
|
|
1866
|
+
function highlightJsonText(text) {
|
|
1867
|
+
const escapeToken = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
1868
|
+
switch (char) {
|
|
1869
|
+
case "&":
|
|
1870
|
+
return "&";
|
|
1871
|
+
case "<":
|
|
1872
|
+
return "<";
|
|
1873
|
+
case ">":
|
|
1874
|
+
return ">";
|
|
1875
|
+
case '"':
|
|
1876
|
+
return """;
|
|
1877
|
+
default:
|
|
1878
|
+
return "'";
|
|
1879
|
+
}
|
|
1880
|
+
});
|
|
1881
|
+
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;
|
|
1882
|
+
let cursor = 0;
|
|
1883
|
+
let html2 = "";
|
|
1884
|
+
for (const match of text.matchAll(token)) {
|
|
1885
|
+
const index = match.index ?? 0;
|
|
1886
|
+
const value = match[0];
|
|
1887
|
+
html2 += escapeToken(text.slice(cursor, index));
|
|
1888
|
+
const kind = value.startsWith('"') ? value.trimEnd().endsWith(":") ? "key" : "string" : value === "true" || value === "false" ? "boolean" : value === "null" ? "null" : "number";
|
|
1889
|
+
html2 += `<span class="json-${kind}">${escapeToken(value)}</span>`;
|
|
1890
|
+
cursor = index + value.length;
|
|
1891
|
+
}
|
|
1892
|
+
return html2 + escapeToken(text.slice(cursor));
|
|
1893
|
+
}
|
|
1827
1894
|
function mountAdminBrand(doc, host, service, catalog) {
|
|
1828
1895
|
const clip = (value, max) => {
|
|
1829
1896
|
if (typeof value !== "string")
|
|
@@ -2185,14 +2252,16 @@ function bootAdmin(config) {
|
|
|
2185
2252
|
const state = {
|
|
2186
2253
|
namespace: "default",
|
|
2187
2254
|
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
2188
|
-
view: "overview",
|
|
2255
|
+
view: ["overview", "state", "clock", "faults", "journal", "routes"].includes(location.hash.slice(1)) ? location.hash.slice(1) : "overview",
|
|
2189
2256
|
collection: "",
|
|
2190
2257
|
shape: null,
|
|
2191
2258
|
route: null,
|
|
2259
|
+
routes: [],
|
|
2192
2260
|
editing: null
|
|
2193
2261
|
};
|
|
2194
2262
|
const standard = new Set(config.standardRoutes);
|
|
2195
2263
|
const banner = el("banner", HTMLElement);
|
|
2264
|
+
const bannerMessage = el("banner-message", HTMLElement);
|
|
2196
2265
|
const keyInput = el("admin-key", HTMLInputElement);
|
|
2197
2266
|
keyInput.value = state.key;
|
|
2198
2267
|
const params = new URLSearchParams(location.search);
|
|
@@ -2203,18 +2272,21 @@ function bootAdmin(config) {
|
|
|
2203
2272
|
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
2204
2273
|
params.delete("key");
|
|
2205
2274
|
const next = params.toString();
|
|
2206
|
-
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
2275
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : "") + location.hash);
|
|
2207
2276
|
}
|
|
2208
2277
|
const queryNamespace = params.get("namespace");
|
|
2209
2278
|
if (queryNamespace)
|
|
2210
2279
|
state.namespace = queryNamespace;
|
|
2211
2280
|
const showError = (error2) => {
|
|
2212
|
-
|
|
2281
|
+
bannerMessage.textContent = error2 instanceof Error ? error2.message : String(error2);
|
|
2213
2282
|
banner.classList.add("show");
|
|
2283
|
+
banner.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
2214
2284
|
};
|
|
2215
2285
|
const clearError = () => {
|
|
2216
2286
|
banner.classList.remove("show");
|
|
2287
|
+
bannerMessage.textContent = "";
|
|
2217
2288
|
};
|
|
2289
|
+
el("banner-dismiss", HTMLButtonElement).addEventListener("click", clearError);
|
|
2218
2290
|
const api = async (method, path, body) => {
|
|
2219
2291
|
const headers = { accept: "application/json" };
|
|
2220
2292
|
if (state.key !== "")
|
|
@@ -2258,16 +2330,21 @@ function bootAdmin(config) {
|
|
|
2258
2330
|
namespace: () => state.namespace
|
|
2259
2331
|
};
|
|
2260
2332
|
const isPanelScript = (value) => typeof value === "function";
|
|
2333
|
+
const themeButton = el("theme", HTMLButtonElement);
|
|
2261
2334
|
const setTheme = (theme) => {
|
|
2262
2335
|
if (theme)
|
|
2263
2336
|
document.documentElement.dataset.theme = theme;
|
|
2264
2337
|
else
|
|
2265
2338
|
delete document.documentElement.dataset.theme;
|
|
2339
|
+
const dark = theme ? theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2340
|
+
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>';
|
|
2341
|
+
const label = dark ? "Use light theme" : "Use dark theme";
|
|
2342
|
+
themeButton.setAttribute("aria-label", label);
|
|
2343
|
+
themeButton.title = label;
|
|
2266
2344
|
};
|
|
2267
2345
|
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
2346
|
+
setTheme(storedTheme === "dark" || storedTheme === "light" ? storedTheme : null);
|
|
2347
|
+
themeButton.addEventListener("click", () => {
|
|
2271
2348
|
const current = document.documentElement.dataset.theme;
|
|
2272
2349
|
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2273
2350
|
const next = dark ? "light" : "dark";
|
|
@@ -2286,6 +2363,7 @@ function bootAdmin(config) {
|
|
|
2286
2363
|
continue;
|
|
2287
2364
|
section.hidden = section.id !== `view-${view}`;
|
|
2288
2365
|
}
|
|
2366
|
+
history.replaceState(null, "", `${location.pathname}${location.search}#${encodeURIComponent(view)}`);
|
|
2289
2367
|
};
|
|
2290
2368
|
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
2291
2369
|
const target = event.target;
|
|
@@ -2300,7 +2378,7 @@ function bootAdmin(config) {
|
|
|
2300
2378
|
show(view);
|
|
2301
2379
|
refresh().catch(showError);
|
|
2302
2380
|
});
|
|
2303
|
-
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>`;
|
|
2381
|
+
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>`;
|
|
2304
2382
|
const table = (columns, rows) => {
|
|
2305
2383
|
if (rows.length === 0)
|
|
2306
2384
|
return '<p class="empty">Nothing here yet.</p>';
|
|
@@ -2310,16 +2388,22 @@ function bootAdmin(config) {
|
|
|
2310
2388
|
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
2311
2389
|
return `<tr${attrs}>${cells}</tr>`;
|
|
2312
2390
|
}).join("");
|
|
2313
|
-
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
2391
|
+
return `<div class="table-shell"><table><thead>${head}</thead><tbody>${body}</tbody></table></div>`;
|
|
2314
2392
|
};
|
|
2315
2393
|
const preview = (value) => {
|
|
2316
2394
|
const text = JSON.stringify(value) ?? "null";
|
|
2317
|
-
|
|
2395
|
+
const clipped = text.length > 180 ? `${text.slice(0, 180)}\u2026` : text;
|
|
2396
|
+
return `<code class="json-inline">${highlightJsonText(clipped)}</code>`;
|
|
2318
2397
|
};
|
|
2319
2398
|
const when = (epoch) => {
|
|
2320
2399
|
const date = new Date(epoch);
|
|
2321
2400
|
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
2322
2401
|
};
|
|
2402
|
+
const journalWhen = (value) => {
|
|
2403
|
+
const numeric = value.trim() === "" ? Number.NaN : Number(value);
|
|
2404
|
+
const epoch = Number.isFinite(numeric) ? numeric : Date.parse(value);
|
|
2405
|
+
return Number.isFinite(epoch) ? when(epoch) : value;
|
|
2406
|
+
};
|
|
2323
2407
|
const clockCard = (epoch) => {
|
|
2324
2408
|
const date = new Date(epoch);
|
|
2325
2409
|
if (Number.isNaN(date.getTime()))
|
|
@@ -2439,7 +2523,7 @@ function bootAdmin(config) {
|
|
|
2439
2523
|
};
|
|
2440
2524
|
const renderOverview = async () => {
|
|
2441
2525
|
const [health, shape, clock, journal] = await Promise.all([
|
|
2442
|
-
originGet(
|
|
2526
|
+
originGet(`${config.adminPrefix}/health`),
|
|
2443
2527
|
api("GET", "/state"),
|
|
2444
2528
|
api("GET", "/clock"),
|
|
2445
2529
|
api("GET", "/requests?limit=8")
|
|
@@ -2456,7 +2540,7 @@ function bootAdmin(config) {
|
|
|
2456
2540
|
},
|
|
2457
2541
|
{
|
|
2458
2542
|
k: "Status",
|
|
2459
|
-
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
2543
|
+
v: `<span class="badge ok">${esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")}</span>`
|
|
2460
2544
|
},
|
|
2461
2545
|
{ k: "Namespace", v: esc(state.namespace) },
|
|
2462
2546
|
{ k: "Collections", v: String(decoded.collections.length) },
|
|
@@ -2543,26 +2627,46 @@ function bootAdmin(config) {
|
|
|
2543
2627
|
const renderJournal = async () => {
|
|
2544
2628
|
const journal = await api("GET", "/requests?limit=100");
|
|
2545
2629
|
el("journal", HTMLElement).innerHTML = table([
|
|
2546
|
-
{ label: "When", cell: (row) => esc(row.at) },
|
|
2547
|
-
{
|
|
2630
|
+
{ label: "When", cell: (row) => esc(journalWhen(row.at)) },
|
|
2631
|
+
{
|
|
2632
|
+
label: "Method",
|
|
2633
|
+
cell: (row) => `<span class="method">${esc(row.method)}</span>`
|
|
2634
|
+
},
|
|
2548
2635
|
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2549
|
-
{
|
|
2636
|
+
{
|
|
2637
|
+
label: "Status",
|
|
2638
|
+
cell: (row) => `<span class="badge ${row.status.startsWith("2") ? "ok" : row.status.startsWith("4") || row.status.startsWith("5") ? "warn" : ""}">${esc(row.status)}</span>`
|
|
2639
|
+
},
|
|
2550
2640
|
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
2551
2641
|
], decodeJournal(journal));
|
|
2552
2642
|
};
|
|
2553
|
-
const
|
|
2554
|
-
const
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2643
|
+
const paintRoutes = () => {
|
|
2644
|
+
const query = el("route-search", HTMLInputElement).value.trim().toLocaleLowerCase();
|
|
2645
|
+
const routes = state.routes.filter((route) => route.toLocaleLowerCase().includes(query));
|
|
2646
|
+
el("route-count", HTMLElement).textContent = `${routes.length} of ${state.routes.length}`;
|
|
2647
|
+
if (state.routes.length === 0) {
|
|
2648
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No admin routes are available.</p>';
|
|
2649
|
+
return;
|
|
2650
|
+
}
|
|
2651
|
+
if (routes.length === 0) {
|
|
2652
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No routes match this filter.</p>';
|
|
2653
|
+
return;
|
|
2654
|
+
}
|
|
2558
2655
|
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2559
2656
|
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2560
2657
|
const parts = route.split(" ");
|
|
2561
2658
|
const method = parts[0] ?? "";
|
|
2562
2659
|
const path = parts.slice(1).join(" ");
|
|
2563
|
-
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>`;
|
|
2660
|
+
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>`;
|
|
2564
2661
|
}).join("");
|
|
2565
2662
|
};
|
|
2663
|
+
const renderRoutes = async () => {
|
|
2664
|
+
const data = await api("GET", "/");
|
|
2665
|
+
if (!isRecord2(data))
|
|
2666
|
+
throw new Error("Route list was not an object");
|
|
2667
|
+
state.routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
2668
|
+
paintRoutes();
|
|
2669
|
+
};
|
|
2566
2670
|
const renderers = {
|
|
2567
2671
|
overview: renderOverview,
|
|
2568
2672
|
state: renderState,
|
|
@@ -2571,19 +2675,50 @@ function bootAdmin(config) {
|
|
|
2571
2675
|
journal: renderJournal,
|
|
2572
2676
|
routes: renderRoutes
|
|
2573
2677
|
};
|
|
2678
|
+
let refreshToken = 0;
|
|
2574
2679
|
async function refresh() {
|
|
2680
|
+
const token = ++refreshToken;
|
|
2575
2681
|
clearError();
|
|
2682
|
+
const main = el("views", HTMLElement);
|
|
2683
|
+
const sync = el("sync-status", HTMLElement);
|
|
2684
|
+
main.setAttribute("aria-busy", "true");
|
|
2685
|
+
sync.classList.add("busy");
|
|
2686
|
+
sync.textContent = "Updating";
|
|
2576
2687
|
const render = renderers[state.view];
|
|
2577
|
-
|
|
2578
|
-
|
|
2688
|
+
try {
|
|
2689
|
+
if (render)
|
|
2690
|
+
await render();
|
|
2691
|
+
if (token === refreshToken)
|
|
2692
|
+
sync.textContent = "Up to date";
|
|
2693
|
+
} finally {
|
|
2694
|
+
if (token === refreshToken) {
|
|
2695
|
+
main.removeAttribute("aria-busy");
|
|
2696
|
+
sync.classList.remove("busy");
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2579
2699
|
}
|
|
2580
2700
|
const delay = (ms) => new Promise((resolve) => {
|
|
2581
2701
|
window.setTimeout(resolve, ms);
|
|
2582
2702
|
});
|
|
2583
|
-
const confirmAction = async (button, pending, done, work) => {
|
|
2703
|
+
const confirmAction = async (button, pending, done, work, confirmLabel) => {
|
|
2584
2704
|
if (button.dataset.pending === "1")
|
|
2585
2705
|
return;
|
|
2586
2706
|
const label = button.textContent ?? "";
|
|
2707
|
+
if (confirmLabel !== void 0 && button.dataset.confirming !== "1") {
|
|
2708
|
+
button.dataset.confirming = "1";
|
|
2709
|
+
button.classList.add("confirming");
|
|
2710
|
+
button.textContent = confirmLabel;
|
|
2711
|
+
window.setTimeout(() => {
|
|
2712
|
+
if (button.dataset.confirming !== "1")
|
|
2713
|
+
return;
|
|
2714
|
+
button.textContent = label;
|
|
2715
|
+
button.classList.remove("confirming");
|
|
2716
|
+
delete button.dataset.confirming;
|
|
2717
|
+
}, 3e3);
|
|
2718
|
+
return;
|
|
2719
|
+
}
|
|
2720
|
+
button.classList.remove("confirming");
|
|
2721
|
+
delete button.dataset.confirming;
|
|
2587
2722
|
const width = button.offsetWidth;
|
|
2588
2723
|
button.dataset.pending = "1";
|
|
2589
2724
|
button.classList.add("pressed");
|
|
@@ -2604,6 +2739,65 @@ function bootAdmin(config) {
|
|
|
2604
2739
|
delete button.dataset.pending;
|
|
2605
2740
|
}
|
|
2606
2741
|
};
|
|
2742
|
+
const wireJsonEditor = (textareaId, highlightId, statusId, shellId, allowEmpty) => {
|
|
2743
|
+
const textarea = el(textareaId, HTMLTextAreaElement);
|
|
2744
|
+
const highlight = el(highlightId, HTMLElement);
|
|
2745
|
+
const status = el(statusId, HTMLElement);
|
|
2746
|
+
const shell = el(shellId, HTMLElement);
|
|
2747
|
+
const sync = () => {
|
|
2748
|
+
const text = textarea.value;
|
|
2749
|
+
highlight.innerHTML = `${highlightJsonText(text)}
|
|
2750
|
+
`;
|
|
2751
|
+
let valid = true;
|
|
2752
|
+
let message = "Valid JSON";
|
|
2753
|
+
if (allowEmpty && text.trim() === "")
|
|
2754
|
+
message = "No body";
|
|
2755
|
+
else {
|
|
2756
|
+
try {
|
|
2757
|
+
readJson2(text);
|
|
2758
|
+
} catch {
|
|
2759
|
+
valid = false;
|
|
2760
|
+
message = "Invalid JSON";
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
shell.classList.toggle("invalid", !valid);
|
|
2764
|
+
status.classList.toggle("invalid", !valid);
|
|
2765
|
+
status.textContent = message;
|
|
2766
|
+
textarea.setAttribute("aria-invalid", valid ? "false" : "true");
|
|
2767
|
+
};
|
|
2768
|
+
const syncScroll = () => {
|
|
2769
|
+
const preview2 = highlight.parentElement;
|
|
2770
|
+
if (!(preview2 instanceof HTMLElement))
|
|
2771
|
+
return;
|
|
2772
|
+
preview2.scrollTop = textarea.scrollTop;
|
|
2773
|
+
preview2.scrollLeft = textarea.scrollLeft;
|
|
2774
|
+
};
|
|
2775
|
+
textarea.addEventListener("input", sync);
|
|
2776
|
+
textarea.addEventListener("scroll", syncScroll);
|
|
2777
|
+
sync();
|
|
2778
|
+
return sync;
|
|
2779
|
+
};
|
|
2780
|
+
const syncRouteEditor = wireJsonEditor("route-body", "route-highlight", "route-json-status", "route-editor", true);
|
|
2781
|
+
const syncRecordEditor = wireJsonEditor("editor-value", "editor-highlight", "editor-json-status", "record-editor", false);
|
|
2782
|
+
for (const button of document.querySelectorAll("[data-format]")) {
|
|
2783
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2784
|
+
continue;
|
|
2785
|
+
button.addEventListener("click", () => {
|
|
2786
|
+
const target = button.dataset.format;
|
|
2787
|
+
if (target === void 0)
|
|
2788
|
+
return;
|
|
2789
|
+
const textarea = document.getElementById(target);
|
|
2790
|
+
if (!(textarea instanceof HTMLTextAreaElement) || textarea.value.trim() === "")
|
|
2791
|
+
return;
|
|
2792
|
+
try {
|
|
2793
|
+
textarea.value = JSON.stringify(readJson2(textarea.value), null, 2) ?? "";
|
|
2794
|
+
textarea.dispatchEvent(new Event("input"));
|
|
2795
|
+
textarea.focus();
|
|
2796
|
+
} catch {
|
|
2797
|
+
showError(new Error("Fix the JSON before formatting it"));
|
|
2798
|
+
}
|
|
2799
|
+
});
|
|
2800
|
+
}
|
|
2607
2801
|
document.addEventListener("click", (event) => {
|
|
2608
2802
|
const target = event.target;
|
|
2609
2803
|
if (!(target instanceof Element))
|
|
@@ -2676,6 +2870,7 @@ function bootAdmin(config) {
|
|
|
2676
2870
|
} else {
|
|
2677
2871
|
editorValue.value = "{\n \n}";
|
|
2678
2872
|
}
|
|
2873
|
+
syncRecordEditor();
|
|
2679
2874
|
editor.showModal();
|
|
2680
2875
|
}
|
|
2681
2876
|
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -2705,10 +2900,10 @@ function bootAdmin(config) {
|
|
|
2705
2900
|
const id = state.editing;
|
|
2706
2901
|
if (!id)
|
|
2707
2902
|
return;
|
|
2708
|
-
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2903
|
+
confirmAction(editorDelete, "Deleting...", "Deleted", () => api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2709
2904
|
editor.close();
|
|
2710
2905
|
return renderState();
|
|
2711
|
-
}).catch(showError);
|
|
2906
|
+
}), "Confirm delete").catch(showError);
|
|
2712
2907
|
});
|
|
2713
2908
|
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2714
2909
|
if (!(button instanceof HTMLButtonElement))
|
|
@@ -2754,10 +2949,10 @@ function bootAdmin(config) {
|
|
|
2754
2949
|
}).then(() => renderFaults()).catch(showError);
|
|
2755
2950
|
});
|
|
2756
2951
|
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2757
|
-
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
|
|
2952
|
+
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults()), "Confirm clear").catch(showError);
|
|
2758
2953
|
});
|
|
2759
2954
|
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2760
|
-
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
|
|
2955
|
+
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal()), "Confirm clear").catch(showError);
|
|
2761
2956
|
});
|
|
2762
2957
|
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2763
2958
|
const target = event.target;
|
|
@@ -2771,9 +2966,15 @@ function bootAdmin(config) {
|
|
|
2771
2966
|
return;
|
|
2772
2967
|
state.route = route;
|
|
2773
2968
|
el("route-title", HTMLElement).textContent = route;
|
|
2969
|
+
el("route-send", HTMLButtonElement).disabled = false;
|
|
2774
2970
|
const method = route.split(" ")[0] ?? "";
|
|
2775
|
-
el("route-body", HTMLTextAreaElement)
|
|
2971
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
2972
|
+
routeBody.disabled = method === "GET" || method === "DELETE";
|
|
2973
|
+
el("route-editor", HTMLElement).hidden = routeBody.disabled;
|
|
2974
|
+
el("route-json-status", HTMLElement).parentElement?.toggleAttribute("hidden", routeBody.disabled);
|
|
2975
|
+
syncRouteEditor();
|
|
2776
2976
|
});
|
|
2977
|
+
el("route-search", HTMLInputElement).addEventListener("input", paintRoutes);
|
|
2777
2978
|
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2778
2979
|
event.preventDefault();
|
|
2779
2980
|
const send = async () => {
|
|
@@ -2795,7 +2996,8 @@ function bootAdmin(config) {
|
|
|
2795
2996
|
}
|
|
2796
2997
|
}
|
|
2797
2998
|
const data = await api(method, concrete, body);
|
|
2798
|
-
|
|
2999
|
+
const text = JSON.stringify(data, null, 2) ?? "null";
|
|
3000
|
+
el("route-result", HTMLElement).innerHTML = `<code>${highlightJsonText(text)}</code>`;
|
|
2799
3001
|
};
|
|
2800
3002
|
send().catch(showError);
|
|
2801
3003
|
});
|
|
@@ -2913,11 +3115,13 @@ function bootAdmin(config) {
|
|
|
2913
3115
|
views.append(section);
|
|
2914
3116
|
mountSqlExplorer(body, panelApi);
|
|
2915
3117
|
}
|
|
3118
|
+
show(document.getElementById(`view-${state.view}`) === null ? "overview" : state.view);
|
|
2916
3119
|
return refresh();
|
|
2917
3120
|
}).catch(showError);
|
|
2918
3121
|
}
|
|
2919
3122
|
var clientPrelude = [
|
|
2920
3123
|
isRecord2,
|
|
3124
|
+
highlightJsonText,
|
|
2921
3125
|
mountAdminBrand,
|
|
2922
3126
|
startAdminBrand,
|
|
2923
3127
|
faultPresetList,
|
|
@@ -2940,6 +3144,7 @@ ${entry}(${payload});
|
|
|
2940
3144
|
var defineMock = (runtime) => runtime;
|
|
2941
3145
|
var STANDARD_ADMIN_ROUTES = [
|
|
2942
3146
|
"GET /",
|
|
3147
|
+
"GET /health",
|
|
2943
3148
|
"POST /reset",
|
|
2944
3149
|
"GET /namespaces",
|
|
2945
3150
|
"GET /clock",
|
|
@@ -3019,11 +3224,11 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
3019
3224
|
}
|
|
3020
3225
|
});
|
|
3021
3226
|
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
3022
|
-
var renderAdminDocument = (service) => {
|
|
3227
|
+
var renderAdminDocument = (service, adminPrefix = ADMIN_PREFIX) => {
|
|
3023
3228
|
const name = escapeHtml(service);
|
|
3024
3229
|
const script = adminClientSource({
|
|
3025
3230
|
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
3026
|
-
adminPrefix
|
|
3231
|
+
adminPrefix,
|
|
3027
3232
|
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
3028
3233
|
brandsUrl: ADMIN_BRANDS_URL,
|
|
3029
3234
|
service
|
|
@@ -3040,75 +3245,99 @@ var renderAdminDocument = (service) => {
|
|
|
3040
3245
|
${CSS_RESET}
|
|
3041
3246
|
:root {
|
|
3042
3247
|
color-scheme: light;
|
|
3043
|
-
--bg: #
|
|
3248
|
+
--bg: #f7f8fb;
|
|
3044
3249
|
--bg-elev: #ffffff;
|
|
3045
|
-
--bg-muted: #
|
|
3046
|
-
--bg-inset: #
|
|
3047
|
-
--line: #
|
|
3048
|
-
--line-strong: #
|
|
3049
|
-
--ink: #
|
|
3050
|
-
--muted: #
|
|
3051
|
-
--faint: #
|
|
3052
|
-
--accent: #
|
|
3250
|
+
--bg-muted: #f1f3f7;
|
|
3251
|
+
--bg-inset: #f8f9fc;
|
|
3252
|
+
--line: #e4e7ec;
|
|
3253
|
+
--line-strong: #cfd4dc;
|
|
3254
|
+
--ink: #101828;
|
|
3255
|
+
--muted: #475467;
|
|
3256
|
+
--faint: #667085;
|
|
3257
|
+
--accent: #6941c6;
|
|
3258
|
+
--accent-hover: #5933b4;
|
|
3053
3259
|
--accent-ink: #ffffff;
|
|
3054
|
-
--accent-soft: #
|
|
3055
|
-
--ok: #
|
|
3056
|
-
--ok-soft: #
|
|
3057
|
-
--warn: #
|
|
3058
|
-
--warn-soft: #
|
|
3059
|
-
--err: #
|
|
3060
|
-
--err-soft: #
|
|
3061
|
-
--
|
|
3260
|
+
--accent-soft: #f4f0ff;
|
|
3261
|
+
--ok: #067647;
|
|
3262
|
+
--ok-soft: #ecfdf3;
|
|
3263
|
+
--warn: #b54708;
|
|
3264
|
+
--warn-soft: #fffaeb;
|
|
3265
|
+
--err: #b42318;
|
|
3266
|
+
--err-soft: #fef3f2;
|
|
3267
|
+
--code-key: #6941c6;
|
|
3268
|
+
--code-string: #067647;
|
|
3269
|
+
--code-number: #175cd3;
|
|
3270
|
+
--code-boolean: #c11574;
|
|
3271
|
+
--code-null: #667085;
|
|
3272
|
+
--shadow-xs: 0 1px 2px rgb(16 24 40 / 0.05);
|
|
3273
|
+
--shadow-sm: 0 1px 3px rgb(16 24 40 / 0.08), 0 1px 2px rgb(16 24 40 / 0.04);
|
|
3274
|
+
--shadow-lg: 0 18px 48px rgb(16 24 40 / 0.16), 0 4px 12px rgb(16 24 40 / 0.08);
|
|
3062
3275
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
3063
3276
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
3064
|
-
--radius:
|
|
3065
|
-
--header:
|
|
3066
|
-
--nav:
|
|
3277
|
+
--radius: 12px;
|
|
3278
|
+
--header: 72px;
|
|
3279
|
+
--nav: 224px;
|
|
3067
3280
|
}
|
|
3068
3281
|
:root[data-theme="dark"] {
|
|
3069
3282
|
color-scheme: dark;
|
|
3070
|
-
--bg: #
|
|
3071
|
-
--bg-elev: #
|
|
3072
|
-
--bg-muted: #
|
|
3073
|
-
--bg-inset: #
|
|
3074
|
-
--line: #
|
|
3075
|
-
--line-strong: #
|
|
3076
|
-
--ink: #
|
|
3077
|
-
--muted: #
|
|
3078
|
-
--faint: #
|
|
3079
|
-
--accent: #
|
|
3080
|
-
--accent-
|
|
3081
|
-
--accent-
|
|
3082
|
-
--
|
|
3083
|
-
--ok
|
|
3084
|
-
--
|
|
3085
|
-
--warn
|
|
3086
|
-
--
|
|
3087
|
-
--err
|
|
3088
|
-
--
|
|
3283
|
+
--bg: #0c0e14;
|
|
3284
|
+
--bg-elev: #151821;
|
|
3285
|
+
--bg-muted: #20232e;
|
|
3286
|
+
--bg-inset: #11141c;
|
|
3287
|
+
--line: #2b303d;
|
|
3288
|
+
--line-strong: #414858;
|
|
3289
|
+
--ink: #f5f7fa;
|
|
3290
|
+
--muted: #b6bdc9;
|
|
3291
|
+
--faint: #8d96a7;
|
|
3292
|
+
--accent: #9e77ed;
|
|
3293
|
+
--accent-hover: #b692f6;
|
|
3294
|
+
--accent-ink: #ffffff;
|
|
3295
|
+
--accent-soft: #2b2142;
|
|
3296
|
+
--ok: #75e0a7;
|
|
3297
|
+
--ok-soft: #102a20;
|
|
3298
|
+
--warn: #fec84b;
|
|
3299
|
+
--warn-soft: #302611;
|
|
3300
|
+
--err: #fda29b;
|
|
3301
|
+
--err-soft: #321817;
|
|
3302
|
+
--code-key: #c3a6ff;
|
|
3303
|
+
--code-string: #75e0a7;
|
|
3304
|
+
--code-number: #84adff;
|
|
3305
|
+
--code-boolean: #f9a8d4;
|
|
3306
|
+
--code-null: #98a2b3;
|
|
3307
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3308
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3309
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
3089
3310
|
}
|
|
3090
3311
|
@media (prefers-color-scheme: dark) {
|
|
3091
3312
|
:root:not([data-theme="light"]) {
|
|
3092
3313
|
color-scheme: dark;
|
|
3093
|
-
--bg: #
|
|
3094
|
-
--bg-elev: #
|
|
3095
|
-
--bg-muted: #
|
|
3096
|
-
--bg-inset: #
|
|
3097
|
-
--line: #
|
|
3098
|
-
--line-strong: #
|
|
3099
|
-
--ink: #
|
|
3100
|
-
--muted: #
|
|
3101
|
-
--faint: #
|
|
3102
|
-
--accent: #
|
|
3103
|
-
--accent-
|
|
3104
|
-
--accent-
|
|
3105
|
-
--
|
|
3106
|
-
--ok
|
|
3107
|
-
--
|
|
3108
|
-
--warn
|
|
3109
|
-
--
|
|
3110
|
-
--err
|
|
3111
|
-
--
|
|
3314
|
+
--bg: #0c0e14;
|
|
3315
|
+
--bg-elev: #151821;
|
|
3316
|
+
--bg-muted: #20232e;
|
|
3317
|
+
--bg-inset: #11141c;
|
|
3318
|
+
--line: #2b303d;
|
|
3319
|
+
--line-strong: #414858;
|
|
3320
|
+
--ink: #f5f7fa;
|
|
3321
|
+
--muted: #b6bdc9;
|
|
3322
|
+
--faint: #8d96a7;
|
|
3323
|
+
--accent: #9e77ed;
|
|
3324
|
+
--accent-hover: #b692f6;
|
|
3325
|
+
--accent-ink: #ffffff;
|
|
3326
|
+
--accent-soft: #2b2142;
|
|
3327
|
+
--ok: #75e0a7;
|
|
3328
|
+
--ok-soft: #102a20;
|
|
3329
|
+
--warn: #fec84b;
|
|
3330
|
+
--warn-soft: #302611;
|
|
3331
|
+
--err: #fda29b;
|
|
3332
|
+
--err-soft: #321817;
|
|
3333
|
+
--code-key: #c3a6ff;
|
|
3334
|
+
--code-string: #75e0a7;
|
|
3335
|
+
--code-number: #84adff;
|
|
3336
|
+
--code-boolean: #f9a8d4;
|
|
3337
|
+
--code-null: #98a2b3;
|
|
3338
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3339
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3340
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
3112
3341
|
}
|
|
3113
3342
|
}
|
|
3114
3343
|
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
@@ -3126,13 +3355,17 @@ var renderAdminDocument = (service) => {
|
|
|
3126
3355
|
padding-right: env(safe-area-inset-right);
|
|
3127
3356
|
-webkit-text-size-adjust: 100%;
|
|
3128
3357
|
}
|
|
3129
|
-
|
|
3358
|
+
::selection { background: color-mix(in srgb, var(--accent) 28%, transparent); }
|
|
3359
|
+
:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px; }
|
|
3360
|
+
button, input, select, textarea { transition: border-color 140ms ease, box-shadow 140ms ease, transform 100ms ease; }
|
|
3361
|
+
button:disabled, input:disabled, select:disabled, textarea:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
3130
3362
|
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
3131
3363
|
header.top {
|
|
3132
|
-
position: sticky; top: 0; z-index:
|
|
3364
|
+
position: sticky; top: 0; z-index: 20;
|
|
3133
3365
|
display: flex; align-items: center; gap: 12px;
|
|
3134
|
-
min-height: var(--header); padding:
|
|
3135
|
-
background: var(--bg-elev);
|
|
3366
|
+
min-height: var(--header); padding: 12px 20px;
|
|
3367
|
+
background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
|
|
3368
|
+
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
3136
3369
|
border-bottom: 1px solid var(--line);
|
|
3137
3370
|
min-width: 0; max-width: 100%;
|
|
3138
3371
|
}
|
|
@@ -3140,12 +3373,21 @@ var renderAdminDocument = (service) => {
|
|
|
3140
3373
|
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
3141
3374
|
min-width: 0; max-width: 100%; margin-right: auto;
|
|
3142
3375
|
}
|
|
3143
|
-
.brand-
|
|
3144
|
-
.brand-
|
|
3376
|
+
.brand-lockup { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
3377
|
+
.brand-mark {
|
|
3378
|
+
width: 38px; height: 38px; display: grid; place-items: center; flex: none;
|
|
3379
|
+
border-radius: 11px; color: #fff;
|
|
3380
|
+
background: linear-gradient(145deg, #7f56d9 0%, #53389e 100%);
|
|
3381
|
+
box-shadow: 0 6px 16px rgb(105 65 198 / 0.24), inset 0 1px 0 rgb(255 255 255 / 0.25);
|
|
3382
|
+
}
|
|
3383
|
+
.brand-mark svg { width: 22px; height: 22px; }
|
|
3384
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; line-height: 1.25; }
|
|
3385
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
3145
3386
|
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
3146
3387
|
.vendor {
|
|
3147
3388
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
3148
3389
|
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
3390
|
+
padding: 5px 8px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-inset);
|
|
3149
3391
|
}
|
|
3150
3392
|
.vendor[hidden] { display: none; }
|
|
3151
3393
|
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
@@ -3157,12 +3399,17 @@ var renderAdminDocument = (service) => {
|
|
|
3157
3399
|
.vendor-sep { color: var(--faint); }
|
|
3158
3400
|
.vendor-link { color: inherit; text-decoration: none; }
|
|
3159
3401
|
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
3160
|
-
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items:
|
|
3161
|
-
|
|
3402
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
|
|
3403
|
+
.sync-status { align-self: center; display: flex; align-items: center; gap: 6px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3404
|
+
.sync-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
|
|
3405
|
+
.sync-status.busy::before { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); animation: pulse 1.2s ease-in-out infinite; }
|
|
3406
|
+
label.field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; font-weight: 600; color: var(--faint); }
|
|
3162
3407
|
label.field input, label.field select, .controls input[type="password"] {
|
|
3163
|
-
background-color: var(--bg-
|
|
3164
|
-
padding:
|
|
3408
|
+
background-color: var(--bg-elev); border: 1px solid var(--line-strong); border-radius: 9px;
|
|
3409
|
+
padding: 8px 10px; min-height: 38px; line-height: 1.2; box-shadow: var(--shadow-xs);
|
|
3165
3410
|
}
|
|
3411
|
+
label.field input:hover, label.field select:hover, textarea:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--line-strong)); }
|
|
3412
|
+
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; }
|
|
3166
3413
|
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
3167
3414
|
the text fields in the same form. Paint it like those fields. */
|
|
3168
3415
|
label.field select {
|
|
@@ -3179,12 +3426,20 @@ var renderAdminDocument = (service) => {
|
|
|
3179
3426
|
dialog .field > textarea { width: 100%; }
|
|
3180
3427
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
3181
3428
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
.btn
|
|
3429
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
|
3430
|
+
border-radius: 9px; min-height: 38px; padding: 0 13px; border: 1px solid var(--line-strong);
|
|
3431
|
+
background: var(--bg-elev); color: var(--ink); font-size: 13px; font-weight: 600; line-height: 1;
|
|
3432
|
+
box-shadow: var(--shadow-xs);
|
|
3433
|
+
}
|
|
3434
|
+
.icon-btn { width: 38px; padding: 0; }
|
|
3435
|
+
.icon-btn svg, .btn svg, .btn-primary svg { width: 16px; height: 16px; }
|
|
3436
|
+
.icon-btn:hover, .btn:hover { background: var(--bg-muted); border-color: var(--line-strong); }
|
|
3437
|
+
.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; }
|
|
3438
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
3439
|
+
.btn-quiet { background: transparent; border-color: transparent; box-shadow: none; }
|
|
3440
|
+
.btn-quiet:hover { background: var(--bg-muted); }
|
|
3441
|
+
.btn-danger { color: var(--err); border-color: color-mix(in srgb, var(--err) 25%, var(--line)); }
|
|
3442
|
+
.btn-danger:hover, .btn-danger.confirming { background: var(--err-soft); border-color: color-mix(in srgb, var(--err) 45%, var(--line)); }
|
|
3188
3443
|
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
3189
3444
|
so the press itself has to stay visible. */
|
|
3190
3445
|
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
@@ -3200,93 +3455,159 @@ var renderAdminDocument = (service) => {
|
|
|
3200
3455
|
flex: 1 0 auto; min-width: 0;
|
|
3201
3456
|
}
|
|
3202
3457
|
nav.side {
|
|
3203
|
-
|
|
3458
|
+
position: sticky; top: var(--header); align-self: start; height: calc(100vh - var(--header));
|
|
3459
|
+
border-right: 1px solid var(--line); padding: 18px 12px;
|
|
3204
3460
|
display: flex; flex-direction: column; gap: 4px;
|
|
3205
|
-
background: var(--bg-elev);
|
|
3461
|
+
background: var(--bg-elev); overflow-y: auto;
|
|
3206
3462
|
}
|
|
3463
|
+
.nav-label { padding: 0 10px 8px; color: var(--faint); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
|
|
3207
3464
|
nav.side button {
|
|
3465
|
+
position: relative; display: flex; align-items: center; gap: 10px; width: 100%;
|
|
3208
3466
|
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
3209
|
-
padding: 9px 10px; min-height:
|
|
3210
|
-
}
|
|
3211
|
-
nav.side button
|
|
3212
|
-
|
|
3213
|
-
.
|
|
3214
|
-
.
|
|
3215
|
-
.
|
|
3467
|
+
padding: 9px 10px; min-height: 42px; color: var(--muted); font-size: 13px; font-weight: 550;
|
|
3468
|
+
}
|
|
3469
|
+
nav.side button:hover { background: var(--bg-muted); color: var(--ink); }
|
|
3470
|
+
nav.side button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 650; }
|
|
3471
|
+
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); }
|
|
3472
|
+
.nav-icon { width: 18px; height: 18px; display: grid; place-items: center; flex: none; }
|
|
3473
|
+
.nav-icon svg { width: 18px; height: 18px; }
|
|
3474
|
+
main { padding: 32px clamp(20px, 4vw, 48px) 64px; max-width: 1320px; width: 100%; min-width: 0; }
|
|
3475
|
+
main[aria-busy="true"] .view { opacity: 0.62; }
|
|
3476
|
+
.view { animation: view-in 180ms ease-out; }
|
|
3477
|
+
.view[hidden] { display: none; }
|
|
3478
|
+
.view-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
|
|
3479
|
+
.view h2 { margin: 0 0 5px; font-size: clamp(24px, 3vw, 30px); line-height: 1.2; letter-spacing: -0.035em; }
|
|
3480
|
+
.view h3 { margin: 24px 0 10px; font-size: 15px; letter-spacing: -0.01em; }
|
|
3481
|
+
.lede { margin: 0; color: var(--muted); max-width: 72ch; font-size: 14px; }
|
|
3482
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 175px), 1fr)); gap: 14px; }
|
|
3216
3483
|
.card {
|
|
3217
3484
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
3218
|
-
padding:
|
|
3485
|
+
padding: 18px; box-shadow: var(--shadow-sm);
|
|
3219
3486
|
}
|
|
3220
|
-
.card
|
|
3221
|
-
.card
|
|
3487
|
+
.stat-card { position: relative; overflow: hidden; }
|
|
3488
|
+
.stat-card::after { content: ""; position: absolute; inset: 0 0 auto; height: 2px; background: linear-gradient(90deg, var(--accent), transparent 78%); opacity: 0.7; }
|
|
3489
|
+
.card .k { color: var(--faint); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
3490
|
+
.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; }
|
|
3222
3491
|
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
3223
|
-
.
|
|
3492
|
+
.section-card { background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-xs); overflow: hidden; }
|
|
3493
|
+
.section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
|
|
3494
|
+
.section-title h3 { margin: 0; }
|
|
3495
|
+
.split { display: grid; grid-template-columns: minmax(0, 250px) minmax(0, 1fr); gap: 18px; align-items: start; }
|
|
3496
|
+
#collections { padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); box-shadow: var(--shadow-xs); }
|
|
3224
3497
|
.collection {
|
|
3225
3498
|
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
3226
|
-
border-radius:
|
|
3499
|
+
border-radius: 9px; padding: 10px 11px; min-height: 48px; color: var(--ink); cursor: pointer;
|
|
3227
3500
|
}
|
|
3228
|
-
.collection
|
|
3501
|
+
.collection:hover { background: var(--bg-muted); }
|
|
3502
|
+
.collection[aria-current="true"] { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 18%, var(--line)); color: var(--accent); }
|
|
3229
3503
|
.collection small { display: block; color: var(--faint); }
|
|
3230
3504
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
3231
|
-
.chip {
|
|
3232
|
-
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
3233
|
-
border-radius: 999px; padding: 4px 8px;
|
|
3234
|
-
}
|
|
3505
|
+
.chip { font-family: var(--mono); font-size: 11px; background: var(--bg-muted); border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; }
|
|
3235
3506
|
.chip i { color: var(--faint); font-style: normal; }
|
|
3236
3507
|
.sql-layout {
|
|
3237
3508
|
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
3238
3509
|
}
|
|
3239
|
-
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
3240
|
-
.sql-tables button { text-align: left; }
|
|
3510
|
+
.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); }
|
|
3511
|
+
.sql-tables button { text-align: left; border: 0; border-radius: 8px; padding: 9px 10px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
3512
|
+
.sql-tables button:hover, .sql-tables button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); }
|
|
3241
3513
|
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
3242
|
-
table { width: 100%; border-
|
|
3243
|
-
|
|
3244
|
-
th {
|
|
3514
|
+
.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); }
|
|
3515
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
3516
|
+
th, td { text-align: left; padding: 11px 13px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
3517
|
+
th { color: var(--faint); background: var(--bg-inset); font-weight: 650; font-size: 11px; text-transform: uppercase; letter-spacing: 0.045em; }
|
|
3518
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
3245
3519
|
tr.clickable { cursor: pointer; }
|
|
3246
|
-
tr.clickable:hover { background: var(--
|
|
3520
|
+
tr.clickable:hover { background: var(--accent-soft); }
|
|
3247
3521
|
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
3248
3522
|
pre {
|
|
3249
3523
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
3250
|
-
background: var(--bg-inset); border-radius: 10px; padding:
|
|
3251
|
-
}
|
|
3252
|
-
.
|
|
3524
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 10px; padding: 14px; font-size: 12px; line-height: 1.65;
|
|
3525
|
+
}
|
|
3526
|
+
.json-key { color: var(--code-key); }
|
|
3527
|
+
.json-string { color: var(--code-string); }
|
|
3528
|
+
.json-number { color: var(--code-number); }
|
|
3529
|
+
.json-boolean { color: var(--code-boolean); }
|
|
3530
|
+
.json-null { color: var(--code-null); font-style: italic; }
|
|
3531
|
+
.json-inline { display: block; max-width: 44rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
3532
|
+
.json-view { position: relative; max-height: 26rem; overflow: auto; }
|
|
3533
|
+
.json-editor { position: relative; min-height: 190px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--bg-inset); overflow: hidden; }
|
|
3534
|
+
.json-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); }
|
|
3535
|
+
.json-editor.invalid { border-color: var(--err); }
|
|
3536
|
+
.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; }
|
|
3537
|
+
.json-editor pre { position: absolute; inset: 0; pointer-events: none; background: transparent; }
|
|
3538
|
+
.json-editor textarea { position: relative; z-index: 1; resize: vertical; background: transparent; color: transparent; caret-color: var(--ink); -webkit-text-fill-color: transparent; }
|
|
3539
|
+
.json-editor textarea::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); }
|
|
3540
|
+
.editor-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; }
|
|
3541
|
+
.json-status { display: inline-flex; align-items: center; gap: 6px; color: var(--ok); font-size: 11px; }
|
|
3542
|
+
.json-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
3543
|
+
.json-status.invalid { color: var(--err); }
|
|
3544
|
+
.format-json { border: 0; padding: 3px 5px; background: transparent; color: var(--accent); font-size: 11px; font-weight: 650; cursor: pointer; }
|
|
3545
|
+
.row-actions { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 14px 0; }
|
|
3253
3546
|
.banner {
|
|
3254
|
-
display: none;
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3547
|
+
display: none; position: sticky; top: calc(var(--header) + 12px); z-index: 15;
|
|
3548
|
+
align-items: center; gap: 10px; margin: 0 0 16px; padding: 11px 12px 11px 14px;
|
|
3549
|
+
border: 1px solid color-mix(in srgb, var(--err) 30%, var(--line)); border-radius: 10px;
|
|
3550
|
+
background: var(--err-soft); color: var(--err); box-shadow: var(--shadow-sm); font-size: 13px;
|
|
3551
|
+
}
|
|
3552
|
+
.banner.show { display: flex; }
|
|
3553
|
+
.banner span { flex: 1; }
|
|
3554
|
+
.banner button { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 7px; background: transparent; color: currentColor; cursor: pointer; }
|
|
3555
|
+
.banner button:hover { background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
3556
|
+
.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); }
|
|
3259
3557
|
dialog {
|
|
3260
|
-
border: 1px solid var(--line); border-radius:
|
|
3261
|
-
color: var(--ink); width: min(
|
|
3262
|
-
}
|
|
3263
|
-
dialog
|
|
3264
|
-
dialog
|
|
3558
|
+
border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--bg-elev);
|
|
3559
|
+
color: var(--ink); width: min(720px, calc(100% - 24px)); max-width: calc(100vw - 24px); box-shadow: var(--shadow-lg);
|
|
3560
|
+
}
|
|
3561
|
+
dialog[open] { animation: dialog-in 160ms ease-out; }
|
|
3562
|
+
dialog::backdrop { background: rgb(12 14 20 / 0.58); backdrop-filter: blur(3px); }
|
|
3563
|
+
dialog form, .dialog-body { padding: 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
3564
|
+
.dialog-heading { display: flex; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
|
3565
|
+
.dialog-heading strong { font-size: 18px; letter-spacing: -0.02em; }
|
|
3566
|
+
.dialog-heading span { color: var(--muted); font-size: 12px; }
|
|
3265
3567
|
textarea {
|
|
3266
3568
|
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
3267
3569
|
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
3268
3570
|
font-family: var(--mono); font-size: 12px;
|
|
3269
3571
|
}
|
|
3270
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
3271
|
-
.
|
|
3572
|
+
.dialog-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding-top: 4px; }
|
|
3573
|
+
.dialog-actions .btn-danger { margin-right: auto; }
|
|
3574
|
+
.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); }
|
|
3272
3575
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
3273
3576
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
3274
3577
|
.status-2 { color: var(--ok); }
|
|
3275
3578
|
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
3276
3579
|
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
3580
|
+
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
3581
|
+
.form-grid .wide { grid-column: span 2; }
|
|
3277
3582
|
.route {
|
|
3278
|
-
display: grid; grid-template-columns:
|
|
3279
|
-
padding:
|
|
3280
|
-
}
|
|
3281
|
-
.
|
|
3583
|
+
display: grid; grid-template-columns: 84px minmax(0, 1fr) auto; gap: 10px; align-items: center;
|
|
3584
|
+
padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--bg-elev);
|
|
3585
|
+
}
|
|
3586
|
+
.route:first-child { border-radius: var(--radius) var(--radius) 0 0; }
|
|
3587
|
+
.route:last-child { border-bottom: 0; border-radius: 0 0 var(--radius) var(--radius); }
|
|
3588
|
+
.route-tools { display: flex; align-items: flex-end; gap: 12px; margin-bottom: 12px; }
|
|
3589
|
+
.route-tools .field { flex: 1; }
|
|
3590
|
+
.route-tools input { width: 100%; }
|
|
3591
|
+
.route-count { padding-bottom: 10px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3592
|
+
#route-list { max-height: 34rem; border: 1px solid var(--line); border-radius: var(--radius); overflow: auto; box-shadow: var(--shadow-xs); }
|
|
3593
|
+
.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; }
|
|
3594
|
+
@keyframes view-in { from { transform: translateY(3px); } to { transform: translateY(0); } }
|
|
3595
|
+
@keyframes dialog-in { from { transform: translateY(8px) scale(0.99); } to { transform: translateY(0) scale(1); } }
|
|
3596
|
+
@keyframes pulse { 50% { opacity: 0.45; } }
|
|
3597
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
|
|
3598
|
+
@media (forced-colors: active) { .json-editor pre { display: none; } .json-editor textarea { color: CanvasText; -webkit-text-fill-color: CanvasText; } }
|
|
3282
3599
|
@media (max-width: 800px) {
|
|
3600
|
+
:root { --header: 0px; }
|
|
3283
3601
|
header.top {
|
|
3284
|
-
align-items: stretch; flex-direction: column;
|
|
3285
|
-
padding
|
|
3602
|
+
position: relative; align-items: stretch; flex-direction: column;
|
|
3603
|
+
padding: max(12px, env(safe-area-inset-top)) 14px 12px;
|
|
3286
3604
|
}
|
|
3287
3605
|
input, select, textarea { font-size: 16px; }
|
|
3288
3606
|
.brand, .vendor, .controls { width: 100%; }
|
|
3607
|
+
.brand { gap: 10px; }
|
|
3608
|
+
.vendor { order: 2; border-radius: 9px; }
|
|
3289
3609
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
3610
|
+
.sync-status { display: none; }
|
|
3290
3611
|
/* The layout fills the screen. A single-column grid stretches every auto
|
|
3291
3612
|
row, so the tab bar grew with the leftover height and the buttons floated
|
|
3292
3613
|
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
@@ -3296,14 +3617,23 @@ var renderAdminDocument = (service) => {
|
|
|
3296
3617
|
align-content: start;
|
|
3297
3618
|
}
|
|
3298
3619
|
nav.side {
|
|
3299
|
-
align-self: start; height: auto;
|
|
3620
|
+
position: sticky; top: 0; z-index: 12; align-self: start; height: auto;
|
|
3300
3621
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
3301
3622
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
3302
|
-
padding: 8px;
|
|
3623
|
+
padding: 8px; box-shadow: var(--shadow-xs);
|
|
3303
3624
|
}
|
|
3304
|
-
nav
|
|
3625
|
+
.nav-label { display: none; }
|
|
3626
|
+
nav.side button { width: auto; white-space: nowrap; flex: none; }
|
|
3627
|
+
nav.side button[aria-current="true"]::before { inset: auto 9px -8px; width: auto; height: 3px; border-radius: 3px 3px 0 0; }
|
|
3305
3628
|
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
3306
|
-
|
|
3629
|
+
.form-grid { grid-template-columns: minmax(0, 1fr); }
|
|
3630
|
+
.form-grid .wide { grid-column: auto; }
|
|
3631
|
+
main { padding: 22px 14px 48px; }
|
|
3632
|
+
.view-head { margin-bottom: 18px; }
|
|
3633
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
3634
|
+
.card { padding: 14px; }
|
|
3635
|
+
.card .v { font-size: 20px; }
|
|
3636
|
+
.table-shell { border: 0; background: transparent; box-shadow: none; overflow: visible; }
|
|
3307
3637
|
table, thead, tbody, tr, th, td { display: block; }
|
|
3308
3638
|
thead { display: none; }
|
|
3309
3639
|
tr {
|
|
@@ -3318,50 +3648,63 @@ var renderAdminDocument = (service) => {
|
|
|
3318
3648
|
.route { grid-template-columns: 72px 1fr; }
|
|
3319
3649
|
.route button { grid-column: 1 / -1; }
|
|
3320
3650
|
}
|
|
3651
|
+
@media (max-width: 460px) {
|
|
3652
|
+
.controls label.field { min-width: calc(50% - 4px); }
|
|
3653
|
+
.controls .icon-btn, .controls .btn { flex: 1; }
|
|
3654
|
+
.grid { grid-template-columns: minmax(0, 1fr); }
|
|
3655
|
+
.dialog-actions { flex-wrap: wrap; }
|
|
3656
|
+
.dialog-actions button { flex: 1; }
|
|
3657
|
+
.dialog-actions .btn-danger { flex-basis: 100%; margin-right: 0; }
|
|
3658
|
+
}
|
|
3321
3659
|
</style>
|
|
3322
3660
|
</head>
|
|
3323
3661
|
<body>
|
|
3324
3662
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
3325
3663
|
<header class="top">
|
|
3326
3664
|
<div class="brand">
|
|
3327
|
-
<div class="brand-
|
|
3328
|
-
<
|
|
3329
|
-
|
|
3665
|
+
<div class="brand-lockup">
|
|
3666
|
+
<div class="brand-mark" aria-hidden="true">
|
|
3667
|
+
<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>
|
|
3668
|
+
</div>
|
|
3669
|
+
<div class="brand-id">
|
|
3670
|
+
<strong>${name}</strong>
|
|
3671
|
+
<span>Mock admin</span>
|
|
3672
|
+
</div>
|
|
3330
3673
|
</div>
|
|
3331
3674
|
<div class="vendor" id="vendor" hidden></div>
|
|
3332
3675
|
</div>
|
|
3333
3676
|
<div class="controls">
|
|
3677
|
+
<span class="sync-status" id="sync-status" role="status" aria-live="polite">Ready</span>
|
|
3334
3678
|
<label class="field">Namespace
|
|
3335
3679
|
<select id="namespace" aria-label="Namespace"></select>
|
|
3336
3680
|
</label>
|
|
3337
3681
|
<label class="field">Admin key
|
|
3338
3682
|
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
3339
3683
|
</label>
|
|
3340
|
-
<button class="icon-btn" id="theme" type="button" aria-label="
|
|
3341
|
-
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
3684
|
+
<button class="icon-btn" id="theme" type="button" aria-label="Use dark theme" title="Use dark theme"></button>
|
|
3685
|
+
<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>
|
|
3342
3686
|
</div>
|
|
3343
3687
|
</header>
|
|
3344
3688
|
<div class="layout">
|
|
3345
3689
|
<nav class="side" id="nav" aria-label="Admin sections">
|
|
3346
|
-
<
|
|
3347
|
-
<button type="button" data-view="
|
|
3348
|
-
<button type="button" data-view="
|
|
3349
|
-
<button type="button" data-view="
|
|
3350
|
-
<button type="button" data-view="
|
|
3351
|
-
<button type="button" data-view="
|
|
3690
|
+
<span class="nav-label">Workspace</span>
|
|
3691
|
+
<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>
|
|
3692
|
+
<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>
|
|
3693
|
+
<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>
|
|
3694
|
+
<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>
|
|
3695
|
+
<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>
|
|
3696
|
+
<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>
|
|
3352
3697
|
</nav>
|
|
3353
3698
|
<main id="views">
|
|
3354
|
-
<
|
|
3699
|
+
<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>
|
|
3355
3700
|
<section class="view" id="view-overview">
|
|
3356
|
-
<h2>Overview</h2>
|
|
3357
|
-
<p class="lede">Health, clock, and the collections this namespace is holding.</p>
|
|
3701
|
+
<div class="view-head"><div><h2>Overview</h2><p class="lede">Health, clock, and the collections this namespace is holding.</p></div></div>
|
|
3358
3702
|
<div class="grid" id="overview-cards"></div>
|
|
3359
3703
|
<h3>Recent requests</h3>
|
|
3360
3704
|
<div id="overview-requests"></div>
|
|
3361
3705
|
</section>
|
|
3362
3706
|
<section class="view" id="view-state" hidden>
|
|
3363
|
-
<h2>State</h2>
|
|
3364
|
-
<p class="lede">Every collection in this namespace. Declared fields stay visible before any row exists; stored rows add whatever else they contain.</p>
|
|
3707
|
+
<div class="view-head"><div><h2>State</h2><p class="lede">Inspect and edit every collection in this namespace.</p></div></div>
|
|
3365
3708
|
<div class="split">
|
|
3366
3709
|
<div id="collections"></div>
|
|
3367
3710
|
<div>
|
|
@@ -3374,8 +3717,7 @@ var renderAdminDocument = (service) => {
|
|
|
3374
3717
|
</div>
|
|
3375
3718
|
</section>
|
|
3376
3719
|
<section class="view" id="view-clock" hidden>
|
|
3377
|
-
<h2>Clock</h2>
|
|
3378
|
-
<p class="lede">The clock every timestamp in this mock reads.</p>
|
|
3720
|
+
<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>
|
|
3379
3721
|
<div class="card" id="clock-readout"></div>
|
|
3380
3722
|
<div class="row-actions">
|
|
3381
3723
|
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
@@ -3394,21 +3736,22 @@ var renderAdminDocument = (service) => {
|
|
|
3394
3736
|
</form>
|
|
3395
3737
|
</section>
|
|
3396
3738
|
<section class="view" id="view-faults" hidden>
|
|
3397
|
-
<h2>Faults</h2>
|
|
3398
|
-
<p class="lede">Inject a response, a delay, or a named preset. Rules apply to this namespace.</p>
|
|
3739
|
+
<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>
|
|
3399
3740
|
<form id="fault-form" class="card stack">
|
|
3400
|
-
<
|
|
3401
|
-
<
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
<
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
<
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
<
|
|
3411
|
-
|
|
3741
|
+
<div class="form-grid">
|
|
3742
|
+
<label class="field wide">Preset
|
|
3743
|
+
<select id="fault-preset"><option value="">Custom rule</option></select>
|
|
3744
|
+
</label>
|
|
3745
|
+
<label class="field">Status
|
|
3746
|
+
<input id="fault-status" inputmode="numeric" placeholder="503">
|
|
3747
|
+
</label>
|
|
3748
|
+
<label class="field wide">Path prefix
|
|
3749
|
+
<input id="fault-path" placeholder="/v1">
|
|
3750
|
+
</label>
|
|
3751
|
+
<label class="field">Method
|
|
3752
|
+
<input id="fault-method" placeholder="GET">
|
|
3753
|
+
</label>
|
|
3754
|
+
</div>
|
|
3412
3755
|
<div class="dialog-actions">
|
|
3413
3756
|
<button class="btn-primary" type="submit">Add fault</button>
|
|
3414
3757
|
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
@@ -3417,26 +3760,31 @@ var renderAdminDocument = (service) => {
|
|
|
3417
3760
|
<div id="fault-list"></div>
|
|
3418
3761
|
</section>
|
|
3419
3762
|
<section class="view" id="view-journal" hidden>
|
|
3420
|
-
<h2>Journal</h2>
|
|
3421
|
-
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
3422
|
-
<div class="row-actions">
|
|
3423
|
-
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
3424
|
-
</div>
|
|
3763
|
+
<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>
|
|
3425
3764
|
<div id="journal"></div>
|
|
3426
3765
|
</section>
|
|
3427
3766
|
<section class="view" id="view-routes" hidden>
|
|
3428
|
-
<h2>Routes</h2>
|
|
3429
|
-
<
|
|
3767
|
+
<div class="view-head"><div><h2>Routes</h2><p class="lede">Explore shared controls and service-specific admin endpoints.</p></div></div>
|
|
3768
|
+
<div class="route-tools">
|
|
3769
|
+
<label class="field">Filter routes
|
|
3770
|
+
<input id="route-search" type="search" placeholder="Method, path, or extension" autocomplete="off">
|
|
3771
|
+
</label>
|
|
3772
|
+
<span class="route-count" id="route-count"></span>
|
|
3773
|
+
</div>
|
|
3430
3774
|
<div id="route-list"></div>
|
|
3431
|
-
<form id="route-form" class="card stack">
|
|
3775
|
+
<form id="route-form" class="card stack" style="margin-top: 18px">
|
|
3432
3776
|
<strong id="route-title">Select a route</strong>
|
|
3433
3777
|
<label class="field">JSON body
|
|
3434
|
-
<
|
|
3778
|
+
<div class="json-editor" id="route-editor" hidden>
|
|
3779
|
+
<pre aria-hidden="true"><code id="route-highlight"></code></pre>
|
|
3780
|
+
<textarea id="route-body" placeholder="{ }" spellcheck="false" aria-describedby="route-json-status" disabled></textarea>
|
|
3781
|
+
</div>
|
|
3782
|
+
<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>
|
|
3435
3783
|
</label>
|
|
3436
3784
|
<div class="dialog-actions">
|
|
3437
|
-
<button class="btn-primary" type="submit">Send</button>
|
|
3785
|
+
<button class="btn-primary" id="route-send" type="submit" disabled>Send request</button>
|
|
3438
3786
|
</div>
|
|
3439
|
-
<pre id="route-result">Response will show here.</pre>
|
|
3787
|
+
<pre class="json-view" id="route-result"><code>Response will show here.</code></pre>
|
|
3440
3788
|
</form>
|
|
3441
3789
|
</section>
|
|
3442
3790
|
</main>
|
|
@@ -3444,12 +3792,16 @@ var renderAdminDocument = (service) => {
|
|
|
3444
3792
|
</div>
|
|
3445
3793
|
<dialog id="editor">
|
|
3446
3794
|
<form method="dialog" id="editor-form">
|
|
3447
|
-
<strong id="editor-title">Record</strong>
|
|
3795
|
+
<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>
|
|
3448
3796
|
<label class="field">Id
|
|
3449
3797
|
<input id="editor-id" required>
|
|
3450
3798
|
</label>
|
|
3451
3799
|
<label class="field">JSON value
|
|
3452
|
-
<
|
|
3800
|
+
<div class="json-editor" id="record-editor">
|
|
3801
|
+
<pre aria-hidden="true"><code id="editor-highlight"></code></pre>
|
|
3802
|
+
<textarea id="editor-value" spellcheck="false" aria-describedby="editor-json-status"></textarea>
|
|
3803
|
+
</div>
|
|
3804
|
+
<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>
|
|
3453
3805
|
</label>
|
|
3454
3806
|
<div class="dialog-actions">
|
|
3455
3807
|
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
@@ -3491,8 +3843,8 @@ var describeExtension = (extension) => {
|
|
|
3491
3843
|
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
3492
3844
|
};
|
|
3493
3845
|
};
|
|
3494
|
-
var adminUiRoutes = (service, ui) => {
|
|
3495
|
-
const shell = () => renderAdminDocument(service);
|
|
3846
|
+
var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
3847
|
+
const shell = () => renderAdminDocument(service, adminPrefix);
|
|
3496
3848
|
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
3497
3849
|
return {
|
|
3498
3850
|
"GET /ui": () => htmlResponse(document3()),
|
|
@@ -4183,7 +4535,7 @@ var stateAdminRoutes = (routes) => {
|
|
|
4183
4535
|
};
|
|
4184
4536
|
|
|
4185
4537
|
// ../core/dist/version.js
|
|
4186
|
-
var PACKAGE_VERSION = true ? "
|
|
4538
|
+
var PACKAGE_VERSION = true ? "2.0.0" : UNRELEASED_VERSION;
|
|
4187
4539
|
|
|
4188
4540
|
// ../core/dist/signing.js
|
|
4189
4541
|
var encoder = new TextEncoder();
|
|
@@ -4556,7 +4908,10 @@ var createWebhookHub = (options) => {
|
|
|
4556
4908
|
};
|
|
4557
4909
|
return hub;
|
|
4558
4910
|
};
|
|
4559
|
-
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4911
|
+
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4912
|
+
status,
|
|
4913
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4914
|
+
});
|
|
4560
4915
|
var adminError3 = (status, message) => json4(status, { error: { type: "mockingbird_admin", message } });
|
|
4561
4916
|
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4562
4917
|
var parseEndpoint = (value) => {
|
|
@@ -4664,7 +5019,6 @@ var AT_HEADER = "x-mockingbird-at";
|
|
|
4664
5019
|
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
4665
5020
|
var DEFAULT_NAMESPACE = "default";
|
|
4666
5021
|
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4667
|
-
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
4668
5022
|
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4669
5023
|
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
4670
5024
|
var effects = /* @__PURE__ */ new WeakMap();
|
|
@@ -4705,6 +5059,22 @@ var operationMatcher = (document3) => {
|
|
|
4705
5059
|
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
4706
5060
|
};
|
|
4707
5061
|
var createRuntime = (options) => {
|
|
5062
|
+
const adminPrefix = resolveAdminPrefix(options.adminPrefix);
|
|
5063
|
+
const internalPaths = Object.entries(options.document?.paths ?? {}).filter(([, item]) => item["x-mockingbird-internal"] === true).map(([path]) => path);
|
|
5064
|
+
for (const path of internalPaths) {
|
|
5065
|
+
if (!path.startsWith(`${ADMIN_PREFIX}/blobs/`))
|
|
5066
|
+
throw new Error(`internal transport ${path} must live under ${ADMIN_PREFIX}/blobs/`);
|
|
5067
|
+
}
|
|
5068
|
+
assertAdminPrefixAvailable(adminPrefix, Object.keys(options.document?.paths ?? {}).filter((path) => !internalPaths.includes(path)));
|
|
5069
|
+
const internalMatch = (path) => {
|
|
5070
|
+
for (const pattern of internalPaths) {
|
|
5071
|
+
const actual = path.split("/");
|
|
5072
|
+
const expected = pattern.split("/");
|
|
5073
|
+
if (actual.length === expected.length && expected.every((part, i) => part.startsWith("{") || part === actual[i]))
|
|
5074
|
+
return pattern;
|
|
5075
|
+
}
|
|
5076
|
+
return void 0;
|
|
5077
|
+
};
|
|
4708
5078
|
assertAdminUi(options.adminUi);
|
|
4709
5079
|
const sqlite = bootSqlite(options.sqlite);
|
|
4710
5080
|
const clock = options.clock ?? createClock();
|
|
@@ -4750,6 +5120,7 @@ var createRuntime = (options) => {
|
|
|
4750
5120
|
const created = options.create({
|
|
4751
5121
|
namespace: storageNamespace(key),
|
|
4752
5122
|
publicNamespace,
|
|
5123
|
+
adminPrefix,
|
|
4753
5124
|
sqlite,
|
|
4754
5125
|
clock: namespaceClock(publicNamespace),
|
|
4755
5126
|
rng: createdContextRng
|
|
@@ -4982,10 +5353,10 @@ var createRuntime = (options) => {
|
|
|
4982
5353
|
namespaceClock,
|
|
4983
5354
|
namespaceOf(request) {
|
|
4984
5355
|
const url = new URL(request.url);
|
|
4985
|
-
const prefixed =
|
|
5356
|
+
const prefixed = matchNamespacePath(url.pathname, adminPrefix);
|
|
4986
5357
|
const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
|
|
4987
5358
|
const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
|
|
4988
|
-
if (path
|
|
5359
|
+
if (isAdminPath(path, adminPrefix))
|
|
4989
5360
|
return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
|
|
4990
5361
|
if (selected !== void 0)
|
|
4991
5362
|
return selected;
|
|
@@ -5051,7 +5422,7 @@ var createRuntime = (options) => {
|
|
|
5051
5422
|
state: (namespace = DEFAULT_NAMESPACE) => inspectState(stateScope(namespace)),
|
|
5052
5423
|
fetch: async (incoming) => {
|
|
5053
5424
|
let request = incoming;
|
|
5054
|
-
const prefixed =
|
|
5425
|
+
const prefixed = matchNamespacePath(new URL(request.url).pathname, adminPrefix);
|
|
5055
5426
|
if (prefixed) {
|
|
5056
5427
|
const url2 = new URL(request.url);
|
|
5057
5428
|
url2.pathname = prefixed[2] ?? "/";
|
|
@@ -5067,7 +5438,15 @@ var createRuntime = (options) => {
|
|
|
5067
5438
|
signal: request.signal
|
|
5068
5439
|
});
|
|
5069
5440
|
}
|
|
5070
|
-
|
|
5441
|
+
const internalUrl = new URL(request.url);
|
|
5442
|
+
const internalPath = isAdminPath(internalUrl.pathname, adminPrefix) ? `${ADMIN_PREFIX}${internalUrl.pathname.slice(adminPrefix.length)}` : "";
|
|
5443
|
+
const internal = internalMatch(internalPath);
|
|
5444
|
+
const obsoleteInternalPath = adminPrefix !== ADMIN_PREFIX && internalMatch(internalUrl.pathname) && !internal;
|
|
5445
|
+
if (internal) {
|
|
5446
|
+
internalUrl.pathname = internalPath;
|
|
5447
|
+
request = new Request(internalUrl, request);
|
|
5448
|
+
}
|
|
5449
|
+
let namespace = internal ? internalUrl.searchParams.get("namespace") ?? control.namespaceOf(request) : control.namespaceOf(request);
|
|
5071
5450
|
if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
5072
5451
|
const credential = options.credential(request);
|
|
5073
5452
|
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
@@ -5087,7 +5466,9 @@ var createRuntime = (options) => {
|
|
|
5087
5466
|
return copy;
|
|
5088
5467
|
}
|
|
5089
5468
|
};
|
|
5090
|
-
|
|
5469
|
+
if (obsoleteInternalPath)
|
|
5470
|
+
return stamp(adminFail(404, "internal transport is only available under adminPrefix"));
|
|
5471
|
+
const handled = internal ? void 0 : await control.handle(request);
|
|
5091
5472
|
if (handled)
|
|
5092
5473
|
return stamp(handled);
|
|
5093
5474
|
const started = monotonicNow();
|
|
@@ -5128,7 +5509,7 @@ var createRuntime = (options) => {
|
|
|
5128
5509
|
type: "mockingbird_admin",
|
|
5129
5510
|
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
5130
5511
|
}
|
|
5131
|
-
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
5512
|
+
}), { status: 400, headers: { "content-type": "application/json; charset=utf-8" } }));
|
|
5132
5513
|
}
|
|
5133
5514
|
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
5134
5515
|
log(400);
|
|
@@ -5245,15 +5626,16 @@ var createRuntime = (options) => {
|
|
|
5245
5626
|
...options.admin?.(runtime) ?? {},
|
|
5246
5627
|
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
5247
5628
|
// maps `presets`, and a service route must not replace that list with a record.
|
|
5248
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
5629
|
+
...options.presets ? presetRoutes(options.presets, runtime, adminPrefix) : {},
|
|
5249
5630
|
...stateAdminRoutes({
|
|
5250
5631
|
open: stateScope,
|
|
5251
5632
|
afterWrite: (namespace) => {
|
|
5252
5633
|
checkpoint(namespace, "main");
|
|
5253
5634
|
}
|
|
5254
5635
|
}),
|
|
5255
|
-
...adminUiRoutes(options.name, options.adminUi)
|
|
5636
|
+
...adminUiRoutes(options.name, options.adminUi, adminPrefix)
|
|
5256
5637
|
},
|
|
5638
|
+
adminPrefix,
|
|
5257
5639
|
adminKey: options.adminKey
|
|
5258
5640
|
});
|
|
5259
5641
|
return defineMock(runtime);
|
|
@@ -5267,7 +5649,10 @@ var mutableResponse = (response) => {
|
|
|
5267
5649
|
return new Response(response.body, response);
|
|
5268
5650
|
}
|
|
5269
5651
|
};
|
|
5270
|
-
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5652
|
+
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5653
|
+
status,
|
|
5654
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
5655
|
+
});
|
|
5271
5656
|
var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
|
|
5272
5657
|
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5273
5658
|
var credentialRoutes = (registry) => ({
|
|
@@ -5319,7 +5704,7 @@ var credentialRoutes = (registry) => ({
|
|
|
5319
5704
|
return adminJson(200, { status: "ok" });
|
|
5320
5705
|
}
|
|
5321
5706
|
});
|
|
5322
|
-
var presetRoutes = (presets, runtime) => ({
|
|
5707
|
+
var presetRoutes = (presets, runtime, adminPrefix) => ({
|
|
5323
5708
|
"GET /faults/presets": () => {
|
|
5324
5709
|
const body = {
|
|
5325
5710
|
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
@@ -5329,7 +5714,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
5329
5714
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
5330
5715
|
const name = params.name;
|
|
5331
5716
|
if (!presets[name])
|
|
5332
|
-
return adminFail(404, `no fault preset ${name}; GET /
|
|
5717
|
+
return adminFail(404, `no fault preset ${name}; GET ${adminPrefix}/faults/presets`);
|
|
5333
5718
|
const overrides = isObject(body) ? body : {};
|
|
5334
5719
|
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
5335
5720
|
}
|
|
@@ -6509,15 +6894,17 @@ var createRuntime2 = (options = {}) => {
|
|
|
6509
6894
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
6510
6895
|
...options.clock ? { clock: options.clock } : {},
|
|
6511
6896
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
6897
|
+
...options.adminPrefix !== void 0 ? { adminPrefix: options.adminPrefix } : {},
|
|
6512
6898
|
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
6513
6899
|
...options.onLog ? { onLog: options.onLog } : {},
|
|
6514
6900
|
credential: bearerToken,
|
|
6515
6901
|
presets: FLEX_PRESETS,
|
|
6516
6902
|
webhooks: hub,
|
|
6517
|
-
create: ({ sqlite, namespace, publicNamespace, clock }) => new FlexAPI({
|
|
6903
|
+
create: ({ sqlite, namespace, publicNamespace, adminPrefix, clock }) => new FlexAPI({
|
|
6518
6904
|
sqlite,
|
|
6519
6905
|
namespace,
|
|
6520
6906
|
publicNamespace,
|
|
6907
|
+
adminPrefix,
|
|
6521
6908
|
now: clock.now,
|
|
6522
6909
|
...options.products ? { products: options.products } : {},
|
|
6523
6910
|
...options.settings ? { settings: options.settings } : {},
|
|
@@ -6618,12 +7005,14 @@ var FlexAPI = class {
|
|
|
6618
7005
|
service;
|
|
6619
7006
|
idempotency;
|
|
6620
7007
|
now;
|
|
7008
|
+
adminPrefix;
|
|
6621
7009
|
publicNamespace;
|
|
6622
7010
|
onEvent;
|
|
6623
7011
|
constructor(options = {}) {
|
|
6624
7012
|
const sqlite = bootSqlite(options.sqlite);
|
|
6625
7013
|
const namespace = options.namespace ?? FLEX_NAMESPACE;
|
|
6626
7014
|
this.now = options.now ?? (() => Date.now());
|
|
7015
|
+
this.adminPrefix = options.adminPrefix ?? "/__admin";
|
|
6627
7016
|
this.publicNamespace = options.publicNamespace ?? "default";
|
|
6628
7017
|
this.onEvent = options.onEvent;
|
|
6629
7018
|
this.state = new FlexState(sqlite, namespace, {
|
|
@@ -6792,7 +7181,7 @@ var FlexAPI = class {
|
|
|
6792
7181
|
pageBase(context) {
|
|
6793
7182
|
const configured = this.state.current().publicUrl;
|
|
6794
7183
|
if (configured) return configured.replace(/\/$/, "");
|
|
6795
|
-
const prefix = this.publicNamespace === "default" ? "" :
|
|
7184
|
+
const prefix = this.publicNamespace === "default" ? "" : `${this.adminPrefix}/ns/${encodeURIComponent(this.publicNamespace)}`;
|
|
6796
7185
|
return `${context.url.origin}${prefix}`;
|
|
6797
7186
|
}
|
|
6798
7187
|
async createSession(context) {
|
|
@@ -7418,6 +7807,7 @@ var isNextActionType = (value) => typeof value === "string" && NEXT_ACTION_TYPES
|
|
|
7418
7807
|
|
|
7419
7808
|
export {
|
|
7420
7809
|
createClock,
|
|
7810
|
+
resolveAdminPrefix,
|
|
7421
7811
|
parseDuration,
|
|
7422
7812
|
document2 as document,
|
|
7423
7813
|
operationIds,
|
|
@@ -7441,4 +7831,4 @@ export {
|
|
|
7441
7831
|
FlexAPI,
|
|
7442
7832
|
isNextActionType
|
|
7443
7833
|
};
|
|
7444
|
-
//# sourceMappingURL=chunk-
|
|
7834
|
+
//# sourceMappingURL=chunk-E3W2D3EY.js.map
|