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