@crvouga/mockingbird-service-resend 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 +24 -1
- package/DISCOVERY.md +55 -0
- package/README.md +2 -2
- package/SUPPORT.md +15 -0
- package/dist/{chunk-RGWENFI4.js → chunk-AVLXDCB3.js} +653 -257
- package/dist/chunk-AVLXDCB3.js.map +7 -0
- package/dist/{chunk-BHIPX7QV.js → chunk-K5P64BHJ.js} +45 -24
- package/dist/chunk-K5P64BHJ.js.map +7 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +8 -3
- package/dist/index.js +1 -1
- package/dist/server.d.ts +7 -1
- package/dist/server.js +2 -2
- package/openapi.yaml +495 -0
- package/package.json +29 -4
- package/dist/chunk-BHIPX7QV.js.map +0 -7
- package/dist/chunk-RGWENFI4.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
|
};
|
|
@@ -1255,7 +1294,10 @@ var OutboxStore = class {
|
|
|
1255
1294
|
return query.limit !== void 0 ? matched.slice(-Math.max(0, query.limit)) : matched;
|
|
1256
1295
|
}
|
|
1257
1296
|
};
|
|
1258
|
-
var json2 = (status, body) => new Response(JSON.stringify(body), {
|
|
1297
|
+
var json2 = (status, body) => new Response(JSON.stringify(body), {
|
|
1298
|
+
status,
|
|
1299
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
1300
|
+
});
|
|
1259
1301
|
var parseSince = (value) => {
|
|
1260
1302
|
if (value === null)
|
|
1261
1303
|
return void 0;
|
|
@@ -1901,6 +1943,34 @@ dialog:not([open]) {
|
|
|
1901
1943
|
function isRecord2(value) {
|
|
1902
1944
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1903
1945
|
}
|
|
1946
|
+
function highlightJsonText(text) {
|
|
1947
|
+
const escapeToken = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
1948
|
+
switch (char) {
|
|
1949
|
+
case "&":
|
|
1950
|
+
return "&";
|
|
1951
|
+
case "<":
|
|
1952
|
+
return "<";
|
|
1953
|
+
case ">":
|
|
1954
|
+
return ">";
|
|
1955
|
+
case '"':
|
|
1956
|
+
return """;
|
|
1957
|
+
default:
|
|
1958
|
+
return "'";
|
|
1959
|
+
}
|
|
1960
|
+
});
|
|
1961
|
+
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;
|
|
1962
|
+
let cursor = 0;
|
|
1963
|
+
let html = "";
|
|
1964
|
+
for (const match of text.matchAll(token)) {
|
|
1965
|
+
const index = match.index ?? 0;
|
|
1966
|
+
const value = match[0];
|
|
1967
|
+
html += escapeToken(text.slice(cursor, index));
|
|
1968
|
+
const kind = value.startsWith('"') ? value.trimEnd().endsWith(":") ? "key" : "string" : value === "true" || value === "false" ? "boolean" : value === "null" ? "null" : "number";
|
|
1969
|
+
html += `<span class="json-${kind}">${escapeToken(value)}</span>`;
|
|
1970
|
+
cursor = index + value.length;
|
|
1971
|
+
}
|
|
1972
|
+
return html + escapeToken(text.slice(cursor));
|
|
1973
|
+
}
|
|
1904
1974
|
function mountAdminBrand(doc, host, service, catalog) {
|
|
1905
1975
|
const clip = (value, max) => {
|
|
1906
1976
|
if (typeof value !== "string")
|
|
@@ -2262,14 +2332,16 @@ function bootAdmin(config) {
|
|
|
2262
2332
|
const state = {
|
|
2263
2333
|
namespace: "default",
|
|
2264
2334
|
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
2265
|
-
view: "overview",
|
|
2335
|
+
view: ["overview", "state", "clock", "faults", "journal", "routes"].includes(location.hash.slice(1)) ? location.hash.slice(1) : "overview",
|
|
2266
2336
|
collection: "",
|
|
2267
2337
|
shape: null,
|
|
2268
2338
|
route: null,
|
|
2339
|
+
routes: [],
|
|
2269
2340
|
editing: null
|
|
2270
2341
|
};
|
|
2271
2342
|
const standard = new Set(config.standardRoutes);
|
|
2272
2343
|
const banner = el("banner", HTMLElement);
|
|
2344
|
+
const bannerMessage = el("banner-message", HTMLElement);
|
|
2273
2345
|
const keyInput = el("admin-key", HTMLInputElement);
|
|
2274
2346
|
keyInput.value = state.key;
|
|
2275
2347
|
const params = new URLSearchParams(location.search);
|
|
@@ -2280,18 +2352,21 @@ function bootAdmin(config) {
|
|
|
2280
2352
|
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
2281
2353
|
params.delete("key");
|
|
2282
2354
|
const next = params.toString();
|
|
2283
|
-
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
2355
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : "") + location.hash);
|
|
2284
2356
|
}
|
|
2285
2357
|
const queryNamespace = params.get("namespace");
|
|
2286
2358
|
if (queryNamespace)
|
|
2287
2359
|
state.namespace = queryNamespace;
|
|
2288
2360
|
const showError = (error2) => {
|
|
2289
|
-
|
|
2361
|
+
bannerMessage.textContent = error2 instanceof Error ? error2.message : String(error2);
|
|
2290
2362
|
banner.classList.add("show");
|
|
2363
|
+
banner.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
2291
2364
|
};
|
|
2292
2365
|
const clearError = () => {
|
|
2293
2366
|
banner.classList.remove("show");
|
|
2367
|
+
bannerMessage.textContent = "";
|
|
2294
2368
|
};
|
|
2369
|
+
el("banner-dismiss", HTMLButtonElement).addEventListener("click", clearError);
|
|
2295
2370
|
const api = async (method, path, body) => {
|
|
2296
2371
|
const headers = { accept: "application/json" };
|
|
2297
2372
|
if (state.key !== "")
|
|
@@ -2335,16 +2410,21 @@ function bootAdmin(config) {
|
|
|
2335
2410
|
namespace: () => state.namespace
|
|
2336
2411
|
};
|
|
2337
2412
|
const isPanelScript = (value) => typeof value === "function";
|
|
2413
|
+
const themeButton = el("theme", HTMLButtonElement);
|
|
2338
2414
|
const setTheme = (theme) => {
|
|
2339
2415
|
if (theme)
|
|
2340
2416
|
document.documentElement.dataset.theme = theme;
|
|
2341
2417
|
else
|
|
2342
2418
|
delete document.documentElement.dataset.theme;
|
|
2419
|
+
const dark = theme ? theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2420
|
+
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>';
|
|
2421
|
+
const label = dark ? "Use light theme" : "Use dark theme";
|
|
2422
|
+
themeButton.setAttribute("aria-label", label);
|
|
2423
|
+
themeButton.title = label;
|
|
2343
2424
|
};
|
|
2344
2425
|
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
2426
|
+
setTheme(storedTheme === "dark" || storedTheme === "light" ? storedTheme : null);
|
|
2427
|
+
themeButton.addEventListener("click", () => {
|
|
2348
2428
|
const current = document.documentElement.dataset.theme;
|
|
2349
2429
|
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2350
2430
|
const next = dark ? "light" : "dark";
|
|
@@ -2363,6 +2443,7 @@ function bootAdmin(config) {
|
|
|
2363
2443
|
continue;
|
|
2364
2444
|
section.hidden = section.id !== `view-${view}`;
|
|
2365
2445
|
}
|
|
2446
|
+
history.replaceState(null, "", `${location.pathname}${location.search}#${encodeURIComponent(view)}`);
|
|
2366
2447
|
};
|
|
2367
2448
|
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
2368
2449
|
const target = event.target;
|
|
@@ -2377,7 +2458,7 @@ function bootAdmin(config) {
|
|
|
2377
2458
|
show(view);
|
|
2378
2459
|
refresh().catch(showError);
|
|
2379
2460
|
});
|
|
2380
|
-
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>`;
|
|
2461
|
+
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>`;
|
|
2381
2462
|
const table = (columns, rows) => {
|
|
2382
2463
|
if (rows.length === 0)
|
|
2383
2464
|
return '<p class="empty">Nothing here yet.</p>';
|
|
@@ -2387,16 +2468,22 @@ function bootAdmin(config) {
|
|
|
2387
2468
|
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
2388
2469
|
return `<tr${attrs}>${cells}</tr>`;
|
|
2389
2470
|
}).join("");
|
|
2390
|
-
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
2471
|
+
return `<div class="table-shell"><table><thead>${head}</thead><tbody>${body}</tbody></table></div>`;
|
|
2391
2472
|
};
|
|
2392
2473
|
const preview = (value) => {
|
|
2393
2474
|
const text = JSON.stringify(value) ?? "null";
|
|
2394
|
-
|
|
2475
|
+
const clipped = text.length > 180 ? `${text.slice(0, 180)}\u2026` : text;
|
|
2476
|
+
return `<code class="json-inline">${highlightJsonText(clipped)}</code>`;
|
|
2395
2477
|
};
|
|
2396
2478
|
const when = (epoch) => {
|
|
2397
2479
|
const date = new Date(epoch);
|
|
2398
2480
|
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
2399
2481
|
};
|
|
2482
|
+
const journalWhen = (value) => {
|
|
2483
|
+
const numeric = value.trim() === "" ? Number.NaN : Number(value);
|
|
2484
|
+
const epoch = Number.isFinite(numeric) ? numeric : Date.parse(value);
|
|
2485
|
+
return Number.isFinite(epoch) ? when(epoch) : value;
|
|
2486
|
+
};
|
|
2400
2487
|
const clockCard = (epoch) => {
|
|
2401
2488
|
const date = new Date(epoch);
|
|
2402
2489
|
if (Number.isNaN(date.getTime()))
|
|
@@ -2516,7 +2603,7 @@ function bootAdmin(config) {
|
|
|
2516
2603
|
};
|
|
2517
2604
|
const renderOverview = async () => {
|
|
2518
2605
|
const [health, shape, clock, journal] = await Promise.all([
|
|
2519
|
-
originGet(
|
|
2606
|
+
originGet(`${config.adminPrefix}/health`),
|
|
2520
2607
|
api("GET", "/state"),
|
|
2521
2608
|
api("GET", "/clock"),
|
|
2522
2609
|
api("GET", "/requests?limit=8")
|
|
@@ -2533,7 +2620,7 @@ function bootAdmin(config) {
|
|
|
2533
2620
|
},
|
|
2534
2621
|
{
|
|
2535
2622
|
k: "Status",
|
|
2536
|
-
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
2623
|
+
v: `<span class="badge ok">${esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")}</span>`
|
|
2537
2624
|
},
|
|
2538
2625
|
{ k: "Namespace", v: esc(state.namespace) },
|
|
2539
2626
|
{ k: "Collections", v: String(decoded.collections.length) },
|
|
@@ -2620,26 +2707,46 @@ function bootAdmin(config) {
|
|
|
2620
2707
|
const renderJournal = async () => {
|
|
2621
2708
|
const journal = await api("GET", "/requests?limit=100");
|
|
2622
2709
|
el("journal", HTMLElement).innerHTML = table([
|
|
2623
|
-
{ label: "When", cell: (row) => esc(row.at) },
|
|
2624
|
-
{
|
|
2710
|
+
{ label: "When", cell: (row) => esc(journalWhen(row.at)) },
|
|
2711
|
+
{
|
|
2712
|
+
label: "Method",
|
|
2713
|
+
cell: (row) => `<span class="method">${esc(row.method)}</span>`
|
|
2714
|
+
},
|
|
2625
2715
|
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2626
|
-
{
|
|
2716
|
+
{
|
|
2717
|
+
label: "Status",
|
|
2718
|
+
cell: (row) => `<span class="badge ${row.status.startsWith("2") ? "ok" : row.status.startsWith("4") || row.status.startsWith("5") ? "warn" : ""}">${esc(row.status)}</span>`
|
|
2719
|
+
},
|
|
2627
2720
|
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
2628
2721
|
], decodeJournal(journal));
|
|
2629
2722
|
};
|
|
2630
|
-
const
|
|
2631
|
-
const
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2723
|
+
const paintRoutes = () => {
|
|
2724
|
+
const query = el("route-search", HTMLInputElement).value.trim().toLocaleLowerCase();
|
|
2725
|
+
const routes = state.routes.filter((route) => route.toLocaleLowerCase().includes(query));
|
|
2726
|
+
el("route-count", HTMLElement).textContent = `${routes.length} of ${state.routes.length}`;
|
|
2727
|
+
if (state.routes.length === 0) {
|
|
2728
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No admin routes are available.</p>';
|
|
2729
|
+
return;
|
|
2730
|
+
}
|
|
2731
|
+
if (routes.length === 0) {
|
|
2732
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No routes match this filter.</p>';
|
|
2733
|
+
return;
|
|
2734
|
+
}
|
|
2635
2735
|
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2636
2736
|
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2637
2737
|
const parts = route.split(" ");
|
|
2638
2738
|
const method = parts[0] ?? "";
|
|
2639
2739
|
const path = parts.slice(1).join(" ");
|
|
2640
|
-
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>`;
|
|
2740
|
+
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>`;
|
|
2641
2741
|
}).join("");
|
|
2642
2742
|
};
|
|
2743
|
+
const renderRoutes = async () => {
|
|
2744
|
+
const data = await api("GET", "/");
|
|
2745
|
+
if (!isRecord2(data))
|
|
2746
|
+
throw new Error("Route list was not an object");
|
|
2747
|
+
state.routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
2748
|
+
paintRoutes();
|
|
2749
|
+
};
|
|
2643
2750
|
const renderers = {
|
|
2644
2751
|
overview: renderOverview,
|
|
2645
2752
|
state: renderState,
|
|
@@ -2648,19 +2755,50 @@ function bootAdmin(config) {
|
|
|
2648
2755
|
journal: renderJournal,
|
|
2649
2756
|
routes: renderRoutes
|
|
2650
2757
|
};
|
|
2758
|
+
let refreshToken = 0;
|
|
2651
2759
|
async function refresh() {
|
|
2760
|
+
const token = ++refreshToken;
|
|
2652
2761
|
clearError();
|
|
2762
|
+
const main = el("views", HTMLElement);
|
|
2763
|
+
const sync = el("sync-status", HTMLElement);
|
|
2764
|
+
main.setAttribute("aria-busy", "true");
|
|
2765
|
+
sync.classList.add("busy");
|
|
2766
|
+
sync.textContent = "Updating";
|
|
2653
2767
|
const render = renderers[state.view];
|
|
2654
|
-
|
|
2655
|
-
|
|
2768
|
+
try {
|
|
2769
|
+
if (render)
|
|
2770
|
+
await render();
|
|
2771
|
+
if (token === refreshToken)
|
|
2772
|
+
sync.textContent = "Up to date";
|
|
2773
|
+
} finally {
|
|
2774
|
+
if (token === refreshToken) {
|
|
2775
|
+
main.removeAttribute("aria-busy");
|
|
2776
|
+
sync.classList.remove("busy");
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2656
2779
|
}
|
|
2657
2780
|
const delay = (ms) => new Promise((resolve) => {
|
|
2658
2781
|
window.setTimeout(resolve, ms);
|
|
2659
2782
|
});
|
|
2660
|
-
const confirmAction = async (button, pending, done, work) => {
|
|
2783
|
+
const confirmAction = async (button, pending, done, work, confirmLabel) => {
|
|
2661
2784
|
if (button.dataset.pending === "1")
|
|
2662
2785
|
return;
|
|
2663
2786
|
const label = button.textContent ?? "";
|
|
2787
|
+
if (confirmLabel !== void 0 && button.dataset.confirming !== "1") {
|
|
2788
|
+
button.dataset.confirming = "1";
|
|
2789
|
+
button.classList.add("confirming");
|
|
2790
|
+
button.textContent = confirmLabel;
|
|
2791
|
+
window.setTimeout(() => {
|
|
2792
|
+
if (button.dataset.confirming !== "1")
|
|
2793
|
+
return;
|
|
2794
|
+
button.textContent = label;
|
|
2795
|
+
button.classList.remove("confirming");
|
|
2796
|
+
delete button.dataset.confirming;
|
|
2797
|
+
}, 3e3);
|
|
2798
|
+
return;
|
|
2799
|
+
}
|
|
2800
|
+
button.classList.remove("confirming");
|
|
2801
|
+
delete button.dataset.confirming;
|
|
2664
2802
|
const width = button.offsetWidth;
|
|
2665
2803
|
button.dataset.pending = "1";
|
|
2666
2804
|
button.classList.add("pressed");
|
|
@@ -2681,6 +2819,65 @@ function bootAdmin(config) {
|
|
|
2681
2819
|
delete button.dataset.pending;
|
|
2682
2820
|
}
|
|
2683
2821
|
};
|
|
2822
|
+
const wireJsonEditor = (textareaId, highlightId, statusId, shellId, allowEmpty) => {
|
|
2823
|
+
const textarea = el(textareaId, HTMLTextAreaElement);
|
|
2824
|
+
const highlight = el(highlightId, HTMLElement);
|
|
2825
|
+
const status = el(statusId, HTMLElement);
|
|
2826
|
+
const shell = el(shellId, HTMLElement);
|
|
2827
|
+
const sync = () => {
|
|
2828
|
+
const text = textarea.value;
|
|
2829
|
+
highlight.innerHTML = `${highlightJsonText(text)}
|
|
2830
|
+
`;
|
|
2831
|
+
let valid = true;
|
|
2832
|
+
let message = "Valid JSON";
|
|
2833
|
+
if (allowEmpty && text.trim() === "")
|
|
2834
|
+
message = "No body";
|
|
2835
|
+
else {
|
|
2836
|
+
try {
|
|
2837
|
+
readJson2(text);
|
|
2838
|
+
} catch {
|
|
2839
|
+
valid = false;
|
|
2840
|
+
message = "Invalid JSON";
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
shell.classList.toggle("invalid", !valid);
|
|
2844
|
+
status.classList.toggle("invalid", !valid);
|
|
2845
|
+
status.textContent = message;
|
|
2846
|
+
textarea.setAttribute("aria-invalid", valid ? "false" : "true");
|
|
2847
|
+
};
|
|
2848
|
+
const syncScroll = () => {
|
|
2849
|
+
const preview2 = highlight.parentElement;
|
|
2850
|
+
if (!(preview2 instanceof HTMLElement))
|
|
2851
|
+
return;
|
|
2852
|
+
preview2.scrollTop = textarea.scrollTop;
|
|
2853
|
+
preview2.scrollLeft = textarea.scrollLeft;
|
|
2854
|
+
};
|
|
2855
|
+
textarea.addEventListener("input", sync);
|
|
2856
|
+
textarea.addEventListener("scroll", syncScroll);
|
|
2857
|
+
sync();
|
|
2858
|
+
return sync;
|
|
2859
|
+
};
|
|
2860
|
+
const syncRouteEditor = wireJsonEditor("route-body", "route-highlight", "route-json-status", "route-editor", true);
|
|
2861
|
+
const syncRecordEditor = wireJsonEditor("editor-value", "editor-highlight", "editor-json-status", "record-editor", false);
|
|
2862
|
+
for (const button of document.querySelectorAll("[data-format]")) {
|
|
2863
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2864
|
+
continue;
|
|
2865
|
+
button.addEventListener("click", () => {
|
|
2866
|
+
const target = button.dataset.format;
|
|
2867
|
+
if (target === void 0)
|
|
2868
|
+
return;
|
|
2869
|
+
const textarea = document.getElementById(target);
|
|
2870
|
+
if (!(textarea instanceof HTMLTextAreaElement) || textarea.value.trim() === "")
|
|
2871
|
+
return;
|
|
2872
|
+
try {
|
|
2873
|
+
textarea.value = JSON.stringify(readJson2(textarea.value), null, 2) ?? "";
|
|
2874
|
+
textarea.dispatchEvent(new Event("input"));
|
|
2875
|
+
textarea.focus();
|
|
2876
|
+
} catch {
|
|
2877
|
+
showError(new Error("Fix the JSON before formatting it"));
|
|
2878
|
+
}
|
|
2879
|
+
});
|
|
2880
|
+
}
|
|
2684
2881
|
document.addEventListener("click", (event) => {
|
|
2685
2882
|
const target = event.target;
|
|
2686
2883
|
if (!(target instanceof Element))
|
|
@@ -2753,6 +2950,7 @@ function bootAdmin(config) {
|
|
|
2753
2950
|
} else {
|
|
2754
2951
|
editorValue.value = "{\n \n}";
|
|
2755
2952
|
}
|
|
2953
|
+
syncRecordEditor();
|
|
2756
2954
|
editor.showModal();
|
|
2757
2955
|
}
|
|
2758
2956
|
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -2782,10 +2980,10 @@ function bootAdmin(config) {
|
|
|
2782
2980
|
const id = state.editing;
|
|
2783
2981
|
if (!id)
|
|
2784
2982
|
return;
|
|
2785
|
-
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2983
|
+
confirmAction(editorDelete, "Deleting...", "Deleted", () => api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2786
2984
|
editor.close();
|
|
2787
2985
|
return renderState();
|
|
2788
|
-
}).catch(showError);
|
|
2986
|
+
}), "Confirm delete").catch(showError);
|
|
2789
2987
|
});
|
|
2790
2988
|
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2791
2989
|
if (!(button instanceof HTMLButtonElement))
|
|
@@ -2831,10 +3029,10 @@ function bootAdmin(config) {
|
|
|
2831
3029
|
}).then(() => renderFaults()).catch(showError);
|
|
2832
3030
|
});
|
|
2833
3031
|
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2834
|
-
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
|
|
3032
|
+
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults()), "Confirm clear").catch(showError);
|
|
2835
3033
|
});
|
|
2836
3034
|
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2837
|
-
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
|
|
3035
|
+
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal()), "Confirm clear").catch(showError);
|
|
2838
3036
|
});
|
|
2839
3037
|
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2840
3038
|
const target = event.target;
|
|
@@ -2848,9 +3046,15 @@ function bootAdmin(config) {
|
|
|
2848
3046
|
return;
|
|
2849
3047
|
state.route = route;
|
|
2850
3048
|
el("route-title", HTMLElement).textContent = route;
|
|
3049
|
+
el("route-send", HTMLButtonElement).disabled = false;
|
|
2851
3050
|
const method = route.split(" ")[0] ?? "";
|
|
2852
|
-
el("route-body", HTMLTextAreaElement)
|
|
3051
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
3052
|
+
routeBody.disabled = method === "GET" || method === "DELETE";
|
|
3053
|
+
el("route-editor", HTMLElement).hidden = routeBody.disabled;
|
|
3054
|
+
el("route-json-status", HTMLElement).parentElement?.toggleAttribute("hidden", routeBody.disabled);
|
|
3055
|
+
syncRouteEditor();
|
|
2853
3056
|
});
|
|
3057
|
+
el("route-search", HTMLInputElement).addEventListener("input", paintRoutes);
|
|
2854
3058
|
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2855
3059
|
event.preventDefault();
|
|
2856
3060
|
const send = async () => {
|
|
@@ -2872,7 +3076,8 @@ function bootAdmin(config) {
|
|
|
2872
3076
|
}
|
|
2873
3077
|
}
|
|
2874
3078
|
const data = await api(method, concrete, body);
|
|
2875
|
-
|
|
3079
|
+
const text = JSON.stringify(data, null, 2) ?? "null";
|
|
3080
|
+
el("route-result", HTMLElement).innerHTML = `<code>${highlightJsonText(text)}</code>`;
|
|
2876
3081
|
};
|
|
2877
3082
|
send().catch(showError);
|
|
2878
3083
|
});
|
|
@@ -2990,11 +3195,13 @@ function bootAdmin(config) {
|
|
|
2990
3195
|
views.append(section);
|
|
2991
3196
|
mountSqlExplorer(body, panelApi);
|
|
2992
3197
|
}
|
|
3198
|
+
show(document.getElementById(`view-${state.view}`) === null ? "overview" : state.view);
|
|
2993
3199
|
return refresh();
|
|
2994
3200
|
}).catch(showError);
|
|
2995
3201
|
}
|
|
2996
3202
|
var clientPrelude = [
|
|
2997
3203
|
isRecord2,
|
|
3204
|
+
highlightJsonText,
|
|
2998
3205
|
mountAdminBrand,
|
|
2999
3206
|
startAdminBrand,
|
|
3000
3207
|
faultPresetList,
|
|
@@ -3017,6 +3224,7 @@ ${entry}(${payload});
|
|
|
3017
3224
|
var defineMock = (runtime) => runtime;
|
|
3018
3225
|
var STANDARD_ADMIN_ROUTES = [
|
|
3019
3226
|
"GET /",
|
|
3227
|
+
"GET /health",
|
|
3020
3228
|
"POST /reset",
|
|
3021
3229
|
"GET /namespaces",
|
|
3022
3230
|
"GET /clock",
|
|
@@ -3096,11 +3304,11 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
3096
3304
|
}
|
|
3097
3305
|
});
|
|
3098
3306
|
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
3099
|
-
var renderAdminDocument = (service) => {
|
|
3307
|
+
var renderAdminDocument = (service, adminPrefix = ADMIN_PREFIX) => {
|
|
3100
3308
|
const name = escapeHtml(service);
|
|
3101
3309
|
const script = adminClientSource({
|
|
3102
3310
|
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
3103
|
-
adminPrefix
|
|
3311
|
+
adminPrefix,
|
|
3104
3312
|
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
3105
3313
|
brandsUrl: ADMIN_BRANDS_URL,
|
|
3106
3314
|
service
|
|
@@ -3117,75 +3325,99 @@ var renderAdminDocument = (service) => {
|
|
|
3117
3325
|
${CSS_RESET}
|
|
3118
3326
|
:root {
|
|
3119
3327
|
color-scheme: light;
|
|
3120
|
-
--bg: #
|
|
3328
|
+
--bg: #f7f8fb;
|
|
3121
3329
|
--bg-elev: #ffffff;
|
|
3122
|
-
--bg-muted: #
|
|
3123
|
-
--bg-inset: #
|
|
3124
|
-
--line: #
|
|
3125
|
-
--line-strong: #
|
|
3126
|
-
--ink: #
|
|
3127
|
-
--muted: #
|
|
3128
|
-
--faint: #
|
|
3129
|
-
--accent: #
|
|
3330
|
+
--bg-muted: #f1f3f7;
|
|
3331
|
+
--bg-inset: #f8f9fc;
|
|
3332
|
+
--line: #e4e7ec;
|
|
3333
|
+
--line-strong: #cfd4dc;
|
|
3334
|
+
--ink: #101828;
|
|
3335
|
+
--muted: #475467;
|
|
3336
|
+
--faint: #667085;
|
|
3337
|
+
--accent: #6941c6;
|
|
3338
|
+
--accent-hover: #5933b4;
|
|
3130
3339
|
--accent-ink: #ffffff;
|
|
3131
|
-
--accent-soft: #
|
|
3132
|
-
--ok: #
|
|
3133
|
-
--ok-soft: #
|
|
3134
|
-
--warn: #
|
|
3135
|
-
--warn-soft: #
|
|
3136
|
-
--err: #
|
|
3137
|
-
--err-soft: #
|
|
3138
|
-
--
|
|
3340
|
+
--accent-soft: #f4f0ff;
|
|
3341
|
+
--ok: #067647;
|
|
3342
|
+
--ok-soft: #ecfdf3;
|
|
3343
|
+
--warn: #b54708;
|
|
3344
|
+
--warn-soft: #fffaeb;
|
|
3345
|
+
--err: #b42318;
|
|
3346
|
+
--err-soft: #fef3f2;
|
|
3347
|
+
--code-key: #6941c6;
|
|
3348
|
+
--code-string: #067647;
|
|
3349
|
+
--code-number: #175cd3;
|
|
3350
|
+
--code-boolean: #c11574;
|
|
3351
|
+
--code-null: #667085;
|
|
3352
|
+
--shadow-xs: 0 1px 2px rgb(16 24 40 / 0.05);
|
|
3353
|
+
--shadow-sm: 0 1px 3px rgb(16 24 40 / 0.08), 0 1px 2px rgb(16 24 40 / 0.04);
|
|
3354
|
+
--shadow-lg: 0 18px 48px rgb(16 24 40 / 0.16), 0 4px 12px rgb(16 24 40 / 0.08);
|
|
3139
3355
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
3140
3356
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
3141
|
-
--radius:
|
|
3142
|
-
--header:
|
|
3143
|
-
--nav:
|
|
3357
|
+
--radius: 12px;
|
|
3358
|
+
--header: 72px;
|
|
3359
|
+
--nav: 224px;
|
|
3144
3360
|
}
|
|
3145
3361
|
:root[data-theme="dark"] {
|
|
3146
3362
|
color-scheme: dark;
|
|
3147
|
-
--bg: #
|
|
3148
|
-
--bg-elev: #
|
|
3149
|
-
--bg-muted: #
|
|
3150
|
-
--bg-inset: #
|
|
3151
|
-
--line: #
|
|
3152
|
-
--line-strong: #
|
|
3153
|
-
--ink: #
|
|
3154
|
-
--muted: #
|
|
3155
|
-
--faint: #
|
|
3156
|
-
--accent: #
|
|
3157
|
-
--accent-
|
|
3158
|
-
--accent-
|
|
3159
|
-
--
|
|
3160
|
-
--ok
|
|
3161
|
-
--
|
|
3162
|
-
--warn
|
|
3163
|
-
--
|
|
3164
|
-
--err
|
|
3165
|
-
--
|
|
3363
|
+
--bg: #0c0e14;
|
|
3364
|
+
--bg-elev: #151821;
|
|
3365
|
+
--bg-muted: #20232e;
|
|
3366
|
+
--bg-inset: #11141c;
|
|
3367
|
+
--line: #2b303d;
|
|
3368
|
+
--line-strong: #414858;
|
|
3369
|
+
--ink: #f5f7fa;
|
|
3370
|
+
--muted: #b6bdc9;
|
|
3371
|
+
--faint: #8d96a7;
|
|
3372
|
+
--accent: #9e77ed;
|
|
3373
|
+
--accent-hover: #b692f6;
|
|
3374
|
+
--accent-ink: #ffffff;
|
|
3375
|
+
--accent-soft: #2b2142;
|
|
3376
|
+
--ok: #75e0a7;
|
|
3377
|
+
--ok-soft: #102a20;
|
|
3378
|
+
--warn: #fec84b;
|
|
3379
|
+
--warn-soft: #302611;
|
|
3380
|
+
--err: #fda29b;
|
|
3381
|
+
--err-soft: #321817;
|
|
3382
|
+
--code-key: #c3a6ff;
|
|
3383
|
+
--code-string: #75e0a7;
|
|
3384
|
+
--code-number: #84adff;
|
|
3385
|
+
--code-boolean: #f9a8d4;
|
|
3386
|
+
--code-null: #98a2b3;
|
|
3387
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3388
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3389
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
3166
3390
|
}
|
|
3167
3391
|
@media (prefers-color-scheme: dark) {
|
|
3168
3392
|
:root:not([data-theme="light"]) {
|
|
3169
3393
|
color-scheme: dark;
|
|
3170
|
-
--bg: #
|
|
3171
|
-
--bg-elev: #
|
|
3172
|
-
--bg-muted: #
|
|
3173
|
-
--bg-inset: #
|
|
3174
|
-
--line: #
|
|
3175
|
-
--line-strong: #
|
|
3176
|
-
--ink: #
|
|
3177
|
-
--muted: #
|
|
3178
|
-
--faint: #
|
|
3179
|
-
--accent: #
|
|
3180
|
-
--accent-
|
|
3181
|
-
--accent-
|
|
3182
|
-
--
|
|
3183
|
-
--ok
|
|
3184
|
-
--
|
|
3185
|
-
--warn
|
|
3186
|
-
--
|
|
3187
|
-
--err
|
|
3188
|
-
--
|
|
3394
|
+
--bg: #0c0e14;
|
|
3395
|
+
--bg-elev: #151821;
|
|
3396
|
+
--bg-muted: #20232e;
|
|
3397
|
+
--bg-inset: #11141c;
|
|
3398
|
+
--line: #2b303d;
|
|
3399
|
+
--line-strong: #414858;
|
|
3400
|
+
--ink: #f5f7fa;
|
|
3401
|
+
--muted: #b6bdc9;
|
|
3402
|
+
--faint: #8d96a7;
|
|
3403
|
+
--accent: #9e77ed;
|
|
3404
|
+
--accent-hover: #b692f6;
|
|
3405
|
+
--accent-ink: #ffffff;
|
|
3406
|
+
--accent-soft: #2b2142;
|
|
3407
|
+
--ok: #75e0a7;
|
|
3408
|
+
--ok-soft: #102a20;
|
|
3409
|
+
--warn: #fec84b;
|
|
3410
|
+
--warn-soft: #302611;
|
|
3411
|
+
--err: #fda29b;
|
|
3412
|
+
--err-soft: #321817;
|
|
3413
|
+
--code-key: #c3a6ff;
|
|
3414
|
+
--code-string: #75e0a7;
|
|
3415
|
+
--code-number: #84adff;
|
|
3416
|
+
--code-boolean: #f9a8d4;
|
|
3417
|
+
--code-null: #98a2b3;
|
|
3418
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3419
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3420
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
3189
3421
|
}
|
|
3190
3422
|
}
|
|
3191
3423
|
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
@@ -3203,13 +3435,17 @@ var renderAdminDocument = (service) => {
|
|
|
3203
3435
|
padding-right: env(safe-area-inset-right);
|
|
3204
3436
|
-webkit-text-size-adjust: 100%;
|
|
3205
3437
|
}
|
|
3206
|
-
|
|
3438
|
+
::selection { background: color-mix(in srgb, var(--accent) 28%, transparent); }
|
|
3439
|
+
:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px; }
|
|
3440
|
+
button, input, select, textarea { transition: border-color 140ms ease, box-shadow 140ms ease, transform 100ms ease; }
|
|
3441
|
+
button:disabled, input:disabled, select:disabled, textarea:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
3207
3442
|
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
3208
3443
|
header.top {
|
|
3209
|
-
position: sticky; top: 0; z-index:
|
|
3444
|
+
position: sticky; top: 0; z-index: 20;
|
|
3210
3445
|
display: flex; align-items: center; gap: 12px;
|
|
3211
|
-
min-height: var(--header); padding:
|
|
3212
|
-
background: var(--bg-elev);
|
|
3446
|
+
min-height: var(--header); padding: 12px 20px;
|
|
3447
|
+
background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
|
|
3448
|
+
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
3213
3449
|
border-bottom: 1px solid var(--line);
|
|
3214
3450
|
min-width: 0; max-width: 100%;
|
|
3215
3451
|
}
|
|
@@ -3217,12 +3453,21 @@ var renderAdminDocument = (service) => {
|
|
|
3217
3453
|
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
3218
3454
|
min-width: 0; max-width: 100%; margin-right: auto;
|
|
3219
3455
|
}
|
|
3220
|
-
.brand-
|
|
3221
|
-
.brand-
|
|
3456
|
+
.brand-lockup { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
3457
|
+
.brand-mark {
|
|
3458
|
+
width: 38px; height: 38px; display: grid; place-items: center; flex: none;
|
|
3459
|
+
border-radius: 11px; color: #fff;
|
|
3460
|
+
background: linear-gradient(145deg, #7f56d9 0%, #53389e 100%);
|
|
3461
|
+
box-shadow: 0 6px 16px rgb(105 65 198 / 0.24), inset 0 1px 0 rgb(255 255 255 / 0.25);
|
|
3462
|
+
}
|
|
3463
|
+
.brand-mark svg { width: 22px; height: 22px; }
|
|
3464
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; line-height: 1.25; }
|
|
3465
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
3222
3466
|
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
3223
3467
|
.vendor {
|
|
3224
3468
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
3225
3469
|
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
3470
|
+
padding: 5px 8px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-inset);
|
|
3226
3471
|
}
|
|
3227
3472
|
.vendor[hidden] { display: none; }
|
|
3228
3473
|
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
@@ -3234,12 +3479,17 @@ var renderAdminDocument = (service) => {
|
|
|
3234
3479
|
.vendor-sep { color: var(--faint); }
|
|
3235
3480
|
.vendor-link { color: inherit; text-decoration: none; }
|
|
3236
3481
|
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
3237
|
-
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items:
|
|
3238
|
-
|
|
3482
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
|
|
3483
|
+
.sync-status { align-self: center; display: flex; align-items: center; gap: 6px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3484
|
+
.sync-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
|
|
3485
|
+
.sync-status.busy::before { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); animation: pulse 1.2s ease-in-out infinite; }
|
|
3486
|
+
label.field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; font-weight: 600; color: var(--faint); }
|
|
3239
3487
|
label.field input, label.field select, .controls input[type="password"] {
|
|
3240
|
-
background-color: var(--bg-
|
|
3241
|
-
padding:
|
|
3488
|
+
background-color: var(--bg-elev); border: 1px solid var(--line-strong); border-radius: 9px;
|
|
3489
|
+
padding: 8px 10px; min-height: 38px; line-height: 1.2; box-shadow: var(--shadow-xs);
|
|
3242
3490
|
}
|
|
3491
|
+
label.field input:hover, label.field select:hover, textarea:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--line-strong)); }
|
|
3492
|
+
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; }
|
|
3243
3493
|
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
3244
3494
|
the text fields in the same form. Paint it like those fields. */
|
|
3245
3495
|
label.field select {
|
|
@@ -3256,12 +3506,20 @@ var renderAdminDocument = (service) => {
|
|
|
3256
3506
|
dialog .field > textarea { width: 100%; }
|
|
3257
3507
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
3258
3508
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
.btn
|
|
3509
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
|
3510
|
+
border-radius: 9px; min-height: 38px; padding: 0 13px; border: 1px solid var(--line-strong);
|
|
3511
|
+
background: var(--bg-elev); color: var(--ink); font-size: 13px; font-weight: 600; line-height: 1;
|
|
3512
|
+
box-shadow: var(--shadow-xs);
|
|
3513
|
+
}
|
|
3514
|
+
.icon-btn { width: 38px; padding: 0; }
|
|
3515
|
+
.icon-btn svg, .btn svg, .btn-primary svg { width: 16px; height: 16px; }
|
|
3516
|
+
.icon-btn:hover, .btn:hover { background: var(--bg-muted); border-color: var(--line-strong); }
|
|
3517
|
+
.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; }
|
|
3518
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
3519
|
+
.btn-quiet { background: transparent; border-color: transparent; box-shadow: none; }
|
|
3520
|
+
.btn-quiet:hover { background: var(--bg-muted); }
|
|
3521
|
+
.btn-danger { color: var(--err); border-color: color-mix(in srgb, var(--err) 25%, var(--line)); }
|
|
3522
|
+
.btn-danger:hover, .btn-danger.confirming { background: var(--err-soft); border-color: color-mix(in srgb, var(--err) 45%, var(--line)); }
|
|
3265
3523
|
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
3266
3524
|
so the press itself has to stay visible. */
|
|
3267
3525
|
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
@@ -3277,93 +3535,159 @@ var renderAdminDocument = (service) => {
|
|
|
3277
3535
|
flex: 1 0 auto; min-width: 0;
|
|
3278
3536
|
}
|
|
3279
3537
|
nav.side {
|
|
3280
|
-
|
|
3538
|
+
position: sticky; top: var(--header); align-self: start; height: calc(100vh - var(--header));
|
|
3539
|
+
border-right: 1px solid var(--line); padding: 18px 12px;
|
|
3281
3540
|
display: flex; flex-direction: column; gap: 4px;
|
|
3282
|
-
background: var(--bg-elev);
|
|
3541
|
+
background: var(--bg-elev); overflow-y: auto;
|
|
3283
3542
|
}
|
|
3543
|
+
.nav-label { padding: 0 10px 8px; color: var(--faint); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
|
|
3284
3544
|
nav.side button {
|
|
3545
|
+
position: relative; display: flex; align-items: center; gap: 10px; width: 100%;
|
|
3285
3546
|
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
3286
|
-
padding: 9px 10px; min-height:
|
|
3287
|
-
}
|
|
3288
|
-
nav.side button
|
|
3289
|
-
|
|
3290
|
-
.
|
|
3291
|
-
.
|
|
3292
|
-
.
|
|
3547
|
+
padding: 9px 10px; min-height: 42px; color: var(--muted); font-size: 13px; font-weight: 550;
|
|
3548
|
+
}
|
|
3549
|
+
nav.side button:hover { background: var(--bg-muted); color: var(--ink); }
|
|
3550
|
+
nav.side button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 650; }
|
|
3551
|
+
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); }
|
|
3552
|
+
.nav-icon { width: 18px; height: 18px; display: grid; place-items: center; flex: none; }
|
|
3553
|
+
.nav-icon svg { width: 18px; height: 18px; }
|
|
3554
|
+
main { padding: 32px clamp(20px, 4vw, 48px) 64px; max-width: 1320px; width: 100%; min-width: 0; }
|
|
3555
|
+
main[aria-busy="true"] .view { opacity: 0.62; }
|
|
3556
|
+
.view { animation: view-in 180ms ease-out; }
|
|
3557
|
+
.view[hidden] { display: none; }
|
|
3558
|
+
.view-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
|
|
3559
|
+
.view h2 { margin: 0 0 5px; font-size: clamp(24px, 3vw, 30px); line-height: 1.2; letter-spacing: -0.035em; }
|
|
3560
|
+
.view h3 { margin: 24px 0 10px; font-size: 15px; letter-spacing: -0.01em; }
|
|
3561
|
+
.lede { margin: 0; color: var(--muted); max-width: 72ch; font-size: 14px; }
|
|
3562
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 175px), 1fr)); gap: 14px; }
|
|
3293
3563
|
.card {
|
|
3294
3564
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
3295
|
-
padding:
|
|
3565
|
+
padding: 18px; box-shadow: var(--shadow-sm);
|
|
3296
3566
|
}
|
|
3297
|
-
.card
|
|
3298
|
-
.card
|
|
3567
|
+
.stat-card { position: relative; overflow: hidden; }
|
|
3568
|
+
.stat-card::after { content: ""; position: absolute; inset: 0 0 auto; height: 2px; background: linear-gradient(90deg, var(--accent), transparent 78%); opacity: 0.7; }
|
|
3569
|
+
.card .k { color: var(--faint); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
3570
|
+
.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; }
|
|
3299
3571
|
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
3300
|
-
.
|
|
3572
|
+
.section-card { background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-xs); overflow: hidden; }
|
|
3573
|
+
.section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
|
|
3574
|
+
.section-title h3 { margin: 0; }
|
|
3575
|
+
.split { display: grid; grid-template-columns: minmax(0, 250px) minmax(0, 1fr); gap: 18px; align-items: start; }
|
|
3576
|
+
#collections { padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); box-shadow: var(--shadow-xs); }
|
|
3301
3577
|
.collection {
|
|
3302
3578
|
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
3303
|
-
border-radius:
|
|
3579
|
+
border-radius: 9px; padding: 10px 11px; min-height: 48px; color: var(--ink); cursor: pointer;
|
|
3304
3580
|
}
|
|
3305
|
-
.collection
|
|
3581
|
+
.collection:hover { background: var(--bg-muted); }
|
|
3582
|
+
.collection[aria-current="true"] { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 18%, var(--line)); color: var(--accent); }
|
|
3306
3583
|
.collection small { display: block; color: var(--faint); }
|
|
3307
3584
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
3308
|
-
.chip {
|
|
3309
|
-
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
3310
|
-
border-radius: 999px; padding: 4px 8px;
|
|
3311
|
-
}
|
|
3585
|
+
.chip { font-family: var(--mono); font-size: 11px; background: var(--bg-muted); border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; }
|
|
3312
3586
|
.chip i { color: var(--faint); font-style: normal; }
|
|
3313
3587
|
.sql-layout {
|
|
3314
3588
|
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
3315
3589
|
}
|
|
3316
|
-
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
3317
|
-
.sql-tables button { text-align: left; }
|
|
3590
|
+
.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); }
|
|
3591
|
+
.sql-tables button { text-align: left; border: 0; border-radius: 8px; padding: 9px 10px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
3592
|
+
.sql-tables button:hover, .sql-tables button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); }
|
|
3318
3593
|
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
3319
|
-
table { width: 100%; border-
|
|
3320
|
-
|
|
3321
|
-
th {
|
|
3594
|
+
.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); }
|
|
3595
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
3596
|
+
th, td { text-align: left; padding: 11px 13px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
3597
|
+
th { color: var(--faint); background: var(--bg-inset); font-weight: 650; font-size: 11px; text-transform: uppercase; letter-spacing: 0.045em; }
|
|
3598
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
3322
3599
|
tr.clickable { cursor: pointer; }
|
|
3323
|
-
tr.clickable:hover { background: var(--
|
|
3600
|
+
tr.clickable:hover { background: var(--accent-soft); }
|
|
3324
3601
|
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
3325
3602
|
pre {
|
|
3326
3603
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
3327
|
-
background: var(--bg-inset); border-radius: 10px; padding:
|
|
3328
|
-
}
|
|
3329
|
-
.
|
|
3604
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 10px; padding: 14px; font-size: 12px; line-height: 1.65;
|
|
3605
|
+
}
|
|
3606
|
+
.json-key { color: var(--code-key); }
|
|
3607
|
+
.json-string { color: var(--code-string); }
|
|
3608
|
+
.json-number { color: var(--code-number); }
|
|
3609
|
+
.json-boolean { color: var(--code-boolean); }
|
|
3610
|
+
.json-null { color: var(--code-null); font-style: italic; }
|
|
3611
|
+
.json-inline { display: block; max-width: 44rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
3612
|
+
.json-view { position: relative; max-height: 26rem; overflow: auto; }
|
|
3613
|
+
.json-editor { position: relative; min-height: 190px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--bg-inset); overflow: hidden; }
|
|
3614
|
+
.json-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); }
|
|
3615
|
+
.json-editor.invalid { border-color: var(--err); }
|
|
3616
|
+
.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; }
|
|
3617
|
+
.json-editor pre { position: absolute; inset: 0; pointer-events: none; background: transparent; }
|
|
3618
|
+
.json-editor textarea { position: relative; z-index: 1; resize: vertical; background: transparent; color: transparent; caret-color: var(--ink); -webkit-text-fill-color: transparent; }
|
|
3619
|
+
.json-editor textarea::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); }
|
|
3620
|
+
.editor-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; }
|
|
3621
|
+
.json-status { display: inline-flex; align-items: center; gap: 6px; color: var(--ok); font-size: 11px; }
|
|
3622
|
+
.json-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
3623
|
+
.json-status.invalid { color: var(--err); }
|
|
3624
|
+
.format-json { border: 0; padding: 3px 5px; background: transparent; color: var(--accent); font-size: 11px; font-weight: 650; cursor: pointer; }
|
|
3625
|
+
.row-actions { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 14px 0; }
|
|
3330
3626
|
.banner {
|
|
3331
|
-
display: none;
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3627
|
+
display: none; position: sticky; top: calc(var(--header) + 12px); z-index: 15;
|
|
3628
|
+
align-items: center; gap: 10px; margin: 0 0 16px; padding: 11px 12px 11px 14px;
|
|
3629
|
+
border: 1px solid color-mix(in srgb, var(--err) 30%, var(--line)); border-radius: 10px;
|
|
3630
|
+
background: var(--err-soft); color: var(--err); box-shadow: var(--shadow-sm); font-size: 13px;
|
|
3631
|
+
}
|
|
3632
|
+
.banner.show { display: flex; }
|
|
3633
|
+
.banner span { flex: 1; }
|
|
3634
|
+
.banner button { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 7px; background: transparent; color: currentColor; cursor: pointer; }
|
|
3635
|
+
.banner button:hover { background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
3636
|
+
.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); }
|
|
3336
3637
|
dialog {
|
|
3337
|
-
border: 1px solid var(--line); border-radius:
|
|
3338
|
-
color: var(--ink); width: min(
|
|
3339
|
-
}
|
|
3340
|
-
dialog
|
|
3341
|
-
dialog
|
|
3638
|
+
border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--bg-elev);
|
|
3639
|
+
color: var(--ink); width: min(720px, calc(100% - 24px)); max-width: calc(100vw - 24px); box-shadow: var(--shadow-lg);
|
|
3640
|
+
}
|
|
3641
|
+
dialog[open] { animation: dialog-in 160ms ease-out; }
|
|
3642
|
+
dialog::backdrop { background: rgb(12 14 20 / 0.58); backdrop-filter: blur(3px); }
|
|
3643
|
+
dialog form, .dialog-body { padding: 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
3644
|
+
.dialog-heading { display: flex; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
|
3645
|
+
.dialog-heading strong { font-size: 18px; letter-spacing: -0.02em; }
|
|
3646
|
+
.dialog-heading span { color: var(--muted); font-size: 12px; }
|
|
3342
3647
|
textarea {
|
|
3343
3648
|
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
3344
3649
|
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
3345
3650
|
font-family: var(--mono); font-size: 12px;
|
|
3346
3651
|
}
|
|
3347
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
3348
|
-
.
|
|
3652
|
+
.dialog-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding-top: 4px; }
|
|
3653
|
+
.dialog-actions .btn-danger { margin-right: auto; }
|
|
3654
|
+
.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); }
|
|
3349
3655
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
3350
3656
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
3351
3657
|
.status-2 { color: var(--ok); }
|
|
3352
3658
|
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
3353
3659
|
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
3660
|
+
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
3661
|
+
.form-grid .wide { grid-column: span 2; }
|
|
3354
3662
|
.route {
|
|
3355
|
-
display: grid; grid-template-columns:
|
|
3356
|
-
padding:
|
|
3357
|
-
}
|
|
3358
|
-
.
|
|
3663
|
+
display: grid; grid-template-columns: 84px minmax(0, 1fr) auto; gap: 10px; align-items: center;
|
|
3664
|
+
padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--bg-elev);
|
|
3665
|
+
}
|
|
3666
|
+
.route:first-child { border-radius: var(--radius) var(--radius) 0 0; }
|
|
3667
|
+
.route:last-child { border-bottom: 0; border-radius: 0 0 var(--radius) var(--radius); }
|
|
3668
|
+
.route-tools { display: flex; align-items: flex-end; gap: 12px; margin-bottom: 12px; }
|
|
3669
|
+
.route-tools .field { flex: 1; }
|
|
3670
|
+
.route-tools input { width: 100%; }
|
|
3671
|
+
.route-count { padding-bottom: 10px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3672
|
+
#route-list { max-height: 34rem; border: 1px solid var(--line); border-radius: var(--radius); overflow: auto; box-shadow: var(--shadow-xs); }
|
|
3673
|
+
.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; }
|
|
3674
|
+
@keyframes view-in { from { transform: translateY(3px); } to { transform: translateY(0); } }
|
|
3675
|
+
@keyframes dialog-in { from { transform: translateY(8px) scale(0.99); } to { transform: translateY(0) scale(1); } }
|
|
3676
|
+
@keyframes pulse { 50% { opacity: 0.45; } }
|
|
3677
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
|
|
3678
|
+
@media (forced-colors: active) { .json-editor pre { display: none; } .json-editor textarea { color: CanvasText; -webkit-text-fill-color: CanvasText; } }
|
|
3359
3679
|
@media (max-width: 800px) {
|
|
3680
|
+
:root { --header: 0px; }
|
|
3360
3681
|
header.top {
|
|
3361
|
-
align-items: stretch; flex-direction: column;
|
|
3362
|
-
padding
|
|
3682
|
+
position: relative; align-items: stretch; flex-direction: column;
|
|
3683
|
+
padding: max(12px, env(safe-area-inset-top)) 14px 12px;
|
|
3363
3684
|
}
|
|
3364
3685
|
input, select, textarea { font-size: 16px; }
|
|
3365
3686
|
.brand, .vendor, .controls { width: 100%; }
|
|
3687
|
+
.brand { gap: 10px; }
|
|
3688
|
+
.vendor { order: 2; border-radius: 9px; }
|
|
3366
3689
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
3690
|
+
.sync-status { display: none; }
|
|
3367
3691
|
/* The layout fills the screen. A single-column grid stretches every auto
|
|
3368
3692
|
row, so the tab bar grew with the leftover height and the buttons floated
|
|
3369
3693
|
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
@@ -3373,14 +3697,23 @@ var renderAdminDocument = (service) => {
|
|
|
3373
3697
|
align-content: start;
|
|
3374
3698
|
}
|
|
3375
3699
|
nav.side {
|
|
3376
|
-
align-self: start; height: auto;
|
|
3700
|
+
position: sticky; top: 0; z-index: 12; align-self: start; height: auto;
|
|
3377
3701
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
3378
3702
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
3379
|
-
padding: 8px;
|
|
3703
|
+
padding: 8px; box-shadow: var(--shadow-xs);
|
|
3380
3704
|
}
|
|
3381
|
-
nav
|
|
3705
|
+
.nav-label { display: none; }
|
|
3706
|
+
nav.side button { width: auto; white-space: nowrap; flex: none; }
|
|
3707
|
+
nav.side button[aria-current="true"]::before { inset: auto 9px -8px; width: auto; height: 3px; border-radius: 3px 3px 0 0; }
|
|
3382
3708
|
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
3383
|
-
|
|
3709
|
+
.form-grid { grid-template-columns: minmax(0, 1fr); }
|
|
3710
|
+
.form-grid .wide { grid-column: auto; }
|
|
3711
|
+
main { padding: 22px 14px 48px; }
|
|
3712
|
+
.view-head { margin-bottom: 18px; }
|
|
3713
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
3714
|
+
.card { padding: 14px; }
|
|
3715
|
+
.card .v { font-size: 20px; }
|
|
3716
|
+
.table-shell { border: 0; background: transparent; box-shadow: none; overflow: visible; }
|
|
3384
3717
|
table, thead, tbody, tr, th, td { display: block; }
|
|
3385
3718
|
thead { display: none; }
|
|
3386
3719
|
tr {
|
|
@@ -3395,50 +3728,63 @@ var renderAdminDocument = (service) => {
|
|
|
3395
3728
|
.route { grid-template-columns: 72px 1fr; }
|
|
3396
3729
|
.route button { grid-column: 1 / -1; }
|
|
3397
3730
|
}
|
|
3731
|
+
@media (max-width: 460px) {
|
|
3732
|
+
.controls label.field { min-width: calc(50% - 4px); }
|
|
3733
|
+
.controls .icon-btn, .controls .btn { flex: 1; }
|
|
3734
|
+
.grid { grid-template-columns: minmax(0, 1fr); }
|
|
3735
|
+
.dialog-actions { flex-wrap: wrap; }
|
|
3736
|
+
.dialog-actions button { flex: 1; }
|
|
3737
|
+
.dialog-actions .btn-danger { flex-basis: 100%; margin-right: 0; }
|
|
3738
|
+
}
|
|
3398
3739
|
</style>
|
|
3399
3740
|
</head>
|
|
3400
3741
|
<body>
|
|
3401
3742
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
3402
3743
|
<header class="top">
|
|
3403
3744
|
<div class="brand">
|
|
3404
|
-
<div class="brand-
|
|
3405
|
-
<
|
|
3406
|
-
|
|
3745
|
+
<div class="brand-lockup">
|
|
3746
|
+
<div class="brand-mark" aria-hidden="true">
|
|
3747
|
+
<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>
|
|
3748
|
+
</div>
|
|
3749
|
+
<div class="brand-id">
|
|
3750
|
+
<strong>${name}</strong>
|
|
3751
|
+
<span>Mock admin</span>
|
|
3752
|
+
</div>
|
|
3407
3753
|
</div>
|
|
3408
3754
|
<div class="vendor" id="vendor" hidden></div>
|
|
3409
3755
|
</div>
|
|
3410
3756
|
<div class="controls">
|
|
3757
|
+
<span class="sync-status" id="sync-status" role="status" aria-live="polite">Ready</span>
|
|
3411
3758
|
<label class="field">Namespace
|
|
3412
3759
|
<select id="namespace" aria-label="Namespace"></select>
|
|
3413
3760
|
</label>
|
|
3414
3761
|
<label class="field">Admin key
|
|
3415
3762
|
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
3416
3763
|
</label>
|
|
3417
|
-
<button class="icon-btn" id="theme" type="button" aria-label="
|
|
3418
|
-
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
3764
|
+
<button class="icon-btn" id="theme" type="button" aria-label="Use dark theme" title="Use dark theme"></button>
|
|
3765
|
+
<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>
|
|
3419
3766
|
</div>
|
|
3420
3767
|
</header>
|
|
3421
3768
|
<div class="layout">
|
|
3422
3769
|
<nav class="side" id="nav" aria-label="Admin sections">
|
|
3423
|
-
<
|
|
3424
|
-
<button type="button" data-view="
|
|
3425
|
-
<button type="button" data-view="
|
|
3426
|
-
<button type="button" data-view="
|
|
3427
|
-
<button type="button" data-view="
|
|
3428
|
-
<button type="button" data-view="
|
|
3770
|
+
<span class="nav-label">Workspace</span>
|
|
3771
|
+
<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>
|
|
3772
|
+
<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>
|
|
3773
|
+
<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>
|
|
3774
|
+
<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>
|
|
3775
|
+
<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>
|
|
3776
|
+
<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>
|
|
3429
3777
|
</nav>
|
|
3430
3778
|
<main id="views">
|
|
3431
|
-
<
|
|
3779
|
+
<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>
|
|
3432
3780
|
<section class="view" id="view-overview">
|
|
3433
|
-
<h2>Overview</h2>
|
|
3434
|
-
<p class="lede">Health, clock, and the collections this namespace is holding.</p>
|
|
3781
|
+
<div class="view-head"><div><h2>Overview</h2><p class="lede">Health, clock, and the collections this namespace is holding.</p></div></div>
|
|
3435
3782
|
<div class="grid" id="overview-cards"></div>
|
|
3436
3783
|
<h3>Recent requests</h3>
|
|
3437
3784
|
<div id="overview-requests"></div>
|
|
3438
3785
|
</section>
|
|
3439
3786
|
<section class="view" id="view-state" hidden>
|
|
3440
|
-
<h2>State</h2>
|
|
3441
|
-
<p class="lede">Every collection in this namespace. Declared fields stay visible before any row exists; stored rows add whatever else they contain.</p>
|
|
3787
|
+
<div class="view-head"><div><h2>State</h2><p class="lede">Inspect and edit every collection in this namespace.</p></div></div>
|
|
3442
3788
|
<div class="split">
|
|
3443
3789
|
<div id="collections"></div>
|
|
3444
3790
|
<div>
|
|
@@ -3451,8 +3797,7 @@ var renderAdminDocument = (service) => {
|
|
|
3451
3797
|
</div>
|
|
3452
3798
|
</section>
|
|
3453
3799
|
<section class="view" id="view-clock" hidden>
|
|
3454
|
-
<h2>Clock</h2>
|
|
3455
|
-
<p class="lede">The clock every timestamp in this mock reads.</p>
|
|
3800
|
+
<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>
|
|
3456
3801
|
<div class="card" id="clock-readout"></div>
|
|
3457
3802
|
<div class="row-actions">
|
|
3458
3803
|
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
@@ -3471,21 +3816,22 @@ var renderAdminDocument = (service) => {
|
|
|
3471
3816
|
</form>
|
|
3472
3817
|
</section>
|
|
3473
3818
|
<section class="view" id="view-faults" hidden>
|
|
3474
|
-
<h2>Faults</h2>
|
|
3475
|
-
<p class="lede">Inject a response, a delay, or a named preset. Rules apply to this namespace.</p>
|
|
3819
|
+
<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>
|
|
3476
3820
|
<form id="fault-form" class="card stack">
|
|
3477
|
-
<
|
|
3478
|
-
<
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
<
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
<
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
<
|
|
3488
|
-
|
|
3821
|
+
<div class="form-grid">
|
|
3822
|
+
<label class="field wide">Preset
|
|
3823
|
+
<select id="fault-preset"><option value="">Custom rule</option></select>
|
|
3824
|
+
</label>
|
|
3825
|
+
<label class="field">Status
|
|
3826
|
+
<input id="fault-status" inputmode="numeric" placeholder="503">
|
|
3827
|
+
</label>
|
|
3828
|
+
<label class="field wide">Path prefix
|
|
3829
|
+
<input id="fault-path" placeholder="/v1">
|
|
3830
|
+
</label>
|
|
3831
|
+
<label class="field">Method
|
|
3832
|
+
<input id="fault-method" placeholder="GET">
|
|
3833
|
+
</label>
|
|
3834
|
+
</div>
|
|
3489
3835
|
<div class="dialog-actions">
|
|
3490
3836
|
<button class="btn-primary" type="submit">Add fault</button>
|
|
3491
3837
|
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
@@ -3494,26 +3840,31 @@ var renderAdminDocument = (service) => {
|
|
|
3494
3840
|
<div id="fault-list"></div>
|
|
3495
3841
|
</section>
|
|
3496
3842
|
<section class="view" id="view-journal" hidden>
|
|
3497
|
-
<h2>Journal</h2>
|
|
3498
|
-
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
3499
|
-
<div class="row-actions">
|
|
3500
|
-
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
3501
|
-
</div>
|
|
3843
|
+
<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>
|
|
3502
3844
|
<div id="journal"></div>
|
|
3503
3845
|
</section>
|
|
3504
3846
|
<section class="view" id="view-routes" hidden>
|
|
3505
|
-
<h2>Routes</h2>
|
|
3506
|
-
<
|
|
3847
|
+
<div class="view-head"><div><h2>Routes</h2><p class="lede">Explore shared controls and service-specific admin endpoints.</p></div></div>
|
|
3848
|
+
<div class="route-tools">
|
|
3849
|
+
<label class="field">Filter routes
|
|
3850
|
+
<input id="route-search" type="search" placeholder="Method, path, or extension" autocomplete="off">
|
|
3851
|
+
</label>
|
|
3852
|
+
<span class="route-count" id="route-count"></span>
|
|
3853
|
+
</div>
|
|
3507
3854
|
<div id="route-list"></div>
|
|
3508
|
-
<form id="route-form" class="card stack">
|
|
3855
|
+
<form id="route-form" class="card stack" style="margin-top: 18px">
|
|
3509
3856
|
<strong id="route-title">Select a route</strong>
|
|
3510
3857
|
<label class="field">JSON body
|
|
3511
|
-
<
|
|
3858
|
+
<div class="json-editor" id="route-editor" hidden>
|
|
3859
|
+
<pre aria-hidden="true"><code id="route-highlight"></code></pre>
|
|
3860
|
+
<textarea id="route-body" placeholder="{ }" spellcheck="false" aria-describedby="route-json-status" disabled></textarea>
|
|
3861
|
+
</div>
|
|
3862
|
+
<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>
|
|
3512
3863
|
</label>
|
|
3513
3864
|
<div class="dialog-actions">
|
|
3514
|
-
<button class="btn-primary" type="submit">Send</button>
|
|
3865
|
+
<button class="btn-primary" id="route-send" type="submit" disabled>Send request</button>
|
|
3515
3866
|
</div>
|
|
3516
|
-
<pre id="route-result">Response will show here.</pre>
|
|
3867
|
+
<pre class="json-view" id="route-result"><code>Response will show here.</code></pre>
|
|
3517
3868
|
</form>
|
|
3518
3869
|
</section>
|
|
3519
3870
|
</main>
|
|
@@ -3521,12 +3872,16 @@ var renderAdminDocument = (service) => {
|
|
|
3521
3872
|
</div>
|
|
3522
3873
|
<dialog id="editor">
|
|
3523
3874
|
<form method="dialog" id="editor-form">
|
|
3524
|
-
<strong id="editor-title">Record</strong>
|
|
3875
|
+
<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>
|
|
3525
3876
|
<label class="field">Id
|
|
3526
3877
|
<input id="editor-id" required>
|
|
3527
3878
|
</label>
|
|
3528
3879
|
<label class="field">JSON value
|
|
3529
|
-
<
|
|
3880
|
+
<div class="json-editor" id="record-editor">
|
|
3881
|
+
<pre aria-hidden="true"><code id="editor-highlight"></code></pre>
|
|
3882
|
+
<textarea id="editor-value" spellcheck="false" aria-describedby="editor-json-status"></textarea>
|
|
3883
|
+
</div>
|
|
3884
|
+
<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>
|
|
3530
3885
|
</label>
|
|
3531
3886
|
<div class="dialog-actions">
|
|
3532
3887
|
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
@@ -3568,8 +3923,8 @@ var describeExtension = (extension) => {
|
|
|
3568
3923
|
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
3569
3924
|
};
|
|
3570
3925
|
};
|
|
3571
|
-
var adminUiRoutes = (service, ui) => {
|
|
3572
|
-
const shell = () => renderAdminDocument(service);
|
|
3926
|
+
var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
3927
|
+
const shell = () => renderAdminDocument(service, adminPrefix);
|
|
3573
3928
|
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
3574
3929
|
return {
|
|
3575
3930
|
"GET /ui": () => htmlResponse(document3()),
|
|
@@ -4260,7 +4615,7 @@ var stateAdminRoutes = (routes) => {
|
|
|
4260
4615
|
};
|
|
4261
4616
|
|
|
4262
4617
|
// ../core/dist/version.js
|
|
4263
|
-
var PACKAGE_VERSION = true ? "
|
|
4618
|
+
var PACKAGE_VERSION = true ? "2.0.0" : UNRELEASED_VERSION;
|
|
4264
4619
|
|
|
4265
4620
|
// ../core/dist/signing.js
|
|
4266
4621
|
var encoder = new TextEncoder();
|
|
@@ -4633,7 +4988,10 @@ var createWebhookHub = (options) => {
|
|
|
4633
4988
|
};
|
|
4634
4989
|
return hub;
|
|
4635
4990
|
};
|
|
4636
|
-
var json5 = (status, body) => new Response(JSON.stringify(body), {
|
|
4991
|
+
var json5 = (status, body) => new Response(JSON.stringify(body), {
|
|
4992
|
+
status,
|
|
4993
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4994
|
+
});
|
|
4637
4995
|
var adminError3 = (status, message) => json5(status, { error: { type: "mockingbird_admin", message } });
|
|
4638
4996
|
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4639
4997
|
var parseEndpoint = (value) => {
|
|
@@ -4741,7 +5099,6 @@ var AT_HEADER = "x-mockingbird-at";
|
|
|
4741
5099
|
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
4742
5100
|
var DEFAULT_NAMESPACE = "default";
|
|
4743
5101
|
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4744
|
-
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
4745
5102
|
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4746
5103
|
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
4747
5104
|
var effects = /* @__PURE__ */ new WeakMap();
|
|
@@ -4782,6 +5139,22 @@ var operationMatcher = (document3) => {
|
|
|
4782
5139
|
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
4783
5140
|
};
|
|
4784
5141
|
var createRuntime = (options) => {
|
|
5142
|
+
const adminPrefix = resolveAdminPrefix(options.adminPrefix);
|
|
5143
|
+
const internalPaths = Object.entries(options.document?.paths ?? {}).filter(([, item]) => item["x-mockingbird-internal"] === true).map(([path]) => path);
|
|
5144
|
+
for (const path of internalPaths) {
|
|
5145
|
+
if (!path.startsWith(`${ADMIN_PREFIX}/blobs/`))
|
|
5146
|
+
throw new Error(`internal transport ${path} must live under ${ADMIN_PREFIX}/blobs/`);
|
|
5147
|
+
}
|
|
5148
|
+
assertAdminPrefixAvailable(adminPrefix, Object.keys(options.document?.paths ?? {}).filter((path) => !internalPaths.includes(path)));
|
|
5149
|
+
const internalMatch = (path) => {
|
|
5150
|
+
for (const pattern of internalPaths) {
|
|
5151
|
+
const actual = path.split("/");
|
|
5152
|
+
const expected = pattern.split("/");
|
|
5153
|
+
if (actual.length === expected.length && expected.every((part, i) => part.startsWith("{") || part === actual[i]))
|
|
5154
|
+
return pattern;
|
|
5155
|
+
}
|
|
5156
|
+
return void 0;
|
|
5157
|
+
};
|
|
4785
5158
|
assertAdminUi(options.adminUi);
|
|
4786
5159
|
const sqlite = bootSqlite(options.sqlite);
|
|
4787
5160
|
const clock = options.clock ?? createClock();
|
|
@@ -4827,6 +5200,7 @@ var createRuntime = (options) => {
|
|
|
4827
5200
|
const created = options.create({
|
|
4828
5201
|
namespace: storageNamespace(key),
|
|
4829
5202
|
publicNamespace,
|
|
5203
|
+
adminPrefix,
|
|
4830
5204
|
sqlite,
|
|
4831
5205
|
clock: namespaceClock(publicNamespace),
|
|
4832
5206
|
rng: createdContextRng
|
|
@@ -5059,10 +5433,10 @@ var createRuntime = (options) => {
|
|
|
5059
5433
|
namespaceClock,
|
|
5060
5434
|
namespaceOf(request) {
|
|
5061
5435
|
const url = new URL(request.url);
|
|
5062
|
-
const prefixed =
|
|
5436
|
+
const prefixed = matchNamespacePath(url.pathname, adminPrefix);
|
|
5063
5437
|
const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
|
|
5064
5438
|
const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
|
|
5065
|
-
if (path
|
|
5439
|
+
if (isAdminPath(path, adminPrefix))
|
|
5066
5440
|
return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
|
|
5067
5441
|
if (selected !== void 0)
|
|
5068
5442
|
return selected;
|
|
@@ -5128,7 +5502,7 @@ var createRuntime = (options) => {
|
|
|
5128
5502
|
state: (namespace = DEFAULT_NAMESPACE) => inspectState(stateScope(namespace)),
|
|
5129
5503
|
fetch: async (incoming) => {
|
|
5130
5504
|
let request = incoming;
|
|
5131
|
-
const prefixed =
|
|
5505
|
+
const prefixed = matchNamespacePath(new URL(request.url).pathname, adminPrefix);
|
|
5132
5506
|
if (prefixed) {
|
|
5133
5507
|
const url2 = new URL(request.url);
|
|
5134
5508
|
url2.pathname = prefixed[2] ?? "/";
|
|
@@ -5144,7 +5518,15 @@ var createRuntime = (options) => {
|
|
|
5144
5518
|
signal: request.signal
|
|
5145
5519
|
});
|
|
5146
5520
|
}
|
|
5147
|
-
|
|
5521
|
+
const internalUrl = new URL(request.url);
|
|
5522
|
+
const internalPath = isAdminPath(internalUrl.pathname, adminPrefix) ? `${ADMIN_PREFIX}${internalUrl.pathname.slice(adminPrefix.length)}` : "";
|
|
5523
|
+
const internal = internalMatch(internalPath);
|
|
5524
|
+
const obsoleteInternalPath = adminPrefix !== ADMIN_PREFIX && internalMatch(internalUrl.pathname) && !internal;
|
|
5525
|
+
if (internal) {
|
|
5526
|
+
internalUrl.pathname = internalPath;
|
|
5527
|
+
request = new Request(internalUrl, request);
|
|
5528
|
+
}
|
|
5529
|
+
let namespace = internal ? internalUrl.searchParams.get("namespace") ?? control.namespaceOf(request) : control.namespaceOf(request);
|
|
5148
5530
|
if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
5149
5531
|
const credential = options.credential(request);
|
|
5150
5532
|
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
@@ -5164,7 +5546,9 @@ var createRuntime = (options) => {
|
|
|
5164
5546
|
return copy;
|
|
5165
5547
|
}
|
|
5166
5548
|
};
|
|
5167
|
-
|
|
5549
|
+
if (obsoleteInternalPath)
|
|
5550
|
+
return stamp(adminFail(404, "internal transport is only available under adminPrefix"));
|
|
5551
|
+
const handled = internal ? void 0 : await control.handle(request);
|
|
5168
5552
|
if (handled)
|
|
5169
5553
|
return stamp(handled);
|
|
5170
5554
|
const started = monotonicNow();
|
|
@@ -5205,7 +5589,7 @@ var createRuntime = (options) => {
|
|
|
5205
5589
|
type: "mockingbird_admin",
|
|
5206
5590
|
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
5207
5591
|
}
|
|
5208
|
-
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
5592
|
+
}), { status: 400, headers: { "content-type": "application/json; charset=utf-8" } }));
|
|
5209
5593
|
}
|
|
5210
5594
|
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
5211
5595
|
log(400);
|
|
@@ -5322,15 +5706,16 @@ var createRuntime = (options) => {
|
|
|
5322
5706
|
...options.admin?.(runtime) ?? {},
|
|
5323
5707
|
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
5324
5708
|
// maps `presets`, and a service route must not replace that list with a record.
|
|
5325
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
5709
|
+
...options.presets ? presetRoutes(options.presets, runtime, adminPrefix) : {},
|
|
5326
5710
|
...stateAdminRoutes({
|
|
5327
5711
|
open: stateScope,
|
|
5328
5712
|
afterWrite: (namespace) => {
|
|
5329
5713
|
checkpoint(namespace, "main");
|
|
5330
5714
|
}
|
|
5331
5715
|
}),
|
|
5332
|
-
...adminUiRoutes(options.name, options.adminUi)
|
|
5716
|
+
...adminUiRoutes(options.name, options.adminUi, adminPrefix)
|
|
5333
5717
|
},
|
|
5718
|
+
adminPrefix,
|
|
5334
5719
|
adminKey: options.adminKey
|
|
5335
5720
|
});
|
|
5336
5721
|
return defineMock(runtime);
|
|
@@ -5344,7 +5729,10 @@ var mutableResponse = (response) => {
|
|
|
5344
5729
|
return new Response(response.body, response);
|
|
5345
5730
|
}
|
|
5346
5731
|
};
|
|
5347
|
-
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5732
|
+
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5733
|
+
status,
|
|
5734
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
5735
|
+
});
|
|
5348
5736
|
var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
|
|
5349
5737
|
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5350
5738
|
var credentialRoutes = (registry) => ({
|
|
@@ -5396,7 +5784,7 @@ var credentialRoutes = (registry) => ({
|
|
|
5396
5784
|
return adminJson(200, { status: "ok" });
|
|
5397
5785
|
}
|
|
5398
5786
|
});
|
|
5399
|
-
var presetRoutes = (presets, runtime) => ({
|
|
5787
|
+
var presetRoutes = (presets, runtime, adminPrefix) => ({
|
|
5400
5788
|
"GET /faults/presets": () => {
|
|
5401
5789
|
const body = {
|
|
5402
5790
|
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
@@ -5406,7 +5794,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
5406
5794
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
5407
5795
|
const name = params.name;
|
|
5408
5796
|
if (!presets[name])
|
|
5409
|
-
return adminFail(404, `no fault preset ${name}; GET /
|
|
5797
|
+
return adminFail(404, `no fault preset ${name}; GET ${adminPrefix}/faults/presets`);
|
|
5410
5798
|
const overrides = isObject(body) ? body : {};
|
|
5411
5799
|
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
5412
5800
|
}
|
|
@@ -5539,25 +5927,28 @@ var RESEND_PRESETS = {
|
|
|
5539
5927
|
var forwardToInbox = async (target, email, namespace) => {
|
|
5540
5928
|
const send = target.fetch ?? ((request) => fetch(request));
|
|
5541
5929
|
const response = await send(
|
|
5542
|
-
new Request(
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5930
|
+
new Request(
|
|
5931
|
+
`${target.url.replace(/\/$/, "")}${resolveAdminPrefix(target.adminPrefix)}/ingest`,
|
|
5932
|
+
{
|
|
5933
|
+
method: "POST",
|
|
5934
|
+
headers: {
|
|
5935
|
+
"content-type": "application/json",
|
|
5936
|
+
"x-mockingbird-namespace": namespace,
|
|
5937
|
+
...target.adminKey ? { "x-mockingbird-admin-key": target.adminKey } : {}
|
|
5938
|
+
},
|
|
5939
|
+
body: JSON.stringify({
|
|
5940
|
+
from: email.from,
|
|
5941
|
+
to: email.toHeader,
|
|
5942
|
+
cc: email.cc,
|
|
5943
|
+
bcc: email.bcc,
|
|
5944
|
+
subject: email.subject,
|
|
5945
|
+
html: email.html,
|
|
5946
|
+
text: email.text,
|
|
5947
|
+
headers: email.headers
|
|
5948
|
+
}),
|
|
5949
|
+
signal: AbortSignal.timeout(target.timeoutMs ?? 2e3)
|
|
5950
|
+
}
|
|
5951
|
+
)
|
|
5561
5952
|
);
|
|
5562
5953
|
await response.body?.cancel();
|
|
5563
5954
|
return response.ok;
|
|
@@ -5644,15 +6035,17 @@ var createRuntime2 = (options = {}) => {
|
|
|
5644
6035
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
5645
6036
|
...options.clock ? { clock: options.clock } : {},
|
|
5646
6037
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
6038
|
+
...options.adminPrefix !== void 0 ? { adminPrefix: options.adminPrefix } : {},
|
|
5647
6039
|
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
5648
6040
|
...options.onLog ? { onLog: options.onLog } : {},
|
|
5649
6041
|
credential: bearerToken,
|
|
5650
6042
|
presets: RESEND_PRESETS,
|
|
5651
6043
|
webhooks: hub,
|
|
5652
|
-
create: ({ sqlite, namespace, publicNamespace, clock }) => new ResendAPI({
|
|
6044
|
+
create: ({ sqlite, namespace, publicNamespace, adminPrefix, clock }) => new ResendAPI({
|
|
5653
6045
|
sqlite,
|
|
5654
6046
|
namespace,
|
|
5655
6047
|
publicNamespace,
|
|
6048
|
+
adminPrefix,
|
|
5656
6049
|
now: clock.now,
|
|
5657
6050
|
onOutcome: (event) => recordOutcome(event),
|
|
5658
6051
|
...target ? {
|
|
@@ -5764,6 +6157,7 @@ var ResendAPI = class {
|
|
|
5764
6157
|
state;
|
|
5765
6158
|
service;
|
|
5766
6159
|
now;
|
|
6160
|
+
adminPrefix;
|
|
5767
6161
|
publicNamespace;
|
|
5768
6162
|
onSent;
|
|
5769
6163
|
onOutcome;
|
|
@@ -5777,6 +6171,7 @@ var ResendAPI = class {
|
|
|
5777
6171
|
const sqlite = bootSqlite(options.sqlite);
|
|
5778
6172
|
const namespace = options.namespace ?? RESEND_NAMESPACE;
|
|
5779
6173
|
this.now = options.now ?? (() => Date.now());
|
|
6174
|
+
this.adminPrefix = options.adminPrefix ?? "/__admin";
|
|
5780
6175
|
this.publicNamespace = options.publicNamespace;
|
|
5781
6176
|
this.onSent = options.onSent;
|
|
5782
6177
|
this.onOutcome = options.onOutcome;
|
|
@@ -6028,7 +6423,7 @@ var ResendAPI = class {
|
|
|
6028
6423
|
};
|
|
6029
6424
|
}
|
|
6030
6425
|
downloadUrl(origin, attachmentId) {
|
|
6031
|
-
const prefix = this.publicNamespace && this.publicNamespace !== "default" ?
|
|
6426
|
+
const prefix = this.publicNamespace && this.publicNamespace !== "default" ? `${this.adminPrefix}/ns/${encodeURIComponent(this.publicNamespace)}` : "";
|
|
6032
6427
|
return `${origin}${prefix}/downloads/inbound/${attachmentId}`;
|
|
6033
6428
|
}
|
|
6034
6429
|
getReceived(context) {
|
|
@@ -6090,6 +6485,7 @@ var ResendAPI = class {
|
|
|
6090
6485
|
|
|
6091
6486
|
export {
|
|
6092
6487
|
createClock,
|
|
6488
|
+
resolveAdminPrefix,
|
|
6093
6489
|
parseDuration,
|
|
6094
6490
|
document2 as document,
|
|
6095
6491
|
operationIds,
|
|
@@ -6102,4 +6498,4 @@ export {
|
|
|
6102
6498
|
bareAddress,
|
|
6103
6499
|
ResendAPI
|
|
6104
6500
|
};
|
|
6105
|
-
//# sourceMappingURL=chunk-
|
|
6501
|
+
//# sourceMappingURL=chunk-AVLXDCB3.js.map
|