@crvouga/mockingbird-service-step-functions 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 +11 -0
- package/dist/{chunk-E4E6U7WJ.js → chunk-EBPLA3FJ.js} +784 -258
- package/dist/chunk-EBPLA3FJ.js.map +7 -0
- package/dist/chunk-PIVL5IYK.js +821 -0
- package/dist/chunk-PIVL5IYK.js.map +7 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +32 -2
- package/dist/index.js +1 -1
- package/dist/server.d.ts +64 -4
- package/dist/server.js +2 -2
- package/openapi.yaml +22 -0
- package/package.json +29 -4
- package/dist/chunk-E4E6U7WJ.js.map +0 -7
- package/dist/chunk-QZ6BIENV.js +0 -334
- package/dist/chunk-QZ6BIENV.js.map +0 -7
|
@@ -110,13 +110,31 @@ var Collection = class {
|
|
|
110
110
|
};
|
|
111
111
|
|
|
112
112
|
// ../core/dist/control.js
|
|
113
|
-
var HEALTH_PATH = "/health";
|
|
114
113
|
var ADMIN_PREFIX = "/__admin";
|
|
114
|
+
var resolveAdminPrefix = (prefix = ADMIN_PREFIX) => {
|
|
115
|
+
if (!/^\/[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*$/.test(prefix)) {
|
|
116
|
+
throw new Error("adminPrefix must be an absolute non-root path of literal segments, without a trailing slash");
|
|
117
|
+
}
|
|
118
|
+
return prefix;
|
|
119
|
+
};
|
|
120
|
+
var isAdminPath = (path, prefix) => path === prefix || path.startsWith(`${prefix}/`);
|
|
121
|
+
var matchNamespacePath = (path, prefix) => /^\/ns\/([^/]+)(\/.*)?$/.exec(isAdminPath(path, prefix) ? path.slice(prefix.length) : "");
|
|
122
|
+
var assertAdminPrefixAvailable = (prefix, paths) => {
|
|
123
|
+
const reserved = prefix.split("/").slice(1);
|
|
124
|
+
for (const path of paths) {
|
|
125
|
+
const segments = path.split("/").slice(1);
|
|
126
|
+
if (segments.length < reserved.length || /^\{[^}]+\}$/.test(segments[0] ?? ""))
|
|
127
|
+
continue;
|
|
128
|
+
if (reserved.every((part, i) => segments[i] === part || /^\{[^}]+\}$/.test(segments[i] ?? ""))) {
|
|
129
|
+
throw new Error(`vendor route ${path} collides with adminPrefix ${prefix}; choose a different adminPrefix`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
115
133
|
var ADMIN_KEY_HEADER = "x-mockingbird-admin-key";
|
|
116
134
|
var NAMESPACE_HEADER = "x-mockingbird-namespace";
|
|
117
135
|
var json = (status, body) => new Response(JSON.stringify(body), {
|
|
118
136
|
status,
|
|
119
|
-
headers: { "content-type": "application/json" }
|
|
137
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
120
138
|
});
|
|
121
139
|
var adminError = (status, message) => json(status, { error: { type: "mockingbird_admin", message } });
|
|
122
140
|
var UNITS = {
|
|
@@ -168,11 +186,25 @@ var readJson = async (request) => {
|
|
|
168
186
|
};
|
|
169
187
|
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
170
188
|
var createControlPlane = (context) => {
|
|
189
|
+
const adminPrefix = resolveAdminPrefix(context.adminPrefix);
|
|
171
190
|
const snapshots = /* @__PURE__ */ new Map();
|
|
172
191
|
let snapshotCounter = 0;
|
|
173
192
|
const headerNamespace = (request) => request.headers.get(NAMESPACE_HEADER) ?? context.defaultNamespace;
|
|
174
193
|
const adminNamespace = (request, url) => url.searchParams.get("namespace") ?? headerNamespace(request);
|
|
194
|
+
const health = () => {
|
|
195
|
+
return json(200, {
|
|
196
|
+
...context.describe(),
|
|
197
|
+
status: "ok",
|
|
198
|
+
service: context.name,
|
|
199
|
+
uptimeMs: context.wallNow() - context.startedAt,
|
|
200
|
+
clock: context.clock.state(),
|
|
201
|
+
namespaces: context.namespaces().length,
|
|
202
|
+
adminPrefix,
|
|
203
|
+
adminUi: `${adminPrefix}/ui`
|
|
204
|
+
});
|
|
205
|
+
};
|
|
175
206
|
const builtin = {
|
|
207
|
+
"GET /health": health,
|
|
176
208
|
"GET /": () => json(200, {
|
|
177
209
|
service: context.name,
|
|
178
210
|
routes: [...Object.keys(builtin), ...Object.keys(context.routes)].sort()
|
|
@@ -183,29 +215,30 @@ var createControlPlane = (context) => {
|
|
|
183
215
|
return json(200, { status: "ok", reset: target === "*" ? context.namespaces() : [target] });
|
|
184
216
|
},
|
|
185
217
|
"GET /namespaces": () => json(200, { default: context.defaultNamespace, namespaces: context.namespaces() }),
|
|
186
|
-
"GET /clock": () => json(200, context.clock.state()),
|
|
187
|
-
"POST /clock": ({ body }) => {
|
|
218
|
+
"GET /clock": ({ namespace }) => json(200, (context.namespaceClock?.(namespace) ?? context.clock).state()),
|
|
219
|
+
"POST /clock": ({ body, namespace }) => {
|
|
220
|
+
const clock = context.namespaceClock?.(namespace) ?? context.clock;
|
|
188
221
|
if (!isRecord(body))
|
|
189
222
|
return adminError(400, "expected a JSON object");
|
|
190
223
|
if (body.reset === true)
|
|
191
|
-
|
|
224
|
+
clock.reset();
|
|
192
225
|
if (body.freeze === true)
|
|
193
|
-
|
|
226
|
+
clock.freeze();
|
|
194
227
|
if (body.set !== void 0) {
|
|
195
228
|
const instant = parseInstant(body.set);
|
|
196
229
|
if (instant === void 0)
|
|
197
230
|
return adminError(400, "set: expected epoch ms or ISO-8601");
|
|
198
|
-
|
|
231
|
+
clock.set(instant);
|
|
199
232
|
}
|
|
200
233
|
if (body.advance !== void 0) {
|
|
201
234
|
const delta = parseDuration(body.advance);
|
|
202
235
|
if (delta === void 0)
|
|
203
236
|
return adminError(400, 'advance: expected ms or "15m"-style');
|
|
204
|
-
|
|
237
|
+
clock.advance(delta);
|
|
205
238
|
}
|
|
206
239
|
if (body.freeze === false)
|
|
207
|
-
|
|
208
|
-
return json(200,
|
|
240
|
+
clock.unfreeze();
|
|
241
|
+
return json(200, clock.state());
|
|
209
242
|
},
|
|
210
243
|
"GET /faults": () => json(200, { faults: context.faults.list() }),
|
|
211
244
|
"POST /faults": ({ body, namespace }) => {
|
|
@@ -338,7 +371,14 @@ var createControlPlane = (context) => {
|
|
|
338
371
|
return json(200, { status: "ok" });
|
|
339
372
|
}
|
|
340
373
|
};
|
|
341
|
-
const
|
|
374
|
+
for (const key of Object.keys(context.routes)) {
|
|
375
|
+
if (Object.hasOwn(builtin, key))
|
|
376
|
+
throw new Error(`admin route ${key} overrides a shared route`);
|
|
377
|
+
const path = key.slice(key.indexOf(" ") + 1);
|
|
378
|
+
if (isAdminPath(path, "/ns") || isAdminPath(path, "/blobs"))
|
|
379
|
+
throw new Error(`admin route ${key} overlaps an internal transport`);
|
|
380
|
+
}
|
|
381
|
+
const routes = [...Object.entries(builtin), ...Object.entries(context.routes)].map(([key, handler]) => {
|
|
342
382
|
const space = key.indexOf(" ");
|
|
343
383
|
return { method: key.slice(0, space), pattern: key.slice(space + 1), handler };
|
|
344
384
|
});
|
|
@@ -346,21 +386,13 @@ var createControlPlane = (context) => {
|
|
|
346
386
|
namespaceOf: headerNamespace,
|
|
347
387
|
async handle(request) {
|
|
348
388
|
const url = new URL(request.url);
|
|
349
|
-
if (url.pathname ===
|
|
350
|
-
return
|
|
351
|
-
status: "ok",
|
|
352
|
-
service: context.name,
|
|
353
|
-
uptimeMs: context.wallNow() - context.startedAt,
|
|
354
|
-
clock: context.clock.state(),
|
|
355
|
-
namespaces: context.namespaces().length,
|
|
356
|
-
adminUi: `${ADMIN_PREFIX}/ui`,
|
|
357
|
-
...context.describe()
|
|
358
|
-
});
|
|
389
|
+
if (url.pathname === `${adminPrefix}/health` && request.method === "GET") {
|
|
390
|
+
return health();
|
|
359
391
|
}
|
|
360
|
-
if (url.pathname
|
|
392
|
+
if (!isAdminPath(url.pathname, adminPrefix)) {
|
|
361
393
|
return void 0;
|
|
362
394
|
}
|
|
363
|
-
const path = url.pathname.slice(
|
|
395
|
+
const path = url.pathname.slice(adminPrefix.length) || "/";
|
|
364
396
|
const shell = request.method === "GET" && (path === "/ui" || path === "/ui/");
|
|
365
397
|
if (!shell && context.adminKey !== void 0 && request.headers.get(ADMIN_KEY_HEADER) !== context.adminKey) {
|
|
366
398
|
return adminError(401, `missing or wrong ${ADMIN_KEY_HEADER}`);
|
|
@@ -368,7 +400,12 @@ var createControlPlane = (context) => {
|
|
|
368
400
|
for (const route of routes) {
|
|
369
401
|
if (route.method !== request.method)
|
|
370
402
|
continue;
|
|
371
|
-
|
|
403
|
+
let params;
|
|
404
|
+
try {
|
|
405
|
+
params = matchRoute(route.pattern, path);
|
|
406
|
+
} catch {
|
|
407
|
+
return adminError(400, "invalid path encoding");
|
|
408
|
+
}
|
|
372
409
|
if (!params)
|
|
373
410
|
continue;
|
|
374
411
|
let body;
|
|
@@ -377,15 +414,18 @@ var createControlPlane = (context) => {
|
|
|
377
414
|
} catch {
|
|
378
415
|
return adminError(400, "request body is not valid JSON");
|
|
379
416
|
}
|
|
417
|
+
const namespace = adminNamespace(request, url);
|
|
418
|
+
if (!/^[A-Za-z0-9_.-]{1,64}$/.test(namespace))
|
|
419
|
+
return adminError(400, "invalid namespace");
|
|
380
420
|
return route.handler({
|
|
381
421
|
request,
|
|
382
422
|
url,
|
|
383
423
|
params,
|
|
384
|
-
namespace
|
|
424
|
+
namespace,
|
|
385
425
|
body
|
|
386
426
|
});
|
|
387
427
|
}
|
|
388
|
-
return adminError(404, `no admin route ${request.method} ${path}; GET ${
|
|
428
|
+
return adminError(404, `no admin route ${request.method} ${path}; GET ${adminPrefix} lists them`);
|
|
389
429
|
}
|
|
390
430
|
};
|
|
391
431
|
};
|
|
@@ -475,7 +515,33 @@ var faultResponse = (rule) => {
|
|
|
475
515
|
};
|
|
476
516
|
var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) => {
|
|
477
517
|
const entries = [];
|
|
518
|
+
let isolated = false;
|
|
519
|
+
const scopedRngs = /* @__PURE__ */ new Map();
|
|
520
|
+
const random = (namespace) => {
|
|
521
|
+
if (!isolated || namespace === "default")
|
|
522
|
+
return rng;
|
|
523
|
+
let scoped = scopedRngs.get(namespace);
|
|
524
|
+
if (!scoped) {
|
|
525
|
+
scoped = createRng(rng.seed);
|
|
526
|
+
scopedRngs.set(namespace, scoped);
|
|
527
|
+
}
|
|
528
|
+
return scoped;
|
|
529
|
+
};
|
|
478
530
|
return {
|
|
531
|
+
isolateNamespaces() {
|
|
532
|
+
isolated = true;
|
|
533
|
+
},
|
|
534
|
+
snapshot(namespace) {
|
|
535
|
+
return {
|
|
536
|
+
rules: structuredClone(entries.map((entry, position) => ({
|
|
537
|
+
...entry.rule,
|
|
538
|
+
remaining: entry.remaining,
|
|
539
|
+
hits: entry.hits,
|
|
540
|
+
position
|
|
541
|
+
})).filter((entry) => entry.namespace === namespace)),
|
|
542
|
+
rngState: random(namespace).state()
|
|
543
|
+
};
|
|
544
|
+
},
|
|
479
545
|
add(rule) {
|
|
480
546
|
const existing = entries.findIndex((e) => e.rule.id === rule.id);
|
|
481
547
|
const entry = { rule, remaining: rule.count ?? null, hits: 0 };
|
|
@@ -495,6 +561,16 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
|
|
|
495
561
|
},
|
|
496
562
|
clear() {
|
|
497
563
|
entries.length = 0;
|
|
564
|
+
rng.reset();
|
|
565
|
+
scopedRngs.clear();
|
|
566
|
+
},
|
|
567
|
+
restore(namespace, state) {
|
|
568
|
+
for (let i = entries.length - 1; i >= 0; i--)
|
|
569
|
+
if (entries[i]?.rule.namespace === namespace)
|
|
570
|
+
entries.splice(i, 1);
|
|
571
|
+
for (const { remaining, hits, position, ...rule } of state.rules)
|
|
572
|
+
entries.splice(Math.min(position, entries.length), 0, { rule, remaining, hits });
|
|
573
|
+
random(namespace).setState(state.rngState);
|
|
498
574
|
},
|
|
499
575
|
async take(candidate) {
|
|
500
576
|
const hits = [];
|
|
@@ -504,7 +580,7 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
|
|
|
504
580
|
if (!matches(entry.rule, candidate))
|
|
505
581
|
continue;
|
|
506
582
|
const rate = entry.rule.rate ?? 1;
|
|
507
|
-
if (
|
|
583
|
+
if (random(candidate.namespace).next() >= rate)
|
|
508
584
|
continue;
|
|
509
585
|
entry.hits++;
|
|
510
586
|
if (entry.remaining !== null)
|
|
@@ -706,7 +782,8 @@ var notes = /* @__PURE__ */ new WeakMap();
|
|
|
706
782
|
var responseNotes = (response) => notes.get(response);
|
|
707
783
|
|
|
708
784
|
// ../core/dist/metrics.js
|
|
709
|
-
var createMetrics = () => {
|
|
785
|
+
var createMetrics = (partition = true) => {
|
|
786
|
+
const namespaces = /* @__PURE__ */ new Map();
|
|
710
787
|
let requests = 0;
|
|
711
788
|
let faults = 0;
|
|
712
789
|
let totalDurationMs = 0;
|
|
@@ -714,6 +791,14 @@ var createMetrics = () => {
|
|
|
714
791
|
const unmatched = /* @__PURE__ */ new Map();
|
|
715
792
|
return {
|
|
716
793
|
record(entry) {
|
|
794
|
+
if (partition) {
|
|
795
|
+
let scoped = namespaces.get(entry.namespace);
|
|
796
|
+
if (!scoped) {
|
|
797
|
+
scoped = createMetrics(false);
|
|
798
|
+
namespaces.set(entry.namespace, scoped);
|
|
799
|
+
}
|
|
800
|
+
scoped.record(entry);
|
|
801
|
+
}
|
|
717
802
|
requests++;
|
|
718
803
|
totalDurationMs += entry.durationMs;
|
|
719
804
|
if (entry.faultId !== void 0)
|
|
@@ -725,7 +810,7 @@ var createMetrics = () => {
|
|
|
725
810
|
unmatched.set(route, (unmatched.get(route) ?? 0) + 1);
|
|
726
811
|
}
|
|
727
812
|
},
|
|
728
|
-
report: () => ({
|
|
813
|
+
report: (namespace) => namespace !== void 0 ? (namespaces.get(namespace) ?? createMetrics(false)).report() : {
|
|
729
814
|
requests,
|
|
730
815
|
byOperation: Object.fromEntries([...byOperation].sort(([a], [b]) => a.localeCompare(b))),
|
|
731
816
|
unmatched: [...unmatched].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([route, count]) => {
|
|
@@ -734,8 +819,13 @@ var createMetrics = () => {
|
|
|
734
819
|
}),
|
|
735
820
|
faults,
|
|
736
821
|
totalDurationMs
|
|
737
|
-
}
|
|
738
|
-
reset() {
|
|
822
|
+
},
|
|
823
|
+
reset(namespace) {
|
|
824
|
+
if (namespace !== void 0) {
|
|
825
|
+
namespaces.delete(namespace);
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
namespaces.clear();
|
|
739
829
|
requests = 0;
|
|
740
830
|
faults = 0;
|
|
741
831
|
totalDurationMs = 0;
|
|
@@ -1348,6 +1438,34 @@ dialog:not([open]) {
|
|
|
1348
1438
|
function isRecord2(value) {
|
|
1349
1439
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1350
1440
|
}
|
|
1441
|
+
function highlightJsonText(text) {
|
|
1442
|
+
const escapeToken = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
1443
|
+
switch (char) {
|
|
1444
|
+
case "&":
|
|
1445
|
+
return "&";
|
|
1446
|
+
case "<":
|
|
1447
|
+
return "<";
|
|
1448
|
+
case ">":
|
|
1449
|
+
return ">";
|
|
1450
|
+
case '"':
|
|
1451
|
+
return """;
|
|
1452
|
+
default:
|
|
1453
|
+
return "'";
|
|
1454
|
+
}
|
|
1455
|
+
});
|
|
1456
|
+
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;
|
|
1457
|
+
let cursor = 0;
|
|
1458
|
+
let html = "";
|
|
1459
|
+
for (const match2 of text.matchAll(token)) {
|
|
1460
|
+
const index = match2.index ?? 0;
|
|
1461
|
+
const value = match2[0];
|
|
1462
|
+
html += escapeToken(text.slice(cursor, index));
|
|
1463
|
+
const kind = value.startsWith('"') ? value.trimEnd().endsWith(":") ? "key" : "string" : value === "true" || value === "false" ? "boolean" : value === "null" ? "null" : "number";
|
|
1464
|
+
html += `<span class="json-${kind}">${escapeToken(value)}</span>`;
|
|
1465
|
+
cursor = index + value.length;
|
|
1466
|
+
}
|
|
1467
|
+
return html + escapeToken(text.slice(cursor));
|
|
1468
|
+
}
|
|
1351
1469
|
function mountAdminBrand(doc, host, service, catalog) {
|
|
1352
1470
|
const clip = (value, max) => {
|
|
1353
1471
|
if (typeof value !== "string")
|
|
@@ -1709,14 +1827,16 @@ function bootAdmin(config) {
|
|
|
1709
1827
|
const state = {
|
|
1710
1828
|
namespace: "default",
|
|
1711
1829
|
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
1712
|
-
view: "overview",
|
|
1830
|
+
view: ["overview", "state", "clock", "faults", "journal", "routes"].includes(location.hash.slice(1)) ? location.hash.slice(1) : "overview",
|
|
1713
1831
|
collection: "",
|
|
1714
1832
|
shape: null,
|
|
1715
1833
|
route: null,
|
|
1834
|
+
routes: [],
|
|
1716
1835
|
editing: null
|
|
1717
1836
|
};
|
|
1718
1837
|
const standard = new Set(config.standardRoutes);
|
|
1719
1838
|
const banner = el("banner", HTMLElement);
|
|
1839
|
+
const bannerMessage = el("banner-message", HTMLElement);
|
|
1720
1840
|
const keyInput = el("admin-key", HTMLInputElement);
|
|
1721
1841
|
keyInput.value = state.key;
|
|
1722
1842
|
const params = new URLSearchParams(location.search);
|
|
@@ -1727,18 +1847,21 @@ function bootAdmin(config) {
|
|
|
1727
1847
|
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
1728
1848
|
params.delete("key");
|
|
1729
1849
|
const next = params.toString();
|
|
1730
|
-
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
1850
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : "") + location.hash);
|
|
1731
1851
|
}
|
|
1732
1852
|
const queryNamespace = params.get("namespace");
|
|
1733
1853
|
if (queryNamespace)
|
|
1734
1854
|
state.namespace = queryNamespace;
|
|
1735
1855
|
const showError = (error) => {
|
|
1736
|
-
|
|
1856
|
+
bannerMessage.textContent = error instanceof Error ? error.message : String(error);
|
|
1737
1857
|
banner.classList.add("show");
|
|
1858
|
+
banner.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
1738
1859
|
};
|
|
1739
1860
|
const clearError = () => {
|
|
1740
1861
|
banner.classList.remove("show");
|
|
1862
|
+
bannerMessage.textContent = "";
|
|
1741
1863
|
};
|
|
1864
|
+
el("banner-dismiss", HTMLButtonElement).addEventListener("click", clearError);
|
|
1742
1865
|
const api = async (method, path, body) => {
|
|
1743
1866
|
const headers = { accept: "application/json" };
|
|
1744
1867
|
if (state.key !== "")
|
|
@@ -1782,16 +1905,21 @@ function bootAdmin(config) {
|
|
|
1782
1905
|
namespace: () => state.namespace
|
|
1783
1906
|
};
|
|
1784
1907
|
const isPanelScript = (value) => typeof value === "function";
|
|
1908
|
+
const themeButton = el("theme", HTMLButtonElement);
|
|
1785
1909
|
const setTheme = (theme) => {
|
|
1786
1910
|
if (theme)
|
|
1787
1911
|
document.documentElement.dataset.theme = theme;
|
|
1788
1912
|
else
|
|
1789
1913
|
delete document.documentElement.dataset.theme;
|
|
1914
|
+
const dark = theme ? theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
1915
|
+
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>';
|
|
1916
|
+
const label = dark ? "Use light theme" : "Use dark theme";
|
|
1917
|
+
themeButton.setAttribute("aria-label", label);
|
|
1918
|
+
themeButton.title = label;
|
|
1790
1919
|
};
|
|
1791
1920
|
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
1921
|
+
setTheme(storedTheme === "dark" || storedTheme === "light" ? storedTheme : null);
|
|
1922
|
+
themeButton.addEventListener("click", () => {
|
|
1795
1923
|
const current = document.documentElement.dataset.theme;
|
|
1796
1924
|
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
1797
1925
|
const next = dark ? "light" : "dark";
|
|
@@ -1810,6 +1938,7 @@ function bootAdmin(config) {
|
|
|
1810
1938
|
continue;
|
|
1811
1939
|
section.hidden = section.id !== `view-${view}`;
|
|
1812
1940
|
}
|
|
1941
|
+
history.replaceState(null, "", `${location.pathname}${location.search}#${encodeURIComponent(view)}`);
|
|
1813
1942
|
};
|
|
1814
1943
|
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
1815
1944
|
const target = event.target;
|
|
@@ -1824,7 +1953,7 @@ function bootAdmin(config) {
|
|
|
1824
1953
|
show(view);
|
|
1825
1954
|
refresh().catch(showError);
|
|
1826
1955
|
});
|
|
1827
|
-
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>`;
|
|
1956
|
+
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>`;
|
|
1828
1957
|
const table = (columns, rows) => {
|
|
1829
1958
|
if (rows.length === 0)
|
|
1830
1959
|
return '<p class="empty">Nothing here yet.</p>';
|
|
@@ -1834,16 +1963,22 @@ function bootAdmin(config) {
|
|
|
1834
1963
|
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
1835
1964
|
return `<tr${attrs}>${cells}</tr>`;
|
|
1836
1965
|
}).join("");
|
|
1837
|
-
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
1966
|
+
return `<div class="table-shell"><table><thead>${head}</thead><tbody>${body}</tbody></table></div>`;
|
|
1838
1967
|
};
|
|
1839
1968
|
const preview = (value) => {
|
|
1840
1969
|
const text = JSON.stringify(value) ?? "null";
|
|
1841
|
-
|
|
1970
|
+
const clipped = text.length > 180 ? `${text.slice(0, 180)}\u2026` : text;
|
|
1971
|
+
return `<code class="json-inline">${highlightJsonText(clipped)}</code>`;
|
|
1842
1972
|
};
|
|
1843
1973
|
const when = (epoch) => {
|
|
1844
1974
|
const date = new Date(epoch);
|
|
1845
1975
|
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
1846
1976
|
};
|
|
1977
|
+
const journalWhen = (value) => {
|
|
1978
|
+
const numeric = value.trim() === "" ? Number.NaN : Number(value);
|
|
1979
|
+
const epoch = Number.isFinite(numeric) ? numeric : Date.parse(value);
|
|
1980
|
+
return Number.isFinite(epoch) ? when(epoch) : value;
|
|
1981
|
+
};
|
|
1847
1982
|
const clockCard = (epoch) => {
|
|
1848
1983
|
const date = new Date(epoch);
|
|
1849
1984
|
if (Number.isNaN(date.getTime()))
|
|
@@ -1963,7 +2098,7 @@ function bootAdmin(config) {
|
|
|
1963
2098
|
};
|
|
1964
2099
|
const renderOverview = async () => {
|
|
1965
2100
|
const [health, shape, clock, journal] = await Promise.all([
|
|
1966
|
-
originGet(
|
|
2101
|
+
originGet(`${config.adminPrefix}/health`),
|
|
1967
2102
|
api("GET", "/state"),
|
|
1968
2103
|
api("GET", "/clock"),
|
|
1969
2104
|
api("GET", "/requests?limit=8")
|
|
@@ -1980,7 +2115,7 @@ function bootAdmin(config) {
|
|
|
1980
2115
|
},
|
|
1981
2116
|
{
|
|
1982
2117
|
k: "Status",
|
|
1983
|
-
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
2118
|
+
v: `<span class="badge ok">${esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")}</span>`
|
|
1984
2119
|
},
|
|
1985
2120
|
{ k: "Namespace", v: esc(state.namespace) },
|
|
1986
2121
|
{ k: "Collections", v: String(decoded.collections.length) },
|
|
@@ -2067,26 +2202,46 @@ function bootAdmin(config) {
|
|
|
2067
2202
|
const renderJournal = async () => {
|
|
2068
2203
|
const journal = await api("GET", "/requests?limit=100");
|
|
2069
2204
|
el("journal", HTMLElement).innerHTML = table([
|
|
2070
|
-
{ label: "When", cell: (row) => esc(row.at) },
|
|
2071
|
-
{
|
|
2205
|
+
{ label: "When", cell: (row) => esc(journalWhen(row.at)) },
|
|
2206
|
+
{
|
|
2207
|
+
label: "Method",
|
|
2208
|
+
cell: (row) => `<span class="method">${esc(row.method)}</span>`
|
|
2209
|
+
},
|
|
2072
2210
|
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2073
|
-
{
|
|
2211
|
+
{
|
|
2212
|
+
label: "Status",
|
|
2213
|
+
cell: (row) => `<span class="badge ${row.status.startsWith("2") ? "ok" : row.status.startsWith("4") || row.status.startsWith("5") ? "warn" : ""}">${esc(row.status)}</span>`
|
|
2214
|
+
},
|
|
2074
2215
|
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
2075
2216
|
], decodeJournal(journal));
|
|
2076
2217
|
};
|
|
2077
|
-
const
|
|
2078
|
-
const
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2218
|
+
const paintRoutes = () => {
|
|
2219
|
+
const query = el("route-search", HTMLInputElement).value.trim().toLocaleLowerCase();
|
|
2220
|
+
const routes = state.routes.filter((route) => route.toLocaleLowerCase().includes(query));
|
|
2221
|
+
el("route-count", HTMLElement).textContent = `${routes.length} of ${state.routes.length}`;
|
|
2222
|
+
if (state.routes.length === 0) {
|
|
2223
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No admin routes are available.</p>';
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
if (routes.length === 0) {
|
|
2227
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No routes match this filter.</p>';
|
|
2228
|
+
return;
|
|
2229
|
+
}
|
|
2082
2230
|
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2083
2231
|
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2084
2232
|
const parts = route.split(" ");
|
|
2085
2233
|
const method = parts[0] ?? "";
|
|
2086
2234
|
const path = parts.slice(1).join(" ");
|
|
2087
|
-
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>`;
|
|
2235
|
+
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>`;
|
|
2088
2236
|
}).join("");
|
|
2089
2237
|
};
|
|
2238
|
+
const renderRoutes = async () => {
|
|
2239
|
+
const data = await api("GET", "/");
|
|
2240
|
+
if (!isRecord2(data))
|
|
2241
|
+
throw new Error("Route list was not an object");
|
|
2242
|
+
state.routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
2243
|
+
paintRoutes();
|
|
2244
|
+
};
|
|
2090
2245
|
const renderers = {
|
|
2091
2246
|
overview: renderOverview,
|
|
2092
2247
|
state: renderState,
|
|
@@ -2095,19 +2250,50 @@ function bootAdmin(config) {
|
|
|
2095
2250
|
journal: renderJournal,
|
|
2096
2251
|
routes: renderRoutes
|
|
2097
2252
|
};
|
|
2253
|
+
let refreshToken = 0;
|
|
2098
2254
|
async function refresh() {
|
|
2255
|
+
const token = ++refreshToken;
|
|
2099
2256
|
clearError();
|
|
2257
|
+
const main = el("views", HTMLElement);
|
|
2258
|
+
const sync = el("sync-status", HTMLElement);
|
|
2259
|
+
main.setAttribute("aria-busy", "true");
|
|
2260
|
+
sync.classList.add("busy");
|
|
2261
|
+
sync.textContent = "Updating";
|
|
2100
2262
|
const render = renderers[state.view];
|
|
2101
|
-
|
|
2102
|
-
|
|
2263
|
+
try {
|
|
2264
|
+
if (render)
|
|
2265
|
+
await render();
|
|
2266
|
+
if (token === refreshToken)
|
|
2267
|
+
sync.textContent = "Up to date";
|
|
2268
|
+
} finally {
|
|
2269
|
+
if (token === refreshToken) {
|
|
2270
|
+
main.removeAttribute("aria-busy");
|
|
2271
|
+
sync.classList.remove("busy");
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2103
2274
|
}
|
|
2104
2275
|
const delay = (ms) => new Promise((resolve) => {
|
|
2105
2276
|
window.setTimeout(resolve, ms);
|
|
2106
2277
|
});
|
|
2107
|
-
const confirmAction = async (button, pending, done, work) => {
|
|
2278
|
+
const confirmAction = async (button, pending, done, work, confirmLabel) => {
|
|
2108
2279
|
if (button.dataset.pending === "1")
|
|
2109
2280
|
return;
|
|
2110
2281
|
const label = button.textContent ?? "";
|
|
2282
|
+
if (confirmLabel !== void 0 && button.dataset.confirming !== "1") {
|
|
2283
|
+
button.dataset.confirming = "1";
|
|
2284
|
+
button.classList.add("confirming");
|
|
2285
|
+
button.textContent = confirmLabel;
|
|
2286
|
+
window.setTimeout(() => {
|
|
2287
|
+
if (button.dataset.confirming !== "1")
|
|
2288
|
+
return;
|
|
2289
|
+
button.textContent = label;
|
|
2290
|
+
button.classList.remove("confirming");
|
|
2291
|
+
delete button.dataset.confirming;
|
|
2292
|
+
}, 3e3);
|
|
2293
|
+
return;
|
|
2294
|
+
}
|
|
2295
|
+
button.classList.remove("confirming");
|
|
2296
|
+
delete button.dataset.confirming;
|
|
2111
2297
|
const width = button.offsetWidth;
|
|
2112
2298
|
button.dataset.pending = "1";
|
|
2113
2299
|
button.classList.add("pressed");
|
|
@@ -2128,6 +2314,65 @@ function bootAdmin(config) {
|
|
|
2128
2314
|
delete button.dataset.pending;
|
|
2129
2315
|
}
|
|
2130
2316
|
};
|
|
2317
|
+
const wireJsonEditor = (textareaId, highlightId, statusId, shellId, allowEmpty) => {
|
|
2318
|
+
const textarea = el(textareaId, HTMLTextAreaElement);
|
|
2319
|
+
const highlight = el(highlightId, HTMLElement);
|
|
2320
|
+
const status = el(statusId, HTMLElement);
|
|
2321
|
+
const shell = el(shellId, HTMLElement);
|
|
2322
|
+
const sync = () => {
|
|
2323
|
+
const text = textarea.value;
|
|
2324
|
+
highlight.innerHTML = `${highlightJsonText(text)}
|
|
2325
|
+
`;
|
|
2326
|
+
let valid = true;
|
|
2327
|
+
let message = "Valid JSON";
|
|
2328
|
+
if (allowEmpty && text.trim() === "")
|
|
2329
|
+
message = "No body";
|
|
2330
|
+
else {
|
|
2331
|
+
try {
|
|
2332
|
+
readJson2(text);
|
|
2333
|
+
} catch {
|
|
2334
|
+
valid = false;
|
|
2335
|
+
message = "Invalid JSON";
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
shell.classList.toggle("invalid", !valid);
|
|
2339
|
+
status.classList.toggle("invalid", !valid);
|
|
2340
|
+
status.textContent = message;
|
|
2341
|
+
textarea.setAttribute("aria-invalid", valid ? "false" : "true");
|
|
2342
|
+
};
|
|
2343
|
+
const syncScroll = () => {
|
|
2344
|
+
const preview2 = highlight.parentElement;
|
|
2345
|
+
if (!(preview2 instanceof HTMLElement))
|
|
2346
|
+
return;
|
|
2347
|
+
preview2.scrollTop = textarea.scrollTop;
|
|
2348
|
+
preview2.scrollLeft = textarea.scrollLeft;
|
|
2349
|
+
};
|
|
2350
|
+
textarea.addEventListener("input", sync);
|
|
2351
|
+
textarea.addEventListener("scroll", syncScroll);
|
|
2352
|
+
sync();
|
|
2353
|
+
return sync;
|
|
2354
|
+
};
|
|
2355
|
+
const syncRouteEditor = wireJsonEditor("route-body", "route-highlight", "route-json-status", "route-editor", true);
|
|
2356
|
+
const syncRecordEditor = wireJsonEditor("editor-value", "editor-highlight", "editor-json-status", "record-editor", false);
|
|
2357
|
+
for (const button of document.querySelectorAll("[data-format]")) {
|
|
2358
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2359
|
+
continue;
|
|
2360
|
+
button.addEventListener("click", () => {
|
|
2361
|
+
const target = button.dataset.format;
|
|
2362
|
+
if (target === void 0)
|
|
2363
|
+
return;
|
|
2364
|
+
const textarea = document.getElementById(target);
|
|
2365
|
+
if (!(textarea instanceof HTMLTextAreaElement) || textarea.value.trim() === "")
|
|
2366
|
+
return;
|
|
2367
|
+
try {
|
|
2368
|
+
textarea.value = JSON.stringify(readJson2(textarea.value), null, 2) ?? "";
|
|
2369
|
+
textarea.dispatchEvent(new Event("input"));
|
|
2370
|
+
textarea.focus();
|
|
2371
|
+
} catch {
|
|
2372
|
+
showError(new Error("Fix the JSON before formatting it"));
|
|
2373
|
+
}
|
|
2374
|
+
});
|
|
2375
|
+
}
|
|
2131
2376
|
document.addEventListener("click", (event) => {
|
|
2132
2377
|
const target = event.target;
|
|
2133
2378
|
if (!(target instanceof Element))
|
|
@@ -2200,6 +2445,7 @@ function bootAdmin(config) {
|
|
|
2200
2445
|
} else {
|
|
2201
2446
|
editorValue.value = "{\n \n}";
|
|
2202
2447
|
}
|
|
2448
|
+
syncRecordEditor();
|
|
2203
2449
|
editor.showModal();
|
|
2204
2450
|
}
|
|
2205
2451
|
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -2229,10 +2475,10 @@ function bootAdmin(config) {
|
|
|
2229
2475
|
const id = state.editing;
|
|
2230
2476
|
if (!id)
|
|
2231
2477
|
return;
|
|
2232
|
-
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2478
|
+
confirmAction(editorDelete, "Deleting...", "Deleted", () => api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2233
2479
|
editor.close();
|
|
2234
2480
|
return renderState();
|
|
2235
|
-
}).catch(showError);
|
|
2481
|
+
}), "Confirm delete").catch(showError);
|
|
2236
2482
|
});
|
|
2237
2483
|
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2238
2484
|
if (!(button instanceof HTMLButtonElement))
|
|
@@ -2278,10 +2524,10 @@ function bootAdmin(config) {
|
|
|
2278
2524
|
}).then(() => renderFaults()).catch(showError);
|
|
2279
2525
|
});
|
|
2280
2526
|
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2281
|
-
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
|
|
2527
|
+
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults()), "Confirm clear").catch(showError);
|
|
2282
2528
|
});
|
|
2283
2529
|
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2284
|
-
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
|
|
2530
|
+
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal()), "Confirm clear").catch(showError);
|
|
2285
2531
|
});
|
|
2286
2532
|
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2287
2533
|
const target = event.target;
|
|
@@ -2295,9 +2541,15 @@ function bootAdmin(config) {
|
|
|
2295
2541
|
return;
|
|
2296
2542
|
state.route = route;
|
|
2297
2543
|
el("route-title", HTMLElement).textContent = route;
|
|
2544
|
+
el("route-send", HTMLButtonElement).disabled = false;
|
|
2298
2545
|
const method = route.split(" ")[0] ?? "";
|
|
2299
|
-
el("route-body", HTMLTextAreaElement)
|
|
2546
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
2547
|
+
routeBody.disabled = method === "GET" || method === "DELETE";
|
|
2548
|
+
el("route-editor", HTMLElement).hidden = routeBody.disabled;
|
|
2549
|
+
el("route-json-status", HTMLElement).parentElement?.toggleAttribute("hidden", routeBody.disabled);
|
|
2550
|
+
syncRouteEditor();
|
|
2300
2551
|
});
|
|
2552
|
+
el("route-search", HTMLInputElement).addEventListener("input", paintRoutes);
|
|
2301
2553
|
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2302
2554
|
event.preventDefault();
|
|
2303
2555
|
const send = async () => {
|
|
@@ -2319,7 +2571,8 @@ function bootAdmin(config) {
|
|
|
2319
2571
|
}
|
|
2320
2572
|
}
|
|
2321
2573
|
const data = await api(method, concrete, body);
|
|
2322
|
-
|
|
2574
|
+
const text = JSON.stringify(data, null, 2) ?? "null";
|
|
2575
|
+
el("route-result", HTMLElement).innerHTML = `<code>${highlightJsonText(text)}</code>`;
|
|
2323
2576
|
};
|
|
2324
2577
|
send().catch(showError);
|
|
2325
2578
|
});
|
|
@@ -2437,11 +2690,13 @@ function bootAdmin(config) {
|
|
|
2437
2690
|
views.append(section);
|
|
2438
2691
|
mountSqlExplorer(body, panelApi);
|
|
2439
2692
|
}
|
|
2693
|
+
show(document.getElementById(`view-${state.view}`) === null ? "overview" : state.view);
|
|
2440
2694
|
return refresh();
|
|
2441
2695
|
}).catch(showError);
|
|
2442
2696
|
}
|
|
2443
2697
|
var clientPrelude = [
|
|
2444
2698
|
isRecord2,
|
|
2699
|
+
highlightJsonText,
|
|
2445
2700
|
mountAdminBrand,
|
|
2446
2701
|
startAdminBrand,
|
|
2447
2702
|
faultPresetList,
|
|
@@ -2464,6 +2719,7 @@ ${entry}(${payload});
|
|
|
2464
2719
|
var defineMock = (runtime) => runtime;
|
|
2465
2720
|
var STANDARD_ADMIN_ROUTES = [
|
|
2466
2721
|
"GET /",
|
|
2722
|
+
"GET /health",
|
|
2467
2723
|
"POST /reset",
|
|
2468
2724
|
"GET /namespaces",
|
|
2469
2725
|
"GET /clock",
|
|
@@ -2543,11 +2799,11 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
2543
2799
|
}
|
|
2544
2800
|
});
|
|
2545
2801
|
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
2546
|
-
var renderAdminDocument = (service) => {
|
|
2802
|
+
var renderAdminDocument = (service, adminPrefix = ADMIN_PREFIX) => {
|
|
2547
2803
|
const name = escapeHtml(service);
|
|
2548
2804
|
const script = adminClientSource({
|
|
2549
2805
|
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
2550
|
-
adminPrefix
|
|
2806
|
+
adminPrefix,
|
|
2551
2807
|
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
2552
2808
|
brandsUrl: ADMIN_BRANDS_URL,
|
|
2553
2809
|
service
|
|
@@ -2564,75 +2820,99 @@ var renderAdminDocument = (service) => {
|
|
|
2564
2820
|
${CSS_RESET}
|
|
2565
2821
|
:root {
|
|
2566
2822
|
color-scheme: light;
|
|
2567
|
-
--bg: #
|
|
2823
|
+
--bg: #f7f8fb;
|
|
2568
2824
|
--bg-elev: #ffffff;
|
|
2569
|
-
--bg-muted: #
|
|
2570
|
-
--bg-inset: #
|
|
2571
|
-
--line: #
|
|
2572
|
-
--line-strong: #
|
|
2573
|
-
--ink: #
|
|
2574
|
-
--muted: #
|
|
2575
|
-
--faint: #
|
|
2576
|
-
--accent: #
|
|
2825
|
+
--bg-muted: #f1f3f7;
|
|
2826
|
+
--bg-inset: #f8f9fc;
|
|
2827
|
+
--line: #e4e7ec;
|
|
2828
|
+
--line-strong: #cfd4dc;
|
|
2829
|
+
--ink: #101828;
|
|
2830
|
+
--muted: #475467;
|
|
2831
|
+
--faint: #667085;
|
|
2832
|
+
--accent: #6941c6;
|
|
2833
|
+
--accent-hover: #5933b4;
|
|
2577
2834
|
--accent-ink: #ffffff;
|
|
2578
|
-
--accent-soft: #
|
|
2579
|
-
--ok: #
|
|
2580
|
-
--ok-soft: #
|
|
2581
|
-
--warn: #
|
|
2582
|
-
--warn-soft: #
|
|
2583
|
-
--err: #
|
|
2584
|
-
--err-soft: #
|
|
2585
|
-
--
|
|
2835
|
+
--accent-soft: #f4f0ff;
|
|
2836
|
+
--ok: #067647;
|
|
2837
|
+
--ok-soft: #ecfdf3;
|
|
2838
|
+
--warn: #b54708;
|
|
2839
|
+
--warn-soft: #fffaeb;
|
|
2840
|
+
--err: #b42318;
|
|
2841
|
+
--err-soft: #fef3f2;
|
|
2842
|
+
--code-key: #6941c6;
|
|
2843
|
+
--code-string: #067647;
|
|
2844
|
+
--code-number: #175cd3;
|
|
2845
|
+
--code-boolean: #c11574;
|
|
2846
|
+
--code-null: #667085;
|
|
2847
|
+
--shadow-xs: 0 1px 2px rgb(16 24 40 / 0.05);
|
|
2848
|
+
--shadow-sm: 0 1px 3px rgb(16 24 40 / 0.08), 0 1px 2px rgb(16 24 40 / 0.04);
|
|
2849
|
+
--shadow-lg: 0 18px 48px rgb(16 24 40 / 0.16), 0 4px 12px rgb(16 24 40 / 0.08);
|
|
2586
2850
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2587
2851
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
2588
|
-
--radius:
|
|
2589
|
-
--header:
|
|
2590
|
-
--nav:
|
|
2852
|
+
--radius: 12px;
|
|
2853
|
+
--header: 72px;
|
|
2854
|
+
--nav: 224px;
|
|
2591
2855
|
}
|
|
2592
2856
|
:root[data-theme="dark"] {
|
|
2593
2857
|
color-scheme: dark;
|
|
2594
|
-
--bg: #
|
|
2595
|
-
--bg-elev: #
|
|
2596
|
-
--bg-muted: #
|
|
2597
|
-
--bg-inset: #
|
|
2598
|
-
--line: #
|
|
2599
|
-
--line-strong: #
|
|
2600
|
-
--ink: #
|
|
2601
|
-
--muted: #
|
|
2602
|
-
--faint: #
|
|
2603
|
-
--accent: #
|
|
2604
|
-
--accent-
|
|
2605
|
-
--accent-
|
|
2606
|
-
--
|
|
2607
|
-
--ok
|
|
2608
|
-
--
|
|
2609
|
-
--warn
|
|
2610
|
-
--
|
|
2611
|
-
--err
|
|
2612
|
-
--
|
|
2858
|
+
--bg: #0c0e14;
|
|
2859
|
+
--bg-elev: #151821;
|
|
2860
|
+
--bg-muted: #20232e;
|
|
2861
|
+
--bg-inset: #11141c;
|
|
2862
|
+
--line: #2b303d;
|
|
2863
|
+
--line-strong: #414858;
|
|
2864
|
+
--ink: #f5f7fa;
|
|
2865
|
+
--muted: #b6bdc9;
|
|
2866
|
+
--faint: #8d96a7;
|
|
2867
|
+
--accent: #9e77ed;
|
|
2868
|
+
--accent-hover: #b692f6;
|
|
2869
|
+
--accent-ink: #ffffff;
|
|
2870
|
+
--accent-soft: #2b2142;
|
|
2871
|
+
--ok: #75e0a7;
|
|
2872
|
+
--ok-soft: #102a20;
|
|
2873
|
+
--warn: #fec84b;
|
|
2874
|
+
--warn-soft: #302611;
|
|
2875
|
+
--err: #fda29b;
|
|
2876
|
+
--err-soft: #321817;
|
|
2877
|
+
--code-key: #c3a6ff;
|
|
2878
|
+
--code-string: #75e0a7;
|
|
2879
|
+
--code-number: #84adff;
|
|
2880
|
+
--code-boolean: #f9a8d4;
|
|
2881
|
+
--code-null: #98a2b3;
|
|
2882
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
2883
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
2884
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2613
2885
|
}
|
|
2614
2886
|
@media (prefers-color-scheme: dark) {
|
|
2615
2887
|
:root:not([data-theme="light"]) {
|
|
2616
2888
|
color-scheme: dark;
|
|
2617
|
-
--bg: #
|
|
2618
|
-
--bg-elev: #
|
|
2619
|
-
--bg-muted: #
|
|
2620
|
-
--bg-inset: #
|
|
2621
|
-
--line: #
|
|
2622
|
-
--line-strong: #
|
|
2623
|
-
--ink: #
|
|
2624
|
-
--muted: #
|
|
2625
|
-
--faint: #
|
|
2626
|
-
--accent: #
|
|
2627
|
-
--accent-
|
|
2628
|
-
--accent-
|
|
2629
|
-
--
|
|
2630
|
-
--ok
|
|
2631
|
-
--
|
|
2632
|
-
--warn
|
|
2633
|
-
--
|
|
2634
|
-
--err
|
|
2635
|
-
--
|
|
2889
|
+
--bg: #0c0e14;
|
|
2890
|
+
--bg-elev: #151821;
|
|
2891
|
+
--bg-muted: #20232e;
|
|
2892
|
+
--bg-inset: #11141c;
|
|
2893
|
+
--line: #2b303d;
|
|
2894
|
+
--line-strong: #414858;
|
|
2895
|
+
--ink: #f5f7fa;
|
|
2896
|
+
--muted: #b6bdc9;
|
|
2897
|
+
--faint: #8d96a7;
|
|
2898
|
+
--accent: #9e77ed;
|
|
2899
|
+
--accent-hover: #b692f6;
|
|
2900
|
+
--accent-ink: #ffffff;
|
|
2901
|
+
--accent-soft: #2b2142;
|
|
2902
|
+
--ok: #75e0a7;
|
|
2903
|
+
--ok-soft: #102a20;
|
|
2904
|
+
--warn: #fec84b;
|
|
2905
|
+
--warn-soft: #302611;
|
|
2906
|
+
--err: #fda29b;
|
|
2907
|
+
--err-soft: #321817;
|
|
2908
|
+
--code-key: #c3a6ff;
|
|
2909
|
+
--code-string: #75e0a7;
|
|
2910
|
+
--code-number: #84adff;
|
|
2911
|
+
--code-boolean: #f9a8d4;
|
|
2912
|
+
--code-null: #98a2b3;
|
|
2913
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
2914
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
2915
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2636
2916
|
}
|
|
2637
2917
|
}
|
|
2638
2918
|
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
@@ -2650,13 +2930,17 @@ var renderAdminDocument = (service) => {
|
|
|
2650
2930
|
padding-right: env(safe-area-inset-right);
|
|
2651
2931
|
-webkit-text-size-adjust: 100%;
|
|
2652
2932
|
}
|
|
2653
|
-
|
|
2933
|
+
::selection { background: color-mix(in srgb, var(--accent) 28%, transparent); }
|
|
2934
|
+
:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px; }
|
|
2935
|
+
button, input, select, textarea { transition: border-color 140ms ease, box-shadow 140ms ease, transform 100ms ease; }
|
|
2936
|
+
button:disabled, input:disabled, select:disabled, textarea:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
2654
2937
|
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
2655
2938
|
header.top {
|
|
2656
|
-
position: sticky; top: 0; z-index:
|
|
2939
|
+
position: sticky; top: 0; z-index: 20;
|
|
2657
2940
|
display: flex; align-items: center; gap: 12px;
|
|
2658
|
-
min-height: var(--header); padding:
|
|
2659
|
-
background: var(--bg-elev);
|
|
2941
|
+
min-height: var(--header); padding: 12px 20px;
|
|
2942
|
+
background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
|
|
2943
|
+
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
2660
2944
|
border-bottom: 1px solid var(--line);
|
|
2661
2945
|
min-width: 0; max-width: 100%;
|
|
2662
2946
|
}
|
|
@@ -2664,12 +2948,21 @@ var renderAdminDocument = (service) => {
|
|
|
2664
2948
|
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
2665
2949
|
min-width: 0; max-width: 100%; margin-right: auto;
|
|
2666
2950
|
}
|
|
2667
|
-
.brand-
|
|
2668
|
-
.brand-
|
|
2951
|
+
.brand-lockup { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
2952
|
+
.brand-mark {
|
|
2953
|
+
width: 38px; height: 38px; display: grid; place-items: center; flex: none;
|
|
2954
|
+
border-radius: 11px; color: #fff;
|
|
2955
|
+
background: linear-gradient(145deg, #7f56d9 0%, #53389e 100%);
|
|
2956
|
+
box-shadow: 0 6px 16px rgb(105 65 198 / 0.24), inset 0 1px 0 rgb(255 255 255 / 0.25);
|
|
2957
|
+
}
|
|
2958
|
+
.brand-mark svg { width: 22px; height: 22px; }
|
|
2959
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; line-height: 1.25; }
|
|
2960
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
2669
2961
|
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
2670
2962
|
.vendor {
|
|
2671
2963
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
2672
2964
|
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
2965
|
+
padding: 5px 8px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-inset);
|
|
2673
2966
|
}
|
|
2674
2967
|
.vendor[hidden] { display: none; }
|
|
2675
2968
|
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
@@ -2681,12 +2974,17 @@ var renderAdminDocument = (service) => {
|
|
|
2681
2974
|
.vendor-sep { color: var(--faint); }
|
|
2682
2975
|
.vendor-link { color: inherit; text-decoration: none; }
|
|
2683
2976
|
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
2684
|
-
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items:
|
|
2685
|
-
|
|
2977
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
|
|
2978
|
+
.sync-status { align-self: center; display: flex; align-items: center; gap: 6px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
2979
|
+
.sync-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
|
|
2980
|
+
.sync-status.busy::before { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); animation: pulse 1.2s ease-in-out infinite; }
|
|
2981
|
+
label.field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; font-weight: 600; color: var(--faint); }
|
|
2686
2982
|
label.field input, label.field select, .controls input[type="password"] {
|
|
2687
|
-
background-color: var(--bg-
|
|
2688
|
-
padding:
|
|
2983
|
+
background-color: var(--bg-elev); border: 1px solid var(--line-strong); border-radius: 9px;
|
|
2984
|
+
padding: 8px 10px; min-height: 38px; line-height: 1.2; box-shadow: var(--shadow-xs);
|
|
2689
2985
|
}
|
|
2986
|
+
label.field input:hover, label.field select:hover, textarea:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--line-strong)); }
|
|
2987
|
+
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; }
|
|
2690
2988
|
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
2691
2989
|
the text fields in the same form. Paint it like those fields. */
|
|
2692
2990
|
label.field select {
|
|
@@ -2703,12 +3001,20 @@ var renderAdminDocument = (service) => {
|
|
|
2703
3001
|
dialog .field > textarea { width: 100%; }
|
|
2704
3002
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
2705
3003
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
.btn
|
|
3004
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
|
3005
|
+
border-radius: 9px; min-height: 38px; padding: 0 13px; border: 1px solid var(--line-strong);
|
|
3006
|
+
background: var(--bg-elev); color: var(--ink); font-size: 13px; font-weight: 600; line-height: 1;
|
|
3007
|
+
box-shadow: var(--shadow-xs);
|
|
3008
|
+
}
|
|
3009
|
+
.icon-btn { width: 38px; padding: 0; }
|
|
3010
|
+
.icon-btn svg, .btn svg, .btn-primary svg { width: 16px; height: 16px; }
|
|
3011
|
+
.icon-btn:hover, .btn:hover { background: var(--bg-muted); border-color: var(--line-strong); }
|
|
3012
|
+
.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; }
|
|
3013
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
3014
|
+
.btn-quiet { background: transparent; border-color: transparent; box-shadow: none; }
|
|
3015
|
+
.btn-quiet:hover { background: var(--bg-muted); }
|
|
3016
|
+
.btn-danger { color: var(--err); border-color: color-mix(in srgb, var(--err) 25%, var(--line)); }
|
|
3017
|
+
.btn-danger:hover, .btn-danger.confirming { background: var(--err-soft); border-color: color-mix(in srgb, var(--err) 45%, var(--line)); }
|
|
2712
3018
|
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
2713
3019
|
so the press itself has to stay visible. */
|
|
2714
3020
|
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
@@ -2724,93 +3030,159 @@ var renderAdminDocument = (service) => {
|
|
|
2724
3030
|
flex: 1 0 auto; min-width: 0;
|
|
2725
3031
|
}
|
|
2726
3032
|
nav.side {
|
|
2727
|
-
|
|
3033
|
+
position: sticky; top: var(--header); align-self: start; height: calc(100vh - var(--header));
|
|
3034
|
+
border-right: 1px solid var(--line); padding: 18px 12px;
|
|
2728
3035
|
display: flex; flex-direction: column; gap: 4px;
|
|
2729
|
-
background: var(--bg-elev);
|
|
3036
|
+
background: var(--bg-elev); overflow-y: auto;
|
|
2730
3037
|
}
|
|
3038
|
+
.nav-label { padding: 0 10px 8px; color: var(--faint); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
|
|
2731
3039
|
nav.side button {
|
|
3040
|
+
position: relative; display: flex; align-items: center; gap: 10px; width: 100%;
|
|
2732
3041
|
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
2733
|
-
padding: 9px 10px; min-height:
|
|
2734
|
-
}
|
|
2735
|
-
nav.side button
|
|
2736
|
-
|
|
2737
|
-
.
|
|
2738
|
-
.
|
|
2739
|
-
.
|
|
3042
|
+
padding: 9px 10px; min-height: 42px; color: var(--muted); font-size: 13px; font-weight: 550;
|
|
3043
|
+
}
|
|
3044
|
+
nav.side button:hover { background: var(--bg-muted); color: var(--ink); }
|
|
3045
|
+
nav.side button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 650; }
|
|
3046
|
+
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); }
|
|
3047
|
+
.nav-icon { width: 18px; height: 18px; display: grid; place-items: center; flex: none; }
|
|
3048
|
+
.nav-icon svg { width: 18px; height: 18px; }
|
|
3049
|
+
main { padding: 32px clamp(20px, 4vw, 48px) 64px; max-width: 1320px; width: 100%; min-width: 0; }
|
|
3050
|
+
main[aria-busy="true"] .view { opacity: 0.62; }
|
|
3051
|
+
.view { animation: view-in 180ms ease-out; }
|
|
3052
|
+
.view[hidden] { display: none; }
|
|
3053
|
+
.view-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
|
|
3054
|
+
.view h2 { margin: 0 0 5px; font-size: clamp(24px, 3vw, 30px); line-height: 1.2; letter-spacing: -0.035em; }
|
|
3055
|
+
.view h3 { margin: 24px 0 10px; font-size: 15px; letter-spacing: -0.01em; }
|
|
3056
|
+
.lede { margin: 0; color: var(--muted); max-width: 72ch; font-size: 14px; }
|
|
3057
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 175px), 1fr)); gap: 14px; }
|
|
2740
3058
|
.card {
|
|
2741
3059
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
2742
|
-
padding:
|
|
3060
|
+
padding: 18px; box-shadow: var(--shadow-sm);
|
|
2743
3061
|
}
|
|
2744
|
-
.card
|
|
2745
|
-
.card
|
|
3062
|
+
.stat-card { position: relative; overflow: hidden; }
|
|
3063
|
+
.stat-card::after { content: ""; position: absolute; inset: 0 0 auto; height: 2px; background: linear-gradient(90deg, var(--accent), transparent 78%); opacity: 0.7; }
|
|
3064
|
+
.card .k { color: var(--faint); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
3065
|
+
.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; }
|
|
2746
3066
|
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
2747
|
-
.
|
|
3067
|
+
.section-card { background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-xs); overflow: hidden; }
|
|
3068
|
+
.section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
|
|
3069
|
+
.section-title h3 { margin: 0; }
|
|
3070
|
+
.split { display: grid; grid-template-columns: minmax(0, 250px) minmax(0, 1fr); gap: 18px; align-items: start; }
|
|
3071
|
+
#collections { padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); box-shadow: var(--shadow-xs); }
|
|
2748
3072
|
.collection {
|
|
2749
3073
|
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
2750
|
-
border-radius:
|
|
3074
|
+
border-radius: 9px; padding: 10px 11px; min-height: 48px; color: var(--ink); cursor: pointer;
|
|
2751
3075
|
}
|
|
2752
|
-
.collection
|
|
3076
|
+
.collection:hover { background: var(--bg-muted); }
|
|
3077
|
+
.collection[aria-current="true"] { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 18%, var(--line)); color: var(--accent); }
|
|
2753
3078
|
.collection small { display: block; color: var(--faint); }
|
|
2754
3079
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
2755
|
-
.chip {
|
|
2756
|
-
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
2757
|
-
border-radius: 999px; padding: 4px 8px;
|
|
2758
|
-
}
|
|
3080
|
+
.chip { font-family: var(--mono); font-size: 11px; background: var(--bg-muted); border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; }
|
|
2759
3081
|
.chip i { color: var(--faint); font-style: normal; }
|
|
2760
3082
|
.sql-layout {
|
|
2761
3083
|
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
2762
3084
|
}
|
|
2763
|
-
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
2764
|
-
.sql-tables button { text-align: left; }
|
|
3085
|
+
.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); }
|
|
3086
|
+
.sql-tables button { text-align: left; border: 0; border-radius: 8px; padding: 9px 10px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
3087
|
+
.sql-tables button:hover, .sql-tables button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); }
|
|
2765
3088
|
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
2766
|
-
table { width: 100%; border-
|
|
2767
|
-
|
|
2768
|
-
th {
|
|
3089
|
+
.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); }
|
|
3090
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
3091
|
+
th, td { text-align: left; padding: 11px 13px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
3092
|
+
th { color: var(--faint); background: var(--bg-inset); font-weight: 650; font-size: 11px; text-transform: uppercase; letter-spacing: 0.045em; }
|
|
3093
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
2769
3094
|
tr.clickable { cursor: pointer; }
|
|
2770
|
-
tr.clickable:hover { background: var(--
|
|
3095
|
+
tr.clickable:hover { background: var(--accent-soft); }
|
|
2771
3096
|
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
2772
3097
|
pre {
|
|
2773
3098
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
2774
|
-
background: var(--bg-inset); border-radius: 10px; padding:
|
|
2775
|
-
}
|
|
2776
|
-
.
|
|
3099
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 10px; padding: 14px; font-size: 12px; line-height: 1.65;
|
|
3100
|
+
}
|
|
3101
|
+
.json-key { color: var(--code-key); }
|
|
3102
|
+
.json-string { color: var(--code-string); }
|
|
3103
|
+
.json-number { color: var(--code-number); }
|
|
3104
|
+
.json-boolean { color: var(--code-boolean); }
|
|
3105
|
+
.json-null { color: var(--code-null); font-style: italic; }
|
|
3106
|
+
.json-inline { display: block; max-width: 44rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
3107
|
+
.json-view { position: relative; max-height: 26rem; overflow: auto; }
|
|
3108
|
+
.json-editor { position: relative; min-height: 190px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--bg-inset); overflow: hidden; }
|
|
3109
|
+
.json-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); }
|
|
3110
|
+
.json-editor.invalid { border-color: var(--err); }
|
|
3111
|
+
.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; }
|
|
3112
|
+
.json-editor pre { position: absolute; inset: 0; pointer-events: none; background: transparent; }
|
|
3113
|
+
.json-editor textarea { position: relative; z-index: 1; resize: vertical; background: transparent; color: transparent; caret-color: var(--ink); -webkit-text-fill-color: transparent; }
|
|
3114
|
+
.json-editor textarea::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); }
|
|
3115
|
+
.editor-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; }
|
|
3116
|
+
.json-status { display: inline-flex; align-items: center; gap: 6px; color: var(--ok); font-size: 11px; }
|
|
3117
|
+
.json-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
3118
|
+
.json-status.invalid { color: var(--err); }
|
|
3119
|
+
.format-json { border: 0; padding: 3px 5px; background: transparent; color: var(--accent); font-size: 11px; font-weight: 650; cursor: pointer; }
|
|
3120
|
+
.row-actions { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 14px 0; }
|
|
2777
3121
|
.banner {
|
|
2778
|
-
display: none;
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
3122
|
+
display: none; position: sticky; top: calc(var(--header) + 12px); z-index: 15;
|
|
3123
|
+
align-items: center; gap: 10px; margin: 0 0 16px; padding: 11px 12px 11px 14px;
|
|
3124
|
+
border: 1px solid color-mix(in srgb, var(--err) 30%, var(--line)); border-radius: 10px;
|
|
3125
|
+
background: var(--err-soft); color: var(--err); box-shadow: var(--shadow-sm); font-size: 13px;
|
|
3126
|
+
}
|
|
3127
|
+
.banner.show { display: flex; }
|
|
3128
|
+
.banner span { flex: 1; }
|
|
3129
|
+
.banner button { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 7px; background: transparent; color: currentColor; cursor: pointer; }
|
|
3130
|
+
.banner button:hover { background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
3131
|
+
.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); }
|
|
2783
3132
|
dialog {
|
|
2784
|
-
border: 1px solid var(--line); border-radius:
|
|
2785
|
-
color: var(--ink); width: min(
|
|
2786
|
-
}
|
|
2787
|
-
dialog
|
|
2788
|
-
dialog
|
|
3133
|
+
border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--bg-elev);
|
|
3134
|
+
color: var(--ink); width: min(720px, calc(100% - 24px)); max-width: calc(100vw - 24px); box-shadow: var(--shadow-lg);
|
|
3135
|
+
}
|
|
3136
|
+
dialog[open] { animation: dialog-in 160ms ease-out; }
|
|
3137
|
+
dialog::backdrop { background: rgb(12 14 20 / 0.58); backdrop-filter: blur(3px); }
|
|
3138
|
+
dialog form, .dialog-body { padding: 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
3139
|
+
.dialog-heading { display: flex; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
|
3140
|
+
.dialog-heading strong { font-size: 18px; letter-spacing: -0.02em; }
|
|
3141
|
+
.dialog-heading span { color: var(--muted); font-size: 12px; }
|
|
2789
3142
|
textarea {
|
|
2790
3143
|
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
2791
3144
|
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
2792
3145
|
font-family: var(--mono); font-size: 12px;
|
|
2793
3146
|
}
|
|
2794
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
2795
|
-
.
|
|
3147
|
+
.dialog-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding-top: 4px; }
|
|
3148
|
+
.dialog-actions .btn-danger { margin-right: auto; }
|
|
3149
|
+
.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); }
|
|
2796
3150
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
2797
3151
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
2798
3152
|
.status-2 { color: var(--ok); }
|
|
2799
3153
|
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
2800
3154
|
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
3155
|
+
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
3156
|
+
.form-grid .wide { grid-column: span 2; }
|
|
2801
3157
|
.route {
|
|
2802
|
-
display: grid; grid-template-columns:
|
|
2803
|
-
padding:
|
|
2804
|
-
}
|
|
2805
|
-
.
|
|
3158
|
+
display: grid; grid-template-columns: 84px minmax(0, 1fr) auto; gap: 10px; align-items: center;
|
|
3159
|
+
padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--bg-elev);
|
|
3160
|
+
}
|
|
3161
|
+
.route:first-child { border-radius: var(--radius) var(--radius) 0 0; }
|
|
3162
|
+
.route:last-child { border-bottom: 0; border-radius: 0 0 var(--radius) var(--radius); }
|
|
3163
|
+
.route-tools { display: flex; align-items: flex-end; gap: 12px; margin-bottom: 12px; }
|
|
3164
|
+
.route-tools .field { flex: 1; }
|
|
3165
|
+
.route-tools input { width: 100%; }
|
|
3166
|
+
.route-count { padding-bottom: 10px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3167
|
+
#route-list { max-height: 34rem; border: 1px solid var(--line); border-radius: var(--radius); overflow: auto; box-shadow: var(--shadow-xs); }
|
|
3168
|
+
.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; }
|
|
3169
|
+
@keyframes view-in { from { transform: translateY(3px); } to { transform: translateY(0); } }
|
|
3170
|
+
@keyframes dialog-in { from { transform: translateY(8px) scale(0.99); } to { transform: translateY(0) scale(1); } }
|
|
3171
|
+
@keyframes pulse { 50% { opacity: 0.45; } }
|
|
3172
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
|
|
3173
|
+
@media (forced-colors: active) { .json-editor pre { display: none; } .json-editor textarea { color: CanvasText; -webkit-text-fill-color: CanvasText; } }
|
|
2806
3174
|
@media (max-width: 800px) {
|
|
3175
|
+
:root { --header: 0px; }
|
|
2807
3176
|
header.top {
|
|
2808
|
-
align-items: stretch; flex-direction: column;
|
|
2809
|
-
padding
|
|
3177
|
+
position: relative; align-items: stretch; flex-direction: column;
|
|
3178
|
+
padding: max(12px, env(safe-area-inset-top)) 14px 12px;
|
|
2810
3179
|
}
|
|
2811
3180
|
input, select, textarea { font-size: 16px; }
|
|
2812
3181
|
.brand, .vendor, .controls { width: 100%; }
|
|
3182
|
+
.brand { gap: 10px; }
|
|
3183
|
+
.vendor { order: 2; border-radius: 9px; }
|
|
2813
3184
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
3185
|
+
.sync-status { display: none; }
|
|
2814
3186
|
/* The layout fills the screen. A single-column grid stretches every auto
|
|
2815
3187
|
row, so the tab bar grew with the leftover height and the buttons floated
|
|
2816
3188
|
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
@@ -2820,14 +3192,23 @@ var renderAdminDocument = (service) => {
|
|
|
2820
3192
|
align-content: start;
|
|
2821
3193
|
}
|
|
2822
3194
|
nav.side {
|
|
2823
|
-
align-self: start; height: auto;
|
|
3195
|
+
position: sticky; top: 0; z-index: 12; align-self: start; height: auto;
|
|
2824
3196
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
2825
3197
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
2826
|
-
padding: 8px;
|
|
3198
|
+
padding: 8px; box-shadow: var(--shadow-xs);
|
|
2827
3199
|
}
|
|
2828
|
-
nav
|
|
3200
|
+
.nav-label { display: none; }
|
|
3201
|
+
nav.side button { width: auto; white-space: nowrap; flex: none; }
|
|
3202
|
+
nav.side button[aria-current="true"]::before { inset: auto 9px -8px; width: auto; height: 3px; border-radius: 3px 3px 0 0; }
|
|
2829
3203
|
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
2830
|
-
|
|
3204
|
+
.form-grid { grid-template-columns: minmax(0, 1fr); }
|
|
3205
|
+
.form-grid .wide { grid-column: auto; }
|
|
3206
|
+
main { padding: 22px 14px 48px; }
|
|
3207
|
+
.view-head { margin-bottom: 18px; }
|
|
3208
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
3209
|
+
.card { padding: 14px; }
|
|
3210
|
+
.card .v { font-size: 20px; }
|
|
3211
|
+
.table-shell { border: 0; background: transparent; box-shadow: none; overflow: visible; }
|
|
2831
3212
|
table, thead, tbody, tr, th, td { display: block; }
|
|
2832
3213
|
thead { display: none; }
|
|
2833
3214
|
tr {
|
|
@@ -2842,50 +3223,63 @@ var renderAdminDocument = (service) => {
|
|
|
2842
3223
|
.route { grid-template-columns: 72px 1fr; }
|
|
2843
3224
|
.route button { grid-column: 1 / -1; }
|
|
2844
3225
|
}
|
|
3226
|
+
@media (max-width: 460px) {
|
|
3227
|
+
.controls label.field { min-width: calc(50% - 4px); }
|
|
3228
|
+
.controls .icon-btn, .controls .btn { flex: 1; }
|
|
3229
|
+
.grid { grid-template-columns: minmax(0, 1fr); }
|
|
3230
|
+
.dialog-actions { flex-wrap: wrap; }
|
|
3231
|
+
.dialog-actions button { flex: 1; }
|
|
3232
|
+
.dialog-actions .btn-danger { flex-basis: 100%; margin-right: 0; }
|
|
3233
|
+
}
|
|
2845
3234
|
</style>
|
|
2846
3235
|
</head>
|
|
2847
3236
|
<body>
|
|
2848
3237
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
2849
3238
|
<header class="top">
|
|
2850
3239
|
<div class="brand">
|
|
2851
|
-
<div class="brand-
|
|
2852
|
-
<
|
|
2853
|
-
|
|
3240
|
+
<div class="brand-lockup">
|
|
3241
|
+
<div class="brand-mark" aria-hidden="true">
|
|
3242
|
+
<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>
|
|
3243
|
+
</div>
|
|
3244
|
+
<div class="brand-id">
|
|
3245
|
+
<strong>${name}</strong>
|
|
3246
|
+
<span>Mock admin</span>
|
|
3247
|
+
</div>
|
|
2854
3248
|
</div>
|
|
2855
3249
|
<div class="vendor" id="vendor" hidden></div>
|
|
2856
3250
|
</div>
|
|
2857
3251
|
<div class="controls">
|
|
3252
|
+
<span class="sync-status" id="sync-status" role="status" aria-live="polite">Ready</span>
|
|
2858
3253
|
<label class="field">Namespace
|
|
2859
3254
|
<select id="namespace" aria-label="Namespace"></select>
|
|
2860
3255
|
</label>
|
|
2861
3256
|
<label class="field">Admin key
|
|
2862
3257
|
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
2863
3258
|
</label>
|
|
2864
|
-
<button class="icon-btn" id="theme" type="button" aria-label="
|
|
2865
|
-
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
3259
|
+
<button class="icon-btn" id="theme" type="button" aria-label="Use dark theme" title="Use dark theme"></button>
|
|
3260
|
+
<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>
|
|
2866
3261
|
</div>
|
|
2867
3262
|
</header>
|
|
2868
3263
|
<div class="layout">
|
|
2869
3264
|
<nav class="side" id="nav" aria-label="Admin sections">
|
|
2870
|
-
<
|
|
2871
|
-
<button type="button" data-view="
|
|
2872
|
-
<button type="button" data-view="
|
|
2873
|
-
<button type="button" data-view="
|
|
2874
|
-
<button type="button" data-view="
|
|
2875
|
-
<button type="button" data-view="
|
|
3265
|
+
<span class="nav-label">Workspace</span>
|
|
3266
|
+
<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>
|
|
3267
|
+
<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>
|
|
3268
|
+
<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>
|
|
3269
|
+
<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>
|
|
3270
|
+
<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>
|
|
3271
|
+
<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>
|
|
2876
3272
|
</nav>
|
|
2877
3273
|
<main id="views">
|
|
2878
|
-
<
|
|
3274
|
+
<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>
|
|
2879
3275
|
<section class="view" id="view-overview">
|
|
2880
|
-
<h2>Overview</h2>
|
|
2881
|
-
<p class="lede">Health, clock, and the collections this namespace is holding.</p>
|
|
3276
|
+
<div class="view-head"><div><h2>Overview</h2><p class="lede">Health, clock, and the collections this namespace is holding.</p></div></div>
|
|
2882
3277
|
<div class="grid" id="overview-cards"></div>
|
|
2883
3278
|
<h3>Recent requests</h3>
|
|
2884
3279
|
<div id="overview-requests"></div>
|
|
2885
3280
|
</section>
|
|
2886
3281
|
<section class="view" id="view-state" hidden>
|
|
2887
|
-
<h2>State</h2>
|
|
2888
|
-
<p class="lede">Every collection in this namespace. Declared fields stay visible before any row exists; stored rows add whatever else they contain.</p>
|
|
3282
|
+
<div class="view-head"><div><h2>State</h2><p class="lede">Inspect and edit every collection in this namespace.</p></div></div>
|
|
2889
3283
|
<div class="split">
|
|
2890
3284
|
<div id="collections"></div>
|
|
2891
3285
|
<div>
|
|
@@ -2898,8 +3292,7 @@ var renderAdminDocument = (service) => {
|
|
|
2898
3292
|
</div>
|
|
2899
3293
|
</section>
|
|
2900
3294
|
<section class="view" id="view-clock" hidden>
|
|
2901
|
-
<h2>Clock</h2>
|
|
2902
|
-
<p class="lede">The clock every timestamp in this mock reads.</p>
|
|
3295
|
+
<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>
|
|
2903
3296
|
<div class="card" id="clock-readout"></div>
|
|
2904
3297
|
<div class="row-actions">
|
|
2905
3298
|
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
@@ -2918,21 +3311,22 @@ var renderAdminDocument = (service) => {
|
|
|
2918
3311
|
</form>
|
|
2919
3312
|
</section>
|
|
2920
3313
|
<section class="view" id="view-faults" hidden>
|
|
2921
|
-
<h2>Faults</h2>
|
|
2922
|
-
<p class="lede">Inject a response, a delay, or a named preset. Rules apply to this namespace.</p>
|
|
3314
|
+
<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>
|
|
2923
3315
|
<form id="fault-form" class="card stack">
|
|
2924
|
-
<
|
|
2925
|
-
<
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
<
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
<
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
<
|
|
2935
|
-
|
|
3316
|
+
<div class="form-grid">
|
|
3317
|
+
<label class="field wide">Preset
|
|
3318
|
+
<select id="fault-preset"><option value="">Custom rule</option></select>
|
|
3319
|
+
</label>
|
|
3320
|
+
<label class="field">Status
|
|
3321
|
+
<input id="fault-status" inputmode="numeric" placeholder="503">
|
|
3322
|
+
</label>
|
|
3323
|
+
<label class="field wide">Path prefix
|
|
3324
|
+
<input id="fault-path" placeholder="/v1">
|
|
3325
|
+
</label>
|
|
3326
|
+
<label class="field">Method
|
|
3327
|
+
<input id="fault-method" placeholder="GET">
|
|
3328
|
+
</label>
|
|
3329
|
+
</div>
|
|
2936
3330
|
<div class="dialog-actions">
|
|
2937
3331
|
<button class="btn-primary" type="submit">Add fault</button>
|
|
2938
3332
|
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
@@ -2941,26 +3335,31 @@ var renderAdminDocument = (service) => {
|
|
|
2941
3335
|
<div id="fault-list"></div>
|
|
2942
3336
|
</section>
|
|
2943
3337
|
<section class="view" id="view-journal" hidden>
|
|
2944
|
-
<h2>Journal</h2>
|
|
2945
|
-
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
2946
|
-
<div class="row-actions">
|
|
2947
|
-
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
2948
|
-
</div>
|
|
3338
|
+
<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>
|
|
2949
3339
|
<div id="journal"></div>
|
|
2950
3340
|
</section>
|
|
2951
3341
|
<section class="view" id="view-routes" hidden>
|
|
2952
|
-
<h2>Routes</h2>
|
|
2953
|
-
<
|
|
3342
|
+
<div class="view-head"><div><h2>Routes</h2><p class="lede">Explore shared controls and service-specific admin endpoints.</p></div></div>
|
|
3343
|
+
<div class="route-tools">
|
|
3344
|
+
<label class="field">Filter routes
|
|
3345
|
+
<input id="route-search" type="search" placeholder="Method, path, or extension" autocomplete="off">
|
|
3346
|
+
</label>
|
|
3347
|
+
<span class="route-count" id="route-count"></span>
|
|
3348
|
+
</div>
|
|
2954
3349
|
<div id="route-list"></div>
|
|
2955
|
-
<form id="route-form" class="card stack">
|
|
3350
|
+
<form id="route-form" class="card stack" style="margin-top: 18px">
|
|
2956
3351
|
<strong id="route-title">Select a route</strong>
|
|
2957
3352
|
<label class="field">JSON body
|
|
2958
|
-
<
|
|
3353
|
+
<div class="json-editor" id="route-editor" hidden>
|
|
3354
|
+
<pre aria-hidden="true"><code id="route-highlight"></code></pre>
|
|
3355
|
+
<textarea id="route-body" placeholder="{ }" spellcheck="false" aria-describedby="route-json-status" disabled></textarea>
|
|
3356
|
+
</div>
|
|
3357
|
+
<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>
|
|
2959
3358
|
</label>
|
|
2960
3359
|
<div class="dialog-actions">
|
|
2961
|
-
<button class="btn-primary" type="submit">Send</button>
|
|
3360
|
+
<button class="btn-primary" id="route-send" type="submit" disabled>Send request</button>
|
|
2962
3361
|
</div>
|
|
2963
|
-
<pre id="route-result">Response will show here.</pre>
|
|
3362
|
+
<pre class="json-view" id="route-result"><code>Response will show here.</code></pre>
|
|
2964
3363
|
</form>
|
|
2965
3364
|
</section>
|
|
2966
3365
|
</main>
|
|
@@ -2968,12 +3367,16 @@ var renderAdminDocument = (service) => {
|
|
|
2968
3367
|
</div>
|
|
2969
3368
|
<dialog id="editor">
|
|
2970
3369
|
<form method="dialog" id="editor-form">
|
|
2971
|
-
<strong id="editor-title">Record</strong>
|
|
3370
|
+
<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>
|
|
2972
3371
|
<label class="field">Id
|
|
2973
3372
|
<input id="editor-id" required>
|
|
2974
3373
|
</label>
|
|
2975
3374
|
<label class="field">JSON value
|
|
2976
|
-
<
|
|
3375
|
+
<div class="json-editor" id="record-editor">
|
|
3376
|
+
<pre aria-hidden="true"><code id="editor-highlight"></code></pre>
|
|
3377
|
+
<textarea id="editor-value" spellcheck="false" aria-describedby="editor-json-status"></textarea>
|
|
3378
|
+
</div>
|
|
3379
|
+
<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>
|
|
2977
3380
|
</label>
|
|
2978
3381
|
<div class="dialog-actions">
|
|
2979
3382
|
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
@@ -3015,8 +3418,8 @@ var describeExtension = (extension) => {
|
|
|
3015
3418
|
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
3016
3419
|
};
|
|
3017
3420
|
};
|
|
3018
|
-
var adminUiRoutes = (service, ui) => {
|
|
3019
|
-
const shell = () => renderAdminDocument(service);
|
|
3421
|
+
var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
3422
|
+
const shell = () => renderAdminDocument(service, adminPrefix);
|
|
3020
3423
|
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
3021
3424
|
return {
|
|
3022
3425
|
"GET /ui": () => htmlResponse(document3()),
|
|
@@ -3937,10 +4340,13 @@ var stateAdminRoutes = (routes) => {
|
|
|
3937
4340
|
};
|
|
3938
4341
|
|
|
3939
4342
|
// ../core/dist/version.js
|
|
3940
|
-
var PACKAGE_VERSION = true ? "
|
|
4343
|
+
var PACKAGE_VERSION = true ? "2.0.0" : UNRELEASED_VERSION;
|
|
3941
4344
|
|
|
3942
4345
|
// ../core/dist/webhooks.js
|
|
3943
|
-
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4346
|
+
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4347
|
+
status,
|
|
4348
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4349
|
+
});
|
|
3944
4350
|
var adminError3 = (status, message) => json4(status, { error: { type: "mockingbird_admin", message } });
|
|
3945
4351
|
var isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3946
4352
|
var parseEndpoint = (value) => {
|
|
@@ -4048,7 +4454,6 @@ var AT_HEADER = "x-mockingbird-at";
|
|
|
4048
4454
|
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
4049
4455
|
var DEFAULT_NAMESPACE = "default";
|
|
4050
4456
|
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4051
|
-
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
4052
4457
|
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4053
4458
|
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
4054
4459
|
var effects = /* @__PURE__ */ new WeakMap();
|
|
@@ -4087,6 +4492,22 @@ var operationMatcher = (document3) => {
|
|
|
4087
4492
|
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
4088
4493
|
};
|
|
4089
4494
|
var createRuntime = (options) => {
|
|
4495
|
+
const adminPrefix = resolveAdminPrefix(options.adminPrefix);
|
|
4496
|
+
const internalPaths = Object.entries(options.document?.paths ?? {}).filter(([, item]) => item["x-mockingbird-internal"] === true).map(([path]) => path);
|
|
4497
|
+
for (const path of internalPaths) {
|
|
4498
|
+
if (!path.startsWith(`${ADMIN_PREFIX}/blobs/`))
|
|
4499
|
+
throw new Error(`internal transport ${path} must live under ${ADMIN_PREFIX}/blobs/`);
|
|
4500
|
+
}
|
|
4501
|
+
assertAdminPrefixAvailable(adminPrefix, Object.keys(options.document?.paths ?? {}).filter((path) => !internalPaths.includes(path)));
|
|
4502
|
+
const internalMatch = (path) => {
|
|
4503
|
+
for (const pattern of internalPaths) {
|
|
4504
|
+
const actual = path.split("/");
|
|
4505
|
+
const expected = pattern.split("/");
|
|
4506
|
+
if (actual.length === expected.length && expected.every((part, i) => part.startsWith("{") || part === actual[i]))
|
|
4507
|
+
return pattern;
|
|
4508
|
+
}
|
|
4509
|
+
return void 0;
|
|
4510
|
+
};
|
|
4090
4511
|
assertAdminUi(options.adminUi);
|
|
4091
4512
|
const sqlite = bootSqlite(options.sqlite);
|
|
4092
4513
|
const clock = options.clock ?? createClock();
|
|
@@ -4101,6 +4522,20 @@ var createRuntime = (options) => {
|
|
|
4101
4522
|
const instances = /* @__PURE__ */ new Map();
|
|
4102
4523
|
const publicNamespaces = /* @__PURE__ */ new Set();
|
|
4103
4524
|
const branchRngs = /* @__PURE__ */ new Map();
|
|
4525
|
+
const namespaceClocks = /* @__PURE__ */ new Map();
|
|
4526
|
+
const namespaceForStorage = /* @__PURE__ */ new Map();
|
|
4527
|
+
let isolatedNamespaces = false;
|
|
4528
|
+
const namespaceClock = (name = DEFAULT_NAMESPACE) => {
|
|
4529
|
+
if (!isolatedNamespaces || name === DEFAULT_NAMESPACE)
|
|
4530
|
+
return clock;
|
|
4531
|
+
let found = namespaceClocks.get(name);
|
|
4532
|
+
if (!found) {
|
|
4533
|
+
found = createClock();
|
|
4534
|
+
namespaceClocks.set(name, found);
|
|
4535
|
+
}
|
|
4536
|
+
return found;
|
|
4537
|
+
};
|
|
4538
|
+
const fleetPoints = /* @__PURE__ */ new WeakMap();
|
|
4104
4539
|
const timelines = /* @__PURE__ */ new Map();
|
|
4105
4540
|
const branchStorage = /* @__PURE__ */ new Map();
|
|
4106
4541
|
const captured = /* @__PURE__ */ new Map();
|
|
@@ -4114,17 +4549,22 @@ var createRuntime = (options) => {
|
|
|
4114
4549
|
if (!NAMESPACE_PATTERN.test(key) || !NAMESPACE_PATTERN.test(publicNamespace)) {
|
|
4115
4550
|
throw new RangeError(`namespace must match ${NAMESPACE_PATTERN}: ${JSON.stringify(publicNamespace)}`);
|
|
4116
4551
|
}
|
|
4552
|
+
const createdContextRng = isolatedRng ?? (isolatedNamespaces && key !== DEFAULT_NAMESPACE ? createRng(options.seed ?? 0) : rng);
|
|
4117
4553
|
const created = options.create({
|
|
4118
4554
|
namespace: storageNamespace(key),
|
|
4119
4555
|
publicNamespace,
|
|
4556
|
+
adminPrefix,
|
|
4120
4557
|
sqlite,
|
|
4121
|
-
clock,
|
|
4122
|
-
rng:
|
|
4558
|
+
clock: namespaceClock(publicNamespace),
|
|
4559
|
+
rng: createdContextRng
|
|
4123
4560
|
});
|
|
4124
4561
|
instances.set(key, created);
|
|
4562
|
+
namespaceForStorage.set(key, publicNamespace);
|
|
4125
4563
|
publicNamespaces.add(publicNamespace);
|
|
4126
4564
|
if (isolatedRng)
|
|
4127
4565
|
branchRngs.set(key, isolatedRng);
|
|
4566
|
+
else if (isolatedNamespaces && key !== DEFAULT_NAMESPACE)
|
|
4567
|
+
branchRngs.set(key, createdContextRng);
|
|
4128
4568
|
return created;
|
|
4129
4569
|
};
|
|
4130
4570
|
const instance = (name = DEFAULT_NAMESPACE) => instanceFor(name);
|
|
@@ -4142,7 +4582,7 @@ var createRuntime = (options) => {
|
|
|
4142
4582
|
captured.set(storage, snapshot2);
|
|
4143
4583
|
return Object.freeze({
|
|
4144
4584
|
snapshot: snapshot2,
|
|
4145
|
-
clock: Object.freeze(
|
|
4585
|
+
clock: Object.freeze(namespaceClock(namespaceForStorage.get(storage) ?? storage).state()),
|
|
4146
4586
|
rngState: (branchRngs.get(storage) ?? rng).state()
|
|
4147
4587
|
});
|
|
4148
4588
|
};
|
|
@@ -4152,7 +4592,7 @@ var createRuntime = (options) => {
|
|
|
4152
4592
|
return found;
|
|
4153
4593
|
instance(name);
|
|
4154
4594
|
found = new Timeline({
|
|
4155
|
-
now:
|
|
4595
|
+
now: namespaceClock(name).now,
|
|
4156
4596
|
...options.maxCheckpoints !== void 0 ? { maxCheckpoints: options.maxCheckpoints } : {}
|
|
4157
4597
|
});
|
|
4158
4598
|
found.commit(capture(name));
|
|
@@ -4179,12 +4619,13 @@ var createRuntime = (options) => {
|
|
|
4179
4619
|
const point = history2.checkout("main", at);
|
|
4180
4620
|
restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
|
|
4181
4621
|
captured.set(namespace, point.value.snapshot);
|
|
4182
|
-
rng.setState(point.value.rngState);
|
|
4183
|
-
|
|
4622
|
+
(branchRngs.get(namespace) ?? rng).setState(point.value.rngState);
|
|
4623
|
+
const selectedClock = namespaceClock(namespace);
|
|
4624
|
+
selectedClock.set(point.value.clock.now);
|
|
4184
4625
|
if (point.value.clock.frozen)
|
|
4185
|
-
|
|
4626
|
+
selectedClock.freeze();
|
|
4186
4627
|
else
|
|
4187
|
-
|
|
4628
|
+
selectedClock.unfreeze();
|
|
4188
4629
|
}
|
|
4189
4630
|
return namespace;
|
|
4190
4631
|
}
|
|
@@ -4242,11 +4683,12 @@ var createRuntime = (options) => {
|
|
|
4242
4683
|
const storage = ensureBranch(namespace, branchName);
|
|
4243
4684
|
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
4244
4685
|
captured.set(storage, point.value.snapshot);
|
|
4245
|
-
|
|
4686
|
+
const selectedClock = namespaceClock(namespace);
|
|
4687
|
+
selectedClock.set(point.value.clock.now);
|
|
4246
4688
|
if (point.value.clock.frozen)
|
|
4247
|
-
|
|
4689
|
+
selectedClock.freeze();
|
|
4248
4690
|
else
|
|
4249
|
-
|
|
4691
|
+
selectedClock.unfreeze();
|
|
4250
4692
|
(branchRngs.get(storage) ?? rng).setState(point.value.rngState);
|
|
4251
4693
|
};
|
|
4252
4694
|
const reset = async (name = DEFAULT_NAMESPACE) => {
|
|
@@ -4254,10 +4696,21 @@ var createRuntime = (options) => {
|
|
|
4254
4696
|
options.webhooks?.clear();
|
|
4255
4697
|
for (const each of instances.values())
|
|
4256
4698
|
await each.reset();
|
|
4699
|
+
for (const each of branchRngs.values())
|
|
4700
|
+
each.reset();
|
|
4257
4701
|
timelines.clear();
|
|
4702
|
+
for (const storage of branchStorage.values()) {
|
|
4703
|
+
instances.delete(storage);
|
|
4704
|
+
branchRngs.delete(storage);
|
|
4705
|
+
namespaceForStorage.delete(storage);
|
|
4706
|
+
}
|
|
4258
4707
|
branchStorage.clear();
|
|
4259
|
-
branchRngs.clear();
|
|
4260
4708
|
captured.clear();
|
|
4709
|
+
faults.clear();
|
|
4710
|
+
clock.reset();
|
|
4711
|
+
rng.reset();
|
|
4712
|
+
for (const each of namespaceClocks.values())
|
|
4713
|
+
each.reset();
|
|
4261
4714
|
return;
|
|
4262
4715
|
}
|
|
4263
4716
|
options.webhooks?.clear(name);
|
|
@@ -4275,11 +4728,18 @@ var createRuntime = (options) => {
|
|
|
4275
4728
|
else
|
|
4276
4729
|
clearNamespace(sqlite, storageNamespace(storage));
|
|
4277
4730
|
branchStorage.delete(mapping);
|
|
4731
|
+
instances.delete(storage);
|
|
4732
|
+
namespaceForStorage.delete(storage);
|
|
4278
4733
|
branchRngs.delete(storage);
|
|
4279
4734
|
captured.delete(storage);
|
|
4280
4735
|
}
|
|
4281
4736
|
timelines.delete(name);
|
|
4282
4737
|
captured.delete(name);
|
|
4738
|
+
faults.restore(name, { rules: [], rngState: createRng(options.seed ?? 0).state() });
|
|
4739
|
+
namespaceClock(name).reset();
|
|
4740
|
+
(branchRngs.get(name) ?? rng).reset();
|
|
4741
|
+
journal.clear(name);
|
|
4742
|
+
metrics.reset(name);
|
|
4283
4743
|
};
|
|
4284
4744
|
const snapshot = (name = DEFAULT_NAMESPACE) => {
|
|
4285
4745
|
return checkpoint(name, "main").value.snapshot;
|
|
@@ -4317,6 +4777,53 @@ var createRuntime = (options) => {
|
|
|
4317
4777
|
rng,
|
|
4318
4778
|
credentials,
|
|
4319
4779
|
webhooks: options.webhooks,
|
|
4780
|
+
isolateNamespaces() {
|
|
4781
|
+
if (publicNamespaces.size > 1)
|
|
4782
|
+
throw new Error("enable namespace isolation before creating named namespaces");
|
|
4783
|
+
isolatedNamespaces = true;
|
|
4784
|
+
faults.isolateNamespaces();
|
|
4785
|
+
},
|
|
4786
|
+
namespaceClock,
|
|
4787
|
+
namespaceOf(request) {
|
|
4788
|
+
const url = new URL(request.url);
|
|
4789
|
+
const prefixed = matchNamespacePath(url.pathname, adminPrefix);
|
|
4790
|
+
const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
|
|
4791
|
+
const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
|
|
4792
|
+
if (isAdminPath(path, adminPrefix))
|
|
4793
|
+
return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
|
|
4794
|
+
if (selected !== void 0)
|
|
4795
|
+
return selected;
|
|
4796
|
+
const credential = options.credential?.(request);
|
|
4797
|
+
return (credential !== void 0 ? credentials.get(credential) : void 0) ?? DEFAULT_NAMESPACE;
|
|
4798
|
+
},
|
|
4799
|
+
fleetSnapshot(namespace) {
|
|
4800
|
+
instance(namespace);
|
|
4801
|
+
const handle = {};
|
|
4802
|
+
fleetPoints.set(handle, {
|
|
4803
|
+
namespace,
|
|
4804
|
+
state: capture(namespace),
|
|
4805
|
+
faults: faults.snapshot(namespace),
|
|
4806
|
+
webhooks: options.webhooks?.snapshot(namespace)
|
|
4807
|
+
});
|
|
4808
|
+
return handle;
|
|
4809
|
+
},
|
|
4810
|
+
fleetRestore(handle, namespace) {
|
|
4811
|
+
const point = typeof handle === "object" && handle !== null ? fleetPoints.get(handle) : void 0;
|
|
4812
|
+
if (!point || point.namespace !== namespace)
|
|
4813
|
+
throw new Error("invalid fleet checkpoint");
|
|
4814
|
+
restoreNamespace(sqlite, storageNamespace(namespace), point.state.snapshot);
|
|
4815
|
+
captured.set(namespace, point.state.snapshot);
|
|
4816
|
+
const selectedClock = namespaceClock(namespace);
|
|
4817
|
+
selectedClock.freeze();
|
|
4818
|
+
selectedClock.set(point.state.clock.now);
|
|
4819
|
+
if (!point.state.clock.frozen)
|
|
4820
|
+
selectedClock.unfreeze();
|
|
4821
|
+
(branchRngs.get(namespace) ?? rng).setState(point.state.rngState);
|
|
4822
|
+
faults.restore(namespace, point.faults);
|
|
4823
|
+
if (point.webhooks !== void 0)
|
|
4824
|
+
options.webhooks?.restore(namespace, point.webhooks);
|
|
4825
|
+
timelines.delete(namespace);
|
|
4826
|
+
},
|
|
4320
4827
|
applyPreset: (name, namespace = DEFAULT_NAMESPACE, overrides = {}) => {
|
|
4321
4828
|
const preset = options.presets?.[name];
|
|
4322
4829
|
if (!preset)
|
|
@@ -4348,7 +4855,7 @@ var createRuntime = (options) => {
|
|
|
4348
4855
|
state: (namespace = DEFAULT_NAMESPACE) => inspectState(stateScope(namespace)),
|
|
4349
4856
|
fetch: async (incoming) => {
|
|
4350
4857
|
let request = incoming;
|
|
4351
|
-
const prefixed =
|
|
4858
|
+
const prefixed = matchNamespacePath(new URL(request.url).pathname, adminPrefix);
|
|
4352
4859
|
if (prefixed) {
|
|
4353
4860
|
const url2 = new URL(request.url);
|
|
4354
4861
|
url2.pathname = prefixed[2] ?? "/";
|
|
@@ -4364,7 +4871,15 @@ var createRuntime = (options) => {
|
|
|
4364
4871
|
signal: request.signal
|
|
4365
4872
|
});
|
|
4366
4873
|
}
|
|
4367
|
-
|
|
4874
|
+
const internalUrl = new URL(request.url);
|
|
4875
|
+
const internalPath = isAdminPath(internalUrl.pathname, adminPrefix) ? `${ADMIN_PREFIX}${internalUrl.pathname.slice(adminPrefix.length)}` : "";
|
|
4876
|
+
const internal = internalMatch(internalPath);
|
|
4877
|
+
const obsoleteInternalPath = adminPrefix !== ADMIN_PREFIX && internalMatch(internalUrl.pathname) && !internal;
|
|
4878
|
+
if (internal) {
|
|
4879
|
+
internalUrl.pathname = internalPath;
|
|
4880
|
+
request = new Request(internalUrl, request);
|
|
4881
|
+
}
|
|
4882
|
+
let namespace = internal ? internalUrl.searchParams.get("namespace") ?? control.namespaceOf(request) : control.namespaceOf(request);
|
|
4368
4883
|
if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
4369
4884
|
const credential = options.credential(request);
|
|
4370
4885
|
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
@@ -4384,7 +4899,9 @@ var createRuntime = (options) => {
|
|
|
4384
4899
|
return copy;
|
|
4385
4900
|
}
|
|
4386
4901
|
};
|
|
4387
|
-
|
|
4902
|
+
if (obsoleteInternalPath)
|
|
4903
|
+
return stamp(adminFail(404, "internal transport is only available under adminPrefix"));
|
|
4904
|
+
const handled = internal ? void 0 : await control.handle(request);
|
|
4388
4905
|
if (handled)
|
|
4389
4906
|
return stamp(handled);
|
|
4390
4907
|
const started = monotonicNow();
|
|
@@ -4425,7 +4942,7 @@ var createRuntime = (options) => {
|
|
|
4425
4942
|
type: "mockingbird_admin",
|
|
4426
4943
|
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
4427
4944
|
}
|
|
4428
|
-
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
4945
|
+
}), { status: 400, headers: { "content-type": "application/json; charset=utf-8" } }));
|
|
4429
4946
|
}
|
|
4430
4947
|
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
4431
4948
|
log(400);
|
|
@@ -4492,6 +5009,7 @@ var createRuntime = (options) => {
|
|
|
4492
5009
|
startedAt: wallNow(),
|
|
4493
5010
|
wallNow,
|
|
4494
5011
|
clock,
|
|
5012
|
+
namespaceClock,
|
|
4495
5013
|
faults,
|
|
4496
5014
|
metrics,
|
|
4497
5015
|
journal,
|
|
@@ -4541,15 +5059,16 @@ var createRuntime = (options) => {
|
|
|
4541
5059
|
...options.admin?.(runtime) ?? {},
|
|
4542
5060
|
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
4543
5061
|
// maps `presets`, and a service route must not replace that list with a record.
|
|
4544
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
5062
|
+
...options.presets ? presetRoutes(options.presets, runtime, adminPrefix) : {},
|
|
4545
5063
|
...stateAdminRoutes({
|
|
4546
5064
|
open: stateScope,
|
|
4547
5065
|
afterWrite: (namespace) => {
|
|
4548
5066
|
checkpoint(namespace, "main");
|
|
4549
5067
|
}
|
|
4550
5068
|
}),
|
|
4551
|
-
...adminUiRoutes(options.name, options.adminUi)
|
|
5069
|
+
...adminUiRoutes(options.name, options.adminUi, adminPrefix)
|
|
4552
5070
|
},
|
|
5071
|
+
adminPrefix,
|
|
4553
5072
|
adminKey: options.adminKey
|
|
4554
5073
|
});
|
|
4555
5074
|
return defineMock(runtime);
|
|
@@ -4563,7 +5082,10 @@ var mutableResponse = (response) => {
|
|
|
4563
5082
|
return new Response(response.body, response);
|
|
4564
5083
|
}
|
|
4565
5084
|
};
|
|
4566
|
-
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5085
|
+
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5086
|
+
status,
|
|
5087
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
5088
|
+
});
|
|
4567
5089
|
var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
|
|
4568
5090
|
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4569
5091
|
var credentialRoutes = (registry) => ({
|
|
@@ -4615,7 +5137,7 @@ var credentialRoutes = (registry) => ({
|
|
|
4615
5137
|
return adminJson(200, { status: "ok" });
|
|
4616
5138
|
}
|
|
4617
5139
|
});
|
|
4618
|
-
var presetRoutes = (presets, runtime) => ({
|
|
5140
|
+
var presetRoutes = (presets, runtime, adminPrefix) => ({
|
|
4619
5141
|
"GET /faults/presets": () => {
|
|
4620
5142
|
const body = {
|
|
4621
5143
|
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
@@ -4625,7 +5147,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
4625
5147
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
4626
5148
|
const name = params.name;
|
|
4627
5149
|
if (!presets[name])
|
|
4628
|
-
return adminFail(404, `no fault preset ${name}; GET /
|
|
5150
|
+
return adminFail(404, `no fault preset ${name}; GET ${adminPrefix}/faults/presets`);
|
|
4629
5151
|
const overrides = isObject(body) ? body : {};
|
|
4630
5152
|
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
4631
5153
|
}
|
|
@@ -4701,6 +5223,7 @@ var createRuntime2 = (options = {}) => createRuntime({
|
|
|
4701
5223
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
4702
5224
|
...options.clock ? { clock: options.clock } : {},
|
|
4703
5225
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
5226
|
+
...options.adminPrefix !== void 0 ? { adminPrefix: options.adminPrefix } : {},
|
|
4704
5227
|
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
4705
5228
|
...options.onLog ? { onLog: options.onLog } : {},
|
|
4706
5229
|
credential: accessKeyCredential,
|
|
@@ -4960,6 +5483,9 @@ var StepFunctionsAPI = class {
|
|
|
4960
5483
|
};
|
|
4961
5484
|
|
|
4962
5485
|
export {
|
|
5486
|
+
createClock,
|
|
5487
|
+
resolveAdminPrefix,
|
|
5488
|
+
parseDuration,
|
|
4963
5489
|
document2 as document,
|
|
4964
5490
|
operationIds,
|
|
4965
5491
|
supportedOperationIds,
|
|
@@ -4969,4 +5495,4 @@ export {
|
|
|
4969
5495
|
accessKeyCredential,
|
|
4970
5496
|
StepFunctionsAPI
|
|
4971
5497
|
};
|
|
4972
|
-
//# sourceMappingURL=chunk-
|
|
5498
|
+
//# sourceMappingURL=chunk-EBPLA3FJ.js.map
|