@crvouga/mockingbird-service-junction 1.8.2 → 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 +39 -0
- package/DISCOVERY.md +55 -0
- package/README.md +4 -4
- package/SUPPORT.md +50 -0
- package/dist/chunk-7LPCTLIJ.js +1027 -0
- package/dist/chunk-7LPCTLIJ.js.map +7 -0
- package/dist/{chunk-ADSZBAJZ.js → chunk-FZVFIPML.js} +785 -259
- package/dist/chunk-FZVFIPML.js.map +7 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +35 -4
- package/dist/index.js +1 -1
- package/dist/server.d.ts +66 -5
- package/dist/server.js +2 -2
- package/openapi.yaml +3707 -0
- package/package.json +29 -4
- package/dist/chunk-4QHCRJO4.js +0 -540
- package/dist/chunk-4QHCRJO4.js.map +0 -7
- package/dist/chunk-ADSZBAJZ.js.map +0 -7
|
@@ -114,13 +114,31 @@ var Collection = class {
|
|
|
114
114
|
};
|
|
115
115
|
|
|
116
116
|
// ../core/dist/control.js
|
|
117
|
-
var HEALTH_PATH = "/health";
|
|
118
117
|
var ADMIN_PREFIX = "/__admin";
|
|
118
|
+
var resolveAdminPrefix = (prefix = ADMIN_PREFIX) => {
|
|
119
|
+
if (!/^\/[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*$/.test(prefix)) {
|
|
120
|
+
throw new Error("adminPrefix must be an absolute non-root path of literal segments, without a trailing slash");
|
|
121
|
+
}
|
|
122
|
+
return prefix;
|
|
123
|
+
};
|
|
124
|
+
var isAdminPath = (path, prefix) => path === prefix || path.startsWith(`${prefix}/`);
|
|
125
|
+
var matchNamespacePath = (path, prefix) => /^\/ns\/([^/]+)(\/.*)?$/.exec(isAdminPath(path, prefix) ? path.slice(prefix.length) : "");
|
|
126
|
+
var assertAdminPrefixAvailable = (prefix, paths) => {
|
|
127
|
+
const reserved = prefix.split("/").slice(1);
|
|
128
|
+
for (const path of paths) {
|
|
129
|
+
const segments = path.split("/").slice(1);
|
|
130
|
+
if (segments.length < reserved.length || /^\{[^}]+\}$/.test(segments[0] ?? ""))
|
|
131
|
+
continue;
|
|
132
|
+
if (reserved.every((part, i) => segments[i] === part || /^\{[^}]+\}$/.test(segments[i] ?? ""))) {
|
|
133
|
+
throw new Error(`vendor route ${path} collides with adminPrefix ${prefix}; choose a different adminPrefix`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
119
137
|
var ADMIN_KEY_HEADER = "x-mockingbird-admin-key";
|
|
120
138
|
var NAMESPACE_HEADER = "x-mockingbird-namespace";
|
|
121
139
|
var json = (status, body) => new Response(JSON.stringify(body), {
|
|
122
140
|
status,
|
|
123
|
-
headers: { "content-type": "application/json" }
|
|
141
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
124
142
|
});
|
|
125
143
|
var adminError = (status, message) => json(status, { error: { type: "mockingbird_admin", message } });
|
|
126
144
|
var UNITS = {
|
|
@@ -172,11 +190,25 @@ var readJson = async (request2) => {
|
|
|
172
190
|
};
|
|
173
191
|
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
174
192
|
var createControlPlane = (context) => {
|
|
193
|
+
const adminPrefix = resolveAdminPrefix(context.adminPrefix);
|
|
175
194
|
const snapshots = /* @__PURE__ */ new Map();
|
|
176
195
|
let snapshotCounter = 0;
|
|
177
196
|
const headerNamespace = (request2) => request2.headers.get(NAMESPACE_HEADER) ?? context.defaultNamespace;
|
|
178
197
|
const adminNamespace = (request2, url) => url.searchParams.get("namespace") ?? headerNamespace(request2);
|
|
198
|
+
const health = () => {
|
|
199
|
+
return json(200, {
|
|
200
|
+
...context.describe(),
|
|
201
|
+
status: "ok",
|
|
202
|
+
service: context.name,
|
|
203
|
+
uptimeMs: context.wallNow() - context.startedAt,
|
|
204
|
+
clock: context.clock.state(),
|
|
205
|
+
namespaces: context.namespaces().length,
|
|
206
|
+
adminPrefix,
|
|
207
|
+
adminUi: `${adminPrefix}/ui`
|
|
208
|
+
});
|
|
209
|
+
};
|
|
179
210
|
const builtin = {
|
|
211
|
+
"GET /health": health,
|
|
180
212
|
"GET /": () => json(200, {
|
|
181
213
|
service: context.name,
|
|
182
214
|
routes: [...Object.keys(builtin), ...Object.keys(context.routes)].sort()
|
|
@@ -187,29 +219,30 @@ var createControlPlane = (context) => {
|
|
|
187
219
|
return json(200, { status: "ok", reset: target === "*" ? context.namespaces() : [target] });
|
|
188
220
|
},
|
|
189
221
|
"GET /namespaces": () => json(200, { default: context.defaultNamespace, namespaces: context.namespaces() }),
|
|
190
|
-
"GET /clock": () => json(200, context.clock.state()),
|
|
191
|
-
"POST /clock": ({ body }) => {
|
|
222
|
+
"GET /clock": ({ namespace }) => json(200, (context.namespaceClock?.(namespace) ?? context.clock).state()),
|
|
223
|
+
"POST /clock": ({ body, namespace }) => {
|
|
224
|
+
const clock = context.namespaceClock?.(namespace) ?? context.clock;
|
|
192
225
|
if (!isRecord(body))
|
|
193
226
|
return adminError(400, "expected a JSON object");
|
|
194
227
|
if (body.reset === true)
|
|
195
|
-
|
|
228
|
+
clock.reset();
|
|
196
229
|
if (body.freeze === true)
|
|
197
|
-
|
|
230
|
+
clock.freeze();
|
|
198
231
|
if (body.set !== void 0) {
|
|
199
232
|
const instant2 = parseInstant(body.set);
|
|
200
233
|
if (instant2 === void 0)
|
|
201
234
|
return adminError(400, "set: expected epoch ms or ISO-8601");
|
|
202
|
-
|
|
235
|
+
clock.set(instant2);
|
|
203
236
|
}
|
|
204
237
|
if (body.advance !== void 0) {
|
|
205
238
|
const delta = parseDuration(body.advance);
|
|
206
239
|
if (delta === void 0)
|
|
207
240
|
return adminError(400, 'advance: expected ms or "15m"-style');
|
|
208
|
-
|
|
241
|
+
clock.advance(delta);
|
|
209
242
|
}
|
|
210
243
|
if (body.freeze === false)
|
|
211
|
-
|
|
212
|
-
return json(200,
|
|
244
|
+
clock.unfreeze();
|
|
245
|
+
return json(200, clock.state());
|
|
213
246
|
},
|
|
214
247
|
"GET /faults": () => json(200, { faults: context.faults.list() }),
|
|
215
248
|
"POST /faults": ({ body, namespace }) => {
|
|
@@ -342,7 +375,14 @@ var createControlPlane = (context) => {
|
|
|
342
375
|
return json(200, { status: "ok" });
|
|
343
376
|
}
|
|
344
377
|
};
|
|
345
|
-
const
|
|
378
|
+
for (const key of Object.keys(context.routes)) {
|
|
379
|
+
if (Object.hasOwn(builtin, key))
|
|
380
|
+
throw new Error(`admin route ${key} overrides a shared route`);
|
|
381
|
+
const path = key.slice(key.indexOf(" ") + 1);
|
|
382
|
+
if (isAdminPath(path, "/ns") || isAdminPath(path, "/blobs"))
|
|
383
|
+
throw new Error(`admin route ${key} overlaps an internal transport`);
|
|
384
|
+
}
|
|
385
|
+
const routes = [...Object.entries(builtin), ...Object.entries(context.routes)].map(([key, handler]) => {
|
|
346
386
|
const space = key.indexOf(" ");
|
|
347
387
|
return { method: key.slice(0, space), pattern: key.slice(space + 1), handler };
|
|
348
388
|
});
|
|
@@ -350,21 +390,13 @@ var createControlPlane = (context) => {
|
|
|
350
390
|
namespaceOf: headerNamespace,
|
|
351
391
|
async handle(request2) {
|
|
352
392
|
const url = new URL(request2.url);
|
|
353
|
-
if (url.pathname ===
|
|
354
|
-
return
|
|
355
|
-
status: "ok",
|
|
356
|
-
service: context.name,
|
|
357
|
-
uptimeMs: context.wallNow() - context.startedAt,
|
|
358
|
-
clock: context.clock.state(),
|
|
359
|
-
namespaces: context.namespaces().length,
|
|
360
|
-
adminUi: `${ADMIN_PREFIX}/ui`,
|
|
361
|
-
...context.describe()
|
|
362
|
-
});
|
|
393
|
+
if (url.pathname === `${adminPrefix}/health` && request2.method === "GET") {
|
|
394
|
+
return health();
|
|
363
395
|
}
|
|
364
|
-
if (url.pathname
|
|
396
|
+
if (!isAdminPath(url.pathname, adminPrefix)) {
|
|
365
397
|
return void 0;
|
|
366
398
|
}
|
|
367
|
-
const path = url.pathname.slice(
|
|
399
|
+
const path = url.pathname.slice(adminPrefix.length) || "/";
|
|
368
400
|
const shell = request2.method === "GET" && (path === "/ui" || path === "/ui/");
|
|
369
401
|
if (!shell && context.adminKey !== void 0 && request2.headers.get(ADMIN_KEY_HEADER) !== context.adminKey) {
|
|
370
402
|
return adminError(401, `missing or wrong ${ADMIN_KEY_HEADER}`);
|
|
@@ -372,7 +404,12 @@ var createControlPlane = (context) => {
|
|
|
372
404
|
for (const route of routes) {
|
|
373
405
|
if (route.method !== request2.method)
|
|
374
406
|
continue;
|
|
375
|
-
|
|
407
|
+
let params;
|
|
408
|
+
try {
|
|
409
|
+
params = matchRoute(route.pattern, path);
|
|
410
|
+
} catch {
|
|
411
|
+
return adminError(400, "invalid path encoding");
|
|
412
|
+
}
|
|
376
413
|
if (!params)
|
|
377
414
|
continue;
|
|
378
415
|
let body;
|
|
@@ -381,15 +418,18 @@ var createControlPlane = (context) => {
|
|
|
381
418
|
} catch {
|
|
382
419
|
return adminError(400, "request body is not valid JSON");
|
|
383
420
|
}
|
|
421
|
+
const namespace = adminNamespace(request2, url);
|
|
422
|
+
if (!/^[A-Za-z0-9_.-]{1,64}$/.test(namespace))
|
|
423
|
+
return adminError(400, "invalid namespace");
|
|
384
424
|
return route.handler({
|
|
385
425
|
request: request2,
|
|
386
426
|
url,
|
|
387
427
|
params,
|
|
388
|
-
namespace
|
|
428
|
+
namespace,
|
|
389
429
|
body
|
|
390
430
|
});
|
|
391
431
|
}
|
|
392
|
-
return adminError(404, `no admin route ${request2.method} ${path}; GET ${
|
|
432
|
+
return adminError(404, `no admin route ${request2.method} ${path}; GET ${adminPrefix} lists them`);
|
|
393
433
|
}
|
|
394
434
|
};
|
|
395
435
|
};
|
|
@@ -471,7 +511,33 @@ var faultResponse = (rule) => {
|
|
|
471
511
|
};
|
|
472
512
|
var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) => {
|
|
473
513
|
const entries = [];
|
|
514
|
+
let isolated = false;
|
|
515
|
+
const scopedRngs = /* @__PURE__ */ new Map();
|
|
516
|
+
const random = (namespace) => {
|
|
517
|
+
if (!isolated || namespace === "default")
|
|
518
|
+
return rng;
|
|
519
|
+
let scoped = scopedRngs.get(namespace);
|
|
520
|
+
if (!scoped) {
|
|
521
|
+
scoped = createRng(rng.seed);
|
|
522
|
+
scopedRngs.set(namespace, scoped);
|
|
523
|
+
}
|
|
524
|
+
return scoped;
|
|
525
|
+
};
|
|
474
526
|
return {
|
|
527
|
+
isolateNamespaces() {
|
|
528
|
+
isolated = true;
|
|
529
|
+
},
|
|
530
|
+
snapshot(namespace) {
|
|
531
|
+
return {
|
|
532
|
+
rules: structuredClone(entries.map((entry, position) => ({
|
|
533
|
+
...entry.rule,
|
|
534
|
+
remaining: entry.remaining,
|
|
535
|
+
hits: entry.hits,
|
|
536
|
+
position
|
|
537
|
+
})).filter((entry) => entry.namespace === namespace)),
|
|
538
|
+
rngState: random(namespace).state()
|
|
539
|
+
};
|
|
540
|
+
},
|
|
475
541
|
add(rule) {
|
|
476
542
|
const existing = entries.findIndex((e) => e.rule.id === rule.id);
|
|
477
543
|
const entry = { rule, remaining: rule.count ?? null, hits: 0 };
|
|
@@ -491,6 +557,16 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
|
|
|
491
557
|
},
|
|
492
558
|
clear() {
|
|
493
559
|
entries.length = 0;
|
|
560
|
+
rng.reset();
|
|
561
|
+
scopedRngs.clear();
|
|
562
|
+
},
|
|
563
|
+
restore(namespace, state) {
|
|
564
|
+
for (let i = entries.length - 1; i >= 0; i--)
|
|
565
|
+
if (entries[i]?.rule.namespace === namespace)
|
|
566
|
+
entries.splice(i, 1);
|
|
567
|
+
for (const { remaining, hits, position, ...rule } of state.rules)
|
|
568
|
+
entries.splice(Math.min(position, entries.length), 0, { rule, remaining, hits });
|
|
569
|
+
random(namespace).setState(state.rngState);
|
|
494
570
|
},
|
|
495
571
|
async take(candidate) {
|
|
496
572
|
const hits = [];
|
|
@@ -500,7 +576,7 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
|
|
|
500
576
|
if (!matches(entry.rule, candidate))
|
|
501
577
|
continue;
|
|
502
578
|
const rate = entry.rule.rate ?? 1;
|
|
503
|
-
if (
|
|
579
|
+
if (random(candidate.namespace).next() >= rate)
|
|
504
580
|
continue;
|
|
505
581
|
entry.hits++;
|
|
506
582
|
if (entry.remaining !== null)
|
|
@@ -877,7 +953,8 @@ var annotateResponse = (response, extra) => {
|
|
|
877
953
|
var responseNotes = (response) => notes.get(response);
|
|
878
954
|
|
|
879
955
|
// ../core/dist/metrics.js
|
|
880
|
-
var createMetrics = () => {
|
|
956
|
+
var createMetrics = (partition = true) => {
|
|
957
|
+
const namespaces = /* @__PURE__ */ new Map();
|
|
881
958
|
let requests = 0;
|
|
882
959
|
let faults = 0;
|
|
883
960
|
let totalDurationMs = 0;
|
|
@@ -885,6 +962,14 @@ var createMetrics = () => {
|
|
|
885
962
|
const unmatched = /* @__PURE__ */ new Map();
|
|
886
963
|
return {
|
|
887
964
|
record(entry) {
|
|
965
|
+
if (partition) {
|
|
966
|
+
let scoped = namespaces.get(entry.namespace);
|
|
967
|
+
if (!scoped) {
|
|
968
|
+
scoped = createMetrics(false);
|
|
969
|
+
namespaces.set(entry.namespace, scoped);
|
|
970
|
+
}
|
|
971
|
+
scoped.record(entry);
|
|
972
|
+
}
|
|
888
973
|
requests++;
|
|
889
974
|
totalDurationMs += entry.durationMs;
|
|
890
975
|
if (entry.faultId !== void 0)
|
|
@@ -896,7 +981,7 @@ var createMetrics = () => {
|
|
|
896
981
|
unmatched.set(route, (unmatched.get(route) ?? 0) + 1);
|
|
897
982
|
}
|
|
898
983
|
},
|
|
899
|
-
report: () => ({
|
|
984
|
+
report: (namespace) => namespace !== void 0 ? (namespaces.get(namespace) ?? createMetrics(false)).report() : {
|
|
900
985
|
requests,
|
|
901
986
|
byOperation: Object.fromEntries([...byOperation].sort(([a], [b]) => a.localeCompare(b))),
|
|
902
987
|
unmatched: [...unmatched].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([route, count]) => {
|
|
@@ -905,8 +990,13 @@ var createMetrics = () => {
|
|
|
905
990
|
}),
|
|
906
991
|
faults,
|
|
907
992
|
totalDurationMs
|
|
908
|
-
}
|
|
909
|
-
reset() {
|
|
993
|
+
},
|
|
994
|
+
reset(namespace) {
|
|
995
|
+
if (namespace !== void 0) {
|
|
996
|
+
namespaces.delete(namespace);
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
namespaces.clear();
|
|
910
1000
|
requests = 0;
|
|
911
1001
|
faults = 0;
|
|
912
1002
|
totalDurationMs = 0;
|
|
@@ -1519,6 +1609,34 @@ dialog:not([open]) {
|
|
|
1519
1609
|
function isRecord2(value) {
|
|
1520
1610
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1521
1611
|
}
|
|
1612
|
+
function highlightJsonText(text) {
|
|
1613
|
+
const escapeToken = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
1614
|
+
switch (char) {
|
|
1615
|
+
case "&":
|
|
1616
|
+
return "&";
|
|
1617
|
+
case "<":
|
|
1618
|
+
return "<";
|
|
1619
|
+
case ">":
|
|
1620
|
+
return ">";
|
|
1621
|
+
case '"':
|
|
1622
|
+
return """;
|
|
1623
|
+
default:
|
|
1624
|
+
return "'";
|
|
1625
|
+
}
|
|
1626
|
+
});
|
|
1627
|
+
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;
|
|
1628
|
+
let cursor = 0;
|
|
1629
|
+
let html = "";
|
|
1630
|
+
for (const match of text.matchAll(token)) {
|
|
1631
|
+
const index = match.index ?? 0;
|
|
1632
|
+
const value = match[0];
|
|
1633
|
+
html += escapeToken(text.slice(cursor, index));
|
|
1634
|
+
const kind = value.startsWith('"') ? value.trimEnd().endsWith(":") ? "key" : "string" : value === "true" || value === "false" ? "boolean" : value === "null" ? "null" : "number";
|
|
1635
|
+
html += `<span class="json-${kind}">${escapeToken(value)}</span>`;
|
|
1636
|
+
cursor = index + value.length;
|
|
1637
|
+
}
|
|
1638
|
+
return html + escapeToken(text.slice(cursor));
|
|
1639
|
+
}
|
|
1522
1640
|
function mountAdminBrand(doc, host, service, catalog) {
|
|
1523
1641
|
const clip = (value, max) => {
|
|
1524
1642
|
if (typeof value !== "string")
|
|
@@ -1880,14 +1998,16 @@ function bootAdmin(config) {
|
|
|
1880
1998
|
const state = {
|
|
1881
1999
|
namespace: "default",
|
|
1882
2000
|
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
1883
|
-
view: "overview",
|
|
2001
|
+
view: ["overview", "state", "clock", "faults", "journal", "routes"].includes(location.hash.slice(1)) ? location.hash.slice(1) : "overview",
|
|
1884
2002
|
collection: "",
|
|
1885
2003
|
shape: null,
|
|
1886
2004
|
route: null,
|
|
2005
|
+
routes: [],
|
|
1887
2006
|
editing: null
|
|
1888
2007
|
};
|
|
1889
2008
|
const standard = new Set(config.standardRoutes);
|
|
1890
2009
|
const banner = el("banner", HTMLElement);
|
|
2010
|
+
const bannerMessage = el("banner-message", HTMLElement);
|
|
1891
2011
|
const keyInput = el("admin-key", HTMLInputElement);
|
|
1892
2012
|
keyInput.value = state.key;
|
|
1893
2013
|
const params = new URLSearchParams(location.search);
|
|
@@ -1898,18 +2018,21 @@ function bootAdmin(config) {
|
|
|
1898
2018
|
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
1899
2019
|
params.delete("key");
|
|
1900
2020
|
const next = params.toString();
|
|
1901
|
-
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
2021
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : "") + location.hash);
|
|
1902
2022
|
}
|
|
1903
2023
|
const queryNamespace = params.get("namespace");
|
|
1904
2024
|
if (queryNamespace)
|
|
1905
2025
|
state.namespace = queryNamespace;
|
|
1906
2026
|
const showError = (error) => {
|
|
1907
|
-
|
|
2027
|
+
bannerMessage.textContent = error instanceof Error ? error.message : String(error);
|
|
1908
2028
|
banner.classList.add("show");
|
|
2029
|
+
banner.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
1909
2030
|
};
|
|
1910
2031
|
const clearError = () => {
|
|
1911
2032
|
banner.classList.remove("show");
|
|
2033
|
+
bannerMessage.textContent = "";
|
|
1912
2034
|
};
|
|
2035
|
+
el("banner-dismiss", HTMLButtonElement).addEventListener("click", clearError);
|
|
1913
2036
|
const api = async (method, path, body) => {
|
|
1914
2037
|
const headers = { accept: "application/json" };
|
|
1915
2038
|
if (state.key !== "")
|
|
@@ -1953,16 +2076,21 @@ function bootAdmin(config) {
|
|
|
1953
2076
|
namespace: () => state.namespace
|
|
1954
2077
|
};
|
|
1955
2078
|
const isPanelScript = (value) => typeof value === "function";
|
|
2079
|
+
const themeButton = el("theme", HTMLButtonElement);
|
|
1956
2080
|
const setTheme = (theme) => {
|
|
1957
2081
|
if (theme)
|
|
1958
2082
|
document.documentElement.dataset.theme = theme;
|
|
1959
2083
|
else
|
|
1960
2084
|
delete document.documentElement.dataset.theme;
|
|
2085
|
+
const dark = theme ? theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2086
|
+
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>';
|
|
2087
|
+
const label = dark ? "Use light theme" : "Use dark theme";
|
|
2088
|
+
themeButton.setAttribute("aria-label", label);
|
|
2089
|
+
themeButton.title = label;
|
|
1961
2090
|
};
|
|
1962
2091
|
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
2092
|
+
setTheme(storedTheme === "dark" || storedTheme === "light" ? storedTheme : null);
|
|
2093
|
+
themeButton.addEventListener("click", () => {
|
|
1966
2094
|
const current = document.documentElement.dataset.theme;
|
|
1967
2095
|
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
1968
2096
|
const next = dark ? "light" : "dark";
|
|
@@ -1981,6 +2109,7 @@ function bootAdmin(config) {
|
|
|
1981
2109
|
continue;
|
|
1982
2110
|
section.hidden = section.id !== `view-${view}`;
|
|
1983
2111
|
}
|
|
2112
|
+
history.replaceState(null, "", `${location.pathname}${location.search}#${encodeURIComponent(view)}`);
|
|
1984
2113
|
};
|
|
1985
2114
|
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
1986
2115
|
const target = event.target;
|
|
@@ -1995,7 +2124,7 @@ function bootAdmin(config) {
|
|
|
1995
2124
|
show(view);
|
|
1996
2125
|
refresh().catch(showError);
|
|
1997
2126
|
});
|
|
1998
|
-
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>`;
|
|
2127
|
+
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>`;
|
|
1999
2128
|
const table = (columns, rows) => {
|
|
2000
2129
|
if (rows.length === 0)
|
|
2001
2130
|
return '<p class="empty">Nothing here yet.</p>';
|
|
@@ -2005,16 +2134,22 @@ function bootAdmin(config) {
|
|
|
2005
2134
|
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
2006
2135
|
return `<tr${attrs}>${cells}</tr>`;
|
|
2007
2136
|
}).join("");
|
|
2008
|
-
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
2137
|
+
return `<div class="table-shell"><table><thead>${head}</thead><tbody>${body}</tbody></table></div>`;
|
|
2009
2138
|
};
|
|
2010
2139
|
const preview = (value) => {
|
|
2011
2140
|
const text = JSON.stringify(value) ?? "null";
|
|
2012
|
-
|
|
2141
|
+
const clipped = text.length > 180 ? `${text.slice(0, 180)}\u2026` : text;
|
|
2142
|
+
return `<code class="json-inline">${highlightJsonText(clipped)}</code>`;
|
|
2013
2143
|
};
|
|
2014
2144
|
const when = (epoch) => {
|
|
2015
2145
|
const date = new Date(epoch);
|
|
2016
2146
|
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
2017
2147
|
};
|
|
2148
|
+
const journalWhen = (value) => {
|
|
2149
|
+
const numeric = value.trim() === "" ? Number.NaN : Number(value);
|
|
2150
|
+
const epoch = Number.isFinite(numeric) ? numeric : Date.parse(value);
|
|
2151
|
+
return Number.isFinite(epoch) ? when(epoch) : value;
|
|
2152
|
+
};
|
|
2018
2153
|
const clockCard = (epoch) => {
|
|
2019
2154
|
const date = new Date(epoch);
|
|
2020
2155
|
if (Number.isNaN(date.getTime()))
|
|
@@ -2134,7 +2269,7 @@ function bootAdmin(config) {
|
|
|
2134
2269
|
};
|
|
2135
2270
|
const renderOverview = async () => {
|
|
2136
2271
|
const [health, shape, clock, journal] = await Promise.all([
|
|
2137
|
-
originGet(
|
|
2272
|
+
originGet(`${config.adminPrefix}/health`),
|
|
2138
2273
|
api("GET", "/state"),
|
|
2139
2274
|
api("GET", "/clock"),
|
|
2140
2275
|
api("GET", "/requests?limit=8")
|
|
@@ -2151,7 +2286,7 @@ function bootAdmin(config) {
|
|
|
2151
2286
|
},
|
|
2152
2287
|
{
|
|
2153
2288
|
k: "Status",
|
|
2154
|
-
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
2289
|
+
v: `<span class="badge ok">${esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")}</span>`
|
|
2155
2290
|
},
|
|
2156
2291
|
{ k: "Namespace", v: esc(state.namespace) },
|
|
2157
2292
|
{ k: "Collections", v: String(decoded.collections.length) },
|
|
@@ -2238,26 +2373,46 @@ function bootAdmin(config) {
|
|
|
2238
2373
|
const renderJournal = async () => {
|
|
2239
2374
|
const journal = await api("GET", "/requests?limit=100");
|
|
2240
2375
|
el("journal", HTMLElement).innerHTML = table([
|
|
2241
|
-
{ label: "When", cell: (row) => esc(row.at) },
|
|
2242
|
-
{
|
|
2376
|
+
{ label: "When", cell: (row) => esc(journalWhen(row.at)) },
|
|
2377
|
+
{
|
|
2378
|
+
label: "Method",
|
|
2379
|
+
cell: (row) => `<span class="method">${esc(row.method)}</span>`
|
|
2380
|
+
},
|
|
2243
2381
|
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2244
|
-
{
|
|
2382
|
+
{
|
|
2383
|
+
label: "Status",
|
|
2384
|
+
cell: (row) => `<span class="badge ${row.status.startsWith("2") ? "ok" : row.status.startsWith("4") || row.status.startsWith("5") ? "warn" : ""}">${esc(row.status)}</span>`
|
|
2385
|
+
},
|
|
2245
2386
|
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
2246
2387
|
], decodeJournal(journal));
|
|
2247
2388
|
};
|
|
2248
|
-
const
|
|
2249
|
-
const
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2389
|
+
const paintRoutes = () => {
|
|
2390
|
+
const query = el("route-search", HTMLInputElement).value.trim().toLocaleLowerCase();
|
|
2391
|
+
const routes = state.routes.filter((route) => route.toLocaleLowerCase().includes(query));
|
|
2392
|
+
el("route-count", HTMLElement).textContent = `${routes.length} of ${state.routes.length}`;
|
|
2393
|
+
if (state.routes.length === 0) {
|
|
2394
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No admin routes are available.</p>';
|
|
2395
|
+
return;
|
|
2396
|
+
}
|
|
2397
|
+
if (routes.length === 0) {
|
|
2398
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No routes match this filter.</p>';
|
|
2399
|
+
return;
|
|
2400
|
+
}
|
|
2253
2401
|
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2254
2402
|
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2255
2403
|
const parts = route.split(" ");
|
|
2256
2404
|
const method = parts[0] ?? "";
|
|
2257
2405
|
const path = parts.slice(1).join(" ");
|
|
2258
|
-
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>`;
|
|
2406
|
+
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>`;
|
|
2259
2407
|
}).join("");
|
|
2260
2408
|
};
|
|
2409
|
+
const renderRoutes = async () => {
|
|
2410
|
+
const data = await api("GET", "/");
|
|
2411
|
+
if (!isRecord2(data))
|
|
2412
|
+
throw new Error("Route list was not an object");
|
|
2413
|
+
state.routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
2414
|
+
paintRoutes();
|
|
2415
|
+
};
|
|
2261
2416
|
const renderers = {
|
|
2262
2417
|
overview: renderOverview,
|
|
2263
2418
|
state: renderState,
|
|
@@ -2266,19 +2421,50 @@ function bootAdmin(config) {
|
|
|
2266
2421
|
journal: renderJournal,
|
|
2267
2422
|
routes: renderRoutes
|
|
2268
2423
|
};
|
|
2424
|
+
let refreshToken = 0;
|
|
2269
2425
|
async function refresh() {
|
|
2426
|
+
const token = ++refreshToken;
|
|
2270
2427
|
clearError();
|
|
2428
|
+
const main = el("views", HTMLElement);
|
|
2429
|
+
const sync = el("sync-status", HTMLElement);
|
|
2430
|
+
main.setAttribute("aria-busy", "true");
|
|
2431
|
+
sync.classList.add("busy");
|
|
2432
|
+
sync.textContent = "Updating";
|
|
2271
2433
|
const render2 = renderers[state.view];
|
|
2272
|
-
|
|
2273
|
-
|
|
2434
|
+
try {
|
|
2435
|
+
if (render2)
|
|
2436
|
+
await render2();
|
|
2437
|
+
if (token === refreshToken)
|
|
2438
|
+
sync.textContent = "Up to date";
|
|
2439
|
+
} finally {
|
|
2440
|
+
if (token === refreshToken) {
|
|
2441
|
+
main.removeAttribute("aria-busy");
|
|
2442
|
+
sync.classList.remove("busy");
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2274
2445
|
}
|
|
2275
2446
|
const delay = (ms) => new Promise((resolve) => {
|
|
2276
2447
|
window.setTimeout(resolve, ms);
|
|
2277
2448
|
});
|
|
2278
|
-
const confirmAction = async (button, pending, done, work) => {
|
|
2449
|
+
const confirmAction = async (button, pending, done, work, confirmLabel) => {
|
|
2279
2450
|
if (button.dataset.pending === "1")
|
|
2280
2451
|
return;
|
|
2281
2452
|
const label = button.textContent ?? "";
|
|
2453
|
+
if (confirmLabel !== void 0 && button.dataset.confirming !== "1") {
|
|
2454
|
+
button.dataset.confirming = "1";
|
|
2455
|
+
button.classList.add("confirming");
|
|
2456
|
+
button.textContent = confirmLabel;
|
|
2457
|
+
window.setTimeout(() => {
|
|
2458
|
+
if (button.dataset.confirming !== "1")
|
|
2459
|
+
return;
|
|
2460
|
+
button.textContent = label;
|
|
2461
|
+
button.classList.remove("confirming");
|
|
2462
|
+
delete button.dataset.confirming;
|
|
2463
|
+
}, 3e3);
|
|
2464
|
+
return;
|
|
2465
|
+
}
|
|
2466
|
+
button.classList.remove("confirming");
|
|
2467
|
+
delete button.dataset.confirming;
|
|
2282
2468
|
const width = button.offsetWidth;
|
|
2283
2469
|
button.dataset.pending = "1";
|
|
2284
2470
|
button.classList.add("pressed");
|
|
@@ -2299,6 +2485,65 @@ function bootAdmin(config) {
|
|
|
2299
2485
|
delete button.dataset.pending;
|
|
2300
2486
|
}
|
|
2301
2487
|
};
|
|
2488
|
+
const wireJsonEditor = (textareaId, highlightId, statusId, shellId, allowEmpty) => {
|
|
2489
|
+
const textarea = el(textareaId, HTMLTextAreaElement);
|
|
2490
|
+
const highlight = el(highlightId, HTMLElement);
|
|
2491
|
+
const status = el(statusId, HTMLElement);
|
|
2492
|
+
const shell = el(shellId, HTMLElement);
|
|
2493
|
+
const sync = () => {
|
|
2494
|
+
const text = textarea.value;
|
|
2495
|
+
highlight.innerHTML = `${highlightJsonText(text)}
|
|
2496
|
+
`;
|
|
2497
|
+
let valid = true;
|
|
2498
|
+
let message = "Valid JSON";
|
|
2499
|
+
if (allowEmpty && text.trim() === "")
|
|
2500
|
+
message = "No body";
|
|
2501
|
+
else {
|
|
2502
|
+
try {
|
|
2503
|
+
readJson3(text);
|
|
2504
|
+
} catch {
|
|
2505
|
+
valid = false;
|
|
2506
|
+
message = "Invalid JSON";
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
shell.classList.toggle("invalid", !valid);
|
|
2510
|
+
status.classList.toggle("invalid", !valid);
|
|
2511
|
+
status.textContent = message;
|
|
2512
|
+
textarea.setAttribute("aria-invalid", valid ? "false" : "true");
|
|
2513
|
+
};
|
|
2514
|
+
const syncScroll = () => {
|
|
2515
|
+
const preview2 = highlight.parentElement;
|
|
2516
|
+
if (!(preview2 instanceof HTMLElement))
|
|
2517
|
+
return;
|
|
2518
|
+
preview2.scrollTop = textarea.scrollTop;
|
|
2519
|
+
preview2.scrollLeft = textarea.scrollLeft;
|
|
2520
|
+
};
|
|
2521
|
+
textarea.addEventListener("input", sync);
|
|
2522
|
+
textarea.addEventListener("scroll", syncScroll);
|
|
2523
|
+
sync();
|
|
2524
|
+
return sync;
|
|
2525
|
+
};
|
|
2526
|
+
const syncRouteEditor = wireJsonEditor("route-body", "route-highlight", "route-json-status", "route-editor", true);
|
|
2527
|
+
const syncRecordEditor = wireJsonEditor("editor-value", "editor-highlight", "editor-json-status", "record-editor", false);
|
|
2528
|
+
for (const button of document.querySelectorAll("[data-format]")) {
|
|
2529
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2530
|
+
continue;
|
|
2531
|
+
button.addEventListener("click", () => {
|
|
2532
|
+
const target = button.dataset.format;
|
|
2533
|
+
if (target === void 0)
|
|
2534
|
+
return;
|
|
2535
|
+
const textarea = document.getElementById(target);
|
|
2536
|
+
if (!(textarea instanceof HTMLTextAreaElement) || textarea.value.trim() === "")
|
|
2537
|
+
return;
|
|
2538
|
+
try {
|
|
2539
|
+
textarea.value = JSON.stringify(readJson3(textarea.value), null, 2) ?? "";
|
|
2540
|
+
textarea.dispatchEvent(new Event("input"));
|
|
2541
|
+
textarea.focus();
|
|
2542
|
+
} catch {
|
|
2543
|
+
showError(new Error("Fix the JSON before formatting it"));
|
|
2544
|
+
}
|
|
2545
|
+
});
|
|
2546
|
+
}
|
|
2302
2547
|
document.addEventListener("click", (event) => {
|
|
2303
2548
|
const target = event.target;
|
|
2304
2549
|
if (!(target instanceof Element))
|
|
@@ -2371,6 +2616,7 @@ function bootAdmin(config) {
|
|
|
2371
2616
|
} else {
|
|
2372
2617
|
editorValue.value = "{\n \n}";
|
|
2373
2618
|
}
|
|
2619
|
+
syncRecordEditor();
|
|
2374
2620
|
editor.showModal();
|
|
2375
2621
|
}
|
|
2376
2622
|
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -2400,10 +2646,10 @@ function bootAdmin(config) {
|
|
|
2400
2646
|
const id = state.editing;
|
|
2401
2647
|
if (!id)
|
|
2402
2648
|
return;
|
|
2403
|
-
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2649
|
+
confirmAction(editorDelete, "Deleting...", "Deleted", () => api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2404
2650
|
editor.close();
|
|
2405
2651
|
return renderState();
|
|
2406
|
-
}).catch(showError);
|
|
2652
|
+
}), "Confirm delete").catch(showError);
|
|
2407
2653
|
});
|
|
2408
2654
|
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2409
2655
|
if (!(button instanceof HTMLButtonElement))
|
|
@@ -2449,10 +2695,10 @@ function bootAdmin(config) {
|
|
|
2449
2695
|
}).then(() => renderFaults()).catch(showError);
|
|
2450
2696
|
});
|
|
2451
2697
|
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2452
|
-
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
|
|
2698
|
+
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults()), "Confirm clear").catch(showError);
|
|
2453
2699
|
});
|
|
2454
2700
|
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2455
|
-
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
|
|
2701
|
+
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal()), "Confirm clear").catch(showError);
|
|
2456
2702
|
});
|
|
2457
2703
|
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2458
2704
|
const target = event.target;
|
|
@@ -2466,9 +2712,15 @@ function bootAdmin(config) {
|
|
|
2466
2712
|
return;
|
|
2467
2713
|
state.route = route;
|
|
2468
2714
|
el("route-title", HTMLElement).textContent = route;
|
|
2715
|
+
el("route-send", HTMLButtonElement).disabled = false;
|
|
2469
2716
|
const method = route.split(" ")[0] ?? "";
|
|
2470
|
-
el("route-body", HTMLTextAreaElement)
|
|
2717
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
2718
|
+
routeBody.disabled = method === "GET" || method === "DELETE";
|
|
2719
|
+
el("route-editor", HTMLElement).hidden = routeBody.disabled;
|
|
2720
|
+
el("route-json-status", HTMLElement).parentElement?.toggleAttribute("hidden", routeBody.disabled);
|
|
2721
|
+
syncRouteEditor();
|
|
2471
2722
|
});
|
|
2723
|
+
el("route-search", HTMLInputElement).addEventListener("input", paintRoutes);
|
|
2472
2724
|
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2473
2725
|
event.preventDefault();
|
|
2474
2726
|
const send = async () => {
|
|
@@ -2490,7 +2742,8 @@ function bootAdmin(config) {
|
|
|
2490
2742
|
}
|
|
2491
2743
|
}
|
|
2492
2744
|
const data = await api(method, concrete, body);
|
|
2493
|
-
|
|
2745
|
+
const text = JSON.stringify(data, null, 2) ?? "null";
|
|
2746
|
+
el("route-result", HTMLElement).innerHTML = `<code>${highlightJsonText(text)}</code>`;
|
|
2494
2747
|
};
|
|
2495
2748
|
send().catch(showError);
|
|
2496
2749
|
});
|
|
@@ -2608,11 +2861,13 @@ function bootAdmin(config) {
|
|
|
2608
2861
|
views.append(section);
|
|
2609
2862
|
mountSqlExplorer(body, panelApi);
|
|
2610
2863
|
}
|
|
2864
|
+
show(document.getElementById(`view-${state.view}`) === null ? "overview" : state.view);
|
|
2611
2865
|
return refresh();
|
|
2612
2866
|
}).catch(showError);
|
|
2613
2867
|
}
|
|
2614
2868
|
var clientPrelude = [
|
|
2615
2869
|
isRecord2,
|
|
2870
|
+
highlightJsonText,
|
|
2616
2871
|
mountAdminBrand,
|
|
2617
2872
|
startAdminBrand,
|
|
2618
2873
|
faultPresetList,
|
|
@@ -2635,6 +2890,7 @@ ${entry}(${payload});
|
|
|
2635
2890
|
var defineMock = (runtime) => runtime;
|
|
2636
2891
|
var STANDARD_ADMIN_ROUTES = [
|
|
2637
2892
|
"GET /",
|
|
2893
|
+
"GET /health",
|
|
2638
2894
|
"POST /reset",
|
|
2639
2895
|
"GET /namespaces",
|
|
2640
2896
|
"GET /clock",
|
|
@@ -2714,11 +2970,11 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
2714
2970
|
}
|
|
2715
2971
|
});
|
|
2716
2972
|
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
2717
|
-
var renderAdminDocument = (service) => {
|
|
2973
|
+
var renderAdminDocument = (service, adminPrefix = ADMIN_PREFIX) => {
|
|
2718
2974
|
const name = escapeHtml(service);
|
|
2719
2975
|
const script = adminClientSource({
|
|
2720
2976
|
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
2721
|
-
adminPrefix
|
|
2977
|
+
adminPrefix,
|
|
2722
2978
|
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
2723
2979
|
brandsUrl: ADMIN_BRANDS_URL,
|
|
2724
2980
|
service
|
|
@@ -2735,75 +2991,99 @@ var renderAdminDocument = (service) => {
|
|
|
2735
2991
|
${CSS_RESET}
|
|
2736
2992
|
:root {
|
|
2737
2993
|
color-scheme: light;
|
|
2738
|
-
--bg: #
|
|
2994
|
+
--bg: #f7f8fb;
|
|
2739
2995
|
--bg-elev: #ffffff;
|
|
2740
|
-
--bg-muted: #
|
|
2741
|
-
--bg-inset: #
|
|
2742
|
-
--line: #
|
|
2743
|
-
--line-strong: #
|
|
2744
|
-
--ink: #
|
|
2745
|
-
--muted: #
|
|
2746
|
-
--faint: #
|
|
2747
|
-
--accent: #
|
|
2996
|
+
--bg-muted: #f1f3f7;
|
|
2997
|
+
--bg-inset: #f8f9fc;
|
|
2998
|
+
--line: #e4e7ec;
|
|
2999
|
+
--line-strong: #cfd4dc;
|
|
3000
|
+
--ink: #101828;
|
|
3001
|
+
--muted: #475467;
|
|
3002
|
+
--faint: #667085;
|
|
3003
|
+
--accent: #6941c6;
|
|
3004
|
+
--accent-hover: #5933b4;
|
|
2748
3005
|
--accent-ink: #ffffff;
|
|
2749
|
-
--accent-soft: #
|
|
2750
|
-
--ok: #
|
|
2751
|
-
--ok-soft: #
|
|
2752
|
-
--warn: #
|
|
2753
|
-
--warn-soft: #
|
|
2754
|
-
--err: #
|
|
2755
|
-
--err-soft: #
|
|
2756
|
-
--
|
|
3006
|
+
--accent-soft: #f4f0ff;
|
|
3007
|
+
--ok: #067647;
|
|
3008
|
+
--ok-soft: #ecfdf3;
|
|
3009
|
+
--warn: #b54708;
|
|
3010
|
+
--warn-soft: #fffaeb;
|
|
3011
|
+
--err: #b42318;
|
|
3012
|
+
--err-soft: #fef3f2;
|
|
3013
|
+
--code-key: #6941c6;
|
|
3014
|
+
--code-string: #067647;
|
|
3015
|
+
--code-number: #175cd3;
|
|
3016
|
+
--code-boolean: #c11574;
|
|
3017
|
+
--code-null: #667085;
|
|
3018
|
+
--shadow-xs: 0 1px 2px rgb(16 24 40 / 0.05);
|
|
3019
|
+
--shadow-sm: 0 1px 3px rgb(16 24 40 / 0.08), 0 1px 2px rgb(16 24 40 / 0.04);
|
|
3020
|
+
--shadow-lg: 0 18px 48px rgb(16 24 40 / 0.16), 0 4px 12px rgb(16 24 40 / 0.08);
|
|
2757
3021
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2758
3022
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
2759
|
-
--radius:
|
|
2760
|
-
--header:
|
|
2761
|
-
--nav:
|
|
3023
|
+
--radius: 12px;
|
|
3024
|
+
--header: 72px;
|
|
3025
|
+
--nav: 224px;
|
|
2762
3026
|
}
|
|
2763
3027
|
:root[data-theme="dark"] {
|
|
2764
3028
|
color-scheme: dark;
|
|
2765
|
-
--bg: #
|
|
2766
|
-
--bg-elev: #
|
|
2767
|
-
--bg-muted: #
|
|
2768
|
-
--bg-inset: #
|
|
2769
|
-
--line: #
|
|
2770
|
-
--line-strong: #
|
|
2771
|
-
--ink: #
|
|
2772
|
-
--muted: #
|
|
2773
|
-
--faint: #
|
|
2774
|
-
--accent: #
|
|
2775
|
-
--accent-
|
|
2776
|
-
--accent-
|
|
2777
|
-
--
|
|
2778
|
-
--ok
|
|
2779
|
-
--
|
|
2780
|
-
--warn
|
|
2781
|
-
--
|
|
2782
|
-
--err
|
|
2783
|
-
--
|
|
3029
|
+
--bg: #0c0e14;
|
|
3030
|
+
--bg-elev: #151821;
|
|
3031
|
+
--bg-muted: #20232e;
|
|
3032
|
+
--bg-inset: #11141c;
|
|
3033
|
+
--line: #2b303d;
|
|
3034
|
+
--line-strong: #414858;
|
|
3035
|
+
--ink: #f5f7fa;
|
|
3036
|
+
--muted: #b6bdc9;
|
|
3037
|
+
--faint: #8d96a7;
|
|
3038
|
+
--accent: #9e77ed;
|
|
3039
|
+
--accent-hover: #b692f6;
|
|
3040
|
+
--accent-ink: #ffffff;
|
|
3041
|
+
--accent-soft: #2b2142;
|
|
3042
|
+
--ok: #75e0a7;
|
|
3043
|
+
--ok-soft: #102a20;
|
|
3044
|
+
--warn: #fec84b;
|
|
3045
|
+
--warn-soft: #302611;
|
|
3046
|
+
--err: #fda29b;
|
|
3047
|
+
--err-soft: #321817;
|
|
3048
|
+
--code-key: #c3a6ff;
|
|
3049
|
+
--code-string: #75e0a7;
|
|
3050
|
+
--code-number: #84adff;
|
|
3051
|
+
--code-boolean: #f9a8d4;
|
|
3052
|
+
--code-null: #98a2b3;
|
|
3053
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3054
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3055
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2784
3056
|
}
|
|
2785
3057
|
@media (prefers-color-scheme: dark) {
|
|
2786
3058
|
:root:not([data-theme="light"]) {
|
|
2787
3059
|
color-scheme: dark;
|
|
2788
|
-
--bg: #
|
|
2789
|
-
--bg-elev: #
|
|
2790
|
-
--bg-muted: #
|
|
2791
|
-
--bg-inset: #
|
|
2792
|
-
--line: #
|
|
2793
|
-
--line-strong: #
|
|
2794
|
-
--ink: #
|
|
2795
|
-
--muted: #
|
|
2796
|
-
--faint: #
|
|
2797
|
-
--accent: #
|
|
2798
|
-
--accent-
|
|
2799
|
-
--accent-
|
|
2800
|
-
--
|
|
2801
|
-
--ok
|
|
2802
|
-
--
|
|
2803
|
-
--warn
|
|
2804
|
-
--
|
|
2805
|
-
--err
|
|
2806
|
-
--
|
|
3060
|
+
--bg: #0c0e14;
|
|
3061
|
+
--bg-elev: #151821;
|
|
3062
|
+
--bg-muted: #20232e;
|
|
3063
|
+
--bg-inset: #11141c;
|
|
3064
|
+
--line: #2b303d;
|
|
3065
|
+
--line-strong: #414858;
|
|
3066
|
+
--ink: #f5f7fa;
|
|
3067
|
+
--muted: #b6bdc9;
|
|
3068
|
+
--faint: #8d96a7;
|
|
3069
|
+
--accent: #9e77ed;
|
|
3070
|
+
--accent-hover: #b692f6;
|
|
3071
|
+
--accent-ink: #ffffff;
|
|
3072
|
+
--accent-soft: #2b2142;
|
|
3073
|
+
--ok: #75e0a7;
|
|
3074
|
+
--ok-soft: #102a20;
|
|
3075
|
+
--warn: #fec84b;
|
|
3076
|
+
--warn-soft: #302611;
|
|
3077
|
+
--err: #fda29b;
|
|
3078
|
+
--err-soft: #321817;
|
|
3079
|
+
--code-key: #c3a6ff;
|
|
3080
|
+
--code-string: #75e0a7;
|
|
3081
|
+
--code-number: #84adff;
|
|
3082
|
+
--code-boolean: #f9a8d4;
|
|
3083
|
+
--code-null: #98a2b3;
|
|
3084
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3085
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3086
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2807
3087
|
}
|
|
2808
3088
|
}
|
|
2809
3089
|
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
@@ -2821,13 +3101,17 @@ var renderAdminDocument = (service) => {
|
|
|
2821
3101
|
padding-right: env(safe-area-inset-right);
|
|
2822
3102
|
-webkit-text-size-adjust: 100%;
|
|
2823
3103
|
}
|
|
2824
|
-
|
|
3104
|
+
::selection { background: color-mix(in srgb, var(--accent) 28%, transparent); }
|
|
3105
|
+
:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px; }
|
|
3106
|
+
button, input, select, textarea { transition: border-color 140ms ease, box-shadow 140ms ease, transform 100ms ease; }
|
|
3107
|
+
button:disabled, input:disabled, select:disabled, textarea:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
2825
3108
|
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
2826
3109
|
header.top {
|
|
2827
|
-
position: sticky; top: 0; z-index:
|
|
3110
|
+
position: sticky; top: 0; z-index: 20;
|
|
2828
3111
|
display: flex; align-items: center; gap: 12px;
|
|
2829
|
-
min-height: var(--header); padding:
|
|
2830
|
-
background: var(--bg-elev);
|
|
3112
|
+
min-height: var(--header); padding: 12px 20px;
|
|
3113
|
+
background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
|
|
3114
|
+
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
2831
3115
|
border-bottom: 1px solid var(--line);
|
|
2832
3116
|
min-width: 0; max-width: 100%;
|
|
2833
3117
|
}
|
|
@@ -2835,12 +3119,21 @@ var renderAdminDocument = (service) => {
|
|
|
2835
3119
|
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
2836
3120
|
min-width: 0; max-width: 100%; margin-right: auto;
|
|
2837
3121
|
}
|
|
2838
|
-
.brand-
|
|
2839
|
-
.brand-
|
|
3122
|
+
.brand-lockup { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
3123
|
+
.brand-mark {
|
|
3124
|
+
width: 38px; height: 38px; display: grid; place-items: center; flex: none;
|
|
3125
|
+
border-radius: 11px; color: #fff;
|
|
3126
|
+
background: linear-gradient(145deg, #7f56d9 0%, #53389e 100%);
|
|
3127
|
+
box-shadow: 0 6px 16px rgb(105 65 198 / 0.24), inset 0 1px 0 rgb(255 255 255 / 0.25);
|
|
3128
|
+
}
|
|
3129
|
+
.brand-mark svg { width: 22px; height: 22px; }
|
|
3130
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; line-height: 1.25; }
|
|
3131
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
2840
3132
|
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
2841
3133
|
.vendor {
|
|
2842
3134
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
2843
3135
|
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
3136
|
+
padding: 5px 8px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-inset);
|
|
2844
3137
|
}
|
|
2845
3138
|
.vendor[hidden] { display: none; }
|
|
2846
3139
|
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
@@ -2852,12 +3145,17 @@ var renderAdminDocument = (service) => {
|
|
|
2852
3145
|
.vendor-sep { color: var(--faint); }
|
|
2853
3146
|
.vendor-link { color: inherit; text-decoration: none; }
|
|
2854
3147
|
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
2855
|
-
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items:
|
|
2856
|
-
|
|
3148
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
|
|
3149
|
+
.sync-status { align-self: center; display: flex; align-items: center; gap: 6px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3150
|
+
.sync-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
|
|
3151
|
+
.sync-status.busy::before { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); animation: pulse 1.2s ease-in-out infinite; }
|
|
3152
|
+
label.field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; font-weight: 600; color: var(--faint); }
|
|
2857
3153
|
label.field input, label.field select, .controls input[type="password"] {
|
|
2858
|
-
background-color: var(--bg-
|
|
2859
|
-
padding:
|
|
3154
|
+
background-color: var(--bg-elev); border: 1px solid var(--line-strong); border-radius: 9px;
|
|
3155
|
+
padding: 8px 10px; min-height: 38px; line-height: 1.2; box-shadow: var(--shadow-xs);
|
|
2860
3156
|
}
|
|
3157
|
+
label.field input:hover, label.field select:hover, textarea:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--line-strong)); }
|
|
3158
|
+
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; }
|
|
2861
3159
|
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
2862
3160
|
the text fields in the same form. Paint it like those fields. */
|
|
2863
3161
|
label.field select {
|
|
@@ -2874,12 +3172,20 @@ var renderAdminDocument = (service) => {
|
|
|
2874
3172
|
dialog .field > textarea { width: 100%; }
|
|
2875
3173
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
2876
3174
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
.btn
|
|
3175
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
|
3176
|
+
border-radius: 9px; min-height: 38px; padding: 0 13px; border: 1px solid var(--line-strong);
|
|
3177
|
+
background: var(--bg-elev); color: var(--ink); font-size: 13px; font-weight: 600; line-height: 1;
|
|
3178
|
+
box-shadow: var(--shadow-xs);
|
|
3179
|
+
}
|
|
3180
|
+
.icon-btn { width: 38px; padding: 0; }
|
|
3181
|
+
.icon-btn svg, .btn svg, .btn-primary svg { width: 16px; height: 16px; }
|
|
3182
|
+
.icon-btn:hover, .btn:hover { background: var(--bg-muted); border-color: var(--line-strong); }
|
|
3183
|
+
.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; }
|
|
3184
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
3185
|
+
.btn-quiet { background: transparent; border-color: transparent; box-shadow: none; }
|
|
3186
|
+
.btn-quiet:hover { background: var(--bg-muted); }
|
|
3187
|
+
.btn-danger { color: var(--err); border-color: color-mix(in srgb, var(--err) 25%, var(--line)); }
|
|
3188
|
+
.btn-danger:hover, .btn-danger.confirming { background: var(--err-soft); border-color: color-mix(in srgb, var(--err) 45%, var(--line)); }
|
|
2883
3189
|
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
2884
3190
|
so the press itself has to stay visible. */
|
|
2885
3191
|
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
@@ -2895,93 +3201,159 @@ var renderAdminDocument = (service) => {
|
|
|
2895
3201
|
flex: 1 0 auto; min-width: 0;
|
|
2896
3202
|
}
|
|
2897
3203
|
nav.side {
|
|
2898
|
-
|
|
3204
|
+
position: sticky; top: var(--header); align-self: start; height: calc(100vh - var(--header));
|
|
3205
|
+
border-right: 1px solid var(--line); padding: 18px 12px;
|
|
2899
3206
|
display: flex; flex-direction: column; gap: 4px;
|
|
2900
|
-
background: var(--bg-elev);
|
|
3207
|
+
background: var(--bg-elev); overflow-y: auto;
|
|
2901
3208
|
}
|
|
3209
|
+
.nav-label { padding: 0 10px 8px; color: var(--faint); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
|
|
2902
3210
|
nav.side button {
|
|
3211
|
+
position: relative; display: flex; align-items: center; gap: 10px; width: 100%;
|
|
2903
3212
|
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
2904
|
-
padding: 9px 10px; min-height:
|
|
2905
|
-
}
|
|
2906
|
-
nav.side button
|
|
2907
|
-
|
|
2908
|
-
.
|
|
2909
|
-
.
|
|
2910
|
-
.
|
|
3213
|
+
padding: 9px 10px; min-height: 42px; color: var(--muted); font-size: 13px; font-weight: 550;
|
|
3214
|
+
}
|
|
3215
|
+
nav.side button:hover { background: var(--bg-muted); color: var(--ink); }
|
|
3216
|
+
nav.side button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 650; }
|
|
3217
|
+
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); }
|
|
3218
|
+
.nav-icon { width: 18px; height: 18px; display: grid; place-items: center; flex: none; }
|
|
3219
|
+
.nav-icon svg { width: 18px; height: 18px; }
|
|
3220
|
+
main { padding: 32px clamp(20px, 4vw, 48px) 64px; max-width: 1320px; width: 100%; min-width: 0; }
|
|
3221
|
+
main[aria-busy="true"] .view { opacity: 0.62; }
|
|
3222
|
+
.view { animation: view-in 180ms ease-out; }
|
|
3223
|
+
.view[hidden] { display: none; }
|
|
3224
|
+
.view-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
|
|
3225
|
+
.view h2 { margin: 0 0 5px; font-size: clamp(24px, 3vw, 30px); line-height: 1.2; letter-spacing: -0.035em; }
|
|
3226
|
+
.view h3 { margin: 24px 0 10px; font-size: 15px; letter-spacing: -0.01em; }
|
|
3227
|
+
.lede { margin: 0; color: var(--muted); max-width: 72ch; font-size: 14px; }
|
|
3228
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 175px), 1fr)); gap: 14px; }
|
|
2911
3229
|
.card {
|
|
2912
3230
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
2913
|
-
padding:
|
|
3231
|
+
padding: 18px; box-shadow: var(--shadow-sm);
|
|
2914
3232
|
}
|
|
2915
|
-
.card
|
|
2916
|
-
.card
|
|
3233
|
+
.stat-card { position: relative; overflow: hidden; }
|
|
3234
|
+
.stat-card::after { content: ""; position: absolute; inset: 0 0 auto; height: 2px; background: linear-gradient(90deg, var(--accent), transparent 78%); opacity: 0.7; }
|
|
3235
|
+
.card .k { color: var(--faint); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
3236
|
+
.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; }
|
|
2917
3237
|
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
2918
|
-
.
|
|
3238
|
+
.section-card { background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-xs); overflow: hidden; }
|
|
3239
|
+
.section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
|
|
3240
|
+
.section-title h3 { margin: 0; }
|
|
3241
|
+
.split { display: grid; grid-template-columns: minmax(0, 250px) minmax(0, 1fr); gap: 18px; align-items: start; }
|
|
3242
|
+
#collections { padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); box-shadow: var(--shadow-xs); }
|
|
2919
3243
|
.collection {
|
|
2920
3244
|
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
2921
|
-
border-radius:
|
|
3245
|
+
border-radius: 9px; padding: 10px 11px; min-height: 48px; color: var(--ink); cursor: pointer;
|
|
2922
3246
|
}
|
|
2923
|
-
.collection
|
|
3247
|
+
.collection:hover { background: var(--bg-muted); }
|
|
3248
|
+
.collection[aria-current="true"] { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 18%, var(--line)); color: var(--accent); }
|
|
2924
3249
|
.collection small { display: block; color: var(--faint); }
|
|
2925
3250
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
2926
|
-
.chip {
|
|
2927
|
-
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
2928
|
-
border-radius: 999px; padding: 4px 8px;
|
|
2929
|
-
}
|
|
3251
|
+
.chip { font-family: var(--mono); font-size: 11px; background: var(--bg-muted); border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; }
|
|
2930
3252
|
.chip i { color: var(--faint); font-style: normal; }
|
|
2931
3253
|
.sql-layout {
|
|
2932
3254
|
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
2933
3255
|
}
|
|
2934
|
-
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
2935
|
-
.sql-tables button { text-align: left; }
|
|
3256
|
+
.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); }
|
|
3257
|
+
.sql-tables button { text-align: left; border: 0; border-radius: 8px; padding: 9px 10px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
3258
|
+
.sql-tables button:hover, .sql-tables button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); }
|
|
2936
3259
|
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
2937
|
-
table { width: 100%; border-
|
|
2938
|
-
|
|
2939
|
-
th {
|
|
3260
|
+
.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); }
|
|
3261
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
3262
|
+
th, td { text-align: left; padding: 11px 13px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
3263
|
+
th { color: var(--faint); background: var(--bg-inset); font-weight: 650; font-size: 11px; text-transform: uppercase; letter-spacing: 0.045em; }
|
|
3264
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
2940
3265
|
tr.clickable { cursor: pointer; }
|
|
2941
|
-
tr.clickable:hover { background: var(--
|
|
3266
|
+
tr.clickable:hover { background: var(--accent-soft); }
|
|
2942
3267
|
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
2943
3268
|
pre {
|
|
2944
3269
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
2945
|
-
background: var(--bg-inset); border-radius: 10px; padding:
|
|
2946
|
-
}
|
|
2947
|
-
.
|
|
3270
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 10px; padding: 14px; font-size: 12px; line-height: 1.65;
|
|
3271
|
+
}
|
|
3272
|
+
.json-key { color: var(--code-key); }
|
|
3273
|
+
.json-string { color: var(--code-string); }
|
|
3274
|
+
.json-number { color: var(--code-number); }
|
|
3275
|
+
.json-boolean { color: var(--code-boolean); }
|
|
3276
|
+
.json-null { color: var(--code-null); font-style: italic; }
|
|
3277
|
+
.json-inline { display: block; max-width: 44rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
3278
|
+
.json-view { position: relative; max-height: 26rem; overflow: auto; }
|
|
3279
|
+
.json-editor { position: relative; min-height: 190px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--bg-inset); overflow: hidden; }
|
|
3280
|
+
.json-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); }
|
|
3281
|
+
.json-editor.invalid { border-color: var(--err); }
|
|
3282
|
+
.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; }
|
|
3283
|
+
.json-editor pre { position: absolute; inset: 0; pointer-events: none; background: transparent; }
|
|
3284
|
+
.json-editor textarea { position: relative; z-index: 1; resize: vertical; background: transparent; color: transparent; caret-color: var(--ink); -webkit-text-fill-color: transparent; }
|
|
3285
|
+
.json-editor textarea::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); }
|
|
3286
|
+
.editor-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; }
|
|
3287
|
+
.json-status { display: inline-flex; align-items: center; gap: 6px; color: var(--ok); font-size: 11px; }
|
|
3288
|
+
.json-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
3289
|
+
.json-status.invalid { color: var(--err); }
|
|
3290
|
+
.format-json { border: 0; padding: 3px 5px; background: transparent; color: var(--accent); font-size: 11px; font-weight: 650; cursor: pointer; }
|
|
3291
|
+
.row-actions { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 14px 0; }
|
|
2948
3292
|
.banner {
|
|
2949
|
-
display: none;
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
3293
|
+
display: none; position: sticky; top: calc(var(--header) + 12px); z-index: 15;
|
|
3294
|
+
align-items: center; gap: 10px; margin: 0 0 16px; padding: 11px 12px 11px 14px;
|
|
3295
|
+
border: 1px solid color-mix(in srgb, var(--err) 30%, var(--line)); border-radius: 10px;
|
|
3296
|
+
background: var(--err-soft); color: var(--err); box-shadow: var(--shadow-sm); font-size: 13px;
|
|
3297
|
+
}
|
|
3298
|
+
.banner.show { display: flex; }
|
|
3299
|
+
.banner span { flex: 1; }
|
|
3300
|
+
.banner button { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 7px; background: transparent; color: currentColor; cursor: pointer; }
|
|
3301
|
+
.banner button:hover { background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
3302
|
+
.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); }
|
|
2954
3303
|
dialog {
|
|
2955
|
-
border: 1px solid var(--line); border-radius:
|
|
2956
|
-
color: var(--ink); width: min(
|
|
2957
|
-
}
|
|
2958
|
-
dialog
|
|
2959
|
-
dialog
|
|
3304
|
+
border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--bg-elev);
|
|
3305
|
+
color: var(--ink); width: min(720px, calc(100% - 24px)); max-width: calc(100vw - 24px); box-shadow: var(--shadow-lg);
|
|
3306
|
+
}
|
|
3307
|
+
dialog[open] { animation: dialog-in 160ms ease-out; }
|
|
3308
|
+
dialog::backdrop { background: rgb(12 14 20 / 0.58); backdrop-filter: blur(3px); }
|
|
3309
|
+
dialog form, .dialog-body { padding: 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
3310
|
+
.dialog-heading { display: flex; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
|
3311
|
+
.dialog-heading strong { font-size: 18px; letter-spacing: -0.02em; }
|
|
3312
|
+
.dialog-heading span { color: var(--muted); font-size: 12px; }
|
|
2960
3313
|
textarea {
|
|
2961
3314
|
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
2962
3315
|
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
2963
3316
|
font-family: var(--mono); font-size: 12px;
|
|
2964
3317
|
}
|
|
2965
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
2966
|
-
.
|
|
3318
|
+
.dialog-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding-top: 4px; }
|
|
3319
|
+
.dialog-actions .btn-danger { margin-right: auto; }
|
|
3320
|
+
.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); }
|
|
2967
3321
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
2968
3322
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
2969
3323
|
.status-2 { color: var(--ok); }
|
|
2970
3324
|
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
2971
3325
|
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
3326
|
+
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
3327
|
+
.form-grid .wide { grid-column: span 2; }
|
|
2972
3328
|
.route {
|
|
2973
|
-
display: grid; grid-template-columns:
|
|
2974
|
-
padding:
|
|
2975
|
-
}
|
|
2976
|
-
.
|
|
3329
|
+
display: grid; grid-template-columns: 84px minmax(0, 1fr) auto; gap: 10px; align-items: center;
|
|
3330
|
+
padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--bg-elev);
|
|
3331
|
+
}
|
|
3332
|
+
.route:first-child { border-radius: var(--radius) var(--radius) 0 0; }
|
|
3333
|
+
.route:last-child { border-bottom: 0; border-radius: 0 0 var(--radius) var(--radius); }
|
|
3334
|
+
.route-tools { display: flex; align-items: flex-end; gap: 12px; margin-bottom: 12px; }
|
|
3335
|
+
.route-tools .field { flex: 1; }
|
|
3336
|
+
.route-tools input { width: 100%; }
|
|
3337
|
+
.route-count { padding-bottom: 10px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3338
|
+
#route-list { max-height: 34rem; border: 1px solid var(--line); border-radius: var(--radius); overflow: auto; box-shadow: var(--shadow-xs); }
|
|
3339
|
+
.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; }
|
|
3340
|
+
@keyframes view-in { from { transform: translateY(3px); } to { transform: translateY(0); } }
|
|
3341
|
+
@keyframes dialog-in { from { transform: translateY(8px) scale(0.99); } to { transform: translateY(0) scale(1); } }
|
|
3342
|
+
@keyframes pulse { 50% { opacity: 0.45; } }
|
|
3343
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
|
|
3344
|
+
@media (forced-colors: active) { .json-editor pre { display: none; } .json-editor textarea { color: CanvasText; -webkit-text-fill-color: CanvasText; } }
|
|
2977
3345
|
@media (max-width: 800px) {
|
|
3346
|
+
:root { --header: 0px; }
|
|
2978
3347
|
header.top {
|
|
2979
|
-
align-items: stretch; flex-direction: column;
|
|
2980
|
-
padding
|
|
3348
|
+
position: relative; align-items: stretch; flex-direction: column;
|
|
3349
|
+
padding: max(12px, env(safe-area-inset-top)) 14px 12px;
|
|
2981
3350
|
}
|
|
2982
3351
|
input, select, textarea { font-size: 16px; }
|
|
2983
3352
|
.brand, .vendor, .controls { width: 100%; }
|
|
3353
|
+
.brand { gap: 10px; }
|
|
3354
|
+
.vendor { order: 2; border-radius: 9px; }
|
|
2984
3355
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
3356
|
+
.sync-status { display: none; }
|
|
2985
3357
|
/* The layout fills the screen. A single-column grid stretches every auto
|
|
2986
3358
|
row, so the tab bar grew with the leftover height and the buttons floated
|
|
2987
3359
|
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
@@ -2991,14 +3363,23 @@ var renderAdminDocument = (service) => {
|
|
|
2991
3363
|
align-content: start;
|
|
2992
3364
|
}
|
|
2993
3365
|
nav.side {
|
|
2994
|
-
align-self: start; height: auto;
|
|
3366
|
+
position: sticky; top: 0; z-index: 12; align-self: start; height: auto;
|
|
2995
3367
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
2996
3368
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
2997
|
-
padding: 8px;
|
|
3369
|
+
padding: 8px; box-shadow: var(--shadow-xs);
|
|
2998
3370
|
}
|
|
2999
|
-
nav
|
|
3371
|
+
.nav-label { display: none; }
|
|
3372
|
+
nav.side button { width: auto; white-space: nowrap; flex: none; }
|
|
3373
|
+
nav.side button[aria-current="true"]::before { inset: auto 9px -8px; width: auto; height: 3px; border-radius: 3px 3px 0 0; }
|
|
3000
3374
|
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
3001
|
-
|
|
3375
|
+
.form-grid { grid-template-columns: minmax(0, 1fr); }
|
|
3376
|
+
.form-grid .wide { grid-column: auto; }
|
|
3377
|
+
main { padding: 22px 14px 48px; }
|
|
3378
|
+
.view-head { margin-bottom: 18px; }
|
|
3379
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
3380
|
+
.card { padding: 14px; }
|
|
3381
|
+
.card .v { font-size: 20px; }
|
|
3382
|
+
.table-shell { border: 0; background: transparent; box-shadow: none; overflow: visible; }
|
|
3002
3383
|
table, thead, tbody, tr, th, td { display: block; }
|
|
3003
3384
|
thead { display: none; }
|
|
3004
3385
|
tr {
|
|
@@ -3013,50 +3394,63 @@ var renderAdminDocument = (service) => {
|
|
|
3013
3394
|
.route { grid-template-columns: 72px 1fr; }
|
|
3014
3395
|
.route button { grid-column: 1 / -1; }
|
|
3015
3396
|
}
|
|
3397
|
+
@media (max-width: 460px) {
|
|
3398
|
+
.controls label.field { min-width: calc(50% - 4px); }
|
|
3399
|
+
.controls .icon-btn, .controls .btn { flex: 1; }
|
|
3400
|
+
.grid { grid-template-columns: minmax(0, 1fr); }
|
|
3401
|
+
.dialog-actions { flex-wrap: wrap; }
|
|
3402
|
+
.dialog-actions button { flex: 1; }
|
|
3403
|
+
.dialog-actions .btn-danger { flex-basis: 100%; margin-right: 0; }
|
|
3404
|
+
}
|
|
3016
3405
|
</style>
|
|
3017
3406
|
</head>
|
|
3018
3407
|
<body>
|
|
3019
3408
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
3020
3409
|
<header class="top">
|
|
3021
3410
|
<div class="brand">
|
|
3022
|
-
<div class="brand-
|
|
3023
|
-
<
|
|
3024
|
-
|
|
3411
|
+
<div class="brand-lockup">
|
|
3412
|
+
<div class="brand-mark" aria-hidden="true">
|
|
3413
|
+
<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>
|
|
3414
|
+
</div>
|
|
3415
|
+
<div class="brand-id">
|
|
3416
|
+
<strong>${name}</strong>
|
|
3417
|
+
<span>Mock admin</span>
|
|
3418
|
+
</div>
|
|
3025
3419
|
</div>
|
|
3026
3420
|
<div class="vendor" id="vendor" hidden></div>
|
|
3027
3421
|
</div>
|
|
3028
3422
|
<div class="controls">
|
|
3423
|
+
<span class="sync-status" id="sync-status" role="status" aria-live="polite">Ready</span>
|
|
3029
3424
|
<label class="field">Namespace
|
|
3030
3425
|
<select id="namespace" aria-label="Namespace"></select>
|
|
3031
3426
|
</label>
|
|
3032
3427
|
<label class="field">Admin key
|
|
3033
3428
|
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
3034
3429
|
</label>
|
|
3035
|
-
<button class="icon-btn" id="theme" type="button" aria-label="
|
|
3036
|
-
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
3430
|
+
<button class="icon-btn" id="theme" type="button" aria-label="Use dark theme" title="Use dark theme"></button>
|
|
3431
|
+
<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>
|
|
3037
3432
|
</div>
|
|
3038
3433
|
</header>
|
|
3039
3434
|
<div class="layout">
|
|
3040
3435
|
<nav class="side" id="nav" aria-label="Admin sections">
|
|
3041
|
-
<
|
|
3042
|
-
<button type="button" data-view="
|
|
3043
|
-
<button type="button" data-view="
|
|
3044
|
-
<button type="button" data-view="
|
|
3045
|
-
<button type="button" data-view="
|
|
3046
|
-
<button type="button" data-view="
|
|
3436
|
+
<span class="nav-label">Workspace</span>
|
|
3437
|
+
<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>
|
|
3438
|
+
<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>
|
|
3439
|
+
<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>
|
|
3440
|
+
<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>
|
|
3441
|
+
<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>
|
|
3442
|
+
<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>
|
|
3047
3443
|
</nav>
|
|
3048
3444
|
<main id="views">
|
|
3049
|
-
<
|
|
3445
|
+
<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>
|
|
3050
3446
|
<section class="view" id="view-overview">
|
|
3051
|
-
<h2>Overview</h2>
|
|
3052
|
-
<p class="lede">Health, clock, and the collections this namespace is holding.</p>
|
|
3447
|
+
<div class="view-head"><div><h2>Overview</h2><p class="lede">Health, clock, and the collections this namespace is holding.</p></div></div>
|
|
3053
3448
|
<div class="grid" id="overview-cards"></div>
|
|
3054
3449
|
<h3>Recent requests</h3>
|
|
3055
3450
|
<div id="overview-requests"></div>
|
|
3056
3451
|
</section>
|
|
3057
3452
|
<section class="view" id="view-state" hidden>
|
|
3058
|
-
<h2>State</h2>
|
|
3059
|
-
<p class="lede">Every collection in this namespace. Declared fields stay visible before any row exists; stored rows add whatever else they contain.</p>
|
|
3453
|
+
<div class="view-head"><div><h2>State</h2><p class="lede">Inspect and edit every collection in this namespace.</p></div></div>
|
|
3060
3454
|
<div class="split">
|
|
3061
3455
|
<div id="collections"></div>
|
|
3062
3456
|
<div>
|
|
@@ -3069,8 +3463,7 @@ var renderAdminDocument = (service) => {
|
|
|
3069
3463
|
</div>
|
|
3070
3464
|
</section>
|
|
3071
3465
|
<section class="view" id="view-clock" hidden>
|
|
3072
|
-
<h2>Clock</h2>
|
|
3073
|
-
<p class="lede">The clock every timestamp in this mock reads.</p>
|
|
3466
|
+
<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>
|
|
3074
3467
|
<div class="card" id="clock-readout"></div>
|
|
3075
3468
|
<div class="row-actions">
|
|
3076
3469
|
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
@@ -3089,21 +3482,22 @@ var renderAdminDocument = (service) => {
|
|
|
3089
3482
|
</form>
|
|
3090
3483
|
</section>
|
|
3091
3484
|
<section class="view" id="view-faults" hidden>
|
|
3092
|
-
<h2>Faults</h2>
|
|
3093
|
-
<p class="lede">Inject a response, a delay, or a named preset. Rules apply to this namespace.</p>
|
|
3485
|
+
<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>
|
|
3094
3486
|
<form id="fault-form" class="card stack">
|
|
3095
|
-
<
|
|
3096
|
-
<
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
<
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
<
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
<
|
|
3106
|
-
|
|
3487
|
+
<div class="form-grid">
|
|
3488
|
+
<label class="field wide">Preset
|
|
3489
|
+
<select id="fault-preset"><option value="">Custom rule</option></select>
|
|
3490
|
+
</label>
|
|
3491
|
+
<label class="field">Status
|
|
3492
|
+
<input id="fault-status" inputmode="numeric" placeholder="503">
|
|
3493
|
+
</label>
|
|
3494
|
+
<label class="field wide">Path prefix
|
|
3495
|
+
<input id="fault-path" placeholder="/v1">
|
|
3496
|
+
</label>
|
|
3497
|
+
<label class="field">Method
|
|
3498
|
+
<input id="fault-method" placeholder="GET">
|
|
3499
|
+
</label>
|
|
3500
|
+
</div>
|
|
3107
3501
|
<div class="dialog-actions">
|
|
3108
3502
|
<button class="btn-primary" type="submit">Add fault</button>
|
|
3109
3503
|
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
@@ -3112,26 +3506,31 @@ var renderAdminDocument = (service) => {
|
|
|
3112
3506
|
<div id="fault-list"></div>
|
|
3113
3507
|
</section>
|
|
3114
3508
|
<section class="view" id="view-journal" hidden>
|
|
3115
|
-
<h2>Journal</h2>
|
|
3116
|
-
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
3117
|
-
<div class="row-actions">
|
|
3118
|
-
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
3119
|
-
</div>
|
|
3509
|
+
<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>
|
|
3120
3510
|
<div id="journal"></div>
|
|
3121
3511
|
</section>
|
|
3122
3512
|
<section class="view" id="view-routes" hidden>
|
|
3123
|
-
<h2>Routes</h2>
|
|
3124
|
-
<
|
|
3513
|
+
<div class="view-head"><div><h2>Routes</h2><p class="lede">Explore shared controls and service-specific admin endpoints.</p></div></div>
|
|
3514
|
+
<div class="route-tools">
|
|
3515
|
+
<label class="field">Filter routes
|
|
3516
|
+
<input id="route-search" type="search" placeholder="Method, path, or extension" autocomplete="off">
|
|
3517
|
+
</label>
|
|
3518
|
+
<span class="route-count" id="route-count"></span>
|
|
3519
|
+
</div>
|
|
3125
3520
|
<div id="route-list"></div>
|
|
3126
|
-
<form id="route-form" class="card stack">
|
|
3521
|
+
<form id="route-form" class="card stack" style="margin-top: 18px">
|
|
3127
3522
|
<strong id="route-title">Select a route</strong>
|
|
3128
3523
|
<label class="field">JSON body
|
|
3129
|
-
<
|
|
3524
|
+
<div class="json-editor" id="route-editor" hidden>
|
|
3525
|
+
<pre aria-hidden="true"><code id="route-highlight"></code></pre>
|
|
3526
|
+
<textarea id="route-body" placeholder="{ }" spellcheck="false" aria-describedby="route-json-status" disabled></textarea>
|
|
3527
|
+
</div>
|
|
3528
|
+
<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>
|
|
3130
3529
|
</label>
|
|
3131
3530
|
<div class="dialog-actions">
|
|
3132
|
-
<button class="btn-primary" type="submit">Send</button>
|
|
3531
|
+
<button class="btn-primary" id="route-send" type="submit" disabled>Send request</button>
|
|
3133
3532
|
</div>
|
|
3134
|
-
<pre id="route-result">Response will show here.</pre>
|
|
3533
|
+
<pre class="json-view" id="route-result"><code>Response will show here.</code></pre>
|
|
3135
3534
|
</form>
|
|
3136
3535
|
</section>
|
|
3137
3536
|
</main>
|
|
@@ -3139,12 +3538,16 @@ var renderAdminDocument = (service) => {
|
|
|
3139
3538
|
</div>
|
|
3140
3539
|
<dialog id="editor">
|
|
3141
3540
|
<form method="dialog" id="editor-form">
|
|
3142
|
-
<strong id="editor-title">Record</strong>
|
|
3541
|
+
<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>
|
|
3143
3542
|
<label class="field">Id
|
|
3144
3543
|
<input id="editor-id" required>
|
|
3145
3544
|
</label>
|
|
3146
3545
|
<label class="field">JSON value
|
|
3147
|
-
<
|
|
3546
|
+
<div class="json-editor" id="record-editor">
|
|
3547
|
+
<pre aria-hidden="true"><code id="editor-highlight"></code></pre>
|
|
3548
|
+
<textarea id="editor-value" spellcheck="false" aria-describedby="editor-json-status"></textarea>
|
|
3549
|
+
</div>
|
|
3550
|
+
<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>
|
|
3148
3551
|
</label>
|
|
3149
3552
|
<div class="dialog-actions">
|
|
3150
3553
|
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
@@ -3186,8 +3589,8 @@ var describeExtension = (extension) => {
|
|
|
3186
3589
|
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
3187
3590
|
};
|
|
3188
3591
|
};
|
|
3189
|
-
var adminUiRoutes = (service, ui) => {
|
|
3190
|
-
const shell = () => renderAdminDocument(service);
|
|
3592
|
+
var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
3593
|
+
const shell = () => renderAdminDocument(service, adminPrefix);
|
|
3191
3594
|
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
3192
3595
|
return {
|
|
3193
3596
|
"GET /ui": () => htmlResponse(document3()),
|
|
@@ -3804,10 +4207,13 @@ var stateAdminRoutes = (routes) => {
|
|
|
3804
4207
|
};
|
|
3805
4208
|
|
|
3806
4209
|
// ../core/dist/version.js
|
|
3807
|
-
var PACKAGE_VERSION = true ? "
|
|
4210
|
+
var PACKAGE_VERSION = true ? "2.0.0" : UNRELEASED_VERSION;
|
|
3808
4211
|
|
|
3809
4212
|
// ../core/dist/webhooks.js
|
|
3810
|
-
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4213
|
+
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4214
|
+
status,
|
|
4215
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4216
|
+
});
|
|
3811
4217
|
var adminError3 = (status, message) => json4(status, { error: { type: "mockingbird_admin", message } });
|
|
3812
4218
|
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3813
4219
|
var parseEndpoint = (value) => {
|
|
@@ -3915,7 +4321,6 @@ var AT_HEADER = "x-mockingbird-at";
|
|
|
3915
4321
|
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
3916
4322
|
var DEFAULT_NAMESPACE = "default";
|
|
3917
4323
|
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
3918
|
-
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
3919
4324
|
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
3920
4325
|
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
3921
4326
|
var effects = /* @__PURE__ */ new WeakMap();
|
|
@@ -3954,6 +4359,22 @@ var operationMatcher = (document3) => {
|
|
|
3954
4359
|
return (request2, path) => matchers.find((m) => m.method === request2.method && m.pattern.test(path))?.operationId;
|
|
3955
4360
|
};
|
|
3956
4361
|
var createRuntime = (options) => {
|
|
4362
|
+
const adminPrefix = resolveAdminPrefix(options.adminPrefix);
|
|
4363
|
+
const internalPaths = Object.entries(options.document?.paths ?? {}).filter(([, item]) => item["x-mockingbird-internal"] === true).map(([path]) => path);
|
|
4364
|
+
for (const path of internalPaths) {
|
|
4365
|
+
if (!path.startsWith(`${ADMIN_PREFIX}/blobs/`))
|
|
4366
|
+
throw new Error(`internal transport ${path} must live under ${ADMIN_PREFIX}/blobs/`);
|
|
4367
|
+
}
|
|
4368
|
+
assertAdminPrefixAvailable(adminPrefix, Object.keys(options.document?.paths ?? {}).filter((path) => !internalPaths.includes(path)));
|
|
4369
|
+
const internalMatch = (path) => {
|
|
4370
|
+
for (const pattern of internalPaths) {
|
|
4371
|
+
const actual = path.split("/");
|
|
4372
|
+
const expected = pattern.split("/");
|
|
4373
|
+
if (actual.length === expected.length && expected.every((part, i) => part.startsWith("{") || part === actual[i]))
|
|
4374
|
+
return pattern;
|
|
4375
|
+
}
|
|
4376
|
+
return void 0;
|
|
4377
|
+
};
|
|
3957
4378
|
assertAdminUi(options.adminUi);
|
|
3958
4379
|
const sqlite = bootSqlite(options.sqlite);
|
|
3959
4380
|
const clock = options.clock ?? createClock();
|
|
@@ -3968,6 +4389,20 @@ var createRuntime = (options) => {
|
|
|
3968
4389
|
const instances = /* @__PURE__ */ new Map();
|
|
3969
4390
|
const publicNamespaces = /* @__PURE__ */ new Set();
|
|
3970
4391
|
const branchRngs = /* @__PURE__ */ new Map();
|
|
4392
|
+
const namespaceClocks = /* @__PURE__ */ new Map();
|
|
4393
|
+
const namespaceForStorage = /* @__PURE__ */ new Map();
|
|
4394
|
+
let isolatedNamespaces = false;
|
|
4395
|
+
const namespaceClock = (name = DEFAULT_NAMESPACE) => {
|
|
4396
|
+
if (!isolatedNamespaces || name === DEFAULT_NAMESPACE)
|
|
4397
|
+
return clock;
|
|
4398
|
+
let found = namespaceClocks.get(name);
|
|
4399
|
+
if (!found) {
|
|
4400
|
+
found = createClock();
|
|
4401
|
+
namespaceClocks.set(name, found);
|
|
4402
|
+
}
|
|
4403
|
+
return found;
|
|
4404
|
+
};
|
|
4405
|
+
const fleetPoints = /* @__PURE__ */ new WeakMap();
|
|
3971
4406
|
const timelines = /* @__PURE__ */ new Map();
|
|
3972
4407
|
const branchStorage = /* @__PURE__ */ new Map();
|
|
3973
4408
|
const captured = /* @__PURE__ */ new Map();
|
|
@@ -3981,17 +4416,22 @@ var createRuntime = (options) => {
|
|
|
3981
4416
|
if (!NAMESPACE_PATTERN.test(key) || !NAMESPACE_PATTERN.test(publicNamespace)) {
|
|
3982
4417
|
throw new RangeError(`namespace must match ${NAMESPACE_PATTERN}: ${JSON.stringify(publicNamespace)}`);
|
|
3983
4418
|
}
|
|
4419
|
+
const createdContextRng = isolatedRng ?? (isolatedNamespaces && key !== DEFAULT_NAMESPACE ? createRng(options.seed ?? 0) : rng);
|
|
3984
4420
|
const created = options.create({
|
|
3985
4421
|
namespace: storageNamespace(key),
|
|
3986
4422
|
publicNamespace,
|
|
4423
|
+
adminPrefix,
|
|
3987
4424
|
sqlite,
|
|
3988
|
-
clock,
|
|
3989
|
-
rng:
|
|
4425
|
+
clock: namespaceClock(publicNamespace),
|
|
4426
|
+
rng: createdContextRng
|
|
3990
4427
|
});
|
|
3991
4428
|
instances.set(key, created);
|
|
4429
|
+
namespaceForStorage.set(key, publicNamespace);
|
|
3992
4430
|
publicNamespaces.add(publicNamespace);
|
|
3993
4431
|
if (isolatedRng)
|
|
3994
4432
|
branchRngs.set(key, isolatedRng);
|
|
4433
|
+
else if (isolatedNamespaces && key !== DEFAULT_NAMESPACE)
|
|
4434
|
+
branchRngs.set(key, createdContextRng);
|
|
3995
4435
|
return created;
|
|
3996
4436
|
};
|
|
3997
4437
|
const instance = (name = DEFAULT_NAMESPACE) => instanceFor(name);
|
|
@@ -4009,7 +4449,7 @@ var createRuntime = (options) => {
|
|
|
4009
4449
|
captured.set(storage, snapshot2);
|
|
4010
4450
|
return Object.freeze({
|
|
4011
4451
|
snapshot: snapshot2,
|
|
4012
|
-
clock: Object.freeze(
|
|
4452
|
+
clock: Object.freeze(namespaceClock(namespaceForStorage.get(storage) ?? storage).state()),
|
|
4013
4453
|
rngState: (branchRngs.get(storage) ?? rng).state()
|
|
4014
4454
|
});
|
|
4015
4455
|
};
|
|
@@ -4019,7 +4459,7 @@ var createRuntime = (options) => {
|
|
|
4019
4459
|
return found;
|
|
4020
4460
|
instance(name);
|
|
4021
4461
|
found = new Timeline({
|
|
4022
|
-
now:
|
|
4462
|
+
now: namespaceClock(name).now,
|
|
4023
4463
|
...options.maxCheckpoints !== void 0 ? { maxCheckpoints: options.maxCheckpoints } : {}
|
|
4024
4464
|
});
|
|
4025
4465
|
found.commit(capture(name));
|
|
@@ -4046,12 +4486,13 @@ var createRuntime = (options) => {
|
|
|
4046
4486
|
const point = history2.checkout("main", at);
|
|
4047
4487
|
restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
|
|
4048
4488
|
captured.set(namespace, point.value.snapshot);
|
|
4049
|
-
rng.setState(point.value.rngState);
|
|
4050
|
-
|
|
4489
|
+
(branchRngs.get(namespace) ?? rng).setState(point.value.rngState);
|
|
4490
|
+
const selectedClock = namespaceClock(namespace);
|
|
4491
|
+
selectedClock.set(point.value.clock.now);
|
|
4051
4492
|
if (point.value.clock.frozen)
|
|
4052
|
-
|
|
4493
|
+
selectedClock.freeze();
|
|
4053
4494
|
else
|
|
4054
|
-
|
|
4495
|
+
selectedClock.unfreeze();
|
|
4055
4496
|
}
|
|
4056
4497
|
return namespace;
|
|
4057
4498
|
}
|
|
@@ -4109,11 +4550,12 @@ var createRuntime = (options) => {
|
|
|
4109
4550
|
const storage = ensureBranch(namespace, branchName);
|
|
4110
4551
|
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
4111
4552
|
captured.set(storage, point.value.snapshot);
|
|
4112
|
-
|
|
4553
|
+
const selectedClock = namespaceClock(namespace);
|
|
4554
|
+
selectedClock.set(point.value.clock.now);
|
|
4113
4555
|
if (point.value.clock.frozen)
|
|
4114
|
-
|
|
4556
|
+
selectedClock.freeze();
|
|
4115
4557
|
else
|
|
4116
|
-
|
|
4558
|
+
selectedClock.unfreeze();
|
|
4117
4559
|
(branchRngs.get(storage) ?? rng).setState(point.value.rngState);
|
|
4118
4560
|
};
|
|
4119
4561
|
const reset = async (name = DEFAULT_NAMESPACE) => {
|
|
@@ -4121,10 +4563,21 @@ var createRuntime = (options) => {
|
|
|
4121
4563
|
options.webhooks?.clear();
|
|
4122
4564
|
for (const each of instances.values())
|
|
4123
4565
|
await each.reset();
|
|
4566
|
+
for (const each of branchRngs.values())
|
|
4567
|
+
each.reset();
|
|
4124
4568
|
timelines.clear();
|
|
4569
|
+
for (const storage of branchStorage.values()) {
|
|
4570
|
+
instances.delete(storage);
|
|
4571
|
+
branchRngs.delete(storage);
|
|
4572
|
+
namespaceForStorage.delete(storage);
|
|
4573
|
+
}
|
|
4125
4574
|
branchStorage.clear();
|
|
4126
|
-
branchRngs.clear();
|
|
4127
4575
|
captured.clear();
|
|
4576
|
+
faults.clear();
|
|
4577
|
+
clock.reset();
|
|
4578
|
+
rng.reset();
|
|
4579
|
+
for (const each of namespaceClocks.values())
|
|
4580
|
+
each.reset();
|
|
4128
4581
|
return;
|
|
4129
4582
|
}
|
|
4130
4583
|
options.webhooks?.clear(name);
|
|
@@ -4142,11 +4595,18 @@ var createRuntime = (options) => {
|
|
|
4142
4595
|
else
|
|
4143
4596
|
clearNamespace(sqlite, storageNamespace(storage));
|
|
4144
4597
|
branchStorage.delete(mapping);
|
|
4598
|
+
instances.delete(storage);
|
|
4599
|
+
namespaceForStorage.delete(storage);
|
|
4145
4600
|
branchRngs.delete(storage);
|
|
4146
4601
|
captured.delete(storage);
|
|
4147
4602
|
}
|
|
4148
4603
|
timelines.delete(name);
|
|
4149
4604
|
captured.delete(name);
|
|
4605
|
+
faults.restore(name, { rules: [], rngState: createRng(options.seed ?? 0).state() });
|
|
4606
|
+
namespaceClock(name).reset();
|
|
4607
|
+
(branchRngs.get(name) ?? rng).reset();
|
|
4608
|
+
journal.clear(name);
|
|
4609
|
+
metrics.reset(name);
|
|
4150
4610
|
};
|
|
4151
4611
|
const snapshot = (name = DEFAULT_NAMESPACE) => {
|
|
4152
4612
|
return checkpoint(name, "main").value.snapshot;
|
|
@@ -4184,6 +4644,53 @@ var createRuntime = (options) => {
|
|
|
4184
4644
|
rng,
|
|
4185
4645
|
credentials,
|
|
4186
4646
|
webhooks: options.webhooks,
|
|
4647
|
+
isolateNamespaces() {
|
|
4648
|
+
if (publicNamespaces.size > 1)
|
|
4649
|
+
throw new Error("enable namespace isolation before creating named namespaces");
|
|
4650
|
+
isolatedNamespaces = true;
|
|
4651
|
+
faults.isolateNamespaces();
|
|
4652
|
+
},
|
|
4653
|
+
namespaceClock,
|
|
4654
|
+
namespaceOf(request2) {
|
|
4655
|
+
const url = new URL(request2.url);
|
|
4656
|
+
const prefixed = matchNamespacePath(url.pathname, adminPrefix);
|
|
4657
|
+
const selected = request2.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
|
|
4658
|
+
const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
|
|
4659
|
+
if (isAdminPath(path, adminPrefix))
|
|
4660
|
+
return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
|
|
4661
|
+
if (selected !== void 0)
|
|
4662
|
+
return selected;
|
|
4663
|
+
const credential = options.credential?.(request2);
|
|
4664
|
+
return (credential !== void 0 ? credentials.get(credential) : void 0) ?? DEFAULT_NAMESPACE;
|
|
4665
|
+
},
|
|
4666
|
+
fleetSnapshot(namespace) {
|
|
4667
|
+
instance(namespace);
|
|
4668
|
+
const handle = {};
|
|
4669
|
+
fleetPoints.set(handle, {
|
|
4670
|
+
namespace,
|
|
4671
|
+
state: capture(namespace),
|
|
4672
|
+
faults: faults.snapshot(namespace),
|
|
4673
|
+
webhooks: options.webhooks?.snapshot(namespace)
|
|
4674
|
+
});
|
|
4675
|
+
return handle;
|
|
4676
|
+
},
|
|
4677
|
+
fleetRestore(handle, namespace) {
|
|
4678
|
+
const point = typeof handle === "object" && handle !== null ? fleetPoints.get(handle) : void 0;
|
|
4679
|
+
if (!point || point.namespace !== namespace)
|
|
4680
|
+
throw new Error("invalid fleet checkpoint");
|
|
4681
|
+
restoreNamespace(sqlite, storageNamespace(namespace), point.state.snapshot);
|
|
4682
|
+
captured.set(namespace, point.state.snapshot);
|
|
4683
|
+
const selectedClock = namespaceClock(namespace);
|
|
4684
|
+
selectedClock.freeze();
|
|
4685
|
+
selectedClock.set(point.state.clock.now);
|
|
4686
|
+
if (!point.state.clock.frozen)
|
|
4687
|
+
selectedClock.unfreeze();
|
|
4688
|
+
(branchRngs.get(namespace) ?? rng).setState(point.state.rngState);
|
|
4689
|
+
faults.restore(namespace, point.faults);
|
|
4690
|
+
if (point.webhooks !== void 0)
|
|
4691
|
+
options.webhooks?.restore(namespace, point.webhooks);
|
|
4692
|
+
timelines.delete(namespace);
|
|
4693
|
+
},
|
|
4187
4694
|
applyPreset: (name, namespace = DEFAULT_NAMESPACE, overrides = {}) => {
|
|
4188
4695
|
const preset = options.presets?.[name];
|
|
4189
4696
|
if (!preset)
|
|
@@ -4215,7 +4722,7 @@ var createRuntime = (options) => {
|
|
|
4215
4722
|
state: (namespace = DEFAULT_NAMESPACE) => inspectState(stateScope(namespace)),
|
|
4216
4723
|
fetch: async (incoming) => {
|
|
4217
4724
|
let request2 = incoming;
|
|
4218
|
-
const prefixed =
|
|
4725
|
+
const prefixed = matchNamespacePath(new URL(request2.url).pathname, adminPrefix);
|
|
4219
4726
|
if (prefixed) {
|
|
4220
4727
|
const url2 = new URL(request2.url);
|
|
4221
4728
|
url2.pathname = prefixed[2] ?? "/";
|
|
@@ -4231,7 +4738,15 @@ var createRuntime = (options) => {
|
|
|
4231
4738
|
signal: request2.signal
|
|
4232
4739
|
});
|
|
4233
4740
|
}
|
|
4234
|
-
|
|
4741
|
+
const internalUrl = new URL(request2.url);
|
|
4742
|
+
const internalPath = isAdminPath(internalUrl.pathname, adminPrefix) ? `${ADMIN_PREFIX}${internalUrl.pathname.slice(adminPrefix.length)}` : "";
|
|
4743
|
+
const internal = internalMatch(internalPath);
|
|
4744
|
+
const obsoleteInternalPath = adminPrefix !== ADMIN_PREFIX && internalMatch(internalUrl.pathname) && !internal;
|
|
4745
|
+
if (internal) {
|
|
4746
|
+
internalUrl.pathname = internalPath;
|
|
4747
|
+
request2 = new Request(internalUrl, request2);
|
|
4748
|
+
}
|
|
4749
|
+
let namespace = internal ? internalUrl.searchParams.get("namespace") ?? control.namespaceOf(request2) : control.namespaceOf(request2);
|
|
4235
4750
|
if (!request2.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
4236
4751
|
const credential = options.credential(request2);
|
|
4237
4752
|
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
@@ -4251,7 +4766,9 @@ var createRuntime = (options) => {
|
|
|
4251
4766
|
return copy;
|
|
4252
4767
|
}
|
|
4253
4768
|
};
|
|
4254
|
-
|
|
4769
|
+
if (obsoleteInternalPath)
|
|
4770
|
+
return stamp(adminFail(404, "internal transport is only available under adminPrefix"));
|
|
4771
|
+
const handled = internal ? void 0 : await control.handle(request2);
|
|
4255
4772
|
if (handled)
|
|
4256
4773
|
return stamp(handled);
|
|
4257
4774
|
const started = monotonicNow();
|
|
@@ -4292,7 +4809,7 @@ var createRuntime = (options) => {
|
|
|
4292
4809
|
type: "mockingbird_admin",
|
|
4293
4810
|
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
4294
4811
|
}
|
|
4295
|
-
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
4812
|
+
}), { status: 400, headers: { "content-type": "application/json; charset=utf-8" } }));
|
|
4296
4813
|
}
|
|
4297
4814
|
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
4298
4815
|
log(400);
|
|
@@ -4359,6 +4876,7 @@ var createRuntime = (options) => {
|
|
|
4359
4876
|
startedAt: wallNow(),
|
|
4360
4877
|
wallNow,
|
|
4361
4878
|
clock,
|
|
4879
|
+
namespaceClock,
|
|
4362
4880
|
faults,
|
|
4363
4881
|
metrics,
|
|
4364
4882
|
journal,
|
|
@@ -4408,15 +4926,16 @@ var createRuntime = (options) => {
|
|
|
4408
4926
|
...options.admin?.(runtime) ?? {},
|
|
4409
4927
|
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
4410
4928
|
// maps `presets`, and a service route must not replace that list with a record.
|
|
4411
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
4929
|
+
...options.presets ? presetRoutes(options.presets, runtime, adminPrefix) : {},
|
|
4412
4930
|
...stateAdminRoutes({
|
|
4413
4931
|
open: stateScope,
|
|
4414
4932
|
afterWrite: (namespace) => {
|
|
4415
4933
|
checkpoint(namespace, "main");
|
|
4416
4934
|
}
|
|
4417
4935
|
}),
|
|
4418
|
-
...adminUiRoutes(options.name, options.adminUi)
|
|
4936
|
+
...adminUiRoutes(options.name, options.adminUi, adminPrefix)
|
|
4419
4937
|
},
|
|
4938
|
+
adminPrefix,
|
|
4420
4939
|
adminKey: options.adminKey
|
|
4421
4940
|
});
|
|
4422
4941
|
return defineMock(runtime);
|
|
@@ -4430,7 +4949,10 @@ var mutableResponse = (response) => {
|
|
|
4430
4949
|
return new Response(response.body, response);
|
|
4431
4950
|
}
|
|
4432
4951
|
};
|
|
4433
|
-
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
4952
|
+
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
4953
|
+
status,
|
|
4954
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4955
|
+
});
|
|
4434
4956
|
var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
|
|
4435
4957
|
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4436
4958
|
var credentialRoutes = (registry) => ({
|
|
@@ -4482,7 +5004,7 @@ var credentialRoutes = (registry) => ({
|
|
|
4482
5004
|
return adminJson(200, { status: "ok" });
|
|
4483
5005
|
}
|
|
4484
5006
|
});
|
|
4485
|
-
var presetRoutes = (presets, runtime) => ({
|
|
5007
|
+
var presetRoutes = (presets, runtime, adminPrefix) => ({
|
|
4486
5008
|
"GET /faults/presets": () => {
|
|
4487
5009
|
const body = {
|
|
4488
5010
|
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
@@ -4492,7 +5014,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
4492
5014
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
4493
5015
|
const name = params.name;
|
|
4494
5016
|
if (!presets[name])
|
|
4495
|
-
return adminFail(404, `no fault preset ${name}; GET /
|
|
5017
|
+
return adminFail(404, `no fault preset ${name}; GET ${adminPrefix}/faults/presets`);
|
|
4496
5018
|
const overrides = isObject(body) ? body : {};
|
|
4497
5019
|
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
4498
5020
|
}
|
|
@@ -5379,7 +5901,7 @@ var JunctionState = class {
|
|
|
5379
5901
|
adoptedRequests = /* @__PURE__ */ new WeakSet();
|
|
5380
5902
|
/** ZIPs the loaded corpus has serviceability records for. */
|
|
5381
5903
|
coveredZips = /* @__PURE__ */ new Set();
|
|
5382
|
-
/** Identifies the loaded corpus in errors and `/health`. */
|
|
5904
|
+
/** Identifies the loaded corpus in errors and `/__admin/health`. */
|
|
5383
5905
|
corpusLabel;
|
|
5384
5906
|
/**
|
|
5385
5907
|
* The loaded corpus's recorded reads. Immutable and shared by every namespace, so it
|
|
@@ -11502,6 +12024,7 @@ var createRuntime2 = (options = {}) => {
|
|
|
11502
12024
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
11503
12025
|
...options.clock ? { clock: options.clock } : {},
|
|
11504
12026
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
12027
|
+
...options.adminPrefix !== void 0 ? { adminPrefix: options.adminPrefix } : {},
|
|
11505
12028
|
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
11506
12029
|
...options.onLog ? { onLog: options.onLog } : {},
|
|
11507
12030
|
...options.journalSize !== void 0 ? { journalSize: options.journalSize } : {},
|
|
@@ -12361,6 +12884,9 @@ var normalizePath = (request2) => {
|
|
|
12361
12884
|
};
|
|
12362
12885
|
|
|
12363
12886
|
export {
|
|
12887
|
+
createClock,
|
|
12888
|
+
resolveAdminPrefix,
|
|
12889
|
+
parseDuration,
|
|
12364
12890
|
SANDBOX_KEY_PREFIXES,
|
|
12365
12891
|
isSandboxKey,
|
|
12366
12892
|
DEFAULT_LIMITS,
|
|
@@ -12417,4 +12943,4 @@ export {
|
|
|
12417
12943
|
JunctionAPI,
|
|
12418
12944
|
LabAccountConflict
|
|
12419
12945
|
};
|
|
12420
|
-
//# sourceMappingURL=chunk-
|
|
12946
|
+
//# sourceMappingURL=chunk-FZVFIPML.js.map
|