@crvouga/mockingbird-service-intercom 1.9.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -1
- package/DISCOVERY.md +55 -0
- package/README.md +1 -1
- package/SUPPORT.md +22 -0
- package/dist/{chunk-PB2BRO3M.js → chunk-DHV6Y6ZE.js} +626 -236
- package/dist/chunk-DHV6Y6ZE.js.map +7 -0
- package/dist/{chunk-6ZFVOGHR.js → chunk-DN7TMKH7.js} +44 -24
- package/dist/chunk-DN7TMKH7.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 +1078 -0
- package/package.json +29 -4
- package/dist/chunk-6ZFVOGHR.js.map +0 -7
- package/dist/chunk-PB2BRO3M.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
|
};
|
|
@@ -1236,7 +1275,10 @@ var OutboxStore = class {
|
|
|
1236
1275
|
return query.limit !== void 0 ? matched.slice(-Math.max(0, query.limit)) : matched;
|
|
1237
1276
|
}
|
|
1238
1277
|
};
|
|
1239
|
-
var json2 = (status, body) => new Response(JSON.stringify(body), {
|
|
1278
|
+
var json2 = (status, body) => new Response(JSON.stringify(body), {
|
|
1279
|
+
status,
|
|
1280
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
1281
|
+
});
|
|
1240
1282
|
var parseSince = (value) => {
|
|
1241
1283
|
if (value === null)
|
|
1242
1284
|
return void 0;
|
|
@@ -1872,6 +1914,34 @@ dialog:not([open]) {
|
|
|
1872
1914
|
function isRecord2(value) {
|
|
1873
1915
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1874
1916
|
}
|
|
1917
|
+
function highlightJsonText(text2) {
|
|
1918
|
+
const escapeToken = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
1919
|
+
switch (char) {
|
|
1920
|
+
case "&":
|
|
1921
|
+
return "&";
|
|
1922
|
+
case "<":
|
|
1923
|
+
return "<";
|
|
1924
|
+
case ">":
|
|
1925
|
+
return ">";
|
|
1926
|
+
case '"':
|
|
1927
|
+
return """;
|
|
1928
|
+
default:
|
|
1929
|
+
return "'";
|
|
1930
|
+
}
|
|
1931
|
+
});
|
|
1932
|
+
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;
|
|
1933
|
+
let cursor = 0;
|
|
1934
|
+
let html = "";
|
|
1935
|
+
for (const match of text2.matchAll(token)) {
|
|
1936
|
+
const index = match.index ?? 0;
|
|
1937
|
+
const value = match[0];
|
|
1938
|
+
html += escapeToken(text2.slice(cursor, index));
|
|
1939
|
+
const kind = value.startsWith('"') ? value.trimEnd().endsWith(":") ? "key" : "string" : value === "true" || value === "false" ? "boolean" : value === "null" ? "null" : "number";
|
|
1940
|
+
html += `<span class="json-${kind}">${escapeToken(value)}</span>`;
|
|
1941
|
+
cursor = index + value.length;
|
|
1942
|
+
}
|
|
1943
|
+
return html + escapeToken(text2.slice(cursor));
|
|
1944
|
+
}
|
|
1875
1945
|
function mountAdminBrand(doc, host, service, catalog) {
|
|
1876
1946
|
const clip = (value, max) => {
|
|
1877
1947
|
if (typeof value !== "string")
|
|
@@ -2233,14 +2303,16 @@ function bootAdmin(config) {
|
|
|
2233
2303
|
const state = {
|
|
2234
2304
|
namespace: "default",
|
|
2235
2305
|
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
2236
|
-
view: "overview",
|
|
2306
|
+
view: ["overview", "state", "clock", "faults", "journal", "routes"].includes(location.hash.slice(1)) ? location.hash.slice(1) : "overview",
|
|
2237
2307
|
collection: "",
|
|
2238
2308
|
shape: null,
|
|
2239
2309
|
route: null,
|
|
2310
|
+
routes: [],
|
|
2240
2311
|
editing: null
|
|
2241
2312
|
};
|
|
2242
2313
|
const standard = new Set(config.standardRoutes);
|
|
2243
2314
|
const banner = el("banner", HTMLElement);
|
|
2315
|
+
const bannerMessage = el("banner-message", HTMLElement);
|
|
2244
2316
|
const keyInput = el("admin-key", HTMLInputElement);
|
|
2245
2317
|
keyInput.value = state.key;
|
|
2246
2318
|
const params = new URLSearchParams(location.search);
|
|
@@ -2251,18 +2323,21 @@ function bootAdmin(config) {
|
|
|
2251
2323
|
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
2252
2324
|
params.delete("key");
|
|
2253
2325
|
const next = params.toString();
|
|
2254
|
-
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
2326
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : "") + location.hash);
|
|
2255
2327
|
}
|
|
2256
2328
|
const queryNamespace = params.get("namespace");
|
|
2257
2329
|
if (queryNamespace)
|
|
2258
2330
|
state.namespace = queryNamespace;
|
|
2259
2331
|
const showError = (error) => {
|
|
2260
|
-
|
|
2332
|
+
bannerMessage.textContent = error instanceof Error ? error.message : String(error);
|
|
2261
2333
|
banner.classList.add("show");
|
|
2334
|
+
banner.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
2262
2335
|
};
|
|
2263
2336
|
const clearError = () => {
|
|
2264
2337
|
banner.classList.remove("show");
|
|
2338
|
+
bannerMessage.textContent = "";
|
|
2265
2339
|
};
|
|
2340
|
+
el("banner-dismiss", HTMLButtonElement).addEventListener("click", clearError);
|
|
2266
2341
|
const api = async (method, path, body) => {
|
|
2267
2342
|
const headers = { accept: "application/json" };
|
|
2268
2343
|
if (state.key !== "")
|
|
@@ -2306,16 +2381,21 @@ function bootAdmin(config) {
|
|
|
2306
2381
|
namespace: () => state.namespace
|
|
2307
2382
|
};
|
|
2308
2383
|
const isPanelScript = (value) => typeof value === "function";
|
|
2384
|
+
const themeButton = el("theme", HTMLButtonElement);
|
|
2309
2385
|
const setTheme = (theme) => {
|
|
2310
2386
|
if (theme)
|
|
2311
2387
|
document.documentElement.dataset.theme = theme;
|
|
2312
2388
|
else
|
|
2313
2389
|
delete document.documentElement.dataset.theme;
|
|
2390
|
+
const dark = theme ? theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2391
|
+
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>';
|
|
2392
|
+
const label = dark ? "Use light theme" : "Use dark theme";
|
|
2393
|
+
themeButton.setAttribute("aria-label", label);
|
|
2394
|
+
themeButton.title = label;
|
|
2314
2395
|
};
|
|
2315
2396
|
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
2397
|
+
setTheme(storedTheme === "dark" || storedTheme === "light" ? storedTheme : null);
|
|
2398
|
+
themeButton.addEventListener("click", () => {
|
|
2319
2399
|
const current = document.documentElement.dataset.theme;
|
|
2320
2400
|
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2321
2401
|
const next = dark ? "light" : "dark";
|
|
@@ -2334,6 +2414,7 @@ function bootAdmin(config) {
|
|
|
2334
2414
|
continue;
|
|
2335
2415
|
section.hidden = section.id !== `view-${view}`;
|
|
2336
2416
|
}
|
|
2417
|
+
history.replaceState(null, "", `${location.pathname}${location.search}#${encodeURIComponent(view)}`);
|
|
2337
2418
|
};
|
|
2338
2419
|
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
2339
2420
|
const target = event.target;
|
|
@@ -2348,7 +2429,7 @@ function bootAdmin(config) {
|
|
|
2348
2429
|
show(view);
|
|
2349
2430
|
refresh().catch(showError);
|
|
2350
2431
|
});
|
|
2351
|
-
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>`;
|
|
2432
|
+
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>`;
|
|
2352
2433
|
const table = (columns, rows) => {
|
|
2353
2434
|
if (rows.length === 0)
|
|
2354
2435
|
return '<p class="empty">Nothing here yet.</p>';
|
|
@@ -2358,16 +2439,22 @@ function bootAdmin(config) {
|
|
|
2358
2439
|
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
2359
2440
|
return `<tr${attrs}>${cells}</tr>`;
|
|
2360
2441
|
}).join("");
|
|
2361
|
-
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
2442
|
+
return `<div class="table-shell"><table><thead>${head}</thead><tbody>${body}</tbody></table></div>`;
|
|
2362
2443
|
};
|
|
2363
2444
|
const preview = (value) => {
|
|
2364
2445
|
const text2 = JSON.stringify(value) ?? "null";
|
|
2365
|
-
|
|
2446
|
+
const clipped = text2.length > 180 ? `${text2.slice(0, 180)}\u2026` : text2;
|
|
2447
|
+
return `<code class="json-inline">${highlightJsonText(clipped)}</code>`;
|
|
2366
2448
|
};
|
|
2367
2449
|
const when = (epoch) => {
|
|
2368
2450
|
const date = new Date(epoch);
|
|
2369
2451
|
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
2370
2452
|
};
|
|
2453
|
+
const journalWhen = (value) => {
|
|
2454
|
+
const numeric = value.trim() === "" ? Number.NaN : Number(value);
|
|
2455
|
+
const epoch = Number.isFinite(numeric) ? numeric : Date.parse(value);
|
|
2456
|
+
return Number.isFinite(epoch) ? when(epoch) : value;
|
|
2457
|
+
};
|
|
2371
2458
|
const clockCard = (epoch) => {
|
|
2372
2459
|
const date = new Date(epoch);
|
|
2373
2460
|
if (Number.isNaN(date.getTime()))
|
|
@@ -2487,7 +2574,7 @@ function bootAdmin(config) {
|
|
|
2487
2574
|
};
|
|
2488
2575
|
const renderOverview = async () => {
|
|
2489
2576
|
const [health, shape, clock, journal] = await Promise.all([
|
|
2490
|
-
originGet(
|
|
2577
|
+
originGet(`${config.adminPrefix}/health`),
|
|
2491
2578
|
api("GET", "/state"),
|
|
2492
2579
|
api("GET", "/clock"),
|
|
2493
2580
|
api("GET", "/requests?limit=8")
|
|
@@ -2504,7 +2591,7 @@ function bootAdmin(config) {
|
|
|
2504
2591
|
},
|
|
2505
2592
|
{
|
|
2506
2593
|
k: "Status",
|
|
2507
|
-
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
2594
|
+
v: `<span class="badge ok">${esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")}</span>`
|
|
2508
2595
|
},
|
|
2509
2596
|
{ k: "Namespace", v: esc(state.namespace) },
|
|
2510
2597
|
{ k: "Collections", v: String(decoded.collections.length) },
|
|
@@ -2591,26 +2678,46 @@ function bootAdmin(config) {
|
|
|
2591
2678
|
const renderJournal = async () => {
|
|
2592
2679
|
const journal = await api("GET", "/requests?limit=100");
|
|
2593
2680
|
el("journal", HTMLElement).innerHTML = table([
|
|
2594
|
-
{ label: "When", cell: (row) => esc(row.at) },
|
|
2595
|
-
{
|
|
2681
|
+
{ label: "When", cell: (row) => esc(journalWhen(row.at)) },
|
|
2682
|
+
{
|
|
2683
|
+
label: "Method",
|
|
2684
|
+
cell: (row) => `<span class="method">${esc(row.method)}</span>`
|
|
2685
|
+
},
|
|
2596
2686
|
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2597
|
-
{
|
|
2687
|
+
{
|
|
2688
|
+
label: "Status",
|
|
2689
|
+
cell: (row) => `<span class="badge ${row.status.startsWith("2") ? "ok" : row.status.startsWith("4") || row.status.startsWith("5") ? "warn" : ""}">${esc(row.status)}</span>`
|
|
2690
|
+
},
|
|
2598
2691
|
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
2599
2692
|
], decodeJournal(journal));
|
|
2600
2693
|
};
|
|
2601
|
-
const
|
|
2602
|
-
const
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2694
|
+
const paintRoutes = () => {
|
|
2695
|
+
const query = el("route-search", HTMLInputElement).value.trim().toLocaleLowerCase();
|
|
2696
|
+
const routes = state.routes.filter((route) => route.toLocaleLowerCase().includes(query));
|
|
2697
|
+
el("route-count", HTMLElement).textContent = `${routes.length} of ${state.routes.length}`;
|
|
2698
|
+
if (state.routes.length === 0) {
|
|
2699
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No admin routes are available.</p>';
|
|
2700
|
+
return;
|
|
2701
|
+
}
|
|
2702
|
+
if (routes.length === 0) {
|
|
2703
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No routes match this filter.</p>';
|
|
2704
|
+
return;
|
|
2705
|
+
}
|
|
2606
2706
|
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2607
2707
|
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2608
2708
|
const parts = route.split(" ");
|
|
2609
2709
|
const method = parts[0] ?? "";
|
|
2610
2710
|
const path = parts.slice(1).join(" ");
|
|
2611
|
-
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>`;
|
|
2711
|
+
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>`;
|
|
2612
2712
|
}).join("");
|
|
2613
2713
|
};
|
|
2714
|
+
const renderRoutes = async () => {
|
|
2715
|
+
const data = await api("GET", "/");
|
|
2716
|
+
if (!isRecord2(data))
|
|
2717
|
+
throw new Error("Route list was not an object");
|
|
2718
|
+
state.routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
2719
|
+
paintRoutes();
|
|
2720
|
+
};
|
|
2614
2721
|
const renderers = {
|
|
2615
2722
|
overview: renderOverview,
|
|
2616
2723
|
state: renderState,
|
|
@@ -2619,19 +2726,50 @@ function bootAdmin(config) {
|
|
|
2619
2726
|
journal: renderJournal,
|
|
2620
2727
|
routes: renderRoutes
|
|
2621
2728
|
};
|
|
2729
|
+
let refreshToken = 0;
|
|
2622
2730
|
async function refresh() {
|
|
2731
|
+
const token = ++refreshToken;
|
|
2623
2732
|
clearError();
|
|
2733
|
+
const main = el("views", HTMLElement);
|
|
2734
|
+
const sync = el("sync-status", HTMLElement);
|
|
2735
|
+
main.setAttribute("aria-busy", "true");
|
|
2736
|
+
sync.classList.add("busy");
|
|
2737
|
+
sync.textContent = "Updating";
|
|
2624
2738
|
const render = renderers[state.view];
|
|
2625
|
-
|
|
2626
|
-
|
|
2739
|
+
try {
|
|
2740
|
+
if (render)
|
|
2741
|
+
await render();
|
|
2742
|
+
if (token === refreshToken)
|
|
2743
|
+
sync.textContent = "Up to date";
|
|
2744
|
+
} finally {
|
|
2745
|
+
if (token === refreshToken) {
|
|
2746
|
+
main.removeAttribute("aria-busy");
|
|
2747
|
+
sync.classList.remove("busy");
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2627
2750
|
}
|
|
2628
2751
|
const delay = (ms) => new Promise((resolve) => {
|
|
2629
2752
|
window.setTimeout(resolve, ms);
|
|
2630
2753
|
});
|
|
2631
|
-
const confirmAction = async (button, pending, done, work) => {
|
|
2754
|
+
const confirmAction = async (button, pending, done, work, confirmLabel) => {
|
|
2632
2755
|
if (button.dataset.pending === "1")
|
|
2633
2756
|
return;
|
|
2634
2757
|
const label = button.textContent ?? "";
|
|
2758
|
+
if (confirmLabel !== void 0 && button.dataset.confirming !== "1") {
|
|
2759
|
+
button.dataset.confirming = "1";
|
|
2760
|
+
button.classList.add("confirming");
|
|
2761
|
+
button.textContent = confirmLabel;
|
|
2762
|
+
window.setTimeout(() => {
|
|
2763
|
+
if (button.dataset.confirming !== "1")
|
|
2764
|
+
return;
|
|
2765
|
+
button.textContent = label;
|
|
2766
|
+
button.classList.remove("confirming");
|
|
2767
|
+
delete button.dataset.confirming;
|
|
2768
|
+
}, 3e3);
|
|
2769
|
+
return;
|
|
2770
|
+
}
|
|
2771
|
+
button.classList.remove("confirming");
|
|
2772
|
+
delete button.dataset.confirming;
|
|
2635
2773
|
const width = button.offsetWidth;
|
|
2636
2774
|
button.dataset.pending = "1";
|
|
2637
2775
|
button.classList.add("pressed");
|
|
@@ -2652,6 +2790,65 @@ function bootAdmin(config) {
|
|
|
2652
2790
|
delete button.dataset.pending;
|
|
2653
2791
|
}
|
|
2654
2792
|
};
|
|
2793
|
+
const wireJsonEditor = (textareaId, highlightId, statusId, shellId, allowEmpty) => {
|
|
2794
|
+
const textarea = el(textareaId, HTMLTextAreaElement);
|
|
2795
|
+
const highlight = el(highlightId, HTMLElement);
|
|
2796
|
+
const status = el(statusId, HTMLElement);
|
|
2797
|
+
const shell = el(shellId, HTMLElement);
|
|
2798
|
+
const sync = () => {
|
|
2799
|
+
const text2 = textarea.value;
|
|
2800
|
+
highlight.innerHTML = `${highlightJsonText(text2)}
|
|
2801
|
+
`;
|
|
2802
|
+
let valid = true;
|
|
2803
|
+
let message = "Valid JSON";
|
|
2804
|
+
if (allowEmpty && text2.trim() === "")
|
|
2805
|
+
message = "No body";
|
|
2806
|
+
else {
|
|
2807
|
+
try {
|
|
2808
|
+
readJson2(text2);
|
|
2809
|
+
} catch {
|
|
2810
|
+
valid = false;
|
|
2811
|
+
message = "Invalid JSON";
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
shell.classList.toggle("invalid", !valid);
|
|
2815
|
+
status.classList.toggle("invalid", !valid);
|
|
2816
|
+
status.textContent = message;
|
|
2817
|
+
textarea.setAttribute("aria-invalid", valid ? "false" : "true");
|
|
2818
|
+
};
|
|
2819
|
+
const syncScroll = () => {
|
|
2820
|
+
const preview2 = highlight.parentElement;
|
|
2821
|
+
if (!(preview2 instanceof HTMLElement))
|
|
2822
|
+
return;
|
|
2823
|
+
preview2.scrollTop = textarea.scrollTop;
|
|
2824
|
+
preview2.scrollLeft = textarea.scrollLeft;
|
|
2825
|
+
};
|
|
2826
|
+
textarea.addEventListener("input", sync);
|
|
2827
|
+
textarea.addEventListener("scroll", syncScroll);
|
|
2828
|
+
sync();
|
|
2829
|
+
return sync;
|
|
2830
|
+
};
|
|
2831
|
+
const syncRouteEditor = wireJsonEditor("route-body", "route-highlight", "route-json-status", "route-editor", true);
|
|
2832
|
+
const syncRecordEditor = wireJsonEditor("editor-value", "editor-highlight", "editor-json-status", "record-editor", false);
|
|
2833
|
+
for (const button of document.querySelectorAll("[data-format]")) {
|
|
2834
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2835
|
+
continue;
|
|
2836
|
+
button.addEventListener("click", () => {
|
|
2837
|
+
const target = button.dataset.format;
|
|
2838
|
+
if (target === void 0)
|
|
2839
|
+
return;
|
|
2840
|
+
const textarea = document.getElementById(target);
|
|
2841
|
+
if (!(textarea instanceof HTMLTextAreaElement) || textarea.value.trim() === "")
|
|
2842
|
+
return;
|
|
2843
|
+
try {
|
|
2844
|
+
textarea.value = JSON.stringify(readJson2(textarea.value), null, 2) ?? "";
|
|
2845
|
+
textarea.dispatchEvent(new Event("input"));
|
|
2846
|
+
textarea.focus();
|
|
2847
|
+
} catch {
|
|
2848
|
+
showError(new Error("Fix the JSON before formatting it"));
|
|
2849
|
+
}
|
|
2850
|
+
});
|
|
2851
|
+
}
|
|
2655
2852
|
document.addEventListener("click", (event) => {
|
|
2656
2853
|
const target = event.target;
|
|
2657
2854
|
if (!(target instanceof Element))
|
|
@@ -2724,6 +2921,7 @@ function bootAdmin(config) {
|
|
|
2724
2921
|
} else {
|
|
2725
2922
|
editorValue.value = "{\n \n}";
|
|
2726
2923
|
}
|
|
2924
|
+
syncRecordEditor();
|
|
2727
2925
|
editor.showModal();
|
|
2728
2926
|
}
|
|
2729
2927
|
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -2753,10 +2951,10 @@ function bootAdmin(config) {
|
|
|
2753
2951
|
const id = state.editing;
|
|
2754
2952
|
if (!id)
|
|
2755
2953
|
return;
|
|
2756
|
-
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2954
|
+
confirmAction(editorDelete, "Deleting...", "Deleted", () => api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2757
2955
|
editor.close();
|
|
2758
2956
|
return renderState();
|
|
2759
|
-
}).catch(showError);
|
|
2957
|
+
}), "Confirm delete").catch(showError);
|
|
2760
2958
|
});
|
|
2761
2959
|
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2762
2960
|
if (!(button instanceof HTMLButtonElement))
|
|
@@ -2802,10 +3000,10 @@ function bootAdmin(config) {
|
|
|
2802
3000
|
}).then(() => renderFaults()).catch(showError);
|
|
2803
3001
|
});
|
|
2804
3002
|
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2805
|
-
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
|
|
3003
|
+
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults()), "Confirm clear").catch(showError);
|
|
2806
3004
|
});
|
|
2807
3005
|
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2808
|
-
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
|
|
3006
|
+
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal()), "Confirm clear").catch(showError);
|
|
2809
3007
|
});
|
|
2810
3008
|
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2811
3009
|
const target = event.target;
|
|
@@ -2819,9 +3017,15 @@ function bootAdmin(config) {
|
|
|
2819
3017
|
return;
|
|
2820
3018
|
state.route = route;
|
|
2821
3019
|
el("route-title", HTMLElement).textContent = route;
|
|
3020
|
+
el("route-send", HTMLButtonElement).disabled = false;
|
|
2822
3021
|
const method = route.split(" ")[0] ?? "";
|
|
2823
|
-
el("route-body", HTMLTextAreaElement)
|
|
3022
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
3023
|
+
routeBody.disabled = method === "GET" || method === "DELETE";
|
|
3024
|
+
el("route-editor", HTMLElement).hidden = routeBody.disabled;
|
|
3025
|
+
el("route-json-status", HTMLElement).parentElement?.toggleAttribute("hidden", routeBody.disabled);
|
|
3026
|
+
syncRouteEditor();
|
|
2824
3027
|
});
|
|
3028
|
+
el("route-search", HTMLInputElement).addEventListener("input", paintRoutes);
|
|
2825
3029
|
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2826
3030
|
event.preventDefault();
|
|
2827
3031
|
const send = async () => {
|
|
@@ -2843,7 +3047,8 @@ function bootAdmin(config) {
|
|
|
2843
3047
|
}
|
|
2844
3048
|
}
|
|
2845
3049
|
const data = await api(method, concrete, body);
|
|
2846
|
-
|
|
3050
|
+
const text2 = JSON.stringify(data, null, 2) ?? "null";
|
|
3051
|
+
el("route-result", HTMLElement).innerHTML = `<code>${highlightJsonText(text2)}</code>`;
|
|
2847
3052
|
};
|
|
2848
3053
|
send().catch(showError);
|
|
2849
3054
|
});
|
|
@@ -2961,11 +3166,13 @@ function bootAdmin(config) {
|
|
|
2961
3166
|
views.append(section);
|
|
2962
3167
|
mountSqlExplorer(body, panelApi);
|
|
2963
3168
|
}
|
|
3169
|
+
show(document.getElementById(`view-${state.view}`) === null ? "overview" : state.view);
|
|
2964
3170
|
return refresh();
|
|
2965
3171
|
}).catch(showError);
|
|
2966
3172
|
}
|
|
2967
3173
|
var clientPrelude = [
|
|
2968
3174
|
isRecord2,
|
|
3175
|
+
highlightJsonText,
|
|
2969
3176
|
mountAdminBrand,
|
|
2970
3177
|
startAdminBrand,
|
|
2971
3178
|
faultPresetList,
|
|
@@ -2988,6 +3195,7 @@ ${entry}(${payload});
|
|
|
2988
3195
|
var defineMock = (runtime) => runtime;
|
|
2989
3196
|
var STANDARD_ADMIN_ROUTES = [
|
|
2990
3197
|
"GET /",
|
|
3198
|
+
"GET /health",
|
|
2991
3199
|
"POST /reset",
|
|
2992
3200
|
"GET /namespaces",
|
|
2993
3201
|
"GET /clock",
|
|
@@ -3067,11 +3275,11 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
3067
3275
|
}
|
|
3068
3276
|
});
|
|
3069
3277
|
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
3070
|
-
var renderAdminDocument = (service) => {
|
|
3278
|
+
var renderAdminDocument = (service, adminPrefix = ADMIN_PREFIX) => {
|
|
3071
3279
|
const name = escapeHtml(service);
|
|
3072
3280
|
const script = adminClientSource({
|
|
3073
3281
|
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
3074
|
-
adminPrefix
|
|
3282
|
+
adminPrefix,
|
|
3075
3283
|
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
3076
3284
|
brandsUrl: ADMIN_BRANDS_URL,
|
|
3077
3285
|
service
|
|
@@ -3088,75 +3296,99 @@ var renderAdminDocument = (service) => {
|
|
|
3088
3296
|
${CSS_RESET}
|
|
3089
3297
|
:root {
|
|
3090
3298
|
color-scheme: light;
|
|
3091
|
-
--bg: #
|
|
3299
|
+
--bg: #f7f8fb;
|
|
3092
3300
|
--bg-elev: #ffffff;
|
|
3093
|
-
--bg-muted: #
|
|
3094
|
-
--bg-inset: #
|
|
3095
|
-
--line: #
|
|
3096
|
-
--line-strong: #
|
|
3097
|
-
--ink: #
|
|
3098
|
-
--muted: #
|
|
3099
|
-
--faint: #
|
|
3100
|
-
--accent: #
|
|
3301
|
+
--bg-muted: #f1f3f7;
|
|
3302
|
+
--bg-inset: #f8f9fc;
|
|
3303
|
+
--line: #e4e7ec;
|
|
3304
|
+
--line-strong: #cfd4dc;
|
|
3305
|
+
--ink: #101828;
|
|
3306
|
+
--muted: #475467;
|
|
3307
|
+
--faint: #667085;
|
|
3308
|
+
--accent: #6941c6;
|
|
3309
|
+
--accent-hover: #5933b4;
|
|
3101
3310
|
--accent-ink: #ffffff;
|
|
3102
|
-
--accent-soft: #
|
|
3103
|
-
--ok: #
|
|
3104
|
-
--ok-soft: #
|
|
3105
|
-
--warn: #
|
|
3106
|
-
--warn-soft: #
|
|
3107
|
-
--err: #
|
|
3108
|
-
--err-soft: #
|
|
3109
|
-
--
|
|
3311
|
+
--accent-soft: #f4f0ff;
|
|
3312
|
+
--ok: #067647;
|
|
3313
|
+
--ok-soft: #ecfdf3;
|
|
3314
|
+
--warn: #b54708;
|
|
3315
|
+
--warn-soft: #fffaeb;
|
|
3316
|
+
--err: #b42318;
|
|
3317
|
+
--err-soft: #fef3f2;
|
|
3318
|
+
--code-key: #6941c6;
|
|
3319
|
+
--code-string: #067647;
|
|
3320
|
+
--code-number: #175cd3;
|
|
3321
|
+
--code-boolean: #c11574;
|
|
3322
|
+
--code-null: #667085;
|
|
3323
|
+
--shadow-xs: 0 1px 2px rgb(16 24 40 / 0.05);
|
|
3324
|
+
--shadow-sm: 0 1px 3px rgb(16 24 40 / 0.08), 0 1px 2px rgb(16 24 40 / 0.04);
|
|
3325
|
+
--shadow-lg: 0 18px 48px rgb(16 24 40 / 0.16), 0 4px 12px rgb(16 24 40 / 0.08);
|
|
3110
3326
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
3111
3327
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
3112
|
-
--radius:
|
|
3113
|
-
--header:
|
|
3114
|
-
--nav:
|
|
3328
|
+
--radius: 12px;
|
|
3329
|
+
--header: 72px;
|
|
3330
|
+
--nav: 224px;
|
|
3115
3331
|
}
|
|
3116
3332
|
:root[data-theme="dark"] {
|
|
3117
3333
|
color-scheme: dark;
|
|
3118
|
-
--bg: #
|
|
3119
|
-
--bg-elev: #
|
|
3120
|
-
--bg-muted: #
|
|
3121
|
-
--bg-inset: #
|
|
3122
|
-
--line: #
|
|
3123
|
-
--line-strong: #
|
|
3124
|
-
--ink: #
|
|
3125
|
-
--muted: #
|
|
3126
|
-
--faint: #
|
|
3127
|
-
--accent: #
|
|
3128
|
-
--accent-
|
|
3129
|
-
--accent-
|
|
3130
|
-
--
|
|
3131
|
-
--ok
|
|
3132
|
-
--
|
|
3133
|
-
--warn
|
|
3134
|
-
--
|
|
3135
|
-
--err
|
|
3136
|
-
--
|
|
3334
|
+
--bg: #0c0e14;
|
|
3335
|
+
--bg-elev: #151821;
|
|
3336
|
+
--bg-muted: #20232e;
|
|
3337
|
+
--bg-inset: #11141c;
|
|
3338
|
+
--line: #2b303d;
|
|
3339
|
+
--line-strong: #414858;
|
|
3340
|
+
--ink: #f5f7fa;
|
|
3341
|
+
--muted: #b6bdc9;
|
|
3342
|
+
--faint: #8d96a7;
|
|
3343
|
+
--accent: #9e77ed;
|
|
3344
|
+
--accent-hover: #b692f6;
|
|
3345
|
+
--accent-ink: #ffffff;
|
|
3346
|
+
--accent-soft: #2b2142;
|
|
3347
|
+
--ok: #75e0a7;
|
|
3348
|
+
--ok-soft: #102a20;
|
|
3349
|
+
--warn: #fec84b;
|
|
3350
|
+
--warn-soft: #302611;
|
|
3351
|
+
--err: #fda29b;
|
|
3352
|
+
--err-soft: #321817;
|
|
3353
|
+
--code-key: #c3a6ff;
|
|
3354
|
+
--code-string: #75e0a7;
|
|
3355
|
+
--code-number: #84adff;
|
|
3356
|
+
--code-boolean: #f9a8d4;
|
|
3357
|
+
--code-null: #98a2b3;
|
|
3358
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3359
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3360
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
3137
3361
|
}
|
|
3138
3362
|
@media (prefers-color-scheme: dark) {
|
|
3139
3363
|
:root:not([data-theme="light"]) {
|
|
3140
3364
|
color-scheme: dark;
|
|
3141
|
-
--bg: #
|
|
3142
|
-
--bg-elev: #
|
|
3143
|
-
--bg-muted: #
|
|
3144
|
-
--bg-inset: #
|
|
3145
|
-
--line: #
|
|
3146
|
-
--line-strong: #
|
|
3147
|
-
--ink: #
|
|
3148
|
-
--muted: #
|
|
3149
|
-
--faint: #
|
|
3150
|
-
--accent: #
|
|
3151
|
-
--accent-
|
|
3152
|
-
--accent-
|
|
3153
|
-
--
|
|
3154
|
-
--ok
|
|
3155
|
-
--
|
|
3156
|
-
--warn
|
|
3157
|
-
--
|
|
3158
|
-
--err
|
|
3159
|
-
--
|
|
3365
|
+
--bg: #0c0e14;
|
|
3366
|
+
--bg-elev: #151821;
|
|
3367
|
+
--bg-muted: #20232e;
|
|
3368
|
+
--bg-inset: #11141c;
|
|
3369
|
+
--line: #2b303d;
|
|
3370
|
+
--line-strong: #414858;
|
|
3371
|
+
--ink: #f5f7fa;
|
|
3372
|
+
--muted: #b6bdc9;
|
|
3373
|
+
--faint: #8d96a7;
|
|
3374
|
+
--accent: #9e77ed;
|
|
3375
|
+
--accent-hover: #b692f6;
|
|
3376
|
+
--accent-ink: #ffffff;
|
|
3377
|
+
--accent-soft: #2b2142;
|
|
3378
|
+
--ok: #75e0a7;
|
|
3379
|
+
--ok-soft: #102a20;
|
|
3380
|
+
--warn: #fec84b;
|
|
3381
|
+
--warn-soft: #302611;
|
|
3382
|
+
--err: #fda29b;
|
|
3383
|
+
--err-soft: #321817;
|
|
3384
|
+
--code-key: #c3a6ff;
|
|
3385
|
+
--code-string: #75e0a7;
|
|
3386
|
+
--code-number: #84adff;
|
|
3387
|
+
--code-boolean: #f9a8d4;
|
|
3388
|
+
--code-null: #98a2b3;
|
|
3389
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3390
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3391
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
3160
3392
|
}
|
|
3161
3393
|
}
|
|
3162
3394
|
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
@@ -3174,13 +3406,17 @@ var renderAdminDocument = (service) => {
|
|
|
3174
3406
|
padding-right: env(safe-area-inset-right);
|
|
3175
3407
|
-webkit-text-size-adjust: 100%;
|
|
3176
3408
|
}
|
|
3177
|
-
|
|
3409
|
+
::selection { background: color-mix(in srgb, var(--accent) 28%, transparent); }
|
|
3410
|
+
:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px; }
|
|
3411
|
+
button, input, select, textarea { transition: border-color 140ms ease, box-shadow 140ms ease, transform 100ms ease; }
|
|
3412
|
+
button:disabled, input:disabled, select:disabled, textarea:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
3178
3413
|
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
3179
3414
|
header.top {
|
|
3180
|
-
position: sticky; top: 0; z-index:
|
|
3415
|
+
position: sticky; top: 0; z-index: 20;
|
|
3181
3416
|
display: flex; align-items: center; gap: 12px;
|
|
3182
|
-
min-height: var(--header); padding:
|
|
3183
|
-
background: var(--bg-elev);
|
|
3417
|
+
min-height: var(--header); padding: 12px 20px;
|
|
3418
|
+
background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
|
|
3419
|
+
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
3184
3420
|
border-bottom: 1px solid var(--line);
|
|
3185
3421
|
min-width: 0; max-width: 100%;
|
|
3186
3422
|
}
|
|
@@ -3188,12 +3424,21 @@ var renderAdminDocument = (service) => {
|
|
|
3188
3424
|
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
3189
3425
|
min-width: 0; max-width: 100%; margin-right: auto;
|
|
3190
3426
|
}
|
|
3191
|
-
.brand-
|
|
3192
|
-
.brand-
|
|
3427
|
+
.brand-lockup { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
3428
|
+
.brand-mark {
|
|
3429
|
+
width: 38px; height: 38px; display: grid; place-items: center; flex: none;
|
|
3430
|
+
border-radius: 11px; color: #fff;
|
|
3431
|
+
background: linear-gradient(145deg, #7f56d9 0%, #53389e 100%);
|
|
3432
|
+
box-shadow: 0 6px 16px rgb(105 65 198 / 0.24), inset 0 1px 0 rgb(255 255 255 / 0.25);
|
|
3433
|
+
}
|
|
3434
|
+
.brand-mark svg { width: 22px; height: 22px; }
|
|
3435
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; line-height: 1.25; }
|
|
3436
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
3193
3437
|
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
3194
3438
|
.vendor {
|
|
3195
3439
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
3196
3440
|
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
3441
|
+
padding: 5px 8px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-inset);
|
|
3197
3442
|
}
|
|
3198
3443
|
.vendor[hidden] { display: none; }
|
|
3199
3444
|
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
@@ -3205,12 +3450,17 @@ var renderAdminDocument = (service) => {
|
|
|
3205
3450
|
.vendor-sep { color: var(--faint); }
|
|
3206
3451
|
.vendor-link { color: inherit; text-decoration: none; }
|
|
3207
3452
|
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
3208
|
-
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items:
|
|
3209
|
-
|
|
3453
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
|
|
3454
|
+
.sync-status { align-self: center; display: flex; align-items: center; gap: 6px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3455
|
+
.sync-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
|
|
3456
|
+
.sync-status.busy::before { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); animation: pulse 1.2s ease-in-out infinite; }
|
|
3457
|
+
label.field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; font-weight: 600; color: var(--faint); }
|
|
3210
3458
|
label.field input, label.field select, .controls input[type="password"] {
|
|
3211
|
-
background-color: var(--bg-
|
|
3212
|
-
padding:
|
|
3459
|
+
background-color: var(--bg-elev); border: 1px solid var(--line-strong); border-radius: 9px;
|
|
3460
|
+
padding: 8px 10px; min-height: 38px; line-height: 1.2; box-shadow: var(--shadow-xs);
|
|
3213
3461
|
}
|
|
3462
|
+
label.field input:hover, label.field select:hover, textarea:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--line-strong)); }
|
|
3463
|
+
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; }
|
|
3214
3464
|
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
3215
3465
|
the text fields in the same form. Paint it like those fields. */
|
|
3216
3466
|
label.field select {
|
|
@@ -3227,12 +3477,20 @@ var renderAdminDocument = (service) => {
|
|
|
3227
3477
|
dialog .field > textarea { width: 100%; }
|
|
3228
3478
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
3229
3479
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
.btn
|
|
3480
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
|
3481
|
+
border-radius: 9px; min-height: 38px; padding: 0 13px; border: 1px solid var(--line-strong);
|
|
3482
|
+
background: var(--bg-elev); color: var(--ink); font-size: 13px; font-weight: 600; line-height: 1;
|
|
3483
|
+
box-shadow: var(--shadow-xs);
|
|
3484
|
+
}
|
|
3485
|
+
.icon-btn { width: 38px; padding: 0; }
|
|
3486
|
+
.icon-btn svg, .btn svg, .btn-primary svg { width: 16px; height: 16px; }
|
|
3487
|
+
.icon-btn:hover, .btn:hover { background: var(--bg-muted); border-color: var(--line-strong); }
|
|
3488
|
+
.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; }
|
|
3489
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
3490
|
+
.btn-quiet { background: transparent; border-color: transparent; box-shadow: none; }
|
|
3491
|
+
.btn-quiet:hover { background: var(--bg-muted); }
|
|
3492
|
+
.btn-danger { color: var(--err); border-color: color-mix(in srgb, var(--err) 25%, var(--line)); }
|
|
3493
|
+
.btn-danger:hover, .btn-danger.confirming { background: var(--err-soft); border-color: color-mix(in srgb, var(--err) 45%, var(--line)); }
|
|
3236
3494
|
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
3237
3495
|
so the press itself has to stay visible. */
|
|
3238
3496
|
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
@@ -3248,93 +3506,159 @@ var renderAdminDocument = (service) => {
|
|
|
3248
3506
|
flex: 1 0 auto; min-width: 0;
|
|
3249
3507
|
}
|
|
3250
3508
|
nav.side {
|
|
3251
|
-
|
|
3509
|
+
position: sticky; top: var(--header); align-self: start; height: calc(100vh - var(--header));
|
|
3510
|
+
border-right: 1px solid var(--line); padding: 18px 12px;
|
|
3252
3511
|
display: flex; flex-direction: column; gap: 4px;
|
|
3253
|
-
background: var(--bg-elev);
|
|
3512
|
+
background: var(--bg-elev); overflow-y: auto;
|
|
3254
3513
|
}
|
|
3514
|
+
.nav-label { padding: 0 10px 8px; color: var(--faint); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
|
|
3255
3515
|
nav.side button {
|
|
3516
|
+
position: relative; display: flex; align-items: center; gap: 10px; width: 100%;
|
|
3256
3517
|
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
3257
|
-
padding: 9px 10px; min-height:
|
|
3258
|
-
}
|
|
3259
|
-
nav.side button
|
|
3260
|
-
|
|
3261
|
-
.
|
|
3262
|
-
.
|
|
3263
|
-
.
|
|
3518
|
+
padding: 9px 10px; min-height: 42px; color: var(--muted); font-size: 13px; font-weight: 550;
|
|
3519
|
+
}
|
|
3520
|
+
nav.side button:hover { background: var(--bg-muted); color: var(--ink); }
|
|
3521
|
+
nav.side button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 650; }
|
|
3522
|
+
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); }
|
|
3523
|
+
.nav-icon { width: 18px; height: 18px; display: grid; place-items: center; flex: none; }
|
|
3524
|
+
.nav-icon svg { width: 18px; height: 18px; }
|
|
3525
|
+
main { padding: 32px clamp(20px, 4vw, 48px) 64px; max-width: 1320px; width: 100%; min-width: 0; }
|
|
3526
|
+
main[aria-busy="true"] .view { opacity: 0.62; }
|
|
3527
|
+
.view { animation: view-in 180ms ease-out; }
|
|
3528
|
+
.view[hidden] { display: none; }
|
|
3529
|
+
.view-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
|
|
3530
|
+
.view h2 { margin: 0 0 5px; font-size: clamp(24px, 3vw, 30px); line-height: 1.2; letter-spacing: -0.035em; }
|
|
3531
|
+
.view h3 { margin: 24px 0 10px; font-size: 15px; letter-spacing: -0.01em; }
|
|
3532
|
+
.lede { margin: 0; color: var(--muted); max-width: 72ch; font-size: 14px; }
|
|
3533
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 175px), 1fr)); gap: 14px; }
|
|
3264
3534
|
.card {
|
|
3265
3535
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
3266
|
-
padding:
|
|
3536
|
+
padding: 18px; box-shadow: var(--shadow-sm);
|
|
3267
3537
|
}
|
|
3268
|
-
.card
|
|
3269
|
-
.card
|
|
3538
|
+
.stat-card { position: relative; overflow: hidden; }
|
|
3539
|
+
.stat-card::after { content: ""; position: absolute; inset: 0 0 auto; height: 2px; background: linear-gradient(90deg, var(--accent), transparent 78%); opacity: 0.7; }
|
|
3540
|
+
.card .k { color: var(--faint); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
3541
|
+
.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; }
|
|
3270
3542
|
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
3271
|
-
.
|
|
3543
|
+
.section-card { background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-xs); overflow: hidden; }
|
|
3544
|
+
.section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
|
|
3545
|
+
.section-title h3 { margin: 0; }
|
|
3546
|
+
.split { display: grid; grid-template-columns: minmax(0, 250px) minmax(0, 1fr); gap: 18px; align-items: start; }
|
|
3547
|
+
#collections { padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); box-shadow: var(--shadow-xs); }
|
|
3272
3548
|
.collection {
|
|
3273
3549
|
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
3274
|
-
border-radius:
|
|
3550
|
+
border-radius: 9px; padding: 10px 11px; min-height: 48px; color: var(--ink); cursor: pointer;
|
|
3275
3551
|
}
|
|
3276
|
-
.collection
|
|
3552
|
+
.collection:hover { background: var(--bg-muted); }
|
|
3553
|
+
.collection[aria-current="true"] { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 18%, var(--line)); color: var(--accent); }
|
|
3277
3554
|
.collection small { display: block; color: var(--faint); }
|
|
3278
3555
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
3279
|
-
.chip {
|
|
3280
|
-
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
3281
|
-
border-radius: 999px; padding: 4px 8px;
|
|
3282
|
-
}
|
|
3556
|
+
.chip { font-family: var(--mono); font-size: 11px; background: var(--bg-muted); border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; }
|
|
3283
3557
|
.chip i { color: var(--faint); font-style: normal; }
|
|
3284
3558
|
.sql-layout {
|
|
3285
3559
|
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
3286
3560
|
}
|
|
3287
|
-
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
3288
|
-
.sql-tables button { text-align: left; }
|
|
3561
|
+
.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); }
|
|
3562
|
+
.sql-tables button { text-align: left; border: 0; border-radius: 8px; padding: 9px 10px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
3563
|
+
.sql-tables button:hover, .sql-tables button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); }
|
|
3289
3564
|
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
3290
|
-
table { width: 100%; border-
|
|
3291
|
-
|
|
3292
|
-
th {
|
|
3565
|
+
.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); }
|
|
3566
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
3567
|
+
th, td { text-align: left; padding: 11px 13px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
3568
|
+
th { color: var(--faint); background: var(--bg-inset); font-weight: 650; font-size: 11px; text-transform: uppercase; letter-spacing: 0.045em; }
|
|
3569
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
3293
3570
|
tr.clickable { cursor: pointer; }
|
|
3294
|
-
tr.clickable:hover { background: var(--
|
|
3571
|
+
tr.clickable:hover { background: var(--accent-soft); }
|
|
3295
3572
|
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
3296
3573
|
pre {
|
|
3297
3574
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
3298
|
-
background: var(--bg-inset); border-radius: 10px; padding:
|
|
3299
|
-
}
|
|
3300
|
-
.
|
|
3575
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 10px; padding: 14px; font-size: 12px; line-height: 1.65;
|
|
3576
|
+
}
|
|
3577
|
+
.json-key { color: var(--code-key); }
|
|
3578
|
+
.json-string { color: var(--code-string); }
|
|
3579
|
+
.json-number { color: var(--code-number); }
|
|
3580
|
+
.json-boolean { color: var(--code-boolean); }
|
|
3581
|
+
.json-null { color: var(--code-null); font-style: italic; }
|
|
3582
|
+
.json-inline { display: block; max-width: 44rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
3583
|
+
.json-view { position: relative; max-height: 26rem; overflow: auto; }
|
|
3584
|
+
.json-editor { position: relative; min-height: 190px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--bg-inset); overflow: hidden; }
|
|
3585
|
+
.json-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); }
|
|
3586
|
+
.json-editor.invalid { border-color: var(--err); }
|
|
3587
|
+
.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; }
|
|
3588
|
+
.json-editor pre { position: absolute; inset: 0; pointer-events: none; background: transparent; }
|
|
3589
|
+
.json-editor textarea { position: relative; z-index: 1; resize: vertical; background: transparent; color: transparent; caret-color: var(--ink); -webkit-text-fill-color: transparent; }
|
|
3590
|
+
.json-editor textarea::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); }
|
|
3591
|
+
.editor-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; }
|
|
3592
|
+
.json-status { display: inline-flex; align-items: center; gap: 6px; color: var(--ok); font-size: 11px; }
|
|
3593
|
+
.json-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
3594
|
+
.json-status.invalid { color: var(--err); }
|
|
3595
|
+
.format-json { border: 0; padding: 3px 5px; background: transparent; color: var(--accent); font-size: 11px; font-weight: 650; cursor: pointer; }
|
|
3596
|
+
.row-actions { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 14px 0; }
|
|
3301
3597
|
.banner {
|
|
3302
|
-
display: none;
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3598
|
+
display: none; position: sticky; top: calc(var(--header) + 12px); z-index: 15;
|
|
3599
|
+
align-items: center; gap: 10px; margin: 0 0 16px; padding: 11px 12px 11px 14px;
|
|
3600
|
+
border: 1px solid color-mix(in srgb, var(--err) 30%, var(--line)); border-radius: 10px;
|
|
3601
|
+
background: var(--err-soft); color: var(--err); box-shadow: var(--shadow-sm); font-size: 13px;
|
|
3602
|
+
}
|
|
3603
|
+
.banner.show { display: flex; }
|
|
3604
|
+
.banner span { flex: 1; }
|
|
3605
|
+
.banner button { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 7px; background: transparent; color: currentColor; cursor: pointer; }
|
|
3606
|
+
.banner button:hover { background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
3607
|
+
.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); }
|
|
3307
3608
|
dialog {
|
|
3308
|
-
border: 1px solid var(--line); border-radius:
|
|
3309
|
-
color: var(--ink); width: min(
|
|
3310
|
-
}
|
|
3311
|
-
dialog
|
|
3312
|
-
dialog
|
|
3609
|
+
border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--bg-elev);
|
|
3610
|
+
color: var(--ink); width: min(720px, calc(100% - 24px)); max-width: calc(100vw - 24px); box-shadow: var(--shadow-lg);
|
|
3611
|
+
}
|
|
3612
|
+
dialog[open] { animation: dialog-in 160ms ease-out; }
|
|
3613
|
+
dialog::backdrop { background: rgb(12 14 20 / 0.58); backdrop-filter: blur(3px); }
|
|
3614
|
+
dialog form, .dialog-body { padding: 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
3615
|
+
.dialog-heading { display: flex; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
|
3616
|
+
.dialog-heading strong { font-size: 18px; letter-spacing: -0.02em; }
|
|
3617
|
+
.dialog-heading span { color: var(--muted); font-size: 12px; }
|
|
3313
3618
|
textarea {
|
|
3314
3619
|
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
3315
3620
|
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
3316
3621
|
font-family: var(--mono); font-size: 12px;
|
|
3317
3622
|
}
|
|
3318
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
3319
|
-
.
|
|
3623
|
+
.dialog-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding-top: 4px; }
|
|
3624
|
+
.dialog-actions .btn-danger { margin-right: auto; }
|
|
3625
|
+
.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); }
|
|
3320
3626
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
3321
3627
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
3322
3628
|
.status-2 { color: var(--ok); }
|
|
3323
3629
|
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
3324
3630
|
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
3631
|
+
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
3632
|
+
.form-grid .wide { grid-column: span 2; }
|
|
3325
3633
|
.route {
|
|
3326
|
-
display: grid; grid-template-columns:
|
|
3327
|
-
padding:
|
|
3328
|
-
}
|
|
3329
|
-
.
|
|
3634
|
+
display: grid; grid-template-columns: 84px minmax(0, 1fr) auto; gap: 10px; align-items: center;
|
|
3635
|
+
padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--bg-elev);
|
|
3636
|
+
}
|
|
3637
|
+
.route:first-child { border-radius: var(--radius) var(--radius) 0 0; }
|
|
3638
|
+
.route:last-child { border-bottom: 0; border-radius: 0 0 var(--radius) var(--radius); }
|
|
3639
|
+
.route-tools { display: flex; align-items: flex-end; gap: 12px; margin-bottom: 12px; }
|
|
3640
|
+
.route-tools .field { flex: 1; }
|
|
3641
|
+
.route-tools input { width: 100%; }
|
|
3642
|
+
.route-count { padding-bottom: 10px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3643
|
+
#route-list { max-height: 34rem; border: 1px solid var(--line); border-radius: var(--radius); overflow: auto; box-shadow: var(--shadow-xs); }
|
|
3644
|
+
.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; }
|
|
3645
|
+
@keyframes view-in { from { transform: translateY(3px); } to { transform: translateY(0); } }
|
|
3646
|
+
@keyframes dialog-in { from { transform: translateY(8px) scale(0.99); } to { transform: translateY(0) scale(1); } }
|
|
3647
|
+
@keyframes pulse { 50% { opacity: 0.45; } }
|
|
3648
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
|
|
3649
|
+
@media (forced-colors: active) { .json-editor pre { display: none; } .json-editor textarea { color: CanvasText; -webkit-text-fill-color: CanvasText; } }
|
|
3330
3650
|
@media (max-width: 800px) {
|
|
3651
|
+
:root { --header: 0px; }
|
|
3331
3652
|
header.top {
|
|
3332
|
-
align-items: stretch; flex-direction: column;
|
|
3333
|
-
padding
|
|
3653
|
+
position: relative; align-items: stretch; flex-direction: column;
|
|
3654
|
+
padding: max(12px, env(safe-area-inset-top)) 14px 12px;
|
|
3334
3655
|
}
|
|
3335
3656
|
input, select, textarea { font-size: 16px; }
|
|
3336
3657
|
.brand, .vendor, .controls { width: 100%; }
|
|
3658
|
+
.brand { gap: 10px; }
|
|
3659
|
+
.vendor { order: 2; border-radius: 9px; }
|
|
3337
3660
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
3661
|
+
.sync-status { display: none; }
|
|
3338
3662
|
/* The layout fills the screen. A single-column grid stretches every auto
|
|
3339
3663
|
row, so the tab bar grew with the leftover height and the buttons floated
|
|
3340
3664
|
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
@@ -3344,14 +3668,23 @@ var renderAdminDocument = (service) => {
|
|
|
3344
3668
|
align-content: start;
|
|
3345
3669
|
}
|
|
3346
3670
|
nav.side {
|
|
3347
|
-
align-self: start; height: auto;
|
|
3671
|
+
position: sticky; top: 0; z-index: 12; align-self: start; height: auto;
|
|
3348
3672
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
3349
3673
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
3350
|
-
padding: 8px;
|
|
3674
|
+
padding: 8px; box-shadow: var(--shadow-xs);
|
|
3351
3675
|
}
|
|
3352
|
-
nav
|
|
3676
|
+
.nav-label { display: none; }
|
|
3677
|
+
nav.side button { width: auto; white-space: nowrap; flex: none; }
|
|
3678
|
+
nav.side button[aria-current="true"]::before { inset: auto 9px -8px; width: auto; height: 3px; border-radius: 3px 3px 0 0; }
|
|
3353
3679
|
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
3354
|
-
|
|
3680
|
+
.form-grid { grid-template-columns: minmax(0, 1fr); }
|
|
3681
|
+
.form-grid .wide { grid-column: auto; }
|
|
3682
|
+
main { padding: 22px 14px 48px; }
|
|
3683
|
+
.view-head { margin-bottom: 18px; }
|
|
3684
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
3685
|
+
.card { padding: 14px; }
|
|
3686
|
+
.card .v { font-size: 20px; }
|
|
3687
|
+
.table-shell { border: 0; background: transparent; box-shadow: none; overflow: visible; }
|
|
3355
3688
|
table, thead, tbody, tr, th, td { display: block; }
|
|
3356
3689
|
thead { display: none; }
|
|
3357
3690
|
tr {
|
|
@@ -3366,50 +3699,63 @@ var renderAdminDocument = (service) => {
|
|
|
3366
3699
|
.route { grid-template-columns: 72px 1fr; }
|
|
3367
3700
|
.route button { grid-column: 1 / -1; }
|
|
3368
3701
|
}
|
|
3702
|
+
@media (max-width: 460px) {
|
|
3703
|
+
.controls label.field { min-width: calc(50% - 4px); }
|
|
3704
|
+
.controls .icon-btn, .controls .btn { flex: 1; }
|
|
3705
|
+
.grid { grid-template-columns: minmax(0, 1fr); }
|
|
3706
|
+
.dialog-actions { flex-wrap: wrap; }
|
|
3707
|
+
.dialog-actions button { flex: 1; }
|
|
3708
|
+
.dialog-actions .btn-danger { flex-basis: 100%; margin-right: 0; }
|
|
3709
|
+
}
|
|
3369
3710
|
</style>
|
|
3370
3711
|
</head>
|
|
3371
3712
|
<body>
|
|
3372
3713
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
3373
3714
|
<header class="top">
|
|
3374
3715
|
<div class="brand">
|
|
3375
|
-
<div class="brand-
|
|
3376
|
-
<
|
|
3377
|
-
|
|
3716
|
+
<div class="brand-lockup">
|
|
3717
|
+
<div class="brand-mark" aria-hidden="true">
|
|
3718
|
+
<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>
|
|
3719
|
+
</div>
|
|
3720
|
+
<div class="brand-id">
|
|
3721
|
+
<strong>${name}</strong>
|
|
3722
|
+
<span>Mock admin</span>
|
|
3723
|
+
</div>
|
|
3378
3724
|
</div>
|
|
3379
3725
|
<div class="vendor" id="vendor" hidden></div>
|
|
3380
3726
|
</div>
|
|
3381
3727
|
<div class="controls">
|
|
3728
|
+
<span class="sync-status" id="sync-status" role="status" aria-live="polite">Ready</span>
|
|
3382
3729
|
<label class="field">Namespace
|
|
3383
3730
|
<select id="namespace" aria-label="Namespace"></select>
|
|
3384
3731
|
</label>
|
|
3385
3732
|
<label class="field">Admin key
|
|
3386
3733
|
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
3387
3734
|
</label>
|
|
3388
|
-
<button class="icon-btn" id="theme" type="button" aria-label="
|
|
3389
|
-
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
3735
|
+
<button class="icon-btn" id="theme" type="button" aria-label="Use dark theme" title="Use dark theme"></button>
|
|
3736
|
+
<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>
|
|
3390
3737
|
</div>
|
|
3391
3738
|
</header>
|
|
3392
3739
|
<div class="layout">
|
|
3393
3740
|
<nav class="side" id="nav" aria-label="Admin sections">
|
|
3394
|
-
<
|
|
3395
|
-
<button type="button" data-view="
|
|
3396
|
-
<button type="button" data-view="
|
|
3397
|
-
<button type="button" data-view="
|
|
3398
|
-
<button type="button" data-view="
|
|
3399
|
-
<button type="button" data-view="
|
|
3741
|
+
<span class="nav-label">Workspace</span>
|
|
3742
|
+
<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>
|
|
3743
|
+
<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>
|
|
3744
|
+
<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>
|
|
3745
|
+
<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>
|
|
3746
|
+
<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>
|
|
3747
|
+
<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>
|
|
3400
3748
|
</nav>
|
|
3401
3749
|
<main id="views">
|
|
3402
|
-
<
|
|
3750
|
+
<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>
|
|
3403
3751
|
<section class="view" id="view-overview">
|
|
3404
|
-
<h2>Overview</h2>
|
|
3405
|
-
<p class="lede">Health, clock, and the collections this namespace is holding.</p>
|
|
3752
|
+
<div class="view-head"><div><h2>Overview</h2><p class="lede">Health, clock, and the collections this namespace is holding.</p></div></div>
|
|
3406
3753
|
<div class="grid" id="overview-cards"></div>
|
|
3407
3754
|
<h3>Recent requests</h3>
|
|
3408
3755
|
<div id="overview-requests"></div>
|
|
3409
3756
|
</section>
|
|
3410
3757
|
<section class="view" id="view-state" hidden>
|
|
3411
|
-
<h2>State</h2>
|
|
3412
|
-
<p class="lede">Every collection in this namespace. Declared fields stay visible before any row exists; stored rows add whatever else they contain.</p>
|
|
3758
|
+
<div class="view-head"><div><h2>State</h2><p class="lede">Inspect and edit every collection in this namespace.</p></div></div>
|
|
3413
3759
|
<div class="split">
|
|
3414
3760
|
<div id="collections"></div>
|
|
3415
3761
|
<div>
|
|
@@ -3422,8 +3768,7 @@ var renderAdminDocument = (service) => {
|
|
|
3422
3768
|
</div>
|
|
3423
3769
|
</section>
|
|
3424
3770
|
<section class="view" id="view-clock" hidden>
|
|
3425
|
-
<h2>Clock</h2>
|
|
3426
|
-
<p class="lede">The clock every timestamp in this mock reads.</p>
|
|
3771
|
+
<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>
|
|
3427
3772
|
<div class="card" id="clock-readout"></div>
|
|
3428
3773
|
<div class="row-actions">
|
|
3429
3774
|
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
@@ -3442,21 +3787,22 @@ var renderAdminDocument = (service) => {
|
|
|
3442
3787
|
</form>
|
|
3443
3788
|
</section>
|
|
3444
3789
|
<section class="view" id="view-faults" hidden>
|
|
3445
|
-
<h2>Faults</h2>
|
|
3446
|
-
<p class="lede">Inject a response, a delay, or a named preset. Rules apply to this namespace.</p>
|
|
3790
|
+
<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>
|
|
3447
3791
|
<form id="fault-form" class="card stack">
|
|
3448
|
-
<
|
|
3449
|
-
<
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
<
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
<
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
<
|
|
3459
|
-
|
|
3792
|
+
<div class="form-grid">
|
|
3793
|
+
<label class="field wide">Preset
|
|
3794
|
+
<select id="fault-preset"><option value="">Custom rule</option></select>
|
|
3795
|
+
</label>
|
|
3796
|
+
<label class="field">Status
|
|
3797
|
+
<input id="fault-status" inputmode="numeric" placeholder="503">
|
|
3798
|
+
</label>
|
|
3799
|
+
<label class="field wide">Path prefix
|
|
3800
|
+
<input id="fault-path" placeholder="/v1">
|
|
3801
|
+
</label>
|
|
3802
|
+
<label class="field">Method
|
|
3803
|
+
<input id="fault-method" placeholder="GET">
|
|
3804
|
+
</label>
|
|
3805
|
+
</div>
|
|
3460
3806
|
<div class="dialog-actions">
|
|
3461
3807
|
<button class="btn-primary" type="submit">Add fault</button>
|
|
3462
3808
|
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
@@ -3465,26 +3811,31 @@ var renderAdminDocument = (service) => {
|
|
|
3465
3811
|
<div id="fault-list"></div>
|
|
3466
3812
|
</section>
|
|
3467
3813
|
<section class="view" id="view-journal" hidden>
|
|
3468
|
-
<h2>Journal</h2>
|
|
3469
|
-
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
3470
|
-
<div class="row-actions">
|
|
3471
|
-
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
3472
|
-
</div>
|
|
3814
|
+
<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>
|
|
3473
3815
|
<div id="journal"></div>
|
|
3474
3816
|
</section>
|
|
3475
3817
|
<section class="view" id="view-routes" hidden>
|
|
3476
|
-
<h2>Routes</h2>
|
|
3477
|
-
<
|
|
3818
|
+
<div class="view-head"><div><h2>Routes</h2><p class="lede">Explore shared controls and service-specific admin endpoints.</p></div></div>
|
|
3819
|
+
<div class="route-tools">
|
|
3820
|
+
<label class="field">Filter routes
|
|
3821
|
+
<input id="route-search" type="search" placeholder="Method, path, or extension" autocomplete="off">
|
|
3822
|
+
</label>
|
|
3823
|
+
<span class="route-count" id="route-count"></span>
|
|
3824
|
+
</div>
|
|
3478
3825
|
<div id="route-list"></div>
|
|
3479
|
-
<form id="route-form" class="card stack">
|
|
3826
|
+
<form id="route-form" class="card stack" style="margin-top: 18px">
|
|
3480
3827
|
<strong id="route-title">Select a route</strong>
|
|
3481
3828
|
<label class="field">JSON body
|
|
3482
|
-
<
|
|
3829
|
+
<div class="json-editor" id="route-editor" hidden>
|
|
3830
|
+
<pre aria-hidden="true"><code id="route-highlight"></code></pre>
|
|
3831
|
+
<textarea id="route-body" placeholder="{ }" spellcheck="false" aria-describedby="route-json-status" disabled></textarea>
|
|
3832
|
+
</div>
|
|
3833
|
+
<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>
|
|
3483
3834
|
</label>
|
|
3484
3835
|
<div class="dialog-actions">
|
|
3485
|
-
<button class="btn-primary" type="submit">Send</button>
|
|
3836
|
+
<button class="btn-primary" id="route-send" type="submit" disabled>Send request</button>
|
|
3486
3837
|
</div>
|
|
3487
|
-
<pre id="route-result">Response will show here.</pre>
|
|
3838
|
+
<pre class="json-view" id="route-result"><code>Response will show here.</code></pre>
|
|
3488
3839
|
</form>
|
|
3489
3840
|
</section>
|
|
3490
3841
|
</main>
|
|
@@ -3492,12 +3843,16 @@ var renderAdminDocument = (service) => {
|
|
|
3492
3843
|
</div>
|
|
3493
3844
|
<dialog id="editor">
|
|
3494
3845
|
<form method="dialog" id="editor-form">
|
|
3495
|
-
<strong id="editor-title">Record</strong>
|
|
3846
|
+
<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>
|
|
3496
3847
|
<label class="field">Id
|
|
3497
3848
|
<input id="editor-id" required>
|
|
3498
3849
|
</label>
|
|
3499
3850
|
<label class="field">JSON value
|
|
3500
|
-
<
|
|
3851
|
+
<div class="json-editor" id="record-editor">
|
|
3852
|
+
<pre aria-hidden="true"><code id="editor-highlight"></code></pre>
|
|
3853
|
+
<textarea id="editor-value" spellcheck="false" aria-describedby="editor-json-status"></textarea>
|
|
3854
|
+
</div>
|
|
3855
|
+
<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>
|
|
3501
3856
|
</label>
|
|
3502
3857
|
<div class="dialog-actions">
|
|
3503
3858
|
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
@@ -3539,8 +3894,8 @@ var describeExtension = (extension) => {
|
|
|
3539
3894
|
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
3540
3895
|
};
|
|
3541
3896
|
};
|
|
3542
|
-
var adminUiRoutes = (service, ui) => {
|
|
3543
|
-
const shell = () => renderAdminDocument(service);
|
|
3897
|
+
var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
3898
|
+
const shell = () => renderAdminDocument(service, adminPrefix);
|
|
3544
3899
|
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
3545
3900
|
return {
|
|
3546
3901
|
"GET /ui": () => htmlResponse(document3()),
|
|
@@ -4231,7 +4586,7 @@ var stateAdminRoutes = (routes) => {
|
|
|
4231
4586
|
};
|
|
4232
4587
|
|
|
4233
4588
|
// ../core/dist/version.js
|
|
4234
|
-
var PACKAGE_VERSION = true ? "
|
|
4589
|
+
var PACKAGE_VERSION = true ? "2.0.0" : UNRELEASED_VERSION;
|
|
4235
4590
|
|
|
4236
4591
|
// ../core/dist/signing.js
|
|
4237
4592
|
var encoder = new TextEncoder();
|
|
@@ -4604,7 +4959,10 @@ var createWebhookHub = (options) => {
|
|
|
4604
4959
|
};
|
|
4605
4960
|
return hub;
|
|
4606
4961
|
};
|
|
4607
|
-
var json5 = (status, body) => new Response(JSON.stringify(body), {
|
|
4962
|
+
var json5 = (status, body) => new Response(JSON.stringify(body), {
|
|
4963
|
+
status,
|
|
4964
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4965
|
+
});
|
|
4608
4966
|
var adminError3 = (status, message) => json5(status, { error: { type: "mockingbird_admin", message } });
|
|
4609
4967
|
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4610
4968
|
var parseEndpoint = (value) => {
|
|
@@ -4712,7 +5070,6 @@ var AT_HEADER = "x-mockingbird-at";
|
|
|
4712
5070
|
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
4713
5071
|
var DEFAULT_NAMESPACE = "default";
|
|
4714
5072
|
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4715
|
-
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
4716
5073
|
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4717
5074
|
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
4718
5075
|
var effects = /* @__PURE__ */ new WeakMap();
|
|
@@ -4753,6 +5110,22 @@ var operationMatcher = (document3) => {
|
|
|
4753
5110
|
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
4754
5111
|
};
|
|
4755
5112
|
var createRuntime = (options) => {
|
|
5113
|
+
const adminPrefix = resolveAdminPrefix(options.adminPrefix);
|
|
5114
|
+
const internalPaths = Object.entries(options.document?.paths ?? {}).filter(([, item]) => item["x-mockingbird-internal"] === true).map(([path]) => path);
|
|
5115
|
+
for (const path of internalPaths) {
|
|
5116
|
+
if (!path.startsWith(`${ADMIN_PREFIX}/blobs/`))
|
|
5117
|
+
throw new Error(`internal transport ${path} must live under ${ADMIN_PREFIX}/blobs/`);
|
|
5118
|
+
}
|
|
5119
|
+
assertAdminPrefixAvailable(adminPrefix, Object.keys(options.document?.paths ?? {}).filter((path) => !internalPaths.includes(path)));
|
|
5120
|
+
const internalMatch = (path) => {
|
|
5121
|
+
for (const pattern of internalPaths) {
|
|
5122
|
+
const actual = path.split("/");
|
|
5123
|
+
const expected = pattern.split("/");
|
|
5124
|
+
if (actual.length === expected.length && expected.every((part, i) => part.startsWith("{") || part === actual[i]))
|
|
5125
|
+
return pattern;
|
|
5126
|
+
}
|
|
5127
|
+
return void 0;
|
|
5128
|
+
};
|
|
4756
5129
|
assertAdminUi(options.adminUi);
|
|
4757
5130
|
const sqlite = bootSqlite(options.sqlite);
|
|
4758
5131
|
const clock = options.clock ?? createClock();
|
|
@@ -4798,6 +5171,7 @@ var createRuntime = (options) => {
|
|
|
4798
5171
|
const created = options.create({
|
|
4799
5172
|
namespace: storageNamespace(key),
|
|
4800
5173
|
publicNamespace,
|
|
5174
|
+
adminPrefix,
|
|
4801
5175
|
sqlite,
|
|
4802
5176
|
clock: namespaceClock(publicNamespace),
|
|
4803
5177
|
rng: createdContextRng
|
|
@@ -5030,10 +5404,10 @@ var createRuntime = (options) => {
|
|
|
5030
5404
|
namespaceClock,
|
|
5031
5405
|
namespaceOf(request) {
|
|
5032
5406
|
const url = new URL(request.url);
|
|
5033
|
-
const prefixed =
|
|
5407
|
+
const prefixed = matchNamespacePath(url.pathname, adminPrefix);
|
|
5034
5408
|
const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
|
|
5035
5409
|
const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
|
|
5036
|
-
if (path
|
|
5410
|
+
if (isAdminPath(path, adminPrefix))
|
|
5037
5411
|
return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
|
|
5038
5412
|
if (selected !== void 0)
|
|
5039
5413
|
return selected;
|
|
@@ -5099,7 +5473,7 @@ var createRuntime = (options) => {
|
|
|
5099
5473
|
state: (namespace = DEFAULT_NAMESPACE) => inspectState(stateScope(namespace)),
|
|
5100
5474
|
fetch: async (incoming) => {
|
|
5101
5475
|
let request = incoming;
|
|
5102
|
-
const prefixed =
|
|
5476
|
+
const prefixed = matchNamespacePath(new URL(request.url).pathname, adminPrefix);
|
|
5103
5477
|
if (prefixed) {
|
|
5104
5478
|
const url2 = new URL(request.url);
|
|
5105
5479
|
url2.pathname = prefixed[2] ?? "/";
|
|
@@ -5115,7 +5489,15 @@ var createRuntime = (options) => {
|
|
|
5115
5489
|
signal: request.signal
|
|
5116
5490
|
});
|
|
5117
5491
|
}
|
|
5118
|
-
|
|
5492
|
+
const internalUrl = new URL(request.url);
|
|
5493
|
+
const internalPath = isAdminPath(internalUrl.pathname, adminPrefix) ? `${ADMIN_PREFIX}${internalUrl.pathname.slice(adminPrefix.length)}` : "";
|
|
5494
|
+
const internal = internalMatch(internalPath);
|
|
5495
|
+
const obsoleteInternalPath = adminPrefix !== ADMIN_PREFIX && internalMatch(internalUrl.pathname) && !internal;
|
|
5496
|
+
if (internal) {
|
|
5497
|
+
internalUrl.pathname = internalPath;
|
|
5498
|
+
request = new Request(internalUrl, request);
|
|
5499
|
+
}
|
|
5500
|
+
let namespace = internal ? internalUrl.searchParams.get("namespace") ?? control.namespaceOf(request) : control.namespaceOf(request);
|
|
5119
5501
|
if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
5120
5502
|
const credential = options.credential(request);
|
|
5121
5503
|
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
@@ -5135,7 +5517,9 @@ var createRuntime = (options) => {
|
|
|
5135
5517
|
return copy;
|
|
5136
5518
|
}
|
|
5137
5519
|
};
|
|
5138
|
-
|
|
5520
|
+
if (obsoleteInternalPath)
|
|
5521
|
+
return stamp(adminFail(404, "internal transport is only available under adminPrefix"));
|
|
5522
|
+
const handled = internal ? void 0 : await control.handle(request);
|
|
5139
5523
|
if (handled)
|
|
5140
5524
|
return stamp(handled);
|
|
5141
5525
|
const started = monotonicNow();
|
|
@@ -5176,7 +5560,7 @@ var createRuntime = (options) => {
|
|
|
5176
5560
|
type: "mockingbird_admin",
|
|
5177
5561
|
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
5178
5562
|
}
|
|
5179
|
-
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
5563
|
+
}), { status: 400, headers: { "content-type": "application/json; charset=utf-8" } }));
|
|
5180
5564
|
}
|
|
5181
5565
|
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
5182
5566
|
log(400);
|
|
@@ -5293,15 +5677,16 @@ var createRuntime = (options) => {
|
|
|
5293
5677
|
...options.admin?.(runtime) ?? {},
|
|
5294
5678
|
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
5295
5679
|
// maps `presets`, and a service route must not replace that list with a record.
|
|
5296
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
5680
|
+
...options.presets ? presetRoutes(options.presets, runtime, adminPrefix) : {},
|
|
5297
5681
|
...stateAdminRoutes({
|
|
5298
5682
|
open: stateScope,
|
|
5299
5683
|
afterWrite: (namespace) => {
|
|
5300
5684
|
checkpoint(namespace, "main");
|
|
5301
5685
|
}
|
|
5302
5686
|
}),
|
|
5303
|
-
...adminUiRoutes(options.name, options.adminUi)
|
|
5687
|
+
...adminUiRoutes(options.name, options.adminUi, adminPrefix)
|
|
5304
5688
|
},
|
|
5689
|
+
adminPrefix,
|
|
5305
5690
|
adminKey: options.adminKey
|
|
5306
5691
|
});
|
|
5307
5692
|
return defineMock(runtime);
|
|
@@ -5315,7 +5700,10 @@ var mutableResponse = (response) => {
|
|
|
5315
5700
|
return new Response(response.body, response);
|
|
5316
5701
|
}
|
|
5317
5702
|
};
|
|
5318
|
-
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5703
|
+
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5704
|
+
status,
|
|
5705
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
5706
|
+
});
|
|
5319
5707
|
var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
|
|
5320
5708
|
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5321
5709
|
var credentialRoutes = (registry) => ({
|
|
@@ -5367,7 +5755,7 @@ var credentialRoutes = (registry) => ({
|
|
|
5367
5755
|
return adminJson(200, { status: "ok" });
|
|
5368
5756
|
}
|
|
5369
5757
|
});
|
|
5370
|
-
var presetRoutes = (presets, runtime) => ({
|
|
5758
|
+
var presetRoutes = (presets, runtime, adminPrefix) => ({
|
|
5371
5759
|
"GET /faults/presets": () => {
|
|
5372
5760
|
const body = {
|
|
5373
5761
|
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
@@ -5377,7 +5765,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
5377
5765
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
5378
5766
|
const name = params.name;
|
|
5379
5767
|
if (!presets[name])
|
|
5380
|
-
return adminFail(404, `no fault preset ${name}; GET /
|
|
5768
|
+
return adminFail(404, `no fault preset ${name}; GET ${adminPrefix}/faults/presets`);
|
|
5381
5769
|
const overrides = isObject(body) ? body : {};
|
|
5382
5770
|
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
5383
5771
|
}
|
|
@@ -5797,6 +6185,7 @@ var createRuntime2 = (options = {}) => {
|
|
|
5797
6185
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
5798
6186
|
...options.clock ? { clock: options.clock } : {},
|
|
5799
6187
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
6188
|
+
...options.adminPrefix !== void 0 ? { adminPrefix: options.adminPrefix } : {},
|
|
5800
6189
|
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
5801
6190
|
...options.onLog ? { onLog: options.onLog } : {},
|
|
5802
6191
|
credential: bearerToken,
|
|
@@ -6716,6 +7105,7 @@ var IntercomAPI = class {
|
|
|
6716
7105
|
|
|
6717
7106
|
export {
|
|
6718
7107
|
createClock,
|
|
7108
|
+
resolveAdminPrefix,
|
|
6719
7109
|
parseDuration,
|
|
6720
7110
|
document2 as document,
|
|
6721
7111
|
operationIds,
|
|
@@ -6735,4 +7125,4 @@ export {
|
|
|
6735
7125
|
IntercomError,
|
|
6736
7126
|
IntercomAPI
|
|
6737
7127
|
};
|
|
6738
|
-
//# sourceMappingURL=chunk-
|
|
7128
|
+
//# sourceMappingURL=chunk-DHV6Y6ZE.js.map
|