@crvouga/mockingbird-service-prism 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 +2 -2
- package/SUPPORT.md +21 -0
- package/dist/{chunk-EJSDM62Y.js → chunk-4RBQFCQP.js} +623 -236
- package/dist/chunk-4RBQFCQP.js.map +7 -0
- package/dist/{chunk-K3GI3XKR.js → chunk-EFVP6ZHO.js} +44 -24
- package/dist/chunk-EFVP6ZHO.js.map +7 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +4 -3
- package/dist/index.js +1 -1
- package/dist/server.d.ts +3 -1
- package/dist/server.js +2 -2
- package/openapi.yaml +719 -0
- package/package.json +29 -4
- package/dist/chunk-EJSDM62Y.js.map +0 -7
- package/dist/chunk-K3GI3XKR.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
|
};
|
|
@@ -1764,6 +1803,34 @@ dialog:not([open]) {
|
|
|
1764
1803
|
function isRecord2(value) {
|
|
1765
1804
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1766
1805
|
}
|
|
1806
|
+
function highlightJsonText(text) {
|
|
1807
|
+
const escapeToken = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
1808
|
+
switch (char) {
|
|
1809
|
+
case "&":
|
|
1810
|
+
return "&";
|
|
1811
|
+
case "<":
|
|
1812
|
+
return "<";
|
|
1813
|
+
case ">":
|
|
1814
|
+
return ">";
|
|
1815
|
+
case '"':
|
|
1816
|
+
return """;
|
|
1817
|
+
default:
|
|
1818
|
+
return "'";
|
|
1819
|
+
}
|
|
1820
|
+
});
|
|
1821
|
+
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;
|
|
1822
|
+
let cursor = 0;
|
|
1823
|
+
let html = "";
|
|
1824
|
+
for (const match of text.matchAll(token)) {
|
|
1825
|
+
const index = match.index ?? 0;
|
|
1826
|
+
const value = match[0];
|
|
1827
|
+
html += escapeToken(text.slice(cursor, index));
|
|
1828
|
+
const kind = value.startsWith('"') ? value.trimEnd().endsWith(":") ? "key" : "string" : value === "true" || value === "false" ? "boolean" : value === "null" ? "null" : "number";
|
|
1829
|
+
html += `<span class="json-${kind}">${escapeToken(value)}</span>`;
|
|
1830
|
+
cursor = index + value.length;
|
|
1831
|
+
}
|
|
1832
|
+
return html + escapeToken(text.slice(cursor));
|
|
1833
|
+
}
|
|
1767
1834
|
function mountAdminBrand(doc, host, service, catalog) {
|
|
1768
1835
|
const clip = (value, max) => {
|
|
1769
1836
|
if (typeof value !== "string")
|
|
@@ -2125,14 +2192,16 @@ function bootAdmin(config) {
|
|
|
2125
2192
|
const state = {
|
|
2126
2193
|
namespace: "default",
|
|
2127
2194
|
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
2128
|
-
view: "overview",
|
|
2195
|
+
view: ["overview", "state", "clock", "faults", "journal", "routes"].includes(location.hash.slice(1)) ? location.hash.slice(1) : "overview",
|
|
2129
2196
|
collection: "",
|
|
2130
2197
|
shape: null,
|
|
2131
2198
|
route: null,
|
|
2199
|
+
routes: [],
|
|
2132
2200
|
editing: null
|
|
2133
2201
|
};
|
|
2134
2202
|
const standard = new Set(config.standardRoutes);
|
|
2135
2203
|
const banner = el("banner", HTMLElement);
|
|
2204
|
+
const bannerMessage = el("banner-message", HTMLElement);
|
|
2136
2205
|
const keyInput = el("admin-key", HTMLInputElement);
|
|
2137
2206
|
keyInput.value = state.key;
|
|
2138
2207
|
const params = new URLSearchParams(location.search);
|
|
@@ -2143,18 +2212,21 @@ function bootAdmin(config) {
|
|
|
2143
2212
|
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
2144
2213
|
params.delete("key");
|
|
2145
2214
|
const next = params.toString();
|
|
2146
|
-
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
2215
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : "") + location.hash);
|
|
2147
2216
|
}
|
|
2148
2217
|
const queryNamespace = params.get("namespace");
|
|
2149
2218
|
if (queryNamespace)
|
|
2150
2219
|
state.namespace = queryNamespace;
|
|
2151
2220
|
const showError = (error) => {
|
|
2152
|
-
|
|
2221
|
+
bannerMessage.textContent = error instanceof Error ? error.message : String(error);
|
|
2153
2222
|
banner.classList.add("show");
|
|
2223
|
+
banner.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
2154
2224
|
};
|
|
2155
2225
|
const clearError = () => {
|
|
2156
2226
|
banner.classList.remove("show");
|
|
2227
|
+
bannerMessage.textContent = "";
|
|
2157
2228
|
};
|
|
2229
|
+
el("banner-dismiss", HTMLButtonElement).addEventListener("click", clearError);
|
|
2158
2230
|
const api = async (method, path, body) => {
|
|
2159
2231
|
const headers = { accept: "application/json" };
|
|
2160
2232
|
if (state.key !== "")
|
|
@@ -2198,16 +2270,21 @@ function bootAdmin(config) {
|
|
|
2198
2270
|
namespace: () => state.namespace
|
|
2199
2271
|
};
|
|
2200
2272
|
const isPanelScript = (value) => typeof value === "function";
|
|
2273
|
+
const themeButton = el("theme", HTMLButtonElement);
|
|
2201
2274
|
const setTheme = (theme) => {
|
|
2202
2275
|
if (theme)
|
|
2203
2276
|
document.documentElement.dataset.theme = theme;
|
|
2204
2277
|
else
|
|
2205
2278
|
delete document.documentElement.dataset.theme;
|
|
2279
|
+
const dark = theme ? theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2280
|
+
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>';
|
|
2281
|
+
const label = dark ? "Use light theme" : "Use dark theme";
|
|
2282
|
+
themeButton.setAttribute("aria-label", label);
|
|
2283
|
+
themeButton.title = label;
|
|
2206
2284
|
};
|
|
2207
2285
|
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
2286
|
+
setTheme(storedTheme === "dark" || storedTheme === "light" ? storedTheme : null);
|
|
2287
|
+
themeButton.addEventListener("click", () => {
|
|
2211
2288
|
const current = document.documentElement.dataset.theme;
|
|
2212
2289
|
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2213
2290
|
const next = dark ? "light" : "dark";
|
|
@@ -2226,6 +2303,7 @@ function bootAdmin(config) {
|
|
|
2226
2303
|
continue;
|
|
2227
2304
|
section.hidden = section.id !== `view-${view}`;
|
|
2228
2305
|
}
|
|
2306
|
+
history.replaceState(null, "", `${location.pathname}${location.search}#${encodeURIComponent(view)}`);
|
|
2229
2307
|
};
|
|
2230
2308
|
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
2231
2309
|
const target = event.target;
|
|
@@ -2240,7 +2318,7 @@ function bootAdmin(config) {
|
|
|
2240
2318
|
show(view);
|
|
2241
2319
|
refresh().catch(showError);
|
|
2242
2320
|
});
|
|
2243
|
-
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>`;
|
|
2321
|
+
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>`;
|
|
2244
2322
|
const table = (columns, rows) => {
|
|
2245
2323
|
if (rows.length === 0)
|
|
2246
2324
|
return '<p class="empty">Nothing here yet.</p>';
|
|
@@ -2250,16 +2328,22 @@ function bootAdmin(config) {
|
|
|
2250
2328
|
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
2251
2329
|
return `<tr${attrs}>${cells}</tr>`;
|
|
2252
2330
|
}).join("");
|
|
2253
|
-
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
2331
|
+
return `<div class="table-shell"><table><thead>${head}</thead><tbody>${body}</tbody></table></div>`;
|
|
2254
2332
|
};
|
|
2255
2333
|
const preview = (value) => {
|
|
2256
2334
|
const text = JSON.stringify(value) ?? "null";
|
|
2257
|
-
|
|
2335
|
+
const clipped = text.length > 180 ? `${text.slice(0, 180)}\u2026` : text;
|
|
2336
|
+
return `<code class="json-inline">${highlightJsonText(clipped)}</code>`;
|
|
2258
2337
|
};
|
|
2259
2338
|
const when = (epoch) => {
|
|
2260
2339
|
const date = new Date(epoch);
|
|
2261
2340
|
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
2262
2341
|
};
|
|
2342
|
+
const journalWhen = (value) => {
|
|
2343
|
+
const numeric = value.trim() === "" ? Number.NaN : Number(value);
|
|
2344
|
+
const epoch = Number.isFinite(numeric) ? numeric : Date.parse(value);
|
|
2345
|
+
return Number.isFinite(epoch) ? when(epoch) : value;
|
|
2346
|
+
};
|
|
2263
2347
|
const clockCard = (epoch) => {
|
|
2264
2348
|
const date = new Date(epoch);
|
|
2265
2349
|
if (Number.isNaN(date.getTime()))
|
|
@@ -2379,7 +2463,7 @@ function bootAdmin(config) {
|
|
|
2379
2463
|
};
|
|
2380
2464
|
const renderOverview = async () => {
|
|
2381
2465
|
const [health, shape, clock, journal] = await Promise.all([
|
|
2382
|
-
originGet(
|
|
2466
|
+
originGet(`${config.adminPrefix}/health`),
|
|
2383
2467
|
api("GET", "/state"),
|
|
2384
2468
|
api("GET", "/clock"),
|
|
2385
2469
|
api("GET", "/requests?limit=8")
|
|
@@ -2396,7 +2480,7 @@ function bootAdmin(config) {
|
|
|
2396
2480
|
},
|
|
2397
2481
|
{
|
|
2398
2482
|
k: "Status",
|
|
2399
|
-
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
2483
|
+
v: `<span class="badge ok">${esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")}</span>`
|
|
2400
2484
|
},
|
|
2401
2485
|
{ k: "Namespace", v: esc(state.namespace) },
|
|
2402
2486
|
{ k: "Collections", v: String(decoded.collections.length) },
|
|
@@ -2483,26 +2567,46 @@ function bootAdmin(config) {
|
|
|
2483
2567
|
const renderJournal = async () => {
|
|
2484
2568
|
const journal = await api("GET", "/requests?limit=100");
|
|
2485
2569
|
el("journal", HTMLElement).innerHTML = table([
|
|
2486
|
-
{ label: "When", cell: (row) => esc(row.at) },
|
|
2487
|
-
{
|
|
2570
|
+
{ label: "When", cell: (row) => esc(journalWhen(row.at)) },
|
|
2571
|
+
{
|
|
2572
|
+
label: "Method",
|
|
2573
|
+
cell: (row) => `<span class="method">${esc(row.method)}</span>`
|
|
2574
|
+
},
|
|
2488
2575
|
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2489
|
-
{
|
|
2576
|
+
{
|
|
2577
|
+
label: "Status",
|
|
2578
|
+
cell: (row) => `<span class="badge ${row.status.startsWith("2") ? "ok" : row.status.startsWith("4") || row.status.startsWith("5") ? "warn" : ""}">${esc(row.status)}</span>`
|
|
2579
|
+
},
|
|
2490
2580
|
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
2491
2581
|
], decodeJournal(journal));
|
|
2492
2582
|
};
|
|
2493
|
-
const
|
|
2494
|
-
const
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2583
|
+
const paintRoutes = () => {
|
|
2584
|
+
const query = el("route-search", HTMLInputElement).value.trim().toLocaleLowerCase();
|
|
2585
|
+
const routes = state.routes.filter((route) => route.toLocaleLowerCase().includes(query));
|
|
2586
|
+
el("route-count", HTMLElement).textContent = `${routes.length} of ${state.routes.length}`;
|
|
2587
|
+
if (state.routes.length === 0) {
|
|
2588
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No admin routes are available.</p>';
|
|
2589
|
+
return;
|
|
2590
|
+
}
|
|
2591
|
+
if (routes.length === 0) {
|
|
2592
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No routes match this filter.</p>';
|
|
2593
|
+
return;
|
|
2594
|
+
}
|
|
2498
2595
|
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2499
2596
|
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2500
2597
|
const parts = route.split(" ");
|
|
2501
2598
|
const method = parts[0] ?? "";
|
|
2502
2599
|
const path = parts.slice(1).join(" ");
|
|
2503
|
-
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>`;
|
|
2600
|
+
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>`;
|
|
2504
2601
|
}).join("");
|
|
2505
2602
|
};
|
|
2603
|
+
const renderRoutes = async () => {
|
|
2604
|
+
const data = await api("GET", "/");
|
|
2605
|
+
if (!isRecord2(data))
|
|
2606
|
+
throw new Error("Route list was not an object");
|
|
2607
|
+
state.routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
2608
|
+
paintRoutes();
|
|
2609
|
+
};
|
|
2506
2610
|
const renderers = {
|
|
2507
2611
|
overview: renderOverview,
|
|
2508
2612
|
state: renderState,
|
|
@@ -2511,19 +2615,50 @@ function bootAdmin(config) {
|
|
|
2511
2615
|
journal: renderJournal,
|
|
2512
2616
|
routes: renderRoutes
|
|
2513
2617
|
};
|
|
2618
|
+
let refreshToken = 0;
|
|
2514
2619
|
async function refresh() {
|
|
2620
|
+
const token = ++refreshToken;
|
|
2515
2621
|
clearError();
|
|
2622
|
+
const main = el("views", HTMLElement);
|
|
2623
|
+
const sync = el("sync-status", HTMLElement);
|
|
2624
|
+
main.setAttribute("aria-busy", "true");
|
|
2625
|
+
sync.classList.add("busy");
|
|
2626
|
+
sync.textContent = "Updating";
|
|
2516
2627
|
const render = renderers[state.view];
|
|
2517
|
-
|
|
2518
|
-
|
|
2628
|
+
try {
|
|
2629
|
+
if (render)
|
|
2630
|
+
await render();
|
|
2631
|
+
if (token === refreshToken)
|
|
2632
|
+
sync.textContent = "Up to date";
|
|
2633
|
+
} finally {
|
|
2634
|
+
if (token === refreshToken) {
|
|
2635
|
+
main.removeAttribute("aria-busy");
|
|
2636
|
+
sync.classList.remove("busy");
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2519
2639
|
}
|
|
2520
2640
|
const delay = (ms) => new Promise((resolve) => {
|
|
2521
2641
|
window.setTimeout(resolve, ms);
|
|
2522
2642
|
});
|
|
2523
|
-
const confirmAction = async (button, pending, done, work) => {
|
|
2643
|
+
const confirmAction = async (button, pending, done, work, confirmLabel) => {
|
|
2524
2644
|
if (button.dataset.pending === "1")
|
|
2525
2645
|
return;
|
|
2526
2646
|
const label = button.textContent ?? "";
|
|
2647
|
+
if (confirmLabel !== void 0 && button.dataset.confirming !== "1") {
|
|
2648
|
+
button.dataset.confirming = "1";
|
|
2649
|
+
button.classList.add("confirming");
|
|
2650
|
+
button.textContent = confirmLabel;
|
|
2651
|
+
window.setTimeout(() => {
|
|
2652
|
+
if (button.dataset.confirming !== "1")
|
|
2653
|
+
return;
|
|
2654
|
+
button.textContent = label;
|
|
2655
|
+
button.classList.remove("confirming");
|
|
2656
|
+
delete button.dataset.confirming;
|
|
2657
|
+
}, 3e3);
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2660
|
+
button.classList.remove("confirming");
|
|
2661
|
+
delete button.dataset.confirming;
|
|
2527
2662
|
const width = button.offsetWidth;
|
|
2528
2663
|
button.dataset.pending = "1";
|
|
2529
2664
|
button.classList.add("pressed");
|
|
@@ -2544,6 +2679,65 @@ function bootAdmin(config) {
|
|
|
2544
2679
|
delete button.dataset.pending;
|
|
2545
2680
|
}
|
|
2546
2681
|
};
|
|
2682
|
+
const wireJsonEditor = (textareaId, highlightId, statusId, shellId, allowEmpty) => {
|
|
2683
|
+
const textarea = el(textareaId, HTMLTextAreaElement);
|
|
2684
|
+
const highlight = el(highlightId, HTMLElement);
|
|
2685
|
+
const status = el(statusId, HTMLElement);
|
|
2686
|
+
const shell = el(shellId, HTMLElement);
|
|
2687
|
+
const sync = () => {
|
|
2688
|
+
const text = textarea.value;
|
|
2689
|
+
highlight.innerHTML = `${highlightJsonText(text)}
|
|
2690
|
+
`;
|
|
2691
|
+
let valid = true;
|
|
2692
|
+
let message = "Valid JSON";
|
|
2693
|
+
if (allowEmpty && text.trim() === "")
|
|
2694
|
+
message = "No body";
|
|
2695
|
+
else {
|
|
2696
|
+
try {
|
|
2697
|
+
readJson2(text);
|
|
2698
|
+
} catch {
|
|
2699
|
+
valid = false;
|
|
2700
|
+
message = "Invalid JSON";
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
shell.classList.toggle("invalid", !valid);
|
|
2704
|
+
status.classList.toggle("invalid", !valid);
|
|
2705
|
+
status.textContent = message;
|
|
2706
|
+
textarea.setAttribute("aria-invalid", valid ? "false" : "true");
|
|
2707
|
+
};
|
|
2708
|
+
const syncScroll = () => {
|
|
2709
|
+
const preview2 = highlight.parentElement;
|
|
2710
|
+
if (!(preview2 instanceof HTMLElement))
|
|
2711
|
+
return;
|
|
2712
|
+
preview2.scrollTop = textarea.scrollTop;
|
|
2713
|
+
preview2.scrollLeft = textarea.scrollLeft;
|
|
2714
|
+
};
|
|
2715
|
+
textarea.addEventListener("input", sync);
|
|
2716
|
+
textarea.addEventListener("scroll", syncScroll);
|
|
2717
|
+
sync();
|
|
2718
|
+
return sync;
|
|
2719
|
+
};
|
|
2720
|
+
const syncRouteEditor = wireJsonEditor("route-body", "route-highlight", "route-json-status", "route-editor", true);
|
|
2721
|
+
const syncRecordEditor = wireJsonEditor("editor-value", "editor-highlight", "editor-json-status", "record-editor", false);
|
|
2722
|
+
for (const button of document.querySelectorAll("[data-format]")) {
|
|
2723
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2724
|
+
continue;
|
|
2725
|
+
button.addEventListener("click", () => {
|
|
2726
|
+
const target = button.dataset.format;
|
|
2727
|
+
if (target === void 0)
|
|
2728
|
+
return;
|
|
2729
|
+
const textarea = document.getElementById(target);
|
|
2730
|
+
if (!(textarea instanceof HTMLTextAreaElement) || textarea.value.trim() === "")
|
|
2731
|
+
return;
|
|
2732
|
+
try {
|
|
2733
|
+
textarea.value = JSON.stringify(readJson2(textarea.value), null, 2) ?? "";
|
|
2734
|
+
textarea.dispatchEvent(new Event("input"));
|
|
2735
|
+
textarea.focus();
|
|
2736
|
+
} catch {
|
|
2737
|
+
showError(new Error("Fix the JSON before formatting it"));
|
|
2738
|
+
}
|
|
2739
|
+
});
|
|
2740
|
+
}
|
|
2547
2741
|
document.addEventListener("click", (event) => {
|
|
2548
2742
|
const target = event.target;
|
|
2549
2743
|
if (!(target instanceof Element))
|
|
@@ -2616,6 +2810,7 @@ function bootAdmin(config) {
|
|
|
2616
2810
|
} else {
|
|
2617
2811
|
editorValue.value = "{\n \n}";
|
|
2618
2812
|
}
|
|
2813
|
+
syncRecordEditor();
|
|
2619
2814
|
editor.showModal();
|
|
2620
2815
|
}
|
|
2621
2816
|
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -2645,10 +2840,10 @@ function bootAdmin(config) {
|
|
|
2645
2840
|
const id = state.editing;
|
|
2646
2841
|
if (!id)
|
|
2647
2842
|
return;
|
|
2648
|
-
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2843
|
+
confirmAction(editorDelete, "Deleting...", "Deleted", () => api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2649
2844
|
editor.close();
|
|
2650
2845
|
return renderState();
|
|
2651
|
-
}).catch(showError);
|
|
2846
|
+
}), "Confirm delete").catch(showError);
|
|
2652
2847
|
});
|
|
2653
2848
|
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2654
2849
|
if (!(button instanceof HTMLButtonElement))
|
|
@@ -2694,10 +2889,10 @@ function bootAdmin(config) {
|
|
|
2694
2889
|
}).then(() => renderFaults()).catch(showError);
|
|
2695
2890
|
});
|
|
2696
2891
|
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2697
|
-
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
|
|
2892
|
+
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults()), "Confirm clear").catch(showError);
|
|
2698
2893
|
});
|
|
2699
2894
|
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2700
|
-
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
|
|
2895
|
+
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal()), "Confirm clear").catch(showError);
|
|
2701
2896
|
});
|
|
2702
2897
|
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2703
2898
|
const target = event.target;
|
|
@@ -2711,9 +2906,15 @@ function bootAdmin(config) {
|
|
|
2711
2906
|
return;
|
|
2712
2907
|
state.route = route;
|
|
2713
2908
|
el("route-title", HTMLElement).textContent = route;
|
|
2909
|
+
el("route-send", HTMLButtonElement).disabled = false;
|
|
2714
2910
|
const method = route.split(" ")[0] ?? "";
|
|
2715
|
-
el("route-body", HTMLTextAreaElement)
|
|
2911
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
2912
|
+
routeBody.disabled = method === "GET" || method === "DELETE";
|
|
2913
|
+
el("route-editor", HTMLElement).hidden = routeBody.disabled;
|
|
2914
|
+
el("route-json-status", HTMLElement).parentElement?.toggleAttribute("hidden", routeBody.disabled);
|
|
2915
|
+
syncRouteEditor();
|
|
2716
2916
|
});
|
|
2917
|
+
el("route-search", HTMLInputElement).addEventListener("input", paintRoutes);
|
|
2717
2918
|
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2718
2919
|
event.preventDefault();
|
|
2719
2920
|
const send = async () => {
|
|
@@ -2735,7 +2936,8 @@ function bootAdmin(config) {
|
|
|
2735
2936
|
}
|
|
2736
2937
|
}
|
|
2737
2938
|
const data = await api(method, concrete, body);
|
|
2738
|
-
|
|
2939
|
+
const text = JSON.stringify(data, null, 2) ?? "null";
|
|
2940
|
+
el("route-result", HTMLElement).innerHTML = `<code>${highlightJsonText(text)}</code>`;
|
|
2739
2941
|
};
|
|
2740
2942
|
send().catch(showError);
|
|
2741
2943
|
});
|
|
@@ -2853,11 +3055,13 @@ function bootAdmin(config) {
|
|
|
2853
3055
|
views.append(section);
|
|
2854
3056
|
mountSqlExplorer(body, panelApi);
|
|
2855
3057
|
}
|
|
3058
|
+
show(document.getElementById(`view-${state.view}`) === null ? "overview" : state.view);
|
|
2856
3059
|
return refresh();
|
|
2857
3060
|
}).catch(showError);
|
|
2858
3061
|
}
|
|
2859
3062
|
var clientPrelude = [
|
|
2860
3063
|
isRecord2,
|
|
3064
|
+
highlightJsonText,
|
|
2861
3065
|
mountAdminBrand,
|
|
2862
3066
|
startAdminBrand,
|
|
2863
3067
|
faultPresetList,
|
|
@@ -2880,6 +3084,7 @@ ${entry}(${payload});
|
|
|
2880
3084
|
var defineMock = (runtime) => runtime;
|
|
2881
3085
|
var STANDARD_ADMIN_ROUTES = [
|
|
2882
3086
|
"GET /",
|
|
3087
|
+
"GET /health",
|
|
2883
3088
|
"POST /reset",
|
|
2884
3089
|
"GET /namespaces",
|
|
2885
3090
|
"GET /clock",
|
|
@@ -2959,11 +3164,11 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
2959
3164
|
}
|
|
2960
3165
|
});
|
|
2961
3166
|
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
2962
|
-
var renderAdminDocument = (service) => {
|
|
3167
|
+
var renderAdminDocument = (service, adminPrefix = ADMIN_PREFIX) => {
|
|
2963
3168
|
const name = escapeHtml(service);
|
|
2964
3169
|
const script = adminClientSource({
|
|
2965
3170
|
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
2966
|
-
adminPrefix
|
|
3171
|
+
adminPrefix,
|
|
2967
3172
|
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
2968
3173
|
brandsUrl: ADMIN_BRANDS_URL,
|
|
2969
3174
|
service
|
|
@@ -2980,75 +3185,99 @@ var renderAdminDocument = (service) => {
|
|
|
2980
3185
|
${CSS_RESET}
|
|
2981
3186
|
:root {
|
|
2982
3187
|
color-scheme: light;
|
|
2983
|
-
--bg: #
|
|
3188
|
+
--bg: #f7f8fb;
|
|
2984
3189
|
--bg-elev: #ffffff;
|
|
2985
|
-
--bg-muted: #
|
|
2986
|
-
--bg-inset: #
|
|
2987
|
-
--line: #
|
|
2988
|
-
--line-strong: #
|
|
2989
|
-
--ink: #
|
|
2990
|
-
--muted: #
|
|
2991
|
-
--faint: #
|
|
2992
|
-
--accent: #
|
|
3190
|
+
--bg-muted: #f1f3f7;
|
|
3191
|
+
--bg-inset: #f8f9fc;
|
|
3192
|
+
--line: #e4e7ec;
|
|
3193
|
+
--line-strong: #cfd4dc;
|
|
3194
|
+
--ink: #101828;
|
|
3195
|
+
--muted: #475467;
|
|
3196
|
+
--faint: #667085;
|
|
3197
|
+
--accent: #6941c6;
|
|
3198
|
+
--accent-hover: #5933b4;
|
|
2993
3199
|
--accent-ink: #ffffff;
|
|
2994
|
-
--accent-soft: #
|
|
2995
|
-
--ok: #
|
|
2996
|
-
--ok-soft: #
|
|
2997
|
-
--warn: #
|
|
2998
|
-
--warn-soft: #
|
|
2999
|
-
--err: #
|
|
3000
|
-
--err-soft: #
|
|
3001
|
-
--
|
|
3200
|
+
--accent-soft: #f4f0ff;
|
|
3201
|
+
--ok: #067647;
|
|
3202
|
+
--ok-soft: #ecfdf3;
|
|
3203
|
+
--warn: #b54708;
|
|
3204
|
+
--warn-soft: #fffaeb;
|
|
3205
|
+
--err: #b42318;
|
|
3206
|
+
--err-soft: #fef3f2;
|
|
3207
|
+
--code-key: #6941c6;
|
|
3208
|
+
--code-string: #067647;
|
|
3209
|
+
--code-number: #175cd3;
|
|
3210
|
+
--code-boolean: #c11574;
|
|
3211
|
+
--code-null: #667085;
|
|
3212
|
+
--shadow-xs: 0 1px 2px rgb(16 24 40 / 0.05);
|
|
3213
|
+
--shadow-sm: 0 1px 3px rgb(16 24 40 / 0.08), 0 1px 2px rgb(16 24 40 / 0.04);
|
|
3214
|
+
--shadow-lg: 0 18px 48px rgb(16 24 40 / 0.16), 0 4px 12px rgb(16 24 40 / 0.08);
|
|
3002
3215
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
3003
3216
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
3004
|
-
--radius:
|
|
3005
|
-
--header:
|
|
3006
|
-
--nav:
|
|
3217
|
+
--radius: 12px;
|
|
3218
|
+
--header: 72px;
|
|
3219
|
+
--nav: 224px;
|
|
3007
3220
|
}
|
|
3008
3221
|
:root[data-theme="dark"] {
|
|
3009
3222
|
color-scheme: dark;
|
|
3010
|
-
--bg: #
|
|
3011
|
-
--bg-elev: #
|
|
3012
|
-
--bg-muted: #
|
|
3013
|
-
--bg-inset: #
|
|
3014
|
-
--line: #
|
|
3015
|
-
--line-strong: #
|
|
3016
|
-
--ink: #
|
|
3017
|
-
--muted: #
|
|
3018
|
-
--faint: #
|
|
3019
|
-
--accent: #
|
|
3020
|
-
--accent-
|
|
3021
|
-
--accent-
|
|
3022
|
-
--
|
|
3023
|
-
--ok
|
|
3024
|
-
--
|
|
3025
|
-
--warn
|
|
3026
|
-
--
|
|
3027
|
-
--err
|
|
3028
|
-
--
|
|
3223
|
+
--bg: #0c0e14;
|
|
3224
|
+
--bg-elev: #151821;
|
|
3225
|
+
--bg-muted: #20232e;
|
|
3226
|
+
--bg-inset: #11141c;
|
|
3227
|
+
--line: #2b303d;
|
|
3228
|
+
--line-strong: #414858;
|
|
3229
|
+
--ink: #f5f7fa;
|
|
3230
|
+
--muted: #b6bdc9;
|
|
3231
|
+
--faint: #8d96a7;
|
|
3232
|
+
--accent: #9e77ed;
|
|
3233
|
+
--accent-hover: #b692f6;
|
|
3234
|
+
--accent-ink: #ffffff;
|
|
3235
|
+
--accent-soft: #2b2142;
|
|
3236
|
+
--ok: #75e0a7;
|
|
3237
|
+
--ok-soft: #102a20;
|
|
3238
|
+
--warn: #fec84b;
|
|
3239
|
+
--warn-soft: #302611;
|
|
3240
|
+
--err: #fda29b;
|
|
3241
|
+
--err-soft: #321817;
|
|
3242
|
+
--code-key: #c3a6ff;
|
|
3243
|
+
--code-string: #75e0a7;
|
|
3244
|
+
--code-number: #84adff;
|
|
3245
|
+
--code-boolean: #f9a8d4;
|
|
3246
|
+
--code-null: #98a2b3;
|
|
3247
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3248
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3249
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
3029
3250
|
}
|
|
3030
3251
|
@media (prefers-color-scheme: dark) {
|
|
3031
3252
|
:root:not([data-theme="light"]) {
|
|
3032
3253
|
color-scheme: dark;
|
|
3033
|
-
--bg: #
|
|
3034
|
-
--bg-elev: #
|
|
3035
|
-
--bg-muted: #
|
|
3036
|
-
--bg-inset: #
|
|
3037
|
-
--line: #
|
|
3038
|
-
--line-strong: #
|
|
3039
|
-
--ink: #
|
|
3040
|
-
--muted: #
|
|
3041
|
-
--faint: #
|
|
3042
|
-
--accent: #
|
|
3043
|
-
--accent-
|
|
3044
|
-
--accent-
|
|
3045
|
-
--
|
|
3046
|
-
--ok
|
|
3047
|
-
--
|
|
3048
|
-
--warn
|
|
3049
|
-
--
|
|
3050
|
-
--err
|
|
3051
|
-
--
|
|
3254
|
+
--bg: #0c0e14;
|
|
3255
|
+
--bg-elev: #151821;
|
|
3256
|
+
--bg-muted: #20232e;
|
|
3257
|
+
--bg-inset: #11141c;
|
|
3258
|
+
--line: #2b303d;
|
|
3259
|
+
--line-strong: #414858;
|
|
3260
|
+
--ink: #f5f7fa;
|
|
3261
|
+
--muted: #b6bdc9;
|
|
3262
|
+
--faint: #8d96a7;
|
|
3263
|
+
--accent: #9e77ed;
|
|
3264
|
+
--accent-hover: #b692f6;
|
|
3265
|
+
--accent-ink: #ffffff;
|
|
3266
|
+
--accent-soft: #2b2142;
|
|
3267
|
+
--ok: #75e0a7;
|
|
3268
|
+
--ok-soft: #102a20;
|
|
3269
|
+
--warn: #fec84b;
|
|
3270
|
+
--warn-soft: #302611;
|
|
3271
|
+
--err: #fda29b;
|
|
3272
|
+
--err-soft: #321817;
|
|
3273
|
+
--code-key: #c3a6ff;
|
|
3274
|
+
--code-string: #75e0a7;
|
|
3275
|
+
--code-number: #84adff;
|
|
3276
|
+
--code-boolean: #f9a8d4;
|
|
3277
|
+
--code-null: #98a2b3;
|
|
3278
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3279
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3280
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
3052
3281
|
}
|
|
3053
3282
|
}
|
|
3054
3283
|
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
@@ -3066,13 +3295,17 @@ var renderAdminDocument = (service) => {
|
|
|
3066
3295
|
padding-right: env(safe-area-inset-right);
|
|
3067
3296
|
-webkit-text-size-adjust: 100%;
|
|
3068
3297
|
}
|
|
3069
|
-
|
|
3298
|
+
::selection { background: color-mix(in srgb, var(--accent) 28%, transparent); }
|
|
3299
|
+
:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px; }
|
|
3300
|
+
button, input, select, textarea { transition: border-color 140ms ease, box-shadow 140ms ease, transform 100ms ease; }
|
|
3301
|
+
button:disabled, input:disabled, select:disabled, textarea:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
3070
3302
|
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
3071
3303
|
header.top {
|
|
3072
|
-
position: sticky; top: 0; z-index:
|
|
3304
|
+
position: sticky; top: 0; z-index: 20;
|
|
3073
3305
|
display: flex; align-items: center; gap: 12px;
|
|
3074
|
-
min-height: var(--header); padding:
|
|
3075
|
-
background: var(--bg-elev);
|
|
3306
|
+
min-height: var(--header); padding: 12px 20px;
|
|
3307
|
+
background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
|
|
3308
|
+
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
3076
3309
|
border-bottom: 1px solid var(--line);
|
|
3077
3310
|
min-width: 0; max-width: 100%;
|
|
3078
3311
|
}
|
|
@@ -3080,12 +3313,21 @@ var renderAdminDocument = (service) => {
|
|
|
3080
3313
|
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
3081
3314
|
min-width: 0; max-width: 100%; margin-right: auto;
|
|
3082
3315
|
}
|
|
3083
|
-
.brand-
|
|
3084
|
-
.brand-
|
|
3316
|
+
.brand-lockup { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
3317
|
+
.brand-mark {
|
|
3318
|
+
width: 38px; height: 38px; display: grid; place-items: center; flex: none;
|
|
3319
|
+
border-radius: 11px; color: #fff;
|
|
3320
|
+
background: linear-gradient(145deg, #7f56d9 0%, #53389e 100%);
|
|
3321
|
+
box-shadow: 0 6px 16px rgb(105 65 198 / 0.24), inset 0 1px 0 rgb(255 255 255 / 0.25);
|
|
3322
|
+
}
|
|
3323
|
+
.brand-mark svg { width: 22px; height: 22px; }
|
|
3324
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; line-height: 1.25; }
|
|
3325
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
3085
3326
|
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
3086
3327
|
.vendor {
|
|
3087
3328
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
3088
3329
|
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
3330
|
+
padding: 5px 8px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-inset);
|
|
3089
3331
|
}
|
|
3090
3332
|
.vendor[hidden] { display: none; }
|
|
3091
3333
|
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
@@ -3097,12 +3339,17 @@ var renderAdminDocument = (service) => {
|
|
|
3097
3339
|
.vendor-sep { color: var(--faint); }
|
|
3098
3340
|
.vendor-link { color: inherit; text-decoration: none; }
|
|
3099
3341
|
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
3100
|
-
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items:
|
|
3101
|
-
|
|
3342
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
|
|
3343
|
+
.sync-status { align-self: center; display: flex; align-items: center; gap: 6px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3344
|
+
.sync-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
|
|
3345
|
+
.sync-status.busy::before { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); animation: pulse 1.2s ease-in-out infinite; }
|
|
3346
|
+
label.field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; font-weight: 600; color: var(--faint); }
|
|
3102
3347
|
label.field input, label.field select, .controls input[type="password"] {
|
|
3103
|
-
background-color: var(--bg-
|
|
3104
|
-
padding:
|
|
3348
|
+
background-color: var(--bg-elev); border: 1px solid var(--line-strong); border-radius: 9px;
|
|
3349
|
+
padding: 8px 10px; min-height: 38px; line-height: 1.2; box-shadow: var(--shadow-xs);
|
|
3105
3350
|
}
|
|
3351
|
+
label.field input:hover, label.field select:hover, textarea:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--line-strong)); }
|
|
3352
|
+
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; }
|
|
3106
3353
|
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
3107
3354
|
the text fields in the same form. Paint it like those fields. */
|
|
3108
3355
|
label.field select {
|
|
@@ -3119,12 +3366,20 @@ var renderAdminDocument = (service) => {
|
|
|
3119
3366
|
dialog .field > textarea { width: 100%; }
|
|
3120
3367
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
3121
3368
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
.btn
|
|
3369
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
|
3370
|
+
border-radius: 9px; min-height: 38px; padding: 0 13px; border: 1px solid var(--line-strong);
|
|
3371
|
+
background: var(--bg-elev); color: var(--ink); font-size: 13px; font-weight: 600; line-height: 1;
|
|
3372
|
+
box-shadow: var(--shadow-xs);
|
|
3373
|
+
}
|
|
3374
|
+
.icon-btn { width: 38px; padding: 0; }
|
|
3375
|
+
.icon-btn svg, .btn svg, .btn-primary svg { width: 16px; height: 16px; }
|
|
3376
|
+
.icon-btn:hover, .btn:hover { background: var(--bg-muted); border-color: var(--line-strong); }
|
|
3377
|
+
.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; }
|
|
3378
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
3379
|
+
.btn-quiet { background: transparent; border-color: transparent; box-shadow: none; }
|
|
3380
|
+
.btn-quiet:hover { background: var(--bg-muted); }
|
|
3381
|
+
.btn-danger { color: var(--err); border-color: color-mix(in srgb, var(--err) 25%, var(--line)); }
|
|
3382
|
+
.btn-danger:hover, .btn-danger.confirming { background: var(--err-soft); border-color: color-mix(in srgb, var(--err) 45%, var(--line)); }
|
|
3128
3383
|
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
3129
3384
|
so the press itself has to stay visible. */
|
|
3130
3385
|
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
@@ -3140,93 +3395,159 @@ var renderAdminDocument = (service) => {
|
|
|
3140
3395
|
flex: 1 0 auto; min-width: 0;
|
|
3141
3396
|
}
|
|
3142
3397
|
nav.side {
|
|
3143
|
-
|
|
3398
|
+
position: sticky; top: var(--header); align-self: start; height: calc(100vh - var(--header));
|
|
3399
|
+
border-right: 1px solid var(--line); padding: 18px 12px;
|
|
3144
3400
|
display: flex; flex-direction: column; gap: 4px;
|
|
3145
|
-
background: var(--bg-elev);
|
|
3401
|
+
background: var(--bg-elev); overflow-y: auto;
|
|
3146
3402
|
}
|
|
3403
|
+
.nav-label { padding: 0 10px 8px; color: var(--faint); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
|
|
3147
3404
|
nav.side button {
|
|
3405
|
+
position: relative; display: flex; align-items: center; gap: 10px; width: 100%;
|
|
3148
3406
|
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
3149
|
-
padding: 9px 10px; min-height:
|
|
3150
|
-
}
|
|
3151
|
-
nav.side button
|
|
3152
|
-
|
|
3153
|
-
.
|
|
3154
|
-
.
|
|
3155
|
-
.
|
|
3407
|
+
padding: 9px 10px; min-height: 42px; color: var(--muted); font-size: 13px; font-weight: 550;
|
|
3408
|
+
}
|
|
3409
|
+
nav.side button:hover { background: var(--bg-muted); color: var(--ink); }
|
|
3410
|
+
nav.side button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 650; }
|
|
3411
|
+
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); }
|
|
3412
|
+
.nav-icon { width: 18px; height: 18px; display: grid; place-items: center; flex: none; }
|
|
3413
|
+
.nav-icon svg { width: 18px; height: 18px; }
|
|
3414
|
+
main { padding: 32px clamp(20px, 4vw, 48px) 64px; max-width: 1320px; width: 100%; min-width: 0; }
|
|
3415
|
+
main[aria-busy="true"] .view { opacity: 0.62; }
|
|
3416
|
+
.view { animation: view-in 180ms ease-out; }
|
|
3417
|
+
.view[hidden] { display: none; }
|
|
3418
|
+
.view-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
|
|
3419
|
+
.view h2 { margin: 0 0 5px; font-size: clamp(24px, 3vw, 30px); line-height: 1.2; letter-spacing: -0.035em; }
|
|
3420
|
+
.view h3 { margin: 24px 0 10px; font-size: 15px; letter-spacing: -0.01em; }
|
|
3421
|
+
.lede { margin: 0; color: var(--muted); max-width: 72ch; font-size: 14px; }
|
|
3422
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 175px), 1fr)); gap: 14px; }
|
|
3156
3423
|
.card {
|
|
3157
3424
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
3158
|
-
padding:
|
|
3425
|
+
padding: 18px; box-shadow: var(--shadow-sm);
|
|
3159
3426
|
}
|
|
3160
|
-
.card
|
|
3161
|
-
.card
|
|
3427
|
+
.stat-card { position: relative; overflow: hidden; }
|
|
3428
|
+
.stat-card::after { content: ""; position: absolute; inset: 0 0 auto; height: 2px; background: linear-gradient(90deg, var(--accent), transparent 78%); opacity: 0.7; }
|
|
3429
|
+
.card .k { color: var(--faint); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
3430
|
+
.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; }
|
|
3162
3431
|
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
3163
|
-
.
|
|
3432
|
+
.section-card { background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-xs); overflow: hidden; }
|
|
3433
|
+
.section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
|
|
3434
|
+
.section-title h3 { margin: 0; }
|
|
3435
|
+
.split { display: grid; grid-template-columns: minmax(0, 250px) minmax(0, 1fr); gap: 18px; align-items: start; }
|
|
3436
|
+
#collections { padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); box-shadow: var(--shadow-xs); }
|
|
3164
3437
|
.collection {
|
|
3165
3438
|
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
3166
|
-
border-radius:
|
|
3439
|
+
border-radius: 9px; padding: 10px 11px; min-height: 48px; color: var(--ink); cursor: pointer;
|
|
3167
3440
|
}
|
|
3168
|
-
.collection
|
|
3441
|
+
.collection:hover { background: var(--bg-muted); }
|
|
3442
|
+
.collection[aria-current="true"] { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 18%, var(--line)); color: var(--accent); }
|
|
3169
3443
|
.collection small { display: block; color: var(--faint); }
|
|
3170
3444
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
3171
|
-
.chip {
|
|
3172
|
-
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
3173
|
-
border-radius: 999px; padding: 4px 8px;
|
|
3174
|
-
}
|
|
3445
|
+
.chip { font-family: var(--mono); font-size: 11px; background: var(--bg-muted); border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; }
|
|
3175
3446
|
.chip i { color: var(--faint); font-style: normal; }
|
|
3176
3447
|
.sql-layout {
|
|
3177
3448
|
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
3178
3449
|
}
|
|
3179
|
-
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
3180
|
-
.sql-tables button { text-align: left; }
|
|
3450
|
+
.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); }
|
|
3451
|
+
.sql-tables button { text-align: left; border: 0; border-radius: 8px; padding: 9px 10px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
3452
|
+
.sql-tables button:hover, .sql-tables button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); }
|
|
3181
3453
|
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
3182
|
-
table { width: 100%; border-
|
|
3183
|
-
|
|
3184
|
-
th {
|
|
3454
|
+
.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); }
|
|
3455
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
3456
|
+
th, td { text-align: left; padding: 11px 13px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
3457
|
+
th { color: var(--faint); background: var(--bg-inset); font-weight: 650; font-size: 11px; text-transform: uppercase; letter-spacing: 0.045em; }
|
|
3458
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
3185
3459
|
tr.clickable { cursor: pointer; }
|
|
3186
|
-
tr.clickable:hover { background: var(--
|
|
3460
|
+
tr.clickable:hover { background: var(--accent-soft); }
|
|
3187
3461
|
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
3188
3462
|
pre {
|
|
3189
3463
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
3190
|
-
background: var(--bg-inset); border-radius: 10px; padding:
|
|
3191
|
-
}
|
|
3192
|
-
.
|
|
3464
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 10px; padding: 14px; font-size: 12px; line-height: 1.65;
|
|
3465
|
+
}
|
|
3466
|
+
.json-key { color: var(--code-key); }
|
|
3467
|
+
.json-string { color: var(--code-string); }
|
|
3468
|
+
.json-number { color: var(--code-number); }
|
|
3469
|
+
.json-boolean { color: var(--code-boolean); }
|
|
3470
|
+
.json-null { color: var(--code-null); font-style: italic; }
|
|
3471
|
+
.json-inline { display: block; max-width: 44rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
3472
|
+
.json-view { position: relative; max-height: 26rem; overflow: auto; }
|
|
3473
|
+
.json-editor { position: relative; min-height: 190px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--bg-inset); overflow: hidden; }
|
|
3474
|
+
.json-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); }
|
|
3475
|
+
.json-editor.invalid { border-color: var(--err); }
|
|
3476
|
+
.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; }
|
|
3477
|
+
.json-editor pre { position: absolute; inset: 0; pointer-events: none; background: transparent; }
|
|
3478
|
+
.json-editor textarea { position: relative; z-index: 1; resize: vertical; background: transparent; color: transparent; caret-color: var(--ink); -webkit-text-fill-color: transparent; }
|
|
3479
|
+
.json-editor textarea::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); }
|
|
3480
|
+
.editor-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; }
|
|
3481
|
+
.json-status { display: inline-flex; align-items: center; gap: 6px; color: var(--ok); font-size: 11px; }
|
|
3482
|
+
.json-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
3483
|
+
.json-status.invalid { color: var(--err); }
|
|
3484
|
+
.format-json { border: 0; padding: 3px 5px; background: transparent; color: var(--accent); font-size: 11px; font-weight: 650; cursor: pointer; }
|
|
3485
|
+
.row-actions { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 14px 0; }
|
|
3193
3486
|
.banner {
|
|
3194
|
-
display: none;
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3487
|
+
display: none; position: sticky; top: calc(var(--header) + 12px); z-index: 15;
|
|
3488
|
+
align-items: center; gap: 10px; margin: 0 0 16px; padding: 11px 12px 11px 14px;
|
|
3489
|
+
border: 1px solid color-mix(in srgb, var(--err) 30%, var(--line)); border-radius: 10px;
|
|
3490
|
+
background: var(--err-soft); color: var(--err); box-shadow: var(--shadow-sm); font-size: 13px;
|
|
3491
|
+
}
|
|
3492
|
+
.banner.show { display: flex; }
|
|
3493
|
+
.banner span { flex: 1; }
|
|
3494
|
+
.banner button { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 7px; background: transparent; color: currentColor; cursor: pointer; }
|
|
3495
|
+
.banner button:hover { background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
3496
|
+
.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); }
|
|
3199
3497
|
dialog {
|
|
3200
|
-
border: 1px solid var(--line); border-radius:
|
|
3201
|
-
color: var(--ink); width: min(
|
|
3202
|
-
}
|
|
3203
|
-
dialog
|
|
3204
|
-
dialog
|
|
3498
|
+
border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--bg-elev);
|
|
3499
|
+
color: var(--ink); width: min(720px, calc(100% - 24px)); max-width: calc(100vw - 24px); box-shadow: var(--shadow-lg);
|
|
3500
|
+
}
|
|
3501
|
+
dialog[open] { animation: dialog-in 160ms ease-out; }
|
|
3502
|
+
dialog::backdrop { background: rgb(12 14 20 / 0.58); backdrop-filter: blur(3px); }
|
|
3503
|
+
dialog form, .dialog-body { padding: 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
3504
|
+
.dialog-heading { display: flex; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
|
3505
|
+
.dialog-heading strong { font-size: 18px; letter-spacing: -0.02em; }
|
|
3506
|
+
.dialog-heading span { color: var(--muted); font-size: 12px; }
|
|
3205
3507
|
textarea {
|
|
3206
3508
|
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
3207
3509
|
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
3208
3510
|
font-family: var(--mono); font-size: 12px;
|
|
3209
3511
|
}
|
|
3210
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
3211
|
-
.
|
|
3512
|
+
.dialog-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding-top: 4px; }
|
|
3513
|
+
.dialog-actions .btn-danger { margin-right: auto; }
|
|
3514
|
+
.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); }
|
|
3212
3515
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
3213
3516
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
3214
3517
|
.status-2 { color: var(--ok); }
|
|
3215
3518
|
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
3216
3519
|
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
3520
|
+
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
3521
|
+
.form-grid .wide { grid-column: span 2; }
|
|
3217
3522
|
.route {
|
|
3218
|
-
display: grid; grid-template-columns:
|
|
3219
|
-
padding:
|
|
3220
|
-
}
|
|
3221
|
-
.
|
|
3523
|
+
display: grid; grid-template-columns: 84px minmax(0, 1fr) auto; gap: 10px; align-items: center;
|
|
3524
|
+
padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--bg-elev);
|
|
3525
|
+
}
|
|
3526
|
+
.route:first-child { border-radius: var(--radius) var(--radius) 0 0; }
|
|
3527
|
+
.route:last-child { border-bottom: 0; border-radius: 0 0 var(--radius) var(--radius); }
|
|
3528
|
+
.route-tools { display: flex; align-items: flex-end; gap: 12px; margin-bottom: 12px; }
|
|
3529
|
+
.route-tools .field { flex: 1; }
|
|
3530
|
+
.route-tools input { width: 100%; }
|
|
3531
|
+
.route-count { padding-bottom: 10px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3532
|
+
#route-list { max-height: 34rem; border: 1px solid var(--line); border-radius: var(--radius); overflow: auto; box-shadow: var(--shadow-xs); }
|
|
3533
|
+
.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; }
|
|
3534
|
+
@keyframes view-in { from { transform: translateY(3px); } to { transform: translateY(0); } }
|
|
3535
|
+
@keyframes dialog-in { from { transform: translateY(8px) scale(0.99); } to { transform: translateY(0) scale(1); } }
|
|
3536
|
+
@keyframes pulse { 50% { opacity: 0.45; } }
|
|
3537
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
|
|
3538
|
+
@media (forced-colors: active) { .json-editor pre { display: none; } .json-editor textarea { color: CanvasText; -webkit-text-fill-color: CanvasText; } }
|
|
3222
3539
|
@media (max-width: 800px) {
|
|
3540
|
+
:root { --header: 0px; }
|
|
3223
3541
|
header.top {
|
|
3224
|
-
align-items: stretch; flex-direction: column;
|
|
3225
|
-
padding
|
|
3542
|
+
position: relative; align-items: stretch; flex-direction: column;
|
|
3543
|
+
padding: max(12px, env(safe-area-inset-top)) 14px 12px;
|
|
3226
3544
|
}
|
|
3227
3545
|
input, select, textarea { font-size: 16px; }
|
|
3228
3546
|
.brand, .vendor, .controls { width: 100%; }
|
|
3547
|
+
.brand { gap: 10px; }
|
|
3548
|
+
.vendor { order: 2; border-radius: 9px; }
|
|
3229
3549
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
3550
|
+
.sync-status { display: none; }
|
|
3230
3551
|
/* The layout fills the screen. A single-column grid stretches every auto
|
|
3231
3552
|
row, so the tab bar grew with the leftover height and the buttons floated
|
|
3232
3553
|
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
@@ -3236,14 +3557,23 @@ var renderAdminDocument = (service) => {
|
|
|
3236
3557
|
align-content: start;
|
|
3237
3558
|
}
|
|
3238
3559
|
nav.side {
|
|
3239
|
-
align-self: start; height: auto;
|
|
3560
|
+
position: sticky; top: 0; z-index: 12; align-self: start; height: auto;
|
|
3240
3561
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
3241
3562
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
3242
|
-
padding: 8px;
|
|
3563
|
+
padding: 8px; box-shadow: var(--shadow-xs);
|
|
3243
3564
|
}
|
|
3244
|
-
nav
|
|
3565
|
+
.nav-label { display: none; }
|
|
3566
|
+
nav.side button { width: auto; white-space: nowrap; flex: none; }
|
|
3567
|
+
nav.side button[aria-current="true"]::before { inset: auto 9px -8px; width: auto; height: 3px; border-radius: 3px 3px 0 0; }
|
|
3245
3568
|
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
3246
|
-
|
|
3569
|
+
.form-grid { grid-template-columns: minmax(0, 1fr); }
|
|
3570
|
+
.form-grid .wide { grid-column: auto; }
|
|
3571
|
+
main { padding: 22px 14px 48px; }
|
|
3572
|
+
.view-head { margin-bottom: 18px; }
|
|
3573
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
3574
|
+
.card { padding: 14px; }
|
|
3575
|
+
.card .v { font-size: 20px; }
|
|
3576
|
+
.table-shell { border: 0; background: transparent; box-shadow: none; overflow: visible; }
|
|
3247
3577
|
table, thead, tbody, tr, th, td { display: block; }
|
|
3248
3578
|
thead { display: none; }
|
|
3249
3579
|
tr {
|
|
@@ -3258,50 +3588,63 @@ var renderAdminDocument = (service) => {
|
|
|
3258
3588
|
.route { grid-template-columns: 72px 1fr; }
|
|
3259
3589
|
.route button { grid-column: 1 / -1; }
|
|
3260
3590
|
}
|
|
3591
|
+
@media (max-width: 460px) {
|
|
3592
|
+
.controls label.field { min-width: calc(50% - 4px); }
|
|
3593
|
+
.controls .icon-btn, .controls .btn { flex: 1; }
|
|
3594
|
+
.grid { grid-template-columns: minmax(0, 1fr); }
|
|
3595
|
+
.dialog-actions { flex-wrap: wrap; }
|
|
3596
|
+
.dialog-actions button { flex: 1; }
|
|
3597
|
+
.dialog-actions .btn-danger { flex-basis: 100%; margin-right: 0; }
|
|
3598
|
+
}
|
|
3261
3599
|
</style>
|
|
3262
3600
|
</head>
|
|
3263
3601
|
<body>
|
|
3264
3602
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
3265
3603
|
<header class="top">
|
|
3266
3604
|
<div class="brand">
|
|
3267
|
-
<div class="brand-
|
|
3268
|
-
<
|
|
3269
|
-
|
|
3605
|
+
<div class="brand-lockup">
|
|
3606
|
+
<div class="brand-mark" aria-hidden="true">
|
|
3607
|
+
<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>
|
|
3608
|
+
</div>
|
|
3609
|
+
<div class="brand-id">
|
|
3610
|
+
<strong>${name}</strong>
|
|
3611
|
+
<span>Mock admin</span>
|
|
3612
|
+
</div>
|
|
3270
3613
|
</div>
|
|
3271
3614
|
<div class="vendor" id="vendor" hidden></div>
|
|
3272
3615
|
</div>
|
|
3273
3616
|
<div class="controls">
|
|
3617
|
+
<span class="sync-status" id="sync-status" role="status" aria-live="polite">Ready</span>
|
|
3274
3618
|
<label class="field">Namespace
|
|
3275
3619
|
<select id="namespace" aria-label="Namespace"></select>
|
|
3276
3620
|
</label>
|
|
3277
3621
|
<label class="field">Admin key
|
|
3278
3622
|
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
3279
3623
|
</label>
|
|
3280
|
-
<button class="icon-btn" id="theme" type="button" aria-label="
|
|
3281
|
-
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
3624
|
+
<button class="icon-btn" id="theme" type="button" aria-label="Use dark theme" title="Use dark theme"></button>
|
|
3625
|
+
<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>
|
|
3282
3626
|
</div>
|
|
3283
3627
|
</header>
|
|
3284
3628
|
<div class="layout">
|
|
3285
3629
|
<nav class="side" id="nav" aria-label="Admin sections">
|
|
3286
|
-
<
|
|
3287
|
-
<button type="button" data-view="
|
|
3288
|
-
<button type="button" data-view="
|
|
3289
|
-
<button type="button" data-view="
|
|
3290
|
-
<button type="button" data-view="
|
|
3291
|
-
<button type="button" data-view="
|
|
3630
|
+
<span class="nav-label">Workspace</span>
|
|
3631
|
+
<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>
|
|
3632
|
+
<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>
|
|
3633
|
+
<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>
|
|
3634
|
+
<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>
|
|
3635
|
+
<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>
|
|
3636
|
+
<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>
|
|
3292
3637
|
</nav>
|
|
3293
3638
|
<main id="views">
|
|
3294
|
-
<
|
|
3639
|
+
<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>
|
|
3295
3640
|
<section class="view" id="view-overview">
|
|
3296
|
-
<h2>Overview</h2>
|
|
3297
|
-
<p class="lede">Health, clock, and the collections this namespace is holding.</p>
|
|
3641
|
+
<div class="view-head"><div><h2>Overview</h2><p class="lede">Health, clock, and the collections this namespace is holding.</p></div></div>
|
|
3298
3642
|
<div class="grid" id="overview-cards"></div>
|
|
3299
3643
|
<h3>Recent requests</h3>
|
|
3300
3644
|
<div id="overview-requests"></div>
|
|
3301
3645
|
</section>
|
|
3302
3646
|
<section class="view" id="view-state" hidden>
|
|
3303
|
-
<h2>State</h2>
|
|
3304
|
-
<p class="lede">Every collection in this namespace. Declared fields stay visible before any row exists; stored rows add whatever else they contain.</p>
|
|
3647
|
+
<div class="view-head"><div><h2>State</h2><p class="lede">Inspect and edit every collection in this namespace.</p></div></div>
|
|
3305
3648
|
<div class="split">
|
|
3306
3649
|
<div id="collections"></div>
|
|
3307
3650
|
<div>
|
|
@@ -3314,8 +3657,7 @@ var renderAdminDocument = (service) => {
|
|
|
3314
3657
|
</div>
|
|
3315
3658
|
</section>
|
|
3316
3659
|
<section class="view" id="view-clock" hidden>
|
|
3317
|
-
<h2>Clock</h2>
|
|
3318
|
-
<p class="lede">The clock every timestamp in this mock reads.</p>
|
|
3660
|
+
<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>
|
|
3319
3661
|
<div class="card" id="clock-readout"></div>
|
|
3320
3662
|
<div class="row-actions">
|
|
3321
3663
|
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
@@ -3334,21 +3676,22 @@ var renderAdminDocument = (service) => {
|
|
|
3334
3676
|
</form>
|
|
3335
3677
|
</section>
|
|
3336
3678
|
<section class="view" id="view-faults" hidden>
|
|
3337
|
-
<h2>Faults</h2>
|
|
3338
|
-
<p class="lede">Inject a response, a delay, or a named preset. Rules apply to this namespace.</p>
|
|
3679
|
+
<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>
|
|
3339
3680
|
<form id="fault-form" class="card stack">
|
|
3340
|
-
<
|
|
3341
|
-
<
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
<
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
<
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
<
|
|
3351
|
-
|
|
3681
|
+
<div class="form-grid">
|
|
3682
|
+
<label class="field wide">Preset
|
|
3683
|
+
<select id="fault-preset"><option value="">Custom rule</option></select>
|
|
3684
|
+
</label>
|
|
3685
|
+
<label class="field">Status
|
|
3686
|
+
<input id="fault-status" inputmode="numeric" placeholder="503">
|
|
3687
|
+
</label>
|
|
3688
|
+
<label class="field wide">Path prefix
|
|
3689
|
+
<input id="fault-path" placeholder="/v1">
|
|
3690
|
+
</label>
|
|
3691
|
+
<label class="field">Method
|
|
3692
|
+
<input id="fault-method" placeholder="GET">
|
|
3693
|
+
</label>
|
|
3694
|
+
</div>
|
|
3352
3695
|
<div class="dialog-actions">
|
|
3353
3696
|
<button class="btn-primary" type="submit">Add fault</button>
|
|
3354
3697
|
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
@@ -3357,26 +3700,31 @@ var renderAdminDocument = (service) => {
|
|
|
3357
3700
|
<div id="fault-list"></div>
|
|
3358
3701
|
</section>
|
|
3359
3702
|
<section class="view" id="view-journal" hidden>
|
|
3360
|
-
<h2>Journal</h2>
|
|
3361
|
-
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
3362
|
-
<div class="row-actions">
|
|
3363
|
-
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
3364
|
-
</div>
|
|
3703
|
+
<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>
|
|
3365
3704
|
<div id="journal"></div>
|
|
3366
3705
|
</section>
|
|
3367
3706
|
<section class="view" id="view-routes" hidden>
|
|
3368
|
-
<h2>Routes</h2>
|
|
3369
|
-
<
|
|
3707
|
+
<div class="view-head"><div><h2>Routes</h2><p class="lede">Explore shared controls and service-specific admin endpoints.</p></div></div>
|
|
3708
|
+
<div class="route-tools">
|
|
3709
|
+
<label class="field">Filter routes
|
|
3710
|
+
<input id="route-search" type="search" placeholder="Method, path, or extension" autocomplete="off">
|
|
3711
|
+
</label>
|
|
3712
|
+
<span class="route-count" id="route-count"></span>
|
|
3713
|
+
</div>
|
|
3370
3714
|
<div id="route-list"></div>
|
|
3371
|
-
<form id="route-form" class="card stack">
|
|
3715
|
+
<form id="route-form" class="card stack" style="margin-top: 18px">
|
|
3372
3716
|
<strong id="route-title">Select a route</strong>
|
|
3373
3717
|
<label class="field">JSON body
|
|
3374
|
-
<
|
|
3718
|
+
<div class="json-editor" id="route-editor" hidden>
|
|
3719
|
+
<pre aria-hidden="true"><code id="route-highlight"></code></pre>
|
|
3720
|
+
<textarea id="route-body" placeholder="{ }" spellcheck="false" aria-describedby="route-json-status" disabled></textarea>
|
|
3721
|
+
</div>
|
|
3722
|
+
<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>
|
|
3375
3723
|
</label>
|
|
3376
3724
|
<div class="dialog-actions">
|
|
3377
|
-
<button class="btn-primary" type="submit">Send</button>
|
|
3725
|
+
<button class="btn-primary" id="route-send" type="submit" disabled>Send request</button>
|
|
3378
3726
|
</div>
|
|
3379
|
-
<pre id="route-result">Response will show here.</pre>
|
|
3727
|
+
<pre class="json-view" id="route-result"><code>Response will show here.</code></pre>
|
|
3380
3728
|
</form>
|
|
3381
3729
|
</section>
|
|
3382
3730
|
</main>
|
|
@@ -3384,12 +3732,16 @@ var renderAdminDocument = (service) => {
|
|
|
3384
3732
|
</div>
|
|
3385
3733
|
<dialog id="editor">
|
|
3386
3734
|
<form method="dialog" id="editor-form">
|
|
3387
|
-
<strong id="editor-title">Record</strong>
|
|
3735
|
+
<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>
|
|
3388
3736
|
<label class="field">Id
|
|
3389
3737
|
<input id="editor-id" required>
|
|
3390
3738
|
</label>
|
|
3391
3739
|
<label class="field">JSON value
|
|
3392
|
-
<
|
|
3740
|
+
<div class="json-editor" id="record-editor">
|
|
3741
|
+
<pre aria-hidden="true"><code id="editor-highlight"></code></pre>
|
|
3742
|
+
<textarea id="editor-value" spellcheck="false" aria-describedby="editor-json-status"></textarea>
|
|
3743
|
+
</div>
|
|
3744
|
+
<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>
|
|
3393
3745
|
</label>
|
|
3394
3746
|
<div class="dialog-actions">
|
|
3395
3747
|
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
@@ -3431,8 +3783,8 @@ var describeExtension = (extension) => {
|
|
|
3431
3783
|
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
3432
3784
|
};
|
|
3433
3785
|
};
|
|
3434
|
-
var adminUiRoutes = (service, ui) => {
|
|
3435
|
-
const shell = () => renderAdminDocument(service);
|
|
3786
|
+
var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
3787
|
+
const shell = () => renderAdminDocument(service, adminPrefix);
|
|
3436
3788
|
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
3437
3789
|
return {
|
|
3438
3790
|
"GET /ui": () => htmlResponse(document3()),
|
|
@@ -4123,10 +4475,13 @@ var stateAdminRoutes = (routes) => {
|
|
|
4123
4475
|
};
|
|
4124
4476
|
|
|
4125
4477
|
// ../core/dist/version.js
|
|
4126
|
-
var PACKAGE_VERSION = true ? "
|
|
4478
|
+
var PACKAGE_VERSION = true ? "2.0.0" : UNRELEASED_VERSION;
|
|
4127
4479
|
|
|
4128
4480
|
// ../core/dist/webhooks.js
|
|
4129
|
-
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4481
|
+
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4482
|
+
status,
|
|
4483
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4484
|
+
});
|
|
4130
4485
|
var adminError3 = (status, message) => json4(status, { error: { type: "mockingbird_admin", message } });
|
|
4131
4486
|
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4132
4487
|
var parseEndpoint = (value) => {
|
|
@@ -4234,7 +4589,6 @@ var AT_HEADER = "x-mockingbird-at";
|
|
|
4234
4589
|
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
4235
4590
|
var DEFAULT_NAMESPACE = "default";
|
|
4236
4591
|
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4237
|
-
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
4238
4592
|
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4239
4593
|
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
4240
4594
|
var effects = /* @__PURE__ */ new WeakMap();
|
|
@@ -4275,6 +4629,22 @@ var operationMatcher = (document3) => {
|
|
|
4275
4629
|
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
4276
4630
|
};
|
|
4277
4631
|
var createRuntime = (options) => {
|
|
4632
|
+
const adminPrefix = resolveAdminPrefix(options.adminPrefix);
|
|
4633
|
+
const internalPaths = Object.entries(options.document?.paths ?? {}).filter(([, item]) => item["x-mockingbird-internal"] === true).map(([path]) => path);
|
|
4634
|
+
for (const path of internalPaths) {
|
|
4635
|
+
if (!path.startsWith(`${ADMIN_PREFIX}/blobs/`))
|
|
4636
|
+
throw new Error(`internal transport ${path} must live under ${ADMIN_PREFIX}/blobs/`);
|
|
4637
|
+
}
|
|
4638
|
+
assertAdminPrefixAvailable(adminPrefix, Object.keys(options.document?.paths ?? {}).filter((path) => !internalPaths.includes(path)));
|
|
4639
|
+
const internalMatch = (path) => {
|
|
4640
|
+
for (const pattern of internalPaths) {
|
|
4641
|
+
const actual = path.split("/");
|
|
4642
|
+
const expected = pattern.split("/");
|
|
4643
|
+
if (actual.length === expected.length && expected.every((part, i) => part.startsWith("{") || part === actual[i]))
|
|
4644
|
+
return pattern;
|
|
4645
|
+
}
|
|
4646
|
+
return void 0;
|
|
4647
|
+
};
|
|
4278
4648
|
assertAdminUi(options.adminUi);
|
|
4279
4649
|
const sqlite = bootSqlite(options.sqlite);
|
|
4280
4650
|
const clock = options.clock ?? createClock();
|
|
@@ -4320,6 +4690,7 @@ var createRuntime = (options) => {
|
|
|
4320
4690
|
const created = options.create({
|
|
4321
4691
|
namespace: storageNamespace(key),
|
|
4322
4692
|
publicNamespace,
|
|
4693
|
+
adminPrefix,
|
|
4323
4694
|
sqlite,
|
|
4324
4695
|
clock: namespaceClock(publicNamespace),
|
|
4325
4696
|
rng: createdContextRng
|
|
@@ -4552,10 +4923,10 @@ var createRuntime = (options) => {
|
|
|
4552
4923
|
namespaceClock,
|
|
4553
4924
|
namespaceOf(request) {
|
|
4554
4925
|
const url = new URL(request.url);
|
|
4555
|
-
const prefixed =
|
|
4926
|
+
const prefixed = matchNamespacePath(url.pathname, adminPrefix);
|
|
4556
4927
|
const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
|
|
4557
4928
|
const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
|
|
4558
|
-
if (path
|
|
4929
|
+
if (isAdminPath(path, adminPrefix))
|
|
4559
4930
|
return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
|
|
4560
4931
|
if (selected !== void 0)
|
|
4561
4932
|
return selected;
|
|
@@ -4621,7 +4992,7 @@ var createRuntime = (options) => {
|
|
|
4621
4992
|
state: (namespace = DEFAULT_NAMESPACE) => inspectState(stateScope(namespace)),
|
|
4622
4993
|
fetch: async (incoming) => {
|
|
4623
4994
|
let request = incoming;
|
|
4624
|
-
const prefixed =
|
|
4995
|
+
const prefixed = matchNamespacePath(new URL(request.url).pathname, adminPrefix);
|
|
4625
4996
|
if (prefixed) {
|
|
4626
4997
|
const url2 = new URL(request.url);
|
|
4627
4998
|
url2.pathname = prefixed[2] ?? "/";
|
|
@@ -4637,7 +5008,15 @@ var createRuntime = (options) => {
|
|
|
4637
5008
|
signal: request.signal
|
|
4638
5009
|
});
|
|
4639
5010
|
}
|
|
4640
|
-
|
|
5011
|
+
const internalUrl = new URL(request.url);
|
|
5012
|
+
const internalPath = isAdminPath(internalUrl.pathname, adminPrefix) ? `${ADMIN_PREFIX}${internalUrl.pathname.slice(adminPrefix.length)}` : "";
|
|
5013
|
+
const internal = internalMatch(internalPath);
|
|
5014
|
+
const obsoleteInternalPath = adminPrefix !== ADMIN_PREFIX && internalMatch(internalUrl.pathname) && !internal;
|
|
5015
|
+
if (internal) {
|
|
5016
|
+
internalUrl.pathname = internalPath;
|
|
5017
|
+
request = new Request(internalUrl, request);
|
|
5018
|
+
}
|
|
5019
|
+
let namespace = internal ? internalUrl.searchParams.get("namespace") ?? control.namespaceOf(request) : control.namespaceOf(request);
|
|
4641
5020
|
if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
4642
5021
|
const credential = options.credential(request);
|
|
4643
5022
|
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
@@ -4657,7 +5036,9 @@ var createRuntime = (options) => {
|
|
|
4657
5036
|
return copy;
|
|
4658
5037
|
}
|
|
4659
5038
|
};
|
|
4660
|
-
|
|
5039
|
+
if (obsoleteInternalPath)
|
|
5040
|
+
return stamp(adminFail(404, "internal transport is only available under adminPrefix"));
|
|
5041
|
+
const handled = internal ? void 0 : await control.handle(request);
|
|
4661
5042
|
if (handled)
|
|
4662
5043
|
return stamp(handled);
|
|
4663
5044
|
const started = monotonicNow();
|
|
@@ -4698,7 +5079,7 @@ var createRuntime = (options) => {
|
|
|
4698
5079
|
type: "mockingbird_admin",
|
|
4699
5080
|
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
4700
5081
|
}
|
|
4701
|
-
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
5082
|
+
}), { status: 400, headers: { "content-type": "application/json; charset=utf-8" } }));
|
|
4702
5083
|
}
|
|
4703
5084
|
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
4704
5085
|
log(400);
|
|
@@ -4815,15 +5196,16 @@ var createRuntime = (options) => {
|
|
|
4815
5196
|
...options.admin?.(runtime) ?? {},
|
|
4816
5197
|
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
4817
5198
|
// maps `presets`, and a service route must not replace that list with a record.
|
|
4818
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
5199
|
+
...options.presets ? presetRoutes(options.presets, runtime, adminPrefix) : {},
|
|
4819
5200
|
...stateAdminRoutes({
|
|
4820
5201
|
open: stateScope,
|
|
4821
5202
|
afterWrite: (namespace) => {
|
|
4822
5203
|
checkpoint(namespace, "main");
|
|
4823
5204
|
}
|
|
4824
5205
|
}),
|
|
4825
|
-
...adminUiRoutes(options.name, options.adminUi)
|
|
5206
|
+
...adminUiRoutes(options.name, options.adminUi, adminPrefix)
|
|
4826
5207
|
},
|
|
5208
|
+
adminPrefix,
|
|
4827
5209
|
adminKey: options.adminKey
|
|
4828
5210
|
});
|
|
4829
5211
|
return defineMock(runtime);
|
|
@@ -4837,7 +5219,10 @@ var mutableResponse = (response) => {
|
|
|
4837
5219
|
return new Response(response.body, response);
|
|
4838
5220
|
}
|
|
4839
5221
|
};
|
|
4840
|
-
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5222
|
+
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5223
|
+
status,
|
|
5224
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
5225
|
+
});
|
|
4841
5226
|
var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
|
|
4842
5227
|
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4843
5228
|
var credentialRoutes = (registry) => ({
|
|
@@ -4889,7 +5274,7 @@ var credentialRoutes = (registry) => ({
|
|
|
4889
5274
|
return adminJson(200, { status: "ok" });
|
|
4890
5275
|
}
|
|
4891
5276
|
});
|
|
4892
|
-
var presetRoutes = (presets, runtime) => ({
|
|
5277
|
+
var presetRoutes = (presets, runtime, adminPrefix) => ({
|
|
4893
5278
|
"GET /faults/presets": () => {
|
|
4894
5279
|
const body = {
|
|
4895
5280
|
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
@@ -4899,7 +5284,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
4899
5284
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
4900
5285
|
const name = params.name;
|
|
4901
5286
|
if (!presets[name])
|
|
4902
|
-
return adminFail(404, `no fault preset ${name}; GET /
|
|
5287
|
+
return adminFail(404, `no fault preset ${name}; GET ${adminPrefix}/faults/presets`);
|
|
4903
5288
|
const overrides = isObject(body) ? body : {};
|
|
4904
5289
|
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
4905
5290
|
}
|
|
@@ -5059,6 +5444,7 @@ var createRuntime2 = (options = {}) => {
|
|
|
5059
5444
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
5060
5445
|
...options.clock ? { clock: options.clock } : {},
|
|
5061
5446
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
5447
|
+
...options.adminPrefix !== void 0 ? { adminPrefix: options.adminPrefix } : {},
|
|
5062
5448
|
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
5063
5449
|
...options.onLog ? { onLog: options.onLog } : {},
|
|
5064
5450
|
credential: bearerToken,
|
|
@@ -5171,7 +5557,7 @@ var PrismAPI = class {
|
|
|
5171
5557
|
const sqlite = bootSqlite(options.sqlite);
|
|
5172
5558
|
const namespace = options.namespace ?? PRISM_NAMESPACE;
|
|
5173
5559
|
this.now = options.now ?? (() => Date.now());
|
|
5174
|
-
this.prefix = options.publicNamespace && options.publicNamespace !== "default" ? `/ns/${encodeURIComponent(options.publicNamespace)}` : "";
|
|
5560
|
+
this.prefix = options.publicNamespace && options.publicNamespace !== "default" ? `/__admin/ns/${encodeURIComponent(options.publicNamespace)}` : "";
|
|
5175
5561
|
this.state = new PrismState(sqlite, namespace, options.settings ?? {});
|
|
5176
5562
|
const handlers = defineOperations({
|
|
5177
5563
|
UpsertUser: (context) => this.upsertUser(context),
|
|
@@ -5489,6 +5875,7 @@ var PrismAPI = class {
|
|
|
5489
5875
|
|
|
5490
5876
|
export {
|
|
5491
5877
|
createClock,
|
|
5878
|
+
resolveAdminPrefix,
|
|
5492
5879
|
parseDuration,
|
|
5493
5880
|
document2 as document,
|
|
5494
5881
|
operationIds,
|
|
@@ -5501,4 +5888,4 @@ export {
|
|
|
5501
5888
|
scanResults,
|
|
5502
5889
|
PrismAPI
|
|
5503
5890
|
};
|
|
5504
|
-
//# sourceMappingURL=chunk-
|
|
5891
|
+
//# sourceMappingURL=chunk-4RBQFCQP.js.map
|