@crvouga/mockingbird-service-easypost 1.8.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -1
- package/DISCOVERY.md +55 -0
- package/README.md +1 -1
- package/SUPPORT.md +13 -0
- package/dist/{chunk-XCSVDCRF.js → chunk-32BNNR7Z.js} +622 -235
- package/dist/chunk-32BNNR7Z.js.map +7 -0
- package/dist/{chunk-ONLSBFMT.js → chunk-NFI2NJUO.js} +44 -24
- package/dist/chunk-NFI2NJUO.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 +286 -0
- package/package.json +29 -4
- package/dist/chunk-ONLSBFMT.js.map +0 -7
- package/dist/chunk-XCSVDCRF.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
|
};
|
|
@@ -1816,6 +1855,34 @@ dialog:not([open]) {
|
|
|
1816
1855
|
function isRecord2(value) {
|
|
1817
1856
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1818
1857
|
}
|
|
1858
|
+
function highlightJsonText(text) {
|
|
1859
|
+
const escapeToken = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
1860
|
+
switch (char) {
|
|
1861
|
+
case "&":
|
|
1862
|
+
return "&";
|
|
1863
|
+
case "<":
|
|
1864
|
+
return "<";
|
|
1865
|
+
case ">":
|
|
1866
|
+
return ">";
|
|
1867
|
+
case '"':
|
|
1868
|
+
return """;
|
|
1869
|
+
default:
|
|
1870
|
+
return "'";
|
|
1871
|
+
}
|
|
1872
|
+
});
|
|
1873
|
+
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;
|
|
1874
|
+
let cursor = 0;
|
|
1875
|
+
let html = "";
|
|
1876
|
+
for (const match of text.matchAll(token)) {
|
|
1877
|
+
const index = match.index ?? 0;
|
|
1878
|
+
const value = match[0];
|
|
1879
|
+
html += escapeToken(text.slice(cursor, index));
|
|
1880
|
+
const kind = value.startsWith('"') ? value.trimEnd().endsWith(":") ? "key" : "string" : value === "true" || value === "false" ? "boolean" : value === "null" ? "null" : "number";
|
|
1881
|
+
html += `<span class="json-${kind}">${escapeToken(value)}</span>`;
|
|
1882
|
+
cursor = index + value.length;
|
|
1883
|
+
}
|
|
1884
|
+
return html + escapeToken(text.slice(cursor));
|
|
1885
|
+
}
|
|
1819
1886
|
function mountAdminBrand(doc, host, service, catalog) {
|
|
1820
1887
|
const clip = (value, max) => {
|
|
1821
1888
|
if (typeof value !== "string")
|
|
@@ -2177,14 +2244,16 @@ function bootAdmin(config) {
|
|
|
2177
2244
|
const state = {
|
|
2178
2245
|
namespace: "default",
|
|
2179
2246
|
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
2180
|
-
view: "overview",
|
|
2247
|
+
view: ["overview", "state", "clock", "faults", "journal", "routes"].includes(location.hash.slice(1)) ? location.hash.slice(1) : "overview",
|
|
2181
2248
|
collection: "",
|
|
2182
2249
|
shape: null,
|
|
2183
2250
|
route: null,
|
|
2251
|
+
routes: [],
|
|
2184
2252
|
editing: null
|
|
2185
2253
|
};
|
|
2186
2254
|
const standard = new Set(config.standardRoutes);
|
|
2187
2255
|
const banner = el("banner", HTMLElement);
|
|
2256
|
+
const bannerMessage = el("banner-message", HTMLElement);
|
|
2188
2257
|
const keyInput = el("admin-key", HTMLInputElement);
|
|
2189
2258
|
keyInput.value = state.key;
|
|
2190
2259
|
const params = new URLSearchParams(location.search);
|
|
@@ -2195,18 +2264,21 @@ function bootAdmin(config) {
|
|
|
2195
2264
|
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
2196
2265
|
params.delete("key");
|
|
2197
2266
|
const next = params.toString();
|
|
2198
|
-
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
2267
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : "") + location.hash);
|
|
2199
2268
|
}
|
|
2200
2269
|
const queryNamespace = params.get("namespace");
|
|
2201
2270
|
if (queryNamespace)
|
|
2202
2271
|
state.namespace = queryNamespace;
|
|
2203
2272
|
const showError = (error) => {
|
|
2204
|
-
|
|
2273
|
+
bannerMessage.textContent = error instanceof Error ? error.message : String(error);
|
|
2205
2274
|
banner.classList.add("show");
|
|
2275
|
+
banner.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
2206
2276
|
};
|
|
2207
2277
|
const clearError = () => {
|
|
2208
2278
|
banner.classList.remove("show");
|
|
2279
|
+
bannerMessage.textContent = "";
|
|
2209
2280
|
};
|
|
2281
|
+
el("banner-dismiss", HTMLButtonElement).addEventListener("click", clearError);
|
|
2210
2282
|
const api = async (method, path, body) => {
|
|
2211
2283
|
const headers = { accept: "application/json" };
|
|
2212
2284
|
if (state.key !== "")
|
|
@@ -2250,16 +2322,21 @@ function bootAdmin(config) {
|
|
|
2250
2322
|
namespace: () => state.namespace
|
|
2251
2323
|
};
|
|
2252
2324
|
const isPanelScript = (value) => typeof value === "function";
|
|
2325
|
+
const themeButton = el("theme", HTMLButtonElement);
|
|
2253
2326
|
const setTheme = (theme) => {
|
|
2254
2327
|
if (theme)
|
|
2255
2328
|
document.documentElement.dataset.theme = theme;
|
|
2256
2329
|
else
|
|
2257
2330
|
delete document.documentElement.dataset.theme;
|
|
2331
|
+
const dark = theme ? theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2332
|
+
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>';
|
|
2333
|
+
const label = dark ? "Use light theme" : "Use dark theme";
|
|
2334
|
+
themeButton.setAttribute("aria-label", label);
|
|
2335
|
+
themeButton.title = label;
|
|
2258
2336
|
};
|
|
2259
2337
|
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
2338
|
+
setTheme(storedTheme === "dark" || storedTheme === "light" ? storedTheme : null);
|
|
2339
|
+
themeButton.addEventListener("click", () => {
|
|
2263
2340
|
const current = document.documentElement.dataset.theme;
|
|
2264
2341
|
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2265
2342
|
const next = dark ? "light" : "dark";
|
|
@@ -2278,6 +2355,7 @@ function bootAdmin(config) {
|
|
|
2278
2355
|
continue;
|
|
2279
2356
|
section.hidden = section.id !== `view-${view}`;
|
|
2280
2357
|
}
|
|
2358
|
+
history.replaceState(null, "", `${location.pathname}${location.search}#${encodeURIComponent(view)}`);
|
|
2281
2359
|
};
|
|
2282
2360
|
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
2283
2361
|
const target = event.target;
|
|
@@ -2292,7 +2370,7 @@ function bootAdmin(config) {
|
|
|
2292
2370
|
show(view);
|
|
2293
2371
|
refresh().catch(showError);
|
|
2294
2372
|
});
|
|
2295
|
-
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>`;
|
|
2373
|
+
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>`;
|
|
2296
2374
|
const table = (columns, rows) => {
|
|
2297
2375
|
if (rows.length === 0)
|
|
2298
2376
|
return '<p class="empty">Nothing here yet.</p>';
|
|
@@ -2302,16 +2380,22 @@ function bootAdmin(config) {
|
|
|
2302
2380
|
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
2303
2381
|
return `<tr${attrs}>${cells}</tr>`;
|
|
2304
2382
|
}).join("");
|
|
2305
|
-
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
2383
|
+
return `<div class="table-shell"><table><thead>${head}</thead><tbody>${body}</tbody></table></div>`;
|
|
2306
2384
|
};
|
|
2307
2385
|
const preview = (value) => {
|
|
2308
2386
|
const text = JSON.stringify(value) ?? "null";
|
|
2309
|
-
|
|
2387
|
+
const clipped = text.length > 180 ? `${text.slice(0, 180)}\u2026` : text;
|
|
2388
|
+
return `<code class="json-inline">${highlightJsonText(clipped)}</code>`;
|
|
2310
2389
|
};
|
|
2311
2390
|
const when = (epoch) => {
|
|
2312
2391
|
const date = new Date(epoch);
|
|
2313
2392
|
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
2314
2393
|
};
|
|
2394
|
+
const journalWhen = (value) => {
|
|
2395
|
+
const numeric = value.trim() === "" ? Number.NaN : Number(value);
|
|
2396
|
+
const epoch = Number.isFinite(numeric) ? numeric : Date.parse(value);
|
|
2397
|
+
return Number.isFinite(epoch) ? when(epoch) : value;
|
|
2398
|
+
};
|
|
2315
2399
|
const clockCard = (epoch) => {
|
|
2316
2400
|
const date = new Date(epoch);
|
|
2317
2401
|
if (Number.isNaN(date.getTime()))
|
|
@@ -2431,7 +2515,7 @@ function bootAdmin(config) {
|
|
|
2431
2515
|
};
|
|
2432
2516
|
const renderOverview = async () => {
|
|
2433
2517
|
const [health, shape, clock, journal] = await Promise.all([
|
|
2434
|
-
originGet(
|
|
2518
|
+
originGet(`${config.adminPrefix}/health`),
|
|
2435
2519
|
api("GET", "/state"),
|
|
2436
2520
|
api("GET", "/clock"),
|
|
2437
2521
|
api("GET", "/requests?limit=8")
|
|
@@ -2448,7 +2532,7 @@ function bootAdmin(config) {
|
|
|
2448
2532
|
},
|
|
2449
2533
|
{
|
|
2450
2534
|
k: "Status",
|
|
2451
|
-
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
2535
|
+
v: `<span class="badge ok">${esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")}</span>`
|
|
2452
2536
|
},
|
|
2453
2537
|
{ k: "Namespace", v: esc(state.namespace) },
|
|
2454
2538
|
{ k: "Collections", v: String(decoded.collections.length) },
|
|
@@ -2535,26 +2619,46 @@ function bootAdmin(config) {
|
|
|
2535
2619
|
const renderJournal = async () => {
|
|
2536
2620
|
const journal = await api("GET", "/requests?limit=100");
|
|
2537
2621
|
el("journal", HTMLElement).innerHTML = table([
|
|
2538
|
-
{ label: "When", cell: (row) => esc(row.at) },
|
|
2539
|
-
{
|
|
2622
|
+
{ label: "When", cell: (row) => esc(journalWhen(row.at)) },
|
|
2623
|
+
{
|
|
2624
|
+
label: "Method",
|
|
2625
|
+
cell: (row) => `<span class="method">${esc(row.method)}</span>`
|
|
2626
|
+
},
|
|
2540
2627
|
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2541
|
-
{
|
|
2628
|
+
{
|
|
2629
|
+
label: "Status",
|
|
2630
|
+
cell: (row) => `<span class="badge ${row.status.startsWith("2") ? "ok" : row.status.startsWith("4") || row.status.startsWith("5") ? "warn" : ""}">${esc(row.status)}</span>`
|
|
2631
|
+
},
|
|
2542
2632
|
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
2543
2633
|
], decodeJournal(journal));
|
|
2544
2634
|
};
|
|
2545
|
-
const
|
|
2546
|
-
const
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2635
|
+
const paintRoutes = () => {
|
|
2636
|
+
const query = el("route-search", HTMLInputElement).value.trim().toLocaleLowerCase();
|
|
2637
|
+
const routes = state.routes.filter((route) => route.toLocaleLowerCase().includes(query));
|
|
2638
|
+
el("route-count", HTMLElement).textContent = `${routes.length} of ${state.routes.length}`;
|
|
2639
|
+
if (state.routes.length === 0) {
|
|
2640
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No admin routes are available.</p>';
|
|
2641
|
+
return;
|
|
2642
|
+
}
|
|
2643
|
+
if (routes.length === 0) {
|
|
2644
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No routes match this filter.</p>';
|
|
2645
|
+
return;
|
|
2646
|
+
}
|
|
2550
2647
|
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2551
2648
|
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2552
2649
|
const parts = route.split(" ");
|
|
2553
2650
|
const method = parts[0] ?? "";
|
|
2554
2651
|
const path = parts.slice(1).join(" ");
|
|
2555
|
-
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>`;
|
|
2652
|
+
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>`;
|
|
2556
2653
|
}).join("");
|
|
2557
2654
|
};
|
|
2655
|
+
const renderRoutes = async () => {
|
|
2656
|
+
const data = await api("GET", "/");
|
|
2657
|
+
if (!isRecord2(data))
|
|
2658
|
+
throw new Error("Route list was not an object");
|
|
2659
|
+
state.routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
2660
|
+
paintRoutes();
|
|
2661
|
+
};
|
|
2558
2662
|
const renderers = {
|
|
2559
2663
|
overview: renderOverview,
|
|
2560
2664
|
state: renderState,
|
|
@@ -2563,19 +2667,50 @@ function bootAdmin(config) {
|
|
|
2563
2667
|
journal: renderJournal,
|
|
2564
2668
|
routes: renderRoutes
|
|
2565
2669
|
};
|
|
2670
|
+
let refreshToken = 0;
|
|
2566
2671
|
async function refresh() {
|
|
2672
|
+
const token = ++refreshToken;
|
|
2567
2673
|
clearError();
|
|
2674
|
+
const main = el("views", HTMLElement);
|
|
2675
|
+
const sync = el("sync-status", HTMLElement);
|
|
2676
|
+
main.setAttribute("aria-busy", "true");
|
|
2677
|
+
sync.classList.add("busy");
|
|
2678
|
+
sync.textContent = "Updating";
|
|
2568
2679
|
const render = renderers[state.view];
|
|
2569
|
-
|
|
2570
|
-
|
|
2680
|
+
try {
|
|
2681
|
+
if (render)
|
|
2682
|
+
await render();
|
|
2683
|
+
if (token === refreshToken)
|
|
2684
|
+
sync.textContent = "Up to date";
|
|
2685
|
+
} finally {
|
|
2686
|
+
if (token === refreshToken) {
|
|
2687
|
+
main.removeAttribute("aria-busy");
|
|
2688
|
+
sync.classList.remove("busy");
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2571
2691
|
}
|
|
2572
2692
|
const delay = (ms) => new Promise((resolve) => {
|
|
2573
2693
|
window.setTimeout(resolve, ms);
|
|
2574
2694
|
});
|
|
2575
|
-
const confirmAction = async (button, pending, done, work) => {
|
|
2695
|
+
const confirmAction = async (button, pending, done, work, confirmLabel) => {
|
|
2576
2696
|
if (button.dataset.pending === "1")
|
|
2577
2697
|
return;
|
|
2578
2698
|
const label = button.textContent ?? "";
|
|
2699
|
+
if (confirmLabel !== void 0 && button.dataset.confirming !== "1") {
|
|
2700
|
+
button.dataset.confirming = "1";
|
|
2701
|
+
button.classList.add("confirming");
|
|
2702
|
+
button.textContent = confirmLabel;
|
|
2703
|
+
window.setTimeout(() => {
|
|
2704
|
+
if (button.dataset.confirming !== "1")
|
|
2705
|
+
return;
|
|
2706
|
+
button.textContent = label;
|
|
2707
|
+
button.classList.remove("confirming");
|
|
2708
|
+
delete button.dataset.confirming;
|
|
2709
|
+
}, 3e3);
|
|
2710
|
+
return;
|
|
2711
|
+
}
|
|
2712
|
+
button.classList.remove("confirming");
|
|
2713
|
+
delete button.dataset.confirming;
|
|
2579
2714
|
const width = button.offsetWidth;
|
|
2580
2715
|
button.dataset.pending = "1";
|
|
2581
2716
|
button.classList.add("pressed");
|
|
@@ -2596,6 +2731,65 @@ function bootAdmin(config) {
|
|
|
2596
2731
|
delete button.dataset.pending;
|
|
2597
2732
|
}
|
|
2598
2733
|
};
|
|
2734
|
+
const wireJsonEditor = (textareaId, highlightId, statusId, shellId, allowEmpty) => {
|
|
2735
|
+
const textarea = el(textareaId, HTMLTextAreaElement);
|
|
2736
|
+
const highlight = el(highlightId, HTMLElement);
|
|
2737
|
+
const status = el(statusId, HTMLElement);
|
|
2738
|
+
const shell = el(shellId, HTMLElement);
|
|
2739
|
+
const sync = () => {
|
|
2740
|
+
const text = textarea.value;
|
|
2741
|
+
highlight.innerHTML = `${highlightJsonText(text)}
|
|
2742
|
+
`;
|
|
2743
|
+
let valid = true;
|
|
2744
|
+
let message = "Valid JSON";
|
|
2745
|
+
if (allowEmpty && text.trim() === "")
|
|
2746
|
+
message = "No body";
|
|
2747
|
+
else {
|
|
2748
|
+
try {
|
|
2749
|
+
readJson2(text);
|
|
2750
|
+
} catch {
|
|
2751
|
+
valid = false;
|
|
2752
|
+
message = "Invalid JSON";
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
shell.classList.toggle("invalid", !valid);
|
|
2756
|
+
status.classList.toggle("invalid", !valid);
|
|
2757
|
+
status.textContent = message;
|
|
2758
|
+
textarea.setAttribute("aria-invalid", valid ? "false" : "true");
|
|
2759
|
+
};
|
|
2760
|
+
const syncScroll = () => {
|
|
2761
|
+
const preview2 = highlight.parentElement;
|
|
2762
|
+
if (!(preview2 instanceof HTMLElement))
|
|
2763
|
+
return;
|
|
2764
|
+
preview2.scrollTop = textarea.scrollTop;
|
|
2765
|
+
preview2.scrollLeft = textarea.scrollLeft;
|
|
2766
|
+
};
|
|
2767
|
+
textarea.addEventListener("input", sync);
|
|
2768
|
+
textarea.addEventListener("scroll", syncScroll);
|
|
2769
|
+
sync();
|
|
2770
|
+
return sync;
|
|
2771
|
+
};
|
|
2772
|
+
const syncRouteEditor = wireJsonEditor("route-body", "route-highlight", "route-json-status", "route-editor", true);
|
|
2773
|
+
const syncRecordEditor = wireJsonEditor("editor-value", "editor-highlight", "editor-json-status", "record-editor", false);
|
|
2774
|
+
for (const button of document.querySelectorAll("[data-format]")) {
|
|
2775
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2776
|
+
continue;
|
|
2777
|
+
button.addEventListener("click", () => {
|
|
2778
|
+
const target = button.dataset.format;
|
|
2779
|
+
if (target === void 0)
|
|
2780
|
+
return;
|
|
2781
|
+
const textarea = document.getElementById(target);
|
|
2782
|
+
if (!(textarea instanceof HTMLTextAreaElement) || textarea.value.trim() === "")
|
|
2783
|
+
return;
|
|
2784
|
+
try {
|
|
2785
|
+
textarea.value = JSON.stringify(readJson2(textarea.value), null, 2) ?? "";
|
|
2786
|
+
textarea.dispatchEvent(new Event("input"));
|
|
2787
|
+
textarea.focus();
|
|
2788
|
+
} catch {
|
|
2789
|
+
showError(new Error("Fix the JSON before formatting it"));
|
|
2790
|
+
}
|
|
2791
|
+
});
|
|
2792
|
+
}
|
|
2599
2793
|
document.addEventListener("click", (event) => {
|
|
2600
2794
|
const target = event.target;
|
|
2601
2795
|
if (!(target instanceof Element))
|
|
@@ -2668,6 +2862,7 @@ function bootAdmin(config) {
|
|
|
2668
2862
|
} else {
|
|
2669
2863
|
editorValue.value = "{\n \n}";
|
|
2670
2864
|
}
|
|
2865
|
+
syncRecordEditor();
|
|
2671
2866
|
editor.showModal();
|
|
2672
2867
|
}
|
|
2673
2868
|
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -2697,10 +2892,10 @@ function bootAdmin(config) {
|
|
|
2697
2892
|
const id = state.editing;
|
|
2698
2893
|
if (!id)
|
|
2699
2894
|
return;
|
|
2700
|
-
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2895
|
+
confirmAction(editorDelete, "Deleting...", "Deleted", () => api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2701
2896
|
editor.close();
|
|
2702
2897
|
return renderState();
|
|
2703
|
-
}).catch(showError);
|
|
2898
|
+
}), "Confirm delete").catch(showError);
|
|
2704
2899
|
});
|
|
2705
2900
|
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2706
2901
|
if (!(button instanceof HTMLButtonElement))
|
|
@@ -2746,10 +2941,10 @@ function bootAdmin(config) {
|
|
|
2746
2941
|
}).then(() => renderFaults()).catch(showError);
|
|
2747
2942
|
});
|
|
2748
2943
|
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2749
|
-
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
|
|
2944
|
+
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults()), "Confirm clear").catch(showError);
|
|
2750
2945
|
});
|
|
2751
2946
|
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2752
|
-
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
|
|
2947
|
+
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal()), "Confirm clear").catch(showError);
|
|
2753
2948
|
});
|
|
2754
2949
|
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2755
2950
|
const target = event.target;
|
|
@@ -2763,9 +2958,15 @@ function bootAdmin(config) {
|
|
|
2763
2958
|
return;
|
|
2764
2959
|
state.route = route;
|
|
2765
2960
|
el("route-title", HTMLElement).textContent = route;
|
|
2961
|
+
el("route-send", HTMLButtonElement).disabled = false;
|
|
2766
2962
|
const method = route.split(" ")[0] ?? "";
|
|
2767
|
-
el("route-body", HTMLTextAreaElement)
|
|
2963
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
2964
|
+
routeBody.disabled = method === "GET" || method === "DELETE";
|
|
2965
|
+
el("route-editor", HTMLElement).hidden = routeBody.disabled;
|
|
2966
|
+
el("route-json-status", HTMLElement).parentElement?.toggleAttribute("hidden", routeBody.disabled);
|
|
2967
|
+
syncRouteEditor();
|
|
2768
2968
|
});
|
|
2969
|
+
el("route-search", HTMLInputElement).addEventListener("input", paintRoutes);
|
|
2769
2970
|
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2770
2971
|
event.preventDefault();
|
|
2771
2972
|
const send = async () => {
|
|
@@ -2787,7 +2988,8 @@ function bootAdmin(config) {
|
|
|
2787
2988
|
}
|
|
2788
2989
|
}
|
|
2789
2990
|
const data = await api(method, concrete, body);
|
|
2790
|
-
|
|
2991
|
+
const text = JSON.stringify(data, null, 2) ?? "null";
|
|
2992
|
+
el("route-result", HTMLElement).innerHTML = `<code>${highlightJsonText(text)}</code>`;
|
|
2791
2993
|
};
|
|
2792
2994
|
send().catch(showError);
|
|
2793
2995
|
});
|
|
@@ -2905,11 +3107,13 @@ function bootAdmin(config) {
|
|
|
2905
3107
|
views.append(section);
|
|
2906
3108
|
mountSqlExplorer(body, panelApi);
|
|
2907
3109
|
}
|
|
3110
|
+
show(document.getElementById(`view-${state.view}`) === null ? "overview" : state.view);
|
|
2908
3111
|
return refresh();
|
|
2909
3112
|
}).catch(showError);
|
|
2910
3113
|
}
|
|
2911
3114
|
var clientPrelude = [
|
|
2912
3115
|
isRecord2,
|
|
3116
|
+
highlightJsonText,
|
|
2913
3117
|
mountAdminBrand,
|
|
2914
3118
|
startAdminBrand,
|
|
2915
3119
|
faultPresetList,
|
|
@@ -2932,6 +3136,7 @@ ${entry}(${payload});
|
|
|
2932
3136
|
var defineMock = (runtime) => runtime;
|
|
2933
3137
|
var STANDARD_ADMIN_ROUTES = [
|
|
2934
3138
|
"GET /",
|
|
3139
|
+
"GET /health",
|
|
2935
3140
|
"POST /reset",
|
|
2936
3141
|
"GET /namespaces",
|
|
2937
3142
|
"GET /clock",
|
|
@@ -3011,11 +3216,11 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
3011
3216
|
}
|
|
3012
3217
|
});
|
|
3013
3218
|
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
3014
|
-
var renderAdminDocument = (service) => {
|
|
3219
|
+
var renderAdminDocument = (service, adminPrefix = ADMIN_PREFIX) => {
|
|
3015
3220
|
const name = escapeHtml(service);
|
|
3016
3221
|
const script = adminClientSource({
|
|
3017
3222
|
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
3018
|
-
adminPrefix
|
|
3223
|
+
adminPrefix,
|
|
3019
3224
|
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
3020
3225
|
brandsUrl: ADMIN_BRANDS_URL,
|
|
3021
3226
|
service
|
|
@@ -3032,75 +3237,99 @@ var renderAdminDocument = (service) => {
|
|
|
3032
3237
|
${CSS_RESET}
|
|
3033
3238
|
:root {
|
|
3034
3239
|
color-scheme: light;
|
|
3035
|
-
--bg: #
|
|
3240
|
+
--bg: #f7f8fb;
|
|
3036
3241
|
--bg-elev: #ffffff;
|
|
3037
|
-
--bg-muted: #
|
|
3038
|
-
--bg-inset: #
|
|
3039
|
-
--line: #
|
|
3040
|
-
--line-strong: #
|
|
3041
|
-
--ink: #
|
|
3042
|
-
--muted: #
|
|
3043
|
-
--faint: #
|
|
3044
|
-
--accent: #
|
|
3242
|
+
--bg-muted: #f1f3f7;
|
|
3243
|
+
--bg-inset: #f8f9fc;
|
|
3244
|
+
--line: #e4e7ec;
|
|
3245
|
+
--line-strong: #cfd4dc;
|
|
3246
|
+
--ink: #101828;
|
|
3247
|
+
--muted: #475467;
|
|
3248
|
+
--faint: #667085;
|
|
3249
|
+
--accent: #6941c6;
|
|
3250
|
+
--accent-hover: #5933b4;
|
|
3045
3251
|
--accent-ink: #ffffff;
|
|
3046
|
-
--accent-soft: #
|
|
3047
|
-
--ok: #
|
|
3048
|
-
--ok-soft: #
|
|
3049
|
-
--warn: #
|
|
3050
|
-
--warn-soft: #
|
|
3051
|
-
--err: #
|
|
3052
|
-
--err-soft: #
|
|
3053
|
-
--
|
|
3252
|
+
--accent-soft: #f4f0ff;
|
|
3253
|
+
--ok: #067647;
|
|
3254
|
+
--ok-soft: #ecfdf3;
|
|
3255
|
+
--warn: #b54708;
|
|
3256
|
+
--warn-soft: #fffaeb;
|
|
3257
|
+
--err: #b42318;
|
|
3258
|
+
--err-soft: #fef3f2;
|
|
3259
|
+
--code-key: #6941c6;
|
|
3260
|
+
--code-string: #067647;
|
|
3261
|
+
--code-number: #175cd3;
|
|
3262
|
+
--code-boolean: #c11574;
|
|
3263
|
+
--code-null: #667085;
|
|
3264
|
+
--shadow-xs: 0 1px 2px rgb(16 24 40 / 0.05);
|
|
3265
|
+
--shadow-sm: 0 1px 3px rgb(16 24 40 / 0.08), 0 1px 2px rgb(16 24 40 / 0.04);
|
|
3266
|
+
--shadow-lg: 0 18px 48px rgb(16 24 40 / 0.16), 0 4px 12px rgb(16 24 40 / 0.08);
|
|
3054
3267
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
3055
3268
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
3056
|
-
--radius:
|
|
3057
|
-
--header:
|
|
3058
|
-
--nav:
|
|
3269
|
+
--radius: 12px;
|
|
3270
|
+
--header: 72px;
|
|
3271
|
+
--nav: 224px;
|
|
3059
3272
|
}
|
|
3060
3273
|
:root[data-theme="dark"] {
|
|
3061
3274
|
color-scheme: dark;
|
|
3062
|
-
--bg: #
|
|
3063
|
-
--bg-elev: #
|
|
3064
|
-
--bg-muted: #
|
|
3065
|
-
--bg-inset: #
|
|
3066
|
-
--line: #
|
|
3067
|
-
--line-strong: #
|
|
3068
|
-
--ink: #
|
|
3069
|
-
--muted: #
|
|
3070
|
-
--faint: #
|
|
3071
|
-
--accent: #
|
|
3072
|
-
--accent-
|
|
3073
|
-
--accent-
|
|
3074
|
-
--
|
|
3075
|
-
--ok
|
|
3076
|
-
--
|
|
3077
|
-
--warn
|
|
3078
|
-
--
|
|
3079
|
-
--err
|
|
3080
|
-
--
|
|
3275
|
+
--bg: #0c0e14;
|
|
3276
|
+
--bg-elev: #151821;
|
|
3277
|
+
--bg-muted: #20232e;
|
|
3278
|
+
--bg-inset: #11141c;
|
|
3279
|
+
--line: #2b303d;
|
|
3280
|
+
--line-strong: #414858;
|
|
3281
|
+
--ink: #f5f7fa;
|
|
3282
|
+
--muted: #b6bdc9;
|
|
3283
|
+
--faint: #8d96a7;
|
|
3284
|
+
--accent: #9e77ed;
|
|
3285
|
+
--accent-hover: #b692f6;
|
|
3286
|
+
--accent-ink: #ffffff;
|
|
3287
|
+
--accent-soft: #2b2142;
|
|
3288
|
+
--ok: #75e0a7;
|
|
3289
|
+
--ok-soft: #102a20;
|
|
3290
|
+
--warn: #fec84b;
|
|
3291
|
+
--warn-soft: #302611;
|
|
3292
|
+
--err: #fda29b;
|
|
3293
|
+
--err-soft: #321817;
|
|
3294
|
+
--code-key: #c3a6ff;
|
|
3295
|
+
--code-string: #75e0a7;
|
|
3296
|
+
--code-number: #84adff;
|
|
3297
|
+
--code-boolean: #f9a8d4;
|
|
3298
|
+
--code-null: #98a2b3;
|
|
3299
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3300
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3301
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
3081
3302
|
}
|
|
3082
3303
|
@media (prefers-color-scheme: dark) {
|
|
3083
3304
|
:root:not([data-theme="light"]) {
|
|
3084
3305
|
color-scheme: dark;
|
|
3085
|
-
--bg: #
|
|
3086
|
-
--bg-elev: #
|
|
3087
|
-
--bg-muted: #
|
|
3088
|
-
--bg-inset: #
|
|
3089
|
-
--line: #
|
|
3090
|
-
--line-strong: #
|
|
3091
|
-
--ink: #
|
|
3092
|
-
--muted: #
|
|
3093
|
-
--faint: #
|
|
3094
|
-
--accent: #
|
|
3095
|
-
--accent-
|
|
3096
|
-
--accent-
|
|
3097
|
-
--
|
|
3098
|
-
--ok
|
|
3099
|
-
--
|
|
3100
|
-
--warn
|
|
3101
|
-
--
|
|
3102
|
-
--err
|
|
3103
|
-
--
|
|
3306
|
+
--bg: #0c0e14;
|
|
3307
|
+
--bg-elev: #151821;
|
|
3308
|
+
--bg-muted: #20232e;
|
|
3309
|
+
--bg-inset: #11141c;
|
|
3310
|
+
--line: #2b303d;
|
|
3311
|
+
--line-strong: #414858;
|
|
3312
|
+
--ink: #f5f7fa;
|
|
3313
|
+
--muted: #b6bdc9;
|
|
3314
|
+
--faint: #8d96a7;
|
|
3315
|
+
--accent: #9e77ed;
|
|
3316
|
+
--accent-hover: #b692f6;
|
|
3317
|
+
--accent-ink: #ffffff;
|
|
3318
|
+
--accent-soft: #2b2142;
|
|
3319
|
+
--ok: #75e0a7;
|
|
3320
|
+
--ok-soft: #102a20;
|
|
3321
|
+
--warn: #fec84b;
|
|
3322
|
+
--warn-soft: #302611;
|
|
3323
|
+
--err: #fda29b;
|
|
3324
|
+
--err-soft: #321817;
|
|
3325
|
+
--code-key: #c3a6ff;
|
|
3326
|
+
--code-string: #75e0a7;
|
|
3327
|
+
--code-number: #84adff;
|
|
3328
|
+
--code-boolean: #f9a8d4;
|
|
3329
|
+
--code-null: #98a2b3;
|
|
3330
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3331
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3332
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
3104
3333
|
}
|
|
3105
3334
|
}
|
|
3106
3335
|
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
@@ -3118,13 +3347,17 @@ var renderAdminDocument = (service) => {
|
|
|
3118
3347
|
padding-right: env(safe-area-inset-right);
|
|
3119
3348
|
-webkit-text-size-adjust: 100%;
|
|
3120
3349
|
}
|
|
3121
|
-
|
|
3350
|
+
::selection { background: color-mix(in srgb, var(--accent) 28%, transparent); }
|
|
3351
|
+
:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px; }
|
|
3352
|
+
button, input, select, textarea { transition: border-color 140ms ease, box-shadow 140ms ease, transform 100ms ease; }
|
|
3353
|
+
button:disabled, input:disabled, select:disabled, textarea:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
3122
3354
|
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
3123
3355
|
header.top {
|
|
3124
|
-
position: sticky; top: 0; z-index:
|
|
3356
|
+
position: sticky; top: 0; z-index: 20;
|
|
3125
3357
|
display: flex; align-items: center; gap: 12px;
|
|
3126
|
-
min-height: var(--header); padding:
|
|
3127
|
-
background: var(--bg-elev);
|
|
3358
|
+
min-height: var(--header); padding: 12px 20px;
|
|
3359
|
+
background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
|
|
3360
|
+
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
3128
3361
|
border-bottom: 1px solid var(--line);
|
|
3129
3362
|
min-width: 0; max-width: 100%;
|
|
3130
3363
|
}
|
|
@@ -3132,12 +3365,21 @@ var renderAdminDocument = (service) => {
|
|
|
3132
3365
|
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
3133
3366
|
min-width: 0; max-width: 100%; margin-right: auto;
|
|
3134
3367
|
}
|
|
3135
|
-
.brand-
|
|
3136
|
-
.brand-
|
|
3368
|
+
.brand-lockup { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
3369
|
+
.brand-mark {
|
|
3370
|
+
width: 38px; height: 38px; display: grid; place-items: center; flex: none;
|
|
3371
|
+
border-radius: 11px; color: #fff;
|
|
3372
|
+
background: linear-gradient(145deg, #7f56d9 0%, #53389e 100%);
|
|
3373
|
+
box-shadow: 0 6px 16px rgb(105 65 198 / 0.24), inset 0 1px 0 rgb(255 255 255 / 0.25);
|
|
3374
|
+
}
|
|
3375
|
+
.brand-mark svg { width: 22px; height: 22px; }
|
|
3376
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; line-height: 1.25; }
|
|
3377
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
3137
3378
|
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
3138
3379
|
.vendor {
|
|
3139
3380
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
3140
3381
|
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
3382
|
+
padding: 5px 8px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-inset);
|
|
3141
3383
|
}
|
|
3142
3384
|
.vendor[hidden] { display: none; }
|
|
3143
3385
|
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
@@ -3149,12 +3391,17 @@ var renderAdminDocument = (service) => {
|
|
|
3149
3391
|
.vendor-sep { color: var(--faint); }
|
|
3150
3392
|
.vendor-link { color: inherit; text-decoration: none; }
|
|
3151
3393
|
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
3152
|
-
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items:
|
|
3153
|
-
|
|
3394
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
|
|
3395
|
+
.sync-status { align-self: center; display: flex; align-items: center; gap: 6px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3396
|
+
.sync-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
|
|
3397
|
+
.sync-status.busy::before { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); animation: pulse 1.2s ease-in-out infinite; }
|
|
3398
|
+
label.field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; font-weight: 600; color: var(--faint); }
|
|
3154
3399
|
label.field input, label.field select, .controls input[type="password"] {
|
|
3155
|
-
background-color: var(--bg-
|
|
3156
|
-
padding:
|
|
3400
|
+
background-color: var(--bg-elev); border: 1px solid var(--line-strong); border-radius: 9px;
|
|
3401
|
+
padding: 8px 10px; min-height: 38px; line-height: 1.2; box-shadow: var(--shadow-xs);
|
|
3157
3402
|
}
|
|
3403
|
+
label.field input:hover, label.field select:hover, textarea:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--line-strong)); }
|
|
3404
|
+
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; }
|
|
3158
3405
|
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
3159
3406
|
the text fields in the same form. Paint it like those fields. */
|
|
3160
3407
|
label.field select {
|
|
@@ -3171,12 +3418,20 @@ var renderAdminDocument = (service) => {
|
|
|
3171
3418
|
dialog .field > textarea { width: 100%; }
|
|
3172
3419
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
3173
3420
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
.btn
|
|
3421
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
|
3422
|
+
border-radius: 9px; min-height: 38px; padding: 0 13px; border: 1px solid var(--line-strong);
|
|
3423
|
+
background: var(--bg-elev); color: var(--ink); font-size: 13px; font-weight: 600; line-height: 1;
|
|
3424
|
+
box-shadow: var(--shadow-xs);
|
|
3425
|
+
}
|
|
3426
|
+
.icon-btn { width: 38px; padding: 0; }
|
|
3427
|
+
.icon-btn svg, .btn svg, .btn-primary svg { width: 16px; height: 16px; }
|
|
3428
|
+
.icon-btn:hover, .btn:hover { background: var(--bg-muted); border-color: var(--line-strong); }
|
|
3429
|
+
.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; }
|
|
3430
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
3431
|
+
.btn-quiet { background: transparent; border-color: transparent; box-shadow: none; }
|
|
3432
|
+
.btn-quiet:hover { background: var(--bg-muted); }
|
|
3433
|
+
.btn-danger { color: var(--err); border-color: color-mix(in srgb, var(--err) 25%, var(--line)); }
|
|
3434
|
+
.btn-danger:hover, .btn-danger.confirming { background: var(--err-soft); border-color: color-mix(in srgb, var(--err) 45%, var(--line)); }
|
|
3180
3435
|
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
3181
3436
|
so the press itself has to stay visible. */
|
|
3182
3437
|
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
@@ -3192,93 +3447,159 @@ var renderAdminDocument = (service) => {
|
|
|
3192
3447
|
flex: 1 0 auto; min-width: 0;
|
|
3193
3448
|
}
|
|
3194
3449
|
nav.side {
|
|
3195
|
-
|
|
3450
|
+
position: sticky; top: var(--header); align-self: start; height: calc(100vh - var(--header));
|
|
3451
|
+
border-right: 1px solid var(--line); padding: 18px 12px;
|
|
3196
3452
|
display: flex; flex-direction: column; gap: 4px;
|
|
3197
|
-
background: var(--bg-elev);
|
|
3453
|
+
background: var(--bg-elev); overflow-y: auto;
|
|
3198
3454
|
}
|
|
3455
|
+
.nav-label { padding: 0 10px 8px; color: var(--faint); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
|
|
3199
3456
|
nav.side button {
|
|
3457
|
+
position: relative; display: flex; align-items: center; gap: 10px; width: 100%;
|
|
3200
3458
|
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
3201
|
-
padding: 9px 10px; min-height:
|
|
3202
|
-
}
|
|
3203
|
-
nav.side button
|
|
3204
|
-
|
|
3205
|
-
.
|
|
3206
|
-
.
|
|
3207
|
-
.
|
|
3459
|
+
padding: 9px 10px; min-height: 42px; color: var(--muted); font-size: 13px; font-weight: 550;
|
|
3460
|
+
}
|
|
3461
|
+
nav.side button:hover { background: var(--bg-muted); color: var(--ink); }
|
|
3462
|
+
nav.side button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 650; }
|
|
3463
|
+
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); }
|
|
3464
|
+
.nav-icon { width: 18px; height: 18px; display: grid; place-items: center; flex: none; }
|
|
3465
|
+
.nav-icon svg { width: 18px; height: 18px; }
|
|
3466
|
+
main { padding: 32px clamp(20px, 4vw, 48px) 64px; max-width: 1320px; width: 100%; min-width: 0; }
|
|
3467
|
+
main[aria-busy="true"] .view { opacity: 0.62; }
|
|
3468
|
+
.view { animation: view-in 180ms ease-out; }
|
|
3469
|
+
.view[hidden] { display: none; }
|
|
3470
|
+
.view-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
|
|
3471
|
+
.view h2 { margin: 0 0 5px; font-size: clamp(24px, 3vw, 30px); line-height: 1.2; letter-spacing: -0.035em; }
|
|
3472
|
+
.view h3 { margin: 24px 0 10px; font-size: 15px; letter-spacing: -0.01em; }
|
|
3473
|
+
.lede { margin: 0; color: var(--muted); max-width: 72ch; font-size: 14px; }
|
|
3474
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 175px), 1fr)); gap: 14px; }
|
|
3208
3475
|
.card {
|
|
3209
3476
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
3210
|
-
padding:
|
|
3477
|
+
padding: 18px; box-shadow: var(--shadow-sm);
|
|
3211
3478
|
}
|
|
3212
|
-
.card
|
|
3213
|
-
.card
|
|
3479
|
+
.stat-card { position: relative; overflow: hidden; }
|
|
3480
|
+
.stat-card::after { content: ""; position: absolute; inset: 0 0 auto; height: 2px; background: linear-gradient(90deg, var(--accent), transparent 78%); opacity: 0.7; }
|
|
3481
|
+
.card .k { color: var(--faint); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
3482
|
+
.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; }
|
|
3214
3483
|
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
3215
|
-
.
|
|
3484
|
+
.section-card { background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-xs); overflow: hidden; }
|
|
3485
|
+
.section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
|
|
3486
|
+
.section-title h3 { margin: 0; }
|
|
3487
|
+
.split { display: grid; grid-template-columns: minmax(0, 250px) minmax(0, 1fr); gap: 18px; align-items: start; }
|
|
3488
|
+
#collections { padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); box-shadow: var(--shadow-xs); }
|
|
3216
3489
|
.collection {
|
|
3217
3490
|
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
3218
|
-
border-radius:
|
|
3491
|
+
border-radius: 9px; padding: 10px 11px; min-height: 48px; color: var(--ink); cursor: pointer;
|
|
3219
3492
|
}
|
|
3220
|
-
.collection
|
|
3493
|
+
.collection:hover { background: var(--bg-muted); }
|
|
3494
|
+
.collection[aria-current="true"] { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 18%, var(--line)); color: var(--accent); }
|
|
3221
3495
|
.collection small { display: block; color: var(--faint); }
|
|
3222
3496
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
3223
|
-
.chip {
|
|
3224
|
-
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
3225
|
-
border-radius: 999px; padding: 4px 8px;
|
|
3226
|
-
}
|
|
3497
|
+
.chip { font-family: var(--mono); font-size: 11px; background: var(--bg-muted); border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; }
|
|
3227
3498
|
.chip i { color: var(--faint); font-style: normal; }
|
|
3228
3499
|
.sql-layout {
|
|
3229
3500
|
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
3230
3501
|
}
|
|
3231
|
-
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
3232
|
-
.sql-tables button { text-align: left; }
|
|
3502
|
+
.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); }
|
|
3503
|
+
.sql-tables button { text-align: left; border: 0; border-radius: 8px; padding: 9px 10px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
3504
|
+
.sql-tables button:hover, .sql-tables button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); }
|
|
3233
3505
|
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
3234
|
-
table { width: 100%; border-
|
|
3235
|
-
|
|
3236
|
-
th {
|
|
3506
|
+
.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); }
|
|
3507
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
3508
|
+
th, td { text-align: left; padding: 11px 13px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
3509
|
+
th { color: var(--faint); background: var(--bg-inset); font-weight: 650; font-size: 11px; text-transform: uppercase; letter-spacing: 0.045em; }
|
|
3510
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
3237
3511
|
tr.clickable { cursor: pointer; }
|
|
3238
|
-
tr.clickable:hover { background: var(--
|
|
3512
|
+
tr.clickable:hover { background: var(--accent-soft); }
|
|
3239
3513
|
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
3240
3514
|
pre {
|
|
3241
3515
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
3242
|
-
background: var(--bg-inset); border-radius: 10px; padding:
|
|
3243
|
-
}
|
|
3244
|
-
.
|
|
3516
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 10px; padding: 14px; font-size: 12px; line-height: 1.65;
|
|
3517
|
+
}
|
|
3518
|
+
.json-key { color: var(--code-key); }
|
|
3519
|
+
.json-string { color: var(--code-string); }
|
|
3520
|
+
.json-number { color: var(--code-number); }
|
|
3521
|
+
.json-boolean { color: var(--code-boolean); }
|
|
3522
|
+
.json-null { color: var(--code-null); font-style: italic; }
|
|
3523
|
+
.json-inline { display: block; max-width: 44rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
3524
|
+
.json-view { position: relative; max-height: 26rem; overflow: auto; }
|
|
3525
|
+
.json-editor { position: relative; min-height: 190px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--bg-inset); overflow: hidden; }
|
|
3526
|
+
.json-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); }
|
|
3527
|
+
.json-editor.invalid { border-color: var(--err); }
|
|
3528
|
+
.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; }
|
|
3529
|
+
.json-editor pre { position: absolute; inset: 0; pointer-events: none; background: transparent; }
|
|
3530
|
+
.json-editor textarea { position: relative; z-index: 1; resize: vertical; background: transparent; color: transparent; caret-color: var(--ink); -webkit-text-fill-color: transparent; }
|
|
3531
|
+
.json-editor textarea::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); }
|
|
3532
|
+
.editor-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; }
|
|
3533
|
+
.json-status { display: inline-flex; align-items: center; gap: 6px; color: var(--ok); font-size: 11px; }
|
|
3534
|
+
.json-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
3535
|
+
.json-status.invalid { color: var(--err); }
|
|
3536
|
+
.format-json { border: 0; padding: 3px 5px; background: transparent; color: var(--accent); font-size: 11px; font-weight: 650; cursor: pointer; }
|
|
3537
|
+
.row-actions { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 14px 0; }
|
|
3245
3538
|
.banner {
|
|
3246
|
-
display: none;
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3539
|
+
display: none; position: sticky; top: calc(var(--header) + 12px); z-index: 15;
|
|
3540
|
+
align-items: center; gap: 10px; margin: 0 0 16px; padding: 11px 12px 11px 14px;
|
|
3541
|
+
border: 1px solid color-mix(in srgb, var(--err) 30%, var(--line)); border-radius: 10px;
|
|
3542
|
+
background: var(--err-soft); color: var(--err); box-shadow: var(--shadow-sm); font-size: 13px;
|
|
3543
|
+
}
|
|
3544
|
+
.banner.show { display: flex; }
|
|
3545
|
+
.banner span { flex: 1; }
|
|
3546
|
+
.banner button { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 7px; background: transparent; color: currentColor; cursor: pointer; }
|
|
3547
|
+
.banner button:hover { background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
3548
|
+
.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); }
|
|
3251
3549
|
dialog {
|
|
3252
|
-
border: 1px solid var(--line); border-radius:
|
|
3253
|
-
color: var(--ink); width: min(
|
|
3254
|
-
}
|
|
3255
|
-
dialog
|
|
3256
|
-
dialog
|
|
3550
|
+
border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--bg-elev);
|
|
3551
|
+
color: var(--ink); width: min(720px, calc(100% - 24px)); max-width: calc(100vw - 24px); box-shadow: var(--shadow-lg);
|
|
3552
|
+
}
|
|
3553
|
+
dialog[open] { animation: dialog-in 160ms ease-out; }
|
|
3554
|
+
dialog::backdrop { background: rgb(12 14 20 / 0.58); backdrop-filter: blur(3px); }
|
|
3555
|
+
dialog form, .dialog-body { padding: 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
3556
|
+
.dialog-heading { display: flex; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
|
3557
|
+
.dialog-heading strong { font-size: 18px; letter-spacing: -0.02em; }
|
|
3558
|
+
.dialog-heading span { color: var(--muted); font-size: 12px; }
|
|
3257
3559
|
textarea {
|
|
3258
3560
|
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
3259
3561
|
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
3260
3562
|
font-family: var(--mono); font-size: 12px;
|
|
3261
3563
|
}
|
|
3262
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
3263
|
-
.
|
|
3564
|
+
.dialog-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding-top: 4px; }
|
|
3565
|
+
.dialog-actions .btn-danger { margin-right: auto; }
|
|
3566
|
+
.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); }
|
|
3264
3567
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
3265
3568
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
3266
3569
|
.status-2 { color: var(--ok); }
|
|
3267
3570
|
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
3268
3571
|
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
3572
|
+
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
3573
|
+
.form-grid .wide { grid-column: span 2; }
|
|
3269
3574
|
.route {
|
|
3270
|
-
display: grid; grid-template-columns:
|
|
3271
|
-
padding:
|
|
3272
|
-
}
|
|
3273
|
-
.
|
|
3575
|
+
display: grid; grid-template-columns: 84px minmax(0, 1fr) auto; gap: 10px; align-items: center;
|
|
3576
|
+
padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--bg-elev);
|
|
3577
|
+
}
|
|
3578
|
+
.route:first-child { border-radius: var(--radius) var(--radius) 0 0; }
|
|
3579
|
+
.route:last-child { border-bottom: 0; border-radius: 0 0 var(--radius) var(--radius); }
|
|
3580
|
+
.route-tools { display: flex; align-items: flex-end; gap: 12px; margin-bottom: 12px; }
|
|
3581
|
+
.route-tools .field { flex: 1; }
|
|
3582
|
+
.route-tools input { width: 100%; }
|
|
3583
|
+
.route-count { padding-bottom: 10px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3584
|
+
#route-list { max-height: 34rem; border: 1px solid var(--line); border-radius: var(--radius); overflow: auto; box-shadow: var(--shadow-xs); }
|
|
3585
|
+
.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; }
|
|
3586
|
+
@keyframes view-in { from { transform: translateY(3px); } to { transform: translateY(0); } }
|
|
3587
|
+
@keyframes dialog-in { from { transform: translateY(8px) scale(0.99); } to { transform: translateY(0) scale(1); } }
|
|
3588
|
+
@keyframes pulse { 50% { opacity: 0.45; } }
|
|
3589
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
|
|
3590
|
+
@media (forced-colors: active) { .json-editor pre { display: none; } .json-editor textarea { color: CanvasText; -webkit-text-fill-color: CanvasText; } }
|
|
3274
3591
|
@media (max-width: 800px) {
|
|
3592
|
+
:root { --header: 0px; }
|
|
3275
3593
|
header.top {
|
|
3276
|
-
align-items: stretch; flex-direction: column;
|
|
3277
|
-
padding
|
|
3594
|
+
position: relative; align-items: stretch; flex-direction: column;
|
|
3595
|
+
padding: max(12px, env(safe-area-inset-top)) 14px 12px;
|
|
3278
3596
|
}
|
|
3279
3597
|
input, select, textarea { font-size: 16px; }
|
|
3280
3598
|
.brand, .vendor, .controls { width: 100%; }
|
|
3599
|
+
.brand { gap: 10px; }
|
|
3600
|
+
.vendor { order: 2; border-radius: 9px; }
|
|
3281
3601
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
3602
|
+
.sync-status { display: none; }
|
|
3282
3603
|
/* The layout fills the screen. A single-column grid stretches every auto
|
|
3283
3604
|
row, so the tab bar grew with the leftover height and the buttons floated
|
|
3284
3605
|
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
@@ -3288,14 +3609,23 @@ var renderAdminDocument = (service) => {
|
|
|
3288
3609
|
align-content: start;
|
|
3289
3610
|
}
|
|
3290
3611
|
nav.side {
|
|
3291
|
-
align-self: start; height: auto;
|
|
3612
|
+
position: sticky; top: 0; z-index: 12; align-self: start; height: auto;
|
|
3292
3613
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
3293
3614
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
3294
|
-
padding: 8px;
|
|
3615
|
+
padding: 8px; box-shadow: var(--shadow-xs);
|
|
3295
3616
|
}
|
|
3296
|
-
nav
|
|
3617
|
+
.nav-label { display: none; }
|
|
3618
|
+
nav.side button { width: auto; white-space: nowrap; flex: none; }
|
|
3619
|
+
nav.side button[aria-current="true"]::before { inset: auto 9px -8px; width: auto; height: 3px; border-radius: 3px 3px 0 0; }
|
|
3297
3620
|
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
3298
|
-
|
|
3621
|
+
.form-grid { grid-template-columns: minmax(0, 1fr); }
|
|
3622
|
+
.form-grid .wide { grid-column: auto; }
|
|
3623
|
+
main { padding: 22px 14px 48px; }
|
|
3624
|
+
.view-head { margin-bottom: 18px; }
|
|
3625
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
3626
|
+
.card { padding: 14px; }
|
|
3627
|
+
.card .v { font-size: 20px; }
|
|
3628
|
+
.table-shell { border: 0; background: transparent; box-shadow: none; overflow: visible; }
|
|
3299
3629
|
table, thead, tbody, tr, th, td { display: block; }
|
|
3300
3630
|
thead { display: none; }
|
|
3301
3631
|
tr {
|
|
@@ -3310,50 +3640,63 @@ var renderAdminDocument = (service) => {
|
|
|
3310
3640
|
.route { grid-template-columns: 72px 1fr; }
|
|
3311
3641
|
.route button { grid-column: 1 / -1; }
|
|
3312
3642
|
}
|
|
3643
|
+
@media (max-width: 460px) {
|
|
3644
|
+
.controls label.field { min-width: calc(50% - 4px); }
|
|
3645
|
+
.controls .icon-btn, .controls .btn { flex: 1; }
|
|
3646
|
+
.grid { grid-template-columns: minmax(0, 1fr); }
|
|
3647
|
+
.dialog-actions { flex-wrap: wrap; }
|
|
3648
|
+
.dialog-actions button { flex: 1; }
|
|
3649
|
+
.dialog-actions .btn-danger { flex-basis: 100%; margin-right: 0; }
|
|
3650
|
+
}
|
|
3313
3651
|
</style>
|
|
3314
3652
|
</head>
|
|
3315
3653
|
<body>
|
|
3316
3654
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
3317
3655
|
<header class="top">
|
|
3318
3656
|
<div class="brand">
|
|
3319
|
-
<div class="brand-
|
|
3320
|
-
<
|
|
3321
|
-
|
|
3657
|
+
<div class="brand-lockup">
|
|
3658
|
+
<div class="brand-mark" aria-hidden="true">
|
|
3659
|
+
<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>
|
|
3660
|
+
</div>
|
|
3661
|
+
<div class="brand-id">
|
|
3662
|
+
<strong>${name}</strong>
|
|
3663
|
+
<span>Mock admin</span>
|
|
3664
|
+
</div>
|
|
3322
3665
|
</div>
|
|
3323
3666
|
<div class="vendor" id="vendor" hidden></div>
|
|
3324
3667
|
</div>
|
|
3325
3668
|
<div class="controls">
|
|
3669
|
+
<span class="sync-status" id="sync-status" role="status" aria-live="polite">Ready</span>
|
|
3326
3670
|
<label class="field">Namespace
|
|
3327
3671
|
<select id="namespace" aria-label="Namespace"></select>
|
|
3328
3672
|
</label>
|
|
3329
3673
|
<label class="field">Admin key
|
|
3330
3674
|
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
3331
3675
|
</label>
|
|
3332
|
-
<button class="icon-btn" id="theme" type="button" aria-label="
|
|
3333
|
-
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
3676
|
+
<button class="icon-btn" id="theme" type="button" aria-label="Use dark theme" title="Use dark theme"></button>
|
|
3677
|
+
<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>
|
|
3334
3678
|
</div>
|
|
3335
3679
|
</header>
|
|
3336
3680
|
<div class="layout">
|
|
3337
3681
|
<nav class="side" id="nav" aria-label="Admin sections">
|
|
3338
|
-
<
|
|
3339
|
-
<button type="button" data-view="
|
|
3340
|
-
<button type="button" data-view="
|
|
3341
|
-
<button type="button" data-view="
|
|
3342
|
-
<button type="button" data-view="
|
|
3343
|
-
<button type="button" data-view="
|
|
3682
|
+
<span class="nav-label">Workspace</span>
|
|
3683
|
+
<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>
|
|
3684
|
+
<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>
|
|
3685
|
+
<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>
|
|
3686
|
+
<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>
|
|
3687
|
+
<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>
|
|
3688
|
+
<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>
|
|
3344
3689
|
</nav>
|
|
3345
3690
|
<main id="views">
|
|
3346
|
-
<
|
|
3691
|
+
<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>
|
|
3347
3692
|
<section class="view" id="view-overview">
|
|
3348
|
-
<h2>Overview</h2>
|
|
3349
|
-
<p class="lede">Health, clock, and the collections this namespace is holding.</p>
|
|
3693
|
+
<div class="view-head"><div><h2>Overview</h2><p class="lede">Health, clock, and the collections this namespace is holding.</p></div></div>
|
|
3350
3694
|
<div class="grid" id="overview-cards"></div>
|
|
3351
3695
|
<h3>Recent requests</h3>
|
|
3352
3696
|
<div id="overview-requests"></div>
|
|
3353
3697
|
</section>
|
|
3354
3698
|
<section class="view" id="view-state" hidden>
|
|
3355
|
-
<h2>State</h2>
|
|
3356
|
-
<p class="lede">Every collection in this namespace. Declared fields stay visible before any row exists; stored rows add whatever else they contain.</p>
|
|
3699
|
+
<div class="view-head"><div><h2>State</h2><p class="lede">Inspect and edit every collection in this namespace.</p></div></div>
|
|
3357
3700
|
<div class="split">
|
|
3358
3701
|
<div id="collections"></div>
|
|
3359
3702
|
<div>
|
|
@@ -3366,8 +3709,7 @@ var renderAdminDocument = (service) => {
|
|
|
3366
3709
|
</div>
|
|
3367
3710
|
</section>
|
|
3368
3711
|
<section class="view" id="view-clock" hidden>
|
|
3369
|
-
<h2>Clock</h2>
|
|
3370
|
-
<p class="lede">The clock every timestamp in this mock reads.</p>
|
|
3712
|
+
<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>
|
|
3371
3713
|
<div class="card" id="clock-readout"></div>
|
|
3372
3714
|
<div class="row-actions">
|
|
3373
3715
|
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
@@ -3386,21 +3728,22 @@ var renderAdminDocument = (service) => {
|
|
|
3386
3728
|
</form>
|
|
3387
3729
|
</section>
|
|
3388
3730
|
<section class="view" id="view-faults" hidden>
|
|
3389
|
-
<h2>Faults</h2>
|
|
3390
|
-
<p class="lede">Inject a response, a delay, or a named preset. Rules apply to this namespace.</p>
|
|
3731
|
+
<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>
|
|
3391
3732
|
<form id="fault-form" class="card stack">
|
|
3392
|
-
<
|
|
3393
|
-
<
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
<
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
<
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
<
|
|
3403
|
-
|
|
3733
|
+
<div class="form-grid">
|
|
3734
|
+
<label class="field wide">Preset
|
|
3735
|
+
<select id="fault-preset"><option value="">Custom rule</option></select>
|
|
3736
|
+
</label>
|
|
3737
|
+
<label class="field">Status
|
|
3738
|
+
<input id="fault-status" inputmode="numeric" placeholder="503">
|
|
3739
|
+
</label>
|
|
3740
|
+
<label class="field wide">Path prefix
|
|
3741
|
+
<input id="fault-path" placeholder="/v1">
|
|
3742
|
+
</label>
|
|
3743
|
+
<label class="field">Method
|
|
3744
|
+
<input id="fault-method" placeholder="GET">
|
|
3745
|
+
</label>
|
|
3746
|
+
</div>
|
|
3404
3747
|
<div class="dialog-actions">
|
|
3405
3748
|
<button class="btn-primary" type="submit">Add fault</button>
|
|
3406
3749
|
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
@@ -3409,26 +3752,31 @@ var renderAdminDocument = (service) => {
|
|
|
3409
3752
|
<div id="fault-list"></div>
|
|
3410
3753
|
</section>
|
|
3411
3754
|
<section class="view" id="view-journal" hidden>
|
|
3412
|
-
<h2>Journal</h2>
|
|
3413
|
-
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
3414
|
-
<div class="row-actions">
|
|
3415
|
-
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
3416
|
-
</div>
|
|
3755
|
+
<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>
|
|
3417
3756
|
<div id="journal"></div>
|
|
3418
3757
|
</section>
|
|
3419
3758
|
<section class="view" id="view-routes" hidden>
|
|
3420
|
-
<h2>Routes</h2>
|
|
3421
|
-
<
|
|
3759
|
+
<div class="view-head"><div><h2>Routes</h2><p class="lede">Explore shared controls and service-specific admin endpoints.</p></div></div>
|
|
3760
|
+
<div class="route-tools">
|
|
3761
|
+
<label class="field">Filter routes
|
|
3762
|
+
<input id="route-search" type="search" placeholder="Method, path, or extension" autocomplete="off">
|
|
3763
|
+
</label>
|
|
3764
|
+
<span class="route-count" id="route-count"></span>
|
|
3765
|
+
</div>
|
|
3422
3766
|
<div id="route-list"></div>
|
|
3423
|
-
<form id="route-form" class="card stack">
|
|
3767
|
+
<form id="route-form" class="card stack" style="margin-top: 18px">
|
|
3424
3768
|
<strong id="route-title">Select a route</strong>
|
|
3425
3769
|
<label class="field">JSON body
|
|
3426
|
-
<
|
|
3770
|
+
<div class="json-editor" id="route-editor" hidden>
|
|
3771
|
+
<pre aria-hidden="true"><code id="route-highlight"></code></pre>
|
|
3772
|
+
<textarea id="route-body" placeholder="{ }" spellcheck="false" aria-describedby="route-json-status" disabled></textarea>
|
|
3773
|
+
</div>
|
|
3774
|
+
<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>
|
|
3427
3775
|
</label>
|
|
3428
3776
|
<div class="dialog-actions">
|
|
3429
|
-
<button class="btn-primary" type="submit">Send</button>
|
|
3777
|
+
<button class="btn-primary" id="route-send" type="submit" disabled>Send request</button>
|
|
3430
3778
|
</div>
|
|
3431
|
-
<pre id="route-result">Response will show here.</pre>
|
|
3779
|
+
<pre class="json-view" id="route-result"><code>Response will show here.</code></pre>
|
|
3432
3780
|
</form>
|
|
3433
3781
|
</section>
|
|
3434
3782
|
</main>
|
|
@@ -3436,12 +3784,16 @@ var renderAdminDocument = (service) => {
|
|
|
3436
3784
|
</div>
|
|
3437
3785
|
<dialog id="editor">
|
|
3438
3786
|
<form method="dialog" id="editor-form">
|
|
3439
|
-
<strong id="editor-title">Record</strong>
|
|
3787
|
+
<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>
|
|
3440
3788
|
<label class="field">Id
|
|
3441
3789
|
<input id="editor-id" required>
|
|
3442
3790
|
</label>
|
|
3443
3791
|
<label class="field">JSON value
|
|
3444
|
-
<
|
|
3792
|
+
<div class="json-editor" id="record-editor">
|
|
3793
|
+
<pre aria-hidden="true"><code id="editor-highlight"></code></pre>
|
|
3794
|
+
<textarea id="editor-value" spellcheck="false" aria-describedby="editor-json-status"></textarea>
|
|
3795
|
+
</div>
|
|
3796
|
+
<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>
|
|
3445
3797
|
</label>
|
|
3446
3798
|
<div class="dialog-actions">
|
|
3447
3799
|
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
@@ -3483,8 +3835,8 @@ var describeExtension = (extension) => {
|
|
|
3483
3835
|
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
3484
3836
|
};
|
|
3485
3837
|
};
|
|
3486
|
-
var adminUiRoutes = (service, ui) => {
|
|
3487
|
-
const shell = () => renderAdminDocument(service);
|
|
3838
|
+
var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
3839
|
+
const shell = () => renderAdminDocument(service, adminPrefix);
|
|
3488
3840
|
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
3489
3841
|
return {
|
|
3490
3842
|
"GET /ui": () => htmlResponse(document3()),
|
|
@@ -4175,10 +4527,13 @@ var stateAdminRoutes = (routes) => {
|
|
|
4175
4527
|
};
|
|
4176
4528
|
|
|
4177
4529
|
// ../core/dist/version.js
|
|
4178
|
-
var PACKAGE_VERSION = true ? "
|
|
4530
|
+
var PACKAGE_VERSION = true ? "2.0.0" : UNRELEASED_VERSION;
|
|
4179
4531
|
|
|
4180
4532
|
// ../core/dist/webhooks.js
|
|
4181
|
-
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4533
|
+
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4534
|
+
status,
|
|
4535
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4536
|
+
});
|
|
4182
4537
|
var adminError3 = (status, message) => json4(status, { error: { type: "mockingbird_admin", message } });
|
|
4183
4538
|
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4184
4539
|
var parseEndpoint = (value) => {
|
|
@@ -4286,7 +4641,6 @@ var AT_HEADER = "x-mockingbird-at";
|
|
|
4286
4641
|
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
4287
4642
|
var DEFAULT_NAMESPACE = "default";
|
|
4288
4643
|
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4289
|
-
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
4290
4644
|
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4291
4645
|
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
4292
4646
|
var effects = /* @__PURE__ */ new WeakMap();
|
|
@@ -4325,6 +4679,22 @@ var operationMatcher = (document3) => {
|
|
|
4325
4679
|
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
4326
4680
|
};
|
|
4327
4681
|
var createRuntime = (options) => {
|
|
4682
|
+
const adminPrefix = resolveAdminPrefix(options.adminPrefix);
|
|
4683
|
+
const internalPaths = Object.entries(options.document?.paths ?? {}).filter(([, item]) => item["x-mockingbird-internal"] === true).map(([path]) => path);
|
|
4684
|
+
for (const path of internalPaths) {
|
|
4685
|
+
if (!path.startsWith(`${ADMIN_PREFIX}/blobs/`))
|
|
4686
|
+
throw new Error(`internal transport ${path} must live under ${ADMIN_PREFIX}/blobs/`);
|
|
4687
|
+
}
|
|
4688
|
+
assertAdminPrefixAvailable(adminPrefix, Object.keys(options.document?.paths ?? {}).filter((path) => !internalPaths.includes(path)));
|
|
4689
|
+
const internalMatch = (path) => {
|
|
4690
|
+
for (const pattern of internalPaths) {
|
|
4691
|
+
const actual = path.split("/");
|
|
4692
|
+
const expected = pattern.split("/");
|
|
4693
|
+
if (actual.length === expected.length && expected.every((part, i) => part.startsWith("{") || part === actual[i]))
|
|
4694
|
+
return pattern;
|
|
4695
|
+
}
|
|
4696
|
+
return void 0;
|
|
4697
|
+
};
|
|
4328
4698
|
assertAdminUi(options.adminUi);
|
|
4329
4699
|
const sqlite = bootSqlite(options.sqlite);
|
|
4330
4700
|
const clock = options.clock ?? createClock();
|
|
@@ -4370,6 +4740,7 @@ var createRuntime = (options) => {
|
|
|
4370
4740
|
const created = options.create({
|
|
4371
4741
|
namespace: storageNamespace(key),
|
|
4372
4742
|
publicNamespace,
|
|
4743
|
+
adminPrefix,
|
|
4373
4744
|
sqlite,
|
|
4374
4745
|
clock: namespaceClock(publicNamespace),
|
|
4375
4746
|
rng: createdContextRng
|
|
@@ -4602,10 +4973,10 @@ var createRuntime = (options) => {
|
|
|
4602
4973
|
namespaceClock,
|
|
4603
4974
|
namespaceOf(request) {
|
|
4604
4975
|
const url = new URL(request.url);
|
|
4605
|
-
const prefixed =
|
|
4976
|
+
const prefixed = matchNamespacePath(url.pathname, adminPrefix);
|
|
4606
4977
|
const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
|
|
4607
4978
|
const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
|
|
4608
|
-
if (path
|
|
4979
|
+
if (isAdminPath(path, adminPrefix))
|
|
4609
4980
|
return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
|
|
4610
4981
|
if (selected !== void 0)
|
|
4611
4982
|
return selected;
|
|
@@ -4671,7 +5042,7 @@ var createRuntime = (options) => {
|
|
|
4671
5042
|
state: (namespace = DEFAULT_NAMESPACE) => inspectState(stateScope(namespace)),
|
|
4672
5043
|
fetch: async (incoming) => {
|
|
4673
5044
|
let request = incoming;
|
|
4674
|
-
const prefixed =
|
|
5045
|
+
const prefixed = matchNamespacePath(new URL(request.url).pathname, adminPrefix);
|
|
4675
5046
|
if (prefixed) {
|
|
4676
5047
|
const url2 = new URL(request.url);
|
|
4677
5048
|
url2.pathname = prefixed[2] ?? "/";
|
|
@@ -4687,7 +5058,15 @@ var createRuntime = (options) => {
|
|
|
4687
5058
|
signal: request.signal
|
|
4688
5059
|
});
|
|
4689
5060
|
}
|
|
4690
|
-
|
|
5061
|
+
const internalUrl = new URL(request.url);
|
|
5062
|
+
const internalPath = isAdminPath(internalUrl.pathname, adminPrefix) ? `${ADMIN_PREFIX}${internalUrl.pathname.slice(adminPrefix.length)}` : "";
|
|
5063
|
+
const internal = internalMatch(internalPath);
|
|
5064
|
+
const obsoleteInternalPath = adminPrefix !== ADMIN_PREFIX && internalMatch(internalUrl.pathname) && !internal;
|
|
5065
|
+
if (internal) {
|
|
5066
|
+
internalUrl.pathname = internalPath;
|
|
5067
|
+
request = new Request(internalUrl, request);
|
|
5068
|
+
}
|
|
5069
|
+
let namespace = internal ? internalUrl.searchParams.get("namespace") ?? control.namespaceOf(request) : control.namespaceOf(request);
|
|
4691
5070
|
if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
4692
5071
|
const credential = options.credential(request);
|
|
4693
5072
|
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
@@ -4707,7 +5086,9 @@ var createRuntime = (options) => {
|
|
|
4707
5086
|
return copy;
|
|
4708
5087
|
}
|
|
4709
5088
|
};
|
|
4710
|
-
|
|
5089
|
+
if (obsoleteInternalPath)
|
|
5090
|
+
return stamp(adminFail(404, "internal transport is only available under adminPrefix"));
|
|
5091
|
+
const handled = internal ? void 0 : await control.handle(request);
|
|
4711
5092
|
if (handled)
|
|
4712
5093
|
return stamp(handled);
|
|
4713
5094
|
const started = monotonicNow();
|
|
@@ -4748,7 +5129,7 @@ var createRuntime = (options) => {
|
|
|
4748
5129
|
type: "mockingbird_admin",
|
|
4749
5130
|
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
4750
5131
|
}
|
|
4751
|
-
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
5132
|
+
}), { status: 400, headers: { "content-type": "application/json; charset=utf-8" } }));
|
|
4752
5133
|
}
|
|
4753
5134
|
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
4754
5135
|
log(400);
|
|
@@ -4865,15 +5246,16 @@ var createRuntime = (options) => {
|
|
|
4865
5246
|
...options.admin?.(runtime) ?? {},
|
|
4866
5247
|
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
4867
5248
|
// maps `presets`, and a service route must not replace that list with a record.
|
|
4868
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
5249
|
+
...options.presets ? presetRoutes(options.presets, runtime, adminPrefix) : {},
|
|
4869
5250
|
...stateAdminRoutes({
|
|
4870
5251
|
open: stateScope,
|
|
4871
5252
|
afterWrite: (namespace) => {
|
|
4872
5253
|
checkpoint(namespace, "main");
|
|
4873
5254
|
}
|
|
4874
5255
|
}),
|
|
4875
|
-
...adminUiRoutes(options.name, options.adminUi)
|
|
5256
|
+
...adminUiRoutes(options.name, options.adminUi, adminPrefix)
|
|
4876
5257
|
},
|
|
5258
|
+
adminPrefix,
|
|
4877
5259
|
adminKey: options.adminKey
|
|
4878
5260
|
});
|
|
4879
5261
|
return defineMock(runtime);
|
|
@@ -4887,7 +5269,10 @@ var mutableResponse = (response) => {
|
|
|
4887
5269
|
return new Response(response.body, response);
|
|
4888
5270
|
}
|
|
4889
5271
|
};
|
|
4890
|
-
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5272
|
+
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5273
|
+
status,
|
|
5274
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
5275
|
+
});
|
|
4891
5276
|
var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
|
|
4892
5277
|
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4893
5278
|
var credentialRoutes = (registry) => ({
|
|
@@ -4939,7 +5324,7 @@ var credentialRoutes = (registry) => ({
|
|
|
4939
5324
|
return adminJson(200, { status: "ok" });
|
|
4940
5325
|
}
|
|
4941
5326
|
});
|
|
4942
|
-
var presetRoutes = (presets, runtime) => ({
|
|
5327
|
+
var presetRoutes = (presets, runtime, adminPrefix) => ({
|
|
4943
5328
|
"GET /faults/presets": () => {
|
|
4944
5329
|
const body = {
|
|
4945
5330
|
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
@@ -4949,7 +5334,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
4949
5334
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
4950
5335
|
const name = params.name;
|
|
4951
5336
|
if (!presets[name])
|
|
4952
|
-
return adminFail(404, `no fault preset ${name}; GET /
|
|
5337
|
+
return adminFail(404, `no fault preset ${name}; GET ${adminPrefix}/faults/presets`);
|
|
4953
5338
|
const overrides = isObject(body) ? body : {};
|
|
4954
5339
|
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
4955
5340
|
}
|
|
@@ -5114,6 +5499,7 @@ var createRuntime2 = (options = {}) => createRuntime({
|
|
|
5114
5499
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
5115
5500
|
...options.clock ? { clock: options.clock } : {},
|
|
5116
5501
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
5502
|
+
...options.adminPrefix !== void 0 ? { adminPrefix: options.adminPrefix } : {},
|
|
5117
5503
|
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
5118
5504
|
...options.onLog ? { onLog: options.onLog } : {},
|
|
5119
5505
|
credential: apiKeyCredential,
|
|
@@ -5353,6 +5739,7 @@ var EasyPostAPI = class {
|
|
|
5353
5739
|
|
|
5354
5740
|
export {
|
|
5355
5741
|
createClock,
|
|
5742
|
+
resolveAdminPrefix,
|
|
5356
5743
|
parseDuration,
|
|
5357
5744
|
document2 as document,
|
|
5358
5745
|
operationIds,
|
|
@@ -5368,4 +5755,4 @@ export {
|
|
|
5368
5755
|
isTrackerStatus,
|
|
5369
5756
|
EasyPostAPI
|
|
5370
5757
|
};
|
|
5371
|
-
//# sourceMappingURL=chunk-
|
|
5758
|
+
//# sourceMappingURL=chunk-32BNNR7Z.js.map
|