@crvouga/mockingbird-service-livekit 1.7.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/DISCOVERY.md +55 -0
- package/README.md +1 -1
- package/SUPPORT.md +13 -0
- package/dist/chunk-DLPZY3HQ.js +821 -0
- package/dist/chunk-DLPZY3HQ.js.map +7 -0
- package/dist/{chunk-7OCMDPZ6.js → chunk-QIRIAV7T.js} +822 -258
- package/dist/chunk-QIRIAV7T.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 +37 -0
- package/package.json +29 -4
- package/dist/chunk-7OCMDPZ6.js.map +0 -7
- package/dist/chunk-KYQFKDDM.js +0 -334
- package/dist/chunk-KYQFKDDM.js.map +0 -7
|
@@ -110,13 +110,31 @@ var Collection = class {
|
|
|
110
110
|
};
|
|
111
111
|
|
|
112
112
|
// ../core/dist/control.js
|
|
113
|
-
var HEALTH_PATH = "/health";
|
|
114
113
|
var ADMIN_PREFIX = "/__admin";
|
|
114
|
+
var resolveAdminPrefix = (prefix = ADMIN_PREFIX) => {
|
|
115
|
+
if (!/^\/[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*$/.test(prefix)) {
|
|
116
|
+
throw new Error("adminPrefix must be an absolute non-root path of literal segments, without a trailing slash");
|
|
117
|
+
}
|
|
118
|
+
return prefix;
|
|
119
|
+
};
|
|
120
|
+
var isAdminPath = (path, prefix) => path === prefix || path.startsWith(`${prefix}/`);
|
|
121
|
+
var matchNamespacePath = (path, prefix) => /^\/ns\/([^/]+)(\/.*)?$/.exec(isAdminPath(path, prefix) ? path.slice(prefix.length) : "");
|
|
122
|
+
var assertAdminPrefixAvailable = (prefix, paths) => {
|
|
123
|
+
const reserved = prefix.split("/").slice(1);
|
|
124
|
+
for (const path of paths) {
|
|
125
|
+
const segments = path.split("/").slice(1);
|
|
126
|
+
if (segments.length < reserved.length || /^\{[^}]+\}$/.test(segments[0] ?? ""))
|
|
127
|
+
continue;
|
|
128
|
+
if (reserved.every((part, i) => segments[i] === part || /^\{[^}]+\}$/.test(segments[i] ?? ""))) {
|
|
129
|
+
throw new Error(`vendor route ${path} collides with adminPrefix ${prefix}; choose a different adminPrefix`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
115
133
|
var ADMIN_KEY_HEADER = "x-mockingbird-admin-key";
|
|
116
134
|
var NAMESPACE_HEADER = "x-mockingbird-namespace";
|
|
117
135
|
var json = (status, body) => new Response(JSON.stringify(body), {
|
|
118
136
|
status,
|
|
119
|
-
headers: { "content-type": "application/json" }
|
|
137
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
120
138
|
});
|
|
121
139
|
var adminError = (status, message) => json(status, { error: { type: "mockingbird_admin", message } });
|
|
122
140
|
var UNITS = {
|
|
@@ -168,11 +186,25 @@ var readJson = async (request) => {
|
|
|
168
186
|
};
|
|
169
187
|
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
170
188
|
var createControlPlane = (context) => {
|
|
189
|
+
const adminPrefix = resolveAdminPrefix(context.adminPrefix);
|
|
171
190
|
const snapshots = /* @__PURE__ */ new Map();
|
|
172
191
|
let snapshotCounter = 0;
|
|
173
192
|
const headerNamespace = (request) => request.headers.get(NAMESPACE_HEADER) ?? context.defaultNamespace;
|
|
174
193
|
const adminNamespace = (request, url) => url.searchParams.get("namespace") ?? headerNamespace(request);
|
|
194
|
+
const health = () => {
|
|
195
|
+
return json(200, {
|
|
196
|
+
...context.describe(),
|
|
197
|
+
status: "ok",
|
|
198
|
+
service: context.name,
|
|
199
|
+
uptimeMs: context.wallNow() - context.startedAt,
|
|
200
|
+
clock: context.clock.state(),
|
|
201
|
+
namespaces: context.namespaces().length,
|
|
202
|
+
adminPrefix,
|
|
203
|
+
adminUi: `${adminPrefix}/ui`
|
|
204
|
+
});
|
|
205
|
+
};
|
|
175
206
|
const builtin = {
|
|
207
|
+
"GET /health": health,
|
|
176
208
|
"GET /": () => json(200, {
|
|
177
209
|
service: context.name,
|
|
178
210
|
routes: [...Object.keys(builtin), ...Object.keys(context.routes)].sort()
|
|
@@ -183,29 +215,30 @@ var createControlPlane = (context) => {
|
|
|
183
215
|
return json(200, { status: "ok", reset: target === "*" ? context.namespaces() : [target] });
|
|
184
216
|
},
|
|
185
217
|
"GET /namespaces": () => json(200, { default: context.defaultNamespace, namespaces: context.namespaces() }),
|
|
186
|
-
"GET /clock": () => json(200, context.clock.state()),
|
|
187
|
-
"POST /clock": ({ body }) => {
|
|
218
|
+
"GET /clock": ({ namespace }) => json(200, (context.namespaceClock?.(namespace) ?? context.clock).state()),
|
|
219
|
+
"POST /clock": ({ body, namespace }) => {
|
|
220
|
+
const clock = context.namespaceClock?.(namespace) ?? context.clock;
|
|
188
221
|
if (!isRecord(body))
|
|
189
222
|
return adminError(400, "expected a JSON object");
|
|
190
223
|
if (body.reset === true)
|
|
191
|
-
|
|
224
|
+
clock.reset();
|
|
192
225
|
if (body.freeze === true)
|
|
193
|
-
|
|
226
|
+
clock.freeze();
|
|
194
227
|
if (body.set !== void 0) {
|
|
195
228
|
const instant = parseInstant(body.set);
|
|
196
229
|
if (instant === void 0)
|
|
197
230
|
return adminError(400, "set: expected epoch ms or ISO-8601");
|
|
198
|
-
|
|
231
|
+
clock.set(instant);
|
|
199
232
|
}
|
|
200
233
|
if (body.advance !== void 0) {
|
|
201
234
|
const delta = parseDuration(body.advance);
|
|
202
235
|
if (delta === void 0)
|
|
203
236
|
return adminError(400, 'advance: expected ms or "15m"-style');
|
|
204
|
-
|
|
237
|
+
clock.advance(delta);
|
|
205
238
|
}
|
|
206
239
|
if (body.freeze === false)
|
|
207
|
-
|
|
208
|
-
return json(200,
|
|
240
|
+
clock.unfreeze();
|
|
241
|
+
return json(200, clock.state());
|
|
209
242
|
},
|
|
210
243
|
"GET /faults": () => json(200, { faults: context.faults.list() }),
|
|
211
244
|
"POST /faults": ({ body, namespace }) => {
|
|
@@ -338,7 +371,14 @@ var createControlPlane = (context) => {
|
|
|
338
371
|
return json(200, { status: "ok" });
|
|
339
372
|
}
|
|
340
373
|
};
|
|
341
|
-
const
|
|
374
|
+
for (const key of Object.keys(context.routes)) {
|
|
375
|
+
if (Object.hasOwn(builtin, key))
|
|
376
|
+
throw new Error(`admin route ${key} overrides a shared route`);
|
|
377
|
+
const path = key.slice(key.indexOf(" ") + 1);
|
|
378
|
+
if (isAdminPath(path, "/ns") || isAdminPath(path, "/blobs"))
|
|
379
|
+
throw new Error(`admin route ${key} overlaps an internal transport`);
|
|
380
|
+
}
|
|
381
|
+
const routes = [...Object.entries(builtin), ...Object.entries(context.routes)].map(([key, handler]) => {
|
|
342
382
|
const space = key.indexOf(" ");
|
|
343
383
|
return { method: key.slice(0, space), pattern: key.slice(space + 1), handler };
|
|
344
384
|
});
|
|
@@ -346,21 +386,13 @@ var createControlPlane = (context) => {
|
|
|
346
386
|
namespaceOf: headerNamespace,
|
|
347
387
|
async handle(request) {
|
|
348
388
|
const url = new URL(request.url);
|
|
349
|
-
if (url.pathname ===
|
|
350
|
-
return
|
|
351
|
-
status: "ok",
|
|
352
|
-
service: context.name,
|
|
353
|
-
uptimeMs: context.wallNow() - context.startedAt,
|
|
354
|
-
clock: context.clock.state(),
|
|
355
|
-
namespaces: context.namespaces().length,
|
|
356
|
-
adminUi: `${ADMIN_PREFIX}/ui`,
|
|
357
|
-
...context.describe()
|
|
358
|
-
});
|
|
389
|
+
if (url.pathname === `${adminPrefix}/health` && request.method === "GET") {
|
|
390
|
+
return health();
|
|
359
391
|
}
|
|
360
|
-
if (url.pathname
|
|
392
|
+
if (!isAdminPath(url.pathname, adminPrefix)) {
|
|
361
393
|
return void 0;
|
|
362
394
|
}
|
|
363
|
-
const path = url.pathname.slice(
|
|
395
|
+
const path = url.pathname.slice(adminPrefix.length) || "/";
|
|
364
396
|
const shell = request.method === "GET" && (path === "/ui" || path === "/ui/");
|
|
365
397
|
if (!shell && context.adminKey !== void 0 && request.headers.get(ADMIN_KEY_HEADER) !== context.adminKey) {
|
|
366
398
|
return adminError(401, `missing or wrong ${ADMIN_KEY_HEADER}`);
|
|
@@ -368,7 +400,12 @@ var createControlPlane = (context) => {
|
|
|
368
400
|
for (const route of routes) {
|
|
369
401
|
if (route.method !== request.method)
|
|
370
402
|
continue;
|
|
371
|
-
|
|
403
|
+
let params;
|
|
404
|
+
try {
|
|
405
|
+
params = matchRoute(route.pattern, path);
|
|
406
|
+
} catch {
|
|
407
|
+
return adminError(400, "invalid path encoding");
|
|
408
|
+
}
|
|
372
409
|
if (!params)
|
|
373
410
|
continue;
|
|
374
411
|
let body;
|
|
@@ -377,15 +414,18 @@ var createControlPlane = (context) => {
|
|
|
377
414
|
} catch {
|
|
378
415
|
return adminError(400, "request body is not valid JSON");
|
|
379
416
|
}
|
|
417
|
+
const namespace = adminNamespace(request, url);
|
|
418
|
+
if (!/^[A-Za-z0-9_.-]{1,64}$/.test(namespace))
|
|
419
|
+
return adminError(400, "invalid namespace");
|
|
380
420
|
return route.handler({
|
|
381
421
|
request,
|
|
382
422
|
url,
|
|
383
423
|
params,
|
|
384
|
-
namespace
|
|
424
|
+
namespace,
|
|
385
425
|
body
|
|
386
426
|
});
|
|
387
427
|
}
|
|
388
|
-
return adminError(404, `no admin route ${request.method} ${path}; GET ${
|
|
428
|
+
return adminError(404, `no admin route ${request.method} ${path}; GET ${adminPrefix} lists them`);
|
|
389
429
|
}
|
|
390
430
|
};
|
|
391
431
|
};
|
|
@@ -467,7 +507,33 @@ var faultResponse = (rule) => {
|
|
|
467
507
|
};
|
|
468
508
|
var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) => {
|
|
469
509
|
const entries = [];
|
|
510
|
+
let isolated = false;
|
|
511
|
+
const scopedRngs = /* @__PURE__ */ new Map();
|
|
512
|
+
const random = (namespace) => {
|
|
513
|
+
if (!isolated || namespace === "default")
|
|
514
|
+
return rng;
|
|
515
|
+
let scoped = scopedRngs.get(namespace);
|
|
516
|
+
if (!scoped) {
|
|
517
|
+
scoped = createRng(rng.seed);
|
|
518
|
+
scopedRngs.set(namespace, scoped);
|
|
519
|
+
}
|
|
520
|
+
return scoped;
|
|
521
|
+
};
|
|
470
522
|
return {
|
|
523
|
+
isolateNamespaces() {
|
|
524
|
+
isolated = true;
|
|
525
|
+
},
|
|
526
|
+
snapshot(namespace) {
|
|
527
|
+
return {
|
|
528
|
+
rules: structuredClone(entries.map((entry, position) => ({
|
|
529
|
+
...entry.rule,
|
|
530
|
+
remaining: entry.remaining,
|
|
531
|
+
hits: entry.hits,
|
|
532
|
+
position
|
|
533
|
+
})).filter((entry) => entry.namespace === namespace)),
|
|
534
|
+
rngState: random(namespace).state()
|
|
535
|
+
};
|
|
536
|
+
},
|
|
471
537
|
add(rule) {
|
|
472
538
|
const existing = entries.findIndex((e) => e.rule.id === rule.id);
|
|
473
539
|
const entry = { rule, remaining: rule.count ?? null, hits: 0 };
|
|
@@ -487,6 +553,16 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
|
|
|
487
553
|
},
|
|
488
554
|
clear() {
|
|
489
555
|
entries.length = 0;
|
|
556
|
+
rng.reset();
|
|
557
|
+
scopedRngs.clear();
|
|
558
|
+
},
|
|
559
|
+
restore(namespace, state) {
|
|
560
|
+
for (let i = entries.length - 1; i >= 0; i--)
|
|
561
|
+
if (entries[i]?.rule.namespace === namespace)
|
|
562
|
+
entries.splice(i, 1);
|
|
563
|
+
for (const { remaining, hits, position, ...rule } of state.rules)
|
|
564
|
+
entries.splice(Math.min(position, entries.length), 0, { rule, remaining, hits });
|
|
565
|
+
random(namespace).setState(state.rngState);
|
|
490
566
|
},
|
|
491
567
|
async take(candidate) {
|
|
492
568
|
const hits = [];
|
|
@@ -496,7 +572,7 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
|
|
|
496
572
|
if (!matches(entry.rule, candidate))
|
|
497
573
|
continue;
|
|
498
574
|
const rate = entry.rule.rate ?? 1;
|
|
499
|
-
if (
|
|
575
|
+
if (random(candidate.namespace).next() >= rate)
|
|
500
576
|
continue;
|
|
501
577
|
entry.hits++;
|
|
502
578
|
if (entry.remaining !== null)
|
|
@@ -698,7 +774,8 @@ var notes = /* @__PURE__ */ new WeakMap();
|
|
|
698
774
|
var responseNotes = (response) => notes.get(response);
|
|
699
775
|
|
|
700
776
|
// ../core/dist/metrics.js
|
|
701
|
-
var createMetrics = () => {
|
|
777
|
+
var createMetrics = (partition = true) => {
|
|
778
|
+
const namespaces = /* @__PURE__ */ new Map();
|
|
702
779
|
let requests = 0;
|
|
703
780
|
let faults = 0;
|
|
704
781
|
let totalDurationMs = 0;
|
|
@@ -706,6 +783,14 @@ var createMetrics = () => {
|
|
|
706
783
|
const unmatched = /* @__PURE__ */ new Map();
|
|
707
784
|
return {
|
|
708
785
|
record(entry) {
|
|
786
|
+
if (partition) {
|
|
787
|
+
let scoped = namespaces.get(entry.namespace);
|
|
788
|
+
if (!scoped) {
|
|
789
|
+
scoped = createMetrics(false);
|
|
790
|
+
namespaces.set(entry.namespace, scoped);
|
|
791
|
+
}
|
|
792
|
+
scoped.record(entry);
|
|
793
|
+
}
|
|
709
794
|
requests++;
|
|
710
795
|
totalDurationMs += entry.durationMs;
|
|
711
796
|
if (entry.faultId !== void 0)
|
|
@@ -717,7 +802,7 @@ var createMetrics = () => {
|
|
|
717
802
|
unmatched.set(route, (unmatched.get(route) ?? 0) + 1);
|
|
718
803
|
}
|
|
719
804
|
},
|
|
720
|
-
report: () => ({
|
|
805
|
+
report: (namespace) => namespace !== void 0 ? (namespaces.get(namespace) ?? createMetrics(false)).report() : {
|
|
721
806
|
requests,
|
|
722
807
|
byOperation: Object.fromEntries([...byOperation].sort(([a], [b]) => a.localeCompare(b))),
|
|
723
808
|
unmatched: [...unmatched].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([route, count]) => {
|
|
@@ -726,8 +811,13 @@ var createMetrics = () => {
|
|
|
726
811
|
}),
|
|
727
812
|
faults,
|
|
728
813
|
totalDurationMs
|
|
729
|
-
}
|
|
730
|
-
reset() {
|
|
814
|
+
},
|
|
815
|
+
reset(namespace) {
|
|
816
|
+
if (namespace !== void 0) {
|
|
817
|
+
namespaces.delete(namespace);
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
namespaces.clear();
|
|
731
821
|
requests = 0;
|
|
732
822
|
faults = 0;
|
|
733
823
|
totalDurationMs = 0;
|
|
@@ -1340,6 +1430,34 @@ dialog:not([open]) {
|
|
|
1340
1430
|
function isRecord2(value) {
|
|
1341
1431
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1342
1432
|
}
|
|
1433
|
+
function highlightJsonText(text) {
|
|
1434
|
+
const escapeToken = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
1435
|
+
switch (char) {
|
|
1436
|
+
case "&":
|
|
1437
|
+
return "&";
|
|
1438
|
+
case "<":
|
|
1439
|
+
return "<";
|
|
1440
|
+
case ">":
|
|
1441
|
+
return ">";
|
|
1442
|
+
case '"':
|
|
1443
|
+
return """;
|
|
1444
|
+
default:
|
|
1445
|
+
return "'";
|
|
1446
|
+
}
|
|
1447
|
+
});
|
|
1448
|
+
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;
|
|
1449
|
+
let cursor = 0;
|
|
1450
|
+
let html = "";
|
|
1451
|
+
for (const match2 of text.matchAll(token)) {
|
|
1452
|
+
const index = match2.index ?? 0;
|
|
1453
|
+
const value = match2[0];
|
|
1454
|
+
html += escapeToken(text.slice(cursor, index));
|
|
1455
|
+
const kind = value.startsWith('"') ? value.trimEnd().endsWith(":") ? "key" : "string" : value === "true" || value === "false" ? "boolean" : value === "null" ? "null" : "number";
|
|
1456
|
+
html += `<span class="json-${kind}">${escapeToken(value)}</span>`;
|
|
1457
|
+
cursor = index + value.length;
|
|
1458
|
+
}
|
|
1459
|
+
return html + escapeToken(text.slice(cursor));
|
|
1460
|
+
}
|
|
1343
1461
|
function mountAdminBrand(doc, host, service, catalog) {
|
|
1344
1462
|
const clip = (value, max) => {
|
|
1345
1463
|
if (typeof value !== "string")
|
|
@@ -1701,14 +1819,16 @@ function bootAdmin(config) {
|
|
|
1701
1819
|
const state = {
|
|
1702
1820
|
namespace: "default",
|
|
1703
1821
|
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
1704
|
-
view: "overview",
|
|
1822
|
+
view: ["overview", "state", "clock", "faults", "journal", "routes"].includes(location.hash.slice(1)) ? location.hash.slice(1) : "overview",
|
|
1705
1823
|
collection: "",
|
|
1706
1824
|
shape: null,
|
|
1707
1825
|
route: null,
|
|
1826
|
+
routes: [],
|
|
1708
1827
|
editing: null
|
|
1709
1828
|
};
|
|
1710
1829
|
const standard = new Set(config.standardRoutes);
|
|
1711
1830
|
const banner = el("banner", HTMLElement);
|
|
1831
|
+
const bannerMessage = el("banner-message", HTMLElement);
|
|
1712
1832
|
const keyInput = el("admin-key", HTMLInputElement);
|
|
1713
1833
|
keyInput.value = state.key;
|
|
1714
1834
|
const params = new URLSearchParams(location.search);
|
|
@@ -1719,18 +1839,21 @@ function bootAdmin(config) {
|
|
|
1719
1839
|
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
1720
1840
|
params.delete("key");
|
|
1721
1841
|
const next = params.toString();
|
|
1722
|
-
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
1842
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : "") + location.hash);
|
|
1723
1843
|
}
|
|
1724
1844
|
const queryNamespace = params.get("namespace");
|
|
1725
1845
|
if (queryNamespace)
|
|
1726
1846
|
state.namespace = queryNamespace;
|
|
1727
1847
|
const showError = (error2) => {
|
|
1728
|
-
|
|
1848
|
+
bannerMessage.textContent = error2 instanceof Error ? error2.message : String(error2);
|
|
1729
1849
|
banner.classList.add("show");
|
|
1850
|
+
banner.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
1730
1851
|
};
|
|
1731
1852
|
const clearError = () => {
|
|
1732
1853
|
banner.classList.remove("show");
|
|
1854
|
+
bannerMessage.textContent = "";
|
|
1733
1855
|
};
|
|
1856
|
+
el("banner-dismiss", HTMLButtonElement).addEventListener("click", clearError);
|
|
1734
1857
|
const api = async (method, path, body) => {
|
|
1735
1858
|
const headers = { accept: "application/json" };
|
|
1736
1859
|
if (state.key !== "")
|
|
@@ -1774,16 +1897,21 @@ function bootAdmin(config) {
|
|
|
1774
1897
|
namespace: () => state.namespace
|
|
1775
1898
|
};
|
|
1776
1899
|
const isPanelScript = (value) => typeof value === "function";
|
|
1900
|
+
const themeButton = el("theme", HTMLButtonElement);
|
|
1777
1901
|
const setTheme = (theme) => {
|
|
1778
1902
|
if (theme)
|
|
1779
1903
|
document.documentElement.dataset.theme = theme;
|
|
1780
1904
|
else
|
|
1781
1905
|
delete document.documentElement.dataset.theme;
|
|
1906
|
+
const dark = theme ? theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
1907
|
+
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>';
|
|
1908
|
+
const label = dark ? "Use light theme" : "Use dark theme";
|
|
1909
|
+
themeButton.setAttribute("aria-label", label);
|
|
1910
|
+
themeButton.title = label;
|
|
1782
1911
|
};
|
|
1783
1912
|
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
1913
|
+
setTheme(storedTheme === "dark" || storedTheme === "light" ? storedTheme : null);
|
|
1914
|
+
themeButton.addEventListener("click", () => {
|
|
1787
1915
|
const current = document.documentElement.dataset.theme;
|
|
1788
1916
|
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
1789
1917
|
const next = dark ? "light" : "dark";
|
|
@@ -1802,6 +1930,7 @@ function bootAdmin(config) {
|
|
|
1802
1930
|
continue;
|
|
1803
1931
|
section.hidden = section.id !== `view-${view}`;
|
|
1804
1932
|
}
|
|
1933
|
+
history.replaceState(null, "", `${location.pathname}${location.search}#${encodeURIComponent(view)}`);
|
|
1805
1934
|
};
|
|
1806
1935
|
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
1807
1936
|
const target = event.target;
|
|
@@ -1816,7 +1945,7 @@ function bootAdmin(config) {
|
|
|
1816
1945
|
show(view);
|
|
1817
1946
|
refresh().catch(showError);
|
|
1818
1947
|
});
|
|
1819
|
-
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>`;
|
|
1948
|
+
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>`;
|
|
1820
1949
|
const table = (columns, rows) => {
|
|
1821
1950
|
if (rows.length === 0)
|
|
1822
1951
|
return '<p class="empty">Nothing here yet.</p>';
|
|
@@ -1826,16 +1955,22 @@ function bootAdmin(config) {
|
|
|
1826
1955
|
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
1827
1956
|
return `<tr${attrs}>${cells}</tr>`;
|
|
1828
1957
|
}).join("");
|
|
1829
|
-
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
1958
|
+
return `<div class="table-shell"><table><thead>${head}</thead><tbody>${body}</tbody></table></div>`;
|
|
1830
1959
|
};
|
|
1831
1960
|
const preview = (value) => {
|
|
1832
1961
|
const text = JSON.stringify(value) ?? "null";
|
|
1833
|
-
|
|
1962
|
+
const clipped = text.length > 180 ? `${text.slice(0, 180)}\u2026` : text;
|
|
1963
|
+
return `<code class="json-inline">${highlightJsonText(clipped)}</code>`;
|
|
1834
1964
|
};
|
|
1835
1965
|
const when = (epoch) => {
|
|
1836
1966
|
const date = new Date(epoch);
|
|
1837
1967
|
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
1838
1968
|
};
|
|
1969
|
+
const journalWhen = (value) => {
|
|
1970
|
+
const numeric = value.trim() === "" ? Number.NaN : Number(value);
|
|
1971
|
+
const epoch = Number.isFinite(numeric) ? numeric : Date.parse(value);
|
|
1972
|
+
return Number.isFinite(epoch) ? when(epoch) : value;
|
|
1973
|
+
};
|
|
1839
1974
|
const clockCard = (epoch) => {
|
|
1840
1975
|
const date = new Date(epoch);
|
|
1841
1976
|
if (Number.isNaN(date.getTime()))
|
|
@@ -1955,7 +2090,7 @@ function bootAdmin(config) {
|
|
|
1955
2090
|
};
|
|
1956
2091
|
const renderOverview = async () => {
|
|
1957
2092
|
const [health, shape, clock, journal] = await Promise.all([
|
|
1958
|
-
originGet(
|
|
2093
|
+
originGet(`${config.adminPrefix}/health`),
|
|
1959
2094
|
api("GET", "/state"),
|
|
1960
2095
|
api("GET", "/clock"),
|
|
1961
2096
|
api("GET", "/requests?limit=8")
|
|
@@ -1972,7 +2107,7 @@ function bootAdmin(config) {
|
|
|
1972
2107
|
},
|
|
1973
2108
|
{
|
|
1974
2109
|
k: "Status",
|
|
1975
|
-
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
2110
|
+
v: `<span class="badge ok">${esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")}</span>`
|
|
1976
2111
|
},
|
|
1977
2112
|
{ k: "Namespace", v: esc(state.namespace) },
|
|
1978
2113
|
{ k: "Collections", v: String(decoded.collections.length) },
|
|
@@ -2059,26 +2194,46 @@ function bootAdmin(config) {
|
|
|
2059
2194
|
const renderJournal = async () => {
|
|
2060
2195
|
const journal = await api("GET", "/requests?limit=100");
|
|
2061
2196
|
el("journal", HTMLElement).innerHTML = table([
|
|
2062
|
-
{ label: "When", cell: (row) => esc(row.at) },
|
|
2063
|
-
{
|
|
2197
|
+
{ label: "When", cell: (row) => esc(journalWhen(row.at)) },
|
|
2198
|
+
{
|
|
2199
|
+
label: "Method",
|
|
2200
|
+
cell: (row) => `<span class="method">${esc(row.method)}</span>`
|
|
2201
|
+
},
|
|
2064
2202
|
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2065
|
-
{
|
|
2203
|
+
{
|
|
2204
|
+
label: "Status",
|
|
2205
|
+
cell: (row) => `<span class="badge ${row.status.startsWith("2") ? "ok" : row.status.startsWith("4") || row.status.startsWith("5") ? "warn" : ""}">${esc(row.status)}</span>`
|
|
2206
|
+
},
|
|
2066
2207
|
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
2067
2208
|
], decodeJournal(journal));
|
|
2068
2209
|
};
|
|
2069
|
-
const
|
|
2070
|
-
const
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2210
|
+
const paintRoutes = () => {
|
|
2211
|
+
const query = el("route-search", HTMLInputElement).value.trim().toLocaleLowerCase();
|
|
2212
|
+
const routes = state.routes.filter((route) => route.toLocaleLowerCase().includes(query));
|
|
2213
|
+
el("route-count", HTMLElement).textContent = `${routes.length} of ${state.routes.length}`;
|
|
2214
|
+
if (state.routes.length === 0) {
|
|
2215
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No admin routes are available.</p>';
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2218
|
+
if (routes.length === 0) {
|
|
2219
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No routes match this filter.</p>';
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2074
2222
|
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2075
2223
|
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2076
2224
|
const parts = route.split(" ");
|
|
2077
2225
|
const method = parts[0] ?? "";
|
|
2078
2226
|
const path = parts.slice(1).join(" ");
|
|
2079
|
-
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>`;
|
|
2227
|
+
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>`;
|
|
2080
2228
|
}).join("");
|
|
2081
2229
|
};
|
|
2230
|
+
const renderRoutes = async () => {
|
|
2231
|
+
const data = await api("GET", "/");
|
|
2232
|
+
if (!isRecord2(data))
|
|
2233
|
+
throw new Error("Route list was not an object");
|
|
2234
|
+
state.routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
2235
|
+
paintRoutes();
|
|
2236
|
+
};
|
|
2082
2237
|
const renderers = {
|
|
2083
2238
|
overview: renderOverview,
|
|
2084
2239
|
state: renderState,
|
|
@@ -2087,19 +2242,50 @@ function bootAdmin(config) {
|
|
|
2087
2242
|
journal: renderJournal,
|
|
2088
2243
|
routes: renderRoutes
|
|
2089
2244
|
};
|
|
2245
|
+
let refreshToken = 0;
|
|
2090
2246
|
async function refresh() {
|
|
2247
|
+
const token = ++refreshToken;
|
|
2091
2248
|
clearError();
|
|
2249
|
+
const main = el("views", HTMLElement);
|
|
2250
|
+
const sync = el("sync-status", HTMLElement);
|
|
2251
|
+
main.setAttribute("aria-busy", "true");
|
|
2252
|
+
sync.classList.add("busy");
|
|
2253
|
+
sync.textContent = "Updating";
|
|
2092
2254
|
const render = renderers[state.view];
|
|
2093
|
-
|
|
2094
|
-
|
|
2255
|
+
try {
|
|
2256
|
+
if (render)
|
|
2257
|
+
await render();
|
|
2258
|
+
if (token === refreshToken)
|
|
2259
|
+
sync.textContent = "Up to date";
|
|
2260
|
+
} finally {
|
|
2261
|
+
if (token === refreshToken) {
|
|
2262
|
+
main.removeAttribute("aria-busy");
|
|
2263
|
+
sync.classList.remove("busy");
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2095
2266
|
}
|
|
2096
2267
|
const delay = (ms) => new Promise((resolve) => {
|
|
2097
2268
|
window.setTimeout(resolve, ms);
|
|
2098
2269
|
});
|
|
2099
|
-
const confirmAction = async (button, pending, done, work) => {
|
|
2270
|
+
const confirmAction = async (button, pending, done, work, confirmLabel) => {
|
|
2100
2271
|
if (button.dataset.pending === "1")
|
|
2101
2272
|
return;
|
|
2102
2273
|
const label = button.textContent ?? "";
|
|
2274
|
+
if (confirmLabel !== void 0 && button.dataset.confirming !== "1") {
|
|
2275
|
+
button.dataset.confirming = "1";
|
|
2276
|
+
button.classList.add("confirming");
|
|
2277
|
+
button.textContent = confirmLabel;
|
|
2278
|
+
window.setTimeout(() => {
|
|
2279
|
+
if (button.dataset.confirming !== "1")
|
|
2280
|
+
return;
|
|
2281
|
+
button.textContent = label;
|
|
2282
|
+
button.classList.remove("confirming");
|
|
2283
|
+
delete button.dataset.confirming;
|
|
2284
|
+
}, 3e3);
|
|
2285
|
+
return;
|
|
2286
|
+
}
|
|
2287
|
+
button.classList.remove("confirming");
|
|
2288
|
+
delete button.dataset.confirming;
|
|
2103
2289
|
const width = button.offsetWidth;
|
|
2104
2290
|
button.dataset.pending = "1";
|
|
2105
2291
|
button.classList.add("pressed");
|
|
@@ -2120,6 +2306,65 @@ function bootAdmin(config) {
|
|
|
2120
2306
|
delete button.dataset.pending;
|
|
2121
2307
|
}
|
|
2122
2308
|
};
|
|
2309
|
+
const wireJsonEditor = (textareaId, highlightId, statusId, shellId, allowEmpty) => {
|
|
2310
|
+
const textarea = el(textareaId, HTMLTextAreaElement);
|
|
2311
|
+
const highlight = el(highlightId, HTMLElement);
|
|
2312
|
+
const status = el(statusId, HTMLElement);
|
|
2313
|
+
const shell = el(shellId, HTMLElement);
|
|
2314
|
+
const sync = () => {
|
|
2315
|
+
const text = textarea.value;
|
|
2316
|
+
highlight.innerHTML = `${highlightJsonText(text)}
|
|
2317
|
+
`;
|
|
2318
|
+
let valid = true;
|
|
2319
|
+
let message = "Valid JSON";
|
|
2320
|
+
if (allowEmpty && text.trim() === "")
|
|
2321
|
+
message = "No body";
|
|
2322
|
+
else {
|
|
2323
|
+
try {
|
|
2324
|
+
readJson2(text);
|
|
2325
|
+
} catch {
|
|
2326
|
+
valid = false;
|
|
2327
|
+
message = "Invalid JSON";
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
shell.classList.toggle("invalid", !valid);
|
|
2331
|
+
status.classList.toggle("invalid", !valid);
|
|
2332
|
+
status.textContent = message;
|
|
2333
|
+
textarea.setAttribute("aria-invalid", valid ? "false" : "true");
|
|
2334
|
+
};
|
|
2335
|
+
const syncScroll = () => {
|
|
2336
|
+
const preview2 = highlight.parentElement;
|
|
2337
|
+
if (!(preview2 instanceof HTMLElement))
|
|
2338
|
+
return;
|
|
2339
|
+
preview2.scrollTop = textarea.scrollTop;
|
|
2340
|
+
preview2.scrollLeft = textarea.scrollLeft;
|
|
2341
|
+
};
|
|
2342
|
+
textarea.addEventListener("input", sync);
|
|
2343
|
+
textarea.addEventListener("scroll", syncScroll);
|
|
2344
|
+
sync();
|
|
2345
|
+
return sync;
|
|
2346
|
+
};
|
|
2347
|
+
const syncRouteEditor = wireJsonEditor("route-body", "route-highlight", "route-json-status", "route-editor", true);
|
|
2348
|
+
const syncRecordEditor = wireJsonEditor("editor-value", "editor-highlight", "editor-json-status", "record-editor", false);
|
|
2349
|
+
for (const button of document.querySelectorAll("[data-format]")) {
|
|
2350
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2351
|
+
continue;
|
|
2352
|
+
button.addEventListener("click", () => {
|
|
2353
|
+
const target = button.dataset.format;
|
|
2354
|
+
if (target === void 0)
|
|
2355
|
+
return;
|
|
2356
|
+
const textarea = document.getElementById(target);
|
|
2357
|
+
if (!(textarea instanceof HTMLTextAreaElement) || textarea.value.trim() === "")
|
|
2358
|
+
return;
|
|
2359
|
+
try {
|
|
2360
|
+
textarea.value = JSON.stringify(readJson2(textarea.value), null, 2) ?? "";
|
|
2361
|
+
textarea.dispatchEvent(new Event("input"));
|
|
2362
|
+
textarea.focus();
|
|
2363
|
+
} catch {
|
|
2364
|
+
showError(new Error("Fix the JSON before formatting it"));
|
|
2365
|
+
}
|
|
2366
|
+
});
|
|
2367
|
+
}
|
|
2123
2368
|
document.addEventListener("click", (event) => {
|
|
2124
2369
|
const target = event.target;
|
|
2125
2370
|
if (!(target instanceof Element))
|
|
@@ -2192,6 +2437,7 @@ function bootAdmin(config) {
|
|
|
2192
2437
|
} else {
|
|
2193
2438
|
editorValue.value = "{\n \n}";
|
|
2194
2439
|
}
|
|
2440
|
+
syncRecordEditor();
|
|
2195
2441
|
editor.showModal();
|
|
2196
2442
|
}
|
|
2197
2443
|
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -2221,10 +2467,10 @@ function bootAdmin(config) {
|
|
|
2221
2467
|
const id = state.editing;
|
|
2222
2468
|
if (!id)
|
|
2223
2469
|
return;
|
|
2224
|
-
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2470
|
+
confirmAction(editorDelete, "Deleting...", "Deleted", () => api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2225
2471
|
editor.close();
|
|
2226
2472
|
return renderState();
|
|
2227
|
-
}).catch(showError);
|
|
2473
|
+
}), "Confirm delete").catch(showError);
|
|
2228
2474
|
});
|
|
2229
2475
|
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2230
2476
|
if (!(button instanceof HTMLButtonElement))
|
|
@@ -2270,10 +2516,10 @@ function bootAdmin(config) {
|
|
|
2270
2516
|
}).then(() => renderFaults()).catch(showError);
|
|
2271
2517
|
});
|
|
2272
2518
|
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2273
|
-
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
|
|
2519
|
+
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults()), "Confirm clear").catch(showError);
|
|
2274
2520
|
});
|
|
2275
2521
|
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2276
|
-
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
|
|
2522
|
+
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal()), "Confirm clear").catch(showError);
|
|
2277
2523
|
});
|
|
2278
2524
|
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2279
2525
|
const target = event.target;
|
|
@@ -2287,9 +2533,15 @@ function bootAdmin(config) {
|
|
|
2287
2533
|
return;
|
|
2288
2534
|
state.route = route;
|
|
2289
2535
|
el("route-title", HTMLElement).textContent = route;
|
|
2536
|
+
el("route-send", HTMLButtonElement).disabled = false;
|
|
2290
2537
|
const method = route.split(" ")[0] ?? "";
|
|
2291
|
-
el("route-body", HTMLTextAreaElement)
|
|
2538
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
2539
|
+
routeBody.disabled = method === "GET" || method === "DELETE";
|
|
2540
|
+
el("route-editor", HTMLElement).hidden = routeBody.disabled;
|
|
2541
|
+
el("route-json-status", HTMLElement).parentElement?.toggleAttribute("hidden", routeBody.disabled);
|
|
2542
|
+
syncRouteEditor();
|
|
2292
2543
|
});
|
|
2544
|
+
el("route-search", HTMLInputElement).addEventListener("input", paintRoutes);
|
|
2293
2545
|
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2294
2546
|
event.preventDefault();
|
|
2295
2547
|
const send = async () => {
|
|
@@ -2311,7 +2563,8 @@ function bootAdmin(config) {
|
|
|
2311
2563
|
}
|
|
2312
2564
|
}
|
|
2313
2565
|
const data = await api(method, concrete, body);
|
|
2314
|
-
|
|
2566
|
+
const text = JSON.stringify(data, null, 2) ?? "null";
|
|
2567
|
+
el("route-result", HTMLElement).innerHTML = `<code>${highlightJsonText(text)}</code>`;
|
|
2315
2568
|
};
|
|
2316
2569
|
send().catch(showError);
|
|
2317
2570
|
});
|
|
@@ -2429,11 +2682,13 @@ function bootAdmin(config) {
|
|
|
2429
2682
|
views.append(section);
|
|
2430
2683
|
mountSqlExplorer(body, panelApi);
|
|
2431
2684
|
}
|
|
2685
|
+
show(document.getElementById(`view-${state.view}`) === null ? "overview" : state.view);
|
|
2432
2686
|
return refresh();
|
|
2433
2687
|
}).catch(showError);
|
|
2434
2688
|
}
|
|
2435
2689
|
var clientPrelude = [
|
|
2436
2690
|
isRecord2,
|
|
2691
|
+
highlightJsonText,
|
|
2437
2692
|
mountAdminBrand,
|
|
2438
2693
|
startAdminBrand,
|
|
2439
2694
|
faultPresetList,
|
|
@@ -2456,6 +2711,7 @@ ${entry}(${payload});
|
|
|
2456
2711
|
var defineMock = (runtime) => runtime;
|
|
2457
2712
|
var STANDARD_ADMIN_ROUTES = [
|
|
2458
2713
|
"GET /",
|
|
2714
|
+
"GET /health",
|
|
2459
2715
|
"POST /reset",
|
|
2460
2716
|
"GET /namespaces",
|
|
2461
2717
|
"GET /clock",
|
|
@@ -2535,11 +2791,11 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
2535
2791
|
}
|
|
2536
2792
|
});
|
|
2537
2793
|
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
2538
|
-
var renderAdminDocument = (service) => {
|
|
2794
|
+
var renderAdminDocument = (service, adminPrefix = ADMIN_PREFIX) => {
|
|
2539
2795
|
const name = escapeHtml(service);
|
|
2540
2796
|
const script = adminClientSource({
|
|
2541
2797
|
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
2542
|
-
adminPrefix
|
|
2798
|
+
adminPrefix,
|
|
2543
2799
|
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
2544
2800
|
brandsUrl: ADMIN_BRANDS_URL,
|
|
2545
2801
|
service
|
|
@@ -2556,75 +2812,99 @@ var renderAdminDocument = (service) => {
|
|
|
2556
2812
|
${CSS_RESET}
|
|
2557
2813
|
:root {
|
|
2558
2814
|
color-scheme: light;
|
|
2559
|
-
--bg: #
|
|
2815
|
+
--bg: #f7f8fb;
|
|
2560
2816
|
--bg-elev: #ffffff;
|
|
2561
|
-
--bg-muted: #
|
|
2562
|
-
--bg-inset: #
|
|
2563
|
-
--line: #
|
|
2564
|
-
--line-strong: #
|
|
2565
|
-
--ink: #
|
|
2566
|
-
--muted: #
|
|
2567
|
-
--faint: #
|
|
2568
|
-
--accent: #
|
|
2817
|
+
--bg-muted: #f1f3f7;
|
|
2818
|
+
--bg-inset: #f8f9fc;
|
|
2819
|
+
--line: #e4e7ec;
|
|
2820
|
+
--line-strong: #cfd4dc;
|
|
2821
|
+
--ink: #101828;
|
|
2822
|
+
--muted: #475467;
|
|
2823
|
+
--faint: #667085;
|
|
2824
|
+
--accent: #6941c6;
|
|
2825
|
+
--accent-hover: #5933b4;
|
|
2569
2826
|
--accent-ink: #ffffff;
|
|
2570
|
-
--accent-soft: #
|
|
2571
|
-
--ok: #
|
|
2572
|
-
--ok-soft: #
|
|
2573
|
-
--warn: #
|
|
2574
|
-
--warn-soft: #
|
|
2575
|
-
--err: #
|
|
2576
|
-
--err-soft: #
|
|
2577
|
-
--
|
|
2827
|
+
--accent-soft: #f4f0ff;
|
|
2828
|
+
--ok: #067647;
|
|
2829
|
+
--ok-soft: #ecfdf3;
|
|
2830
|
+
--warn: #b54708;
|
|
2831
|
+
--warn-soft: #fffaeb;
|
|
2832
|
+
--err: #b42318;
|
|
2833
|
+
--err-soft: #fef3f2;
|
|
2834
|
+
--code-key: #6941c6;
|
|
2835
|
+
--code-string: #067647;
|
|
2836
|
+
--code-number: #175cd3;
|
|
2837
|
+
--code-boolean: #c11574;
|
|
2838
|
+
--code-null: #667085;
|
|
2839
|
+
--shadow-xs: 0 1px 2px rgb(16 24 40 / 0.05);
|
|
2840
|
+
--shadow-sm: 0 1px 3px rgb(16 24 40 / 0.08), 0 1px 2px rgb(16 24 40 / 0.04);
|
|
2841
|
+
--shadow-lg: 0 18px 48px rgb(16 24 40 / 0.16), 0 4px 12px rgb(16 24 40 / 0.08);
|
|
2578
2842
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2579
2843
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
2580
|
-
--radius:
|
|
2581
|
-
--header:
|
|
2582
|
-
--nav:
|
|
2844
|
+
--radius: 12px;
|
|
2845
|
+
--header: 72px;
|
|
2846
|
+
--nav: 224px;
|
|
2583
2847
|
}
|
|
2584
2848
|
:root[data-theme="dark"] {
|
|
2585
2849
|
color-scheme: dark;
|
|
2586
|
-
--bg: #
|
|
2587
|
-
--bg-elev: #
|
|
2588
|
-
--bg-muted: #
|
|
2589
|
-
--bg-inset: #
|
|
2590
|
-
--line: #
|
|
2591
|
-
--line-strong: #
|
|
2592
|
-
--ink: #
|
|
2593
|
-
--muted: #
|
|
2594
|
-
--faint: #
|
|
2595
|
-
--accent: #
|
|
2596
|
-
--accent-
|
|
2597
|
-
--accent-
|
|
2598
|
-
--
|
|
2599
|
-
--ok
|
|
2600
|
-
--
|
|
2601
|
-
--warn
|
|
2602
|
-
--
|
|
2603
|
-
--err
|
|
2604
|
-
--
|
|
2850
|
+
--bg: #0c0e14;
|
|
2851
|
+
--bg-elev: #151821;
|
|
2852
|
+
--bg-muted: #20232e;
|
|
2853
|
+
--bg-inset: #11141c;
|
|
2854
|
+
--line: #2b303d;
|
|
2855
|
+
--line-strong: #414858;
|
|
2856
|
+
--ink: #f5f7fa;
|
|
2857
|
+
--muted: #b6bdc9;
|
|
2858
|
+
--faint: #8d96a7;
|
|
2859
|
+
--accent: #9e77ed;
|
|
2860
|
+
--accent-hover: #b692f6;
|
|
2861
|
+
--accent-ink: #ffffff;
|
|
2862
|
+
--accent-soft: #2b2142;
|
|
2863
|
+
--ok: #75e0a7;
|
|
2864
|
+
--ok-soft: #102a20;
|
|
2865
|
+
--warn: #fec84b;
|
|
2866
|
+
--warn-soft: #302611;
|
|
2867
|
+
--err: #fda29b;
|
|
2868
|
+
--err-soft: #321817;
|
|
2869
|
+
--code-key: #c3a6ff;
|
|
2870
|
+
--code-string: #75e0a7;
|
|
2871
|
+
--code-number: #84adff;
|
|
2872
|
+
--code-boolean: #f9a8d4;
|
|
2873
|
+
--code-null: #98a2b3;
|
|
2874
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
2875
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
2876
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2605
2877
|
}
|
|
2606
2878
|
@media (prefers-color-scheme: dark) {
|
|
2607
2879
|
:root:not([data-theme="light"]) {
|
|
2608
2880
|
color-scheme: dark;
|
|
2609
|
-
--bg: #
|
|
2610
|
-
--bg-elev: #
|
|
2611
|
-
--bg-muted: #
|
|
2612
|
-
--bg-inset: #
|
|
2613
|
-
--line: #
|
|
2614
|
-
--line-strong: #
|
|
2615
|
-
--ink: #
|
|
2616
|
-
--muted: #
|
|
2617
|
-
--faint: #
|
|
2618
|
-
--accent: #
|
|
2619
|
-
--accent-
|
|
2620
|
-
--accent-
|
|
2621
|
-
--
|
|
2622
|
-
--ok
|
|
2623
|
-
--
|
|
2624
|
-
--warn
|
|
2625
|
-
--
|
|
2626
|
-
--err
|
|
2627
|
-
--
|
|
2881
|
+
--bg: #0c0e14;
|
|
2882
|
+
--bg-elev: #151821;
|
|
2883
|
+
--bg-muted: #20232e;
|
|
2884
|
+
--bg-inset: #11141c;
|
|
2885
|
+
--line: #2b303d;
|
|
2886
|
+
--line-strong: #414858;
|
|
2887
|
+
--ink: #f5f7fa;
|
|
2888
|
+
--muted: #b6bdc9;
|
|
2889
|
+
--faint: #8d96a7;
|
|
2890
|
+
--accent: #9e77ed;
|
|
2891
|
+
--accent-hover: #b692f6;
|
|
2892
|
+
--accent-ink: #ffffff;
|
|
2893
|
+
--accent-soft: #2b2142;
|
|
2894
|
+
--ok: #75e0a7;
|
|
2895
|
+
--ok-soft: #102a20;
|
|
2896
|
+
--warn: #fec84b;
|
|
2897
|
+
--warn-soft: #302611;
|
|
2898
|
+
--err: #fda29b;
|
|
2899
|
+
--err-soft: #321817;
|
|
2900
|
+
--code-key: #c3a6ff;
|
|
2901
|
+
--code-string: #75e0a7;
|
|
2902
|
+
--code-number: #84adff;
|
|
2903
|
+
--code-boolean: #f9a8d4;
|
|
2904
|
+
--code-null: #98a2b3;
|
|
2905
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
2906
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
2907
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2628
2908
|
}
|
|
2629
2909
|
}
|
|
2630
2910
|
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
@@ -2642,13 +2922,17 @@ var renderAdminDocument = (service) => {
|
|
|
2642
2922
|
padding-right: env(safe-area-inset-right);
|
|
2643
2923
|
-webkit-text-size-adjust: 100%;
|
|
2644
2924
|
}
|
|
2645
|
-
|
|
2925
|
+
::selection { background: color-mix(in srgb, var(--accent) 28%, transparent); }
|
|
2926
|
+
:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px; }
|
|
2927
|
+
button, input, select, textarea { transition: border-color 140ms ease, box-shadow 140ms ease, transform 100ms ease; }
|
|
2928
|
+
button:disabled, input:disabled, select:disabled, textarea:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
2646
2929
|
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
2647
2930
|
header.top {
|
|
2648
|
-
position: sticky; top: 0; z-index:
|
|
2931
|
+
position: sticky; top: 0; z-index: 20;
|
|
2649
2932
|
display: flex; align-items: center; gap: 12px;
|
|
2650
|
-
min-height: var(--header); padding:
|
|
2651
|
-
background: var(--bg-elev);
|
|
2933
|
+
min-height: var(--header); padding: 12px 20px;
|
|
2934
|
+
background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
|
|
2935
|
+
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
2652
2936
|
border-bottom: 1px solid var(--line);
|
|
2653
2937
|
min-width: 0; max-width: 100%;
|
|
2654
2938
|
}
|
|
@@ -2656,12 +2940,21 @@ var renderAdminDocument = (service) => {
|
|
|
2656
2940
|
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
2657
2941
|
min-width: 0; max-width: 100%; margin-right: auto;
|
|
2658
2942
|
}
|
|
2659
|
-
.brand-
|
|
2660
|
-
.brand-
|
|
2943
|
+
.brand-lockup { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
2944
|
+
.brand-mark {
|
|
2945
|
+
width: 38px; height: 38px; display: grid; place-items: center; flex: none;
|
|
2946
|
+
border-radius: 11px; color: #fff;
|
|
2947
|
+
background: linear-gradient(145deg, #7f56d9 0%, #53389e 100%);
|
|
2948
|
+
box-shadow: 0 6px 16px rgb(105 65 198 / 0.24), inset 0 1px 0 rgb(255 255 255 / 0.25);
|
|
2949
|
+
}
|
|
2950
|
+
.brand-mark svg { width: 22px; height: 22px; }
|
|
2951
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; line-height: 1.25; }
|
|
2952
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
2661
2953
|
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
2662
2954
|
.vendor {
|
|
2663
2955
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
2664
2956
|
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
2957
|
+
padding: 5px 8px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-inset);
|
|
2665
2958
|
}
|
|
2666
2959
|
.vendor[hidden] { display: none; }
|
|
2667
2960
|
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
@@ -2673,12 +2966,17 @@ var renderAdminDocument = (service) => {
|
|
|
2673
2966
|
.vendor-sep { color: var(--faint); }
|
|
2674
2967
|
.vendor-link { color: inherit; text-decoration: none; }
|
|
2675
2968
|
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
2676
|
-
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items:
|
|
2677
|
-
|
|
2969
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
|
|
2970
|
+
.sync-status { align-self: center; display: flex; align-items: center; gap: 6px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
2971
|
+
.sync-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
|
|
2972
|
+
.sync-status.busy::before { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); animation: pulse 1.2s ease-in-out infinite; }
|
|
2973
|
+
label.field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; font-weight: 600; color: var(--faint); }
|
|
2678
2974
|
label.field input, label.field select, .controls input[type="password"] {
|
|
2679
|
-
background-color: var(--bg-
|
|
2680
|
-
padding:
|
|
2975
|
+
background-color: var(--bg-elev); border: 1px solid var(--line-strong); border-radius: 9px;
|
|
2976
|
+
padding: 8px 10px; min-height: 38px; line-height: 1.2; box-shadow: var(--shadow-xs);
|
|
2681
2977
|
}
|
|
2978
|
+
label.field input:hover, label.field select:hover, textarea:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--line-strong)); }
|
|
2979
|
+
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; }
|
|
2682
2980
|
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
2683
2981
|
the text fields in the same form. Paint it like those fields. */
|
|
2684
2982
|
label.field select {
|
|
@@ -2695,12 +2993,20 @@ var renderAdminDocument = (service) => {
|
|
|
2695
2993
|
dialog .field > textarea { width: 100%; }
|
|
2696
2994
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
2697
2995
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
.btn
|
|
2996
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
|
2997
|
+
border-radius: 9px; min-height: 38px; padding: 0 13px; border: 1px solid var(--line-strong);
|
|
2998
|
+
background: var(--bg-elev); color: var(--ink); font-size: 13px; font-weight: 600; line-height: 1;
|
|
2999
|
+
box-shadow: var(--shadow-xs);
|
|
3000
|
+
}
|
|
3001
|
+
.icon-btn { width: 38px; padding: 0; }
|
|
3002
|
+
.icon-btn svg, .btn svg, .btn-primary svg { width: 16px; height: 16px; }
|
|
3003
|
+
.icon-btn:hover, .btn:hover { background: var(--bg-muted); border-color: var(--line-strong); }
|
|
3004
|
+
.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; }
|
|
3005
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
3006
|
+
.btn-quiet { background: transparent; border-color: transparent; box-shadow: none; }
|
|
3007
|
+
.btn-quiet:hover { background: var(--bg-muted); }
|
|
3008
|
+
.btn-danger { color: var(--err); border-color: color-mix(in srgb, var(--err) 25%, var(--line)); }
|
|
3009
|
+
.btn-danger:hover, .btn-danger.confirming { background: var(--err-soft); border-color: color-mix(in srgb, var(--err) 45%, var(--line)); }
|
|
2704
3010
|
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
2705
3011
|
so the press itself has to stay visible. */
|
|
2706
3012
|
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
@@ -2716,93 +3022,159 @@ var renderAdminDocument = (service) => {
|
|
|
2716
3022
|
flex: 1 0 auto; min-width: 0;
|
|
2717
3023
|
}
|
|
2718
3024
|
nav.side {
|
|
2719
|
-
|
|
3025
|
+
position: sticky; top: var(--header); align-self: start; height: calc(100vh - var(--header));
|
|
3026
|
+
border-right: 1px solid var(--line); padding: 18px 12px;
|
|
2720
3027
|
display: flex; flex-direction: column; gap: 4px;
|
|
2721
|
-
background: var(--bg-elev);
|
|
3028
|
+
background: var(--bg-elev); overflow-y: auto;
|
|
2722
3029
|
}
|
|
3030
|
+
.nav-label { padding: 0 10px 8px; color: var(--faint); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
|
|
2723
3031
|
nav.side button {
|
|
3032
|
+
position: relative; display: flex; align-items: center; gap: 10px; width: 100%;
|
|
2724
3033
|
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
2725
|
-
padding: 9px 10px; min-height:
|
|
2726
|
-
}
|
|
2727
|
-
nav.side button
|
|
2728
|
-
|
|
2729
|
-
.
|
|
2730
|
-
.
|
|
2731
|
-
.
|
|
3034
|
+
padding: 9px 10px; min-height: 42px; color: var(--muted); font-size: 13px; font-weight: 550;
|
|
3035
|
+
}
|
|
3036
|
+
nav.side button:hover { background: var(--bg-muted); color: var(--ink); }
|
|
3037
|
+
nav.side button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 650; }
|
|
3038
|
+
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); }
|
|
3039
|
+
.nav-icon { width: 18px; height: 18px; display: grid; place-items: center; flex: none; }
|
|
3040
|
+
.nav-icon svg { width: 18px; height: 18px; }
|
|
3041
|
+
main { padding: 32px clamp(20px, 4vw, 48px) 64px; max-width: 1320px; width: 100%; min-width: 0; }
|
|
3042
|
+
main[aria-busy="true"] .view { opacity: 0.62; }
|
|
3043
|
+
.view { animation: view-in 180ms ease-out; }
|
|
3044
|
+
.view[hidden] { display: none; }
|
|
3045
|
+
.view-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
|
|
3046
|
+
.view h2 { margin: 0 0 5px; font-size: clamp(24px, 3vw, 30px); line-height: 1.2; letter-spacing: -0.035em; }
|
|
3047
|
+
.view h3 { margin: 24px 0 10px; font-size: 15px; letter-spacing: -0.01em; }
|
|
3048
|
+
.lede { margin: 0; color: var(--muted); max-width: 72ch; font-size: 14px; }
|
|
3049
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 175px), 1fr)); gap: 14px; }
|
|
2732
3050
|
.card {
|
|
2733
3051
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
2734
|
-
padding:
|
|
3052
|
+
padding: 18px; box-shadow: var(--shadow-sm);
|
|
2735
3053
|
}
|
|
2736
|
-
.card
|
|
2737
|
-
.card
|
|
3054
|
+
.stat-card { position: relative; overflow: hidden; }
|
|
3055
|
+
.stat-card::after { content: ""; position: absolute; inset: 0 0 auto; height: 2px; background: linear-gradient(90deg, var(--accent), transparent 78%); opacity: 0.7; }
|
|
3056
|
+
.card .k { color: var(--faint); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
3057
|
+
.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; }
|
|
2738
3058
|
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
2739
|
-
.
|
|
3059
|
+
.section-card { background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-xs); overflow: hidden; }
|
|
3060
|
+
.section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
|
|
3061
|
+
.section-title h3 { margin: 0; }
|
|
3062
|
+
.split { display: grid; grid-template-columns: minmax(0, 250px) minmax(0, 1fr); gap: 18px; align-items: start; }
|
|
3063
|
+
#collections { padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); box-shadow: var(--shadow-xs); }
|
|
2740
3064
|
.collection {
|
|
2741
3065
|
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
2742
|
-
border-radius:
|
|
3066
|
+
border-radius: 9px; padding: 10px 11px; min-height: 48px; color: var(--ink); cursor: pointer;
|
|
2743
3067
|
}
|
|
2744
|
-
.collection
|
|
3068
|
+
.collection:hover { background: var(--bg-muted); }
|
|
3069
|
+
.collection[aria-current="true"] { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 18%, var(--line)); color: var(--accent); }
|
|
2745
3070
|
.collection small { display: block; color: var(--faint); }
|
|
2746
3071
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
2747
|
-
.chip {
|
|
2748
|
-
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
2749
|
-
border-radius: 999px; padding: 4px 8px;
|
|
2750
|
-
}
|
|
3072
|
+
.chip { font-family: var(--mono); font-size: 11px; background: var(--bg-muted); border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; }
|
|
2751
3073
|
.chip i { color: var(--faint); font-style: normal; }
|
|
2752
3074
|
.sql-layout {
|
|
2753
3075
|
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
2754
3076
|
}
|
|
2755
|
-
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
2756
|
-
.sql-tables button { text-align: left; }
|
|
3077
|
+
.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); }
|
|
3078
|
+
.sql-tables button { text-align: left; border: 0; border-radius: 8px; padding: 9px 10px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
3079
|
+
.sql-tables button:hover, .sql-tables button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); }
|
|
2757
3080
|
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
2758
|
-
table { width: 100%; border-
|
|
2759
|
-
|
|
2760
|
-
th {
|
|
3081
|
+
.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); }
|
|
3082
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
3083
|
+
th, td { text-align: left; padding: 11px 13px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
3084
|
+
th { color: var(--faint); background: var(--bg-inset); font-weight: 650; font-size: 11px; text-transform: uppercase; letter-spacing: 0.045em; }
|
|
3085
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
2761
3086
|
tr.clickable { cursor: pointer; }
|
|
2762
|
-
tr.clickable:hover { background: var(--
|
|
3087
|
+
tr.clickable:hover { background: var(--accent-soft); }
|
|
2763
3088
|
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
2764
3089
|
pre {
|
|
2765
3090
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
2766
|
-
background: var(--bg-inset); border-radius: 10px; padding:
|
|
2767
|
-
}
|
|
2768
|
-
.
|
|
3091
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 10px; padding: 14px; font-size: 12px; line-height: 1.65;
|
|
3092
|
+
}
|
|
3093
|
+
.json-key { color: var(--code-key); }
|
|
3094
|
+
.json-string { color: var(--code-string); }
|
|
3095
|
+
.json-number { color: var(--code-number); }
|
|
3096
|
+
.json-boolean { color: var(--code-boolean); }
|
|
3097
|
+
.json-null { color: var(--code-null); font-style: italic; }
|
|
3098
|
+
.json-inline { display: block; max-width: 44rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
3099
|
+
.json-view { position: relative; max-height: 26rem; overflow: auto; }
|
|
3100
|
+
.json-editor { position: relative; min-height: 190px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--bg-inset); overflow: hidden; }
|
|
3101
|
+
.json-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); }
|
|
3102
|
+
.json-editor.invalid { border-color: var(--err); }
|
|
3103
|
+
.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; }
|
|
3104
|
+
.json-editor pre { position: absolute; inset: 0; pointer-events: none; background: transparent; }
|
|
3105
|
+
.json-editor textarea { position: relative; z-index: 1; resize: vertical; background: transparent; color: transparent; caret-color: var(--ink); -webkit-text-fill-color: transparent; }
|
|
3106
|
+
.json-editor textarea::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); }
|
|
3107
|
+
.editor-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; }
|
|
3108
|
+
.json-status { display: inline-flex; align-items: center; gap: 6px; color: var(--ok); font-size: 11px; }
|
|
3109
|
+
.json-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
3110
|
+
.json-status.invalid { color: var(--err); }
|
|
3111
|
+
.format-json { border: 0; padding: 3px 5px; background: transparent; color: var(--accent); font-size: 11px; font-weight: 650; cursor: pointer; }
|
|
3112
|
+
.row-actions { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 14px 0; }
|
|
2769
3113
|
.banner {
|
|
2770
|
-
display: none;
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
3114
|
+
display: none; position: sticky; top: calc(var(--header) + 12px); z-index: 15;
|
|
3115
|
+
align-items: center; gap: 10px; margin: 0 0 16px; padding: 11px 12px 11px 14px;
|
|
3116
|
+
border: 1px solid color-mix(in srgb, var(--err) 30%, var(--line)); border-radius: 10px;
|
|
3117
|
+
background: var(--err-soft); color: var(--err); box-shadow: var(--shadow-sm); font-size: 13px;
|
|
3118
|
+
}
|
|
3119
|
+
.banner.show { display: flex; }
|
|
3120
|
+
.banner span { flex: 1; }
|
|
3121
|
+
.banner button { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 7px; background: transparent; color: currentColor; cursor: pointer; }
|
|
3122
|
+
.banner button:hover { background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
3123
|
+
.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); }
|
|
2775
3124
|
dialog {
|
|
2776
|
-
border: 1px solid var(--line); border-radius:
|
|
2777
|
-
color: var(--ink); width: min(
|
|
2778
|
-
}
|
|
2779
|
-
dialog
|
|
2780
|
-
dialog
|
|
3125
|
+
border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--bg-elev);
|
|
3126
|
+
color: var(--ink); width: min(720px, calc(100% - 24px)); max-width: calc(100vw - 24px); box-shadow: var(--shadow-lg);
|
|
3127
|
+
}
|
|
3128
|
+
dialog[open] { animation: dialog-in 160ms ease-out; }
|
|
3129
|
+
dialog::backdrop { background: rgb(12 14 20 / 0.58); backdrop-filter: blur(3px); }
|
|
3130
|
+
dialog form, .dialog-body { padding: 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
3131
|
+
.dialog-heading { display: flex; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
|
3132
|
+
.dialog-heading strong { font-size: 18px; letter-spacing: -0.02em; }
|
|
3133
|
+
.dialog-heading span { color: var(--muted); font-size: 12px; }
|
|
2781
3134
|
textarea {
|
|
2782
3135
|
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
2783
3136
|
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
2784
3137
|
font-family: var(--mono); font-size: 12px;
|
|
2785
3138
|
}
|
|
2786
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
2787
|
-
.
|
|
3139
|
+
.dialog-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding-top: 4px; }
|
|
3140
|
+
.dialog-actions .btn-danger { margin-right: auto; }
|
|
3141
|
+
.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); }
|
|
2788
3142
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
2789
3143
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
2790
3144
|
.status-2 { color: var(--ok); }
|
|
2791
3145
|
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
2792
3146
|
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
3147
|
+
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
3148
|
+
.form-grid .wide { grid-column: span 2; }
|
|
2793
3149
|
.route {
|
|
2794
|
-
display: grid; grid-template-columns:
|
|
2795
|
-
padding:
|
|
2796
|
-
}
|
|
2797
|
-
.
|
|
3150
|
+
display: grid; grid-template-columns: 84px minmax(0, 1fr) auto; gap: 10px; align-items: center;
|
|
3151
|
+
padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--bg-elev);
|
|
3152
|
+
}
|
|
3153
|
+
.route:first-child { border-radius: var(--radius) var(--radius) 0 0; }
|
|
3154
|
+
.route:last-child { border-bottom: 0; border-radius: 0 0 var(--radius) var(--radius); }
|
|
3155
|
+
.route-tools { display: flex; align-items: flex-end; gap: 12px; margin-bottom: 12px; }
|
|
3156
|
+
.route-tools .field { flex: 1; }
|
|
3157
|
+
.route-tools input { width: 100%; }
|
|
3158
|
+
.route-count { padding-bottom: 10px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3159
|
+
#route-list { max-height: 34rem; border: 1px solid var(--line); border-radius: var(--radius); overflow: auto; box-shadow: var(--shadow-xs); }
|
|
3160
|
+
.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; }
|
|
3161
|
+
@keyframes view-in { from { transform: translateY(3px); } to { transform: translateY(0); } }
|
|
3162
|
+
@keyframes dialog-in { from { transform: translateY(8px) scale(0.99); } to { transform: translateY(0) scale(1); } }
|
|
3163
|
+
@keyframes pulse { 50% { opacity: 0.45; } }
|
|
3164
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
|
|
3165
|
+
@media (forced-colors: active) { .json-editor pre { display: none; } .json-editor textarea { color: CanvasText; -webkit-text-fill-color: CanvasText; } }
|
|
2798
3166
|
@media (max-width: 800px) {
|
|
3167
|
+
:root { --header: 0px; }
|
|
2799
3168
|
header.top {
|
|
2800
|
-
align-items: stretch; flex-direction: column;
|
|
2801
|
-
padding
|
|
3169
|
+
position: relative; align-items: stretch; flex-direction: column;
|
|
3170
|
+
padding: max(12px, env(safe-area-inset-top)) 14px 12px;
|
|
2802
3171
|
}
|
|
2803
3172
|
input, select, textarea { font-size: 16px; }
|
|
2804
3173
|
.brand, .vendor, .controls { width: 100%; }
|
|
3174
|
+
.brand { gap: 10px; }
|
|
3175
|
+
.vendor { order: 2; border-radius: 9px; }
|
|
2805
3176
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
3177
|
+
.sync-status { display: none; }
|
|
2806
3178
|
/* The layout fills the screen. A single-column grid stretches every auto
|
|
2807
3179
|
row, so the tab bar grew with the leftover height and the buttons floated
|
|
2808
3180
|
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
@@ -2812,14 +3184,23 @@ var renderAdminDocument = (service) => {
|
|
|
2812
3184
|
align-content: start;
|
|
2813
3185
|
}
|
|
2814
3186
|
nav.side {
|
|
2815
|
-
align-self: start; height: auto;
|
|
3187
|
+
position: sticky; top: 0; z-index: 12; align-self: start; height: auto;
|
|
2816
3188
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
2817
3189
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
2818
|
-
padding: 8px;
|
|
3190
|
+
padding: 8px; box-shadow: var(--shadow-xs);
|
|
2819
3191
|
}
|
|
2820
|
-
nav
|
|
3192
|
+
.nav-label { display: none; }
|
|
3193
|
+
nav.side button { width: auto; white-space: nowrap; flex: none; }
|
|
3194
|
+
nav.side button[aria-current="true"]::before { inset: auto 9px -8px; width: auto; height: 3px; border-radius: 3px 3px 0 0; }
|
|
2821
3195
|
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
2822
|
-
|
|
3196
|
+
.form-grid { grid-template-columns: minmax(0, 1fr); }
|
|
3197
|
+
.form-grid .wide { grid-column: auto; }
|
|
3198
|
+
main { padding: 22px 14px 48px; }
|
|
3199
|
+
.view-head { margin-bottom: 18px; }
|
|
3200
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
3201
|
+
.card { padding: 14px; }
|
|
3202
|
+
.card .v { font-size: 20px; }
|
|
3203
|
+
.table-shell { border: 0; background: transparent; box-shadow: none; overflow: visible; }
|
|
2823
3204
|
table, thead, tbody, tr, th, td { display: block; }
|
|
2824
3205
|
thead { display: none; }
|
|
2825
3206
|
tr {
|
|
@@ -2834,50 +3215,63 @@ var renderAdminDocument = (service) => {
|
|
|
2834
3215
|
.route { grid-template-columns: 72px 1fr; }
|
|
2835
3216
|
.route button { grid-column: 1 / -1; }
|
|
2836
3217
|
}
|
|
3218
|
+
@media (max-width: 460px) {
|
|
3219
|
+
.controls label.field { min-width: calc(50% - 4px); }
|
|
3220
|
+
.controls .icon-btn, .controls .btn { flex: 1; }
|
|
3221
|
+
.grid { grid-template-columns: minmax(0, 1fr); }
|
|
3222
|
+
.dialog-actions { flex-wrap: wrap; }
|
|
3223
|
+
.dialog-actions button { flex: 1; }
|
|
3224
|
+
.dialog-actions .btn-danger { flex-basis: 100%; margin-right: 0; }
|
|
3225
|
+
}
|
|
2837
3226
|
</style>
|
|
2838
3227
|
</head>
|
|
2839
3228
|
<body>
|
|
2840
3229
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
2841
3230
|
<header class="top">
|
|
2842
3231
|
<div class="brand">
|
|
2843
|
-
<div class="brand-
|
|
2844
|
-
<
|
|
2845
|
-
|
|
3232
|
+
<div class="brand-lockup">
|
|
3233
|
+
<div class="brand-mark" aria-hidden="true">
|
|
3234
|
+
<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>
|
|
3235
|
+
</div>
|
|
3236
|
+
<div class="brand-id">
|
|
3237
|
+
<strong>${name}</strong>
|
|
3238
|
+
<span>Mock admin</span>
|
|
3239
|
+
</div>
|
|
2846
3240
|
</div>
|
|
2847
3241
|
<div class="vendor" id="vendor" hidden></div>
|
|
2848
3242
|
</div>
|
|
2849
3243
|
<div class="controls">
|
|
3244
|
+
<span class="sync-status" id="sync-status" role="status" aria-live="polite">Ready</span>
|
|
2850
3245
|
<label class="field">Namespace
|
|
2851
3246
|
<select id="namespace" aria-label="Namespace"></select>
|
|
2852
3247
|
</label>
|
|
2853
3248
|
<label class="field">Admin key
|
|
2854
3249
|
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
2855
3250
|
</label>
|
|
2856
|
-
<button class="icon-btn" id="theme" type="button" aria-label="
|
|
2857
|
-
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
3251
|
+
<button class="icon-btn" id="theme" type="button" aria-label="Use dark theme" title="Use dark theme"></button>
|
|
3252
|
+
<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>
|
|
2858
3253
|
</div>
|
|
2859
3254
|
</header>
|
|
2860
3255
|
<div class="layout">
|
|
2861
3256
|
<nav class="side" id="nav" aria-label="Admin sections">
|
|
2862
|
-
<
|
|
2863
|
-
<button type="button" data-view="
|
|
2864
|
-
<button type="button" data-view="
|
|
2865
|
-
<button type="button" data-view="
|
|
2866
|
-
<button type="button" data-view="
|
|
2867
|
-
<button type="button" data-view="
|
|
3257
|
+
<span class="nav-label">Workspace</span>
|
|
3258
|
+
<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>
|
|
3259
|
+
<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>
|
|
3260
|
+
<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>
|
|
3261
|
+
<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>
|
|
3262
|
+
<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>
|
|
3263
|
+
<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>
|
|
2868
3264
|
</nav>
|
|
2869
3265
|
<main id="views">
|
|
2870
|
-
<
|
|
3266
|
+
<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>
|
|
2871
3267
|
<section class="view" id="view-overview">
|
|
2872
|
-
<h2>Overview</h2>
|
|
2873
|
-
<p class="lede">Health, clock, and the collections this namespace is holding.</p>
|
|
3268
|
+
<div class="view-head"><div><h2>Overview</h2><p class="lede">Health, clock, and the collections this namespace is holding.</p></div></div>
|
|
2874
3269
|
<div class="grid" id="overview-cards"></div>
|
|
2875
3270
|
<h3>Recent requests</h3>
|
|
2876
3271
|
<div id="overview-requests"></div>
|
|
2877
3272
|
</section>
|
|
2878
3273
|
<section class="view" id="view-state" hidden>
|
|
2879
|
-
<h2>State</h2>
|
|
2880
|
-
<p class="lede">Every collection in this namespace. Declared fields stay visible before any row exists; stored rows add whatever else they contain.</p>
|
|
3274
|
+
<div class="view-head"><div><h2>State</h2><p class="lede">Inspect and edit every collection in this namespace.</p></div></div>
|
|
2881
3275
|
<div class="split">
|
|
2882
3276
|
<div id="collections"></div>
|
|
2883
3277
|
<div>
|
|
@@ -2890,8 +3284,7 @@ var renderAdminDocument = (service) => {
|
|
|
2890
3284
|
</div>
|
|
2891
3285
|
</section>
|
|
2892
3286
|
<section class="view" id="view-clock" hidden>
|
|
2893
|
-
<h2>Clock</h2>
|
|
2894
|
-
<p class="lede">The clock every timestamp in this mock reads.</p>
|
|
3287
|
+
<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>
|
|
2895
3288
|
<div class="card" id="clock-readout"></div>
|
|
2896
3289
|
<div class="row-actions">
|
|
2897
3290
|
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
@@ -2910,21 +3303,22 @@ var renderAdminDocument = (service) => {
|
|
|
2910
3303
|
</form>
|
|
2911
3304
|
</section>
|
|
2912
3305
|
<section class="view" id="view-faults" hidden>
|
|
2913
|
-
<h2>Faults</h2>
|
|
2914
|
-
<p class="lede">Inject a response, a delay, or a named preset. Rules apply to this namespace.</p>
|
|
3306
|
+
<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>
|
|
2915
3307
|
<form id="fault-form" class="card stack">
|
|
2916
|
-
<
|
|
2917
|
-
<
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
<
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
<
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
<
|
|
2927
|
-
|
|
3308
|
+
<div class="form-grid">
|
|
3309
|
+
<label class="field wide">Preset
|
|
3310
|
+
<select id="fault-preset"><option value="">Custom rule</option></select>
|
|
3311
|
+
</label>
|
|
3312
|
+
<label class="field">Status
|
|
3313
|
+
<input id="fault-status" inputmode="numeric" placeholder="503">
|
|
3314
|
+
</label>
|
|
3315
|
+
<label class="field wide">Path prefix
|
|
3316
|
+
<input id="fault-path" placeholder="/v1">
|
|
3317
|
+
</label>
|
|
3318
|
+
<label class="field">Method
|
|
3319
|
+
<input id="fault-method" placeholder="GET">
|
|
3320
|
+
</label>
|
|
3321
|
+
</div>
|
|
2928
3322
|
<div class="dialog-actions">
|
|
2929
3323
|
<button class="btn-primary" type="submit">Add fault</button>
|
|
2930
3324
|
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
@@ -2933,26 +3327,31 @@ var renderAdminDocument = (service) => {
|
|
|
2933
3327
|
<div id="fault-list"></div>
|
|
2934
3328
|
</section>
|
|
2935
3329
|
<section class="view" id="view-journal" hidden>
|
|
2936
|
-
<h2>Journal</h2>
|
|
2937
|
-
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
2938
|
-
<div class="row-actions">
|
|
2939
|
-
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
2940
|
-
</div>
|
|
3330
|
+
<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>
|
|
2941
3331
|
<div id="journal"></div>
|
|
2942
3332
|
</section>
|
|
2943
3333
|
<section class="view" id="view-routes" hidden>
|
|
2944
|
-
<h2>Routes</h2>
|
|
2945
|
-
<
|
|
3334
|
+
<div class="view-head"><div><h2>Routes</h2><p class="lede">Explore shared controls and service-specific admin endpoints.</p></div></div>
|
|
3335
|
+
<div class="route-tools">
|
|
3336
|
+
<label class="field">Filter routes
|
|
3337
|
+
<input id="route-search" type="search" placeholder="Method, path, or extension" autocomplete="off">
|
|
3338
|
+
</label>
|
|
3339
|
+
<span class="route-count" id="route-count"></span>
|
|
3340
|
+
</div>
|
|
2946
3341
|
<div id="route-list"></div>
|
|
2947
|
-
<form id="route-form" class="card stack">
|
|
3342
|
+
<form id="route-form" class="card stack" style="margin-top: 18px">
|
|
2948
3343
|
<strong id="route-title">Select a route</strong>
|
|
2949
3344
|
<label class="field">JSON body
|
|
2950
|
-
<
|
|
3345
|
+
<div class="json-editor" id="route-editor" hidden>
|
|
3346
|
+
<pre aria-hidden="true"><code id="route-highlight"></code></pre>
|
|
3347
|
+
<textarea id="route-body" placeholder="{ }" spellcheck="false" aria-describedby="route-json-status" disabled></textarea>
|
|
3348
|
+
</div>
|
|
3349
|
+
<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>
|
|
2951
3350
|
</label>
|
|
2952
3351
|
<div class="dialog-actions">
|
|
2953
|
-
<button class="btn-primary" type="submit">Send</button>
|
|
3352
|
+
<button class="btn-primary" id="route-send" type="submit" disabled>Send request</button>
|
|
2954
3353
|
</div>
|
|
2955
|
-
<pre id="route-result">Response will show here.</pre>
|
|
3354
|
+
<pre class="json-view" id="route-result"><code>Response will show here.</code></pre>
|
|
2956
3355
|
</form>
|
|
2957
3356
|
</section>
|
|
2958
3357
|
</main>
|
|
@@ -2960,12 +3359,16 @@ var renderAdminDocument = (service) => {
|
|
|
2960
3359
|
</div>
|
|
2961
3360
|
<dialog id="editor">
|
|
2962
3361
|
<form method="dialog" id="editor-form">
|
|
2963
|
-
<strong id="editor-title">Record</strong>
|
|
3362
|
+
<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>
|
|
2964
3363
|
<label class="field">Id
|
|
2965
3364
|
<input id="editor-id" required>
|
|
2966
3365
|
</label>
|
|
2967
3366
|
<label class="field">JSON value
|
|
2968
|
-
<
|
|
3367
|
+
<div class="json-editor" id="record-editor">
|
|
3368
|
+
<pre aria-hidden="true"><code id="editor-highlight"></code></pre>
|
|
3369
|
+
<textarea id="editor-value" spellcheck="false" aria-describedby="editor-json-status"></textarea>
|
|
3370
|
+
</div>
|
|
3371
|
+
<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>
|
|
2969
3372
|
</label>
|
|
2970
3373
|
<div class="dialog-actions">
|
|
2971
3374
|
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
@@ -3007,8 +3410,8 @@ var describeExtension = (extension) => {
|
|
|
3007
3410
|
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
3008
3411
|
};
|
|
3009
3412
|
};
|
|
3010
|
-
var adminUiRoutes = (service, ui) => {
|
|
3011
|
-
const shell = () => renderAdminDocument(service);
|
|
3413
|
+
var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
3414
|
+
const shell = () => renderAdminDocument(service, adminPrefix);
|
|
3012
3415
|
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
3013
3416
|
return {
|
|
3014
3417
|
"GET /ui": () => htmlResponse(document3()),
|
|
@@ -3929,7 +4332,7 @@ var stateAdminRoutes = (routes) => {
|
|
|
3929
4332
|
};
|
|
3930
4333
|
|
|
3931
4334
|
// ../core/dist/version.js
|
|
3932
|
-
var PACKAGE_VERSION = true ? "
|
|
4335
|
+
var PACKAGE_VERSION = true ? "2.0.0" : UNRELEASED_VERSION;
|
|
3933
4336
|
|
|
3934
4337
|
// ../core/dist/signing.js
|
|
3935
4338
|
var encoder = new TextEncoder();
|
|
@@ -4022,7 +4425,9 @@ var createWebhookHub = (options) => {
|
|
|
4022
4425
|
const faults = /* @__PURE__ */ new Map();
|
|
4023
4426
|
const held = /* @__PURE__ */ new Map();
|
|
4024
4427
|
const inFlight = /* @__PURE__ */ new Set();
|
|
4428
|
+
const controllers = /* @__PURE__ */ new Map();
|
|
4025
4429
|
let immediate = Promise.resolve();
|
|
4430
|
+
const snapshots = /* @__PURE__ */ new WeakMap();
|
|
4026
4431
|
const track = (work) => {
|
|
4027
4432
|
inFlight.add(work);
|
|
4028
4433
|
void work.finally(() => inFlight.delete(work));
|
|
@@ -4043,6 +4448,7 @@ var createWebhookHub = (options) => {
|
|
|
4043
4448
|
responseBody: null
|
|
4044
4449
|
};
|
|
4045
4450
|
const controller = new AbortController();
|
|
4451
|
+
controllers.set(delivery.id, controller);
|
|
4046
4452
|
const timer = scheduleTimer(() => controller.abort(), timeoutMs);
|
|
4047
4453
|
try {
|
|
4048
4454
|
const signed = await options.signer({
|
|
@@ -4072,6 +4478,7 @@ var createWebhookHub = (options) => {
|
|
|
4072
4478
|
} catch (error2) {
|
|
4073
4479
|
record2.error = controller.signal.aborted ? `timed out after ${timeoutMs}ms` : error2 instanceof Error ? error2.message : String(error2);
|
|
4074
4480
|
} finally {
|
|
4481
|
+
controllers.delete(delivery.id);
|
|
4075
4482
|
cancel(timer);
|
|
4076
4483
|
record2.durationMs = now() - started;
|
|
4077
4484
|
delivery.attempts.push(record2);
|
|
@@ -4089,6 +4496,8 @@ var createWebhookHub = (options) => {
|
|
|
4089
4496
|
if (!payloads.has(delivery.id))
|
|
4090
4497
|
return;
|
|
4091
4498
|
await attempt(delivery).then((ok) => {
|
|
4499
|
+
if (!payloads.has(delivery.id))
|
|
4500
|
+
return;
|
|
4092
4501
|
if (ok)
|
|
4093
4502
|
delivery.state = "delivered";
|
|
4094
4503
|
else
|
|
@@ -4150,6 +4559,37 @@ var createWebhookHub = (options) => {
|
|
|
4150
4559
|
fanOut(message);
|
|
4151
4560
|
};
|
|
4152
4561
|
const hub = {
|
|
4562
|
+
pending(namespace) {
|
|
4563
|
+
return inFlight.size + (held.get(namespace)?.length ?? 0) + [...deliveries.values()].filter((d) => d.namespace === namespace && d.state === "pending").length;
|
|
4564
|
+
},
|
|
4565
|
+
snapshot(namespace) {
|
|
4566
|
+
if (hub.pending(namespace) > 0)
|
|
4567
|
+
throw new Error("webhooks are not quiescent");
|
|
4568
|
+
const handle = {};
|
|
4569
|
+
snapshots.set(handle, structuredClone({
|
|
4570
|
+
namespace,
|
|
4571
|
+
endpoints: own.get(namespace) ?? [],
|
|
4572
|
+
messages: messages.filter((m) => m.namespace === namespace),
|
|
4573
|
+
deliveries: [...deliveries].filter(([, d]) => d.namespace === namespace),
|
|
4574
|
+
payloads: [...payloads].filter(([, p]) => p.message.namespace === namespace),
|
|
4575
|
+
faults: faults.get(namespace) ?? []
|
|
4576
|
+
}));
|
|
4577
|
+
return handle;
|
|
4578
|
+
},
|
|
4579
|
+
restore(namespace, handle) {
|
|
4580
|
+
const point = typeof handle === "object" && handle !== null ? snapshots.get(handle) : void 0;
|
|
4581
|
+
if (!point || point.namespace !== namespace || hub.pending(namespace) > 0)
|
|
4582
|
+
throw new Error("invalid webhook restore");
|
|
4583
|
+
hub.clear(namespace);
|
|
4584
|
+
const copied = structuredClone(point);
|
|
4585
|
+
own.set(namespace, copied.endpoints);
|
|
4586
|
+
messages.push(...copied.messages);
|
|
4587
|
+
for (const [id2, delivery] of copied.deliveries)
|
|
4588
|
+
deliveries.set(id2, delivery);
|
|
4589
|
+
for (const [id2, payload] of copied.payloads)
|
|
4590
|
+
payloads.set(id2, payload);
|
|
4591
|
+
faults.set(namespace, copied.faults);
|
|
4592
|
+
},
|
|
4153
4593
|
publish(input) {
|
|
4154
4594
|
const contentType = input.contentType ?? (input.form ? "application/x-www-form-urlencoded" : "application/json");
|
|
4155
4595
|
const body = typeof input.body === "string" ? input.body : input.form ? new URLSearchParams(input.form).toString() : JSON.stringify(input.body);
|
|
@@ -4240,6 +4680,7 @@ var createWebhookHub = (options) => {
|
|
|
4240
4680
|
for (const [id2, delivery] of deliveries) {
|
|
4241
4681
|
if (namespace !== void 0 && delivery.namespace !== namespace)
|
|
4242
4682
|
continue;
|
|
4683
|
+
controllers.get(id2)?.abort();
|
|
4243
4684
|
const timer = pending.get(id2);
|
|
4244
4685
|
if (timer !== void 0)
|
|
4245
4686
|
cancel(timer);
|
|
@@ -4264,7 +4705,10 @@ var createWebhookHub = (options) => {
|
|
|
4264
4705
|
};
|
|
4265
4706
|
return hub;
|
|
4266
4707
|
};
|
|
4267
|
-
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4708
|
+
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4709
|
+
status,
|
|
4710
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4711
|
+
});
|
|
4268
4712
|
var adminError3 = (status, message) => json4(status, { error: { type: "mockingbird_admin", message } });
|
|
4269
4713
|
var isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4270
4714
|
var parseEndpoint = (value) => {
|
|
@@ -4372,7 +4816,6 @@ var AT_HEADER = "x-mockingbird-at";
|
|
|
4372
4816
|
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
4373
4817
|
var DEFAULT_NAMESPACE = "default";
|
|
4374
4818
|
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4375
|
-
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
4376
4819
|
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4377
4820
|
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
4378
4821
|
var effects = /* @__PURE__ */ new WeakMap();
|
|
@@ -4411,6 +4854,22 @@ var operationMatcher = (document3) => {
|
|
|
4411
4854
|
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
4412
4855
|
};
|
|
4413
4856
|
var createRuntime = (options) => {
|
|
4857
|
+
const adminPrefix = resolveAdminPrefix(options.adminPrefix);
|
|
4858
|
+
const internalPaths = Object.entries(options.document?.paths ?? {}).filter(([, item]) => item["x-mockingbird-internal"] === true).map(([path]) => path);
|
|
4859
|
+
for (const path of internalPaths) {
|
|
4860
|
+
if (!path.startsWith(`${ADMIN_PREFIX}/blobs/`))
|
|
4861
|
+
throw new Error(`internal transport ${path} must live under ${ADMIN_PREFIX}/blobs/`);
|
|
4862
|
+
}
|
|
4863
|
+
assertAdminPrefixAvailable(adminPrefix, Object.keys(options.document?.paths ?? {}).filter((path) => !internalPaths.includes(path)));
|
|
4864
|
+
const internalMatch = (path) => {
|
|
4865
|
+
for (const pattern of internalPaths) {
|
|
4866
|
+
const actual = path.split("/");
|
|
4867
|
+
const expected = pattern.split("/");
|
|
4868
|
+
if (actual.length === expected.length && expected.every((part, i) => part.startsWith("{") || part === actual[i]))
|
|
4869
|
+
return pattern;
|
|
4870
|
+
}
|
|
4871
|
+
return void 0;
|
|
4872
|
+
};
|
|
4414
4873
|
assertAdminUi(options.adminUi);
|
|
4415
4874
|
const sqlite = bootSqlite(options.sqlite);
|
|
4416
4875
|
const clock = options.clock ?? createClock();
|
|
@@ -4425,6 +4884,20 @@ var createRuntime = (options) => {
|
|
|
4425
4884
|
const instances = /* @__PURE__ */ new Map();
|
|
4426
4885
|
const publicNamespaces = /* @__PURE__ */ new Set();
|
|
4427
4886
|
const branchRngs = /* @__PURE__ */ new Map();
|
|
4887
|
+
const namespaceClocks = /* @__PURE__ */ new Map();
|
|
4888
|
+
const namespaceForStorage = /* @__PURE__ */ new Map();
|
|
4889
|
+
let isolatedNamespaces = false;
|
|
4890
|
+
const namespaceClock = (name = DEFAULT_NAMESPACE) => {
|
|
4891
|
+
if (!isolatedNamespaces || name === DEFAULT_NAMESPACE)
|
|
4892
|
+
return clock;
|
|
4893
|
+
let found = namespaceClocks.get(name);
|
|
4894
|
+
if (!found) {
|
|
4895
|
+
found = createClock();
|
|
4896
|
+
namespaceClocks.set(name, found);
|
|
4897
|
+
}
|
|
4898
|
+
return found;
|
|
4899
|
+
};
|
|
4900
|
+
const fleetPoints = /* @__PURE__ */ new WeakMap();
|
|
4428
4901
|
const timelines = /* @__PURE__ */ new Map();
|
|
4429
4902
|
const branchStorage = /* @__PURE__ */ new Map();
|
|
4430
4903
|
const captured = /* @__PURE__ */ new Map();
|
|
@@ -4438,17 +4911,22 @@ var createRuntime = (options) => {
|
|
|
4438
4911
|
if (!NAMESPACE_PATTERN.test(key) || !NAMESPACE_PATTERN.test(publicNamespace)) {
|
|
4439
4912
|
throw new RangeError(`namespace must match ${NAMESPACE_PATTERN}: ${JSON.stringify(publicNamespace)}`);
|
|
4440
4913
|
}
|
|
4914
|
+
const createdContextRng = isolatedRng ?? (isolatedNamespaces && key !== DEFAULT_NAMESPACE ? createRng(options.seed ?? 0) : rng);
|
|
4441
4915
|
const created = options.create({
|
|
4442
4916
|
namespace: storageNamespace(key),
|
|
4443
4917
|
publicNamespace,
|
|
4918
|
+
adminPrefix,
|
|
4444
4919
|
sqlite,
|
|
4445
|
-
clock,
|
|
4446
|
-
rng:
|
|
4920
|
+
clock: namespaceClock(publicNamespace),
|
|
4921
|
+
rng: createdContextRng
|
|
4447
4922
|
});
|
|
4448
4923
|
instances.set(key, created);
|
|
4924
|
+
namespaceForStorage.set(key, publicNamespace);
|
|
4449
4925
|
publicNamespaces.add(publicNamespace);
|
|
4450
4926
|
if (isolatedRng)
|
|
4451
4927
|
branchRngs.set(key, isolatedRng);
|
|
4928
|
+
else if (isolatedNamespaces && key !== DEFAULT_NAMESPACE)
|
|
4929
|
+
branchRngs.set(key, createdContextRng);
|
|
4452
4930
|
return created;
|
|
4453
4931
|
};
|
|
4454
4932
|
const instance = (name = DEFAULT_NAMESPACE) => instanceFor(name);
|
|
@@ -4466,7 +4944,7 @@ var createRuntime = (options) => {
|
|
|
4466
4944
|
captured.set(storage, snapshot2);
|
|
4467
4945
|
return Object.freeze({
|
|
4468
4946
|
snapshot: snapshot2,
|
|
4469
|
-
clock: Object.freeze(
|
|
4947
|
+
clock: Object.freeze(namespaceClock(namespaceForStorage.get(storage) ?? storage).state()),
|
|
4470
4948
|
rngState: (branchRngs.get(storage) ?? rng).state()
|
|
4471
4949
|
});
|
|
4472
4950
|
};
|
|
@@ -4476,7 +4954,7 @@ var createRuntime = (options) => {
|
|
|
4476
4954
|
return found;
|
|
4477
4955
|
instance(name);
|
|
4478
4956
|
found = new Timeline({
|
|
4479
|
-
now:
|
|
4957
|
+
now: namespaceClock(name).now,
|
|
4480
4958
|
...options.maxCheckpoints !== void 0 ? { maxCheckpoints: options.maxCheckpoints } : {}
|
|
4481
4959
|
});
|
|
4482
4960
|
found.commit(capture(name));
|
|
@@ -4503,12 +4981,13 @@ var createRuntime = (options) => {
|
|
|
4503
4981
|
const point = history2.checkout("main", at);
|
|
4504
4982
|
restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
|
|
4505
4983
|
captured.set(namespace, point.value.snapshot);
|
|
4506
|
-
rng.setState(point.value.rngState);
|
|
4507
|
-
|
|
4984
|
+
(branchRngs.get(namespace) ?? rng).setState(point.value.rngState);
|
|
4985
|
+
const selectedClock = namespaceClock(namespace);
|
|
4986
|
+
selectedClock.set(point.value.clock.now);
|
|
4508
4987
|
if (point.value.clock.frozen)
|
|
4509
|
-
|
|
4988
|
+
selectedClock.freeze();
|
|
4510
4989
|
else
|
|
4511
|
-
|
|
4990
|
+
selectedClock.unfreeze();
|
|
4512
4991
|
}
|
|
4513
4992
|
return namespace;
|
|
4514
4993
|
}
|
|
@@ -4566,11 +5045,12 @@ var createRuntime = (options) => {
|
|
|
4566
5045
|
const storage = ensureBranch(namespace, branchName);
|
|
4567
5046
|
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
4568
5047
|
captured.set(storage, point.value.snapshot);
|
|
4569
|
-
|
|
5048
|
+
const selectedClock = namespaceClock(namespace);
|
|
5049
|
+
selectedClock.set(point.value.clock.now);
|
|
4570
5050
|
if (point.value.clock.frozen)
|
|
4571
|
-
|
|
5051
|
+
selectedClock.freeze();
|
|
4572
5052
|
else
|
|
4573
|
-
|
|
5053
|
+
selectedClock.unfreeze();
|
|
4574
5054
|
(branchRngs.get(storage) ?? rng).setState(point.value.rngState);
|
|
4575
5055
|
};
|
|
4576
5056
|
const reset = async (name = DEFAULT_NAMESPACE) => {
|
|
@@ -4578,10 +5058,21 @@ var createRuntime = (options) => {
|
|
|
4578
5058
|
options.webhooks?.clear();
|
|
4579
5059
|
for (const each of instances.values())
|
|
4580
5060
|
await each.reset();
|
|
5061
|
+
for (const each of branchRngs.values())
|
|
5062
|
+
each.reset();
|
|
4581
5063
|
timelines.clear();
|
|
5064
|
+
for (const storage of branchStorage.values()) {
|
|
5065
|
+
instances.delete(storage);
|
|
5066
|
+
branchRngs.delete(storage);
|
|
5067
|
+
namespaceForStorage.delete(storage);
|
|
5068
|
+
}
|
|
4582
5069
|
branchStorage.clear();
|
|
4583
|
-
branchRngs.clear();
|
|
4584
5070
|
captured.clear();
|
|
5071
|
+
faults.clear();
|
|
5072
|
+
clock.reset();
|
|
5073
|
+
rng.reset();
|
|
5074
|
+
for (const each of namespaceClocks.values())
|
|
5075
|
+
each.reset();
|
|
4585
5076
|
return;
|
|
4586
5077
|
}
|
|
4587
5078
|
options.webhooks?.clear(name);
|
|
@@ -4599,11 +5090,18 @@ var createRuntime = (options) => {
|
|
|
4599
5090
|
else
|
|
4600
5091
|
clearNamespace(sqlite, storageNamespace(storage));
|
|
4601
5092
|
branchStorage.delete(mapping);
|
|
5093
|
+
instances.delete(storage);
|
|
5094
|
+
namespaceForStorage.delete(storage);
|
|
4602
5095
|
branchRngs.delete(storage);
|
|
4603
5096
|
captured.delete(storage);
|
|
4604
5097
|
}
|
|
4605
5098
|
timelines.delete(name);
|
|
4606
5099
|
captured.delete(name);
|
|
5100
|
+
faults.restore(name, { rules: [], rngState: createRng(options.seed ?? 0).state() });
|
|
5101
|
+
namespaceClock(name).reset();
|
|
5102
|
+
(branchRngs.get(name) ?? rng).reset();
|
|
5103
|
+
journal.clear(name);
|
|
5104
|
+
metrics.reset(name);
|
|
4607
5105
|
};
|
|
4608
5106
|
const snapshot = (name = DEFAULT_NAMESPACE) => {
|
|
4609
5107
|
return checkpoint(name, "main").value.snapshot;
|
|
@@ -4641,6 +5139,53 @@ var createRuntime = (options) => {
|
|
|
4641
5139
|
rng,
|
|
4642
5140
|
credentials,
|
|
4643
5141
|
webhooks: options.webhooks,
|
|
5142
|
+
isolateNamespaces() {
|
|
5143
|
+
if (publicNamespaces.size > 1)
|
|
5144
|
+
throw new Error("enable namespace isolation before creating named namespaces");
|
|
5145
|
+
isolatedNamespaces = true;
|
|
5146
|
+
faults.isolateNamespaces();
|
|
5147
|
+
},
|
|
5148
|
+
namespaceClock,
|
|
5149
|
+
namespaceOf(request) {
|
|
5150
|
+
const url = new URL(request.url);
|
|
5151
|
+
const prefixed = matchNamespacePath(url.pathname, adminPrefix);
|
|
5152
|
+
const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
|
|
5153
|
+
const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
|
|
5154
|
+
if (isAdminPath(path, adminPrefix))
|
|
5155
|
+
return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
|
|
5156
|
+
if (selected !== void 0)
|
|
5157
|
+
return selected;
|
|
5158
|
+
const credential = options.credential?.(request);
|
|
5159
|
+
return (credential !== void 0 ? credentials.get(credential) : void 0) ?? DEFAULT_NAMESPACE;
|
|
5160
|
+
},
|
|
5161
|
+
fleetSnapshot(namespace) {
|
|
5162
|
+
instance(namespace);
|
|
5163
|
+
const handle = {};
|
|
5164
|
+
fleetPoints.set(handle, {
|
|
5165
|
+
namespace,
|
|
5166
|
+
state: capture(namespace),
|
|
5167
|
+
faults: faults.snapshot(namespace),
|
|
5168
|
+
webhooks: options.webhooks?.snapshot(namespace)
|
|
5169
|
+
});
|
|
5170
|
+
return handle;
|
|
5171
|
+
},
|
|
5172
|
+
fleetRestore(handle, namespace) {
|
|
5173
|
+
const point = typeof handle === "object" && handle !== null ? fleetPoints.get(handle) : void 0;
|
|
5174
|
+
if (!point || point.namespace !== namespace)
|
|
5175
|
+
throw new Error("invalid fleet checkpoint");
|
|
5176
|
+
restoreNamespace(sqlite, storageNamespace(namespace), point.state.snapshot);
|
|
5177
|
+
captured.set(namespace, point.state.snapshot);
|
|
5178
|
+
const selectedClock = namespaceClock(namespace);
|
|
5179
|
+
selectedClock.freeze();
|
|
5180
|
+
selectedClock.set(point.state.clock.now);
|
|
5181
|
+
if (!point.state.clock.frozen)
|
|
5182
|
+
selectedClock.unfreeze();
|
|
5183
|
+
(branchRngs.get(namespace) ?? rng).setState(point.state.rngState);
|
|
5184
|
+
faults.restore(namespace, point.faults);
|
|
5185
|
+
if (point.webhooks !== void 0)
|
|
5186
|
+
options.webhooks?.restore(namespace, point.webhooks);
|
|
5187
|
+
timelines.delete(namespace);
|
|
5188
|
+
},
|
|
4644
5189
|
applyPreset: (name, namespace = DEFAULT_NAMESPACE, overrides = {}) => {
|
|
4645
5190
|
const preset = options.presets?.[name];
|
|
4646
5191
|
if (!preset)
|
|
@@ -4672,7 +5217,7 @@ var createRuntime = (options) => {
|
|
|
4672
5217
|
state: (namespace = DEFAULT_NAMESPACE) => inspectState(stateScope(namespace)),
|
|
4673
5218
|
fetch: async (incoming) => {
|
|
4674
5219
|
let request = incoming;
|
|
4675
|
-
const prefixed =
|
|
5220
|
+
const prefixed = matchNamespacePath(new URL(request.url).pathname, adminPrefix);
|
|
4676
5221
|
if (prefixed) {
|
|
4677
5222
|
const url2 = new URL(request.url);
|
|
4678
5223
|
url2.pathname = prefixed[2] ?? "/";
|
|
@@ -4688,7 +5233,15 @@ var createRuntime = (options) => {
|
|
|
4688
5233
|
signal: request.signal
|
|
4689
5234
|
});
|
|
4690
5235
|
}
|
|
4691
|
-
|
|
5236
|
+
const internalUrl = new URL(request.url);
|
|
5237
|
+
const internalPath = isAdminPath(internalUrl.pathname, adminPrefix) ? `${ADMIN_PREFIX}${internalUrl.pathname.slice(adminPrefix.length)}` : "";
|
|
5238
|
+
const internal = internalMatch(internalPath);
|
|
5239
|
+
const obsoleteInternalPath = adminPrefix !== ADMIN_PREFIX && internalMatch(internalUrl.pathname) && !internal;
|
|
5240
|
+
if (internal) {
|
|
5241
|
+
internalUrl.pathname = internalPath;
|
|
5242
|
+
request = new Request(internalUrl, request);
|
|
5243
|
+
}
|
|
5244
|
+
let namespace = internal ? internalUrl.searchParams.get("namespace") ?? control.namespaceOf(request) : control.namespaceOf(request);
|
|
4692
5245
|
if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
4693
5246
|
const credential = options.credential(request);
|
|
4694
5247
|
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
@@ -4708,7 +5261,9 @@ var createRuntime = (options) => {
|
|
|
4708
5261
|
return copy;
|
|
4709
5262
|
}
|
|
4710
5263
|
};
|
|
4711
|
-
|
|
5264
|
+
if (obsoleteInternalPath)
|
|
5265
|
+
return stamp(adminFail(404, "internal transport is only available under adminPrefix"));
|
|
5266
|
+
const handled = internal ? void 0 : await control.handle(request);
|
|
4712
5267
|
if (handled)
|
|
4713
5268
|
return stamp(handled);
|
|
4714
5269
|
const started = monotonicNow();
|
|
@@ -4749,7 +5304,7 @@ var createRuntime = (options) => {
|
|
|
4749
5304
|
type: "mockingbird_admin",
|
|
4750
5305
|
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
4751
5306
|
}
|
|
4752
|
-
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
5307
|
+
}), { status: 400, headers: { "content-type": "application/json; charset=utf-8" } }));
|
|
4753
5308
|
}
|
|
4754
5309
|
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
4755
5310
|
log(400);
|
|
@@ -4816,6 +5371,7 @@ var createRuntime = (options) => {
|
|
|
4816
5371
|
startedAt: wallNow(),
|
|
4817
5372
|
wallNow,
|
|
4818
5373
|
clock,
|
|
5374
|
+
namespaceClock,
|
|
4819
5375
|
faults,
|
|
4820
5376
|
metrics,
|
|
4821
5377
|
journal,
|
|
@@ -4865,15 +5421,16 @@ var createRuntime = (options) => {
|
|
|
4865
5421
|
...options.admin?.(runtime) ?? {},
|
|
4866
5422
|
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
4867
5423
|
// maps `presets`, and a service route must not replace that list with a record.
|
|
4868
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
5424
|
+
...options.presets ? presetRoutes(options.presets, runtime, adminPrefix) : {},
|
|
4869
5425
|
...stateAdminRoutes({
|
|
4870
5426
|
open: stateScope,
|
|
4871
5427
|
afterWrite: (namespace) => {
|
|
4872
5428
|
checkpoint(namespace, "main");
|
|
4873
5429
|
}
|
|
4874
5430
|
}),
|
|
4875
|
-
...adminUiRoutes(options.name, options.adminUi)
|
|
5431
|
+
...adminUiRoutes(options.name, options.adminUi, adminPrefix)
|
|
4876
5432
|
},
|
|
5433
|
+
adminPrefix,
|
|
4877
5434
|
adminKey: options.adminKey
|
|
4878
5435
|
});
|
|
4879
5436
|
return defineMock(runtime);
|
|
@@ -4887,7 +5444,10 @@ var mutableResponse = (response) => {
|
|
|
4887
5444
|
return new Response(response.body, response);
|
|
4888
5445
|
}
|
|
4889
5446
|
};
|
|
4890
|
-
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5447
|
+
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5448
|
+
status,
|
|
5449
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
5450
|
+
});
|
|
4891
5451
|
var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
|
|
4892
5452
|
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4893
5453
|
var credentialRoutes = (registry) => ({
|
|
@@ -4939,7 +5499,7 @@ var credentialRoutes = (registry) => ({
|
|
|
4939
5499
|
return adminJson(200, { status: "ok" });
|
|
4940
5500
|
}
|
|
4941
5501
|
});
|
|
4942
|
-
var presetRoutes = (presets, runtime) => ({
|
|
5502
|
+
var presetRoutes = (presets, runtime, adminPrefix) => ({
|
|
4943
5503
|
"GET /faults/presets": () => {
|
|
4944
5504
|
const body = {
|
|
4945
5505
|
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
@@ -4949,7 +5509,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
4949
5509
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
4950
5510
|
const name = params.name;
|
|
4951
5511
|
if (!presets[name])
|
|
4952
|
-
return adminFail(404, `no fault preset ${name}; GET /
|
|
5512
|
+
return adminFail(404, `no fault preset ${name}; GET ${adminPrefix}/faults/presets`);
|
|
4953
5513
|
const overrides = isObject(body) ? body : {};
|
|
4954
5514
|
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
4955
5515
|
}
|
|
@@ -5126,6 +5686,7 @@ var createRuntime2 = (options = {}) => {
|
|
|
5126
5686
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
5127
5687
|
...options.clock ? { clock: options.clock } : {},
|
|
5128
5688
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
5689
|
+
...options.adminPrefix !== void 0 ? { adminPrefix: options.adminPrefix } : {},
|
|
5129
5690
|
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
5130
5691
|
...options.onLog ? { onLog: options.onLog } : {},
|
|
5131
5692
|
create: ({ sqlite, namespace, clock }) => new LiveKitAPI({
|
|
@@ -5500,6 +6061,9 @@ var LiveKitAPI = class {
|
|
|
5500
6061
|
};
|
|
5501
6062
|
|
|
5502
6063
|
export {
|
|
6064
|
+
createClock,
|
|
6065
|
+
resolveAdminPrefix,
|
|
6066
|
+
parseDuration,
|
|
5503
6067
|
document2 as document,
|
|
5504
6068
|
operationIds,
|
|
5505
6069
|
supportedOperationIds,
|
|
@@ -5508,4 +6072,4 @@ export {
|
|
|
5508
6072
|
LIVEKIT_NAMESPACE,
|
|
5509
6073
|
LiveKitAPI
|
|
5510
6074
|
};
|
|
5511
|
-
//# sourceMappingURL=chunk-
|
|
6075
|
+
//# sourceMappingURL=chunk-QIRIAV7T.js.map
|