@crvouga/mockingbird-service-plane 1.7.1 → 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 +37 -0
- package/DISCOVERY.md +55 -0
- package/README.md +1 -1
- package/SUPPORT.md +21 -0
- package/dist/{chunk-NVLTLMDI.js → chunk-C6XLQ2US.js} +784 -258
- package/dist/chunk-C6XLQ2US.js.map +7 -0
- package/dist/chunk-IKIADEZM.js +846 -0
- package/dist/chunk-IKIADEZM.js.map +7 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +34 -4
- package/dist/index.js +1 -1
- package/dist/server.d.ts +64 -4
- package/dist/server.js +2 -2
- package/openapi.yaml +1014 -0
- package/package.json +29 -4
- package/dist/chunk-DC2XELGV.js +0 -359
- package/dist/chunk-DC2XELGV.js.map +0 -7
- package/dist/chunk-NVLTLMDI.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()
|
|
@@ -183,29 +215,30 @@ var createControlPlane = (context) => {
|
|
|
183
215
|
return json(200, { status: "ok", reset: target === "*" ? context.namespaces() : [target] });
|
|
184
216
|
},
|
|
185
217
|
"GET /namespaces": () => json(200, { default: context.defaultNamespace, namespaces: context.namespaces() }),
|
|
186
|
-
"GET /clock": () => json(200, context.clock.state()),
|
|
187
|
-
"POST /clock": ({ body }) => {
|
|
218
|
+
"GET /clock": ({ namespace }) => json(200, (context.namespaceClock?.(namespace) ?? context.clock).state()),
|
|
219
|
+
"POST /clock": ({ body, namespace }) => {
|
|
220
|
+
const clock = context.namespaceClock?.(namespace) ?? context.clock;
|
|
188
221
|
if (!isRecord(body))
|
|
189
222
|
return adminError(400, "expected a JSON object");
|
|
190
223
|
if (body.reset === true)
|
|
191
|
-
|
|
224
|
+
clock.reset();
|
|
192
225
|
if (body.freeze === true)
|
|
193
|
-
|
|
226
|
+
clock.freeze();
|
|
194
227
|
if (body.set !== void 0) {
|
|
195
228
|
const instant = parseInstant(body.set);
|
|
196
229
|
if (instant === void 0)
|
|
197
230
|
return adminError(400, "set: expected epoch ms or ISO-8601");
|
|
198
|
-
|
|
231
|
+
clock.set(instant);
|
|
199
232
|
}
|
|
200
233
|
if (body.advance !== void 0) {
|
|
201
234
|
const delta = parseDuration(body.advance);
|
|
202
235
|
if (delta === void 0)
|
|
203
236
|
return adminError(400, 'advance: expected ms or "15m"-style');
|
|
204
|
-
|
|
237
|
+
clock.advance(delta);
|
|
205
238
|
}
|
|
206
239
|
if (body.freeze === false)
|
|
207
|
-
|
|
208
|
-
return json(200,
|
|
240
|
+
clock.unfreeze();
|
|
241
|
+
return json(200, clock.state());
|
|
209
242
|
},
|
|
210
243
|
"GET /faults": () => json(200, { faults: context.faults.list() }),
|
|
211
244
|
"POST /faults": ({ body, namespace }) => {
|
|
@@ -338,7 +371,14 @@ var createControlPlane = (context) => {
|
|
|
338
371
|
return json(200, { status: "ok" });
|
|
339
372
|
}
|
|
340
373
|
};
|
|
341
|
-
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]) => {
|
|
342
382
|
const space = key.indexOf(" ");
|
|
343
383
|
return { method: key.slice(0, space), pattern: key.slice(space + 1), handler };
|
|
344
384
|
});
|
|
@@ -346,21 +386,13 @@ var createControlPlane = (context) => {
|
|
|
346
386
|
namespaceOf: headerNamespace,
|
|
347
387
|
async handle(request) {
|
|
348
388
|
const url = new URL(request.url);
|
|
349
|
-
if (url.pathname ===
|
|
350
|
-
return
|
|
351
|
-
status: "ok",
|
|
352
|
-
service: context.name,
|
|
353
|
-
uptimeMs: context.wallNow() - context.startedAt,
|
|
354
|
-
clock: context.clock.state(),
|
|
355
|
-
namespaces: context.namespaces().length,
|
|
356
|
-
adminUi: `${ADMIN_PREFIX}/ui`,
|
|
357
|
-
...context.describe()
|
|
358
|
-
});
|
|
389
|
+
if (url.pathname === `${adminPrefix}/health` && request.method === "GET") {
|
|
390
|
+
return health();
|
|
359
391
|
}
|
|
360
|
-
if (url.pathname
|
|
392
|
+
if (!isAdminPath(url.pathname, adminPrefix)) {
|
|
361
393
|
return void 0;
|
|
362
394
|
}
|
|
363
|
-
const path = url.pathname.slice(
|
|
395
|
+
const path = url.pathname.slice(adminPrefix.length) || "/";
|
|
364
396
|
const shell = request.method === "GET" && (path === "/ui" || path === "/ui/");
|
|
365
397
|
if (!shell && context.adminKey !== void 0 && request.headers.get(ADMIN_KEY_HEADER) !== context.adminKey) {
|
|
366
398
|
return adminError(401, `missing or wrong ${ADMIN_KEY_HEADER}`);
|
|
@@ -368,7 +400,12 @@ var createControlPlane = (context) => {
|
|
|
368
400
|
for (const route of routes) {
|
|
369
401
|
if (route.method !== request.method)
|
|
370
402
|
continue;
|
|
371
|
-
|
|
403
|
+
let params;
|
|
404
|
+
try {
|
|
405
|
+
params = matchRoute(route.pattern, path);
|
|
406
|
+
} catch {
|
|
407
|
+
return adminError(400, "invalid path encoding");
|
|
408
|
+
}
|
|
372
409
|
if (!params)
|
|
373
410
|
continue;
|
|
374
411
|
let body;
|
|
@@ -377,15 +414,18 @@ var createControlPlane = (context) => {
|
|
|
377
414
|
} catch {
|
|
378
415
|
return adminError(400, "request body is not valid JSON");
|
|
379
416
|
}
|
|
417
|
+
const namespace = adminNamespace(request, url);
|
|
418
|
+
if (!/^[A-Za-z0-9_.-]{1,64}$/.test(namespace))
|
|
419
|
+
return adminError(400, "invalid namespace");
|
|
380
420
|
return route.handler({
|
|
381
421
|
request,
|
|
382
422
|
url,
|
|
383
423
|
params,
|
|
384
|
-
namespace
|
|
424
|
+
namespace,
|
|
385
425
|
body
|
|
386
426
|
});
|
|
387
427
|
}
|
|
388
|
-
return adminError(404, `no admin route ${request.method} ${path}; GET ${
|
|
428
|
+
return adminError(404, `no admin route ${request.method} ${path}; GET ${adminPrefix} lists them`);
|
|
389
429
|
}
|
|
390
430
|
};
|
|
391
431
|
};
|
|
@@ -467,7 +507,33 @@ var faultResponse = (rule) => {
|
|
|
467
507
|
};
|
|
468
508
|
var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) => {
|
|
469
509
|
const entries = [];
|
|
510
|
+
let isolated = false;
|
|
511
|
+
const scopedRngs = /* @__PURE__ */ new Map();
|
|
512
|
+
const random = (namespace) => {
|
|
513
|
+
if (!isolated || namespace === "default")
|
|
514
|
+
return rng;
|
|
515
|
+
let scoped = scopedRngs.get(namespace);
|
|
516
|
+
if (!scoped) {
|
|
517
|
+
scoped = createRng(rng.seed);
|
|
518
|
+
scopedRngs.set(namespace, scoped);
|
|
519
|
+
}
|
|
520
|
+
return scoped;
|
|
521
|
+
};
|
|
470
522
|
return {
|
|
523
|
+
isolateNamespaces() {
|
|
524
|
+
isolated = true;
|
|
525
|
+
},
|
|
526
|
+
snapshot(namespace) {
|
|
527
|
+
return {
|
|
528
|
+
rules: structuredClone(entries.map((entry, position) => ({
|
|
529
|
+
...entry.rule,
|
|
530
|
+
remaining: entry.remaining,
|
|
531
|
+
hits: entry.hits,
|
|
532
|
+
position
|
|
533
|
+
})).filter((entry) => entry.namespace === namespace)),
|
|
534
|
+
rngState: random(namespace).state()
|
|
535
|
+
};
|
|
536
|
+
},
|
|
471
537
|
add(rule) {
|
|
472
538
|
const existing = entries.findIndex((e) => e.rule.id === rule.id);
|
|
473
539
|
const entry = { rule, remaining: rule.count ?? null, hits: 0 };
|
|
@@ -487,6 +553,16 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
|
|
|
487
553
|
},
|
|
488
554
|
clear() {
|
|
489
555
|
entries.length = 0;
|
|
556
|
+
rng.reset();
|
|
557
|
+
scopedRngs.clear();
|
|
558
|
+
},
|
|
559
|
+
restore(namespace, state) {
|
|
560
|
+
for (let i = entries.length - 1; i >= 0; i--)
|
|
561
|
+
if (entries[i]?.rule.namespace === namespace)
|
|
562
|
+
entries.splice(i, 1);
|
|
563
|
+
for (const { remaining, hits, position, ...rule } of state.rules)
|
|
564
|
+
entries.splice(Math.min(position, entries.length), 0, { rule, remaining, hits });
|
|
565
|
+
random(namespace).setState(state.rngState);
|
|
490
566
|
},
|
|
491
567
|
async take(candidate) {
|
|
492
568
|
const hits = [];
|
|
@@ -496,7 +572,7 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
|
|
|
496
572
|
if (!matches(entry.rule, candidate))
|
|
497
573
|
continue;
|
|
498
574
|
const rate = entry.rule.rate ?? 1;
|
|
499
|
-
if (
|
|
575
|
+
if (random(candidate.namespace).next() >= rate)
|
|
500
576
|
continue;
|
|
501
577
|
entry.hits++;
|
|
502
578
|
if (entry.remaining !== null)
|
|
@@ -1105,7 +1181,8 @@ var annotateResponse = (response, extra) => {
|
|
|
1105
1181
|
var responseNotes = (response) => notes.get(response);
|
|
1106
1182
|
|
|
1107
1183
|
// ../core/dist/metrics.js
|
|
1108
|
-
var createMetrics = () => {
|
|
1184
|
+
var createMetrics = (partition = true) => {
|
|
1185
|
+
const namespaces = /* @__PURE__ */ new Map();
|
|
1109
1186
|
let requests = 0;
|
|
1110
1187
|
let faults = 0;
|
|
1111
1188
|
let totalDurationMs = 0;
|
|
@@ -1113,6 +1190,14 @@ var createMetrics = () => {
|
|
|
1113
1190
|
const unmatched = /* @__PURE__ */ new Map();
|
|
1114
1191
|
return {
|
|
1115
1192
|
record(entry) {
|
|
1193
|
+
if (partition) {
|
|
1194
|
+
let scoped = namespaces.get(entry.namespace);
|
|
1195
|
+
if (!scoped) {
|
|
1196
|
+
scoped = createMetrics(false);
|
|
1197
|
+
namespaces.set(entry.namespace, scoped);
|
|
1198
|
+
}
|
|
1199
|
+
scoped.record(entry);
|
|
1200
|
+
}
|
|
1116
1201
|
requests++;
|
|
1117
1202
|
totalDurationMs += entry.durationMs;
|
|
1118
1203
|
if (entry.faultId !== void 0)
|
|
@@ -1124,7 +1209,7 @@ var createMetrics = () => {
|
|
|
1124
1209
|
unmatched.set(route, (unmatched.get(route) ?? 0) + 1);
|
|
1125
1210
|
}
|
|
1126
1211
|
},
|
|
1127
|
-
report: () => ({
|
|
1212
|
+
report: (namespace) => namespace !== void 0 ? (namespaces.get(namespace) ?? createMetrics(false)).report() : {
|
|
1128
1213
|
requests,
|
|
1129
1214
|
byOperation: Object.fromEntries([...byOperation].sort(([a], [b]) => a.localeCompare(b))),
|
|
1130
1215
|
unmatched: [...unmatched].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([route, count]) => {
|
|
@@ -1133,8 +1218,13 @@ var createMetrics = () => {
|
|
|
1133
1218
|
}),
|
|
1134
1219
|
faults,
|
|
1135
1220
|
totalDurationMs
|
|
1136
|
-
}
|
|
1137
|
-
reset() {
|
|
1221
|
+
},
|
|
1222
|
+
reset(namespace) {
|
|
1223
|
+
if (namespace !== void 0) {
|
|
1224
|
+
namespaces.delete(namespace);
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
namespaces.clear();
|
|
1138
1228
|
requests = 0;
|
|
1139
1229
|
faults = 0;
|
|
1140
1230
|
totalDurationMs = 0;
|
|
@@ -1747,6 +1837,34 @@ dialog:not([open]) {
|
|
|
1747
1837
|
function isRecord2(value) {
|
|
1748
1838
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1749
1839
|
}
|
|
1840
|
+
function highlightJsonText(text) {
|
|
1841
|
+
const escapeToken = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
1842
|
+
switch (char) {
|
|
1843
|
+
case "&":
|
|
1844
|
+
return "&";
|
|
1845
|
+
case "<":
|
|
1846
|
+
return "<";
|
|
1847
|
+
case ">":
|
|
1848
|
+
return ">";
|
|
1849
|
+
case '"':
|
|
1850
|
+
return """;
|
|
1851
|
+
default:
|
|
1852
|
+
return "'";
|
|
1853
|
+
}
|
|
1854
|
+
});
|
|
1855
|
+
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;
|
|
1856
|
+
let cursor = 0;
|
|
1857
|
+
let html = "";
|
|
1858
|
+
for (const match of text.matchAll(token)) {
|
|
1859
|
+
const index = match.index ?? 0;
|
|
1860
|
+
const value = match[0];
|
|
1861
|
+
html += escapeToken(text.slice(cursor, index));
|
|
1862
|
+
const kind = value.startsWith('"') ? value.trimEnd().endsWith(":") ? "key" : "string" : value === "true" || value === "false" ? "boolean" : value === "null" ? "null" : "number";
|
|
1863
|
+
html += `<span class="json-${kind}">${escapeToken(value)}</span>`;
|
|
1864
|
+
cursor = index + value.length;
|
|
1865
|
+
}
|
|
1866
|
+
return html + escapeToken(text.slice(cursor));
|
|
1867
|
+
}
|
|
1750
1868
|
function mountAdminBrand(doc, host, service, catalog) {
|
|
1751
1869
|
const clip = (value, max) => {
|
|
1752
1870
|
if (typeof value !== "string")
|
|
@@ -2108,14 +2226,16 @@ function bootAdmin(config) {
|
|
|
2108
2226
|
const state = {
|
|
2109
2227
|
namespace: "default",
|
|
2110
2228
|
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
2111
|
-
view: "overview",
|
|
2229
|
+
view: ["overview", "state", "clock", "faults", "journal", "routes"].includes(location.hash.slice(1)) ? location.hash.slice(1) : "overview",
|
|
2112
2230
|
collection: "",
|
|
2113
2231
|
shape: null,
|
|
2114
2232
|
route: null,
|
|
2233
|
+
routes: [],
|
|
2115
2234
|
editing: null
|
|
2116
2235
|
};
|
|
2117
2236
|
const standard = new Set(config.standardRoutes);
|
|
2118
2237
|
const banner = el("banner", HTMLElement);
|
|
2238
|
+
const bannerMessage = el("banner-message", HTMLElement);
|
|
2119
2239
|
const keyInput = el("admin-key", HTMLInputElement);
|
|
2120
2240
|
keyInput.value = state.key;
|
|
2121
2241
|
const params = new URLSearchParams(location.search);
|
|
@@ -2126,18 +2246,21 @@ function bootAdmin(config) {
|
|
|
2126
2246
|
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
2127
2247
|
params.delete("key");
|
|
2128
2248
|
const next = params.toString();
|
|
2129
|
-
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
2249
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : "") + location.hash);
|
|
2130
2250
|
}
|
|
2131
2251
|
const queryNamespace = params.get("namespace");
|
|
2132
2252
|
if (queryNamespace)
|
|
2133
2253
|
state.namespace = queryNamespace;
|
|
2134
2254
|
const showError = (error) => {
|
|
2135
|
-
|
|
2255
|
+
bannerMessage.textContent = error instanceof Error ? error.message : String(error);
|
|
2136
2256
|
banner.classList.add("show");
|
|
2257
|
+
banner.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
2137
2258
|
};
|
|
2138
2259
|
const clearError = () => {
|
|
2139
2260
|
banner.classList.remove("show");
|
|
2261
|
+
bannerMessage.textContent = "";
|
|
2140
2262
|
};
|
|
2263
|
+
el("banner-dismiss", HTMLButtonElement).addEventListener("click", clearError);
|
|
2141
2264
|
const api = async (method, path, body) => {
|
|
2142
2265
|
const headers = { accept: "application/json" };
|
|
2143
2266
|
if (state.key !== "")
|
|
@@ -2181,16 +2304,21 @@ function bootAdmin(config) {
|
|
|
2181
2304
|
namespace: () => state.namespace
|
|
2182
2305
|
};
|
|
2183
2306
|
const isPanelScript = (value) => typeof value === "function";
|
|
2307
|
+
const themeButton = el("theme", HTMLButtonElement);
|
|
2184
2308
|
const setTheme = (theme) => {
|
|
2185
2309
|
if (theme)
|
|
2186
2310
|
document.documentElement.dataset.theme = theme;
|
|
2187
2311
|
else
|
|
2188
2312
|
delete document.documentElement.dataset.theme;
|
|
2313
|
+
const dark = theme ? theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2314
|
+
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>';
|
|
2315
|
+
const label = dark ? "Use light theme" : "Use dark theme";
|
|
2316
|
+
themeButton.setAttribute("aria-label", label);
|
|
2317
|
+
themeButton.title = label;
|
|
2189
2318
|
};
|
|
2190
2319
|
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
2320
|
+
setTheme(storedTheme === "dark" || storedTheme === "light" ? storedTheme : null);
|
|
2321
|
+
themeButton.addEventListener("click", () => {
|
|
2194
2322
|
const current = document.documentElement.dataset.theme;
|
|
2195
2323
|
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2196
2324
|
const next = dark ? "light" : "dark";
|
|
@@ -2209,6 +2337,7 @@ function bootAdmin(config) {
|
|
|
2209
2337
|
continue;
|
|
2210
2338
|
section.hidden = section.id !== `view-${view}`;
|
|
2211
2339
|
}
|
|
2340
|
+
history.replaceState(null, "", `${location.pathname}${location.search}#${encodeURIComponent(view)}`);
|
|
2212
2341
|
};
|
|
2213
2342
|
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
2214
2343
|
const target = event.target;
|
|
@@ -2223,7 +2352,7 @@ function bootAdmin(config) {
|
|
|
2223
2352
|
show(view);
|
|
2224
2353
|
refresh().catch(showError);
|
|
2225
2354
|
});
|
|
2226
|
-
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>`;
|
|
2355
|
+
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>`;
|
|
2227
2356
|
const table = (columns, rows) => {
|
|
2228
2357
|
if (rows.length === 0)
|
|
2229
2358
|
return '<p class="empty">Nothing here yet.</p>';
|
|
@@ -2233,16 +2362,22 @@ function bootAdmin(config) {
|
|
|
2233
2362
|
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
2234
2363
|
return `<tr${attrs}>${cells}</tr>`;
|
|
2235
2364
|
}).join("");
|
|
2236
|
-
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
2365
|
+
return `<div class="table-shell"><table><thead>${head}</thead><tbody>${body}</tbody></table></div>`;
|
|
2237
2366
|
};
|
|
2238
2367
|
const preview = (value) => {
|
|
2239
2368
|
const text = JSON.stringify(value) ?? "null";
|
|
2240
|
-
|
|
2369
|
+
const clipped = text.length > 180 ? `${text.slice(0, 180)}\u2026` : text;
|
|
2370
|
+
return `<code class="json-inline">${highlightJsonText(clipped)}</code>`;
|
|
2241
2371
|
};
|
|
2242
2372
|
const when = (epoch) => {
|
|
2243
2373
|
const date = new Date(epoch);
|
|
2244
2374
|
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
2245
2375
|
};
|
|
2376
|
+
const journalWhen = (value) => {
|
|
2377
|
+
const numeric = value.trim() === "" ? Number.NaN : Number(value);
|
|
2378
|
+
const epoch = Number.isFinite(numeric) ? numeric : Date.parse(value);
|
|
2379
|
+
return Number.isFinite(epoch) ? when(epoch) : value;
|
|
2380
|
+
};
|
|
2246
2381
|
const clockCard = (epoch) => {
|
|
2247
2382
|
const date = new Date(epoch);
|
|
2248
2383
|
if (Number.isNaN(date.getTime()))
|
|
@@ -2362,7 +2497,7 @@ function bootAdmin(config) {
|
|
|
2362
2497
|
};
|
|
2363
2498
|
const renderOverview = async () => {
|
|
2364
2499
|
const [health, shape, clock, journal] = await Promise.all([
|
|
2365
|
-
originGet(
|
|
2500
|
+
originGet(`${config.adminPrefix}/health`),
|
|
2366
2501
|
api("GET", "/state"),
|
|
2367
2502
|
api("GET", "/clock"),
|
|
2368
2503
|
api("GET", "/requests?limit=8")
|
|
@@ -2379,7 +2514,7 @@ function bootAdmin(config) {
|
|
|
2379
2514
|
},
|
|
2380
2515
|
{
|
|
2381
2516
|
k: "Status",
|
|
2382
|
-
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
2517
|
+
v: `<span class="badge ok">${esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")}</span>`
|
|
2383
2518
|
},
|
|
2384
2519
|
{ k: "Namespace", v: esc(state.namespace) },
|
|
2385
2520
|
{ k: "Collections", v: String(decoded.collections.length) },
|
|
@@ -2466,26 +2601,46 @@ function bootAdmin(config) {
|
|
|
2466
2601
|
const renderJournal = async () => {
|
|
2467
2602
|
const journal = await api("GET", "/requests?limit=100");
|
|
2468
2603
|
el("journal", HTMLElement).innerHTML = table([
|
|
2469
|
-
{ label: "When", cell: (row) => esc(row.at) },
|
|
2470
|
-
{
|
|
2604
|
+
{ label: "When", cell: (row) => esc(journalWhen(row.at)) },
|
|
2605
|
+
{
|
|
2606
|
+
label: "Method",
|
|
2607
|
+
cell: (row) => `<span class="method">${esc(row.method)}</span>`
|
|
2608
|
+
},
|
|
2471
2609
|
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2472
|
-
{
|
|
2610
|
+
{
|
|
2611
|
+
label: "Status",
|
|
2612
|
+
cell: (row) => `<span class="badge ${row.status.startsWith("2") ? "ok" : row.status.startsWith("4") || row.status.startsWith("5") ? "warn" : ""}">${esc(row.status)}</span>`
|
|
2613
|
+
},
|
|
2473
2614
|
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
2474
2615
|
], decodeJournal(journal));
|
|
2475
2616
|
};
|
|
2476
|
-
const
|
|
2477
|
-
const
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2617
|
+
const paintRoutes = () => {
|
|
2618
|
+
const query = el("route-search", HTMLInputElement).value.trim().toLocaleLowerCase();
|
|
2619
|
+
const routes = state.routes.filter((route) => route.toLocaleLowerCase().includes(query));
|
|
2620
|
+
el("route-count", HTMLElement).textContent = `${routes.length} of ${state.routes.length}`;
|
|
2621
|
+
if (state.routes.length === 0) {
|
|
2622
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No admin routes are available.</p>';
|
|
2623
|
+
return;
|
|
2624
|
+
}
|
|
2625
|
+
if (routes.length === 0) {
|
|
2626
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No routes match this filter.</p>';
|
|
2627
|
+
return;
|
|
2628
|
+
}
|
|
2481
2629
|
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2482
2630
|
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2483
2631
|
const parts = route.split(" ");
|
|
2484
2632
|
const method = parts[0] ?? "";
|
|
2485
2633
|
const path = parts.slice(1).join(" ");
|
|
2486
|
-
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>`;
|
|
2634
|
+
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>`;
|
|
2487
2635
|
}).join("");
|
|
2488
2636
|
};
|
|
2637
|
+
const renderRoutes = async () => {
|
|
2638
|
+
const data = await api("GET", "/");
|
|
2639
|
+
if (!isRecord2(data))
|
|
2640
|
+
throw new Error("Route list was not an object");
|
|
2641
|
+
state.routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
2642
|
+
paintRoutes();
|
|
2643
|
+
};
|
|
2489
2644
|
const renderers = {
|
|
2490
2645
|
overview: renderOverview,
|
|
2491
2646
|
state: renderState,
|
|
@@ -2494,19 +2649,50 @@ function bootAdmin(config) {
|
|
|
2494
2649
|
journal: renderJournal,
|
|
2495
2650
|
routes: renderRoutes
|
|
2496
2651
|
};
|
|
2652
|
+
let refreshToken = 0;
|
|
2497
2653
|
async function refresh() {
|
|
2654
|
+
const token = ++refreshToken;
|
|
2498
2655
|
clearError();
|
|
2656
|
+
const main = el("views", HTMLElement);
|
|
2657
|
+
const sync = el("sync-status", HTMLElement);
|
|
2658
|
+
main.setAttribute("aria-busy", "true");
|
|
2659
|
+
sync.classList.add("busy");
|
|
2660
|
+
sync.textContent = "Updating";
|
|
2499
2661
|
const render = renderers[state.view];
|
|
2500
|
-
|
|
2501
|
-
|
|
2662
|
+
try {
|
|
2663
|
+
if (render)
|
|
2664
|
+
await render();
|
|
2665
|
+
if (token === refreshToken)
|
|
2666
|
+
sync.textContent = "Up to date";
|
|
2667
|
+
} finally {
|
|
2668
|
+
if (token === refreshToken) {
|
|
2669
|
+
main.removeAttribute("aria-busy");
|
|
2670
|
+
sync.classList.remove("busy");
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2502
2673
|
}
|
|
2503
2674
|
const delay = (ms) => new Promise((resolve) => {
|
|
2504
2675
|
window.setTimeout(resolve, ms);
|
|
2505
2676
|
});
|
|
2506
|
-
const confirmAction = async (button, pending, done, work) => {
|
|
2677
|
+
const confirmAction = async (button, pending, done, work, confirmLabel) => {
|
|
2507
2678
|
if (button.dataset.pending === "1")
|
|
2508
2679
|
return;
|
|
2509
2680
|
const label = button.textContent ?? "";
|
|
2681
|
+
if (confirmLabel !== void 0 && button.dataset.confirming !== "1") {
|
|
2682
|
+
button.dataset.confirming = "1";
|
|
2683
|
+
button.classList.add("confirming");
|
|
2684
|
+
button.textContent = confirmLabel;
|
|
2685
|
+
window.setTimeout(() => {
|
|
2686
|
+
if (button.dataset.confirming !== "1")
|
|
2687
|
+
return;
|
|
2688
|
+
button.textContent = label;
|
|
2689
|
+
button.classList.remove("confirming");
|
|
2690
|
+
delete button.dataset.confirming;
|
|
2691
|
+
}, 3e3);
|
|
2692
|
+
return;
|
|
2693
|
+
}
|
|
2694
|
+
button.classList.remove("confirming");
|
|
2695
|
+
delete button.dataset.confirming;
|
|
2510
2696
|
const width = button.offsetWidth;
|
|
2511
2697
|
button.dataset.pending = "1";
|
|
2512
2698
|
button.classList.add("pressed");
|
|
@@ -2527,6 +2713,65 @@ function bootAdmin(config) {
|
|
|
2527
2713
|
delete button.dataset.pending;
|
|
2528
2714
|
}
|
|
2529
2715
|
};
|
|
2716
|
+
const wireJsonEditor = (textareaId, highlightId, statusId, shellId, allowEmpty) => {
|
|
2717
|
+
const textarea = el(textareaId, HTMLTextAreaElement);
|
|
2718
|
+
const highlight = el(highlightId, HTMLElement);
|
|
2719
|
+
const status = el(statusId, HTMLElement);
|
|
2720
|
+
const shell = el(shellId, HTMLElement);
|
|
2721
|
+
const sync = () => {
|
|
2722
|
+
const text = textarea.value;
|
|
2723
|
+
highlight.innerHTML = `${highlightJsonText(text)}
|
|
2724
|
+
`;
|
|
2725
|
+
let valid = true;
|
|
2726
|
+
let message = "Valid JSON";
|
|
2727
|
+
if (allowEmpty && text.trim() === "")
|
|
2728
|
+
message = "No body";
|
|
2729
|
+
else {
|
|
2730
|
+
try {
|
|
2731
|
+
readJson2(text);
|
|
2732
|
+
} catch {
|
|
2733
|
+
valid = false;
|
|
2734
|
+
message = "Invalid JSON";
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
shell.classList.toggle("invalid", !valid);
|
|
2738
|
+
status.classList.toggle("invalid", !valid);
|
|
2739
|
+
status.textContent = message;
|
|
2740
|
+
textarea.setAttribute("aria-invalid", valid ? "false" : "true");
|
|
2741
|
+
};
|
|
2742
|
+
const syncScroll = () => {
|
|
2743
|
+
const preview2 = highlight.parentElement;
|
|
2744
|
+
if (!(preview2 instanceof HTMLElement))
|
|
2745
|
+
return;
|
|
2746
|
+
preview2.scrollTop = textarea.scrollTop;
|
|
2747
|
+
preview2.scrollLeft = textarea.scrollLeft;
|
|
2748
|
+
};
|
|
2749
|
+
textarea.addEventListener("input", sync);
|
|
2750
|
+
textarea.addEventListener("scroll", syncScroll);
|
|
2751
|
+
sync();
|
|
2752
|
+
return sync;
|
|
2753
|
+
};
|
|
2754
|
+
const syncRouteEditor = wireJsonEditor("route-body", "route-highlight", "route-json-status", "route-editor", true);
|
|
2755
|
+
const syncRecordEditor = wireJsonEditor("editor-value", "editor-highlight", "editor-json-status", "record-editor", false);
|
|
2756
|
+
for (const button of document.querySelectorAll("[data-format]")) {
|
|
2757
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2758
|
+
continue;
|
|
2759
|
+
button.addEventListener("click", () => {
|
|
2760
|
+
const target = button.dataset.format;
|
|
2761
|
+
if (target === void 0)
|
|
2762
|
+
return;
|
|
2763
|
+
const textarea = document.getElementById(target);
|
|
2764
|
+
if (!(textarea instanceof HTMLTextAreaElement) || textarea.value.trim() === "")
|
|
2765
|
+
return;
|
|
2766
|
+
try {
|
|
2767
|
+
textarea.value = JSON.stringify(readJson2(textarea.value), null, 2) ?? "";
|
|
2768
|
+
textarea.dispatchEvent(new Event("input"));
|
|
2769
|
+
textarea.focus();
|
|
2770
|
+
} catch {
|
|
2771
|
+
showError(new Error("Fix the JSON before formatting it"));
|
|
2772
|
+
}
|
|
2773
|
+
});
|
|
2774
|
+
}
|
|
2530
2775
|
document.addEventListener("click", (event) => {
|
|
2531
2776
|
const target = event.target;
|
|
2532
2777
|
if (!(target instanceof Element))
|
|
@@ -2599,6 +2844,7 @@ function bootAdmin(config) {
|
|
|
2599
2844
|
} else {
|
|
2600
2845
|
editorValue.value = "{\n \n}";
|
|
2601
2846
|
}
|
|
2847
|
+
syncRecordEditor();
|
|
2602
2848
|
editor.showModal();
|
|
2603
2849
|
}
|
|
2604
2850
|
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -2628,10 +2874,10 @@ function bootAdmin(config) {
|
|
|
2628
2874
|
const id = state.editing;
|
|
2629
2875
|
if (!id)
|
|
2630
2876
|
return;
|
|
2631
|
-
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2877
|
+
confirmAction(editorDelete, "Deleting...", "Deleted", () => api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2632
2878
|
editor.close();
|
|
2633
2879
|
return renderState();
|
|
2634
|
-
}).catch(showError);
|
|
2880
|
+
}), "Confirm delete").catch(showError);
|
|
2635
2881
|
});
|
|
2636
2882
|
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2637
2883
|
if (!(button instanceof HTMLButtonElement))
|
|
@@ -2677,10 +2923,10 @@ function bootAdmin(config) {
|
|
|
2677
2923
|
}).then(() => renderFaults()).catch(showError);
|
|
2678
2924
|
});
|
|
2679
2925
|
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2680
|
-
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
|
|
2926
|
+
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults()), "Confirm clear").catch(showError);
|
|
2681
2927
|
});
|
|
2682
2928
|
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2683
|
-
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
|
|
2929
|
+
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal()), "Confirm clear").catch(showError);
|
|
2684
2930
|
});
|
|
2685
2931
|
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2686
2932
|
const target = event.target;
|
|
@@ -2694,9 +2940,15 @@ function bootAdmin(config) {
|
|
|
2694
2940
|
return;
|
|
2695
2941
|
state.route = route;
|
|
2696
2942
|
el("route-title", HTMLElement).textContent = route;
|
|
2943
|
+
el("route-send", HTMLButtonElement).disabled = false;
|
|
2697
2944
|
const method = route.split(" ")[0] ?? "";
|
|
2698
|
-
el("route-body", HTMLTextAreaElement)
|
|
2945
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
2946
|
+
routeBody.disabled = method === "GET" || method === "DELETE";
|
|
2947
|
+
el("route-editor", HTMLElement).hidden = routeBody.disabled;
|
|
2948
|
+
el("route-json-status", HTMLElement).parentElement?.toggleAttribute("hidden", routeBody.disabled);
|
|
2949
|
+
syncRouteEditor();
|
|
2699
2950
|
});
|
|
2951
|
+
el("route-search", HTMLInputElement).addEventListener("input", paintRoutes);
|
|
2700
2952
|
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2701
2953
|
event.preventDefault();
|
|
2702
2954
|
const send = async () => {
|
|
@@ -2718,7 +2970,8 @@ function bootAdmin(config) {
|
|
|
2718
2970
|
}
|
|
2719
2971
|
}
|
|
2720
2972
|
const data = await api(method, concrete, body);
|
|
2721
|
-
|
|
2973
|
+
const text = JSON.stringify(data, null, 2) ?? "null";
|
|
2974
|
+
el("route-result", HTMLElement).innerHTML = `<code>${highlightJsonText(text)}</code>`;
|
|
2722
2975
|
};
|
|
2723
2976
|
send().catch(showError);
|
|
2724
2977
|
});
|
|
@@ -2836,11 +3089,13 @@ function bootAdmin(config) {
|
|
|
2836
3089
|
views.append(section);
|
|
2837
3090
|
mountSqlExplorer(body, panelApi);
|
|
2838
3091
|
}
|
|
3092
|
+
show(document.getElementById(`view-${state.view}`) === null ? "overview" : state.view);
|
|
2839
3093
|
return refresh();
|
|
2840
3094
|
}).catch(showError);
|
|
2841
3095
|
}
|
|
2842
3096
|
var clientPrelude = [
|
|
2843
3097
|
isRecord2,
|
|
3098
|
+
highlightJsonText,
|
|
2844
3099
|
mountAdminBrand,
|
|
2845
3100
|
startAdminBrand,
|
|
2846
3101
|
faultPresetList,
|
|
@@ -2863,6 +3118,7 @@ ${entry}(${payload});
|
|
|
2863
3118
|
var defineMock = (runtime) => runtime;
|
|
2864
3119
|
var STANDARD_ADMIN_ROUTES = [
|
|
2865
3120
|
"GET /",
|
|
3121
|
+
"GET /health",
|
|
2866
3122
|
"POST /reset",
|
|
2867
3123
|
"GET /namespaces",
|
|
2868
3124
|
"GET /clock",
|
|
@@ -2942,11 +3198,11 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
2942
3198
|
}
|
|
2943
3199
|
});
|
|
2944
3200
|
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
2945
|
-
var renderAdminDocument = (service) => {
|
|
3201
|
+
var renderAdminDocument = (service, adminPrefix = ADMIN_PREFIX) => {
|
|
2946
3202
|
const name = escapeHtml(service);
|
|
2947
3203
|
const script = adminClientSource({
|
|
2948
3204
|
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
2949
|
-
adminPrefix
|
|
3205
|
+
adminPrefix,
|
|
2950
3206
|
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
2951
3207
|
brandsUrl: ADMIN_BRANDS_URL,
|
|
2952
3208
|
service
|
|
@@ -2963,75 +3219,99 @@ var renderAdminDocument = (service) => {
|
|
|
2963
3219
|
${CSS_RESET}
|
|
2964
3220
|
:root {
|
|
2965
3221
|
color-scheme: light;
|
|
2966
|
-
--bg: #
|
|
3222
|
+
--bg: #f7f8fb;
|
|
2967
3223
|
--bg-elev: #ffffff;
|
|
2968
|
-
--bg-muted: #
|
|
2969
|
-
--bg-inset: #
|
|
2970
|
-
--line: #
|
|
2971
|
-
--line-strong: #
|
|
2972
|
-
--ink: #
|
|
2973
|
-
--muted: #
|
|
2974
|
-
--faint: #
|
|
2975
|
-
--accent: #
|
|
3224
|
+
--bg-muted: #f1f3f7;
|
|
3225
|
+
--bg-inset: #f8f9fc;
|
|
3226
|
+
--line: #e4e7ec;
|
|
3227
|
+
--line-strong: #cfd4dc;
|
|
3228
|
+
--ink: #101828;
|
|
3229
|
+
--muted: #475467;
|
|
3230
|
+
--faint: #667085;
|
|
3231
|
+
--accent: #6941c6;
|
|
3232
|
+
--accent-hover: #5933b4;
|
|
2976
3233
|
--accent-ink: #ffffff;
|
|
2977
|
-
--accent-soft: #
|
|
2978
|
-
--ok: #
|
|
2979
|
-
--ok-soft: #
|
|
2980
|
-
--warn: #
|
|
2981
|
-
--warn-soft: #
|
|
2982
|
-
--err: #
|
|
2983
|
-
--err-soft: #
|
|
2984
|
-
--
|
|
3234
|
+
--accent-soft: #f4f0ff;
|
|
3235
|
+
--ok: #067647;
|
|
3236
|
+
--ok-soft: #ecfdf3;
|
|
3237
|
+
--warn: #b54708;
|
|
3238
|
+
--warn-soft: #fffaeb;
|
|
3239
|
+
--err: #b42318;
|
|
3240
|
+
--err-soft: #fef3f2;
|
|
3241
|
+
--code-key: #6941c6;
|
|
3242
|
+
--code-string: #067647;
|
|
3243
|
+
--code-number: #175cd3;
|
|
3244
|
+
--code-boolean: #c11574;
|
|
3245
|
+
--code-null: #667085;
|
|
3246
|
+
--shadow-xs: 0 1px 2px rgb(16 24 40 / 0.05);
|
|
3247
|
+
--shadow-sm: 0 1px 3px rgb(16 24 40 / 0.08), 0 1px 2px rgb(16 24 40 / 0.04);
|
|
3248
|
+
--shadow-lg: 0 18px 48px rgb(16 24 40 / 0.16), 0 4px 12px rgb(16 24 40 / 0.08);
|
|
2985
3249
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2986
3250
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
2987
|
-
--radius:
|
|
2988
|
-
--header:
|
|
2989
|
-
--nav:
|
|
3251
|
+
--radius: 12px;
|
|
3252
|
+
--header: 72px;
|
|
3253
|
+
--nav: 224px;
|
|
2990
3254
|
}
|
|
2991
3255
|
:root[data-theme="dark"] {
|
|
2992
3256
|
color-scheme: dark;
|
|
2993
|
-
--bg: #
|
|
2994
|
-
--bg-elev: #
|
|
2995
|
-
--bg-muted: #
|
|
2996
|
-
--bg-inset: #
|
|
2997
|
-
--line: #
|
|
2998
|
-
--line-strong: #
|
|
2999
|
-
--ink: #
|
|
3000
|
-
--muted: #
|
|
3001
|
-
--faint: #
|
|
3002
|
-
--accent: #
|
|
3003
|
-
--accent-
|
|
3004
|
-
--accent-
|
|
3005
|
-
--
|
|
3006
|
-
--ok
|
|
3007
|
-
--
|
|
3008
|
-
--warn
|
|
3009
|
-
--
|
|
3010
|
-
--err
|
|
3011
|
-
--
|
|
3257
|
+
--bg: #0c0e14;
|
|
3258
|
+
--bg-elev: #151821;
|
|
3259
|
+
--bg-muted: #20232e;
|
|
3260
|
+
--bg-inset: #11141c;
|
|
3261
|
+
--line: #2b303d;
|
|
3262
|
+
--line-strong: #414858;
|
|
3263
|
+
--ink: #f5f7fa;
|
|
3264
|
+
--muted: #b6bdc9;
|
|
3265
|
+
--faint: #8d96a7;
|
|
3266
|
+
--accent: #9e77ed;
|
|
3267
|
+
--accent-hover: #b692f6;
|
|
3268
|
+
--accent-ink: #ffffff;
|
|
3269
|
+
--accent-soft: #2b2142;
|
|
3270
|
+
--ok: #75e0a7;
|
|
3271
|
+
--ok-soft: #102a20;
|
|
3272
|
+
--warn: #fec84b;
|
|
3273
|
+
--warn-soft: #302611;
|
|
3274
|
+
--err: #fda29b;
|
|
3275
|
+
--err-soft: #321817;
|
|
3276
|
+
--code-key: #c3a6ff;
|
|
3277
|
+
--code-string: #75e0a7;
|
|
3278
|
+
--code-number: #84adff;
|
|
3279
|
+
--code-boolean: #f9a8d4;
|
|
3280
|
+
--code-null: #98a2b3;
|
|
3281
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3282
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3283
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
3012
3284
|
}
|
|
3013
3285
|
@media (prefers-color-scheme: dark) {
|
|
3014
3286
|
:root:not([data-theme="light"]) {
|
|
3015
3287
|
color-scheme: dark;
|
|
3016
|
-
--bg: #
|
|
3017
|
-
--bg-elev: #
|
|
3018
|
-
--bg-muted: #
|
|
3019
|
-
--bg-inset: #
|
|
3020
|
-
--line: #
|
|
3021
|
-
--line-strong: #
|
|
3022
|
-
--ink: #
|
|
3023
|
-
--muted: #
|
|
3024
|
-
--faint: #
|
|
3025
|
-
--accent: #
|
|
3026
|
-
--accent-
|
|
3027
|
-
--accent-
|
|
3028
|
-
--
|
|
3029
|
-
--ok
|
|
3030
|
-
--
|
|
3031
|
-
--warn
|
|
3032
|
-
--
|
|
3033
|
-
--err
|
|
3034
|
-
--
|
|
3288
|
+
--bg: #0c0e14;
|
|
3289
|
+
--bg-elev: #151821;
|
|
3290
|
+
--bg-muted: #20232e;
|
|
3291
|
+
--bg-inset: #11141c;
|
|
3292
|
+
--line: #2b303d;
|
|
3293
|
+
--line-strong: #414858;
|
|
3294
|
+
--ink: #f5f7fa;
|
|
3295
|
+
--muted: #b6bdc9;
|
|
3296
|
+
--faint: #8d96a7;
|
|
3297
|
+
--accent: #9e77ed;
|
|
3298
|
+
--accent-hover: #b692f6;
|
|
3299
|
+
--accent-ink: #ffffff;
|
|
3300
|
+
--accent-soft: #2b2142;
|
|
3301
|
+
--ok: #75e0a7;
|
|
3302
|
+
--ok-soft: #102a20;
|
|
3303
|
+
--warn: #fec84b;
|
|
3304
|
+
--warn-soft: #302611;
|
|
3305
|
+
--err: #fda29b;
|
|
3306
|
+
--err-soft: #321817;
|
|
3307
|
+
--code-key: #c3a6ff;
|
|
3308
|
+
--code-string: #75e0a7;
|
|
3309
|
+
--code-number: #84adff;
|
|
3310
|
+
--code-boolean: #f9a8d4;
|
|
3311
|
+
--code-null: #98a2b3;
|
|
3312
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3313
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3314
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
3035
3315
|
}
|
|
3036
3316
|
}
|
|
3037
3317
|
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
@@ -3049,13 +3329,17 @@ var renderAdminDocument = (service) => {
|
|
|
3049
3329
|
padding-right: env(safe-area-inset-right);
|
|
3050
3330
|
-webkit-text-size-adjust: 100%;
|
|
3051
3331
|
}
|
|
3052
|
-
|
|
3332
|
+
::selection { background: color-mix(in srgb, var(--accent) 28%, transparent); }
|
|
3333
|
+
:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px; }
|
|
3334
|
+
button, input, select, textarea { transition: border-color 140ms ease, box-shadow 140ms ease, transform 100ms ease; }
|
|
3335
|
+
button:disabled, input:disabled, select:disabled, textarea:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
3053
3336
|
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
3054
3337
|
header.top {
|
|
3055
|
-
position: sticky; top: 0; z-index:
|
|
3338
|
+
position: sticky; top: 0; z-index: 20;
|
|
3056
3339
|
display: flex; align-items: center; gap: 12px;
|
|
3057
|
-
min-height: var(--header); padding:
|
|
3058
|
-
background: var(--bg-elev);
|
|
3340
|
+
min-height: var(--header); padding: 12px 20px;
|
|
3341
|
+
background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
|
|
3342
|
+
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
3059
3343
|
border-bottom: 1px solid var(--line);
|
|
3060
3344
|
min-width: 0; max-width: 100%;
|
|
3061
3345
|
}
|
|
@@ -3063,12 +3347,21 @@ var renderAdminDocument = (service) => {
|
|
|
3063
3347
|
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
3064
3348
|
min-width: 0; max-width: 100%; margin-right: auto;
|
|
3065
3349
|
}
|
|
3066
|
-
.brand-
|
|
3067
|
-
.brand-
|
|
3350
|
+
.brand-lockup { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
3351
|
+
.brand-mark {
|
|
3352
|
+
width: 38px; height: 38px; display: grid; place-items: center; flex: none;
|
|
3353
|
+
border-radius: 11px; color: #fff;
|
|
3354
|
+
background: linear-gradient(145deg, #7f56d9 0%, #53389e 100%);
|
|
3355
|
+
box-shadow: 0 6px 16px rgb(105 65 198 / 0.24), inset 0 1px 0 rgb(255 255 255 / 0.25);
|
|
3356
|
+
}
|
|
3357
|
+
.brand-mark svg { width: 22px; height: 22px; }
|
|
3358
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; line-height: 1.25; }
|
|
3359
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
3068
3360
|
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
3069
3361
|
.vendor {
|
|
3070
3362
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
3071
3363
|
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
3364
|
+
padding: 5px 8px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-inset);
|
|
3072
3365
|
}
|
|
3073
3366
|
.vendor[hidden] { display: none; }
|
|
3074
3367
|
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
@@ -3080,12 +3373,17 @@ var renderAdminDocument = (service) => {
|
|
|
3080
3373
|
.vendor-sep { color: var(--faint); }
|
|
3081
3374
|
.vendor-link { color: inherit; text-decoration: none; }
|
|
3082
3375
|
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
3083
|
-
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items:
|
|
3084
|
-
|
|
3376
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
|
|
3377
|
+
.sync-status { align-self: center; display: flex; align-items: center; gap: 6px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3378
|
+
.sync-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
|
|
3379
|
+
.sync-status.busy::before { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); animation: pulse 1.2s ease-in-out infinite; }
|
|
3380
|
+
label.field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; font-weight: 600; color: var(--faint); }
|
|
3085
3381
|
label.field input, label.field select, .controls input[type="password"] {
|
|
3086
|
-
background-color: var(--bg-
|
|
3087
|
-
padding:
|
|
3382
|
+
background-color: var(--bg-elev); border: 1px solid var(--line-strong); border-radius: 9px;
|
|
3383
|
+
padding: 8px 10px; min-height: 38px; line-height: 1.2; box-shadow: var(--shadow-xs);
|
|
3088
3384
|
}
|
|
3385
|
+
label.field input:hover, label.field select:hover, textarea:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--line-strong)); }
|
|
3386
|
+
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; }
|
|
3089
3387
|
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
3090
3388
|
the text fields in the same form. Paint it like those fields. */
|
|
3091
3389
|
label.field select {
|
|
@@ -3102,12 +3400,20 @@ var renderAdminDocument = (service) => {
|
|
|
3102
3400
|
dialog .field > textarea { width: 100%; }
|
|
3103
3401
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
3104
3402
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
.btn
|
|
3403
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
|
3404
|
+
border-radius: 9px; min-height: 38px; padding: 0 13px; border: 1px solid var(--line-strong);
|
|
3405
|
+
background: var(--bg-elev); color: var(--ink); font-size: 13px; font-weight: 600; line-height: 1;
|
|
3406
|
+
box-shadow: var(--shadow-xs);
|
|
3407
|
+
}
|
|
3408
|
+
.icon-btn { width: 38px; padding: 0; }
|
|
3409
|
+
.icon-btn svg, .btn svg, .btn-primary svg { width: 16px; height: 16px; }
|
|
3410
|
+
.icon-btn:hover, .btn:hover { background: var(--bg-muted); border-color: var(--line-strong); }
|
|
3411
|
+
.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; }
|
|
3412
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
3413
|
+
.btn-quiet { background: transparent; border-color: transparent; box-shadow: none; }
|
|
3414
|
+
.btn-quiet:hover { background: var(--bg-muted); }
|
|
3415
|
+
.btn-danger { color: var(--err); border-color: color-mix(in srgb, var(--err) 25%, var(--line)); }
|
|
3416
|
+
.btn-danger:hover, .btn-danger.confirming { background: var(--err-soft); border-color: color-mix(in srgb, var(--err) 45%, var(--line)); }
|
|
3111
3417
|
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
3112
3418
|
so the press itself has to stay visible. */
|
|
3113
3419
|
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
@@ -3123,93 +3429,159 @@ var renderAdminDocument = (service) => {
|
|
|
3123
3429
|
flex: 1 0 auto; min-width: 0;
|
|
3124
3430
|
}
|
|
3125
3431
|
nav.side {
|
|
3126
|
-
|
|
3432
|
+
position: sticky; top: var(--header); align-self: start; height: calc(100vh - var(--header));
|
|
3433
|
+
border-right: 1px solid var(--line); padding: 18px 12px;
|
|
3127
3434
|
display: flex; flex-direction: column; gap: 4px;
|
|
3128
|
-
background: var(--bg-elev);
|
|
3435
|
+
background: var(--bg-elev); overflow-y: auto;
|
|
3129
3436
|
}
|
|
3437
|
+
.nav-label { padding: 0 10px 8px; color: var(--faint); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
|
|
3130
3438
|
nav.side button {
|
|
3439
|
+
position: relative; display: flex; align-items: center; gap: 10px; width: 100%;
|
|
3131
3440
|
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
3132
|
-
padding: 9px 10px; min-height:
|
|
3133
|
-
}
|
|
3134
|
-
nav.side button
|
|
3135
|
-
|
|
3136
|
-
.
|
|
3137
|
-
.
|
|
3138
|
-
.
|
|
3441
|
+
padding: 9px 10px; min-height: 42px; color: var(--muted); font-size: 13px; font-weight: 550;
|
|
3442
|
+
}
|
|
3443
|
+
nav.side button:hover { background: var(--bg-muted); color: var(--ink); }
|
|
3444
|
+
nav.side button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 650; }
|
|
3445
|
+
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); }
|
|
3446
|
+
.nav-icon { width: 18px; height: 18px; display: grid; place-items: center; flex: none; }
|
|
3447
|
+
.nav-icon svg { width: 18px; height: 18px; }
|
|
3448
|
+
main { padding: 32px clamp(20px, 4vw, 48px) 64px; max-width: 1320px; width: 100%; min-width: 0; }
|
|
3449
|
+
main[aria-busy="true"] .view { opacity: 0.62; }
|
|
3450
|
+
.view { animation: view-in 180ms ease-out; }
|
|
3451
|
+
.view[hidden] { display: none; }
|
|
3452
|
+
.view-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
|
|
3453
|
+
.view h2 { margin: 0 0 5px; font-size: clamp(24px, 3vw, 30px); line-height: 1.2; letter-spacing: -0.035em; }
|
|
3454
|
+
.view h3 { margin: 24px 0 10px; font-size: 15px; letter-spacing: -0.01em; }
|
|
3455
|
+
.lede { margin: 0; color: var(--muted); max-width: 72ch; font-size: 14px; }
|
|
3456
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 175px), 1fr)); gap: 14px; }
|
|
3139
3457
|
.card {
|
|
3140
3458
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
3141
|
-
padding:
|
|
3459
|
+
padding: 18px; box-shadow: var(--shadow-sm);
|
|
3142
3460
|
}
|
|
3143
|
-
.card
|
|
3144
|
-
.card
|
|
3461
|
+
.stat-card { position: relative; overflow: hidden; }
|
|
3462
|
+
.stat-card::after { content: ""; position: absolute; inset: 0 0 auto; height: 2px; background: linear-gradient(90deg, var(--accent), transparent 78%); opacity: 0.7; }
|
|
3463
|
+
.card .k { color: var(--faint); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
3464
|
+
.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; }
|
|
3145
3465
|
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
3146
|
-
.
|
|
3466
|
+
.section-card { background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-xs); overflow: hidden; }
|
|
3467
|
+
.section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
|
|
3468
|
+
.section-title h3 { margin: 0; }
|
|
3469
|
+
.split { display: grid; grid-template-columns: minmax(0, 250px) minmax(0, 1fr); gap: 18px; align-items: start; }
|
|
3470
|
+
#collections { padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); box-shadow: var(--shadow-xs); }
|
|
3147
3471
|
.collection {
|
|
3148
3472
|
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
3149
|
-
border-radius:
|
|
3473
|
+
border-radius: 9px; padding: 10px 11px; min-height: 48px; color: var(--ink); cursor: pointer;
|
|
3150
3474
|
}
|
|
3151
|
-
.collection
|
|
3475
|
+
.collection:hover { background: var(--bg-muted); }
|
|
3476
|
+
.collection[aria-current="true"] { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 18%, var(--line)); color: var(--accent); }
|
|
3152
3477
|
.collection small { display: block; color: var(--faint); }
|
|
3153
3478
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
3154
|
-
.chip {
|
|
3155
|
-
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
3156
|
-
border-radius: 999px; padding: 4px 8px;
|
|
3157
|
-
}
|
|
3479
|
+
.chip { font-family: var(--mono); font-size: 11px; background: var(--bg-muted); border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; }
|
|
3158
3480
|
.chip i { color: var(--faint); font-style: normal; }
|
|
3159
3481
|
.sql-layout {
|
|
3160
3482
|
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
3161
3483
|
}
|
|
3162
|
-
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
3163
|
-
.sql-tables button { text-align: left; }
|
|
3484
|
+
.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); }
|
|
3485
|
+
.sql-tables button { text-align: left; border: 0; border-radius: 8px; padding: 9px 10px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
3486
|
+
.sql-tables button:hover, .sql-tables button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); }
|
|
3164
3487
|
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
3165
|
-
table { width: 100%; border-
|
|
3166
|
-
|
|
3167
|
-
th {
|
|
3488
|
+
.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); }
|
|
3489
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
3490
|
+
th, td { text-align: left; padding: 11px 13px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
3491
|
+
th { color: var(--faint); background: var(--bg-inset); font-weight: 650; font-size: 11px; text-transform: uppercase; letter-spacing: 0.045em; }
|
|
3492
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
3168
3493
|
tr.clickable { cursor: pointer; }
|
|
3169
|
-
tr.clickable:hover { background: var(--
|
|
3494
|
+
tr.clickable:hover { background: var(--accent-soft); }
|
|
3170
3495
|
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
3171
3496
|
pre {
|
|
3172
3497
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
3173
|
-
background: var(--bg-inset); border-radius: 10px; padding:
|
|
3174
|
-
}
|
|
3175
|
-
.
|
|
3498
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 10px; padding: 14px; font-size: 12px; line-height: 1.65;
|
|
3499
|
+
}
|
|
3500
|
+
.json-key { color: var(--code-key); }
|
|
3501
|
+
.json-string { color: var(--code-string); }
|
|
3502
|
+
.json-number { color: var(--code-number); }
|
|
3503
|
+
.json-boolean { color: var(--code-boolean); }
|
|
3504
|
+
.json-null { color: var(--code-null); font-style: italic; }
|
|
3505
|
+
.json-inline { display: block; max-width: 44rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
3506
|
+
.json-view { position: relative; max-height: 26rem; overflow: auto; }
|
|
3507
|
+
.json-editor { position: relative; min-height: 190px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--bg-inset); overflow: hidden; }
|
|
3508
|
+
.json-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); }
|
|
3509
|
+
.json-editor.invalid { border-color: var(--err); }
|
|
3510
|
+
.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; }
|
|
3511
|
+
.json-editor pre { position: absolute; inset: 0; pointer-events: none; background: transparent; }
|
|
3512
|
+
.json-editor textarea { position: relative; z-index: 1; resize: vertical; background: transparent; color: transparent; caret-color: var(--ink); -webkit-text-fill-color: transparent; }
|
|
3513
|
+
.json-editor textarea::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); }
|
|
3514
|
+
.editor-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; }
|
|
3515
|
+
.json-status { display: inline-flex; align-items: center; gap: 6px; color: var(--ok); font-size: 11px; }
|
|
3516
|
+
.json-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
3517
|
+
.json-status.invalid { color: var(--err); }
|
|
3518
|
+
.format-json { border: 0; padding: 3px 5px; background: transparent; color: var(--accent); font-size: 11px; font-weight: 650; cursor: pointer; }
|
|
3519
|
+
.row-actions { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 14px 0; }
|
|
3176
3520
|
.banner {
|
|
3177
|
-
display: none;
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3521
|
+
display: none; position: sticky; top: calc(var(--header) + 12px); z-index: 15;
|
|
3522
|
+
align-items: center; gap: 10px; margin: 0 0 16px; padding: 11px 12px 11px 14px;
|
|
3523
|
+
border: 1px solid color-mix(in srgb, var(--err) 30%, var(--line)); border-radius: 10px;
|
|
3524
|
+
background: var(--err-soft); color: var(--err); box-shadow: var(--shadow-sm); font-size: 13px;
|
|
3525
|
+
}
|
|
3526
|
+
.banner.show { display: flex; }
|
|
3527
|
+
.banner span { flex: 1; }
|
|
3528
|
+
.banner button { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 7px; background: transparent; color: currentColor; cursor: pointer; }
|
|
3529
|
+
.banner button:hover { background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
3530
|
+
.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); }
|
|
3182
3531
|
dialog {
|
|
3183
|
-
border: 1px solid var(--line); border-radius:
|
|
3184
|
-
color: var(--ink); width: min(
|
|
3185
|
-
}
|
|
3186
|
-
dialog
|
|
3187
|
-
dialog
|
|
3532
|
+
border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--bg-elev);
|
|
3533
|
+
color: var(--ink); width: min(720px, calc(100% - 24px)); max-width: calc(100vw - 24px); box-shadow: var(--shadow-lg);
|
|
3534
|
+
}
|
|
3535
|
+
dialog[open] { animation: dialog-in 160ms ease-out; }
|
|
3536
|
+
dialog::backdrop { background: rgb(12 14 20 / 0.58); backdrop-filter: blur(3px); }
|
|
3537
|
+
dialog form, .dialog-body { padding: 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
3538
|
+
.dialog-heading { display: flex; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
|
3539
|
+
.dialog-heading strong { font-size: 18px; letter-spacing: -0.02em; }
|
|
3540
|
+
.dialog-heading span { color: var(--muted); font-size: 12px; }
|
|
3188
3541
|
textarea {
|
|
3189
3542
|
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
3190
3543
|
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
3191
3544
|
font-family: var(--mono); font-size: 12px;
|
|
3192
3545
|
}
|
|
3193
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
3194
|
-
.
|
|
3546
|
+
.dialog-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding-top: 4px; }
|
|
3547
|
+
.dialog-actions .btn-danger { margin-right: auto; }
|
|
3548
|
+
.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); }
|
|
3195
3549
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
3196
3550
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
3197
3551
|
.status-2 { color: var(--ok); }
|
|
3198
3552
|
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
3199
3553
|
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
3554
|
+
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
3555
|
+
.form-grid .wide { grid-column: span 2; }
|
|
3200
3556
|
.route {
|
|
3201
|
-
display: grid; grid-template-columns:
|
|
3202
|
-
padding:
|
|
3203
|
-
}
|
|
3204
|
-
.
|
|
3557
|
+
display: grid; grid-template-columns: 84px minmax(0, 1fr) auto; gap: 10px; align-items: center;
|
|
3558
|
+
padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--bg-elev);
|
|
3559
|
+
}
|
|
3560
|
+
.route:first-child { border-radius: var(--radius) var(--radius) 0 0; }
|
|
3561
|
+
.route:last-child { border-bottom: 0; border-radius: 0 0 var(--radius) var(--radius); }
|
|
3562
|
+
.route-tools { display: flex; align-items: flex-end; gap: 12px; margin-bottom: 12px; }
|
|
3563
|
+
.route-tools .field { flex: 1; }
|
|
3564
|
+
.route-tools input { width: 100%; }
|
|
3565
|
+
.route-count { padding-bottom: 10px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3566
|
+
#route-list { max-height: 34rem; border: 1px solid var(--line); border-radius: var(--radius); overflow: auto; box-shadow: var(--shadow-xs); }
|
|
3567
|
+
.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; }
|
|
3568
|
+
@keyframes view-in { from { transform: translateY(3px); } to { transform: translateY(0); } }
|
|
3569
|
+
@keyframes dialog-in { from { transform: translateY(8px) scale(0.99); } to { transform: translateY(0) scale(1); } }
|
|
3570
|
+
@keyframes pulse { 50% { opacity: 0.45; } }
|
|
3571
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
|
|
3572
|
+
@media (forced-colors: active) { .json-editor pre { display: none; } .json-editor textarea { color: CanvasText; -webkit-text-fill-color: CanvasText; } }
|
|
3205
3573
|
@media (max-width: 800px) {
|
|
3574
|
+
:root { --header: 0px; }
|
|
3206
3575
|
header.top {
|
|
3207
|
-
align-items: stretch; flex-direction: column;
|
|
3208
|
-
padding
|
|
3576
|
+
position: relative; align-items: stretch; flex-direction: column;
|
|
3577
|
+
padding: max(12px, env(safe-area-inset-top)) 14px 12px;
|
|
3209
3578
|
}
|
|
3210
3579
|
input, select, textarea { font-size: 16px; }
|
|
3211
3580
|
.brand, .vendor, .controls { width: 100%; }
|
|
3581
|
+
.brand { gap: 10px; }
|
|
3582
|
+
.vendor { order: 2; border-radius: 9px; }
|
|
3212
3583
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
3584
|
+
.sync-status { display: none; }
|
|
3213
3585
|
/* The layout fills the screen. A single-column grid stretches every auto
|
|
3214
3586
|
row, so the tab bar grew with the leftover height and the buttons floated
|
|
3215
3587
|
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
@@ -3219,14 +3591,23 @@ var renderAdminDocument = (service) => {
|
|
|
3219
3591
|
align-content: start;
|
|
3220
3592
|
}
|
|
3221
3593
|
nav.side {
|
|
3222
|
-
align-self: start; height: auto;
|
|
3594
|
+
position: sticky; top: 0; z-index: 12; align-self: start; height: auto;
|
|
3223
3595
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
3224
3596
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
3225
|
-
padding: 8px;
|
|
3597
|
+
padding: 8px; box-shadow: var(--shadow-xs);
|
|
3226
3598
|
}
|
|
3227
|
-
nav
|
|
3599
|
+
.nav-label { display: none; }
|
|
3600
|
+
nav.side button { width: auto; white-space: nowrap; flex: none; }
|
|
3601
|
+
nav.side button[aria-current="true"]::before { inset: auto 9px -8px; width: auto; height: 3px; border-radius: 3px 3px 0 0; }
|
|
3228
3602
|
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
3229
|
-
|
|
3603
|
+
.form-grid { grid-template-columns: minmax(0, 1fr); }
|
|
3604
|
+
.form-grid .wide { grid-column: auto; }
|
|
3605
|
+
main { padding: 22px 14px 48px; }
|
|
3606
|
+
.view-head { margin-bottom: 18px; }
|
|
3607
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
3608
|
+
.card { padding: 14px; }
|
|
3609
|
+
.card .v { font-size: 20px; }
|
|
3610
|
+
.table-shell { border: 0; background: transparent; box-shadow: none; overflow: visible; }
|
|
3230
3611
|
table, thead, tbody, tr, th, td { display: block; }
|
|
3231
3612
|
thead { display: none; }
|
|
3232
3613
|
tr {
|
|
@@ -3241,50 +3622,63 @@ var renderAdminDocument = (service) => {
|
|
|
3241
3622
|
.route { grid-template-columns: 72px 1fr; }
|
|
3242
3623
|
.route button { grid-column: 1 / -1; }
|
|
3243
3624
|
}
|
|
3625
|
+
@media (max-width: 460px) {
|
|
3626
|
+
.controls label.field { min-width: calc(50% - 4px); }
|
|
3627
|
+
.controls .icon-btn, .controls .btn { flex: 1; }
|
|
3628
|
+
.grid { grid-template-columns: minmax(0, 1fr); }
|
|
3629
|
+
.dialog-actions { flex-wrap: wrap; }
|
|
3630
|
+
.dialog-actions button { flex: 1; }
|
|
3631
|
+
.dialog-actions .btn-danger { flex-basis: 100%; margin-right: 0; }
|
|
3632
|
+
}
|
|
3244
3633
|
</style>
|
|
3245
3634
|
</head>
|
|
3246
3635
|
<body>
|
|
3247
3636
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
3248
3637
|
<header class="top">
|
|
3249
3638
|
<div class="brand">
|
|
3250
|
-
<div class="brand-
|
|
3251
|
-
<
|
|
3252
|
-
|
|
3639
|
+
<div class="brand-lockup">
|
|
3640
|
+
<div class="brand-mark" aria-hidden="true">
|
|
3641
|
+
<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>
|
|
3642
|
+
</div>
|
|
3643
|
+
<div class="brand-id">
|
|
3644
|
+
<strong>${name}</strong>
|
|
3645
|
+
<span>Mock admin</span>
|
|
3646
|
+
</div>
|
|
3253
3647
|
</div>
|
|
3254
3648
|
<div class="vendor" id="vendor" hidden></div>
|
|
3255
3649
|
</div>
|
|
3256
3650
|
<div class="controls">
|
|
3651
|
+
<span class="sync-status" id="sync-status" role="status" aria-live="polite">Ready</span>
|
|
3257
3652
|
<label class="field">Namespace
|
|
3258
3653
|
<select id="namespace" aria-label="Namespace"></select>
|
|
3259
3654
|
</label>
|
|
3260
3655
|
<label class="field">Admin key
|
|
3261
3656
|
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
3262
3657
|
</label>
|
|
3263
|
-
<button class="icon-btn" id="theme" type="button" aria-label="
|
|
3264
|
-
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
3658
|
+
<button class="icon-btn" id="theme" type="button" aria-label="Use dark theme" title="Use dark theme"></button>
|
|
3659
|
+
<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>
|
|
3265
3660
|
</div>
|
|
3266
3661
|
</header>
|
|
3267
3662
|
<div class="layout">
|
|
3268
3663
|
<nav class="side" id="nav" aria-label="Admin sections">
|
|
3269
|
-
<
|
|
3270
|
-
<button type="button" data-view="
|
|
3271
|
-
<button type="button" data-view="
|
|
3272
|
-
<button type="button" data-view="
|
|
3273
|
-
<button type="button" data-view="
|
|
3274
|
-
<button type="button" data-view="
|
|
3664
|
+
<span class="nav-label">Workspace</span>
|
|
3665
|
+
<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>
|
|
3666
|
+
<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>
|
|
3667
|
+
<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>
|
|
3668
|
+
<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>
|
|
3669
|
+
<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>
|
|
3670
|
+
<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>
|
|
3275
3671
|
</nav>
|
|
3276
3672
|
<main id="views">
|
|
3277
|
-
<
|
|
3673
|
+
<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>
|
|
3278
3674
|
<section class="view" id="view-overview">
|
|
3279
|
-
<h2>Overview</h2>
|
|
3280
|
-
<p class="lede">Health, clock, and the collections this namespace is holding.</p>
|
|
3675
|
+
<div class="view-head"><div><h2>Overview</h2><p class="lede">Health, clock, and the collections this namespace is holding.</p></div></div>
|
|
3281
3676
|
<div class="grid" id="overview-cards"></div>
|
|
3282
3677
|
<h3>Recent requests</h3>
|
|
3283
3678
|
<div id="overview-requests"></div>
|
|
3284
3679
|
</section>
|
|
3285
3680
|
<section class="view" id="view-state" hidden>
|
|
3286
|
-
<h2>State</h2>
|
|
3287
|
-
<p class="lede">Every collection in this namespace. Declared fields stay visible before any row exists; stored rows add whatever else they contain.</p>
|
|
3681
|
+
<div class="view-head"><div><h2>State</h2><p class="lede">Inspect and edit every collection in this namespace.</p></div></div>
|
|
3288
3682
|
<div class="split">
|
|
3289
3683
|
<div id="collections"></div>
|
|
3290
3684
|
<div>
|
|
@@ -3297,8 +3691,7 @@ var renderAdminDocument = (service) => {
|
|
|
3297
3691
|
</div>
|
|
3298
3692
|
</section>
|
|
3299
3693
|
<section class="view" id="view-clock" hidden>
|
|
3300
|
-
<h2>Clock</h2>
|
|
3301
|
-
<p class="lede">The clock every timestamp in this mock reads.</p>
|
|
3694
|
+
<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>
|
|
3302
3695
|
<div class="card" id="clock-readout"></div>
|
|
3303
3696
|
<div class="row-actions">
|
|
3304
3697
|
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
@@ -3317,21 +3710,22 @@ var renderAdminDocument = (service) => {
|
|
|
3317
3710
|
</form>
|
|
3318
3711
|
</section>
|
|
3319
3712
|
<section class="view" id="view-faults" hidden>
|
|
3320
|
-
<h2>Faults</h2>
|
|
3321
|
-
<p class="lede">Inject a response, a delay, or a named preset. Rules apply to this namespace.</p>
|
|
3713
|
+
<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>
|
|
3322
3714
|
<form id="fault-form" class="card stack">
|
|
3323
|
-
<
|
|
3324
|
-
<
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
<
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
<
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
<
|
|
3334
|
-
|
|
3715
|
+
<div class="form-grid">
|
|
3716
|
+
<label class="field wide">Preset
|
|
3717
|
+
<select id="fault-preset"><option value="">Custom rule</option></select>
|
|
3718
|
+
</label>
|
|
3719
|
+
<label class="field">Status
|
|
3720
|
+
<input id="fault-status" inputmode="numeric" placeholder="503">
|
|
3721
|
+
</label>
|
|
3722
|
+
<label class="field wide">Path prefix
|
|
3723
|
+
<input id="fault-path" placeholder="/v1">
|
|
3724
|
+
</label>
|
|
3725
|
+
<label class="field">Method
|
|
3726
|
+
<input id="fault-method" placeholder="GET">
|
|
3727
|
+
</label>
|
|
3728
|
+
</div>
|
|
3335
3729
|
<div class="dialog-actions">
|
|
3336
3730
|
<button class="btn-primary" type="submit">Add fault</button>
|
|
3337
3731
|
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
@@ -3340,26 +3734,31 @@ var renderAdminDocument = (service) => {
|
|
|
3340
3734
|
<div id="fault-list"></div>
|
|
3341
3735
|
</section>
|
|
3342
3736
|
<section class="view" id="view-journal" hidden>
|
|
3343
|
-
<h2>Journal</h2>
|
|
3344
|
-
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
3345
|
-
<div class="row-actions">
|
|
3346
|
-
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
3347
|
-
</div>
|
|
3737
|
+
<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>
|
|
3348
3738
|
<div id="journal"></div>
|
|
3349
3739
|
</section>
|
|
3350
3740
|
<section class="view" id="view-routes" hidden>
|
|
3351
|
-
<h2>Routes</h2>
|
|
3352
|
-
<
|
|
3741
|
+
<div class="view-head"><div><h2>Routes</h2><p class="lede">Explore shared controls and service-specific admin endpoints.</p></div></div>
|
|
3742
|
+
<div class="route-tools">
|
|
3743
|
+
<label class="field">Filter routes
|
|
3744
|
+
<input id="route-search" type="search" placeholder="Method, path, or extension" autocomplete="off">
|
|
3745
|
+
</label>
|
|
3746
|
+
<span class="route-count" id="route-count"></span>
|
|
3747
|
+
</div>
|
|
3353
3748
|
<div id="route-list"></div>
|
|
3354
|
-
<form id="route-form" class="card stack">
|
|
3749
|
+
<form id="route-form" class="card stack" style="margin-top: 18px">
|
|
3355
3750
|
<strong id="route-title">Select a route</strong>
|
|
3356
3751
|
<label class="field">JSON body
|
|
3357
|
-
<
|
|
3752
|
+
<div class="json-editor" id="route-editor" hidden>
|
|
3753
|
+
<pre aria-hidden="true"><code id="route-highlight"></code></pre>
|
|
3754
|
+
<textarea id="route-body" placeholder="{ }" spellcheck="false" aria-describedby="route-json-status" disabled></textarea>
|
|
3755
|
+
</div>
|
|
3756
|
+
<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>
|
|
3358
3757
|
</label>
|
|
3359
3758
|
<div class="dialog-actions">
|
|
3360
|
-
<button class="btn-primary" type="submit">Send</button>
|
|
3759
|
+
<button class="btn-primary" id="route-send" type="submit" disabled>Send request</button>
|
|
3361
3760
|
</div>
|
|
3362
|
-
<pre id="route-result">Response will show here.</pre>
|
|
3761
|
+
<pre class="json-view" id="route-result"><code>Response will show here.</code></pre>
|
|
3363
3762
|
</form>
|
|
3364
3763
|
</section>
|
|
3365
3764
|
</main>
|
|
@@ -3367,12 +3766,16 @@ var renderAdminDocument = (service) => {
|
|
|
3367
3766
|
</div>
|
|
3368
3767
|
<dialog id="editor">
|
|
3369
3768
|
<form method="dialog" id="editor-form">
|
|
3370
|
-
<strong id="editor-title">Record</strong>
|
|
3769
|
+
<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>
|
|
3371
3770
|
<label class="field">Id
|
|
3372
3771
|
<input id="editor-id" required>
|
|
3373
3772
|
</label>
|
|
3374
3773
|
<label class="field">JSON value
|
|
3375
|
-
<
|
|
3774
|
+
<div class="json-editor" id="record-editor">
|
|
3775
|
+
<pre aria-hidden="true"><code id="editor-highlight"></code></pre>
|
|
3776
|
+
<textarea id="editor-value" spellcheck="false" aria-describedby="editor-json-status"></textarea>
|
|
3777
|
+
</div>
|
|
3778
|
+
<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>
|
|
3376
3779
|
</label>
|
|
3377
3780
|
<div class="dialog-actions">
|
|
3378
3781
|
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
@@ -3414,8 +3817,8 @@ var describeExtension = (extension) => {
|
|
|
3414
3817
|
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
3415
3818
|
};
|
|
3416
3819
|
};
|
|
3417
|
-
var adminUiRoutes = (service, ui) => {
|
|
3418
|
-
const shell = () => renderAdminDocument(service);
|
|
3820
|
+
var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
3821
|
+
const shell = () => renderAdminDocument(service, adminPrefix);
|
|
3419
3822
|
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
3420
3823
|
return {
|
|
3421
3824
|
"GET /ui": () => htmlResponse(document3()),
|
|
@@ -4106,10 +4509,13 @@ var stateAdminRoutes = (routes) => {
|
|
|
4106
4509
|
};
|
|
4107
4510
|
|
|
4108
4511
|
// ../core/dist/version.js
|
|
4109
|
-
var PACKAGE_VERSION = true ? "
|
|
4512
|
+
var PACKAGE_VERSION = true ? "2.0.0" : UNRELEASED_VERSION;
|
|
4110
4513
|
|
|
4111
4514
|
// ../core/dist/webhooks.js
|
|
4112
|
-
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4515
|
+
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4516
|
+
status,
|
|
4517
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4518
|
+
});
|
|
4113
4519
|
var adminError3 = (status, message) => json4(status, { error: { type: "mockingbird_admin", message } });
|
|
4114
4520
|
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4115
4521
|
var parseEndpoint = (value) => {
|
|
@@ -4217,7 +4623,6 @@ var AT_HEADER = "x-mockingbird-at";
|
|
|
4217
4623
|
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
4218
4624
|
var DEFAULT_NAMESPACE = "default";
|
|
4219
4625
|
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4220
|
-
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
4221
4626
|
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4222
4627
|
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
4223
4628
|
var effects = /* @__PURE__ */ new WeakMap();
|
|
@@ -4258,6 +4663,22 @@ var operationMatcher = (document3) => {
|
|
|
4258
4663
|
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
4259
4664
|
};
|
|
4260
4665
|
var createRuntime = (options) => {
|
|
4666
|
+
const adminPrefix = resolveAdminPrefix(options.adminPrefix);
|
|
4667
|
+
const internalPaths = Object.entries(options.document?.paths ?? {}).filter(([, item]) => item["x-mockingbird-internal"] === true).map(([path]) => path);
|
|
4668
|
+
for (const path of internalPaths) {
|
|
4669
|
+
if (!path.startsWith(`${ADMIN_PREFIX}/blobs/`))
|
|
4670
|
+
throw new Error(`internal transport ${path} must live under ${ADMIN_PREFIX}/blobs/`);
|
|
4671
|
+
}
|
|
4672
|
+
assertAdminPrefixAvailable(adminPrefix, Object.keys(options.document?.paths ?? {}).filter((path) => !internalPaths.includes(path)));
|
|
4673
|
+
const internalMatch = (path) => {
|
|
4674
|
+
for (const pattern of internalPaths) {
|
|
4675
|
+
const actual = path.split("/");
|
|
4676
|
+
const expected = pattern.split("/");
|
|
4677
|
+
if (actual.length === expected.length && expected.every((part, i) => part.startsWith("{") || part === actual[i]))
|
|
4678
|
+
return pattern;
|
|
4679
|
+
}
|
|
4680
|
+
return void 0;
|
|
4681
|
+
};
|
|
4261
4682
|
assertAdminUi(options.adminUi);
|
|
4262
4683
|
const sqlite = bootSqlite(options.sqlite);
|
|
4263
4684
|
const clock = options.clock ?? createClock();
|
|
@@ -4272,6 +4693,20 @@ var createRuntime = (options) => {
|
|
|
4272
4693
|
const instances = /* @__PURE__ */ new Map();
|
|
4273
4694
|
const publicNamespaces = /* @__PURE__ */ new Set();
|
|
4274
4695
|
const branchRngs = /* @__PURE__ */ new Map();
|
|
4696
|
+
const namespaceClocks = /* @__PURE__ */ new Map();
|
|
4697
|
+
const namespaceForStorage = /* @__PURE__ */ new Map();
|
|
4698
|
+
let isolatedNamespaces = false;
|
|
4699
|
+
const namespaceClock = (name = DEFAULT_NAMESPACE) => {
|
|
4700
|
+
if (!isolatedNamespaces || name === DEFAULT_NAMESPACE)
|
|
4701
|
+
return clock;
|
|
4702
|
+
let found = namespaceClocks.get(name);
|
|
4703
|
+
if (!found) {
|
|
4704
|
+
found = createClock();
|
|
4705
|
+
namespaceClocks.set(name, found);
|
|
4706
|
+
}
|
|
4707
|
+
return found;
|
|
4708
|
+
};
|
|
4709
|
+
const fleetPoints = /* @__PURE__ */ new WeakMap();
|
|
4275
4710
|
const timelines = /* @__PURE__ */ new Map();
|
|
4276
4711
|
const branchStorage = /* @__PURE__ */ new Map();
|
|
4277
4712
|
const captured = /* @__PURE__ */ new Map();
|
|
@@ -4285,17 +4720,22 @@ var createRuntime = (options) => {
|
|
|
4285
4720
|
if (!NAMESPACE_PATTERN.test(key) || !NAMESPACE_PATTERN.test(publicNamespace)) {
|
|
4286
4721
|
throw new RangeError(`namespace must match ${NAMESPACE_PATTERN}: ${JSON.stringify(publicNamespace)}`);
|
|
4287
4722
|
}
|
|
4723
|
+
const createdContextRng = isolatedRng ?? (isolatedNamespaces && key !== DEFAULT_NAMESPACE ? createRng(options.seed ?? 0) : rng);
|
|
4288
4724
|
const created = options.create({
|
|
4289
4725
|
namespace: storageNamespace(key),
|
|
4290
4726
|
publicNamespace,
|
|
4727
|
+
adminPrefix,
|
|
4291
4728
|
sqlite,
|
|
4292
|
-
clock,
|
|
4293
|
-
rng:
|
|
4729
|
+
clock: namespaceClock(publicNamespace),
|
|
4730
|
+
rng: createdContextRng
|
|
4294
4731
|
});
|
|
4295
4732
|
instances.set(key, created);
|
|
4733
|
+
namespaceForStorage.set(key, publicNamespace);
|
|
4296
4734
|
publicNamespaces.add(publicNamespace);
|
|
4297
4735
|
if (isolatedRng)
|
|
4298
4736
|
branchRngs.set(key, isolatedRng);
|
|
4737
|
+
else if (isolatedNamespaces && key !== DEFAULT_NAMESPACE)
|
|
4738
|
+
branchRngs.set(key, createdContextRng);
|
|
4299
4739
|
return created;
|
|
4300
4740
|
};
|
|
4301
4741
|
const instance = (name = DEFAULT_NAMESPACE) => instanceFor(name);
|
|
@@ -4313,7 +4753,7 @@ var createRuntime = (options) => {
|
|
|
4313
4753
|
captured.set(storage, snapshot2);
|
|
4314
4754
|
return Object.freeze({
|
|
4315
4755
|
snapshot: snapshot2,
|
|
4316
|
-
clock: Object.freeze(
|
|
4756
|
+
clock: Object.freeze(namespaceClock(namespaceForStorage.get(storage) ?? storage).state()),
|
|
4317
4757
|
rngState: (branchRngs.get(storage) ?? rng).state()
|
|
4318
4758
|
});
|
|
4319
4759
|
};
|
|
@@ -4323,7 +4763,7 @@ var createRuntime = (options) => {
|
|
|
4323
4763
|
return found;
|
|
4324
4764
|
instance(name);
|
|
4325
4765
|
found = new Timeline({
|
|
4326
|
-
now:
|
|
4766
|
+
now: namespaceClock(name).now,
|
|
4327
4767
|
...options.maxCheckpoints !== void 0 ? { maxCheckpoints: options.maxCheckpoints } : {}
|
|
4328
4768
|
});
|
|
4329
4769
|
found.commit(capture(name));
|
|
@@ -4350,12 +4790,13 @@ var createRuntime = (options) => {
|
|
|
4350
4790
|
const point = history2.checkout("main", at);
|
|
4351
4791
|
restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
|
|
4352
4792
|
captured.set(namespace, point.value.snapshot);
|
|
4353
|
-
rng.setState(point.value.rngState);
|
|
4354
|
-
|
|
4793
|
+
(branchRngs.get(namespace) ?? rng).setState(point.value.rngState);
|
|
4794
|
+
const selectedClock = namespaceClock(namespace);
|
|
4795
|
+
selectedClock.set(point.value.clock.now);
|
|
4355
4796
|
if (point.value.clock.frozen)
|
|
4356
|
-
|
|
4797
|
+
selectedClock.freeze();
|
|
4357
4798
|
else
|
|
4358
|
-
|
|
4799
|
+
selectedClock.unfreeze();
|
|
4359
4800
|
}
|
|
4360
4801
|
return namespace;
|
|
4361
4802
|
}
|
|
@@ -4413,11 +4854,12 @@ var createRuntime = (options) => {
|
|
|
4413
4854
|
const storage = ensureBranch(namespace, branchName);
|
|
4414
4855
|
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
4415
4856
|
captured.set(storage, point.value.snapshot);
|
|
4416
|
-
|
|
4857
|
+
const selectedClock = namespaceClock(namespace);
|
|
4858
|
+
selectedClock.set(point.value.clock.now);
|
|
4417
4859
|
if (point.value.clock.frozen)
|
|
4418
|
-
|
|
4860
|
+
selectedClock.freeze();
|
|
4419
4861
|
else
|
|
4420
|
-
|
|
4862
|
+
selectedClock.unfreeze();
|
|
4421
4863
|
(branchRngs.get(storage) ?? rng).setState(point.value.rngState);
|
|
4422
4864
|
};
|
|
4423
4865
|
const reset = async (name = DEFAULT_NAMESPACE) => {
|
|
@@ -4425,10 +4867,21 @@ var createRuntime = (options) => {
|
|
|
4425
4867
|
options.webhooks?.clear();
|
|
4426
4868
|
for (const each of instances.values())
|
|
4427
4869
|
await each.reset();
|
|
4870
|
+
for (const each of branchRngs.values())
|
|
4871
|
+
each.reset();
|
|
4428
4872
|
timelines.clear();
|
|
4873
|
+
for (const storage of branchStorage.values()) {
|
|
4874
|
+
instances.delete(storage);
|
|
4875
|
+
branchRngs.delete(storage);
|
|
4876
|
+
namespaceForStorage.delete(storage);
|
|
4877
|
+
}
|
|
4429
4878
|
branchStorage.clear();
|
|
4430
|
-
branchRngs.clear();
|
|
4431
4879
|
captured.clear();
|
|
4880
|
+
faults.clear();
|
|
4881
|
+
clock.reset();
|
|
4882
|
+
rng.reset();
|
|
4883
|
+
for (const each of namespaceClocks.values())
|
|
4884
|
+
each.reset();
|
|
4432
4885
|
return;
|
|
4433
4886
|
}
|
|
4434
4887
|
options.webhooks?.clear(name);
|
|
@@ -4446,11 +4899,18 @@ var createRuntime = (options) => {
|
|
|
4446
4899
|
else
|
|
4447
4900
|
clearNamespace(sqlite, storageNamespace(storage));
|
|
4448
4901
|
branchStorage.delete(mapping);
|
|
4902
|
+
instances.delete(storage);
|
|
4903
|
+
namespaceForStorage.delete(storage);
|
|
4449
4904
|
branchRngs.delete(storage);
|
|
4450
4905
|
captured.delete(storage);
|
|
4451
4906
|
}
|
|
4452
4907
|
timelines.delete(name);
|
|
4453
4908
|
captured.delete(name);
|
|
4909
|
+
faults.restore(name, { rules: [], rngState: createRng(options.seed ?? 0).state() });
|
|
4910
|
+
namespaceClock(name).reset();
|
|
4911
|
+
(branchRngs.get(name) ?? rng).reset();
|
|
4912
|
+
journal.clear(name);
|
|
4913
|
+
metrics.reset(name);
|
|
4454
4914
|
};
|
|
4455
4915
|
const snapshot = (name = DEFAULT_NAMESPACE) => {
|
|
4456
4916
|
return checkpoint(name, "main").value.snapshot;
|
|
@@ -4488,6 +4948,53 @@ var createRuntime = (options) => {
|
|
|
4488
4948
|
rng,
|
|
4489
4949
|
credentials,
|
|
4490
4950
|
webhooks: options.webhooks,
|
|
4951
|
+
isolateNamespaces() {
|
|
4952
|
+
if (publicNamespaces.size > 1)
|
|
4953
|
+
throw new Error("enable namespace isolation before creating named namespaces");
|
|
4954
|
+
isolatedNamespaces = true;
|
|
4955
|
+
faults.isolateNamespaces();
|
|
4956
|
+
},
|
|
4957
|
+
namespaceClock,
|
|
4958
|
+
namespaceOf(request) {
|
|
4959
|
+
const url = new URL(request.url);
|
|
4960
|
+
const prefixed = matchNamespacePath(url.pathname, adminPrefix);
|
|
4961
|
+
const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
|
|
4962
|
+
const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
|
|
4963
|
+
if (isAdminPath(path, adminPrefix))
|
|
4964
|
+
return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
|
|
4965
|
+
if (selected !== void 0)
|
|
4966
|
+
return selected;
|
|
4967
|
+
const credential = options.credential?.(request);
|
|
4968
|
+
return (credential !== void 0 ? credentials.get(credential) : void 0) ?? DEFAULT_NAMESPACE;
|
|
4969
|
+
},
|
|
4970
|
+
fleetSnapshot(namespace) {
|
|
4971
|
+
instance(namespace);
|
|
4972
|
+
const handle = {};
|
|
4973
|
+
fleetPoints.set(handle, {
|
|
4974
|
+
namespace,
|
|
4975
|
+
state: capture(namespace),
|
|
4976
|
+
faults: faults.snapshot(namespace),
|
|
4977
|
+
webhooks: options.webhooks?.snapshot(namespace)
|
|
4978
|
+
});
|
|
4979
|
+
return handle;
|
|
4980
|
+
},
|
|
4981
|
+
fleetRestore(handle, namespace) {
|
|
4982
|
+
const point = typeof handle === "object" && handle !== null ? fleetPoints.get(handle) : void 0;
|
|
4983
|
+
if (!point || point.namespace !== namespace)
|
|
4984
|
+
throw new Error("invalid fleet checkpoint");
|
|
4985
|
+
restoreNamespace(sqlite, storageNamespace(namespace), point.state.snapshot);
|
|
4986
|
+
captured.set(namespace, point.state.snapshot);
|
|
4987
|
+
const selectedClock = namespaceClock(namespace);
|
|
4988
|
+
selectedClock.freeze();
|
|
4989
|
+
selectedClock.set(point.state.clock.now);
|
|
4990
|
+
if (!point.state.clock.frozen)
|
|
4991
|
+
selectedClock.unfreeze();
|
|
4992
|
+
(branchRngs.get(namespace) ?? rng).setState(point.state.rngState);
|
|
4993
|
+
faults.restore(namespace, point.faults);
|
|
4994
|
+
if (point.webhooks !== void 0)
|
|
4995
|
+
options.webhooks?.restore(namespace, point.webhooks);
|
|
4996
|
+
timelines.delete(namespace);
|
|
4997
|
+
},
|
|
4491
4998
|
applyPreset: (name, namespace = DEFAULT_NAMESPACE, overrides = {}) => {
|
|
4492
4999
|
const preset = options.presets?.[name];
|
|
4493
5000
|
if (!preset)
|
|
@@ -4519,7 +5026,7 @@ var createRuntime = (options) => {
|
|
|
4519
5026
|
state: (namespace = DEFAULT_NAMESPACE) => inspectState(stateScope(namespace)),
|
|
4520
5027
|
fetch: async (incoming) => {
|
|
4521
5028
|
let request = incoming;
|
|
4522
|
-
const prefixed =
|
|
5029
|
+
const prefixed = matchNamespacePath(new URL(request.url).pathname, adminPrefix);
|
|
4523
5030
|
if (prefixed) {
|
|
4524
5031
|
const url2 = new URL(request.url);
|
|
4525
5032
|
url2.pathname = prefixed[2] ?? "/";
|
|
@@ -4535,7 +5042,15 @@ var createRuntime = (options) => {
|
|
|
4535
5042
|
signal: request.signal
|
|
4536
5043
|
});
|
|
4537
5044
|
}
|
|
4538
|
-
|
|
5045
|
+
const internalUrl = new URL(request.url);
|
|
5046
|
+
const internalPath = isAdminPath(internalUrl.pathname, adminPrefix) ? `${ADMIN_PREFIX}${internalUrl.pathname.slice(adminPrefix.length)}` : "";
|
|
5047
|
+
const internal = internalMatch(internalPath);
|
|
5048
|
+
const obsoleteInternalPath = adminPrefix !== ADMIN_PREFIX && internalMatch(internalUrl.pathname) && !internal;
|
|
5049
|
+
if (internal) {
|
|
5050
|
+
internalUrl.pathname = internalPath;
|
|
5051
|
+
request = new Request(internalUrl, request);
|
|
5052
|
+
}
|
|
5053
|
+
let namespace = internal ? internalUrl.searchParams.get("namespace") ?? control.namespaceOf(request) : control.namespaceOf(request);
|
|
4539
5054
|
if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
4540
5055
|
const credential = options.credential(request);
|
|
4541
5056
|
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
@@ -4555,7 +5070,9 @@ var createRuntime = (options) => {
|
|
|
4555
5070
|
return copy;
|
|
4556
5071
|
}
|
|
4557
5072
|
};
|
|
4558
|
-
|
|
5073
|
+
if (obsoleteInternalPath)
|
|
5074
|
+
return stamp(adminFail(404, "internal transport is only available under adminPrefix"));
|
|
5075
|
+
const handled = internal ? void 0 : await control.handle(request);
|
|
4559
5076
|
if (handled)
|
|
4560
5077
|
return stamp(handled);
|
|
4561
5078
|
const started = monotonicNow();
|
|
@@ -4596,7 +5113,7 @@ var createRuntime = (options) => {
|
|
|
4596
5113
|
type: "mockingbird_admin",
|
|
4597
5114
|
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
4598
5115
|
}
|
|
4599
|
-
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
5116
|
+
}), { status: 400, headers: { "content-type": "application/json; charset=utf-8" } }));
|
|
4600
5117
|
}
|
|
4601
5118
|
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
4602
5119
|
log(400);
|
|
@@ -4663,6 +5180,7 @@ var createRuntime = (options) => {
|
|
|
4663
5180
|
startedAt: wallNow(),
|
|
4664
5181
|
wallNow,
|
|
4665
5182
|
clock,
|
|
5183
|
+
namespaceClock,
|
|
4666
5184
|
faults,
|
|
4667
5185
|
metrics,
|
|
4668
5186
|
journal,
|
|
@@ -4712,15 +5230,16 @@ var createRuntime = (options) => {
|
|
|
4712
5230
|
...options.admin?.(runtime) ?? {},
|
|
4713
5231
|
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
4714
5232
|
// maps `presets`, and a service route must not replace that list with a record.
|
|
4715
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
5233
|
+
...options.presets ? presetRoutes(options.presets, runtime, adminPrefix) : {},
|
|
4716
5234
|
...stateAdminRoutes({
|
|
4717
5235
|
open: stateScope,
|
|
4718
5236
|
afterWrite: (namespace) => {
|
|
4719
5237
|
checkpoint(namespace, "main");
|
|
4720
5238
|
}
|
|
4721
5239
|
}),
|
|
4722
|
-
...adminUiRoutes(options.name, options.adminUi)
|
|
5240
|
+
...adminUiRoutes(options.name, options.adminUi, adminPrefix)
|
|
4723
5241
|
},
|
|
5242
|
+
adminPrefix,
|
|
4724
5243
|
adminKey: options.adminKey
|
|
4725
5244
|
});
|
|
4726
5245
|
return defineMock(runtime);
|
|
@@ -4734,7 +5253,10 @@ var mutableResponse = (response) => {
|
|
|
4734
5253
|
return new Response(response.body, response);
|
|
4735
5254
|
}
|
|
4736
5255
|
};
|
|
4737
|
-
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5256
|
+
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5257
|
+
status,
|
|
5258
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
5259
|
+
});
|
|
4738
5260
|
var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
|
|
4739
5261
|
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4740
5262
|
var credentialRoutes = (registry) => ({
|
|
@@ -4786,7 +5308,7 @@ var credentialRoutes = (registry) => ({
|
|
|
4786
5308
|
return adminJson(200, { status: "ok" });
|
|
4787
5309
|
}
|
|
4788
5310
|
});
|
|
4789
|
-
var presetRoutes = (presets, runtime) => ({
|
|
5311
|
+
var presetRoutes = (presets, runtime, adminPrefix) => ({
|
|
4790
5312
|
"GET /faults/presets": () => {
|
|
4791
5313
|
const body = {
|
|
4792
5314
|
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
@@ -4796,7 +5318,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
4796
5318
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
4797
5319
|
const name = params.name;
|
|
4798
5320
|
if (!presets[name])
|
|
4799
|
-
return adminFail(404, `no fault preset ${name}; GET /
|
|
5321
|
+
return adminFail(404, `no fault preset ${name}; GET ${adminPrefix}/faults/presets`);
|
|
4800
5322
|
const overrides = isObject(body) ? body : {};
|
|
4801
5323
|
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
4802
5324
|
}
|
|
@@ -5044,6 +5566,7 @@ var createRuntime2 = (options = {}) => createRuntime({
|
|
|
5044
5566
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
5045
5567
|
...options.clock ? { clock: options.clock } : {},
|
|
5046
5568
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
5569
|
+
...options.adminPrefix !== void 0 ? { adminPrefix: options.adminPrefix } : {},
|
|
5047
5570
|
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
5048
5571
|
...options.onLog ? { onLog: options.onLog } : {},
|
|
5049
5572
|
credential: apiKeyCredential,
|
|
@@ -5465,6 +5988,9 @@ var PlaneAPI = class {
|
|
|
5465
5988
|
};
|
|
5466
5989
|
|
|
5467
5990
|
export {
|
|
5991
|
+
createClock,
|
|
5992
|
+
resolveAdminPrefix,
|
|
5993
|
+
parseDuration,
|
|
5468
5994
|
document2 as document,
|
|
5469
5995
|
operationIds,
|
|
5470
5996
|
supportedOperationIds,
|
|
@@ -5476,4 +6002,4 @@ export {
|
|
|
5476
6002
|
apiKeyCredential,
|
|
5477
6003
|
PlaneAPI
|
|
5478
6004
|
};
|
|
5479
|
-
//# sourceMappingURL=chunk-
|
|
6005
|
+
//# sourceMappingURL=chunk-C6XLQ2US.js.map
|