@crvouga/mockingbird-service-odx 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 +25 -0
- package/dist/{chunk-IS6GBONJ.js → chunk-K6M4YJNF.js} +822 -258
- package/dist/chunk-K6M4YJNF.js.map +7 -0
- package/dist/chunk-YKFRTJP3.js +850 -0
- package/dist/chunk-YKFRTJP3.js.map +7 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +34 -4
- package/dist/index.js +1 -1
- package/dist/server.d.ts +64 -4
- package/dist/server.js +2 -2
- package/openapi.yaml +953 -0
- package/package.json +29 -4
- package/dist/chunk-B3AFDDEE.js +0 -363
- package/dist/chunk-B3AFDDEE.js.map +0 -7
- package/dist/chunk-IS6GBONJ.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, message2) => json(status, { error: { type: "mockingbird_admin", message: message2 } });
|
|
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)
|
|
@@ -832,7 +908,8 @@ var annotateResponse = (response, extra) => {
|
|
|
832
908
|
var responseNotes = (response) => notes.get(response);
|
|
833
909
|
|
|
834
910
|
// ../core/dist/metrics.js
|
|
835
|
-
var createMetrics = () => {
|
|
911
|
+
var createMetrics = (partition = true) => {
|
|
912
|
+
const namespaces = /* @__PURE__ */ new Map();
|
|
836
913
|
let requests = 0;
|
|
837
914
|
let faults = 0;
|
|
838
915
|
let totalDurationMs = 0;
|
|
@@ -840,6 +917,14 @@ var createMetrics = () => {
|
|
|
840
917
|
const unmatched = /* @__PURE__ */ new Map();
|
|
841
918
|
return {
|
|
842
919
|
record(entry) {
|
|
920
|
+
if (partition) {
|
|
921
|
+
let scoped = namespaces.get(entry.namespace);
|
|
922
|
+
if (!scoped) {
|
|
923
|
+
scoped = createMetrics(false);
|
|
924
|
+
namespaces.set(entry.namespace, scoped);
|
|
925
|
+
}
|
|
926
|
+
scoped.record(entry);
|
|
927
|
+
}
|
|
843
928
|
requests++;
|
|
844
929
|
totalDurationMs += entry.durationMs;
|
|
845
930
|
if (entry.faultId !== void 0)
|
|
@@ -851,7 +936,7 @@ var createMetrics = () => {
|
|
|
851
936
|
unmatched.set(route, (unmatched.get(route) ?? 0) + 1);
|
|
852
937
|
}
|
|
853
938
|
},
|
|
854
|
-
report: () => ({
|
|
939
|
+
report: (namespace) => namespace !== void 0 ? (namespaces.get(namespace) ?? createMetrics(false)).report() : {
|
|
855
940
|
requests,
|
|
856
941
|
byOperation: Object.fromEntries([...byOperation].sort(([a], [b]) => a.localeCompare(b))),
|
|
857
942
|
unmatched: [...unmatched].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([route, count]) => {
|
|
@@ -860,8 +945,13 @@ var createMetrics = () => {
|
|
|
860
945
|
}),
|
|
861
946
|
faults,
|
|
862
947
|
totalDurationMs
|
|
863
|
-
}
|
|
864
|
-
reset() {
|
|
948
|
+
},
|
|
949
|
+
reset(namespace) {
|
|
950
|
+
if (namespace !== void 0) {
|
|
951
|
+
namespaces.delete(namespace);
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
namespaces.clear();
|
|
865
955
|
requests = 0;
|
|
866
956
|
faults = 0;
|
|
867
957
|
totalDurationMs = 0;
|
|
@@ -1474,6 +1564,34 @@ dialog:not([open]) {
|
|
|
1474
1564
|
function isRecord2(value) {
|
|
1475
1565
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1476
1566
|
}
|
|
1567
|
+
function highlightJsonText(text2) {
|
|
1568
|
+
const escapeToken = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
1569
|
+
switch (char) {
|
|
1570
|
+
case "&":
|
|
1571
|
+
return "&";
|
|
1572
|
+
case "<":
|
|
1573
|
+
return "<";
|
|
1574
|
+
case ">":
|
|
1575
|
+
return ">";
|
|
1576
|
+
case '"':
|
|
1577
|
+
return """;
|
|
1578
|
+
default:
|
|
1579
|
+
return "'";
|
|
1580
|
+
}
|
|
1581
|
+
});
|
|
1582
|
+
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;
|
|
1583
|
+
let cursor = 0;
|
|
1584
|
+
let html = "";
|
|
1585
|
+
for (const match of text2.matchAll(token)) {
|
|
1586
|
+
const index = match.index ?? 0;
|
|
1587
|
+
const value = match[0];
|
|
1588
|
+
html += escapeToken(text2.slice(cursor, index));
|
|
1589
|
+
const kind = value.startsWith('"') ? value.trimEnd().endsWith(":") ? "key" : "string" : value === "true" || value === "false" ? "boolean" : value === "null" ? "null" : "number";
|
|
1590
|
+
html += `<span class="json-${kind}">${escapeToken(value)}</span>`;
|
|
1591
|
+
cursor = index + value.length;
|
|
1592
|
+
}
|
|
1593
|
+
return html + escapeToken(text2.slice(cursor));
|
|
1594
|
+
}
|
|
1477
1595
|
function mountAdminBrand(doc, host, service, catalog) {
|
|
1478
1596
|
const clip = (value, max) => {
|
|
1479
1597
|
if (typeof value !== "string")
|
|
@@ -1835,14 +1953,16 @@ function bootAdmin(config) {
|
|
|
1835
1953
|
const state = {
|
|
1836
1954
|
namespace: "default",
|
|
1837
1955
|
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
1838
|
-
view: "overview",
|
|
1956
|
+
view: ["overview", "state", "clock", "faults", "journal", "routes"].includes(location.hash.slice(1)) ? location.hash.slice(1) : "overview",
|
|
1839
1957
|
collection: "",
|
|
1840
1958
|
shape: null,
|
|
1841
1959
|
route: null,
|
|
1960
|
+
routes: [],
|
|
1842
1961
|
editing: null
|
|
1843
1962
|
};
|
|
1844
1963
|
const standard = new Set(config.standardRoutes);
|
|
1845
1964
|
const banner = el("banner", HTMLElement);
|
|
1965
|
+
const bannerMessage = el("banner-message", HTMLElement);
|
|
1846
1966
|
const keyInput = el("admin-key", HTMLInputElement);
|
|
1847
1967
|
keyInput.value = state.key;
|
|
1848
1968
|
const params = new URLSearchParams(location.search);
|
|
@@ -1853,18 +1973,21 @@ function bootAdmin(config) {
|
|
|
1853
1973
|
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
1854
1974
|
params.delete("key");
|
|
1855
1975
|
const next = params.toString();
|
|
1856
|
-
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
1976
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : "") + location.hash);
|
|
1857
1977
|
}
|
|
1858
1978
|
const queryNamespace = params.get("namespace");
|
|
1859
1979
|
if (queryNamespace)
|
|
1860
1980
|
state.namespace = queryNamespace;
|
|
1861
1981
|
const showError = (error) => {
|
|
1862
|
-
|
|
1982
|
+
bannerMessage.textContent = error instanceof Error ? error.message : String(error);
|
|
1863
1983
|
banner.classList.add("show");
|
|
1984
|
+
banner.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
1864
1985
|
};
|
|
1865
1986
|
const clearError = () => {
|
|
1866
1987
|
banner.classList.remove("show");
|
|
1988
|
+
bannerMessage.textContent = "";
|
|
1867
1989
|
};
|
|
1990
|
+
el("banner-dismiss", HTMLButtonElement).addEventListener("click", clearError);
|
|
1868
1991
|
const api = async (method, path, body) => {
|
|
1869
1992
|
const headers = { accept: "application/json" };
|
|
1870
1993
|
if (state.key !== "")
|
|
@@ -1908,16 +2031,21 @@ function bootAdmin(config) {
|
|
|
1908
2031
|
namespace: () => state.namespace
|
|
1909
2032
|
};
|
|
1910
2033
|
const isPanelScript = (value) => typeof value === "function";
|
|
2034
|
+
const themeButton = el("theme", HTMLButtonElement);
|
|
1911
2035
|
const setTheme = (theme) => {
|
|
1912
2036
|
if (theme)
|
|
1913
2037
|
document.documentElement.dataset.theme = theme;
|
|
1914
2038
|
else
|
|
1915
2039
|
delete document.documentElement.dataset.theme;
|
|
2040
|
+
const dark = theme ? theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2041
|
+
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>';
|
|
2042
|
+
const label = dark ? "Use light theme" : "Use dark theme";
|
|
2043
|
+
themeButton.setAttribute("aria-label", label);
|
|
2044
|
+
themeButton.title = label;
|
|
1916
2045
|
};
|
|
1917
2046
|
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
2047
|
+
setTheme(storedTheme === "dark" || storedTheme === "light" ? storedTheme : null);
|
|
2048
|
+
themeButton.addEventListener("click", () => {
|
|
1921
2049
|
const current = document.documentElement.dataset.theme;
|
|
1922
2050
|
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
1923
2051
|
const next = dark ? "light" : "dark";
|
|
@@ -1936,6 +2064,7 @@ function bootAdmin(config) {
|
|
|
1936
2064
|
continue;
|
|
1937
2065
|
section.hidden = section.id !== `view-${view}`;
|
|
1938
2066
|
}
|
|
2067
|
+
history.replaceState(null, "", `${location.pathname}${location.search}#${encodeURIComponent(view)}`);
|
|
1939
2068
|
};
|
|
1940
2069
|
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
1941
2070
|
const target = event.target;
|
|
@@ -1950,7 +2079,7 @@ function bootAdmin(config) {
|
|
|
1950
2079
|
show(view);
|
|
1951
2080
|
refresh().catch(showError);
|
|
1952
2081
|
});
|
|
1953
|
-
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>`;
|
|
2082
|
+
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>`;
|
|
1954
2083
|
const table = (columns, rows) => {
|
|
1955
2084
|
if (rows.length === 0)
|
|
1956
2085
|
return '<p class="empty">Nothing here yet.</p>';
|
|
@@ -1960,16 +2089,22 @@ function bootAdmin(config) {
|
|
|
1960
2089
|
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
1961
2090
|
return `<tr${attrs}>${cells}</tr>`;
|
|
1962
2091
|
}).join("");
|
|
1963
|
-
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
2092
|
+
return `<div class="table-shell"><table><thead>${head}</thead><tbody>${body}</tbody></table></div>`;
|
|
1964
2093
|
};
|
|
1965
2094
|
const preview = (value) => {
|
|
1966
2095
|
const text2 = JSON.stringify(value) ?? "null";
|
|
1967
|
-
|
|
2096
|
+
const clipped = text2.length > 180 ? `${text2.slice(0, 180)}\u2026` : text2;
|
|
2097
|
+
return `<code class="json-inline">${highlightJsonText(clipped)}</code>`;
|
|
1968
2098
|
};
|
|
1969
2099
|
const when = (epoch) => {
|
|
1970
2100
|
const date = new Date(epoch);
|
|
1971
2101
|
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
1972
2102
|
};
|
|
2103
|
+
const journalWhen = (value) => {
|
|
2104
|
+
const numeric = value.trim() === "" ? Number.NaN : Number(value);
|
|
2105
|
+
const epoch = Number.isFinite(numeric) ? numeric : Date.parse(value);
|
|
2106
|
+
return Number.isFinite(epoch) ? when(epoch) : value;
|
|
2107
|
+
};
|
|
1973
2108
|
const clockCard = (epoch) => {
|
|
1974
2109
|
const date = new Date(epoch);
|
|
1975
2110
|
if (Number.isNaN(date.getTime()))
|
|
@@ -2089,7 +2224,7 @@ function bootAdmin(config) {
|
|
|
2089
2224
|
};
|
|
2090
2225
|
const renderOverview = async () => {
|
|
2091
2226
|
const [health, shape, clock, journal] = await Promise.all([
|
|
2092
|
-
originGet(
|
|
2227
|
+
originGet(`${config.adminPrefix}/health`),
|
|
2093
2228
|
api("GET", "/state"),
|
|
2094
2229
|
api("GET", "/clock"),
|
|
2095
2230
|
api("GET", "/requests?limit=8")
|
|
@@ -2106,7 +2241,7 @@ function bootAdmin(config) {
|
|
|
2106
2241
|
},
|
|
2107
2242
|
{
|
|
2108
2243
|
k: "Status",
|
|
2109
|
-
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
2244
|
+
v: `<span class="badge ok">${esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")}</span>`
|
|
2110
2245
|
},
|
|
2111
2246
|
{ k: "Namespace", v: esc(state.namespace) },
|
|
2112
2247
|
{ k: "Collections", v: String(decoded.collections.length) },
|
|
@@ -2193,26 +2328,46 @@ function bootAdmin(config) {
|
|
|
2193
2328
|
const renderJournal = async () => {
|
|
2194
2329
|
const journal = await api("GET", "/requests?limit=100");
|
|
2195
2330
|
el("journal", HTMLElement).innerHTML = table([
|
|
2196
|
-
{ label: "When", cell: (row) => esc(row.at) },
|
|
2197
|
-
{
|
|
2331
|
+
{ label: "When", cell: (row) => esc(journalWhen(row.at)) },
|
|
2332
|
+
{
|
|
2333
|
+
label: "Method",
|
|
2334
|
+
cell: (row) => `<span class="method">${esc(row.method)}</span>`
|
|
2335
|
+
},
|
|
2198
2336
|
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2199
|
-
{
|
|
2337
|
+
{
|
|
2338
|
+
label: "Status",
|
|
2339
|
+
cell: (row) => `<span class="badge ${row.status.startsWith("2") ? "ok" : row.status.startsWith("4") || row.status.startsWith("5") ? "warn" : ""}">${esc(row.status)}</span>`
|
|
2340
|
+
},
|
|
2200
2341
|
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
2201
2342
|
], decodeJournal(journal));
|
|
2202
2343
|
};
|
|
2203
|
-
const
|
|
2204
|
-
const
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2344
|
+
const paintRoutes = () => {
|
|
2345
|
+
const query = el("route-search", HTMLInputElement).value.trim().toLocaleLowerCase();
|
|
2346
|
+
const routes = state.routes.filter((route) => route.toLocaleLowerCase().includes(query));
|
|
2347
|
+
el("route-count", HTMLElement).textContent = `${routes.length} of ${state.routes.length}`;
|
|
2348
|
+
if (state.routes.length === 0) {
|
|
2349
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No admin routes are available.</p>';
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2352
|
+
if (routes.length === 0) {
|
|
2353
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No routes match this filter.</p>';
|
|
2354
|
+
return;
|
|
2355
|
+
}
|
|
2208
2356
|
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2209
2357
|
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2210
2358
|
const parts = route.split(" ");
|
|
2211
2359
|
const method = parts[0] ?? "";
|
|
2212
2360
|
const path = parts.slice(1).join(" ");
|
|
2213
|
-
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>`;
|
|
2361
|
+
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>`;
|
|
2214
2362
|
}).join("");
|
|
2215
2363
|
};
|
|
2364
|
+
const renderRoutes = async () => {
|
|
2365
|
+
const data = await api("GET", "/");
|
|
2366
|
+
if (!isRecord2(data))
|
|
2367
|
+
throw new Error("Route list was not an object");
|
|
2368
|
+
state.routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
2369
|
+
paintRoutes();
|
|
2370
|
+
};
|
|
2216
2371
|
const renderers = {
|
|
2217
2372
|
overview: renderOverview,
|
|
2218
2373
|
state: renderState,
|
|
@@ -2221,19 +2376,50 @@ function bootAdmin(config) {
|
|
|
2221
2376
|
journal: renderJournal,
|
|
2222
2377
|
routes: renderRoutes
|
|
2223
2378
|
};
|
|
2379
|
+
let refreshToken = 0;
|
|
2224
2380
|
async function refresh() {
|
|
2381
|
+
const token = ++refreshToken;
|
|
2225
2382
|
clearError();
|
|
2383
|
+
const main = el("views", HTMLElement);
|
|
2384
|
+
const sync = el("sync-status", HTMLElement);
|
|
2385
|
+
main.setAttribute("aria-busy", "true");
|
|
2386
|
+
sync.classList.add("busy");
|
|
2387
|
+
sync.textContent = "Updating";
|
|
2226
2388
|
const render = renderers[state.view];
|
|
2227
|
-
|
|
2228
|
-
|
|
2389
|
+
try {
|
|
2390
|
+
if (render)
|
|
2391
|
+
await render();
|
|
2392
|
+
if (token === refreshToken)
|
|
2393
|
+
sync.textContent = "Up to date";
|
|
2394
|
+
} finally {
|
|
2395
|
+
if (token === refreshToken) {
|
|
2396
|
+
main.removeAttribute("aria-busy");
|
|
2397
|
+
sync.classList.remove("busy");
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2229
2400
|
}
|
|
2230
2401
|
const delay = (ms) => new Promise((resolve) => {
|
|
2231
2402
|
window.setTimeout(resolve, ms);
|
|
2232
2403
|
});
|
|
2233
|
-
const confirmAction = async (button, pending, done, work) => {
|
|
2404
|
+
const confirmAction = async (button, pending, done, work, confirmLabel) => {
|
|
2234
2405
|
if (button.dataset.pending === "1")
|
|
2235
2406
|
return;
|
|
2236
2407
|
const label = button.textContent ?? "";
|
|
2408
|
+
if (confirmLabel !== void 0 && button.dataset.confirming !== "1") {
|
|
2409
|
+
button.dataset.confirming = "1";
|
|
2410
|
+
button.classList.add("confirming");
|
|
2411
|
+
button.textContent = confirmLabel;
|
|
2412
|
+
window.setTimeout(() => {
|
|
2413
|
+
if (button.dataset.confirming !== "1")
|
|
2414
|
+
return;
|
|
2415
|
+
button.textContent = label;
|
|
2416
|
+
button.classList.remove("confirming");
|
|
2417
|
+
delete button.dataset.confirming;
|
|
2418
|
+
}, 3e3);
|
|
2419
|
+
return;
|
|
2420
|
+
}
|
|
2421
|
+
button.classList.remove("confirming");
|
|
2422
|
+
delete button.dataset.confirming;
|
|
2237
2423
|
const width = button.offsetWidth;
|
|
2238
2424
|
button.dataset.pending = "1";
|
|
2239
2425
|
button.classList.add("pressed");
|
|
@@ -2254,6 +2440,65 @@ function bootAdmin(config) {
|
|
|
2254
2440
|
delete button.dataset.pending;
|
|
2255
2441
|
}
|
|
2256
2442
|
};
|
|
2443
|
+
const wireJsonEditor = (textareaId, highlightId, statusId, shellId, allowEmpty) => {
|
|
2444
|
+
const textarea = el(textareaId, HTMLTextAreaElement);
|
|
2445
|
+
const highlight = el(highlightId, HTMLElement);
|
|
2446
|
+
const status = el(statusId, HTMLElement);
|
|
2447
|
+
const shell = el(shellId, HTMLElement);
|
|
2448
|
+
const sync = () => {
|
|
2449
|
+
const text2 = textarea.value;
|
|
2450
|
+
highlight.innerHTML = `${highlightJsonText(text2)}
|
|
2451
|
+
`;
|
|
2452
|
+
let valid = true;
|
|
2453
|
+
let message2 = "Valid JSON";
|
|
2454
|
+
if (allowEmpty && text2.trim() === "")
|
|
2455
|
+
message2 = "No body";
|
|
2456
|
+
else {
|
|
2457
|
+
try {
|
|
2458
|
+
readJson2(text2);
|
|
2459
|
+
} catch {
|
|
2460
|
+
valid = false;
|
|
2461
|
+
message2 = "Invalid JSON";
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
shell.classList.toggle("invalid", !valid);
|
|
2465
|
+
status.classList.toggle("invalid", !valid);
|
|
2466
|
+
status.textContent = message2;
|
|
2467
|
+
textarea.setAttribute("aria-invalid", valid ? "false" : "true");
|
|
2468
|
+
};
|
|
2469
|
+
const syncScroll = () => {
|
|
2470
|
+
const preview2 = highlight.parentElement;
|
|
2471
|
+
if (!(preview2 instanceof HTMLElement))
|
|
2472
|
+
return;
|
|
2473
|
+
preview2.scrollTop = textarea.scrollTop;
|
|
2474
|
+
preview2.scrollLeft = textarea.scrollLeft;
|
|
2475
|
+
};
|
|
2476
|
+
textarea.addEventListener("input", sync);
|
|
2477
|
+
textarea.addEventListener("scroll", syncScroll);
|
|
2478
|
+
sync();
|
|
2479
|
+
return sync;
|
|
2480
|
+
};
|
|
2481
|
+
const syncRouteEditor = wireJsonEditor("route-body", "route-highlight", "route-json-status", "route-editor", true);
|
|
2482
|
+
const syncRecordEditor = wireJsonEditor("editor-value", "editor-highlight", "editor-json-status", "record-editor", false);
|
|
2483
|
+
for (const button of document.querySelectorAll("[data-format]")) {
|
|
2484
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2485
|
+
continue;
|
|
2486
|
+
button.addEventListener("click", () => {
|
|
2487
|
+
const target = button.dataset.format;
|
|
2488
|
+
if (target === void 0)
|
|
2489
|
+
return;
|
|
2490
|
+
const textarea = document.getElementById(target);
|
|
2491
|
+
if (!(textarea instanceof HTMLTextAreaElement) || textarea.value.trim() === "")
|
|
2492
|
+
return;
|
|
2493
|
+
try {
|
|
2494
|
+
textarea.value = JSON.stringify(readJson2(textarea.value), null, 2) ?? "";
|
|
2495
|
+
textarea.dispatchEvent(new Event("input"));
|
|
2496
|
+
textarea.focus();
|
|
2497
|
+
} catch {
|
|
2498
|
+
showError(new Error("Fix the JSON before formatting it"));
|
|
2499
|
+
}
|
|
2500
|
+
});
|
|
2501
|
+
}
|
|
2257
2502
|
document.addEventListener("click", (event) => {
|
|
2258
2503
|
const target = event.target;
|
|
2259
2504
|
if (!(target instanceof Element))
|
|
@@ -2326,6 +2571,7 @@ function bootAdmin(config) {
|
|
|
2326
2571
|
} else {
|
|
2327
2572
|
editorValue.value = "{\n \n}";
|
|
2328
2573
|
}
|
|
2574
|
+
syncRecordEditor();
|
|
2329
2575
|
editor.showModal();
|
|
2330
2576
|
}
|
|
2331
2577
|
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -2355,10 +2601,10 @@ function bootAdmin(config) {
|
|
|
2355
2601
|
const id = state.editing;
|
|
2356
2602
|
if (!id)
|
|
2357
2603
|
return;
|
|
2358
|
-
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2604
|
+
confirmAction(editorDelete, "Deleting...", "Deleted", () => api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2359
2605
|
editor.close();
|
|
2360
2606
|
return renderState();
|
|
2361
|
-
}).catch(showError);
|
|
2607
|
+
}), "Confirm delete").catch(showError);
|
|
2362
2608
|
});
|
|
2363
2609
|
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2364
2610
|
if (!(button instanceof HTMLButtonElement))
|
|
@@ -2404,10 +2650,10 @@ function bootAdmin(config) {
|
|
|
2404
2650
|
}).then(() => renderFaults()).catch(showError);
|
|
2405
2651
|
});
|
|
2406
2652
|
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2407
|
-
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
|
|
2653
|
+
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults()), "Confirm clear").catch(showError);
|
|
2408
2654
|
});
|
|
2409
2655
|
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2410
|
-
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
|
|
2656
|
+
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal()), "Confirm clear").catch(showError);
|
|
2411
2657
|
});
|
|
2412
2658
|
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2413
2659
|
const target = event.target;
|
|
@@ -2421,9 +2667,15 @@ function bootAdmin(config) {
|
|
|
2421
2667
|
return;
|
|
2422
2668
|
state.route = route;
|
|
2423
2669
|
el("route-title", HTMLElement).textContent = route;
|
|
2670
|
+
el("route-send", HTMLButtonElement).disabled = false;
|
|
2424
2671
|
const method = route.split(" ")[0] ?? "";
|
|
2425
|
-
el("route-body", HTMLTextAreaElement)
|
|
2672
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
2673
|
+
routeBody.disabled = method === "GET" || method === "DELETE";
|
|
2674
|
+
el("route-editor", HTMLElement).hidden = routeBody.disabled;
|
|
2675
|
+
el("route-json-status", HTMLElement).parentElement?.toggleAttribute("hidden", routeBody.disabled);
|
|
2676
|
+
syncRouteEditor();
|
|
2426
2677
|
});
|
|
2678
|
+
el("route-search", HTMLInputElement).addEventListener("input", paintRoutes);
|
|
2427
2679
|
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2428
2680
|
event.preventDefault();
|
|
2429
2681
|
const send = async () => {
|
|
@@ -2445,7 +2697,8 @@ function bootAdmin(config) {
|
|
|
2445
2697
|
}
|
|
2446
2698
|
}
|
|
2447
2699
|
const data = await api(method, concrete, body);
|
|
2448
|
-
|
|
2700
|
+
const text2 = JSON.stringify(data, null, 2) ?? "null";
|
|
2701
|
+
el("route-result", HTMLElement).innerHTML = `<code>${highlightJsonText(text2)}</code>`;
|
|
2449
2702
|
};
|
|
2450
2703
|
send().catch(showError);
|
|
2451
2704
|
});
|
|
@@ -2563,11 +2816,13 @@ function bootAdmin(config) {
|
|
|
2563
2816
|
views.append(section);
|
|
2564
2817
|
mountSqlExplorer(body, panelApi);
|
|
2565
2818
|
}
|
|
2819
|
+
show(document.getElementById(`view-${state.view}`) === null ? "overview" : state.view);
|
|
2566
2820
|
return refresh();
|
|
2567
2821
|
}).catch(showError);
|
|
2568
2822
|
}
|
|
2569
2823
|
var clientPrelude = [
|
|
2570
2824
|
isRecord2,
|
|
2825
|
+
highlightJsonText,
|
|
2571
2826
|
mountAdminBrand,
|
|
2572
2827
|
startAdminBrand,
|
|
2573
2828
|
faultPresetList,
|
|
@@ -2590,6 +2845,7 @@ ${entry}(${payload});
|
|
|
2590
2845
|
var defineMock = (runtime) => runtime;
|
|
2591
2846
|
var STANDARD_ADMIN_ROUTES = [
|
|
2592
2847
|
"GET /",
|
|
2848
|
+
"GET /health",
|
|
2593
2849
|
"POST /reset",
|
|
2594
2850
|
"GET /namespaces",
|
|
2595
2851
|
"GET /clock",
|
|
@@ -2669,11 +2925,11 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
2669
2925
|
}
|
|
2670
2926
|
});
|
|
2671
2927
|
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
2672
|
-
var renderAdminDocument = (service) => {
|
|
2928
|
+
var renderAdminDocument = (service, adminPrefix = ADMIN_PREFIX) => {
|
|
2673
2929
|
const name = escapeHtml(service);
|
|
2674
2930
|
const script = adminClientSource({
|
|
2675
2931
|
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
2676
|
-
adminPrefix
|
|
2932
|
+
adminPrefix,
|
|
2677
2933
|
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
2678
2934
|
brandsUrl: ADMIN_BRANDS_URL,
|
|
2679
2935
|
service
|
|
@@ -2690,75 +2946,99 @@ var renderAdminDocument = (service) => {
|
|
|
2690
2946
|
${CSS_RESET}
|
|
2691
2947
|
:root {
|
|
2692
2948
|
color-scheme: light;
|
|
2693
|
-
--bg: #
|
|
2949
|
+
--bg: #f7f8fb;
|
|
2694
2950
|
--bg-elev: #ffffff;
|
|
2695
|
-
--bg-muted: #
|
|
2696
|
-
--bg-inset: #
|
|
2697
|
-
--line: #
|
|
2698
|
-
--line-strong: #
|
|
2699
|
-
--ink: #
|
|
2700
|
-
--muted: #
|
|
2701
|
-
--faint: #
|
|
2702
|
-
--accent: #
|
|
2951
|
+
--bg-muted: #f1f3f7;
|
|
2952
|
+
--bg-inset: #f8f9fc;
|
|
2953
|
+
--line: #e4e7ec;
|
|
2954
|
+
--line-strong: #cfd4dc;
|
|
2955
|
+
--ink: #101828;
|
|
2956
|
+
--muted: #475467;
|
|
2957
|
+
--faint: #667085;
|
|
2958
|
+
--accent: #6941c6;
|
|
2959
|
+
--accent-hover: #5933b4;
|
|
2703
2960
|
--accent-ink: #ffffff;
|
|
2704
|
-
--accent-soft: #
|
|
2705
|
-
--ok: #
|
|
2706
|
-
--ok-soft: #
|
|
2707
|
-
--warn: #
|
|
2708
|
-
--warn-soft: #
|
|
2709
|
-
--err: #
|
|
2710
|
-
--err-soft: #
|
|
2711
|
-
--
|
|
2961
|
+
--accent-soft: #f4f0ff;
|
|
2962
|
+
--ok: #067647;
|
|
2963
|
+
--ok-soft: #ecfdf3;
|
|
2964
|
+
--warn: #b54708;
|
|
2965
|
+
--warn-soft: #fffaeb;
|
|
2966
|
+
--err: #b42318;
|
|
2967
|
+
--err-soft: #fef3f2;
|
|
2968
|
+
--code-key: #6941c6;
|
|
2969
|
+
--code-string: #067647;
|
|
2970
|
+
--code-number: #175cd3;
|
|
2971
|
+
--code-boolean: #c11574;
|
|
2972
|
+
--code-null: #667085;
|
|
2973
|
+
--shadow-xs: 0 1px 2px rgb(16 24 40 / 0.05);
|
|
2974
|
+
--shadow-sm: 0 1px 3px rgb(16 24 40 / 0.08), 0 1px 2px rgb(16 24 40 / 0.04);
|
|
2975
|
+
--shadow-lg: 0 18px 48px rgb(16 24 40 / 0.16), 0 4px 12px rgb(16 24 40 / 0.08);
|
|
2712
2976
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2713
2977
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
2714
|
-
--radius:
|
|
2715
|
-
--header:
|
|
2716
|
-
--nav:
|
|
2978
|
+
--radius: 12px;
|
|
2979
|
+
--header: 72px;
|
|
2980
|
+
--nav: 224px;
|
|
2717
2981
|
}
|
|
2718
2982
|
:root[data-theme="dark"] {
|
|
2719
2983
|
color-scheme: dark;
|
|
2720
|
-
--bg: #
|
|
2721
|
-
--bg-elev: #
|
|
2722
|
-
--bg-muted: #
|
|
2723
|
-
--bg-inset: #
|
|
2724
|
-
--line: #
|
|
2725
|
-
--line-strong: #
|
|
2726
|
-
--ink: #
|
|
2727
|
-
--muted: #
|
|
2728
|
-
--faint: #
|
|
2729
|
-
--accent: #
|
|
2730
|
-
--accent-
|
|
2731
|
-
--accent-
|
|
2732
|
-
--
|
|
2733
|
-
--ok
|
|
2734
|
-
--
|
|
2735
|
-
--warn
|
|
2736
|
-
--
|
|
2737
|
-
--err
|
|
2738
|
-
--
|
|
2984
|
+
--bg: #0c0e14;
|
|
2985
|
+
--bg-elev: #151821;
|
|
2986
|
+
--bg-muted: #20232e;
|
|
2987
|
+
--bg-inset: #11141c;
|
|
2988
|
+
--line: #2b303d;
|
|
2989
|
+
--line-strong: #414858;
|
|
2990
|
+
--ink: #f5f7fa;
|
|
2991
|
+
--muted: #b6bdc9;
|
|
2992
|
+
--faint: #8d96a7;
|
|
2993
|
+
--accent: #9e77ed;
|
|
2994
|
+
--accent-hover: #b692f6;
|
|
2995
|
+
--accent-ink: #ffffff;
|
|
2996
|
+
--accent-soft: #2b2142;
|
|
2997
|
+
--ok: #75e0a7;
|
|
2998
|
+
--ok-soft: #102a20;
|
|
2999
|
+
--warn: #fec84b;
|
|
3000
|
+
--warn-soft: #302611;
|
|
3001
|
+
--err: #fda29b;
|
|
3002
|
+
--err-soft: #321817;
|
|
3003
|
+
--code-key: #c3a6ff;
|
|
3004
|
+
--code-string: #75e0a7;
|
|
3005
|
+
--code-number: #84adff;
|
|
3006
|
+
--code-boolean: #f9a8d4;
|
|
3007
|
+
--code-null: #98a2b3;
|
|
3008
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3009
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3010
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2739
3011
|
}
|
|
2740
3012
|
@media (prefers-color-scheme: dark) {
|
|
2741
3013
|
:root:not([data-theme="light"]) {
|
|
2742
3014
|
color-scheme: dark;
|
|
2743
|
-
--bg: #
|
|
2744
|
-
--bg-elev: #
|
|
2745
|
-
--bg-muted: #
|
|
2746
|
-
--bg-inset: #
|
|
2747
|
-
--line: #
|
|
2748
|
-
--line-strong: #
|
|
2749
|
-
--ink: #
|
|
2750
|
-
--muted: #
|
|
2751
|
-
--faint: #
|
|
2752
|
-
--accent: #
|
|
2753
|
-
--accent-
|
|
2754
|
-
--accent-
|
|
2755
|
-
--
|
|
2756
|
-
--ok
|
|
2757
|
-
--
|
|
2758
|
-
--warn
|
|
2759
|
-
--
|
|
2760
|
-
--err
|
|
2761
|
-
--
|
|
3015
|
+
--bg: #0c0e14;
|
|
3016
|
+
--bg-elev: #151821;
|
|
3017
|
+
--bg-muted: #20232e;
|
|
3018
|
+
--bg-inset: #11141c;
|
|
3019
|
+
--line: #2b303d;
|
|
3020
|
+
--line-strong: #414858;
|
|
3021
|
+
--ink: #f5f7fa;
|
|
3022
|
+
--muted: #b6bdc9;
|
|
3023
|
+
--faint: #8d96a7;
|
|
3024
|
+
--accent: #9e77ed;
|
|
3025
|
+
--accent-hover: #b692f6;
|
|
3026
|
+
--accent-ink: #ffffff;
|
|
3027
|
+
--accent-soft: #2b2142;
|
|
3028
|
+
--ok: #75e0a7;
|
|
3029
|
+
--ok-soft: #102a20;
|
|
3030
|
+
--warn: #fec84b;
|
|
3031
|
+
--warn-soft: #302611;
|
|
3032
|
+
--err: #fda29b;
|
|
3033
|
+
--err-soft: #321817;
|
|
3034
|
+
--code-key: #c3a6ff;
|
|
3035
|
+
--code-string: #75e0a7;
|
|
3036
|
+
--code-number: #84adff;
|
|
3037
|
+
--code-boolean: #f9a8d4;
|
|
3038
|
+
--code-null: #98a2b3;
|
|
3039
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3040
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3041
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2762
3042
|
}
|
|
2763
3043
|
}
|
|
2764
3044
|
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
@@ -2776,13 +3056,17 @@ var renderAdminDocument = (service) => {
|
|
|
2776
3056
|
padding-right: env(safe-area-inset-right);
|
|
2777
3057
|
-webkit-text-size-adjust: 100%;
|
|
2778
3058
|
}
|
|
2779
|
-
|
|
3059
|
+
::selection { background: color-mix(in srgb, var(--accent) 28%, transparent); }
|
|
3060
|
+
:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px; }
|
|
3061
|
+
button, input, select, textarea { transition: border-color 140ms ease, box-shadow 140ms ease, transform 100ms ease; }
|
|
3062
|
+
button:disabled, input:disabled, select:disabled, textarea:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
2780
3063
|
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
2781
3064
|
header.top {
|
|
2782
|
-
position: sticky; top: 0; z-index:
|
|
3065
|
+
position: sticky; top: 0; z-index: 20;
|
|
2783
3066
|
display: flex; align-items: center; gap: 12px;
|
|
2784
|
-
min-height: var(--header); padding:
|
|
2785
|
-
background: var(--bg-elev);
|
|
3067
|
+
min-height: var(--header); padding: 12px 20px;
|
|
3068
|
+
background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
|
|
3069
|
+
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
2786
3070
|
border-bottom: 1px solid var(--line);
|
|
2787
3071
|
min-width: 0; max-width: 100%;
|
|
2788
3072
|
}
|
|
@@ -2790,12 +3074,21 @@ var renderAdminDocument = (service) => {
|
|
|
2790
3074
|
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
2791
3075
|
min-width: 0; max-width: 100%; margin-right: auto;
|
|
2792
3076
|
}
|
|
2793
|
-
.brand-
|
|
2794
|
-
.brand-
|
|
3077
|
+
.brand-lockup { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
3078
|
+
.brand-mark {
|
|
3079
|
+
width: 38px; height: 38px; display: grid; place-items: center; flex: none;
|
|
3080
|
+
border-radius: 11px; color: #fff;
|
|
3081
|
+
background: linear-gradient(145deg, #7f56d9 0%, #53389e 100%);
|
|
3082
|
+
box-shadow: 0 6px 16px rgb(105 65 198 / 0.24), inset 0 1px 0 rgb(255 255 255 / 0.25);
|
|
3083
|
+
}
|
|
3084
|
+
.brand-mark svg { width: 22px; height: 22px; }
|
|
3085
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; line-height: 1.25; }
|
|
3086
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
2795
3087
|
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
2796
3088
|
.vendor {
|
|
2797
3089
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
2798
3090
|
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
3091
|
+
padding: 5px 8px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-inset);
|
|
2799
3092
|
}
|
|
2800
3093
|
.vendor[hidden] { display: none; }
|
|
2801
3094
|
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
@@ -2807,12 +3100,17 @@ var renderAdminDocument = (service) => {
|
|
|
2807
3100
|
.vendor-sep { color: var(--faint); }
|
|
2808
3101
|
.vendor-link { color: inherit; text-decoration: none; }
|
|
2809
3102
|
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
2810
|
-
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items:
|
|
2811
|
-
|
|
3103
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
|
|
3104
|
+
.sync-status { align-self: center; display: flex; align-items: center; gap: 6px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3105
|
+
.sync-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
|
|
3106
|
+
.sync-status.busy::before { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); animation: pulse 1.2s ease-in-out infinite; }
|
|
3107
|
+
label.field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; font-weight: 600; color: var(--faint); }
|
|
2812
3108
|
label.field input, label.field select, .controls input[type="password"] {
|
|
2813
|
-
background-color: var(--bg-
|
|
2814
|
-
padding:
|
|
3109
|
+
background-color: var(--bg-elev); border: 1px solid var(--line-strong); border-radius: 9px;
|
|
3110
|
+
padding: 8px 10px; min-height: 38px; line-height: 1.2; box-shadow: var(--shadow-xs);
|
|
2815
3111
|
}
|
|
3112
|
+
label.field input:hover, label.field select:hover, textarea:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--line-strong)); }
|
|
3113
|
+
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; }
|
|
2816
3114
|
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
2817
3115
|
the text fields in the same form. Paint it like those fields. */
|
|
2818
3116
|
label.field select {
|
|
@@ -2829,12 +3127,20 @@ var renderAdminDocument = (service) => {
|
|
|
2829
3127
|
dialog .field > textarea { width: 100%; }
|
|
2830
3128
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
2831
3129
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
.btn
|
|
3130
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
|
3131
|
+
border-radius: 9px; min-height: 38px; padding: 0 13px; border: 1px solid var(--line-strong);
|
|
3132
|
+
background: var(--bg-elev); color: var(--ink); font-size: 13px; font-weight: 600; line-height: 1;
|
|
3133
|
+
box-shadow: var(--shadow-xs);
|
|
3134
|
+
}
|
|
3135
|
+
.icon-btn { width: 38px; padding: 0; }
|
|
3136
|
+
.icon-btn svg, .btn svg, .btn-primary svg { width: 16px; height: 16px; }
|
|
3137
|
+
.icon-btn:hover, .btn:hover { background: var(--bg-muted); border-color: var(--line-strong); }
|
|
3138
|
+
.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; }
|
|
3139
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
3140
|
+
.btn-quiet { background: transparent; border-color: transparent; box-shadow: none; }
|
|
3141
|
+
.btn-quiet:hover { background: var(--bg-muted); }
|
|
3142
|
+
.btn-danger { color: var(--err); border-color: color-mix(in srgb, var(--err) 25%, var(--line)); }
|
|
3143
|
+
.btn-danger:hover, .btn-danger.confirming { background: var(--err-soft); border-color: color-mix(in srgb, var(--err) 45%, var(--line)); }
|
|
2838
3144
|
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
2839
3145
|
so the press itself has to stay visible. */
|
|
2840
3146
|
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
@@ -2850,93 +3156,159 @@ var renderAdminDocument = (service) => {
|
|
|
2850
3156
|
flex: 1 0 auto; min-width: 0;
|
|
2851
3157
|
}
|
|
2852
3158
|
nav.side {
|
|
2853
|
-
|
|
3159
|
+
position: sticky; top: var(--header); align-self: start; height: calc(100vh - var(--header));
|
|
3160
|
+
border-right: 1px solid var(--line); padding: 18px 12px;
|
|
2854
3161
|
display: flex; flex-direction: column; gap: 4px;
|
|
2855
|
-
background: var(--bg-elev);
|
|
3162
|
+
background: var(--bg-elev); overflow-y: auto;
|
|
2856
3163
|
}
|
|
3164
|
+
.nav-label { padding: 0 10px 8px; color: var(--faint); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
|
|
2857
3165
|
nav.side button {
|
|
3166
|
+
position: relative; display: flex; align-items: center; gap: 10px; width: 100%;
|
|
2858
3167
|
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
2859
|
-
padding: 9px 10px; min-height:
|
|
2860
|
-
}
|
|
2861
|
-
nav.side button
|
|
2862
|
-
|
|
2863
|
-
.
|
|
2864
|
-
.
|
|
2865
|
-
.
|
|
3168
|
+
padding: 9px 10px; min-height: 42px; color: var(--muted); font-size: 13px; font-weight: 550;
|
|
3169
|
+
}
|
|
3170
|
+
nav.side button:hover { background: var(--bg-muted); color: var(--ink); }
|
|
3171
|
+
nav.side button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 650; }
|
|
3172
|
+
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); }
|
|
3173
|
+
.nav-icon { width: 18px; height: 18px; display: grid; place-items: center; flex: none; }
|
|
3174
|
+
.nav-icon svg { width: 18px; height: 18px; }
|
|
3175
|
+
main { padding: 32px clamp(20px, 4vw, 48px) 64px; max-width: 1320px; width: 100%; min-width: 0; }
|
|
3176
|
+
main[aria-busy="true"] .view { opacity: 0.62; }
|
|
3177
|
+
.view { animation: view-in 180ms ease-out; }
|
|
3178
|
+
.view[hidden] { display: none; }
|
|
3179
|
+
.view-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
|
|
3180
|
+
.view h2 { margin: 0 0 5px; font-size: clamp(24px, 3vw, 30px); line-height: 1.2; letter-spacing: -0.035em; }
|
|
3181
|
+
.view h3 { margin: 24px 0 10px; font-size: 15px; letter-spacing: -0.01em; }
|
|
3182
|
+
.lede { margin: 0; color: var(--muted); max-width: 72ch; font-size: 14px; }
|
|
3183
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 175px), 1fr)); gap: 14px; }
|
|
2866
3184
|
.card {
|
|
2867
3185
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
2868
|
-
padding:
|
|
3186
|
+
padding: 18px; box-shadow: var(--shadow-sm);
|
|
2869
3187
|
}
|
|
2870
|
-
.card
|
|
2871
|
-
.card
|
|
3188
|
+
.stat-card { position: relative; overflow: hidden; }
|
|
3189
|
+
.stat-card::after { content: ""; position: absolute; inset: 0 0 auto; height: 2px; background: linear-gradient(90deg, var(--accent), transparent 78%); opacity: 0.7; }
|
|
3190
|
+
.card .k { color: var(--faint); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
3191
|
+
.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; }
|
|
2872
3192
|
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
2873
|
-
.
|
|
3193
|
+
.section-card { background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-xs); overflow: hidden; }
|
|
3194
|
+
.section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
|
|
3195
|
+
.section-title h3 { margin: 0; }
|
|
3196
|
+
.split { display: grid; grid-template-columns: minmax(0, 250px) minmax(0, 1fr); gap: 18px; align-items: start; }
|
|
3197
|
+
#collections { padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); box-shadow: var(--shadow-xs); }
|
|
2874
3198
|
.collection {
|
|
2875
3199
|
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
2876
|
-
border-radius:
|
|
3200
|
+
border-radius: 9px; padding: 10px 11px; min-height: 48px; color: var(--ink); cursor: pointer;
|
|
2877
3201
|
}
|
|
2878
|
-
.collection
|
|
3202
|
+
.collection:hover { background: var(--bg-muted); }
|
|
3203
|
+
.collection[aria-current="true"] { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 18%, var(--line)); color: var(--accent); }
|
|
2879
3204
|
.collection small { display: block; color: var(--faint); }
|
|
2880
3205
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
2881
|
-
.chip {
|
|
2882
|
-
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
2883
|
-
border-radius: 999px; padding: 4px 8px;
|
|
2884
|
-
}
|
|
3206
|
+
.chip { font-family: var(--mono); font-size: 11px; background: var(--bg-muted); border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; }
|
|
2885
3207
|
.chip i { color: var(--faint); font-style: normal; }
|
|
2886
3208
|
.sql-layout {
|
|
2887
3209
|
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
2888
3210
|
}
|
|
2889
|
-
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
2890
|
-
.sql-tables button { text-align: left; }
|
|
3211
|
+
.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); }
|
|
3212
|
+
.sql-tables button { text-align: left; border: 0; border-radius: 8px; padding: 9px 10px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
3213
|
+
.sql-tables button:hover, .sql-tables button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); }
|
|
2891
3214
|
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
2892
|
-
table { width: 100%; border-
|
|
2893
|
-
|
|
2894
|
-
th {
|
|
3215
|
+
.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); }
|
|
3216
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
3217
|
+
th, td { text-align: left; padding: 11px 13px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
3218
|
+
th { color: var(--faint); background: var(--bg-inset); font-weight: 650; font-size: 11px; text-transform: uppercase; letter-spacing: 0.045em; }
|
|
3219
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
2895
3220
|
tr.clickable { cursor: pointer; }
|
|
2896
|
-
tr.clickable:hover { background: var(--
|
|
3221
|
+
tr.clickable:hover { background: var(--accent-soft); }
|
|
2897
3222
|
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
2898
3223
|
pre {
|
|
2899
3224
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
2900
|
-
background: var(--bg-inset); border-radius: 10px; padding:
|
|
2901
|
-
}
|
|
2902
|
-
.
|
|
3225
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 10px; padding: 14px; font-size: 12px; line-height: 1.65;
|
|
3226
|
+
}
|
|
3227
|
+
.json-key { color: var(--code-key); }
|
|
3228
|
+
.json-string { color: var(--code-string); }
|
|
3229
|
+
.json-number { color: var(--code-number); }
|
|
3230
|
+
.json-boolean { color: var(--code-boolean); }
|
|
3231
|
+
.json-null { color: var(--code-null); font-style: italic; }
|
|
3232
|
+
.json-inline { display: block; max-width: 44rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
3233
|
+
.json-view { position: relative; max-height: 26rem; overflow: auto; }
|
|
3234
|
+
.json-editor { position: relative; min-height: 190px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--bg-inset); overflow: hidden; }
|
|
3235
|
+
.json-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); }
|
|
3236
|
+
.json-editor.invalid { border-color: var(--err); }
|
|
3237
|
+
.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; }
|
|
3238
|
+
.json-editor pre { position: absolute; inset: 0; pointer-events: none; background: transparent; }
|
|
3239
|
+
.json-editor textarea { position: relative; z-index: 1; resize: vertical; background: transparent; color: transparent; caret-color: var(--ink); -webkit-text-fill-color: transparent; }
|
|
3240
|
+
.json-editor textarea::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); }
|
|
3241
|
+
.editor-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; }
|
|
3242
|
+
.json-status { display: inline-flex; align-items: center; gap: 6px; color: var(--ok); font-size: 11px; }
|
|
3243
|
+
.json-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
3244
|
+
.json-status.invalid { color: var(--err); }
|
|
3245
|
+
.format-json { border: 0; padding: 3px 5px; background: transparent; color: var(--accent); font-size: 11px; font-weight: 650; cursor: pointer; }
|
|
3246
|
+
.row-actions { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 14px 0; }
|
|
2903
3247
|
.banner {
|
|
2904
|
-
display: none;
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
3248
|
+
display: none; position: sticky; top: calc(var(--header) + 12px); z-index: 15;
|
|
3249
|
+
align-items: center; gap: 10px; margin: 0 0 16px; padding: 11px 12px 11px 14px;
|
|
3250
|
+
border: 1px solid color-mix(in srgb, var(--err) 30%, var(--line)); border-radius: 10px;
|
|
3251
|
+
background: var(--err-soft); color: var(--err); box-shadow: var(--shadow-sm); font-size: 13px;
|
|
3252
|
+
}
|
|
3253
|
+
.banner.show { display: flex; }
|
|
3254
|
+
.banner span { flex: 1; }
|
|
3255
|
+
.banner button { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 7px; background: transparent; color: currentColor; cursor: pointer; }
|
|
3256
|
+
.banner button:hover { background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
3257
|
+
.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); }
|
|
2909
3258
|
dialog {
|
|
2910
|
-
border: 1px solid var(--line); border-radius:
|
|
2911
|
-
color: var(--ink); width: min(
|
|
2912
|
-
}
|
|
2913
|
-
dialog
|
|
2914
|
-
dialog
|
|
3259
|
+
border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--bg-elev);
|
|
3260
|
+
color: var(--ink); width: min(720px, calc(100% - 24px)); max-width: calc(100vw - 24px); box-shadow: var(--shadow-lg);
|
|
3261
|
+
}
|
|
3262
|
+
dialog[open] { animation: dialog-in 160ms ease-out; }
|
|
3263
|
+
dialog::backdrop { background: rgb(12 14 20 / 0.58); backdrop-filter: blur(3px); }
|
|
3264
|
+
dialog form, .dialog-body { padding: 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
3265
|
+
.dialog-heading { display: flex; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
|
3266
|
+
.dialog-heading strong { font-size: 18px; letter-spacing: -0.02em; }
|
|
3267
|
+
.dialog-heading span { color: var(--muted); font-size: 12px; }
|
|
2915
3268
|
textarea {
|
|
2916
3269
|
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
2917
3270
|
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
2918
3271
|
font-family: var(--mono); font-size: 12px;
|
|
2919
3272
|
}
|
|
2920
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
2921
|
-
.
|
|
3273
|
+
.dialog-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding-top: 4px; }
|
|
3274
|
+
.dialog-actions .btn-danger { margin-right: auto; }
|
|
3275
|
+
.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); }
|
|
2922
3276
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
2923
3277
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
2924
3278
|
.status-2 { color: var(--ok); }
|
|
2925
3279
|
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
2926
3280
|
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
3281
|
+
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
3282
|
+
.form-grid .wide { grid-column: span 2; }
|
|
2927
3283
|
.route {
|
|
2928
|
-
display: grid; grid-template-columns:
|
|
2929
|
-
padding:
|
|
2930
|
-
}
|
|
2931
|
-
.
|
|
3284
|
+
display: grid; grid-template-columns: 84px minmax(0, 1fr) auto; gap: 10px; align-items: center;
|
|
3285
|
+
padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--bg-elev);
|
|
3286
|
+
}
|
|
3287
|
+
.route:first-child { border-radius: var(--radius) var(--radius) 0 0; }
|
|
3288
|
+
.route:last-child { border-bottom: 0; border-radius: 0 0 var(--radius) var(--radius); }
|
|
3289
|
+
.route-tools { display: flex; align-items: flex-end; gap: 12px; margin-bottom: 12px; }
|
|
3290
|
+
.route-tools .field { flex: 1; }
|
|
3291
|
+
.route-tools input { width: 100%; }
|
|
3292
|
+
.route-count { padding-bottom: 10px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3293
|
+
#route-list { max-height: 34rem; border: 1px solid var(--line); border-radius: var(--radius); overflow: auto; box-shadow: var(--shadow-xs); }
|
|
3294
|
+
.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; }
|
|
3295
|
+
@keyframes view-in { from { transform: translateY(3px); } to { transform: translateY(0); } }
|
|
3296
|
+
@keyframes dialog-in { from { transform: translateY(8px) scale(0.99); } to { transform: translateY(0) scale(1); } }
|
|
3297
|
+
@keyframes pulse { 50% { opacity: 0.45; } }
|
|
3298
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
|
|
3299
|
+
@media (forced-colors: active) { .json-editor pre { display: none; } .json-editor textarea { color: CanvasText; -webkit-text-fill-color: CanvasText; } }
|
|
2932
3300
|
@media (max-width: 800px) {
|
|
3301
|
+
:root { --header: 0px; }
|
|
2933
3302
|
header.top {
|
|
2934
|
-
align-items: stretch; flex-direction: column;
|
|
2935
|
-
padding
|
|
3303
|
+
position: relative; align-items: stretch; flex-direction: column;
|
|
3304
|
+
padding: max(12px, env(safe-area-inset-top)) 14px 12px;
|
|
2936
3305
|
}
|
|
2937
3306
|
input, select, textarea { font-size: 16px; }
|
|
2938
3307
|
.brand, .vendor, .controls { width: 100%; }
|
|
3308
|
+
.brand { gap: 10px; }
|
|
3309
|
+
.vendor { order: 2; border-radius: 9px; }
|
|
2939
3310
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
3311
|
+
.sync-status { display: none; }
|
|
2940
3312
|
/* The layout fills the screen. A single-column grid stretches every auto
|
|
2941
3313
|
row, so the tab bar grew with the leftover height and the buttons floated
|
|
2942
3314
|
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
@@ -2946,14 +3318,23 @@ var renderAdminDocument = (service) => {
|
|
|
2946
3318
|
align-content: start;
|
|
2947
3319
|
}
|
|
2948
3320
|
nav.side {
|
|
2949
|
-
align-self: start; height: auto;
|
|
3321
|
+
position: sticky; top: 0; z-index: 12; align-self: start; height: auto;
|
|
2950
3322
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
2951
3323
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
2952
|
-
padding: 8px;
|
|
3324
|
+
padding: 8px; box-shadow: var(--shadow-xs);
|
|
2953
3325
|
}
|
|
2954
|
-
nav
|
|
3326
|
+
.nav-label { display: none; }
|
|
3327
|
+
nav.side button { width: auto; white-space: nowrap; flex: none; }
|
|
3328
|
+
nav.side button[aria-current="true"]::before { inset: auto 9px -8px; width: auto; height: 3px; border-radius: 3px 3px 0 0; }
|
|
2955
3329
|
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
2956
|
-
|
|
3330
|
+
.form-grid { grid-template-columns: minmax(0, 1fr); }
|
|
3331
|
+
.form-grid .wide { grid-column: auto; }
|
|
3332
|
+
main { padding: 22px 14px 48px; }
|
|
3333
|
+
.view-head { margin-bottom: 18px; }
|
|
3334
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
3335
|
+
.card { padding: 14px; }
|
|
3336
|
+
.card .v { font-size: 20px; }
|
|
3337
|
+
.table-shell { border: 0; background: transparent; box-shadow: none; overflow: visible; }
|
|
2957
3338
|
table, thead, tbody, tr, th, td { display: block; }
|
|
2958
3339
|
thead { display: none; }
|
|
2959
3340
|
tr {
|
|
@@ -2968,50 +3349,63 @@ var renderAdminDocument = (service) => {
|
|
|
2968
3349
|
.route { grid-template-columns: 72px 1fr; }
|
|
2969
3350
|
.route button { grid-column: 1 / -1; }
|
|
2970
3351
|
}
|
|
3352
|
+
@media (max-width: 460px) {
|
|
3353
|
+
.controls label.field { min-width: calc(50% - 4px); }
|
|
3354
|
+
.controls .icon-btn, .controls .btn { flex: 1; }
|
|
3355
|
+
.grid { grid-template-columns: minmax(0, 1fr); }
|
|
3356
|
+
.dialog-actions { flex-wrap: wrap; }
|
|
3357
|
+
.dialog-actions button { flex: 1; }
|
|
3358
|
+
.dialog-actions .btn-danger { flex-basis: 100%; margin-right: 0; }
|
|
3359
|
+
}
|
|
2971
3360
|
</style>
|
|
2972
3361
|
</head>
|
|
2973
3362
|
<body>
|
|
2974
3363
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
2975
3364
|
<header class="top">
|
|
2976
3365
|
<div class="brand">
|
|
2977
|
-
<div class="brand-
|
|
2978
|
-
<
|
|
2979
|
-
|
|
3366
|
+
<div class="brand-lockup">
|
|
3367
|
+
<div class="brand-mark" aria-hidden="true">
|
|
3368
|
+
<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>
|
|
3369
|
+
</div>
|
|
3370
|
+
<div class="brand-id">
|
|
3371
|
+
<strong>${name}</strong>
|
|
3372
|
+
<span>Mock admin</span>
|
|
3373
|
+
</div>
|
|
2980
3374
|
</div>
|
|
2981
3375
|
<div class="vendor" id="vendor" hidden></div>
|
|
2982
3376
|
</div>
|
|
2983
3377
|
<div class="controls">
|
|
3378
|
+
<span class="sync-status" id="sync-status" role="status" aria-live="polite">Ready</span>
|
|
2984
3379
|
<label class="field">Namespace
|
|
2985
3380
|
<select id="namespace" aria-label="Namespace"></select>
|
|
2986
3381
|
</label>
|
|
2987
3382
|
<label class="field">Admin key
|
|
2988
3383
|
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
2989
3384
|
</label>
|
|
2990
|
-
<button class="icon-btn" id="theme" type="button" aria-label="
|
|
2991
|
-
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
3385
|
+
<button class="icon-btn" id="theme" type="button" aria-label="Use dark theme" title="Use dark theme"></button>
|
|
3386
|
+
<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>
|
|
2992
3387
|
</div>
|
|
2993
3388
|
</header>
|
|
2994
3389
|
<div class="layout">
|
|
2995
3390
|
<nav class="side" id="nav" aria-label="Admin sections">
|
|
2996
|
-
<
|
|
2997
|
-
<button type="button" data-view="
|
|
2998
|
-
<button type="button" data-view="
|
|
2999
|
-
<button type="button" data-view="
|
|
3000
|
-
<button type="button" data-view="
|
|
3001
|
-
<button type="button" data-view="
|
|
3391
|
+
<span class="nav-label">Workspace</span>
|
|
3392
|
+
<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>
|
|
3393
|
+
<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>
|
|
3394
|
+
<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>
|
|
3395
|
+
<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>
|
|
3396
|
+
<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>
|
|
3397
|
+
<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>
|
|
3002
3398
|
</nav>
|
|
3003
3399
|
<main id="views">
|
|
3004
|
-
<
|
|
3400
|
+
<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>
|
|
3005
3401
|
<section class="view" id="view-overview">
|
|
3006
|
-
<h2>Overview</h2>
|
|
3007
|
-
<p class="lede">Health, clock, and the collections this namespace is holding.</p>
|
|
3402
|
+
<div class="view-head"><div><h2>Overview</h2><p class="lede">Health, clock, and the collections this namespace is holding.</p></div></div>
|
|
3008
3403
|
<div class="grid" id="overview-cards"></div>
|
|
3009
3404
|
<h3>Recent requests</h3>
|
|
3010
3405
|
<div id="overview-requests"></div>
|
|
3011
3406
|
</section>
|
|
3012
3407
|
<section class="view" id="view-state" hidden>
|
|
3013
|
-
<h2>State</h2>
|
|
3014
|
-
<p class="lede">Every collection in this namespace. Declared fields stay visible before any row exists; stored rows add whatever else they contain.</p>
|
|
3408
|
+
<div class="view-head"><div><h2>State</h2><p class="lede">Inspect and edit every collection in this namespace.</p></div></div>
|
|
3015
3409
|
<div class="split">
|
|
3016
3410
|
<div id="collections"></div>
|
|
3017
3411
|
<div>
|
|
@@ -3024,8 +3418,7 @@ var renderAdminDocument = (service) => {
|
|
|
3024
3418
|
</div>
|
|
3025
3419
|
</section>
|
|
3026
3420
|
<section class="view" id="view-clock" hidden>
|
|
3027
|
-
<h2>Clock</h2>
|
|
3028
|
-
<p class="lede">The clock every timestamp in this mock reads.</p>
|
|
3421
|
+
<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>
|
|
3029
3422
|
<div class="card" id="clock-readout"></div>
|
|
3030
3423
|
<div class="row-actions">
|
|
3031
3424
|
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
@@ -3044,21 +3437,22 @@ var renderAdminDocument = (service) => {
|
|
|
3044
3437
|
</form>
|
|
3045
3438
|
</section>
|
|
3046
3439
|
<section class="view" id="view-faults" hidden>
|
|
3047
|
-
<h2>Faults</h2>
|
|
3048
|
-
<p class="lede">Inject a response, a delay, or a named preset. Rules apply to this namespace.</p>
|
|
3440
|
+
<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>
|
|
3049
3441
|
<form id="fault-form" class="card stack">
|
|
3050
|
-
<
|
|
3051
|
-
<
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
<
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
<
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
<
|
|
3061
|
-
|
|
3442
|
+
<div class="form-grid">
|
|
3443
|
+
<label class="field wide">Preset
|
|
3444
|
+
<select id="fault-preset"><option value="">Custom rule</option></select>
|
|
3445
|
+
</label>
|
|
3446
|
+
<label class="field">Status
|
|
3447
|
+
<input id="fault-status" inputmode="numeric" placeholder="503">
|
|
3448
|
+
</label>
|
|
3449
|
+
<label class="field wide">Path prefix
|
|
3450
|
+
<input id="fault-path" placeholder="/v1">
|
|
3451
|
+
</label>
|
|
3452
|
+
<label class="field">Method
|
|
3453
|
+
<input id="fault-method" placeholder="GET">
|
|
3454
|
+
</label>
|
|
3455
|
+
</div>
|
|
3062
3456
|
<div class="dialog-actions">
|
|
3063
3457
|
<button class="btn-primary" type="submit">Add fault</button>
|
|
3064
3458
|
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
@@ -3067,26 +3461,31 @@ var renderAdminDocument = (service) => {
|
|
|
3067
3461
|
<div id="fault-list"></div>
|
|
3068
3462
|
</section>
|
|
3069
3463
|
<section class="view" id="view-journal" hidden>
|
|
3070
|
-
<h2>Journal</h2>
|
|
3071
|
-
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
3072
|
-
<div class="row-actions">
|
|
3073
|
-
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
3074
|
-
</div>
|
|
3464
|
+
<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>
|
|
3075
3465
|
<div id="journal"></div>
|
|
3076
3466
|
</section>
|
|
3077
3467
|
<section class="view" id="view-routes" hidden>
|
|
3078
|
-
<h2>Routes</h2>
|
|
3079
|
-
<
|
|
3468
|
+
<div class="view-head"><div><h2>Routes</h2><p class="lede">Explore shared controls and service-specific admin endpoints.</p></div></div>
|
|
3469
|
+
<div class="route-tools">
|
|
3470
|
+
<label class="field">Filter routes
|
|
3471
|
+
<input id="route-search" type="search" placeholder="Method, path, or extension" autocomplete="off">
|
|
3472
|
+
</label>
|
|
3473
|
+
<span class="route-count" id="route-count"></span>
|
|
3474
|
+
</div>
|
|
3080
3475
|
<div id="route-list"></div>
|
|
3081
|
-
<form id="route-form" class="card stack">
|
|
3476
|
+
<form id="route-form" class="card stack" style="margin-top: 18px">
|
|
3082
3477
|
<strong id="route-title">Select a route</strong>
|
|
3083
3478
|
<label class="field">JSON body
|
|
3084
|
-
<
|
|
3479
|
+
<div class="json-editor" id="route-editor" hidden>
|
|
3480
|
+
<pre aria-hidden="true"><code id="route-highlight"></code></pre>
|
|
3481
|
+
<textarea id="route-body" placeholder="{ }" spellcheck="false" aria-describedby="route-json-status" disabled></textarea>
|
|
3482
|
+
</div>
|
|
3483
|
+
<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>
|
|
3085
3484
|
</label>
|
|
3086
3485
|
<div class="dialog-actions">
|
|
3087
|
-
<button class="btn-primary" type="submit">Send</button>
|
|
3486
|
+
<button class="btn-primary" id="route-send" type="submit" disabled>Send request</button>
|
|
3088
3487
|
</div>
|
|
3089
|
-
<pre id="route-result">Response will show here.</pre>
|
|
3488
|
+
<pre class="json-view" id="route-result"><code>Response will show here.</code></pre>
|
|
3090
3489
|
</form>
|
|
3091
3490
|
</section>
|
|
3092
3491
|
</main>
|
|
@@ -3094,12 +3493,16 @@ var renderAdminDocument = (service) => {
|
|
|
3094
3493
|
</div>
|
|
3095
3494
|
<dialog id="editor">
|
|
3096
3495
|
<form method="dialog" id="editor-form">
|
|
3097
|
-
<strong id="editor-title">Record</strong>
|
|
3496
|
+
<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>
|
|
3098
3497
|
<label class="field">Id
|
|
3099
3498
|
<input id="editor-id" required>
|
|
3100
3499
|
</label>
|
|
3101
3500
|
<label class="field">JSON value
|
|
3102
|
-
<
|
|
3501
|
+
<div class="json-editor" id="record-editor">
|
|
3502
|
+
<pre aria-hidden="true"><code id="editor-highlight"></code></pre>
|
|
3503
|
+
<textarea id="editor-value" spellcheck="false" aria-describedby="editor-json-status"></textarea>
|
|
3504
|
+
</div>
|
|
3505
|
+
<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>
|
|
3103
3506
|
</label>
|
|
3104
3507
|
<div class="dialog-actions">
|
|
3105
3508
|
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
@@ -3141,8 +3544,8 @@ var describeExtension = (extension) => {
|
|
|
3141
3544
|
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
3142
3545
|
};
|
|
3143
3546
|
};
|
|
3144
|
-
var adminUiRoutes = (service, ui) => {
|
|
3145
|
-
const shell = () => renderAdminDocument(service);
|
|
3547
|
+
var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
3548
|
+
const shell = () => renderAdminDocument(service, adminPrefix);
|
|
3146
3549
|
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
3147
3550
|
return {
|
|
3148
3551
|
"GET /ui": () => htmlResponse(document3()),
|
|
@@ -3759,7 +4162,7 @@ var stateAdminRoutes = (routes) => {
|
|
|
3759
4162
|
};
|
|
3760
4163
|
|
|
3761
4164
|
// ../core/dist/version.js
|
|
3762
|
-
var PACKAGE_VERSION = true ? "
|
|
4165
|
+
var PACKAGE_VERSION = true ? "2.0.0" : UNRELEASED_VERSION;
|
|
3763
4166
|
|
|
3764
4167
|
// ../core/dist/signing.js
|
|
3765
4168
|
var encoder = new TextEncoder();
|
|
@@ -3852,7 +4255,9 @@ var createWebhookHub = (options) => {
|
|
|
3852
4255
|
const faults = /* @__PURE__ */ new Map();
|
|
3853
4256
|
const held = /* @__PURE__ */ new Map();
|
|
3854
4257
|
const inFlight = /* @__PURE__ */ new Set();
|
|
4258
|
+
const controllers = /* @__PURE__ */ new Map();
|
|
3855
4259
|
let immediate = Promise.resolve();
|
|
4260
|
+
const snapshots = /* @__PURE__ */ new WeakMap();
|
|
3856
4261
|
const track = (work) => {
|
|
3857
4262
|
inFlight.add(work);
|
|
3858
4263
|
void work.finally(() => inFlight.delete(work));
|
|
@@ -3873,6 +4278,7 @@ var createWebhookHub = (options) => {
|
|
|
3873
4278
|
responseBody: null
|
|
3874
4279
|
};
|
|
3875
4280
|
const controller = new AbortController();
|
|
4281
|
+
controllers.set(delivery.id, controller);
|
|
3876
4282
|
const timer = scheduleTimer(() => controller.abort(), timeoutMs);
|
|
3877
4283
|
try {
|
|
3878
4284
|
const signed = await options.signer({
|
|
@@ -3902,6 +4308,7 @@ var createWebhookHub = (options) => {
|
|
|
3902
4308
|
} catch (error) {
|
|
3903
4309
|
record.error = controller.signal.aborted ? `timed out after ${timeoutMs}ms` : error instanceof Error ? error.message : String(error);
|
|
3904
4310
|
} finally {
|
|
4311
|
+
controllers.delete(delivery.id);
|
|
3905
4312
|
cancel(timer);
|
|
3906
4313
|
record.durationMs = now() - started;
|
|
3907
4314
|
delivery.attempts.push(record);
|
|
@@ -3919,6 +4326,8 @@ var createWebhookHub = (options) => {
|
|
|
3919
4326
|
if (!payloads.has(delivery.id))
|
|
3920
4327
|
return;
|
|
3921
4328
|
await attempt(delivery).then((ok) => {
|
|
4329
|
+
if (!payloads.has(delivery.id))
|
|
4330
|
+
return;
|
|
3922
4331
|
if (ok)
|
|
3923
4332
|
delivery.state = "delivered";
|
|
3924
4333
|
else
|
|
@@ -3980,6 +4389,37 @@ var createWebhookHub = (options) => {
|
|
|
3980
4389
|
fanOut(message2);
|
|
3981
4390
|
};
|
|
3982
4391
|
const hub = {
|
|
4392
|
+
pending(namespace) {
|
|
4393
|
+
return inFlight.size + (held.get(namespace)?.length ?? 0) + [...deliveries.values()].filter((d) => d.namespace === namespace && d.state === "pending").length;
|
|
4394
|
+
},
|
|
4395
|
+
snapshot(namespace) {
|
|
4396
|
+
if (hub.pending(namespace) > 0)
|
|
4397
|
+
throw new Error("webhooks are not quiescent");
|
|
4398
|
+
const handle = {};
|
|
4399
|
+
snapshots.set(handle, structuredClone({
|
|
4400
|
+
namespace,
|
|
4401
|
+
endpoints: own.get(namespace) ?? [],
|
|
4402
|
+
messages: messages.filter((m) => m.namespace === namespace),
|
|
4403
|
+
deliveries: [...deliveries].filter(([, d]) => d.namespace === namespace),
|
|
4404
|
+
payloads: [...payloads].filter(([, p]) => p.message.namespace === namespace),
|
|
4405
|
+
faults: faults.get(namespace) ?? []
|
|
4406
|
+
}));
|
|
4407
|
+
return handle;
|
|
4408
|
+
},
|
|
4409
|
+
restore(namespace, handle) {
|
|
4410
|
+
const point = typeof handle === "object" && handle !== null ? snapshots.get(handle) : void 0;
|
|
4411
|
+
if (!point || point.namespace !== namespace || hub.pending(namespace) > 0)
|
|
4412
|
+
throw new Error("invalid webhook restore");
|
|
4413
|
+
hub.clear(namespace);
|
|
4414
|
+
const copied = structuredClone(point);
|
|
4415
|
+
own.set(namespace, copied.endpoints);
|
|
4416
|
+
messages.push(...copied.messages);
|
|
4417
|
+
for (const [id2, delivery] of copied.deliveries)
|
|
4418
|
+
deliveries.set(id2, delivery);
|
|
4419
|
+
for (const [id2, payload] of copied.payloads)
|
|
4420
|
+
payloads.set(id2, payload);
|
|
4421
|
+
faults.set(namespace, copied.faults);
|
|
4422
|
+
},
|
|
3983
4423
|
publish(input) {
|
|
3984
4424
|
const contentType = input.contentType ?? (input.form ? "application/x-www-form-urlencoded" : "application/json");
|
|
3985
4425
|
const body = typeof input.body === "string" ? input.body : input.form ? new URLSearchParams(input.form).toString() : JSON.stringify(input.body);
|
|
@@ -4070,6 +4510,7 @@ var createWebhookHub = (options) => {
|
|
|
4070
4510
|
for (const [id2, delivery] of deliveries) {
|
|
4071
4511
|
if (namespace !== void 0 && delivery.namespace !== namespace)
|
|
4072
4512
|
continue;
|
|
4513
|
+
controllers.get(id2)?.abort();
|
|
4073
4514
|
const timer = pending.get(id2);
|
|
4074
4515
|
if (timer !== void 0)
|
|
4075
4516
|
cancel(timer);
|
|
@@ -4094,7 +4535,10 @@ var createWebhookHub = (options) => {
|
|
|
4094
4535
|
};
|
|
4095
4536
|
return hub;
|
|
4096
4537
|
};
|
|
4097
|
-
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4538
|
+
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4539
|
+
status,
|
|
4540
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4541
|
+
});
|
|
4098
4542
|
var adminError3 = (status, message2) => json4(status, { error: { type: "mockingbird_admin", message: message2 } });
|
|
4099
4543
|
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4100
4544
|
var parseEndpoint = (value) => {
|
|
@@ -4202,7 +4646,6 @@ var AT_HEADER = "x-mockingbird-at";
|
|
|
4202
4646
|
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
4203
4647
|
var DEFAULT_NAMESPACE = "default";
|
|
4204
4648
|
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4205
|
-
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
4206
4649
|
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4207
4650
|
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
4208
4651
|
var effects = /* @__PURE__ */ new WeakMap();
|
|
@@ -4243,6 +4686,22 @@ var operationMatcher = (document3) => {
|
|
|
4243
4686
|
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
4244
4687
|
};
|
|
4245
4688
|
var createRuntime = (options) => {
|
|
4689
|
+
const adminPrefix = resolveAdminPrefix(options.adminPrefix);
|
|
4690
|
+
const internalPaths = Object.entries(options.document?.paths ?? {}).filter(([, item]) => item["x-mockingbird-internal"] === true).map(([path]) => path);
|
|
4691
|
+
for (const path of internalPaths) {
|
|
4692
|
+
if (!path.startsWith(`${ADMIN_PREFIX}/blobs/`))
|
|
4693
|
+
throw new Error(`internal transport ${path} must live under ${ADMIN_PREFIX}/blobs/`);
|
|
4694
|
+
}
|
|
4695
|
+
assertAdminPrefixAvailable(adminPrefix, Object.keys(options.document?.paths ?? {}).filter((path) => !internalPaths.includes(path)));
|
|
4696
|
+
const internalMatch = (path) => {
|
|
4697
|
+
for (const pattern of internalPaths) {
|
|
4698
|
+
const actual = path.split("/");
|
|
4699
|
+
const expected = pattern.split("/");
|
|
4700
|
+
if (actual.length === expected.length && expected.every((part, i) => part.startsWith("{") || part === actual[i]))
|
|
4701
|
+
return pattern;
|
|
4702
|
+
}
|
|
4703
|
+
return void 0;
|
|
4704
|
+
};
|
|
4246
4705
|
assertAdminUi(options.adminUi);
|
|
4247
4706
|
const sqlite = bootSqlite(options.sqlite);
|
|
4248
4707
|
const clock = options.clock ?? createClock();
|
|
@@ -4257,6 +4716,20 @@ var createRuntime = (options) => {
|
|
|
4257
4716
|
const instances = /* @__PURE__ */ new Map();
|
|
4258
4717
|
const publicNamespaces = /* @__PURE__ */ new Set();
|
|
4259
4718
|
const branchRngs = /* @__PURE__ */ new Map();
|
|
4719
|
+
const namespaceClocks = /* @__PURE__ */ new Map();
|
|
4720
|
+
const namespaceForStorage = /* @__PURE__ */ new Map();
|
|
4721
|
+
let isolatedNamespaces = false;
|
|
4722
|
+
const namespaceClock = (name = DEFAULT_NAMESPACE) => {
|
|
4723
|
+
if (!isolatedNamespaces || name === DEFAULT_NAMESPACE)
|
|
4724
|
+
return clock;
|
|
4725
|
+
let found = namespaceClocks.get(name);
|
|
4726
|
+
if (!found) {
|
|
4727
|
+
found = createClock();
|
|
4728
|
+
namespaceClocks.set(name, found);
|
|
4729
|
+
}
|
|
4730
|
+
return found;
|
|
4731
|
+
};
|
|
4732
|
+
const fleetPoints = /* @__PURE__ */ new WeakMap();
|
|
4260
4733
|
const timelines = /* @__PURE__ */ new Map();
|
|
4261
4734
|
const branchStorage = /* @__PURE__ */ new Map();
|
|
4262
4735
|
const captured = /* @__PURE__ */ new Map();
|
|
@@ -4270,17 +4743,22 @@ var createRuntime = (options) => {
|
|
|
4270
4743
|
if (!NAMESPACE_PATTERN.test(key) || !NAMESPACE_PATTERN.test(publicNamespace)) {
|
|
4271
4744
|
throw new RangeError(`namespace must match ${NAMESPACE_PATTERN}: ${JSON.stringify(publicNamespace)}`);
|
|
4272
4745
|
}
|
|
4746
|
+
const createdContextRng = isolatedRng ?? (isolatedNamespaces && key !== DEFAULT_NAMESPACE ? createRng(options.seed ?? 0) : rng);
|
|
4273
4747
|
const created = options.create({
|
|
4274
4748
|
namespace: storageNamespace(key),
|
|
4275
4749
|
publicNamespace,
|
|
4750
|
+
adminPrefix,
|
|
4276
4751
|
sqlite,
|
|
4277
|
-
clock,
|
|
4278
|
-
rng:
|
|
4752
|
+
clock: namespaceClock(publicNamespace),
|
|
4753
|
+
rng: createdContextRng
|
|
4279
4754
|
});
|
|
4280
4755
|
instances.set(key, created);
|
|
4756
|
+
namespaceForStorage.set(key, publicNamespace);
|
|
4281
4757
|
publicNamespaces.add(publicNamespace);
|
|
4282
4758
|
if (isolatedRng)
|
|
4283
4759
|
branchRngs.set(key, isolatedRng);
|
|
4760
|
+
else if (isolatedNamespaces && key !== DEFAULT_NAMESPACE)
|
|
4761
|
+
branchRngs.set(key, createdContextRng);
|
|
4284
4762
|
return created;
|
|
4285
4763
|
};
|
|
4286
4764
|
const instance = (name = DEFAULT_NAMESPACE) => instanceFor(name);
|
|
@@ -4298,7 +4776,7 @@ var createRuntime = (options) => {
|
|
|
4298
4776
|
captured.set(storage, snapshot2);
|
|
4299
4777
|
return Object.freeze({
|
|
4300
4778
|
snapshot: snapshot2,
|
|
4301
|
-
clock: Object.freeze(
|
|
4779
|
+
clock: Object.freeze(namespaceClock(namespaceForStorage.get(storage) ?? storage).state()),
|
|
4302
4780
|
rngState: (branchRngs.get(storage) ?? rng).state()
|
|
4303
4781
|
});
|
|
4304
4782
|
};
|
|
@@ -4308,7 +4786,7 @@ var createRuntime = (options) => {
|
|
|
4308
4786
|
return found;
|
|
4309
4787
|
instance(name);
|
|
4310
4788
|
found = new Timeline({
|
|
4311
|
-
now:
|
|
4789
|
+
now: namespaceClock(name).now,
|
|
4312
4790
|
...options.maxCheckpoints !== void 0 ? { maxCheckpoints: options.maxCheckpoints } : {}
|
|
4313
4791
|
});
|
|
4314
4792
|
found.commit(capture(name));
|
|
@@ -4335,12 +4813,13 @@ var createRuntime = (options) => {
|
|
|
4335
4813
|
const point = history2.checkout("main", at);
|
|
4336
4814
|
restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
|
|
4337
4815
|
captured.set(namespace, point.value.snapshot);
|
|
4338
|
-
rng.setState(point.value.rngState);
|
|
4339
|
-
|
|
4816
|
+
(branchRngs.get(namespace) ?? rng).setState(point.value.rngState);
|
|
4817
|
+
const selectedClock = namespaceClock(namespace);
|
|
4818
|
+
selectedClock.set(point.value.clock.now);
|
|
4340
4819
|
if (point.value.clock.frozen)
|
|
4341
|
-
|
|
4820
|
+
selectedClock.freeze();
|
|
4342
4821
|
else
|
|
4343
|
-
|
|
4822
|
+
selectedClock.unfreeze();
|
|
4344
4823
|
}
|
|
4345
4824
|
return namespace;
|
|
4346
4825
|
}
|
|
@@ -4398,11 +4877,12 @@ var createRuntime = (options) => {
|
|
|
4398
4877
|
const storage = ensureBranch(namespace, branchName);
|
|
4399
4878
|
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
4400
4879
|
captured.set(storage, point.value.snapshot);
|
|
4401
|
-
|
|
4880
|
+
const selectedClock = namespaceClock(namespace);
|
|
4881
|
+
selectedClock.set(point.value.clock.now);
|
|
4402
4882
|
if (point.value.clock.frozen)
|
|
4403
|
-
|
|
4883
|
+
selectedClock.freeze();
|
|
4404
4884
|
else
|
|
4405
|
-
|
|
4885
|
+
selectedClock.unfreeze();
|
|
4406
4886
|
(branchRngs.get(storage) ?? rng).setState(point.value.rngState);
|
|
4407
4887
|
};
|
|
4408
4888
|
const reset = async (name = DEFAULT_NAMESPACE) => {
|
|
@@ -4410,10 +4890,21 @@ var createRuntime = (options) => {
|
|
|
4410
4890
|
options.webhooks?.clear();
|
|
4411
4891
|
for (const each of instances.values())
|
|
4412
4892
|
await each.reset();
|
|
4893
|
+
for (const each of branchRngs.values())
|
|
4894
|
+
each.reset();
|
|
4413
4895
|
timelines.clear();
|
|
4896
|
+
for (const storage of branchStorage.values()) {
|
|
4897
|
+
instances.delete(storage);
|
|
4898
|
+
branchRngs.delete(storage);
|
|
4899
|
+
namespaceForStorage.delete(storage);
|
|
4900
|
+
}
|
|
4414
4901
|
branchStorage.clear();
|
|
4415
|
-
branchRngs.clear();
|
|
4416
4902
|
captured.clear();
|
|
4903
|
+
faults.clear();
|
|
4904
|
+
clock.reset();
|
|
4905
|
+
rng.reset();
|
|
4906
|
+
for (const each of namespaceClocks.values())
|
|
4907
|
+
each.reset();
|
|
4417
4908
|
return;
|
|
4418
4909
|
}
|
|
4419
4910
|
options.webhooks?.clear(name);
|
|
@@ -4431,11 +4922,18 @@ var createRuntime = (options) => {
|
|
|
4431
4922
|
else
|
|
4432
4923
|
clearNamespace(sqlite, storageNamespace(storage));
|
|
4433
4924
|
branchStorage.delete(mapping);
|
|
4925
|
+
instances.delete(storage);
|
|
4926
|
+
namespaceForStorage.delete(storage);
|
|
4434
4927
|
branchRngs.delete(storage);
|
|
4435
4928
|
captured.delete(storage);
|
|
4436
4929
|
}
|
|
4437
4930
|
timelines.delete(name);
|
|
4438
4931
|
captured.delete(name);
|
|
4932
|
+
faults.restore(name, { rules: [], rngState: createRng(options.seed ?? 0).state() });
|
|
4933
|
+
namespaceClock(name).reset();
|
|
4934
|
+
(branchRngs.get(name) ?? rng).reset();
|
|
4935
|
+
journal.clear(name);
|
|
4936
|
+
metrics.reset(name);
|
|
4439
4937
|
};
|
|
4440
4938
|
const snapshot = (name = DEFAULT_NAMESPACE) => {
|
|
4441
4939
|
return checkpoint(name, "main").value.snapshot;
|
|
@@ -4473,6 +4971,53 @@ var createRuntime = (options) => {
|
|
|
4473
4971
|
rng,
|
|
4474
4972
|
credentials,
|
|
4475
4973
|
webhooks: options.webhooks,
|
|
4974
|
+
isolateNamespaces() {
|
|
4975
|
+
if (publicNamespaces.size > 1)
|
|
4976
|
+
throw new Error("enable namespace isolation before creating named namespaces");
|
|
4977
|
+
isolatedNamespaces = true;
|
|
4978
|
+
faults.isolateNamespaces();
|
|
4979
|
+
},
|
|
4980
|
+
namespaceClock,
|
|
4981
|
+
namespaceOf(request) {
|
|
4982
|
+
const url = new URL(request.url);
|
|
4983
|
+
const prefixed = matchNamespacePath(url.pathname, adminPrefix);
|
|
4984
|
+
const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
|
|
4985
|
+
const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
|
|
4986
|
+
if (isAdminPath(path, adminPrefix))
|
|
4987
|
+
return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
|
|
4988
|
+
if (selected !== void 0)
|
|
4989
|
+
return selected;
|
|
4990
|
+
const credential = options.credential?.(request);
|
|
4991
|
+
return (credential !== void 0 ? credentials.get(credential) : void 0) ?? DEFAULT_NAMESPACE;
|
|
4992
|
+
},
|
|
4993
|
+
fleetSnapshot(namespace) {
|
|
4994
|
+
instance(namespace);
|
|
4995
|
+
const handle = {};
|
|
4996
|
+
fleetPoints.set(handle, {
|
|
4997
|
+
namespace,
|
|
4998
|
+
state: capture(namespace),
|
|
4999
|
+
faults: faults.snapshot(namespace),
|
|
5000
|
+
webhooks: options.webhooks?.snapshot(namespace)
|
|
5001
|
+
});
|
|
5002
|
+
return handle;
|
|
5003
|
+
},
|
|
5004
|
+
fleetRestore(handle, namespace) {
|
|
5005
|
+
const point = typeof handle === "object" && handle !== null ? fleetPoints.get(handle) : void 0;
|
|
5006
|
+
if (!point || point.namespace !== namespace)
|
|
5007
|
+
throw new Error("invalid fleet checkpoint");
|
|
5008
|
+
restoreNamespace(sqlite, storageNamespace(namespace), point.state.snapshot);
|
|
5009
|
+
captured.set(namespace, point.state.snapshot);
|
|
5010
|
+
const selectedClock = namespaceClock(namespace);
|
|
5011
|
+
selectedClock.freeze();
|
|
5012
|
+
selectedClock.set(point.state.clock.now);
|
|
5013
|
+
if (!point.state.clock.frozen)
|
|
5014
|
+
selectedClock.unfreeze();
|
|
5015
|
+
(branchRngs.get(namespace) ?? rng).setState(point.state.rngState);
|
|
5016
|
+
faults.restore(namespace, point.faults);
|
|
5017
|
+
if (point.webhooks !== void 0)
|
|
5018
|
+
options.webhooks?.restore(namespace, point.webhooks);
|
|
5019
|
+
timelines.delete(namespace);
|
|
5020
|
+
},
|
|
4476
5021
|
applyPreset: (name, namespace = DEFAULT_NAMESPACE, overrides = {}) => {
|
|
4477
5022
|
const preset = options.presets?.[name];
|
|
4478
5023
|
if (!preset)
|
|
@@ -4504,7 +5049,7 @@ var createRuntime = (options) => {
|
|
|
4504
5049
|
state: (namespace = DEFAULT_NAMESPACE) => inspectState(stateScope(namespace)),
|
|
4505
5050
|
fetch: async (incoming) => {
|
|
4506
5051
|
let request = incoming;
|
|
4507
|
-
const prefixed =
|
|
5052
|
+
const prefixed = matchNamespacePath(new URL(request.url).pathname, adminPrefix);
|
|
4508
5053
|
if (prefixed) {
|
|
4509
5054
|
const url2 = new URL(request.url);
|
|
4510
5055
|
url2.pathname = prefixed[2] ?? "/";
|
|
@@ -4520,7 +5065,15 @@ var createRuntime = (options) => {
|
|
|
4520
5065
|
signal: request.signal
|
|
4521
5066
|
});
|
|
4522
5067
|
}
|
|
4523
|
-
|
|
5068
|
+
const internalUrl = new URL(request.url);
|
|
5069
|
+
const internalPath = isAdminPath(internalUrl.pathname, adminPrefix) ? `${ADMIN_PREFIX}${internalUrl.pathname.slice(adminPrefix.length)}` : "";
|
|
5070
|
+
const internal = internalMatch(internalPath);
|
|
5071
|
+
const obsoleteInternalPath = adminPrefix !== ADMIN_PREFIX && internalMatch(internalUrl.pathname) && !internal;
|
|
5072
|
+
if (internal) {
|
|
5073
|
+
internalUrl.pathname = internalPath;
|
|
5074
|
+
request = new Request(internalUrl, request);
|
|
5075
|
+
}
|
|
5076
|
+
let namespace = internal ? internalUrl.searchParams.get("namespace") ?? control.namespaceOf(request) : control.namespaceOf(request);
|
|
4524
5077
|
if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
4525
5078
|
const credential = options.credential(request);
|
|
4526
5079
|
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
@@ -4540,7 +5093,9 @@ var createRuntime = (options) => {
|
|
|
4540
5093
|
return copy;
|
|
4541
5094
|
}
|
|
4542
5095
|
};
|
|
4543
|
-
|
|
5096
|
+
if (obsoleteInternalPath)
|
|
5097
|
+
return stamp(adminFail(404, "internal transport is only available under adminPrefix"));
|
|
5098
|
+
const handled = internal ? void 0 : await control.handle(request);
|
|
4544
5099
|
if (handled)
|
|
4545
5100
|
return stamp(handled);
|
|
4546
5101
|
const started = monotonicNow();
|
|
@@ -4581,7 +5136,7 @@ var createRuntime = (options) => {
|
|
|
4581
5136
|
type: "mockingbird_admin",
|
|
4582
5137
|
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
4583
5138
|
}
|
|
4584
|
-
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
5139
|
+
}), { status: 400, headers: { "content-type": "application/json; charset=utf-8" } }));
|
|
4585
5140
|
}
|
|
4586
5141
|
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
4587
5142
|
log(400);
|
|
@@ -4648,6 +5203,7 @@ var createRuntime = (options) => {
|
|
|
4648
5203
|
startedAt: wallNow(),
|
|
4649
5204
|
wallNow,
|
|
4650
5205
|
clock,
|
|
5206
|
+
namespaceClock,
|
|
4651
5207
|
faults,
|
|
4652
5208
|
metrics,
|
|
4653
5209
|
journal,
|
|
@@ -4697,15 +5253,16 @@ var createRuntime = (options) => {
|
|
|
4697
5253
|
...options.admin?.(runtime) ?? {},
|
|
4698
5254
|
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
4699
5255
|
// maps `presets`, and a service route must not replace that list with a record.
|
|
4700
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
5256
|
+
...options.presets ? presetRoutes(options.presets, runtime, adminPrefix) : {},
|
|
4701
5257
|
...stateAdminRoutes({
|
|
4702
5258
|
open: stateScope,
|
|
4703
5259
|
afterWrite: (namespace) => {
|
|
4704
5260
|
checkpoint(namespace, "main");
|
|
4705
5261
|
}
|
|
4706
5262
|
}),
|
|
4707
|
-
...adminUiRoutes(options.name, options.adminUi)
|
|
5263
|
+
...adminUiRoutes(options.name, options.adminUi, adminPrefix)
|
|
4708
5264
|
},
|
|
5265
|
+
adminPrefix,
|
|
4709
5266
|
adminKey: options.adminKey
|
|
4710
5267
|
});
|
|
4711
5268
|
return defineMock(runtime);
|
|
@@ -4719,7 +5276,10 @@ var mutableResponse = (response) => {
|
|
|
4719
5276
|
return new Response(response.body, response);
|
|
4720
5277
|
}
|
|
4721
5278
|
};
|
|
4722
|
-
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5279
|
+
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5280
|
+
status,
|
|
5281
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
5282
|
+
});
|
|
4723
5283
|
var adminFail = (status, message2) => adminJson(status, { error: { type: "mockingbird_admin", message: message2 } });
|
|
4724
5284
|
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4725
5285
|
var credentialRoutes = (registry) => ({
|
|
@@ -4771,7 +5331,7 @@ var credentialRoutes = (registry) => ({
|
|
|
4771
5331
|
return adminJson(200, { status: "ok" });
|
|
4772
5332
|
}
|
|
4773
5333
|
});
|
|
4774
|
-
var presetRoutes = (presets, runtime) => ({
|
|
5334
|
+
var presetRoutes = (presets, runtime, adminPrefix) => ({
|
|
4775
5335
|
"GET /faults/presets": () => {
|
|
4776
5336
|
const body = {
|
|
4777
5337
|
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
@@ -4781,7 +5341,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
4781
5341
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
4782
5342
|
const name = params.name;
|
|
4783
5343
|
if (!presets[name])
|
|
4784
|
-
return adminFail(404, `no fault preset ${name}; GET /
|
|
5344
|
+
return adminFail(404, `no fault preset ${name}; GET ${adminPrefix}/faults/presets`);
|
|
4785
5345
|
const overrides = isObject(body) ? body : {};
|
|
4786
5346
|
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
4787
5347
|
}
|
|
@@ -5352,6 +5912,7 @@ var createRuntime2 = (options = {}) => {
|
|
|
5352
5912
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
5353
5913
|
...options.clock ? { clock: options.clock } : {},
|
|
5354
5914
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
5915
|
+
...options.adminPrefix !== void 0 ? { adminPrefix: options.adminPrefix } : {},
|
|
5355
5916
|
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
5356
5917
|
...options.onLog ? { onLog: options.onLog } : {},
|
|
5357
5918
|
credential: apiKeyCredential,
|
|
@@ -5779,6 +6340,9 @@ var OdxAPI = class {
|
|
|
5779
6340
|
var publicPatient = ({ partnerUserId: _partner, ...patient }) => patient;
|
|
5780
6341
|
|
|
5781
6342
|
export {
|
|
6343
|
+
createClock,
|
|
6344
|
+
resolveAdminPrefix,
|
|
6345
|
+
parseDuration,
|
|
5782
6346
|
LABS,
|
|
5783
6347
|
ELEMENTS,
|
|
5784
6348
|
document2 as document,
|
|
@@ -5795,4 +6359,4 @@ export {
|
|
|
5795
6359
|
apiKeyCredential,
|
|
5796
6360
|
OdxAPI
|
|
5797
6361
|
};
|
|
5798
|
-
//# sourceMappingURL=chunk-
|
|
6362
|
+
//# sourceMappingURL=chunk-K6M4YJNF.js.map
|