@crvouga/mockingbird-service-fullscript 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 +20 -0
- package/dist/{chunk-VH7HDXIA.js → chunk-EZGGE5JC.js} +44 -24
- package/dist/chunk-EZGGE5JC.js.map +7 -0
- package/dist/{chunk-L5A2NSLT.js → chunk-R7LD4QOX.js} +625 -237
- package/dist/chunk-R7LD4QOX.js.map +7 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +5 -3
- package/dist/index.js +1 -1
- package/dist/server.d.ts +4 -1
- package/dist/server.js +2 -2
- package/openapi.yaml +776 -0
- package/package.json +29 -4
- package/dist/chunk-L5A2NSLT.js.map +0 -7
- package/dist/chunk-VH7HDXIA.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
|
};
|
|
@@ -1786,6 +1825,34 @@ dialog:not([open]) {
|
|
|
1786
1825
|
function isRecord2(value) {
|
|
1787
1826
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1788
1827
|
}
|
|
1828
|
+
function highlightJsonText(text) {
|
|
1829
|
+
const escapeToken = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
1830
|
+
switch (char) {
|
|
1831
|
+
case "&":
|
|
1832
|
+
return "&";
|
|
1833
|
+
case "<":
|
|
1834
|
+
return "<";
|
|
1835
|
+
case ">":
|
|
1836
|
+
return ">";
|
|
1837
|
+
case '"':
|
|
1838
|
+
return """;
|
|
1839
|
+
default:
|
|
1840
|
+
return "'";
|
|
1841
|
+
}
|
|
1842
|
+
});
|
|
1843
|
+
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;
|
|
1844
|
+
let cursor = 0;
|
|
1845
|
+
let html = "";
|
|
1846
|
+
for (const match of text.matchAll(token)) {
|
|
1847
|
+
const index = match.index ?? 0;
|
|
1848
|
+
const value = match[0];
|
|
1849
|
+
html += escapeToken(text.slice(cursor, index));
|
|
1850
|
+
const kind = value.startsWith('"') ? value.trimEnd().endsWith(":") ? "key" : "string" : value === "true" || value === "false" ? "boolean" : value === "null" ? "null" : "number";
|
|
1851
|
+
html += `<span class="json-${kind}">${escapeToken(value)}</span>`;
|
|
1852
|
+
cursor = index + value.length;
|
|
1853
|
+
}
|
|
1854
|
+
return html + escapeToken(text.slice(cursor));
|
|
1855
|
+
}
|
|
1789
1856
|
function mountAdminBrand(doc, host, service, catalog) {
|
|
1790
1857
|
const clip = (value, max) => {
|
|
1791
1858
|
if (typeof value !== "string")
|
|
@@ -2147,14 +2214,16 @@ function bootAdmin(config) {
|
|
|
2147
2214
|
const state = {
|
|
2148
2215
|
namespace: "default",
|
|
2149
2216
|
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
2150
|
-
view: "overview",
|
|
2217
|
+
view: ["overview", "state", "clock", "faults", "journal", "routes"].includes(location.hash.slice(1)) ? location.hash.slice(1) : "overview",
|
|
2151
2218
|
collection: "",
|
|
2152
2219
|
shape: null,
|
|
2153
2220
|
route: null,
|
|
2221
|
+
routes: [],
|
|
2154
2222
|
editing: null
|
|
2155
2223
|
};
|
|
2156
2224
|
const standard = new Set(config.standardRoutes);
|
|
2157
2225
|
const banner = el("banner", HTMLElement);
|
|
2226
|
+
const bannerMessage = el("banner-message", HTMLElement);
|
|
2158
2227
|
const keyInput = el("admin-key", HTMLInputElement);
|
|
2159
2228
|
keyInput.value = state.key;
|
|
2160
2229
|
const params = new URLSearchParams(location.search);
|
|
@@ -2165,18 +2234,21 @@ function bootAdmin(config) {
|
|
|
2165
2234
|
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
2166
2235
|
params.delete("key");
|
|
2167
2236
|
const next = params.toString();
|
|
2168
|
-
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
2237
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : "") + location.hash);
|
|
2169
2238
|
}
|
|
2170
2239
|
const queryNamespace = params.get("namespace");
|
|
2171
2240
|
if (queryNamespace)
|
|
2172
2241
|
state.namespace = queryNamespace;
|
|
2173
2242
|
const showError = (error) => {
|
|
2174
|
-
|
|
2243
|
+
bannerMessage.textContent = error instanceof Error ? error.message : String(error);
|
|
2175
2244
|
banner.classList.add("show");
|
|
2245
|
+
banner.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
2176
2246
|
};
|
|
2177
2247
|
const clearError = () => {
|
|
2178
2248
|
banner.classList.remove("show");
|
|
2249
|
+
bannerMessage.textContent = "";
|
|
2179
2250
|
};
|
|
2251
|
+
el("banner-dismiss", HTMLButtonElement).addEventListener("click", clearError);
|
|
2180
2252
|
const api = async (method, path, body) => {
|
|
2181
2253
|
const headers = { accept: "application/json" };
|
|
2182
2254
|
if (state.key !== "")
|
|
@@ -2220,16 +2292,21 @@ function bootAdmin(config) {
|
|
|
2220
2292
|
namespace: () => state.namespace
|
|
2221
2293
|
};
|
|
2222
2294
|
const isPanelScript = (value) => typeof value === "function";
|
|
2295
|
+
const themeButton = el("theme", HTMLButtonElement);
|
|
2223
2296
|
const setTheme = (theme) => {
|
|
2224
2297
|
if (theme)
|
|
2225
2298
|
document.documentElement.dataset.theme = theme;
|
|
2226
2299
|
else
|
|
2227
2300
|
delete document.documentElement.dataset.theme;
|
|
2301
|
+
const dark = theme ? theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2302
|
+
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>';
|
|
2303
|
+
const label = dark ? "Use light theme" : "Use dark theme";
|
|
2304
|
+
themeButton.setAttribute("aria-label", label);
|
|
2305
|
+
themeButton.title = label;
|
|
2228
2306
|
};
|
|
2229
2307
|
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
2308
|
+
setTheme(storedTheme === "dark" || storedTheme === "light" ? storedTheme : null);
|
|
2309
|
+
themeButton.addEventListener("click", () => {
|
|
2233
2310
|
const current = document.documentElement.dataset.theme;
|
|
2234
2311
|
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2235
2312
|
const next = dark ? "light" : "dark";
|
|
@@ -2248,6 +2325,7 @@ function bootAdmin(config) {
|
|
|
2248
2325
|
continue;
|
|
2249
2326
|
section.hidden = section.id !== `view-${view}`;
|
|
2250
2327
|
}
|
|
2328
|
+
history.replaceState(null, "", `${location.pathname}${location.search}#${encodeURIComponent(view)}`);
|
|
2251
2329
|
};
|
|
2252
2330
|
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
2253
2331
|
const target = event.target;
|
|
@@ -2262,7 +2340,7 @@ function bootAdmin(config) {
|
|
|
2262
2340
|
show(view);
|
|
2263
2341
|
refresh().catch(showError);
|
|
2264
2342
|
});
|
|
2265
|
-
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>`;
|
|
2343
|
+
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>`;
|
|
2266
2344
|
const table = (columns, rows) => {
|
|
2267
2345
|
if (rows.length === 0)
|
|
2268
2346
|
return '<p class="empty">Nothing here yet.</p>';
|
|
@@ -2272,16 +2350,22 @@ function bootAdmin(config) {
|
|
|
2272
2350
|
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
2273
2351
|
return `<tr${attrs}>${cells}</tr>`;
|
|
2274
2352
|
}).join("");
|
|
2275
|
-
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
2353
|
+
return `<div class="table-shell"><table><thead>${head}</thead><tbody>${body}</tbody></table></div>`;
|
|
2276
2354
|
};
|
|
2277
2355
|
const preview = (value) => {
|
|
2278
2356
|
const text = JSON.stringify(value) ?? "null";
|
|
2279
|
-
|
|
2357
|
+
const clipped = text.length > 180 ? `${text.slice(0, 180)}\u2026` : text;
|
|
2358
|
+
return `<code class="json-inline">${highlightJsonText(clipped)}</code>`;
|
|
2280
2359
|
};
|
|
2281
2360
|
const when = (epoch) => {
|
|
2282
2361
|
const date = new Date(epoch);
|
|
2283
2362
|
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
2284
2363
|
};
|
|
2364
|
+
const journalWhen = (value) => {
|
|
2365
|
+
const numeric = value.trim() === "" ? Number.NaN : Number(value);
|
|
2366
|
+
const epoch = Number.isFinite(numeric) ? numeric : Date.parse(value);
|
|
2367
|
+
return Number.isFinite(epoch) ? when(epoch) : value;
|
|
2368
|
+
};
|
|
2285
2369
|
const clockCard = (epoch) => {
|
|
2286
2370
|
const date = new Date(epoch);
|
|
2287
2371
|
if (Number.isNaN(date.getTime()))
|
|
@@ -2401,7 +2485,7 @@ function bootAdmin(config) {
|
|
|
2401
2485
|
};
|
|
2402
2486
|
const renderOverview = async () => {
|
|
2403
2487
|
const [health, shape, clock, journal] = await Promise.all([
|
|
2404
|
-
originGet(
|
|
2488
|
+
originGet(`${config.adminPrefix}/health`),
|
|
2405
2489
|
api("GET", "/state"),
|
|
2406
2490
|
api("GET", "/clock"),
|
|
2407
2491
|
api("GET", "/requests?limit=8")
|
|
@@ -2418,7 +2502,7 @@ function bootAdmin(config) {
|
|
|
2418
2502
|
},
|
|
2419
2503
|
{
|
|
2420
2504
|
k: "Status",
|
|
2421
|
-
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
2505
|
+
v: `<span class="badge ok">${esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")}</span>`
|
|
2422
2506
|
},
|
|
2423
2507
|
{ k: "Namespace", v: esc(state.namespace) },
|
|
2424
2508
|
{ k: "Collections", v: String(decoded.collections.length) },
|
|
@@ -2505,26 +2589,46 @@ function bootAdmin(config) {
|
|
|
2505
2589
|
const renderJournal = async () => {
|
|
2506
2590
|
const journal = await api("GET", "/requests?limit=100");
|
|
2507
2591
|
el("journal", HTMLElement).innerHTML = table([
|
|
2508
|
-
{ label: "When", cell: (row) => esc(row.at) },
|
|
2509
|
-
{
|
|
2592
|
+
{ label: "When", cell: (row) => esc(journalWhen(row.at)) },
|
|
2593
|
+
{
|
|
2594
|
+
label: "Method",
|
|
2595
|
+
cell: (row) => `<span class="method">${esc(row.method)}</span>`
|
|
2596
|
+
},
|
|
2510
2597
|
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2511
|
-
{
|
|
2598
|
+
{
|
|
2599
|
+
label: "Status",
|
|
2600
|
+
cell: (row) => `<span class="badge ${row.status.startsWith("2") ? "ok" : row.status.startsWith("4") || row.status.startsWith("5") ? "warn" : ""}">${esc(row.status)}</span>`
|
|
2601
|
+
},
|
|
2512
2602
|
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
2513
2603
|
], decodeJournal(journal));
|
|
2514
2604
|
};
|
|
2515
|
-
const
|
|
2516
|
-
const
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2605
|
+
const paintRoutes = () => {
|
|
2606
|
+
const query = el("route-search", HTMLInputElement).value.trim().toLocaleLowerCase();
|
|
2607
|
+
const routes = state.routes.filter((route) => route.toLocaleLowerCase().includes(query));
|
|
2608
|
+
el("route-count", HTMLElement).textContent = `${routes.length} of ${state.routes.length}`;
|
|
2609
|
+
if (state.routes.length === 0) {
|
|
2610
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No admin routes are available.</p>';
|
|
2611
|
+
return;
|
|
2612
|
+
}
|
|
2613
|
+
if (routes.length === 0) {
|
|
2614
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No routes match this filter.</p>';
|
|
2615
|
+
return;
|
|
2616
|
+
}
|
|
2520
2617
|
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2521
2618
|
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2522
2619
|
const parts = route.split(" ");
|
|
2523
2620
|
const method = parts[0] ?? "";
|
|
2524
2621
|
const path = parts.slice(1).join(" ");
|
|
2525
|
-
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>`;
|
|
2622
|
+
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>`;
|
|
2526
2623
|
}).join("");
|
|
2527
2624
|
};
|
|
2625
|
+
const renderRoutes = async () => {
|
|
2626
|
+
const data = await api("GET", "/");
|
|
2627
|
+
if (!isRecord2(data))
|
|
2628
|
+
throw new Error("Route list was not an object");
|
|
2629
|
+
state.routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
2630
|
+
paintRoutes();
|
|
2631
|
+
};
|
|
2528
2632
|
const renderers = {
|
|
2529
2633
|
overview: renderOverview,
|
|
2530
2634
|
state: renderState,
|
|
@@ -2533,19 +2637,50 @@ function bootAdmin(config) {
|
|
|
2533
2637
|
journal: renderJournal,
|
|
2534
2638
|
routes: renderRoutes
|
|
2535
2639
|
};
|
|
2640
|
+
let refreshToken = 0;
|
|
2536
2641
|
async function refresh() {
|
|
2642
|
+
const token = ++refreshToken;
|
|
2537
2643
|
clearError();
|
|
2644
|
+
const main = el("views", HTMLElement);
|
|
2645
|
+
const sync = el("sync-status", HTMLElement);
|
|
2646
|
+
main.setAttribute("aria-busy", "true");
|
|
2647
|
+
sync.classList.add("busy");
|
|
2648
|
+
sync.textContent = "Updating";
|
|
2538
2649
|
const render = renderers[state.view];
|
|
2539
|
-
|
|
2540
|
-
|
|
2650
|
+
try {
|
|
2651
|
+
if (render)
|
|
2652
|
+
await render();
|
|
2653
|
+
if (token === refreshToken)
|
|
2654
|
+
sync.textContent = "Up to date";
|
|
2655
|
+
} finally {
|
|
2656
|
+
if (token === refreshToken) {
|
|
2657
|
+
main.removeAttribute("aria-busy");
|
|
2658
|
+
sync.classList.remove("busy");
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2541
2661
|
}
|
|
2542
2662
|
const delay = (ms) => new Promise((resolve) => {
|
|
2543
2663
|
window.setTimeout(resolve, ms);
|
|
2544
2664
|
});
|
|
2545
|
-
const confirmAction = async (button, pending, done, work) => {
|
|
2665
|
+
const confirmAction = async (button, pending, done, work, confirmLabel) => {
|
|
2546
2666
|
if (button.dataset.pending === "1")
|
|
2547
2667
|
return;
|
|
2548
2668
|
const label = button.textContent ?? "";
|
|
2669
|
+
if (confirmLabel !== void 0 && button.dataset.confirming !== "1") {
|
|
2670
|
+
button.dataset.confirming = "1";
|
|
2671
|
+
button.classList.add("confirming");
|
|
2672
|
+
button.textContent = confirmLabel;
|
|
2673
|
+
window.setTimeout(() => {
|
|
2674
|
+
if (button.dataset.confirming !== "1")
|
|
2675
|
+
return;
|
|
2676
|
+
button.textContent = label;
|
|
2677
|
+
button.classList.remove("confirming");
|
|
2678
|
+
delete button.dataset.confirming;
|
|
2679
|
+
}, 3e3);
|
|
2680
|
+
return;
|
|
2681
|
+
}
|
|
2682
|
+
button.classList.remove("confirming");
|
|
2683
|
+
delete button.dataset.confirming;
|
|
2549
2684
|
const width = button.offsetWidth;
|
|
2550
2685
|
button.dataset.pending = "1";
|
|
2551
2686
|
button.classList.add("pressed");
|
|
@@ -2566,6 +2701,65 @@ function bootAdmin(config) {
|
|
|
2566
2701
|
delete button.dataset.pending;
|
|
2567
2702
|
}
|
|
2568
2703
|
};
|
|
2704
|
+
const wireJsonEditor = (textareaId, highlightId, statusId, shellId, allowEmpty) => {
|
|
2705
|
+
const textarea = el(textareaId, HTMLTextAreaElement);
|
|
2706
|
+
const highlight = el(highlightId, HTMLElement);
|
|
2707
|
+
const status = el(statusId, HTMLElement);
|
|
2708
|
+
const shell = el(shellId, HTMLElement);
|
|
2709
|
+
const sync = () => {
|
|
2710
|
+
const text = textarea.value;
|
|
2711
|
+
highlight.innerHTML = `${highlightJsonText(text)}
|
|
2712
|
+
`;
|
|
2713
|
+
let valid = true;
|
|
2714
|
+
let message = "Valid JSON";
|
|
2715
|
+
if (allowEmpty && text.trim() === "")
|
|
2716
|
+
message = "No body";
|
|
2717
|
+
else {
|
|
2718
|
+
try {
|
|
2719
|
+
readJson2(text);
|
|
2720
|
+
} catch {
|
|
2721
|
+
valid = false;
|
|
2722
|
+
message = "Invalid JSON";
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
shell.classList.toggle("invalid", !valid);
|
|
2726
|
+
status.classList.toggle("invalid", !valid);
|
|
2727
|
+
status.textContent = message;
|
|
2728
|
+
textarea.setAttribute("aria-invalid", valid ? "false" : "true");
|
|
2729
|
+
};
|
|
2730
|
+
const syncScroll = () => {
|
|
2731
|
+
const preview2 = highlight.parentElement;
|
|
2732
|
+
if (!(preview2 instanceof HTMLElement))
|
|
2733
|
+
return;
|
|
2734
|
+
preview2.scrollTop = textarea.scrollTop;
|
|
2735
|
+
preview2.scrollLeft = textarea.scrollLeft;
|
|
2736
|
+
};
|
|
2737
|
+
textarea.addEventListener("input", sync);
|
|
2738
|
+
textarea.addEventListener("scroll", syncScroll);
|
|
2739
|
+
sync();
|
|
2740
|
+
return sync;
|
|
2741
|
+
};
|
|
2742
|
+
const syncRouteEditor = wireJsonEditor("route-body", "route-highlight", "route-json-status", "route-editor", true);
|
|
2743
|
+
const syncRecordEditor = wireJsonEditor("editor-value", "editor-highlight", "editor-json-status", "record-editor", false);
|
|
2744
|
+
for (const button of document.querySelectorAll("[data-format]")) {
|
|
2745
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2746
|
+
continue;
|
|
2747
|
+
button.addEventListener("click", () => {
|
|
2748
|
+
const target = button.dataset.format;
|
|
2749
|
+
if (target === void 0)
|
|
2750
|
+
return;
|
|
2751
|
+
const textarea = document.getElementById(target);
|
|
2752
|
+
if (!(textarea instanceof HTMLTextAreaElement) || textarea.value.trim() === "")
|
|
2753
|
+
return;
|
|
2754
|
+
try {
|
|
2755
|
+
textarea.value = JSON.stringify(readJson2(textarea.value), null, 2) ?? "";
|
|
2756
|
+
textarea.dispatchEvent(new Event("input"));
|
|
2757
|
+
textarea.focus();
|
|
2758
|
+
} catch {
|
|
2759
|
+
showError(new Error("Fix the JSON before formatting it"));
|
|
2760
|
+
}
|
|
2761
|
+
});
|
|
2762
|
+
}
|
|
2569
2763
|
document.addEventListener("click", (event) => {
|
|
2570
2764
|
const target = event.target;
|
|
2571
2765
|
if (!(target instanceof Element))
|
|
@@ -2638,6 +2832,7 @@ function bootAdmin(config) {
|
|
|
2638
2832
|
} else {
|
|
2639
2833
|
editorValue.value = "{\n \n}";
|
|
2640
2834
|
}
|
|
2835
|
+
syncRecordEditor();
|
|
2641
2836
|
editor.showModal();
|
|
2642
2837
|
}
|
|
2643
2838
|
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -2667,10 +2862,10 @@ function bootAdmin(config) {
|
|
|
2667
2862
|
const id = state.editing;
|
|
2668
2863
|
if (!id)
|
|
2669
2864
|
return;
|
|
2670
|
-
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2865
|
+
confirmAction(editorDelete, "Deleting...", "Deleted", () => api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2671
2866
|
editor.close();
|
|
2672
2867
|
return renderState();
|
|
2673
|
-
}).catch(showError);
|
|
2868
|
+
}), "Confirm delete").catch(showError);
|
|
2674
2869
|
});
|
|
2675
2870
|
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2676
2871
|
if (!(button instanceof HTMLButtonElement))
|
|
@@ -2716,10 +2911,10 @@ function bootAdmin(config) {
|
|
|
2716
2911
|
}).then(() => renderFaults()).catch(showError);
|
|
2717
2912
|
});
|
|
2718
2913
|
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2719
|
-
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
|
|
2914
|
+
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults()), "Confirm clear").catch(showError);
|
|
2720
2915
|
});
|
|
2721
2916
|
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2722
|
-
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
|
|
2917
|
+
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal()), "Confirm clear").catch(showError);
|
|
2723
2918
|
});
|
|
2724
2919
|
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2725
2920
|
const target = event.target;
|
|
@@ -2733,9 +2928,15 @@ function bootAdmin(config) {
|
|
|
2733
2928
|
return;
|
|
2734
2929
|
state.route = route;
|
|
2735
2930
|
el("route-title", HTMLElement).textContent = route;
|
|
2931
|
+
el("route-send", HTMLButtonElement).disabled = false;
|
|
2736
2932
|
const method = route.split(" ")[0] ?? "";
|
|
2737
|
-
el("route-body", HTMLTextAreaElement)
|
|
2933
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
2934
|
+
routeBody.disabled = method === "GET" || method === "DELETE";
|
|
2935
|
+
el("route-editor", HTMLElement).hidden = routeBody.disabled;
|
|
2936
|
+
el("route-json-status", HTMLElement).parentElement?.toggleAttribute("hidden", routeBody.disabled);
|
|
2937
|
+
syncRouteEditor();
|
|
2738
2938
|
});
|
|
2939
|
+
el("route-search", HTMLInputElement).addEventListener("input", paintRoutes);
|
|
2739
2940
|
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2740
2941
|
event.preventDefault();
|
|
2741
2942
|
const send = async () => {
|
|
@@ -2757,7 +2958,8 @@ function bootAdmin(config) {
|
|
|
2757
2958
|
}
|
|
2758
2959
|
}
|
|
2759
2960
|
const data = await api(method, concrete, body);
|
|
2760
|
-
|
|
2961
|
+
const text = JSON.stringify(data, null, 2) ?? "null";
|
|
2962
|
+
el("route-result", HTMLElement).innerHTML = `<code>${highlightJsonText(text)}</code>`;
|
|
2761
2963
|
};
|
|
2762
2964
|
send().catch(showError);
|
|
2763
2965
|
});
|
|
@@ -2875,11 +3077,13 @@ function bootAdmin(config) {
|
|
|
2875
3077
|
views.append(section);
|
|
2876
3078
|
mountSqlExplorer(body, panelApi);
|
|
2877
3079
|
}
|
|
3080
|
+
show(document.getElementById(`view-${state.view}`) === null ? "overview" : state.view);
|
|
2878
3081
|
return refresh();
|
|
2879
3082
|
}).catch(showError);
|
|
2880
3083
|
}
|
|
2881
3084
|
var clientPrelude = [
|
|
2882
3085
|
isRecord2,
|
|
3086
|
+
highlightJsonText,
|
|
2883
3087
|
mountAdminBrand,
|
|
2884
3088
|
startAdminBrand,
|
|
2885
3089
|
faultPresetList,
|
|
@@ -2902,6 +3106,7 @@ ${entry}(${payload});
|
|
|
2902
3106
|
var defineMock = (runtime) => runtime;
|
|
2903
3107
|
var STANDARD_ADMIN_ROUTES = [
|
|
2904
3108
|
"GET /",
|
|
3109
|
+
"GET /health",
|
|
2905
3110
|
"POST /reset",
|
|
2906
3111
|
"GET /namespaces",
|
|
2907
3112
|
"GET /clock",
|
|
@@ -2981,11 +3186,11 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
2981
3186
|
}
|
|
2982
3187
|
});
|
|
2983
3188
|
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
2984
|
-
var renderAdminDocument = (service) => {
|
|
3189
|
+
var renderAdminDocument = (service, adminPrefix = ADMIN_PREFIX) => {
|
|
2985
3190
|
const name = escapeHtml(service);
|
|
2986
3191
|
const script = adminClientSource({
|
|
2987
3192
|
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
2988
|
-
adminPrefix
|
|
3193
|
+
adminPrefix,
|
|
2989
3194
|
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
2990
3195
|
brandsUrl: ADMIN_BRANDS_URL,
|
|
2991
3196
|
service
|
|
@@ -3002,75 +3207,99 @@ var renderAdminDocument = (service) => {
|
|
|
3002
3207
|
${CSS_RESET}
|
|
3003
3208
|
:root {
|
|
3004
3209
|
color-scheme: light;
|
|
3005
|
-
--bg: #
|
|
3210
|
+
--bg: #f7f8fb;
|
|
3006
3211
|
--bg-elev: #ffffff;
|
|
3007
|
-
--bg-muted: #
|
|
3008
|
-
--bg-inset: #
|
|
3009
|
-
--line: #
|
|
3010
|
-
--line-strong: #
|
|
3011
|
-
--ink: #
|
|
3012
|
-
--muted: #
|
|
3013
|
-
--faint: #
|
|
3014
|
-
--accent: #
|
|
3212
|
+
--bg-muted: #f1f3f7;
|
|
3213
|
+
--bg-inset: #f8f9fc;
|
|
3214
|
+
--line: #e4e7ec;
|
|
3215
|
+
--line-strong: #cfd4dc;
|
|
3216
|
+
--ink: #101828;
|
|
3217
|
+
--muted: #475467;
|
|
3218
|
+
--faint: #667085;
|
|
3219
|
+
--accent: #6941c6;
|
|
3220
|
+
--accent-hover: #5933b4;
|
|
3015
3221
|
--accent-ink: #ffffff;
|
|
3016
|
-
--accent-soft: #
|
|
3017
|
-
--ok: #
|
|
3018
|
-
--ok-soft: #
|
|
3019
|
-
--warn: #
|
|
3020
|
-
--warn-soft: #
|
|
3021
|
-
--err: #
|
|
3022
|
-
--err-soft: #
|
|
3023
|
-
--
|
|
3222
|
+
--accent-soft: #f4f0ff;
|
|
3223
|
+
--ok: #067647;
|
|
3224
|
+
--ok-soft: #ecfdf3;
|
|
3225
|
+
--warn: #b54708;
|
|
3226
|
+
--warn-soft: #fffaeb;
|
|
3227
|
+
--err: #b42318;
|
|
3228
|
+
--err-soft: #fef3f2;
|
|
3229
|
+
--code-key: #6941c6;
|
|
3230
|
+
--code-string: #067647;
|
|
3231
|
+
--code-number: #175cd3;
|
|
3232
|
+
--code-boolean: #c11574;
|
|
3233
|
+
--code-null: #667085;
|
|
3234
|
+
--shadow-xs: 0 1px 2px rgb(16 24 40 / 0.05);
|
|
3235
|
+
--shadow-sm: 0 1px 3px rgb(16 24 40 / 0.08), 0 1px 2px rgb(16 24 40 / 0.04);
|
|
3236
|
+
--shadow-lg: 0 18px 48px rgb(16 24 40 / 0.16), 0 4px 12px rgb(16 24 40 / 0.08);
|
|
3024
3237
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
3025
3238
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
3026
|
-
--radius:
|
|
3027
|
-
--header:
|
|
3028
|
-
--nav:
|
|
3239
|
+
--radius: 12px;
|
|
3240
|
+
--header: 72px;
|
|
3241
|
+
--nav: 224px;
|
|
3029
3242
|
}
|
|
3030
3243
|
:root[data-theme="dark"] {
|
|
3031
3244
|
color-scheme: dark;
|
|
3032
|
-
--bg: #
|
|
3033
|
-
--bg-elev: #
|
|
3034
|
-
--bg-muted: #
|
|
3035
|
-
--bg-inset: #
|
|
3036
|
-
--line: #
|
|
3037
|
-
--line-strong: #
|
|
3038
|
-
--ink: #
|
|
3039
|
-
--muted: #
|
|
3040
|
-
--faint: #
|
|
3041
|
-
--accent: #
|
|
3042
|
-
--accent-
|
|
3043
|
-
--accent-
|
|
3044
|
-
--
|
|
3045
|
-
--ok
|
|
3046
|
-
--
|
|
3047
|
-
--warn
|
|
3048
|
-
--
|
|
3049
|
-
--err
|
|
3050
|
-
--
|
|
3245
|
+
--bg: #0c0e14;
|
|
3246
|
+
--bg-elev: #151821;
|
|
3247
|
+
--bg-muted: #20232e;
|
|
3248
|
+
--bg-inset: #11141c;
|
|
3249
|
+
--line: #2b303d;
|
|
3250
|
+
--line-strong: #414858;
|
|
3251
|
+
--ink: #f5f7fa;
|
|
3252
|
+
--muted: #b6bdc9;
|
|
3253
|
+
--faint: #8d96a7;
|
|
3254
|
+
--accent: #9e77ed;
|
|
3255
|
+
--accent-hover: #b692f6;
|
|
3256
|
+
--accent-ink: #ffffff;
|
|
3257
|
+
--accent-soft: #2b2142;
|
|
3258
|
+
--ok: #75e0a7;
|
|
3259
|
+
--ok-soft: #102a20;
|
|
3260
|
+
--warn: #fec84b;
|
|
3261
|
+
--warn-soft: #302611;
|
|
3262
|
+
--err: #fda29b;
|
|
3263
|
+
--err-soft: #321817;
|
|
3264
|
+
--code-key: #c3a6ff;
|
|
3265
|
+
--code-string: #75e0a7;
|
|
3266
|
+
--code-number: #84adff;
|
|
3267
|
+
--code-boolean: #f9a8d4;
|
|
3268
|
+
--code-null: #98a2b3;
|
|
3269
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3270
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3271
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
3051
3272
|
}
|
|
3052
3273
|
@media (prefers-color-scheme: dark) {
|
|
3053
3274
|
:root:not([data-theme="light"]) {
|
|
3054
3275
|
color-scheme: dark;
|
|
3055
|
-
--bg: #
|
|
3056
|
-
--bg-elev: #
|
|
3057
|
-
--bg-muted: #
|
|
3058
|
-
--bg-inset: #
|
|
3059
|
-
--line: #
|
|
3060
|
-
--line-strong: #
|
|
3061
|
-
--ink: #
|
|
3062
|
-
--muted: #
|
|
3063
|
-
--faint: #
|
|
3064
|
-
--accent: #
|
|
3065
|
-
--accent-
|
|
3066
|
-
--accent-
|
|
3067
|
-
--
|
|
3068
|
-
--ok
|
|
3069
|
-
--
|
|
3070
|
-
--warn
|
|
3071
|
-
--
|
|
3072
|
-
--err
|
|
3073
|
-
--
|
|
3276
|
+
--bg: #0c0e14;
|
|
3277
|
+
--bg-elev: #151821;
|
|
3278
|
+
--bg-muted: #20232e;
|
|
3279
|
+
--bg-inset: #11141c;
|
|
3280
|
+
--line: #2b303d;
|
|
3281
|
+
--line-strong: #414858;
|
|
3282
|
+
--ink: #f5f7fa;
|
|
3283
|
+
--muted: #b6bdc9;
|
|
3284
|
+
--faint: #8d96a7;
|
|
3285
|
+
--accent: #9e77ed;
|
|
3286
|
+
--accent-hover: #b692f6;
|
|
3287
|
+
--accent-ink: #ffffff;
|
|
3288
|
+
--accent-soft: #2b2142;
|
|
3289
|
+
--ok: #75e0a7;
|
|
3290
|
+
--ok-soft: #102a20;
|
|
3291
|
+
--warn: #fec84b;
|
|
3292
|
+
--warn-soft: #302611;
|
|
3293
|
+
--err: #fda29b;
|
|
3294
|
+
--err-soft: #321817;
|
|
3295
|
+
--code-key: #c3a6ff;
|
|
3296
|
+
--code-string: #75e0a7;
|
|
3297
|
+
--code-number: #84adff;
|
|
3298
|
+
--code-boolean: #f9a8d4;
|
|
3299
|
+
--code-null: #98a2b3;
|
|
3300
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3301
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3302
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
3074
3303
|
}
|
|
3075
3304
|
}
|
|
3076
3305
|
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
@@ -3088,13 +3317,17 @@ var renderAdminDocument = (service) => {
|
|
|
3088
3317
|
padding-right: env(safe-area-inset-right);
|
|
3089
3318
|
-webkit-text-size-adjust: 100%;
|
|
3090
3319
|
}
|
|
3091
|
-
|
|
3320
|
+
::selection { background: color-mix(in srgb, var(--accent) 28%, transparent); }
|
|
3321
|
+
:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px; }
|
|
3322
|
+
button, input, select, textarea { transition: border-color 140ms ease, box-shadow 140ms ease, transform 100ms ease; }
|
|
3323
|
+
button:disabled, input:disabled, select:disabled, textarea:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
3092
3324
|
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
3093
3325
|
header.top {
|
|
3094
|
-
position: sticky; top: 0; z-index:
|
|
3326
|
+
position: sticky; top: 0; z-index: 20;
|
|
3095
3327
|
display: flex; align-items: center; gap: 12px;
|
|
3096
|
-
min-height: var(--header); padding:
|
|
3097
|
-
background: var(--bg-elev);
|
|
3328
|
+
min-height: var(--header); padding: 12px 20px;
|
|
3329
|
+
background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
|
|
3330
|
+
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
3098
3331
|
border-bottom: 1px solid var(--line);
|
|
3099
3332
|
min-width: 0; max-width: 100%;
|
|
3100
3333
|
}
|
|
@@ -3102,12 +3335,21 @@ var renderAdminDocument = (service) => {
|
|
|
3102
3335
|
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
3103
3336
|
min-width: 0; max-width: 100%; margin-right: auto;
|
|
3104
3337
|
}
|
|
3105
|
-
.brand-
|
|
3106
|
-
.brand-
|
|
3338
|
+
.brand-lockup { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
3339
|
+
.brand-mark {
|
|
3340
|
+
width: 38px; height: 38px; display: grid; place-items: center; flex: none;
|
|
3341
|
+
border-radius: 11px; color: #fff;
|
|
3342
|
+
background: linear-gradient(145deg, #7f56d9 0%, #53389e 100%);
|
|
3343
|
+
box-shadow: 0 6px 16px rgb(105 65 198 / 0.24), inset 0 1px 0 rgb(255 255 255 / 0.25);
|
|
3344
|
+
}
|
|
3345
|
+
.brand-mark svg { width: 22px; height: 22px; }
|
|
3346
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; line-height: 1.25; }
|
|
3347
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
3107
3348
|
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
3108
3349
|
.vendor {
|
|
3109
3350
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
3110
3351
|
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
3352
|
+
padding: 5px 8px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-inset);
|
|
3111
3353
|
}
|
|
3112
3354
|
.vendor[hidden] { display: none; }
|
|
3113
3355
|
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
@@ -3119,12 +3361,17 @@ var renderAdminDocument = (service) => {
|
|
|
3119
3361
|
.vendor-sep { color: var(--faint); }
|
|
3120
3362
|
.vendor-link { color: inherit; text-decoration: none; }
|
|
3121
3363
|
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
3122
|
-
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items:
|
|
3123
|
-
|
|
3364
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
|
|
3365
|
+
.sync-status { align-self: center; display: flex; align-items: center; gap: 6px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3366
|
+
.sync-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
|
|
3367
|
+
.sync-status.busy::before { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); animation: pulse 1.2s ease-in-out infinite; }
|
|
3368
|
+
label.field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; font-weight: 600; color: var(--faint); }
|
|
3124
3369
|
label.field input, label.field select, .controls input[type="password"] {
|
|
3125
|
-
background-color: var(--bg-
|
|
3126
|
-
padding:
|
|
3370
|
+
background-color: var(--bg-elev); border: 1px solid var(--line-strong); border-radius: 9px;
|
|
3371
|
+
padding: 8px 10px; min-height: 38px; line-height: 1.2; box-shadow: var(--shadow-xs);
|
|
3127
3372
|
}
|
|
3373
|
+
label.field input:hover, label.field select:hover, textarea:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--line-strong)); }
|
|
3374
|
+
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; }
|
|
3128
3375
|
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
3129
3376
|
the text fields in the same form. Paint it like those fields. */
|
|
3130
3377
|
label.field select {
|
|
@@ -3141,12 +3388,20 @@ var renderAdminDocument = (service) => {
|
|
|
3141
3388
|
dialog .field > textarea { width: 100%; }
|
|
3142
3389
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
3143
3390
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
.btn
|
|
3391
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
|
3392
|
+
border-radius: 9px; min-height: 38px; padding: 0 13px; border: 1px solid var(--line-strong);
|
|
3393
|
+
background: var(--bg-elev); color: var(--ink); font-size: 13px; font-weight: 600; line-height: 1;
|
|
3394
|
+
box-shadow: var(--shadow-xs);
|
|
3395
|
+
}
|
|
3396
|
+
.icon-btn { width: 38px; padding: 0; }
|
|
3397
|
+
.icon-btn svg, .btn svg, .btn-primary svg { width: 16px; height: 16px; }
|
|
3398
|
+
.icon-btn:hover, .btn:hover { background: var(--bg-muted); border-color: var(--line-strong); }
|
|
3399
|
+
.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; }
|
|
3400
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
3401
|
+
.btn-quiet { background: transparent; border-color: transparent; box-shadow: none; }
|
|
3402
|
+
.btn-quiet:hover { background: var(--bg-muted); }
|
|
3403
|
+
.btn-danger { color: var(--err); border-color: color-mix(in srgb, var(--err) 25%, var(--line)); }
|
|
3404
|
+
.btn-danger:hover, .btn-danger.confirming { background: var(--err-soft); border-color: color-mix(in srgb, var(--err) 45%, var(--line)); }
|
|
3150
3405
|
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
3151
3406
|
so the press itself has to stay visible. */
|
|
3152
3407
|
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
@@ -3162,93 +3417,159 @@ var renderAdminDocument = (service) => {
|
|
|
3162
3417
|
flex: 1 0 auto; min-width: 0;
|
|
3163
3418
|
}
|
|
3164
3419
|
nav.side {
|
|
3165
|
-
|
|
3420
|
+
position: sticky; top: var(--header); align-self: start; height: calc(100vh - var(--header));
|
|
3421
|
+
border-right: 1px solid var(--line); padding: 18px 12px;
|
|
3166
3422
|
display: flex; flex-direction: column; gap: 4px;
|
|
3167
|
-
background: var(--bg-elev);
|
|
3423
|
+
background: var(--bg-elev); overflow-y: auto;
|
|
3168
3424
|
}
|
|
3425
|
+
.nav-label { padding: 0 10px 8px; color: var(--faint); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
|
|
3169
3426
|
nav.side button {
|
|
3427
|
+
position: relative; display: flex; align-items: center; gap: 10px; width: 100%;
|
|
3170
3428
|
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
3171
|
-
padding: 9px 10px; min-height:
|
|
3172
|
-
}
|
|
3173
|
-
nav.side button
|
|
3174
|
-
|
|
3175
|
-
.
|
|
3176
|
-
.
|
|
3177
|
-
.
|
|
3429
|
+
padding: 9px 10px; min-height: 42px; color: var(--muted); font-size: 13px; font-weight: 550;
|
|
3430
|
+
}
|
|
3431
|
+
nav.side button:hover { background: var(--bg-muted); color: var(--ink); }
|
|
3432
|
+
nav.side button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 650; }
|
|
3433
|
+
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); }
|
|
3434
|
+
.nav-icon { width: 18px; height: 18px; display: grid; place-items: center; flex: none; }
|
|
3435
|
+
.nav-icon svg { width: 18px; height: 18px; }
|
|
3436
|
+
main { padding: 32px clamp(20px, 4vw, 48px) 64px; max-width: 1320px; width: 100%; min-width: 0; }
|
|
3437
|
+
main[aria-busy="true"] .view { opacity: 0.62; }
|
|
3438
|
+
.view { animation: view-in 180ms ease-out; }
|
|
3439
|
+
.view[hidden] { display: none; }
|
|
3440
|
+
.view-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
|
|
3441
|
+
.view h2 { margin: 0 0 5px; font-size: clamp(24px, 3vw, 30px); line-height: 1.2; letter-spacing: -0.035em; }
|
|
3442
|
+
.view h3 { margin: 24px 0 10px; font-size: 15px; letter-spacing: -0.01em; }
|
|
3443
|
+
.lede { margin: 0; color: var(--muted); max-width: 72ch; font-size: 14px; }
|
|
3444
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 175px), 1fr)); gap: 14px; }
|
|
3178
3445
|
.card {
|
|
3179
3446
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
3180
|
-
padding:
|
|
3447
|
+
padding: 18px; box-shadow: var(--shadow-sm);
|
|
3181
3448
|
}
|
|
3182
|
-
.card
|
|
3183
|
-
.card
|
|
3449
|
+
.stat-card { position: relative; overflow: hidden; }
|
|
3450
|
+
.stat-card::after { content: ""; position: absolute; inset: 0 0 auto; height: 2px; background: linear-gradient(90deg, var(--accent), transparent 78%); opacity: 0.7; }
|
|
3451
|
+
.card .k { color: var(--faint); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
3452
|
+
.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; }
|
|
3184
3453
|
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
3185
|
-
.
|
|
3454
|
+
.section-card { background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-xs); overflow: hidden; }
|
|
3455
|
+
.section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
|
|
3456
|
+
.section-title h3 { margin: 0; }
|
|
3457
|
+
.split { display: grid; grid-template-columns: minmax(0, 250px) minmax(0, 1fr); gap: 18px; align-items: start; }
|
|
3458
|
+
#collections { padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); box-shadow: var(--shadow-xs); }
|
|
3186
3459
|
.collection {
|
|
3187
3460
|
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
3188
|
-
border-radius:
|
|
3461
|
+
border-radius: 9px; padding: 10px 11px; min-height: 48px; color: var(--ink); cursor: pointer;
|
|
3189
3462
|
}
|
|
3190
|
-
.collection
|
|
3463
|
+
.collection:hover { background: var(--bg-muted); }
|
|
3464
|
+
.collection[aria-current="true"] { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 18%, var(--line)); color: var(--accent); }
|
|
3191
3465
|
.collection small { display: block; color: var(--faint); }
|
|
3192
3466
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
3193
|
-
.chip {
|
|
3194
|
-
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
3195
|
-
border-radius: 999px; padding: 4px 8px;
|
|
3196
|
-
}
|
|
3467
|
+
.chip { font-family: var(--mono); font-size: 11px; background: var(--bg-muted); border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; }
|
|
3197
3468
|
.chip i { color: var(--faint); font-style: normal; }
|
|
3198
3469
|
.sql-layout {
|
|
3199
3470
|
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
3200
3471
|
}
|
|
3201
|
-
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
3202
|
-
.sql-tables button { text-align: left; }
|
|
3472
|
+
.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); }
|
|
3473
|
+
.sql-tables button { text-align: left; border: 0; border-radius: 8px; padding: 9px 10px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
3474
|
+
.sql-tables button:hover, .sql-tables button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); }
|
|
3203
3475
|
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
3204
|
-
table { width: 100%; border-
|
|
3205
|
-
|
|
3206
|
-
th {
|
|
3476
|
+
.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); }
|
|
3477
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
3478
|
+
th, td { text-align: left; padding: 11px 13px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
3479
|
+
th { color: var(--faint); background: var(--bg-inset); font-weight: 650; font-size: 11px; text-transform: uppercase; letter-spacing: 0.045em; }
|
|
3480
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
3207
3481
|
tr.clickable { cursor: pointer; }
|
|
3208
|
-
tr.clickable:hover { background: var(--
|
|
3482
|
+
tr.clickable:hover { background: var(--accent-soft); }
|
|
3209
3483
|
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
3210
3484
|
pre {
|
|
3211
3485
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
3212
|
-
background: var(--bg-inset); border-radius: 10px; padding:
|
|
3213
|
-
}
|
|
3214
|
-
.
|
|
3486
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 10px; padding: 14px; font-size: 12px; line-height: 1.65;
|
|
3487
|
+
}
|
|
3488
|
+
.json-key { color: var(--code-key); }
|
|
3489
|
+
.json-string { color: var(--code-string); }
|
|
3490
|
+
.json-number { color: var(--code-number); }
|
|
3491
|
+
.json-boolean { color: var(--code-boolean); }
|
|
3492
|
+
.json-null { color: var(--code-null); font-style: italic; }
|
|
3493
|
+
.json-inline { display: block; max-width: 44rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
3494
|
+
.json-view { position: relative; max-height: 26rem; overflow: auto; }
|
|
3495
|
+
.json-editor { position: relative; min-height: 190px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--bg-inset); overflow: hidden; }
|
|
3496
|
+
.json-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); }
|
|
3497
|
+
.json-editor.invalid { border-color: var(--err); }
|
|
3498
|
+
.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; }
|
|
3499
|
+
.json-editor pre { position: absolute; inset: 0; pointer-events: none; background: transparent; }
|
|
3500
|
+
.json-editor textarea { position: relative; z-index: 1; resize: vertical; background: transparent; color: transparent; caret-color: var(--ink); -webkit-text-fill-color: transparent; }
|
|
3501
|
+
.json-editor textarea::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); }
|
|
3502
|
+
.editor-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; }
|
|
3503
|
+
.json-status { display: inline-flex; align-items: center; gap: 6px; color: var(--ok); font-size: 11px; }
|
|
3504
|
+
.json-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
3505
|
+
.json-status.invalid { color: var(--err); }
|
|
3506
|
+
.format-json { border: 0; padding: 3px 5px; background: transparent; color: var(--accent); font-size: 11px; font-weight: 650; cursor: pointer; }
|
|
3507
|
+
.row-actions { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 14px 0; }
|
|
3215
3508
|
.banner {
|
|
3216
|
-
display: none;
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3509
|
+
display: none; position: sticky; top: calc(var(--header) + 12px); z-index: 15;
|
|
3510
|
+
align-items: center; gap: 10px; margin: 0 0 16px; padding: 11px 12px 11px 14px;
|
|
3511
|
+
border: 1px solid color-mix(in srgb, var(--err) 30%, var(--line)); border-radius: 10px;
|
|
3512
|
+
background: var(--err-soft); color: var(--err); box-shadow: var(--shadow-sm); font-size: 13px;
|
|
3513
|
+
}
|
|
3514
|
+
.banner.show { display: flex; }
|
|
3515
|
+
.banner span { flex: 1; }
|
|
3516
|
+
.banner button { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 7px; background: transparent; color: currentColor; cursor: pointer; }
|
|
3517
|
+
.banner button:hover { background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
3518
|
+
.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); }
|
|
3221
3519
|
dialog {
|
|
3222
|
-
border: 1px solid var(--line); border-radius:
|
|
3223
|
-
color: var(--ink); width: min(
|
|
3224
|
-
}
|
|
3225
|
-
dialog
|
|
3226
|
-
dialog
|
|
3520
|
+
border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--bg-elev);
|
|
3521
|
+
color: var(--ink); width: min(720px, calc(100% - 24px)); max-width: calc(100vw - 24px); box-shadow: var(--shadow-lg);
|
|
3522
|
+
}
|
|
3523
|
+
dialog[open] { animation: dialog-in 160ms ease-out; }
|
|
3524
|
+
dialog::backdrop { background: rgb(12 14 20 / 0.58); backdrop-filter: blur(3px); }
|
|
3525
|
+
dialog form, .dialog-body { padding: 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
3526
|
+
.dialog-heading { display: flex; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
|
3527
|
+
.dialog-heading strong { font-size: 18px; letter-spacing: -0.02em; }
|
|
3528
|
+
.dialog-heading span { color: var(--muted); font-size: 12px; }
|
|
3227
3529
|
textarea {
|
|
3228
3530
|
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
3229
3531
|
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
3230
3532
|
font-family: var(--mono); font-size: 12px;
|
|
3231
3533
|
}
|
|
3232
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
3233
|
-
.
|
|
3534
|
+
.dialog-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding-top: 4px; }
|
|
3535
|
+
.dialog-actions .btn-danger { margin-right: auto; }
|
|
3536
|
+
.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); }
|
|
3234
3537
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
3235
3538
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
3236
3539
|
.status-2 { color: var(--ok); }
|
|
3237
3540
|
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
3238
3541
|
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
3542
|
+
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
3543
|
+
.form-grid .wide { grid-column: span 2; }
|
|
3239
3544
|
.route {
|
|
3240
|
-
display: grid; grid-template-columns:
|
|
3241
|
-
padding:
|
|
3242
|
-
}
|
|
3243
|
-
.
|
|
3545
|
+
display: grid; grid-template-columns: 84px minmax(0, 1fr) auto; gap: 10px; align-items: center;
|
|
3546
|
+
padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--bg-elev);
|
|
3547
|
+
}
|
|
3548
|
+
.route:first-child { border-radius: var(--radius) var(--radius) 0 0; }
|
|
3549
|
+
.route:last-child { border-bottom: 0; border-radius: 0 0 var(--radius) var(--radius); }
|
|
3550
|
+
.route-tools { display: flex; align-items: flex-end; gap: 12px; margin-bottom: 12px; }
|
|
3551
|
+
.route-tools .field { flex: 1; }
|
|
3552
|
+
.route-tools input { width: 100%; }
|
|
3553
|
+
.route-count { padding-bottom: 10px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3554
|
+
#route-list { max-height: 34rem; border: 1px solid var(--line); border-radius: var(--radius); overflow: auto; box-shadow: var(--shadow-xs); }
|
|
3555
|
+
.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; }
|
|
3556
|
+
@keyframes view-in { from { transform: translateY(3px); } to { transform: translateY(0); } }
|
|
3557
|
+
@keyframes dialog-in { from { transform: translateY(8px) scale(0.99); } to { transform: translateY(0) scale(1); } }
|
|
3558
|
+
@keyframes pulse { 50% { opacity: 0.45; } }
|
|
3559
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
|
|
3560
|
+
@media (forced-colors: active) { .json-editor pre { display: none; } .json-editor textarea { color: CanvasText; -webkit-text-fill-color: CanvasText; } }
|
|
3244
3561
|
@media (max-width: 800px) {
|
|
3562
|
+
:root { --header: 0px; }
|
|
3245
3563
|
header.top {
|
|
3246
|
-
align-items: stretch; flex-direction: column;
|
|
3247
|
-
padding
|
|
3564
|
+
position: relative; align-items: stretch; flex-direction: column;
|
|
3565
|
+
padding: max(12px, env(safe-area-inset-top)) 14px 12px;
|
|
3248
3566
|
}
|
|
3249
3567
|
input, select, textarea { font-size: 16px; }
|
|
3250
3568
|
.brand, .vendor, .controls { width: 100%; }
|
|
3569
|
+
.brand { gap: 10px; }
|
|
3570
|
+
.vendor { order: 2; border-radius: 9px; }
|
|
3251
3571
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
3572
|
+
.sync-status { display: none; }
|
|
3252
3573
|
/* The layout fills the screen. A single-column grid stretches every auto
|
|
3253
3574
|
row, so the tab bar grew with the leftover height and the buttons floated
|
|
3254
3575
|
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
@@ -3258,14 +3579,23 @@ var renderAdminDocument = (service) => {
|
|
|
3258
3579
|
align-content: start;
|
|
3259
3580
|
}
|
|
3260
3581
|
nav.side {
|
|
3261
|
-
align-self: start; height: auto;
|
|
3582
|
+
position: sticky; top: 0; z-index: 12; align-self: start; height: auto;
|
|
3262
3583
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
3263
3584
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
3264
|
-
padding: 8px;
|
|
3585
|
+
padding: 8px; box-shadow: var(--shadow-xs);
|
|
3265
3586
|
}
|
|
3266
|
-
nav
|
|
3587
|
+
.nav-label { display: none; }
|
|
3588
|
+
nav.side button { width: auto; white-space: nowrap; flex: none; }
|
|
3589
|
+
nav.side button[aria-current="true"]::before { inset: auto 9px -8px; width: auto; height: 3px; border-radius: 3px 3px 0 0; }
|
|
3267
3590
|
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
3268
|
-
|
|
3591
|
+
.form-grid { grid-template-columns: minmax(0, 1fr); }
|
|
3592
|
+
.form-grid .wide { grid-column: auto; }
|
|
3593
|
+
main { padding: 22px 14px 48px; }
|
|
3594
|
+
.view-head { margin-bottom: 18px; }
|
|
3595
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
3596
|
+
.card { padding: 14px; }
|
|
3597
|
+
.card .v { font-size: 20px; }
|
|
3598
|
+
.table-shell { border: 0; background: transparent; box-shadow: none; overflow: visible; }
|
|
3269
3599
|
table, thead, tbody, tr, th, td { display: block; }
|
|
3270
3600
|
thead { display: none; }
|
|
3271
3601
|
tr {
|
|
@@ -3280,50 +3610,63 @@ var renderAdminDocument = (service) => {
|
|
|
3280
3610
|
.route { grid-template-columns: 72px 1fr; }
|
|
3281
3611
|
.route button { grid-column: 1 / -1; }
|
|
3282
3612
|
}
|
|
3613
|
+
@media (max-width: 460px) {
|
|
3614
|
+
.controls label.field { min-width: calc(50% - 4px); }
|
|
3615
|
+
.controls .icon-btn, .controls .btn { flex: 1; }
|
|
3616
|
+
.grid { grid-template-columns: minmax(0, 1fr); }
|
|
3617
|
+
.dialog-actions { flex-wrap: wrap; }
|
|
3618
|
+
.dialog-actions button { flex: 1; }
|
|
3619
|
+
.dialog-actions .btn-danger { flex-basis: 100%; margin-right: 0; }
|
|
3620
|
+
}
|
|
3283
3621
|
</style>
|
|
3284
3622
|
</head>
|
|
3285
3623
|
<body>
|
|
3286
3624
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
3287
3625
|
<header class="top">
|
|
3288
3626
|
<div class="brand">
|
|
3289
|
-
<div class="brand-
|
|
3290
|
-
<
|
|
3291
|
-
|
|
3627
|
+
<div class="brand-lockup">
|
|
3628
|
+
<div class="brand-mark" aria-hidden="true">
|
|
3629
|
+
<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>
|
|
3630
|
+
</div>
|
|
3631
|
+
<div class="brand-id">
|
|
3632
|
+
<strong>${name}</strong>
|
|
3633
|
+
<span>Mock admin</span>
|
|
3634
|
+
</div>
|
|
3292
3635
|
</div>
|
|
3293
3636
|
<div class="vendor" id="vendor" hidden></div>
|
|
3294
3637
|
</div>
|
|
3295
3638
|
<div class="controls">
|
|
3639
|
+
<span class="sync-status" id="sync-status" role="status" aria-live="polite">Ready</span>
|
|
3296
3640
|
<label class="field">Namespace
|
|
3297
3641
|
<select id="namespace" aria-label="Namespace"></select>
|
|
3298
3642
|
</label>
|
|
3299
3643
|
<label class="field">Admin key
|
|
3300
3644
|
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
3301
3645
|
</label>
|
|
3302
|
-
<button class="icon-btn" id="theme" type="button" aria-label="
|
|
3303
|
-
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
3646
|
+
<button class="icon-btn" id="theme" type="button" aria-label="Use dark theme" title="Use dark theme"></button>
|
|
3647
|
+
<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>
|
|
3304
3648
|
</div>
|
|
3305
3649
|
</header>
|
|
3306
3650
|
<div class="layout">
|
|
3307
3651
|
<nav class="side" id="nav" aria-label="Admin sections">
|
|
3308
|
-
<
|
|
3309
|
-
<button type="button" data-view="
|
|
3310
|
-
<button type="button" data-view="
|
|
3311
|
-
<button type="button" data-view="
|
|
3312
|
-
<button type="button" data-view="
|
|
3313
|
-
<button type="button" data-view="
|
|
3652
|
+
<span class="nav-label">Workspace</span>
|
|
3653
|
+
<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>
|
|
3654
|
+
<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>
|
|
3655
|
+
<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>
|
|
3656
|
+
<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>
|
|
3657
|
+
<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>
|
|
3658
|
+
<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>
|
|
3314
3659
|
</nav>
|
|
3315
3660
|
<main id="views">
|
|
3316
|
-
<
|
|
3661
|
+
<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>
|
|
3317
3662
|
<section class="view" id="view-overview">
|
|
3318
|
-
<h2>Overview</h2>
|
|
3319
|
-
<p class="lede">Health, clock, and the collections this namespace is holding.</p>
|
|
3663
|
+
<div class="view-head"><div><h2>Overview</h2><p class="lede">Health, clock, and the collections this namespace is holding.</p></div></div>
|
|
3320
3664
|
<div class="grid" id="overview-cards"></div>
|
|
3321
3665
|
<h3>Recent requests</h3>
|
|
3322
3666
|
<div id="overview-requests"></div>
|
|
3323
3667
|
</section>
|
|
3324
3668
|
<section class="view" id="view-state" hidden>
|
|
3325
|
-
<h2>State</h2>
|
|
3326
|
-
<p class="lede">Every collection in this namespace. Declared fields stay visible before any row exists; stored rows add whatever else they contain.</p>
|
|
3669
|
+
<div class="view-head"><div><h2>State</h2><p class="lede">Inspect and edit every collection in this namespace.</p></div></div>
|
|
3327
3670
|
<div class="split">
|
|
3328
3671
|
<div id="collections"></div>
|
|
3329
3672
|
<div>
|
|
@@ -3336,8 +3679,7 @@ var renderAdminDocument = (service) => {
|
|
|
3336
3679
|
</div>
|
|
3337
3680
|
</section>
|
|
3338
3681
|
<section class="view" id="view-clock" hidden>
|
|
3339
|
-
<h2>Clock</h2>
|
|
3340
|
-
<p class="lede">The clock every timestamp in this mock reads.</p>
|
|
3682
|
+
<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>
|
|
3341
3683
|
<div class="card" id="clock-readout"></div>
|
|
3342
3684
|
<div class="row-actions">
|
|
3343
3685
|
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
@@ -3356,21 +3698,22 @@ var renderAdminDocument = (service) => {
|
|
|
3356
3698
|
</form>
|
|
3357
3699
|
</section>
|
|
3358
3700
|
<section class="view" id="view-faults" hidden>
|
|
3359
|
-
<h2>Faults</h2>
|
|
3360
|
-
<p class="lede">Inject a response, a delay, or a named preset. Rules apply to this namespace.</p>
|
|
3701
|
+
<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>
|
|
3361
3702
|
<form id="fault-form" class="card stack">
|
|
3362
|
-
<
|
|
3363
|
-
<
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
<
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
<
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
<
|
|
3373
|
-
|
|
3703
|
+
<div class="form-grid">
|
|
3704
|
+
<label class="field wide">Preset
|
|
3705
|
+
<select id="fault-preset"><option value="">Custom rule</option></select>
|
|
3706
|
+
</label>
|
|
3707
|
+
<label class="field">Status
|
|
3708
|
+
<input id="fault-status" inputmode="numeric" placeholder="503">
|
|
3709
|
+
</label>
|
|
3710
|
+
<label class="field wide">Path prefix
|
|
3711
|
+
<input id="fault-path" placeholder="/v1">
|
|
3712
|
+
</label>
|
|
3713
|
+
<label class="field">Method
|
|
3714
|
+
<input id="fault-method" placeholder="GET">
|
|
3715
|
+
</label>
|
|
3716
|
+
</div>
|
|
3374
3717
|
<div class="dialog-actions">
|
|
3375
3718
|
<button class="btn-primary" type="submit">Add fault</button>
|
|
3376
3719
|
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
@@ -3379,26 +3722,31 @@ var renderAdminDocument = (service) => {
|
|
|
3379
3722
|
<div id="fault-list"></div>
|
|
3380
3723
|
</section>
|
|
3381
3724
|
<section class="view" id="view-journal" hidden>
|
|
3382
|
-
<h2>Journal</h2>
|
|
3383
|
-
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
3384
|
-
<div class="row-actions">
|
|
3385
|
-
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
3386
|
-
</div>
|
|
3725
|
+
<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>
|
|
3387
3726
|
<div id="journal"></div>
|
|
3388
3727
|
</section>
|
|
3389
3728
|
<section class="view" id="view-routes" hidden>
|
|
3390
|
-
<h2>Routes</h2>
|
|
3391
|
-
<
|
|
3729
|
+
<div class="view-head"><div><h2>Routes</h2><p class="lede">Explore shared controls and service-specific admin endpoints.</p></div></div>
|
|
3730
|
+
<div class="route-tools">
|
|
3731
|
+
<label class="field">Filter routes
|
|
3732
|
+
<input id="route-search" type="search" placeholder="Method, path, or extension" autocomplete="off">
|
|
3733
|
+
</label>
|
|
3734
|
+
<span class="route-count" id="route-count"></span>
|
|
3735
|
+
</div>
|
|
3392
3736
|
<div id="route-list"></div>
|
|
3393
|
-
<form id="route-form" class="card stack">
|
|
3737
|
+
<form id="route-form" class="card stack" style="margin-top: 18px">
|
|
3394
3738
|
<strong id="route-title">Select a route</strong>
|
|
3395
3739
|
<label class="field">JSON body
|
|
3396
|
-
<
|
|
3740
|
+
<div class="json-editor" id="route-editor" hidden>
|
|
3741
|
+
<pre aria-hidden="true"><code id="route-highlight"></code></pre>
|
|
3742
|
+
<textarea id="route-body" placeholder="{ }" spellcheck="false" aria-describedby="route-json-status" disabled></textarea>
|
|
3743
|
+
</div>
|
|
3744
|
+
<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>
|
|
3397
3745
|
</label>
|
|
3398
3746
|
<div class="dialog-actions">
|
|
3399
|
-
<button class="btn-primary" type="submit">Send</button>
|
|
3747
|
+
<button class="btn-primary" id="route-send" type="submit" disabled>Send request</button>
|
|
3400
3748
|
</div>
|
|
3401
|
-
<pre id="route-result">Response will show here.</pre>
|
|
3749
|
+
<pre class="json-view" id="route-result"><code>Response will show here.</code></pre>
|
|
3402
3750
|
</form>
|
|
3403
3751
|
</section>
|
|
3404
3752
|
</main>
|
|
@@ -3406,12 +3754,16 @@ var renderAdminDocument = (service) => {
|
|
|
3406
3754
|
</div>
|
|
3407
3755
|
<dialog id="editor">
|
|
3408
3756
|
<form method="dialog" id="editor-form">
|
|
3409
|
-
<strong id="editor-title">Record</strong>
|
|
3757
|
+
<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>
|
|
3410
3758
|
<label class="field">Id
|
|
3411
3759
|
<input id="editor-id" required>
|
|
3412
3760
|
</label>
|
|
3413
3761
|
<label class="field">JSON value
|
|
3414
|
-
<
|
|
3762
|
+
<div class="json-editor" id="record-editor">
|
|
3763
|
+
<pre aria-hidden="true"><code id="editor-highlight"></code></pre>
|
|
3764
|
+
<textarea id="editor-value" spellcheck="false" aria-describedby="editor-json-status"></textarea>
|
|
3765
|
+
</div>
|
|
3766
|
+
<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>
|
|
3415
3767
|
</label>
|
|
3416
3768
|
<div class="dialog-actions">
|
|
3417
3769
|
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
@@ -3453,8 +3805,8 @@ var describeExtension = (extension) => {
|
|
|
3453
3805
|
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
3454
3806
|
};
|
|
3455
3807
|
};
|
|
3456
|
-
var adminUiRoutes = (service, ui) => {
|
|
3457
|
-
const shell = () => renderAdminDocument(service);
|
|
3808
|
+
var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
3809
|
+
const shell = () => renderAdminDocument(service, adminPrefix);
|
|
3458
3810
|
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
3459
3811
|
return {
|
|
3460
3812
|
"GET /ui": () => htmlResponse(document3()),
|
|
@@ -4145,7 +4497,7 @@ var stateAdminRoutes = (routes) => {
|
|
|
4145
4497
|
};
|
|
4146
4498
|
|
|
4147
4499
|
// ../core/dist/version.js
|
|
4148
|
-
var PACKAGE_VERSION = true ? "
|
|
4500
|
+
var PACKAGE_VERSION = true ? "2.0.0" : UNRELEASED_VERSION;
|
|
4149
4501
|
|
|
4150
4502
|
// ../core/dist/signing.js
|
|
4151
4503
|
var encoder = new TextEncoder();
|
|
@@ -4518,7 +4870,10 @@ var createWebhookHub = (options) => {
|
|
|
4518
4870
|
};
|
|
4519
4871
|
return hub;
|
|
4520
4872
|
};
|
|
4521
|
-
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4873
|
+
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4874
|
+
status,
|
|
4875
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4876
|
+
});
|
|
4522
4877
|
var adminError3 = (status, message) => json4(status, { error: { type: "mockingbird_admin", message } });
|
|
4523
4878
|
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4524
4879
|
var parseEndpoint = (value) => {
|
|
@@ -4626,7 +4981,6 @@ var AT_HEADER = "x-mockingbird-at";
|
|
|
4626
4981
|
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
4627
4982
|
var DEFAULT_NAMESPACE = "default";
|
|
4628
4983
|
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4629
|
-
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
4630
4984
|
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4631
4985
|
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
4632
4986
|
var effects = /* @__PURE__ */ new WeakMap();
|
|
@@ -4667,6 +5021,22 @@ var operationMatcher = (document3) => {
|
|
|
4667
5021
|
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
4668
5022
|
};
|
|
4669
5023
|
var createRuntime = (options) => {
|
|
5024
|
+
const adminPrefix = resolveAdminPrefix(options.adminPrefix);
|
|
5025
|
+
const internalPaths = Object.entries(options.document?.paths ?? {}).filter(([, item]) => item["x-mockingbird-internal"] === true).map(([path]) => path);
|
|
5026
|
+
for (const path of internalPaths) {
|
|
5027
|
+
if (!path.startsWith(`${ADMIN_PREFIX}/blobs/`))
|
|
5028
|
+
throw new Error(`internal transport ${path} must live under ${ADMIN_PREFIX}/blobs/`);
|
|
5029
|
+
}
|
|
5030
|
+
assertAdminPrefixAvailable(adminPrefix, Object.keys(options.document?.paths ?? {}).filter((path) => !internalPaths.includes(path)));
|
|
5031
|
+
const internalMatch = (path) => {
|
|
5032
|
+
for (const pattern of internalPaths) {
|
|
5033
|
+
const actual = path.split("/");
|
|
5034
|
+
const expected = pattern.split("/");
|
|
5035
|
+
if (actual.length === expected.length && expected.every((part, i) => part.startsWith("{") || part === actual[i]))
|
|
5036
|
+
return pattern;
|
|
5037
|
+
}
|
|
5038
|
+
return void 0;
|
|
5039
|
+
};
|
|
4670
5040
|
assertAdminUi(options.adminUi);
|
|
4671
5041
|
const sqlite = bootSqlite(options.sqlite);
|
|
4672
5042
|
const clock = options.clock ?? createClock();
|
|
@@ -4712,6 +5082,7 @@ var createRuntime = (options) => {
|
|
|
4712
5082
|
const created = options.create({
|
|
4713
5083
|
namespace: storageNamespace(key),
|
|
4714
5084
|
publicNamespace,
|
|
5085
|
+
adminPrefix,
|
|
4715
5086
|
sqlite,
|
|
4716
5087
|
clock: namespaceClock(publicNamespace),
|
|
4717
5088
|
rng: createdContextRng
|
|
@@ -4944,10 +5315,10 @@ var createRuntime = (options) => {
|
|
|
4944
5315
|
namespaceClock,
|
|
4945
5316
|
namespaceOf(request) {
|
|
4946
5317
|
const url = new URL(request.url);
|
|
4947
|
-
const prefixed =
|
|
5318
|
+
const prefixed = matchNamespacePath(url.pathname, adminPrefix);
|
|
4948
5319
|
const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
|
|
4949
5320
|
const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
|
|
4950
|
-
if (path
|
|
5321
|
+
if (isAdminPath(path, adminPrefix))
|
|
4951
5322
|
return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
|
|
4952
5323
|
if (selected !== void 0)
|
|
4953
5324
|
return selected;
|
|
@@ -5013,7 +5384,7 @@ var createRuntime = (options) => {
|
|
|
5013
5384
|
state: (namespace = DEFAULT_NAMESPACE) => inspectState(stateScope(namespace)),
|
|
5014
5385
|
fetch: async (incoming) => {
|
|
5015
5386
|
let request = incoming;
|
|
5016
|
-
const prefixed =
|
|
5387
|
+
const prefixed = matchNamespacePath(new URL(request.url).pathname, adminPrefix);
|
|
5017
5388
|
if (prefixed) {
|
|
5018
5389
|
const url2 = new URL(request.url);
|
|
5019
5390
|
url2.pathname = prefixed[2] ?? "/";
|
|
@@ -5029,7 +5400,15 @@ var createRuntime = (options) => {
|
|
|
5029
5400
|
signal: request.signal
|
|
5030
5401
|
});
|
|
5031
5402
|
}
|
|
5032
|
-
|
|
5403
|
+
const internalUrl = new URL(request.url);
|
|
5404
|
+
const internalPath = isAdminPath(internalUrl.pathname, adminPrefix) ? `${ADMIN_PREFIX}${internalUrl.pathname.slice(adminPrefix.length)}` : "";
|
|
5405
|
+
const internal = internalMatch(internalPath);
|
|
5406
|
+
const obsoleteInternalPath = adminPrefix !== ADMIN_PREFIX && internalMatch(internalUrl.pathname) && !internal;
|
|
5407
|
+
if (internal) {
|
|
5408
|
+
internalUrl.pathname = internalPath;
|
|
5409
|
+
request = new Request(internalUrl, request);
|
|
5410
|
+
}
|
|
5411
|
+
let namespace = internal ? internalUrl.searchParams.get("namespace") ?? control.namespaceOf(request) : control.namespaceOf(request);
|
|
5033
5412
|
if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
5034
5413
|
const credential = options.credential(request);
|
|
5035
5414
|
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
@@ -5049,7 +5428,9 @@ var createRuntime = (options) => {
|
|
|
5049
5428
|
return copy;
|
|
5050
5429
|
}
|
|
5051
5430
|
};
|
|
5052
|
-
|
|
5431
|
+
if (obsoleteInternalPath)
|
|
5432
|
+
return stamp(adminFail(404, "internal transport is only available under adminPrefix"));
|
|
5433
|
+
const handled = internal ? void 0 : await control.handle(request);
|
|
5053
5434
|
if (handled)
|
|
5054
5435
|
return stamp(handled);
|
|
5055
5436
|
const started = monotonicNow();
|
|
@@ -5090,7 +5471,7 @@ var createRuntime = (options) => {
|
|
|
5090
5471
|
type: "mockingbird_admin",
|
|
5091
5472
|
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
5092
5473
|
}
|
|
5093
|
-
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
5474
|
+
}), { status: 400, headers: { "content-type": "application/json; charset=utf-8" } }));
|
|
5094
5475
|
}
|
|
5095
5476
|
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
5096
5477
|
log(400);
|
|
@@ -5207,15 +5588,16 @@ var createRuntime = (options) => {
|
|
|
5207
5588
|
...options.admin?.(runtime) ?? {},
|
|
5208
5589
|
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
5209
5590
|
// maps `presets`, and a service route must not replace that list with a record.
|
|
5210
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
5591
|
+
...options.presets ? presetRoutes(options.presets, runtime, adminPrefix) : {},
|
|
5211
5592
|
...stateAdminRoutes({
|
|
5212
5593
|
open: stateScope,
|
|
5213
5594
|
afterWrite: (namespace) => {
|
|
5214
5595
|
checkpoint(namespace, "main");
|
|
5215
5596
|
}
|
|
5216
5597
|
}),
|
|
5217
|
-
...adminUiRoutes(options.name, options.adminUi)
|
|
5598
|
+
...adminUiRoutes(options.name, options.adminUi, adminPrefix)
|
|
5218
5599
|
},
|
|
5600
|
+
adminPrefix,
|
|
5219
5601
|
adminKey: options.adminKey
|
|
5220
5602
|
});
|
|
5221
5603
|
return defineMock(runtime);
|
|
@@ -5229,7 +5611,10 @@ var mutableResponse = (response) => {
|
|
|
5229
5611
|
return new Response(response.body, response);
|
|
5230
5612
|
}
|
|
5231
5613
|
};
|
|
5232
|
-
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5614
|
+
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5615
|
+
status,
|
|
5616
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
5617
|
+
});
|
|
5233
5618
|
var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
|
|
5234
5619
|
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5235
5620
|
var credentialRoutes = (registry) => ({
|
|
@@ -5281,7 +5666,7 @@ var credentialRoutes = (registry) => ({
|
|
|
5281
5666
|
return adminJson(200, { status: "ok" });
|
|
5282
5667
|
}
|
|
5283
5668
|
});
|
|
5284
|
-
var presetRoutes = (presets, runtime) => ({
|
|
5669
|
+
var presetRoutes = (presets, runtime, adminPrefix) => ({
|
|
5285
5670
|
"GET /faults/presets": () => {
|
|
5286
5671
|
const body = {
|
|
5287
5672
|
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
@@ -5291,7 +5676,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
5291
5676
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
5292
5677
|
const name = params.name;
|
|
5293
5678
|
if (!presets[name])
|
|
5294
|
-
return adminFail(404, `no fault preset ${name}; GET /
|
|
5679
|
+
return adminFail(404, `no fault preset ${name}; GET ${adminPrefix}/faults/presets`);
|
|
5295
5680
|
const overrides = isObject(body) ? body : {};
|
|
5296
5681
|
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
5297
5682
|
}
|
|
@@ -5637,15 +6022,17 @@ var createRuntime2 = (options = {}) => {
|
|
|
5637
6022
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
5638
6023
|
...options.clock ? { clock: options.clock } : {},
|
|
5639
6024
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
6025
|
+
...options.adminPrefix !== void 0 ? { adminPrefix: options.adminPrefix } : {},
|
|
5640
6026
|
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
5641
6027
|
...options.onLog ? { onLog: options.onLog } : {},
|
|
5642
6028
|
credential: tokenCredential,
|
|
5643
6029
|
presets: FULLSCRIPT_PRESETS,
|
|
5644
6030
|
webhooks: hub,
|
|
5645
|
-
create: ({ sqlite, namespace, publicNamespace, clock }) => new FullscriptAPI({
|
|
6031
|
+
create: ({ sqlite, namespace, publicNamespace, adminPrefix, clock }) => new FullscriptAPI({
|
|
5646
6032
|
sqlite,
|
|
5647
6033
|
namespace,
|
|
5648
6034
|
publicNamespace,
|
|
6035
|
+
adminPrefix,
|
|
5649
6036
|
now: clock.now,
|
|
5650
6037
|
...options.settings ? { settings: options.settings } : {},
|
|
5651
6038
|
...options.orders ? { orders: options.orders } : {},
|
|
@@ -5753,7 +6140,7 @@ var FullscriptAPI = class {
|
|
|
5753
6140
|
const namespace = options.namespace ?? FULLSCRIPT_NAMESPACE;
|
|
5754
6141
|
this.now = options.now ?? (() => Date.now());
|
|
5755
6142
|
this.onEvent = options.onEvent;
|
|
5756
|
-
this.prefix = options.publicNamespace && options.publicNamespace !== "default" ?
|
|
6143
|
+
this.prefix = options.publicNamespace && options.publicNamespace !== "default" ? `${options.adminPrefix ?? "/__admin"}/ns/${encodeURIComponent(options.publicNamespace)}` : "";
|
|
5757
6144
|
this.state = new FullscriptState(sqlite, namespace, options.settings ?? {});
|
|
5758
6145
|
const handlers = defineOperations({
|
|
5759
6146
|
OAuthToken: (context) => this.token(context),
|
|
@@ -6176,6 +6563,7 @@ var FullscriptAPI = class {
|
|
|
6176
6563
|
|
|
6177
6564
|
export {
|
|
6178
6565
|
createClock,
|
|
6566
|
+
resolveAdminPrefix,
|
|
6179
6567
|
parseDuration,
|
|
6180
6568
|
document2 as document,
|
|
6181
6569
|
operationIds,
|
|
@@ -6197,4 +6585,4 @@ export {
|
|
|
6197
6585
|
envelope,
|
|
6198
6586
|
FullscriptAPI
|
|
6199
6587
|
};
|
|
6200
|
-
//# sourceMappingURL=chunk-
|
|
6588
|
+
//# sourceMappingURL=chunk-R7LD4QOX.js.map
|