@crvouga/mockingbird-service-aws-speech 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 +14 -0
- package/dist/{chunk-QHTIOFIA.js → chunk-DPBUA2NJ.js} +4 -3
- package/dist/{chunk-QHTIOFIA.js.map → chunk-DPBUA2NJ.js.map} +2 -2
- package/dist/{chunk-AGCUZR5M.js → chunk-RTZSXNQY.js} +784 -258
- package/dist/chunk-RTZSXNQY.js.map +7 -0
- package/dist/cli.js +529 -37
- package/dist/cli.js.map +4 -4
- 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 +334 -0
- package/package.json +29 -4
- package/dist/chunk-AGCUZR5M.js.map +0 -7
|
@@ -110,13 +110,31 @@ var Collection = class {
|
|
|
110
110
|
};
|
|
111
111
|
|
|
112
112
|
// ../core/dist/control.js
|
|
113
|
-
var HEALTH_PATH = "/health";
|
|
114
113
|
var ADMIN_PREFIX = "/__admin";
|
|
114
|
+
var resolveAdminPrefix = (prefix = ADMIN_PREFIX) => {
|
|
115
|
+
if (!/^\/[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*$/.test(prefix)) {
|
|
116
|
+
throw new Error("adminPrefix must be an absolute non-root path of literal segments, without a trailing slash");
|
|
117
|
+
}
|
|
118
|
+
return prefix;
|
|
119
|
+
};
|
|
120
|
+
var isAdminPath = (path, prefix) => path === prefix || path.startsWith(`${prefix}/`);
|
|
121
|
+
var matchNamespacePath = (path, prefix) => /^\/ns\/([^/]+)(\/.*)?$/.exec(isAdminPath(path, prefix) ? path.slice(prefix.length) : "");
|
|
122
|
+
var assertAdminPrefixAvailable = (prefix, paths) => {
|
|
123
|
+
const reserved = prefix.split("/").slice(1);
|
|
124
|
+
for (const path of paths) {
|
|
125
|
+
const segments = path.split("/").slice(1);
|
|
126
|
+
if (segments.length < reserved.length || /^\{[^}]+\}$/.test(segments[0] ?? ""))
|
|
127
|
+
continue;
|
|
128
|
+
if (reserved.every((part, i) => segments[i] === part || /^\{[^}]+\}$/.test(segments[i] ?? ""))) {
|
|
129
|
+
throw new Error(`vendor route ${path} collides with adminPrefix ${prefix}; choose a different adminPrefix`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
115
133
|
var ADMIN_KEY_HEADER = "x-mockingbird-admin-key";
|
|
116
134
|
var NAMESPACE_HEADER = "x-mockingbird-namespace";
|
|
117
135
|
var json = (status, body) => new Response(JSON.stringify(body), {
|
|
118
136
|
status,
|
|
119
|
-
headers: { "content-type": "application/json" }
|
|
137
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
120
138
|
});
|
|
121
139
|
var adminError = (status, message) => json(status, { error: { type: "mockingbird_admin", message } });
|
|
122
140
|
var UNITS = {
|
|
@@ -168,11 +186,25 @@ var readJson = async (request) => {
|
|
|
168
186
|
};
|
|
169
187
|
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
170
188
|
var createControlPlane = (context) => {
|
|
189
|
+
const adminPrefix = resolveAdminPrefix(context.adminPrefix);
|
|
171
190
|
const snapshots = /* @__PURE__ */ new Map();
|
|
172
191
|
let snapshotCounter = 0;
|
|
173
192
|
const headerNamespace = (request) => request.headers.get(NAMESPACE_HEADER) ?? context.defaultNamespace;
|
|
174
193
|
const adminNamespace = (request, url) => url.searchParams.get("namespace") ?? headerNamespace(request);
|
|
194
|
+
const health = () => {
|
|
195
|
+
return json(200, {
|
|
196
|
+
...context.describe(),
|
|
197
|
+
status: "ok",
|
|
198
|
+
service: context.name,
|
|
199
|
+
uptimeMs: context.wallNow() - context.startedAt,
|
|
200
|
+
clock: context.clock.state(),
|
|
201
|
+
namespaces: context.namespaces().length,
|
|
202
|
+
adminPrefix,
|
|
203
|
+
adminUi: `${adminPrefix}/ui`
|
|
204
|
+
});
|
|
205
|
+
};
|
|
175
206
|
const builtin = {
|
|
207
|
+
"GET /health": health,
|
|
176
208
|
"GET /": () => json(200, {
|
|
177
209
|
service: context.name,
|
|
178
210
|
routes: [...Object.keys(builtin), ...Object.keys(context.routes)].sort()
|
|
@@ -183,29 +215,30 @@ var createControlPlane = (context) => {
|
|
|
183
215
|
return json(200, { status: "ok", reset: target === "*" ? context.namespaces() : [target] });
|
|
184
216
|
},
|
|
185
217
|
"GET /namespaces": () => json(200, { default: context.defaultNamespace, namespaces: context.namespaces() }),
|
|
186
|
-
"GET /clock": () => json(200, context.clock.state()),
|
|
187
|
-
"POST /clock": ({ body }) => {
|
|
218
|
+
"GET /clock": ({ namespace }) => json(200, (context.namespaceClock?.(namespace) ?? context.clock).state()),
|
|
219
|
+
"POST /clock": ({ body, namespace }) => {
|
|
220
|
+
const clock = context.namespaceClock?.(namespace) ?? context.clock;
|
|
188
221
|
if (!isRecord(body))
|
|
189
222
|
return adminError(400, "expected a JSON object");
|
|
190
223
|
if (body.reset === true)
|
|
191
|
-
|
|
224
|
+
clock.reset();
|
|
192
225
|
if (body.freeze === true)
|
|
193
|
-
|
|
226
|
+
clock.freeze();
|
|
194
227
|
if (body.set !== void 0) {
|
|
195
228
|
const instant = parseInstant(body.set);
|
|
196
229
|
if (instant === void 0)
|
|
197
230
|
return adminError(400, "set: expected epoch ms or ISO-8601");
|
|
198
|
-
|
|
231
|
+
clock.set(instant);
|
|
199
232
|
}
|
|
200
233
|
if (body.advance !== void 0) {
|
|
201
234
|
const delta = parseDuration(body.advance);
|
|
202
235
|
if (delta === void 0)
|
|
203
236
|
return adminError(400, 'advance: expected ms or "15m"-style');
|
|
204
|
-
|
|
237
|
+
clock.advance(delta);
|
|
205
238
|
}
|
|
206
239
|
if (body.freeze === false)
|
|
207
|
-
|
|
208
|
-
return json(200,
|
|
240
|
+
clock.unfreeze();
|
|
241
|
+
return json(200, clock.state());
|
|
209
242
|
},
|
|
210
243
|
"GET /faults": () => json(200, { faults: context.faults.list() }),
|
|
211
244
|
"POST /faults": ({ body, namespace }) => {
|
|
@@ -338,7 +371,14 @@ var createControlPlane = (context) => {
|
|
|
338
371
|
return json(200, { status: "ok" });
|
|
339
372
|
}
|
|
340
373
|
};
|
|
341
|
-
const
|
|
374
|
+
for (const key of Object.keys(context.routes)) {
|
|
375
|
+
if (Object.hasOwn(builtin, key))
|
|
376
|
+
throw new Error(`admin route ${key} overrides a shared route`);
|
|
377
|
+
const path = key.slice(key.indexOf(" ") + 1);
|
|
378
|
+
if (isAdminPath(path, "/ns") || isAdminPath(path, "/blobs"))
|
|
379
|
+
throw new Error(`admin route ${key} overlaps an internal transport`);
|
|
380
|
+
}
|
|
381
|
+
const routes = [...Object.entries(builtin), ...Object.entries(context.routes)].map(([key, handler]) => {
|
|
342
382
|
const space = key.indexOf(" ");
|
|
343
383
|
return { method: key.slice(0, space), pattern: key.slice(space + 1), handler };
|
|
344
384
|
});
|
|
@@ -346,21 +386,13 @@ var createControlPlane = (context) => {
|
|
|
346
386
|
namespaceOf: headerNamespace,
|
|
347
387
|
async handle(request) {
|
|
348
388
|
const url = new URL(request.url);
|
|
349
|
-
if (url.pathname ===
|
|
350
|
-
return
|
|
351
|
-
status: "ok",
|
|
352
|
-
service: context.name,
|
|
353
|
-
uptimeMs: context.wallNow() - context.startedAt,
|
|
354
|
-
clock: context.clock.state(),
|
|
355
|
-
namespaces: context.namespaces().length,
|
|
356
|
-
adminUi: `${ADMIN_PREFIX}/ui`,
|
|
357
|
-
...context.describe()
|
|
358
|
-
});
|
|
389
|
+
if (url.pathname === `${adminPrefix}/health` && request.method === "GET") {
|
|
390
|
+
return health();
|
|
359
391
|
}
|
|
360
|
-
if (url.pathname
|
|
392
|
+
if (!isAdminPath(url.pathname, adminPrefix)) {
|
|
361
393
|
return void 0;
|
|
362
394
|
}
|
|
363
|
-
const path = url.pathname.slice(
|
|
395
|
+
const path = url.pathname.slice(adminPrefix.length) || "/";
|
|
364
396
|
const shell = request.method === "GET" && (path === "/ui" || path === "/ui/");
|
|
365
397
|
if (!shell && context.adminKey !== void 0 && request.headers.get(ADMIN_KEY_HEADER) !== context.adminKey) {
|
|
366
398
|
return adminError(401, `missing or wrong ${ADMIN_KEY_HEADER}`);
|
|
@@ -368,7 +400,12 @@ var createControlPlane = (context) => {
|
|
|
368
400
|
for (const route of routes) {
|
|
369
401
|
if (route.method !== request.method)
|
|
370
402
|
continue;
|
|
371
|
-
|
|
403
|
+
let params;
|
|
404
|
+
try {
|
|
405
|
+
params = matchRoute(route.pattern, path);
|
|
406
|
+
} catch {
|
|
407
|
+
return adminError(400, "invalid path encoding");
|
|
408
|
+
}
|
|
372
409
|
if (!params)
|
|
373
410
|
continue;
|
|
374
411
|
let body;
|
|
@@ -377,15 +414,18 @@ var createControlPlane = (context) => {
|
|
|
377
414
|
} catch {
|
|
378
415
|
return adminError(400, "request body is not valid JSON");
|
|
379
416
|
}
|
|
417
|
+
const namespace = adminNamespace(request, url);
|
|
418
|
+
if (!/^[A-Za-z0-9_.-]{1,64}$/.test(namespace))
|
|
419
|
+
return adminError(400, "invalid namespace");
|
|
380
420
|
return route.handler({
|
|
381
421
|
request,
|
|
382
422
|
url,
|
|
383
423
|
params,
|
|
384
|
-
namespace
|
|
424
|
+
namespace,
|
|
385
425
|
body
|
|
386
426
|
});
|
|
387
427
|
}
|
|
388
|
-
return adminError(404, `no admin route ${request.method} ${path}; GET ${
|
|
428
|
+
return adminError(404, `no admin route ${request.method} ${path}; GET ${adminPrefix} lists them`);
|
|
389
429
|
}
|
|
390
430
|
};
|
|
391
431
|
};
|
|
@@ -475,7 +515,33 @@ var faultResponse = (rule) => {
|
|
|
475
515
|
};
|
|
476
516
|
var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) => {
|
|
477
517
|
const entries = [];
|
|
518
|
+
let isolated = false;
|
|
519
|
+
const scopedRngs = /* @__PURE__ */ new Map();
|
|
520
|
+
const random = (namespace) => {
|
|
521
|
+
if (!isolated || namespace === "default")
|
|
522
|
+
return rng;
|
|
523
|
+
let scoped = scopedRngs.get(namespace);
|
|
524
|
+
if (!scoped) {
|
|
525
|
+
scoped = createRng(rng.seed);
|
|
526
|
+
scopedRngs.set(namespace, scoped);
|
|
527
|
+
}
|
|
528
|
+
return scoped;
|
|
529
|
+
};
|
|
478
530
|
return {
|
|
531
|
+
isolateNamespaces() {
|
|
532
|
+
isolated = true;
|
|
533
|
+
},
|
|
534
|
+
snapshot(namespace) {
|
|
535
|
+
return {
|
|
536
|
+
rules: structuredClone(entries.map((entry, position) => ({
|
|
537
|
+
...entry.rule,
|
|
538
|
+
remaining: entry.remaining,
|
|
539
|
+
hits: entry.hits,
|
|
540
|
+
position
|
|
541
|
+
})).filter((entry) => entry.namespace === namespace)),
|
|
542
|
+
rngState: random(namespace).state()
|
|
543
|
+
};
|
|
544
|
+
},
|
|
479
545
|
add(rule) {
|
|
480
546
|
const existing = entries.findIndex((e) => e.rule.id === rule.id);
|
|
481
547
|
const entry = { rule, remaining: rule.count ?? null, hits: 0 };
|
|
@@ -495,6 +561,16 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
|
|
|
495
561
|
},
|
|
496
562
|
clear() {
|
|
497
563
|
entries.length = 0;
|
|
564
|
+
rng.reset();
|
|
565
|
+
scopedRngs.clear();
|
|
566
|
+
},
|
|
567
|
+
restore(namespace, state) {
|
|
568
|
+
for (let i = entries.length - 1; i >= 0; i--)
|
|
569
|
+
if (entries[i]?.rule.namespace === namespace)
|
|
570
|
+
entries.splice(i, 1);
|
|
571
|
+
for (const { remaining, hits, position, ...rule } of state.rules)
|
|
572
|
+
entries.splice(Math.min(position, entries.length), 0, { rule, remaining, hits });
|
|
573
|
+
random(namespace).setState(state.rngState);
|
|
498
574
|
},
|
|
499
575
|
async take(candidate) {
|
|
500
576
|
const hits = [];
|
|
@@ -504,7 +580,7 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
|
|
|
504
580
|
if (!matches(entry.rule, candidate))
|
|
505
581
|
continue;
|
|
506
582
|
const rate = entry.rule.rate ?? 1;
|
|
507
|
-
if (
|
|
583
|
+
if (random(candidate.namespace).next() >= rate)
|
|
508
584
|
continue;
|
|
509
585
|
entry.hits++;
|
|
510
586
|
if (entry.remaining !== null)
|
|
@@ -862,7 +938,8 @@ var annotateResponse = (response, extra) => {
|
|
|
862
938
|
var responseNotes = (response) => notes.get(response);
|
|
863
939
|
|
|
864
940
|
// ../core/dist/metrics.js
|
|
865
|
-
var createMetrics = () => {
|
|
941
|
+
var createMetrics = (partition = true) => {
|
|
942
|
+
const namespaces = /* @__PURE__ */ new Map();
|
|
866
943
|
let requests = 0;
|
|
867
944
|
let faults = 0;
|
|
868
945
|
let totalDurationMs = 0;
|
|
@@ -870,6 +947,14 @@ var createMetrics = () => {
|
|
|
870
947
|
const unmatched = /* @__PURE__ */ new Map();
|
|
871
948
|
return {
|
|
872
949
|
record(entry) {
|
|
950
|
+
if (partition) {
|
|
951
|
+
let scoped = namespaces.get(entry.namespace);
|
|
952
|
+
if (!scoped) {
|
|
953
|
+
scoped = createMetrics(false);
|
|
954
|
+
namespaces.set(entry.namespace, scoped);
|
|
955
|
+
}
|
|
956
|
+
scoped.record(entry);
|
|
957
|
+
}
|
|
873
958
|
requests++;
|
|
874
959
|
totalDurationMs += entry.durationMs;
|
|
875
960
|
if (entry.faultId !== void 0)
|
|
@@ -881,7 +966,7 @@ var createMetrics = () => {
|
|
|
881
966
|
unmatched.set(route, (unmatched.get(route) ?? 0) + 1);
|
|
882
967
|
}
|
|
883
968
|
},
|
|
884
|
-
report: () => ({
|
|
969
|
+
report: (namespace) => namespace !== void 0 ? (namespaces.get(namespace) ?? createMetrics(false)).report() : {
|
|
885
970
|
requests,
|
|
886
971
|
byOperation: Object.fromEntries([...byOperation].sort(([a], [b]) => a.localeCompare(b))),
|
|
887
972
|
unmatched: [...unmatched].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([route, count]) => {
|
|
@@ -890,8 +975,13 @@ var createMetrics = () => {
|
|
|
890
975
|
}),
|
|
891
976
|
faults,
|
|
892
977
|
totalDurationMs
|
|
893
|
-
}
|
|
894
|
-
reset() {
|
|
978
|
+
},
|
|
979
|
+
reset(namespace) {
|
|
980
|
+
if (namespace !== void 0) {
|
|
981
|
+
namespaces.delete(namespace);
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
namespaces.clear();
|
|
895
985
|
requests = 0;
|
|
896
986
|
faults = 0;
|
|
897
987
|
totalDurationMs = 0;
|
|
@@ -1504,6 +1594,34 @@ dialog:not([open]) {
|
|
|
1504
1594
|
function isRecord2(value) {
|
|
1505
1595
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1506
1596
|
}
|
|
1597
|
+
function highlightJsonText(text2) {
|
|
1598
|
+
const escapeToken = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
1599
|
+
switch (char) {
|
|
1600
|
+
case "&":
|
|
1601
|
+
return "&";
|
|
1602
|
+
case "<":
|
|
1603
|
+
return "<";
|
|
1604
|
+
case ">":
|
|
1605
|
+
return ">";
|
|
1606
|
+
case '"':
|
|
1607
|
+
return """;
|
|
1608
|
+
default:
|
|
1609
|
+
return "'";
|
|
1610
|
+
}
|
|
1611
|
+
});
|
|
1612
|
+
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;
|
|
1613
|
+
let cursor = 0;
|
|
1614
|
+
let html = "";
|
|
1615
|
+
for (const match of text2.matchAll(token)) {
|
|
1616
|
+
const index = match.index ?? 0;
|
|
1617
|
+
const value = match[0];
|
|
1618
|
+
html += escapeToken(text2.slice(cursor, index));
|
|
1619
|
+
const kind = value.startsWith('"') ? value.trimEnd().endsWith(":") ? "key" : "string" : value === "true" || value === "false" ? "boolean" : value === "null" ? "null" : "number";
|
|
1620
|
+
html += `<span class="json-${kind}">${escapeToken(value)}</span>`;
|
|
1621
|
+
cursor = index + value.length;
|
|
1622
|
+
}
|
|
1623
|
+
return html + escapeToken(text2.slice(cursor));
|
|
1624
|
+
}
|
|
1507
1625
|
function mountAdminBrand(doc, host, service, catalog) {
|
|
1508
1626
|
const clip = (value, max) => {
|
|
1509
1627
|
if (typeof value !== "string")
|
|
@@ -1865,14 +1983,16 @@ function bootAdmin(config) {
|
|
|
1865
1983
|
const state = {
|
|
1866
1984
|
namespace: "default",
|
|
1867
1985
|
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
1868
|
-
view: "overview",
|
|
1986
|
+
view: ["overview", "state", "clock", "faults", "journal", "routes"].includes(location.hash.slice(1)) ? location.hash.slice(1) : "overview",
|
|
1869
1987
|
collection: "",
|
|
1870
1988
|
shape: null,
|
|
1871
1989
|
route: null,
|
|
1990
|
+
routes: [],
|
|
1872
1991
|
editing: null
|
|
1873
1992
|
};
|
|
1874
1993
|
const standard = new Set(config.standardRoutes);
|
|
1875
1994
|
const banner = el("banner", HTMLElement);
|
|
1995
|
+
const bannerMessage = el("banner-message", HTMLElement);
|
|
1876
1996
|
const keyInput = el("admin-key", HTMLInputElement);
|
|
1877
1997
|
keyInput.value = state.key;
|
|
1878
1998
|
const params = new URLSearchParams(location.search);
|
|
@@ -1883,18 +2003,21 @@ function bootAdmin(config) {
|
|
|
1883
2003
|
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
1884
2004
|
params.delete("key");
|
|
1885
2005
|
const next = params.toString();
|
|
1886
|
-
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
2006
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : "") + location.hash);
|
|
1887
2007
|
}
|
|
1888
2008
|
const queryNamespace = params.get("namespace");
|
|
1889
2009
|
if (queryNamespace)
|
|
1890
2010
|
state.namespace = queryNamespace;
|
|
1891
2011
|
const showError = (error) => {
|
|
1892
|
-
|
|
2012
|
+
bannerMessage.textContent = error instanceof Error ? error.message : String(error);
|
|
1893
2013
|
banner.classList.add("show");
|
|
2014
|
+
banner.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
1894
2015
|
};
|
|
1895
2016
|
const clearError = () => {
|
|
1896
2017
|
banner.classList.remove("show");
|
|
2018
|
+
bannerMessage.textContent = "";
|
|
1897
2019
|
};
|
|
2020
|
+
el("banner-dismiss", HTMLButtonElement).addEventListener("click", clearError);
|
|
1898
2021
|
const api = async (method, path, body) => {
|
|
1899
2022
|
const headers = { accept: "application/json" };
|
|
1900
2023
|
if (state.key !== "")
|
|
@@ -1938,16 +2061,21 @@ function bootAdmin(config) {
|
|
|
1938
2061
|
namespace: () => state.namespace
|
|
1939
2062
|
};
|
|
1940
2063
|
const isPanelScript = (value) => typeof value === "function";
|
|
2064
|
+
const themeButton = el("theme", HTMLButtonElement);
|
|
1941
2065
|
const setTheme = (theme) => {
|
|
1942
2066
|
if (theme)
|
|
1943
2067
|
document.documentElement.dataset.theme = theme;
|
|
1944
2068
|
else
|
|
1945
2069
|
delete document.documentElement.dataset.theme;
|
|
2070
|
+
const dark = theme ? theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2071
|
+
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>';
|
|
2072
|
+
const label = dark ? "Use light theme" : "Use dark theme";
|
|
2073
|
+
themeButton.setAttribute("aria-label", label);
|
|
2074
|
+
themeButton.title = label;
|
|
1946
2075
|
};
|
|
1947
2076
|
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
2077
|
+
setTheme(storedTheme === "dark" || storedTheme === "light" ? storedTheme : null);
|
|
2078
|
+
themeButton.addEventListener("click", () => {
|
|
1951
2079
|
const current = document.documentElement.dataset.theme;
|
|
1952
2080
|
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
1953
2081
|
const next = dark ? "light" : "dark";
|
|
@@ -1966,6 +2094,7 @@ function bootAdmin(config) {
|
|
|
1966
2094
|
continue;
|
|
1967
2095
|
section.hidden = section.id !== `view-${view}`;
|
|
1968
2096
|
}
|
|
2097
|
+
history.replaceState(null, "", `${location.pathname}${location.search}#${encodeURIComponent(view)}`);
|
|
1969
2098
|
};
|
|
1970
2099
|
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
1971
2100
|
const target = event.target;
|
|
@@ -1980,7 +2109,7 @@ function bootAdmin(config) {
|
|
|
1980
2109
|
show(view);
|
|
1981
2110
|
refresh().catch(showError);
|
|
1982
2111
|
});
|
|
1983
|
-
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>`;
|
|
2112
|
+
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>`;
|
|
1984
2113
|
const table = (columns, rows) => {
|
|
1985
2114
|
if (rows.length === 0)
|
|
1986
2115
|
return '<p class="empty">Nothing here yet.</p>';
|
|
@@ -1990,16 +2119,22 @@ function bootAdmin(config) {
|
|
|
1990
2119
|
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
1991
2120
|
return `<tr${attrs}>${cells}</tr>`;
|
|
1992
2121
|
}).join("");
|
|
1993
|
-
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
2122
|
+
return `<div class="table-shell"><table><thead>${head}</thead><tbody>${body}</tbody></table></div>`;
|
|
1994
2123
|
};
|
|
1995
2124
|
const preview = (value) => {
|
|
1996
2125
|
const text2 = JSON.stringify(value) ?? "null";
|
|
1997
|
-
|
|
2126
|
+
const clipped = text2.length > 180 ? `${text2.slice(0, 180)}\u2026` : text2;
|
|
2127
|
+
return `<code class="json-inline">${highlightJsonText(clipped)}</code>`;
|
|
1998
2128
|
};
|
|
1999
2129
|
const when = (epoch) => {
|
|
2000
2130
|
const date = new Date(epoch);
|
|
2001
2131
|
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
2002
2132
|
};
|
|
2133
|
+
const journalWhen = (value) => {
|
|
2134
|
+
const numeric = value.trim() === "" ? Number.NaN : Number(value);
|
|
2135
|
+
const epoch = Number.isFinite(numeric) ? numeric : Date.parse(value);
|
|
2136
|
+
return Number.isFinite(epoch) ? when(epoch) : value;
|
|
2137
|
+
};
|
|
2003
2138
|
const clockCard = (epoch) => {
|
|
2004
2139
|
const date = new Date(epoch);
|
|
2005
2140
|
if (Number.isNaN(date.getTime()))
|
|
@@ -2119,7 +2254,7 @@ function bootAdmin(config) {
|
|
|
2119
2254
|
};
|
|
2120
2255
|
const renderOverview = async () => {
|
|
2121
2256
|
const [health, shape, clock, journal] = await Promise.all([
|
|
2122
|
-
originGet(
|
|
2257
|
+
originGet(`${config.adminPrefix}/health`),
|
|
2123
2258
|
api("GET", "/state"),
|
|
2124
2259
|
api("GET", "/clock"),
|
|
2125
2260
|
api("GET", "/requests?limit=8")
|
|
@@ -2136,7 +2271,7 @@ function bootAdmin(config) {
|
|
|
2136
2271
|
},
|
|
2137
2272
|
{
|
|
2138
2273
|
k: "Status",
|
|
2139
|
-
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
2274
|
+
v: `<span class="badge ok">${esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")}</span>`
|
|
2140
2275
|
},
|
|
2141
2276
|
{ k: "Namespace", v: esc(state.namespace) },
|
|
2142
2277
|
{ k: "Collections", v: String(decoded.collections.length) },
|
|
@@ -2223,26 +2358,46 @@ function bootAdmin(config) {
|
|
|
2223
2358
|
const renderJournal = async () => {
|
|
2224
2359
|
const journal = await api("GET", "/requests?limit=100");
|
|
2225
2360
|
el("journal", HTMLElement).innerHTML = table([
|
|
2226
|
-
{ label: "When", cell: (row) => esc(row.at) },
|
|
2227
|
-
{
|
|
2361
|
+
{ label: "When", cell: (row) => esc(journalWhen(row.at)) },
|
|
2362
|
+
{
|
|
2363
|
+
label: "Method",
|
|
2364
|
+
cell: (row) => `<span class="method">${esc(row.method)}</span>`
|
|
2365
|
+
},
|
|
2228
2366
|
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2229
|
-
{
|
|
2367
|
+
{
|
|
2368
|
+
label: "Status",
|
|
2369
|
+
cell: (row) => `<span class="badge ${row.status.startsWith("2") ? "ok" : row.status.startsWith("4") || row.status.startsWith("5") ? "warn" : ""}">${esc(row.status)}</span>`
|
|
2370
|
+
},
|
|
2230
2371
|
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
2231
2372
|
], decodeJournal(journal));
|
|
2232
2373
|
};
|
|
2233
|
-
const
|
|
2234
|
-
const
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2374
|
+
const paintRoutes = () => {
|
|
2375
|
+
const query = el("route-search", HTMLInputElement).value.trim().toLocaleLowerCase();
|
|
2376
|
+
const routes = state.routes.filter((route) => route.toLocaleLowerCase().includes(query));
|
|
2377
|
+
el("route-count", HTMLElement).textContent = `${routes.length} of ${state.routes.length}`;
|
|
2378
|
+
if (state.routes.length === 0) {
|
|
2379
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No admin routes are available.</p>';
|
|
2380
|
+
return;
|
|
2381
|
+
}
|
|
2382
|
+
if (routes.length === 0) {
|
|
2383
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No routes match this filter.</p>';
|
|
2384
|
+
return;
|
|
2385
|
+
}
|
|
2238
2386
|
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2239
2387
|
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2240
2388
|
const parts = route.split(" ");
|
|
2241
2389
|
const method = parts[0] ?? "";
|
|
2242
2390
|
const path = parts.slice(1).join(" ");
|
|
2243
|
-
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>`;
|
|
2391
|
+
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>`;
|
|
2244
2392
|
}).join("");
|
|
2245
2393
|
};
|
|
2394
|
+
const renderRoutes = async () => {
|
|
2395
|
+
const data = await api("GET", "/");
|
|
2396
|
+
if (!isRecord2(data))
|
|
2397
|
+
throw new Error("Route list was not an object");
|
|
2398
|
+
state.routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
2399
|
+
paintRoutes();
|
|
2400
|
+
};
|
|
2246
2401
|
const renderers = {
|
|
2247
2402
|
overview: renderOverview,
|
|
2248
2403
|
state: renderState,
|
|
@@ -2251,19 +2406,50 @@ function bootAdmin(config) {
|
|
|
2251
2406
|
journal: renderJournal,
|
|
2252
2407
|
routes: renderRoutes
|
|
2253
2408
|
};
|
|
2409
|
+
let refreshToken = 0;
|
|
2254
2410
|
async function refresh() {
|
|
2411
|
+
const token = ++refreshToken;
|
|
2255
2412
|
clearError();
|
|
2413
|
+
const main = el("views", HTMLElement);
|
|
2414
|
+
const sync = el("sync-status", HTMLElement);
|
|
2415
|
+
main.setAttribute("aria-busy", "true");
|
|
2416
|
+
sync.classList.add("busy");
|
|
2417
|
+
sync.textContent = "Updating";
|
|
2256
2418
|
const render = renderers[state.view];
|
|
2257
|
-
|
|
2258
|
-
|
|
2419
|
+
try {
|
|
2420
|
+
if (render)
|
|
2421
|
+
await render();
|
|
2422
|
+
if (token === refreshToken)
|
|
2423
|
+
sync.textContent = "Up to date";
|
|
2424
|
+
} finally {
|
|
2425
|
+
if (token === refreshToken) {
|
|
2426
|
+
main.removeAttribute("aria-busy");
|
|
2427
|
+
sync.classList.remove("busy");
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2259
2430
|
}
|
|
2260
2431
|
const delay = (ms) => new Promise((resolve) => {
|
|
2261
2432
|
window.setTimeout(resolve, ms);
|
|
2262
2433
|
});
|
|
2263
|
-
const confirmAction = async (button, pending, done, work) => {
|
|
2434
|
+
const confirmAction = async (button, pending, done, work, confirmLabel) => {
|
|
2264
2435
|
if (button.dataset.pending === "1")
|
|
2265
2436
|
return;
|
|
2266
2437
|
const label = button.textContent ?? "";
|
|
2438
|
+
if (confirmLabel !== void 0 && button.dataset.confirming !== "1") {
|
|
2439
|
+
button.dataset.confirming = "1";
|
|
2440
|
+
button.classList.add("confirming");
|
|
2441
|
+
button.textContent = confirmLabel;
|
|
2442
|
+
window.setTimeout(() => {
|
|
2443
|
+
if (button.dataset.confirming !== "1")
|
|
2444
|
+
return;
|
|
2445
|
+
button.textContent = label;
|
|
2446
|
+
button.classList.remove("confirming");
|
|
2447
|
+
delete button.dataset.confirming;
|
|
2448
|
+
}, 3e3);
|
|
2449
|
+
return;
|
|
2450
|
+
}
|
|
2451
|
+
button.classList.remove("confirming");
|
|
2452
|
+
delete button.dataset.confirming;
|
|
2267
2453
|
const width = button.offsetWidth;
|
|
2268
2454
|
button.dataset.pending = "1";
|
|
2269
2455
|
button.classList.add("pressed");
|
|
@@ -2284,6 +2470,65 @@ function bootAdmin(config) {
|
|
|
2284
2470
|
delete button.dataset.pending;
|
|
2285
2471
|
}
|
|
2286
2472
|
};
|
|
2473
|
+
const wireJsonEditor = (textareaId, highlightId, statusId, shellId, allowEmpty) => {
|
|
2474
|
+
const textarea = el(textareaId, HTMLTextAreaElement);
|
|
2475
|
+
const highlight = el(highlightId, HTMLElement);
|
|
2476
|
+
const status = el(statusId, HTMLElement);
|
|
2477
|
+
const shell = el(shellId, HTMLElement);
|
|
2478
|
+
const sync = () => {
|
|
2479
|
+
const text2 = textarea.value;
|
|
2480
|
+
highlight.innerHTML = `${highlightJsonText(text2)}
|
|
2481
|
+
`;
|
|
2482
|
+
let valid = true;
|
|
2483
|
+
let message = "Valid JSON";
|
|
2484
|
+
if (allowEmpty && text2.trim() === "")
|
|
2485
|
+
message = "No body";
|
|
2486
|
+
else {
|
|
2487
|
+
try {
|
|
2488
|
+
readJson2(text2);
|
|
2489
|
+
} catch {
|
|
2490
|
+
valid = false;
|
|
2491
|
+
message = "Invalid JSON";
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
shell.classList.toggle("invalid", !valid);
|
|
2495
|
+
status.classList.toggle("invalid", !valid);
|
|
2496
|
+
status.textContent = message;
|
|
2497
|
+
textarea.setAttribute("aria-invalid", valid ? "false" : "true");
|
|
2498
|
+
};
|
|
2499
|
+
const syncScroll = () => {
|
|
2500
|
+
const preview2 = highlight.parentElement;
|
|
2501
|
+
if (!(preview2 instanceof HTMLElement))
|
|
2502
|
+
return;
|
|
2503
|
+
preview2.scrollTop = textarea.scrollTop;
|
|
2504
|
+
preview2.scrollLeft = textarea.scrollLeft;
|
|
2505
|
+
};
|
|
2506
|
+
textarea.addEventListener("input", sync);
|
|
2507
|
+
textarea.addEventListener("scroll", syncScroll);
|
|
2508
|
+
sync();
|
|
2509
|
+
return sync;
|
|
2510
|
+
};
|
|
2511
|
+
const syncRouteEditor = wireJsonEditor("route-body", "route-highlight", "route-json-status", "route-editor", true);
|
|
2512
|
+
const syncRecordEditor = wireJsonEditor("editor-value", "editor-highlight", "editor-json-status", "record-editor", false);
|
|
2513
|
+
for (const button of document.querySelectorAll("[data-format]")) {
|
|
2514
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2515
|
+
continue;
|
|
2516
|
+
button.addEventListener("click", () => {
|
|
2517
|
+
const target = button.dataset.format;
|
|
2518
|
+
if (target === void 0)
|
|
2519
|
+
return;
|
|
2520
|
+
const textarea = document.getElementById(target);
|
|
2521
|
+
if (!(textarea instanceof HTMLTextAreaElement) || textarea.value.trim() === "")
|
|
2522
|
+
return;
|
|
2523
|
+
try {
|
|
2524
|
+
textarea.value = JSON.stringify(readJson2(textarea.value), null, 2) ?? "";
|
|
2525
|
+
textarea.dispatchEvent(new Event("input"));
|
|
2526
|
+
textarea.focus();
|
|
2527
|
+
} catch {
|
|
2528
|
+
showError(new Error("Fix the JSON before formatting it"));
|
|
2529
|
+
}
|
|
2530
|
+
});
|
|
2531
|
+
}
|
|
2287
2532
|
document.addEventListener("click", (event) => {
|
|
2288
2533
|
const target = event.target;
|
|
2289
2534
|
if (!(target instanceof Element))
|
|
@@ -2356,6 +2601,7 @@ function bootAdmin(config) {
|
|
|
2356
2601
|
} else {
|
|
2357
2602
|
editorValue.value = "{\n \n}";
|
|
2358
2603
|
}
|
|
2604
|
+
syncRecordEditor();
|
|
2359
2605
|
editor.showModal();
|
|
2360
2606
|
}
|
|
2361
2607
|
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -2385,10 +2631,10 @@ function bootAdmin(config) {
|
|
|
2385
2631
|
const id = state.editing;
|
|
2386
2632
|
if (!id)
|
|
2387
2633
|
return;
|
|
2388
|
-
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2634
|
+
confirmAction(editorDelete, "Deleting...", "Deleted", () => api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2389
2635
|
editor.close();
|
|
2390
2636
|
return renderState();
|
|
2391
|
-
}).catch(showError);
|
|
2637
|
+
}), "Confirm delete").catch(showError);
|
|
2392
2638
|
});
|
|
2393
2639
|
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2394
2640
|
if (!(button instanceof HTMLButtonElement))
|
|
@@ -2434,10 +2680,10 @@ function bootAdmin(config) {
|
|
|
2434
2680
|
}).then(() => renderFaults()).catch(showError);
|
|
2435
2681
|
});
|
|
2436
2682
|
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2437
|
-
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
|
|
2683
|
+
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults()), "Confirm clear").catch(showError);
|
|
2438
2684
|
});
|
|
2439
2685
|
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2440
|
-
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
|
|
2686
|
+
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal()), "Confirm clear").catch(showError);
|
|
2441
2687
|
});
|
|
2442
2688
|
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2443
2689
|
const target = event.target;
|
|
@@ -2451,9 +2697,15 @@ function bootAdmin(config) {
|
|
|
2451
2697
|
return;
|
|
2452
2698
|
state.route = route;
|
|
2453
2699
|
el("route-title", HTMLElement).textContent = route;
|
|
2700
|
+
el("route-send", HTMLButtonElement).disabled = false;
|
|
2454
2701
|
const method = route.split(" ")[0] ?? "";
|
|
2455
|
-
el("route-body", HTMLTextAreaElement)
|
|
2702
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
2703
|
+
routeBody.disabled = method === "GET" || method === "DELETE";
|
|
2704
|
+
el("route-editor", HTMLElement).hidden = routeBody.disabled;
|
|
2705
|
+
el("route-json-status", HTMLElement).parentElement?.toggleAttribute("hidden", routeBody.disabled);
|
|
2706
|
+
syncRouteEditor();
|
|
2456
2707
|
});
|
|
2708
|
+
el("route-search", HTMLInputElement).addEventListener("input", paintRoutes);
|
|
2457
2709
|
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2458
2710
|
event.preventDefault();
|
|
2459
2711
|
const send = async () => {
|
|
@@ -2475,7 +2727,8 @@ function bootAdmin(config) {
|
|
|
2475
2727
|
}
|
|
2476
2728
|
}
|
|
2477
2729
|
const data = await api(method, concrete, body);
|
|
2478
|
-
|
|
2730
|
+
const text2 = JSON.stringify(data, null, 2) ?? "null";
|
|
2731
|
+
el("route-result", HTMLElement).innerHTML = `<code>${highlightJsonText(text2)}</code>`;
|
|
2479
2732
|
};
|
|
2480
2733
|
send().catch(showError);
|
|
2481
2734
|
});
|
|
@@ -2593,11 +2846,13 @@ function bootAdmin(config) {
|
|
|
2593
2846
|
views.append(section);
|
|
2594
2847
|
mountSqlExplorer(body, panelApi);
|
|
2595
2848
|
}
|
|
2849
|
+
show(document.getElementById(`view-${state.view}`) === null ? "overview" : state.view);
|
|
2596
2850
|
return refresh();
|
|
2597
2851
|
}).catch(showError);
|
|
2598
2852
|
}
|
|
2599
2853
|
var clientPrelude = [
|
|
2600
2854
|
isRecord2,
|
|
2855
|
+
highlightJsonText,
|
|
2601
2856
|
mountAdminBrand,
|
|
2602
2857
|
startAdminBrand,
|
|
2603
2858
|
faultPresetList,
|
|
@@ -2620,6 +2875,7 @@ ${entry}(${payload});
|
|
|
2620
2875
|
var defineMock = (runtime) => runtime;
|
|
2621
2876
|
var STANDARD_ADMIN_ROUTES = [
|
|
2622
2877
|
"GET /",
|
|
2878
|
+
"GET /health",
|
|
2623
2879
|
"POST /reset",
|
|
2624
2880
|
"GET /namespaces",
|
|
2625
2881
|
"GET /clock",
|
|
@@ -2699,11 +2955,11 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
2699
2955
|
}
|
|
2700
2956
|
});
|
|
2701
2957
|
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
2702
|
-
var renderAdminDocument = (service) => {
|
|
2958
|
+
var renderAdminDocument = (service, adminPrefix = ADMIN_PREFIX) => {
|
|
2703
2959
|
const name = escapeHtml(service);
|
|
2704
2960
|
const script = adminClientSource({
|
|
2705
2961
|
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
2706
|
-
adminPrefix
|
|
2962
|
+
adminPrefix,
|
|
2707
2963
|
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
2708
2964
|
brandsUrl: ADMIN_BRANDS_URL,
|
|
2709
2965
|
service
|
|
@@ -2720,75 +2976,99 @@ var renderAdminDocument = (service) => {
|
|
|
2720
2976
|
${CSS_RESET}
|
|
2721
2977
|
:root {
|
|
2722
2978
|
color-scheme: light;
|
|
2723
|
-
--bg: #
|
|
2979
|
+
--bg: #f7f8fb;
|
|
2724
2980
|
--bg-elev: #ffffff;
|
|
2725
|
-
--bg-muted: #
|
|
2726
|
-
--bg-inset: #
|
|
2727
|
-
--line: #
|
|
2728
|
-
--line-strong: #
|
|
2729
|
-
--ink: #
|
|
2730
|
-
--muted: #
|
|
2731
|
-
--faint: #
|
|
2732
|
-
--accent: #
|
|
2981
|
+
--bg-muted: #f1f3f7;
|
|
2982
|
+
--bg-inset: #f8f9fc;
|
|
2983
|
+
--line: #e4e7ec;
|
|
2984
|
+
--line-strong: #cfd4dc;
|
|
2985
|
+
--ink: #101828;
|
|
2986
|
+
--muted: #475467;
|
|
2987
|
+
--faint: #667085;
|
|
2988
|
+
--accent: #6941c6;
|
|
2989
|
+
--accent-hover: #5933b4;
|
|
2733
2990
|
--accent-ink: #ffffff;
|
|
2734
|
-
--accent-soft: #
|
|
2735
|
-
--ok: #
|
|
2736
|
-
--ok-soft: #
|
|
2737
|
-
--warn: #
|
|
2738
|
-
--warn-soft: #
|
|
2739
|
-
--err: #
|
|
2740
|
-
--err-soft: #
|
|
2741
|
-
--
|
|
2991
|
+
--accent-soft: #f4f0ff;
|
|
2992
|
+
--ok: #067647;
|
|
2993
|
+
--ok-soft: #ecfdf3;
|
|
2994
|
+
--warn: #b54708;
|
|
2995
|
+
--warn-soft: #fffaeb;
|
|
2996
|
+
--err: #b42318;
|
|
2997
|
+
--err-soft: #fef3f2;
|
|
2998
|
+
--code-key: #6941c6;
|
|
2999
|
+
--code-string: #067647;
|
|
3000
|
+
--code-number: #175cd3;
|
|
3001
|
+
--code-boolean: #c11574;
|
|
3002
|
+
--code-null: #667085;
|
|
3003
|
+
--shadow-xs: 0 1px 2px rgb(16 24 40 / 0.05);
|
|
3004
|
+
--shadow-sm: 0 1px 3px rgb(16 24 40 / 0.08), 0 1px 2px rgb(16 24 40 / 0.04);
|
|
3005
|
+
--shadow-lg: 0 18px 48px rgb(16 24 40 / 0.16), 0 4px 12px rgb(16 24 40 / 0.08);
|
|
2742
3006
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2743
3007
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
2744
|
-
--radius:
|
|
2745
|
-
--header:
|
|
2746
|
-
--nav:
|
|
3008
|
+
--radius: 12px;
|
|
3009
|
+
--header: 72px;
|
|
3010
|
+
--nav: 224px;
|
|
2747
3011
|
}
|
|
2748
3012
|
:root[data-theme="dark"] {
|
|
2749
3013
|
color-scheme: dark;
|
|
2750
|
-
--bg: #
|
|
2751
|
-
--bg-elev: #
|
|
2752
|
-
--bg-muted: #
|
|
2753
|
-
--bg-inset: #
|
|
2754
|
-
--line: #
|
|
2755
|
-
--line-strong: #
|
|
2756
|
-
--ink: #
|
|
2757
|
-
--muted: #
|
|
2758
|
-
--faint: #
|
|
2759
|
-
--accent: #
|
|
2760
|
-
--accent-
|
|
2761
|
-
--accent-
|
|
2762
|
-
--
|
|
2763
|
-
--ok
|
|
2764
|
-
--
|
|
2765
|
-
--warn
|
|
2766
|
-
--
|
|
2767
|
-
--err
|
|
2768
|
-
--
|
|
3014
|
+
--bg: #0c0e14;
|
|
3015
|
+
--bg-elev: #151821;
|
|
3016
|
+
--bg-muted: #20232e;
|
|
3017
|
+
--bg-inset: #11141c;
|
|
3018
|
+
--line: #2b303d;
|
|
3019
|
+
--line-strong: #414858;
|
|
3020
|
+
--ink: #f5f7fa;
|
|
3021
|
+
--muted: #b6bdc9;
|
|
3022
|
+
--faint: #8d96a7;
|
|
3023
|
+
--accent: #9e77ed;
|
|
3024
|
+
--accent-hover: #b692f6;
|
|
3025
|
+
--accent-ink: #ffffff;
|
|
3026
|
+
--accent-soft: #2b2142;
|
|
3027
|
+
--ok: #75e0a7;
|
|
3028
|
+
--ok-soft: #102a20;
|
|
3029
|
+
--warn: #fec84b;
|
|
3030
|
+
--warn-soft: #302611;
|
|
3031
|
+
--err: #fda29b;
|
|
3032
|
+
--err-soft: #321817;
|
|
3033
|
+
--code-key: #c3a6ff;
|
|
3034
|
+
--code-string: #75e0a7;
|
|
3035
|
+
--code-number: #84adff;
|
|
3036
|
+
--code-boolean: #f9a8d4;
|
|
3037
|
+
--code-null: #98a2b3;
|
|
3038
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3039
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3040
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2769
3041
|
}
|
|
2770
3042
|
@media (prefers-color-scheme: dark) {
|
|
2771
3043
|
:root:not([data-theme="light"]) {
|
|
2772
3044
|
color-scheme: dark;
|
|
2773
|
-
--bg: #
|
|
2774
|
-
--bg-elev: #
|
|
2775
|
-
--bg-muted: #
|
|
2776
|
-
--bg-inset: #
|
|
2777
|
-
--line: #
|
|
2778
|
-
--line-strong: #
|
|
2779
|
-
--ink: #
|
|
2780
|
-
--muted: #
|
|
2781
|
-
--faint: #
|
|
2782
|
-
--accent: #
|
|
2783
|
-
--accent-
|
|
2784
|
-
--accent-
|
|
2785
|
-
--
|
|
2786
|
-
--ok
|
|
2787
|
-
--
|
|
2788
|
-
--warn
|
|
2789
|
-
--
|
|
2790
|
-
--err
|
|
2791
|
-
--
|
|
3045
|
+
--bg: #0c0e14;
|
|
3046
|
+
--bg-elev: #151821;
|
|
3047
|
+
--bg-muted: #20232e;
|
|
3048
|
+
--bg-inset: #11141c;
|
|
3049
|
+
--line: #2b303d;
|
|
3050
|
+
--line-strong: #414858;
|
|
3051
|
+
--ink: #f5f7fa;
|
|
3052
|
+
--muted: #b6bdc9;
|
|
3053
|
+
--faint: #8d96a7;
|
|
3054
|
+
--accent: #9e77ed;
|
|
3055
|
+
--accent-hover: #b692f6;
|
|
3056
|
+
--accent-ink: #ffffff;
|
|
3057
|
+
--accent-soft: #2b2142;
|
|
3058
|
+
--ok: #75e0a7;
|
|
3059
|
+
--ok-soft: #102a20;
|
|
3060
|
+
--warn: #fec84b;
|
|
3061
|
+
--warn-soft: #302611;
|
|
3062
|
+
--err: #fda29b;
|
|
3063
|
+
--err-soft: #321817;
|
|
3064
|
+
--code-key: #c3a6ff;
|
|
3065
|
+
--code-string: #75e0a7;
|
|
3066
|
+
--code-number: #84adff;
|
|
3067
|
+
--code-boolean: #f9a8d4;
|
|
3068
|
+
--code-null: #98a2b3;
|
|
3069
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3070
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3071
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2792
3072
|
}
|
|
2793
3073
|
}
|
|
2794
3074
|
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
@@ -2806,13 +3086,17 @@ var renderAdminDocument = (service) => {
|
|
|
2806
3086
|
padding-right: env(safe-area-inset-right);
|
|
2807
3087
|
-webkit-text-size-adjust: 100%;
|
|
2808
3088
|
}
|
|
2809
|
-
|
|
3089
|
+
::selection { background: color-mix(in srgb, var(--accent) 28%, transparent); }
|
|
3090
|
+
:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px; }
|
|
3091
|
+
button, input, select, textarea { transition: border-color 140ms ease, box-shadow 140ms ease, transform 100ms ease; }
|
|
3092
|
+
button:disabled, input:disabled, select:disabled, textarea:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
2810
3093
|
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
2811
3094
|
header.top {
|
|
2812
|
-
position: sticky; top: 0; z-index:
|
|
3095
|
+
position: sticky; top: 0; z-index: 20;
|
|
2813
3096
|
display: flex; align-items: center; gap: 12px;
|
|
2814
|
-
min-height: var(--header); padding:
|
|
2815
|
-
background: var(--bg-elev);
|
|
3097
|
+
min-height: var(--header); padding: 12px 20px;
|
|
3098
|
+
background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
|
|
3099
|
+
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
2816
3100
|
border-bottom: 1px solid var(--line);
|
|
2817
3101
|
min-width: 0; max-width: 100%;
|
|
2818
3102
|
}
|
|
@@ -2820,12 +3104,21 @@ var renderAdminDocument = (service) => {
|
|
|
2820
3104
|
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
2821
3105
|
min-width: 0; max-width: 100%; margin-right: auto;
|
|
2822
3106
|
}
|
|
2823
|
-
.brand-
|
|
2824
|
-
.brand-
|
|
3107
|
+
.brand-lockup { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
3108
|
+
.brand-mark {
|
|
3109
|
+
width: 38px; height: 38px; display: grid; place-items: center; flex: none;
|
|
3110
|
+
border-radius: 11px; color: #fff;
|
|
3111
|
+
background: linear-gradient(145deg, #7f56d9 0%, #53389e 100%);
|
|
3112
|
+
box-shadow: 0 6px 16px rgb(105 65 198 / 0.24), inset 0 1px 0 rgb(255 255 255 / 0.25);
|
|
3113
|
+
}
|
|
3114
|
+
.brand-mark svg { width: 22px; height: 22px; }
|
|
3115
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; line-height: 1.25; }
|
|
3116
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
2825
3117
|
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
2826
3118
|
.vendor {
|
|
2827
3119
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
2828
3120
|
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
3121
|
+
padding: 5px 8px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-inset);
|
|
2829
3122
|
}
|
|
2830
3123
|
.vendor[hidden] { display: none; }
|
|
2831
3124
|
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
@@ -2837,12 +3130,17 @@ var renderAdminDocument = (service) => {
|
|
|
2837
3130
|
.vendor-sep { color: var(--faint); }
|
|
2838
3131
|
.vendor-link { color: inherit; text-decoration: none; }
|
|
2839
3132
|
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
2840
|
-
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items:
|
|
2841
|
-
|
|
3133
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
|
|
3134
|
+
.sync-status { align-self: center; display: flex; align-items: center; gap: 6px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3135
|
+
.sync-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
|
|
3136
|
+
.sync-status.busy::before { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); animation: pulse 1.2s ease-in-out infinite; }
|
|
3137
|
+
label.field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; font-weight: 600; color: var(--faint); }
|
|
2842
3138
|
label.field input, label.field select, .controls input[type="password"] {
|
|
2843
|
-
background-color: var(--bg-
|
|
2844
|
-
padding:
|
|
3139
|
+
background-color: var(--bg-elev); border: 1px solid var(--line-strong); border-radius: 9px;
|
|
3140
|
+
padding: 8px 10px; min-height: 38px; line-height: 1.2; box-shadow: var(--shadow-xs);
|
|
2845
3141
|
}
|
|
3142
|
+
label.field input:hover, label.field select:hover, textarea:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--line-strong)); }
|
|
3143
|
+
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; }
|
|
2846
3144
|
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
2847
3145
|
the text fields in the same form. Paint it like those fields. */
|
|
2848
3146
|
label.field select {
|
|
@@ -2859,12 +3157,20 @@ var renderAdminDocument = (service) => {
|
|
|
2859
3157
|
dialog .field > textarea { width: 100%; }
|
|
2860
3158
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
2861
3159
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
.btn
|
|
3160
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
|
3161
|
+
border-radius: 9px; min-height: 38px; padding: 0 13px; border: 1px solid var(--line-strong);
|
|
3162
|
+
background: var(--bg-elev); color: var(--ink); font-size: 13px; font-weight: 600; line-height: 1;
|
|
3163
|
+
box-shadow: var(--shadow-xs);
|
|
3164
|
+
}
|
|
3165
|
+
.icon-btn { width: 38px; padding: 0; }
|
|
3166
|
+
.icon-btn svg, .btn svg, .btn-primary svg { width: 16px; height: 16px; }
|
|
3167
|
+
.icon-btn:hover, .btn:hover { background: var(--bg-muted); border-color: var(--line-strong); }
|
|
3168
|
+
.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; }
|
|
3169
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
3170
|
+
.btn-quiet { background: transparent; border-color: transparent; box-shadow: none; }
|
|
3171
|
+
.btn-quiet:hover { background: var(--bg-muted); }
|
|
3172
|
+
.btn-danger { color: var(--err); border-color: color-mix(in srgb, var(--err) 25%, var(--line)); }
|
|
3173
|
+
.btn-danger:hover, .btn-danger.confirming { background: var(--err-soft); border-color: color-mix(in srgb, var(--err) 45%, var(--line)); }
|
|
2868
3174
|
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
2869
3175
|
so the press itself has to stay visible. */
|
|
2870
3176
|
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
@@ -2880,93 +3186,159 @@ var renderAdminDocument = (service) => {
|
|
|
2880
3186
|
flex: 1 0 auto; min-width: 0;
|
|
2881
3187
|
}
|
|
2882
3188
|
nav.side {
|
|
2883
|
-
|
|
3189
|
+
position: sticky; top: var(--header); align-self: start; height: calc(100vh - var(--header));
|
|
3190
|
+
border-right: 1px solid var(--line); padding: 18px 12px;
|
|
2884
3191
|
display: flex; flex-direction: column; gap: 4px;
|
|
2885
|
-
background: var(--bg-elev);
|
|
3192
|
+
background: var(--bg-elev); overflow-y: auto;
|
|
2886
3193
|
}
|
|
3194
|
+
.nav-label { padding: 0 10px 8px; color: var(--faint); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
|
|
2887
3195
|
nav.side button {
|
|
3196
|
+
position: relative; display: flex; align-items: center; gap: 10px; width: 100%;
|
|
2888
3197
|
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
2889
|
-
padding: 9px 10px; min-height:
|
|
2890
|
-
}
|
|
2891
|
-
nav.side button
|
|
2892
|
-
|
|
2893
|
-
.
|
|
2894
|
-
.
|
|
2895
|
-
.
|
|
3198
|
+
padding: 9px 10px; min-height: 42px; color: var(--muted); font-size: 13px; font-weight: 550;
|
|
3199
|
+
}
|
|
3200
|
+
nav.side button:hover { background: var(--bg-muted); color: var(--ink); }
|
|
3201
|
+
nav.side button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 650; }
|
|
3202
|
+
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); }
|
|
3203
|
+
.nav-icon { width: 18px; height: 18px; display: grid; place-items: center; flex: none; }
|
|
3204
|
+
.nav-icon svg { width: 18px; height: 18px; }
|
|
3205
|
+
main { padding: 32px clamp(20px, 4vw, 48px) 64px; max-width: 1320px; width: 100%; min-width: 0; }
|
|
3206
|
+
main[aria-busy="true"] .view { opacity: 0.62; }
|
|
3207
|
+
.view { animation: view-in 180ms ease-out; }
|
|
3208
|
+
.view[hidden] { display: none; }
|
|
3209
|
+
.view-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
|
|
3210
|
+
.view h2 { margin: 0 0 5px; font-size: clamp(24px, 3vw, 30px); line-height: 1.2; letter-spacing: -0.035em; }
|
|
3211
|
+
.view h3 { margin: 24px 0 10px; font-size: 15px; letter-spacing: -0.01em; }
|
|
3212
|
+
.lede { margin: 0; color: var(--muted); max-width: 72ch; font-size: 14px; }
|
|
3213
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 175px), 1fr)); gap: 14px; }
|
|
2896
3214
|
.card {
|
|
2897
3215
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
2898
|
-
padding:
|
|
3216
|
+
padding: 18px; box-shadow: var(--shadow-sm);
|
|
2899
3217
|
}
|
|
2900
|
-
.card
|
|
2901
|
-
.card
|
|
3218
|
+
.stat-card { position: relative; overflow: hidden; }
|
|
3219
|
+
.stat-card::after { content: ""; position: absolute; inset: 0 0 auto; height: 2px; background: linear-gradient(90deg, var(--accent), transparent 78%); opacity: 0.7; }
|
|
3220
|
+
.card .k { color: var(--faint); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
3221
|
+
.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; }
|
|
2902
3222
|
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
2903
|
-
.
|
|
3223
|
+
.section-card { background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-xs); overflow: hidden; }
|
|
3224
|
+
.section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
|
|
3225
|
+
.section-title h3 { margin: 0; }
|
|
3226
|
+
.split { display: grid; grid-template-columns: minmax(0, 250px) minmax(0, 1fr); gap: 18px; align-items: start; }
|
|
3227
|
+
#collections { padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); box-shadow: var(--shadow-xs); }
|
|
2904
3228
|
.collection {
|
|
2905
3229
|
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
2906
|
-
border-radius:
|
|
3230
|
+
border-radius: 9px; padding: 10px 11px; min-height: 48px; color: var(--ink); cursor: pointer;
|
|
2907
3231
|
}
|
|
2908
|
-
.collection
|
|
3232
|
+
.collection:hover { background: var(--bg-muted); }
|
|
3233
|
+
.collection[aria-current="true"] { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 18%, var(--line)); color: var(--accent); }
|
|
2909
3234
|
.collection small { display: block; color: var(--faint); }
|
|
2910
3235
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
2911
|
-
.chip {
|
|
2912
|
-
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
2913
|
-
border-radius: 999px; padding: 4px 8px;
|
|
2914
|
-
}
|
|
3236
|
+
.chip { font-family: var(--mono); font-size: 11px; background: var(--bg-muted); border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; }
|
|
2915
3237
|
.chip i { color: var(--faint); font-style: normal; }
|
|
2916
3238
|
.sql-layout {
|
|
2917
3239
|
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
2918
3240
|
}
|
|
2919
|
-
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
2920
|
-
.sql-tables button { text-align: left; }
|
|
3241
|
+
.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); }
|
|
3242
|
+
.sql-tables button { text-align: left; border: 0; border-radius: 8px; padding: 9px 10px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
3243
|
+
.sql-tables button:hover, .sql-tables button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); }
|
|
2921
3244
|
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
2922
|
-
table { width: 100%; border-
|
|
2923
|
-
|
|
2924
|
-
th {
|
|
3245
|
+
.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); }
|
|
3246
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
3247
|
+
th, td { text-align: left; padding: 11px 13px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
3248
|
+
th { color: var(--faint); background: var(--bg-inset); font-weight: 650; font-size: 11px; text-transform: uppercase; letter-spacing: 0.045em; }
|
|
3249
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
2925
3250
|
tr.clickable { cursor: pointer; }
|
|
2926
|
-
tr.clickable:hover { background: var(--
|
|
3251
|
+
tr.clickable:hover { background: var(--accent-soft); }
|
|
2927
3252
|
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
2928
3253
|
pre {
|
|
2929
3254
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
2930
|
-
background: var(--bg-inset); border-radius: 10px; padding:
|
|
2931
|
-
}
|
|
2932
|
-
.
|
|
3255
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 10px; padding: 14px; font-size: 12px; line-height: 1.65;
|
|
3256
|
+
}
|
|
3257
|
+
.json-key { color: var(--code-key); }
|
|
3258
|
+
.json-string { color: var(--code-string); }
|
|
3259
|
+
.json-number { color: var(--code-number); }
|
|
3260
|
+
.json-boolean { color: var(--code-boolean); }
|
|
3261
|
+
.json-null { color: var(--code-null); font-style: italic; }
|
|
3262
|
+
.json-inline { display: block; max-width: 44rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
3263
|
+
.json-view { position: relative; max-height: 26rem; overflow: auto; }
|
|
3264
|
+
.json-editor { position: relative; min-height: 190px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--bg-inset); overflow: hidden; }
|
|
3265
|
+
.json-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); }
|
|
3266
|
+
.json-editor.invalid { border-color: var(--err); }
|
|
3267
|
+
.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; }
|
|
3268
|
+
.json-editor pre { position: absolute; inset: 0; pointer-events: none; background: transparent; }
|
|
3269
|
+
.json-editor textarea { position: relative; z-index: 1; resize: vertical; background: transparent; color: transparent; caret-color: var(--ink); -webkit-text-fill-color: transparent; }
|
|
3270
|
+
.json-editor textarea::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); }
|
|
3271
|
+
.editor-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; }
|
|
3272
|
+
.json-status { display: inline-flex; align-items: center; gap: 6px; color: var(--ok); font-size: 11px; }
|
|
3273
|
+
.json-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
3274
|
+
.json-status.invalid { color: var(--err); }
|
|
3275
|
+
.format-json { border: 0; padding: 3px 5px; background: transparent; color: var(--accent); font-size: 11px; font-weight: 650; cursor: pointer; }
|
|
3276
|
+
.row-actions { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 14px 0; }
|
|
2933
3277
|
.banner {
|
|
2934
|
-
display: none;
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
3278
|
+
display: none; position: sticky; top: calc(var(--header) + 12px); z-index: 15;
|
|
3279
|
+
align-items: center; gap: 10px; margin: 0 0 16px; padding: 11px 12px 11px 14px;
|
|
3280
|
+
border: 1px solid color-mix(in srgb, var(--err) 30%, var(--line)); border-radius: 10px;
|
|
3281
|
+
background: var(--err-soft); color: var(--err); box-shadow: var(--shadow-sm); font-size: 13px;
|
|
3282
|
+
}
|
|
3283
|
+
.banner.show { display: flex; }
|
|
3284
|
+
.banner span { flex: 1; }
|
|
3285
|
+
.banner button { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 7px; background: transparent; color: currentColor; cursor: pointer; }
|
|
3286
|
+
.banner button:hover { background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
3287
|
+
.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); }
|
|
2939
3288
|
dialog {
|
|
2940
|
-
border: 1px solid var(--line); border-radius:
|
|
2941
|
-
color: var(--ink); width: min(
|
|
2942
|
-
}
|
|
2943
|
-
dialog
|
|
2944
|
-
dialog
|
|
3289
|
+
border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--bg-elev);
|
|
3290
|
+
color: var(--ink); width: min(720px, calc(100% - 24px)); max-width: calc(100vw - 24px); box-shadow: var(--shadow-lg);
|
|
3291
|
+
}
|
|
3292
|
+
dialog[open] { animation: dialog-in 160ms ease-out; }
|
|
3293
|
+
dialog::backdrop { background: rgb(12 14 20 / 0.58); backdrop-filter: blur(3px); }
|
|
3294
|
+
dialog form, .dialog-body { padding: 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
3295
|
+
.dialog-heading { display: flex; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
|
3296
|
+
.dialog-heading strong { font-size: 18px; letter-spacing: -0.02em; }
|
|
3297
|
+
.dialog-heading span { color: var(--muted); font-size: 12px; }
|
|
2945
3298
|
textarea {
|
|
2946
3299
|
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
2947
3300
|
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
2948
3301
|
font-family: var(--mono); font-size: 12px;
|
|
2949
3302
|
}
|
|
2950
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
2951
|
-
.
|
|
3303
|
+
.dialog-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding-top: 4px; }
|
|
3304
|
+
.dialog-actions .btn-danger { margin-right: auto; }
|
|
3305
|
+
.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); }
|
|
2952
3306
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
2953
3307
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
2954
3308
|
.status-2 { color: var(--ok); }
|
|
2955
3309
|
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
2956
3310
|
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
3311
|
+
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
3312
|
+
.form-grid .wide { grid-column: span 2; }
|
|
2957
3313
|
.route {
|
|
2958
|
-
display: grid; grid-template-columns:
|
|
2959
|
-
padding:
|
|
2960
|
-
}
|
|
2961
|
-
.
|
|
3314
|
+
display: grid; grid-template-columns: 84px minmax(0, 1fr) auto; gap: 10px; align-items: center;
|
|
3315
|
+
padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--bg-elev);
|
|
3316
|
+
}
|
|
3317
|
+
.route:first-child { border-radius: var(--radius) var(--radius) 0 0; }
|
|
3318
|
+
.route:last-child { border-bottom: 0; border-radius: 0 0 var(--radius) var(--radius); }
|
|
3319
|
+
.route-tools { display: flex; align-items: flex-end; gap: 12px; margin-bottom: 12px; }
|
|
3320
|
+
.route-tools .field { flex: 1; }
|
|
3321
|
+
.route-tools input { width: 100%; }
|
|
3322
|
+
.route-count { padding-bottom: 10px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3323
|
+
#route-list { max-height: 34rem; border: 1px solid var(--line); border-radius: var(--radius); overflow: auto; box-shadow: var(--shadow-xs); }
|
|
3324
|
+
.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; }
|
|
3325
|
+
@keyframes view-in { from { transform: translateY(3px); } to { transform: translateY(0); } }
|
|
3326
|
+
@keyframes dialog-in { from { transform: translateY(8px) scale(0.99); } to { transform: translateY(0) scale(1); } }
|
|
3327
|
+
@keyframes pulse { 50% { opacity: 0.45; } }
|
|
3328
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
|
|
3329
|
+
@media (forced-colors: active) { .json-editor pre { display: none; } .json-editor textarea { color: CanvasText; -webkit-text-fill-color: CanvasText; } }
|
|
2962
3330
|
@media (max-width: 800px) {
|
|
3331
|
+
:root { --header: 0px; }
|
|
2963
3332
|
header.top {
|
|
2964
|
-
align-items: stretch; flex-direction: column;
|
|
2965
|
-
padding
|
|
3333
|
+
position: relative; align-items: stretch; flex-direction: column;
|
|
3334
|
+
padding: max(12px, env(safe-area-inset-top)) 14px 12px;
|
|
2966
3335
|
}
|
|
2967
3336
|
input, select, textarea { font-size: 16px; }
|
|
2968
3337
|
.brand, .vendor, .controls { width: 100%; }
|
|
3338
|
+
.brand { gap: 10px; }
|
|
3339
|
+
.vendor { order: 2; border-radius: 9px; }
|
|
2969
3340
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
3341
|
+
.sync-status { display: none; }
|
|
2970
3342
|
/* The layout fills the screen. A single-column grid stretches every auto
|
|
2971
3343
|
row, so the tab bar grew with the leftover height and the buttons floated
|
|
2972
3344
|
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
@@ -2976,14 +3348,23 @@ var renderAdminDocument = (service) => {
|
|
|
2976
3348
|
align-content: start;
|
|
2977
3349
|
}
|
|
2978
3350
|
nav.side {
|
|
2979
|
-
align-self: start; height: auto;
|
|
3351
|
+
position: sticky; top: 0; z-index: 12; align-self: start; height: auto;
|
|
2980
3352
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
2981
3353
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
2982
|
-
padding: 8px;
|
|
3354
|
+
padding: 8px; box-shadow: var(--shadow-xs);
|
|
2983
3355
|
}
|
|
2984
|
-
nav
|
|
3356
|
+
.nav-label { display: none; }
|
|
3357
|
+
nav.side button { width: auto; white-space: nowrap; flex: none; }
|
|
3358
|
+
nav.side button[aria-current="true"]::before { inset: auto 9px -8px; width: auto; height: 3px; border-radius: 3px 3px 0 0; }
|
|
2985
3359
|
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
2986
|
-
|
|
3360
|
+
.form-grid { grid-template-columns: minmax(0, 1fr); }
|
|
3361
|
+
.form-grid .wide { grid-column: auto; }
|
|
3362
|
+
main { padding: 22px 14px 48px; }
|
|
3363
|
+
.view-head { margin-bottom: 18px; }
|
|
3364
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
3365
|
+
.card { padding: 14px; }
|
|
3366
|
+
.card .v { font-size: 20px; }
|
|
3367
|
+
.table-shell { border: 0; background: transparent; box-shadow: none; overflow: visible; }
|
|
2987
3368
|
table, thead, tbody, tr, th, td { display: block; }
|
|
2988
3369
|
thead { display: none; }
|
|
2989
3370
|
tr {
|
|
@@ -2998,50 +3379,63 @@ var renderAdminDocument = (service) => {
|
|
|
2998
3379
|
.route { grid-template-columns: 72px 1fr; }
|
|
2999
3380
|
.route button { grid-column: 1 / -1; }
|
|
3000
3381
|
}
|
|
3382
|
+
@media (max-width: 460px) {
|
|
3383
|
+
.controls label.field { min-width: calc(50% - 4px); }
|
|
3384
|
+
.controls .icon-btn, .controls .btn { flex: 1; }
|
|
3385
|
+
.grid { grid-template-columns: minmax(0, 1fr); }
|
|
3386
|
+
.dialog-actions { flex-wrap: wrap; }
|
|
3387
|
+
.dialog-actions button { flex: 1; }
|
|
3388
|
+
.dialog-actions .btn-danger { flex-basis: 100%; margin-right: 0; }
|
|
3389
|
+
}
|
|
3001
3390
|
</style>
|
|
3002
3391
|
</head>
|
|
3003
3392
|
<body>
|
|
3004
3393
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
3005
3394
|
<header class="top">
|
|
3006
3395
|
<div class="brand">
|
|
3007
|
-
<div class="brand-
|
|
3008
|
-
<
|
|
3009
|
-
|
|
3396
|
+
<div class="brand-lockup">
|
|
3397
|
+
<div class="brand-mark" aria-hidden="true">
|
|
3398
|
+
<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>
|
|
3399
|
+
</div>
|
|
3400
|
+
<div class="brand-id">
|
|
3401
|
+
<strong>${name}</strong>
|
|
3402
|
+
<span>Mock admin</span>
|
|
3403
|
+
</div>
|
|
3010
3404
|
</div>
|
|
3011
3405
|
<div class="vendor" id="vendor" hidden></div>
|
|
3012
3406
|
</div>
|
|
3013
3407
|
<div class="controls">
|
|
3408
|
+
<span class="sync-status" id="sync-status" role="status" aria-live="polite">Ready</span>
|
|
3014
3409
|
<label class="field">Namespace
|
|
3015
3410
|
<select id="namespace" aria-label="Namespace"></select>
|
|
3016
3411
|
</label>
|
|
3017
3412
|
<label class="field">Admin key
|
|
3018
3413
|
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
3019
3414
|
</label>
|
|
3020
|
-
<button class="icon-btn" id="theme" type="button" aria-label="
|
|
3021
|
-
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
3415
|
+
<button class="icon-btn" id="theme" type="button" aria-label="Use dark theme" title="Use dark theme"></button>
|
|
3416
|
+
<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>
|
|
3022
3417
|
</div>
|
|
3023
3418
|
</header>
|
|
3024
3419
|
<div class="layout">
|
|
3025
3420
|
<nav class="side" id="nav" aria-label="Admin sections">
|
|
3026
|
-
<
|
|
3027
|
-
<button type="button" data-view="
|
|
3028
|
-
<button type="button" data-view="
|
|
3029
|
-
<button type="button" data-view="
|
|
3030
|
-
<button type="button" data-view="
|
|
3031
|
-
<button type="button" data-view="
|
|
3421
|
+
<span class="nav-label">Workspace</span>
|
|
3422
|
+
<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>
|
|
3423
|
+
<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>
|
|
3424
|
+
<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>
|
|
3425
|
+
<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>
|
|
3426
|
+
<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>
|
|
3427
|
+
<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>
|
|
3032
3428
|
</nav>
|
|
3033
3429
|
<main id="views">
|
|
3034
|
-
<
|
|
3430
|
+
<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>
|
|
3035
3431
|
<section class="view" id="view-overview">
|
|
3036
|
-
<h2>Overview</h2>
|
|
3037
|
-
<p class="lede">Health, clock, and the collections this namespace is holding.</p>
|
|
3432
|
+
<div class="view-head"><div><h2>Overview</h2><p class="lede">Health, clock, and the collections this namespace is holding.</p></div></div>
|
|
3038
3433
|
<div class="grid" id="overview-cards"></div>
|
|
3039
3434
|
<h3>Recent requests</h3>
|
|
3040
3435
|
<div id="overview-requests"></div>
|
|
3041
3436
|
</section>
|
|
3042
3437
|
<section class="view" id="view-state" hidden>
|
|
3043
|
-
<h2>State</h2>
|
|
3044
|
-
<p class="lede">Every collection in this namespace. Declared fields stay visible before any row exists; stored rows add whatever else they contain.</p>
|
|
3438
|
+
<div class="view-head"><div><h2>State</h2><p class="lede">Inspect and edit every collection in this namespace.</p></div></div>
|
|
3045
3439
|
<div class="split">
|
|
3046
3440
|
<div id="collections"></div>
|
|
3047
3441
|
<div>
|
|
@@ -3054,8 +3448,7 @@ var renderAdminDocument = (service) => {
|
|
|
3054
3448
|
</div>
|
|
3055
3449
|
</section>
|
|
3056
3450
|
<section class="view" id="view-clock" hidden>
|
|
3057
|
-
<h2>Clock</h2>
|
|
3058
|
-
<p class="lede">The clock every timestamp in this mock reads.</p>
|
|
3451
|
+
<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>
|
|
3059
3452
|
<div class="card" id="clock-readout"></div>
|
|
3060
3453
|
<div class="row-actions">
|
|
3061
3454
|
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
@@ -3074,21 +3467,22 @@ var renderAdminDocument = (service) => {
|
|
|
3074
3467
|
</form>
|
|
3075
3468
|
</section>
|
|
3076
3469
|
<section class="view" id="view-faults" hidden>
|
|
3077
|
-
<h2>Faults</h2>
|
|
3078
|
-
<p class="lede">Inject a response, a delay, or a named preset. Rules apply to this namespace.</p>
|
|
3470
|
+
<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>
|
|
3079
3471
|
<form id="fault-form" class="card stack">
|
|
3080
|
-
<
|
|
3081
|
-
<
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
<
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
<
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
<
|
|
3091
|
-
|
|
3472
|
+
<div class="form-grid">
|
|
3473
|
+
<label class="field wide">Preset
|
|
3474
|
+
<select id="fault-preset"><option value="">Custom rule</option></select>
|
|
3475
|
+
</label>
|
|
3476
|
+
<label class="field">Status
|
|
3477
|
+
<input id="fault-status" inputmode="numeric" placeholder="503">
|
|
3478
|
+
</label>
|
|
3479
|
+
<label class="field wide">Path prefix
|
|
3480
|
+
<input id="fault-path" placeholder="/v1">
|
|
3481
|
+
</label>
|
|
3482
|
+
<label class="field">Method
|
|
3483
|
+
<input id="fault-method" placeholder="GET">
|
|
3484
|
+
</label>
|
|
3485
|
+
</div>
|
|
3092
3486
|
<div class="dialog-actions">
|
|
3093
3487
|
<button class="btn-primary" type="submit">Add fault</button>
|
|
3094
3488
|
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
@@ -3097,26 +3491,31 @@ var renderAdminDocument = (service) => {
|
|
|
3097
3491
|
<div id="fault-list"></div>
|
|
3098
3492
|
</section>
|
|
3099
3493
|
<section class="view" id="view-journal" hidden>
|
|
3100
|
-
<h2>Journal</h2>
|
|
3101
|
-
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
3102
|
-
<div class="row-actions">
|
|
3103
|
-
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
3104
|
-
</div>
|
|
3494
|
+
<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>
|
|
3105
3495
|
<div id="journal"></div>
|
|
3106
3496
|
</section>
|
|
3107
3497
|
<section class="view" id="view-routes" hidden>
|
|
3108
|
-
<h2>Routes</h2>
|
|
3109
|
-
<
|
|
3498
|
+
<div class="view-head"><div><h2>Routes</h2><p class="lede">Explore shared controls and service-specific admin endpoints.</p></div></div>
|
|
3499
|
+
<div class="route-tools">
|
|
3500
|
+
<label class="field">Filter routes
|
|
3501
|
+
<input id="route-search" type="search" placeholder="Method, path, or extension" autocomplete="off">
|
|
3502
|
+
</label>
|
|
3503
|
+
<span class="route-count" id="route-count"></span>
|
|
3504
|
+
</div>
|
|
3110
3505
|
<div id="route-list"></div>
|
|
3111
|
-
<form id="route-form" class="card stack">
|
|
3506
|
+
<form id="route-form" class="card stack" style="margin-top: 18px">
|
|
3112
3507
|
<strong id="route-title">Select a route</strong>
|
|
3113
3508
|
<label class="field">JSON body
|
|
3114
|
-
<
|
|
3509
|
+
<div class="json-editor" id="route-editor" hidden>
|
|
3510
|
+
<pre aria-hidden="true"><code id="route-highlight"></code></pre>
|
|
3511
|
+
<textarea id="route-body" placeholder="{ }" spellcheck="false" aria-describedby="route-json-status" disabled></textarea>
|
|
3512
|
+
</div>
|
|
3513
|
+
<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>
|
|
3115
3514
|
</label>
|
|
3116
3515
|
<div class="dialog-actions">
|
|
3117
|
-
<button class="btn-primary" type="submit">Send</button>
|
|
3516
|
+
<button class="btn-primary" id="route-send" type="submit" disabled>Send request</button>
|
|
3118
3517
|
</div>
|
|
3119
|
-
<pre id="route-result">Response will show here.</pre>
|
|
3518
|
+
<pre class="json-view" id="route-result"><code>Response will show here.</code></pre>
|
|
3120
3519
|
</form>
|
|
3121
3520
|
</section>
|
|
3122
3521
|
</main>
|
|
@@ -3124,12 +3523,16 @@ var renderAdminDocument = (service) => {
|
|
|
3124
3523
|
</div>
|
|
3125
3524
|
<dialog id="editor">
|
|
3126
3525
|
<form method="dialog" id="editor-form">
|
|
3127
|
-
<strong id="editor-title">Record</strong>
|
|
3526
|
+
<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>
|
|
3128
3527
|
<label class="field">Id
|
|
3129
3528
|
<input id="editor-id" required>
|
|
3130
3529
|
</label>
|
|
3131
3530
|
<label class="field">JSON value
|
|
3132
|
-
<
|
|
3531
|
+
<div class="json-editor" id="record-editor">
|
|
3532
|
+
<pre aria-hidden="true"><code id="editor-highlight"></code></pre>
|
|
3533
|
+
<textarea id="editor-value" spellcheck="false" aria-describedby="editor-json-status"></textarea>
|
|
3534
|
+
</div>
|
|
3535
|
+
<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>
|
|
3133
3536
|
</label>
|
|
3134
3537
|
<div class="dialog-actions">
|
|
3135
3538
|
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
@@ -3171,8 +3574,8 @@ var describeExtension = (extension) => {
|
|
|
3171
3574
|
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
3172
3575
|
};
|
|
3173
3576
|
};
|
|
3174
|
-
var adminUiRoutes = (service, ui) => {
|
|
3175
|
-
const shell = () => renderAdminDocument(service);
|
|
3577
|
+
var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
3578
|
+
const shell = () => renderAdminDocument(service, adminPrefix);
|
|
3176
3579
|
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
3177
3580
|
return {
|
|
3178
3581
|
"GET /ui": () => htmlResponse(document3()),
|
|
@@ -3789,14 +4192,17 @@ var stateAdminRoutes = (routes) => {
|
|
|
3789
4192
|
};
|
|
3790
4193
|
|
|
3791
4194
|
// ../core/dist/version.js
|
|
3792
|
-
var PACKAGE_VERSION = true ? "
|
|
4195
|
+
var PACKAGE_VERSION = true ? "2.0.0" : UNRELEASED_VERSION;
|
|
3793
4196
|
|
|
3794
4197
|
// ../core/dist/signing.js
|
|
3795
4198
|
var encoder = new TextEncoder();
|
|
3796
4199
|
var toHex = (bytes) => [...bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
3797
4200
|
|
|
3798
4201
|
// ../core/dist/webhooks.js
|
|
3799
|
-
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4202
|
+
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4203
|
+
status,
|
|
4204
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4205
|
+
});
|
|
3800
4206
|
var adminError3 = (status, message) => json4(status, { error: { type: "mockingbird_admin", message } });
|
|
3801
4207
|
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3802
4208
|
var parseEndpoint = (value) => {
|
|
@@ -3904,7 +4310,6 @@ var AT_HEADER = "x-mockingbird-at";
|
|
|
3904
4310
|
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
3905
4311
|
var DEFAULT_NAMESPACE = "default";
|
|
3906
4312
|
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
3907
|
-
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
3908
4313
|
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
3909
4314
|
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
3910
4315
|
var effects = /* @__PURE__ */ new WeakMap();
|
|
@@ -3945,6 +4350,22 @@ var operationMatcher = (document3) => {
|
|
|
3945
4350
|
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
3946
4351
|
};
|
|
3947
4352
|
var createRuntime = (options) => {
|
|
4353
|
+
const adminPrefix = resolveAdminPrefix(options.adminPrefix);
|
|
4354
|
+
const internalPaths = Object.entries(options.document?.paths ?? {}).filter(([, item]) => item["x-mockingbird-internal"] === true).map(([path]) => path);
|
|
4355
|
+
for (const path of internalPaths) {
|
|
4356
|
+
if (!path.startsWith(`${ADMIN_PREFIX}/blobs/`))
|
|
4357
|
+
throw new Error(`internal transport ${path} must live under ${ADMIN_PREFIX}/blobs/`);
|
|
4358
|
+
}
|
|
4359
|
+
assertAdminPrefixAvailable(adminPrefix, Object.keys(options.document?.paths ?? {}).filter((path) => !internalPaths.includes(path)));
|
|
4360
|
+
const internalMatch = (path) => {
|
|
4361
|
+
for (const pattern of internalPaths) {
|
|
4362
|
+
const actual = path.split("/");
|
|
4363
|
+
const expected = pattern.split("/");
|
|
4364
|
+
if (actual.length === expected.length && expected.every((part, i) => part.startsWith("{") || part === actual[i]))
|
|
4365
|
+
return pattern;
|
|
4366
|
+
}
|
|
4367
|
+
return void 0;
|
|
4368
|
+
};
|
|
3948
4369
|
assertAdminUi(options.adminUi);
|
|
3949
4370
|
const sqlite = bootSqlite(options.sqlite);
|
|
3950
4371
|
const clock = options.clock ?? createClock();
|
|
@@ -3959,6 +4380,20 @@ var createRuntime = (options) => {
|
|
|
3959
4380
|
const instances = /* @__PURE__ */ new Map();
|
|
3960
4381
|
const publicNamespaces = /* @__PURE__ */ new Set();
|
|
3961
4382
|
const branchRngs = /* @__PURE__ */ new Map();
|
|
4383
|
+
const namespaceClocks = /* @__PURE__ */ new Map();
|
|
4384
|
+
const namespaceForStorage = /* @__PURE__ */ new Map();
|
|
4385
|
+
let isolatedNamespaces = false;
|
|
4386
|
+
const namespaceClock = (name = DEFAULT_NAMESPACE) => {
|
|
4387
|
+
if (!isolatedNamespaces || name === DEFAULT_NAMESPACE)
|
|
4388
|
+
return clock;
|
|
4389
|
+
let found = namespaceClocks.get(name);
|
|
4390
|
+
if (!found) {
|
|
4391
|
+
found = createClock();
|
|
4392
|
+
namespaceClocks.set(name, found);
|
|
4393
|
+
}
|
|
4394
|
+
return found;
|
|
4395
|
+
};
|
|
4396
|
+
const fleetPoints = /* @__PURE__ */ new WeakMap();
|
|
3962
4397
|
const timelines = /* @__PURE__ */ new Map();
|
|
3963
4398
|
const branchStorage = /* @__PURE__ */ new Map();
|
|
3964
4399
|
const captured = /* @__PURE__ */ new Map();
|
|
@@ -3972,17 +4407,22 @@ var createRuntime = (options) => {
|
|
|
3972
4407
|
if (!NAMESPACE_PATTERN.test(key) || !NAMESPACE_PATTERN.test(publicNamespace)) {
|
|
3973
4408
|
throw new RangeError(`namespace must match ${NAMESPACE_PATTERN}: ${JSON.stringify(publicNamespace)}`);
|
|
3974
4409
|
}
|
|
4410
|
+
const createdContextRng = isolatedRng ?? (isolatedNamespaces && key !== DEFAULT_NAMESPACE ? createRng(options.seed ?? 0) : rng);
|
|
3975
4411
|
const created = options.create({
|
|
3976
4412
|
namespace: storageNamespace(key),
|
|
3977
4413
|
publicNamespace,
|
|
4414
|
+
adminPrefix,
|
|
3978
4415
|
sqlite,
|
|
3979
|
-
clock,
|
|
3980
|
-
rng:
|
|
4416
|
+
clock: namespaceClock(publicNamespace),
|
|
4417
|
+
rng: createdContextRng
|
|
3981
4418
|
});
|
|
3982
4419
|
instances.set(key, created);
|
|
4420
|
+
namespaceForStorage.set(key, publicNamespace);
|
|
3983
4421
|
publicNamespaces.add(publicNamespace);
|
|
3984
4422
|
if (isolatedRng)
|
|
3985
4423
|
branchRngs.set(key, isolatedRng);
|
|
4424
|
+
else if (isolatedNamespaces && key !== DEFAULT_NAMESPACE)
|
|
4425
|
+
branchRngs.set(key, createdContextRng);
|
|
3986
4426
|
return created;
|
|
3987
4427
|
};
|
|
3988
4428
|
const instance = (name = DEFAULT_NAMESPACE) => instanceFor(name);
|
|
@@ -4000,7 +4440,7 @@ var createRuntime = (options) => {
|
|
|
4000
4440
|
captured.set(storage, snapshot2);
|
|
4001
4441
|
return Object.freeze({
|
|
4002
4442
|
snapshot: snapshot2,
|
|
4003
|
-
clock: Object.freeze(
|
|
4443
|
+
clock: Object.freeze(namespaceClock(namespaceForStorage.get(storage) ?? storage).state()),
|
|
4004
4444
|
rngState: (branchRngs.get(storage) ?? rng).state()
|
|
4005
4445
|
});
|
|
4006
4446
|
};
|
|
@@ -4010,7 +4450,7 @@ var createRuntime = (options) => {
|
|
|
4010
4450
|
return found;
|
|
4011
4451
|
instance(name);
|
|
4012
4452
|
found = new Timeline({
|
|
4013
|
-
now:
|
|
4453
|
+
now: namespaceClock(name).now,
|
|
4014
4454
|
...options.maxCheckpoints !== void 0 ? { maxCheckpoints: options.maxCheckpoints } : {}
|
|
4015
4455
|
});
|
|
4016
4456
|
found.commit(capture(name));
|
|
@@ -4037,12 +4477,13 @@ var createRuntime = (options) => {
|
|
|
4037
4477
|
const point = history2.checkout("main", at);
|
|
4038
4478
|
restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
|
|
4039
4479
|
captured.set(namespace, point.value.snapshot);
|
|
4040
|
-
rng.setState(point.value.rngState);
|
|
4041
|
-
|
|
4480
|
+
(branchRngs.get(namespace) ?? rng).setState(point.value.rngState);
|
|
4481
|
+
const selectedClock = namespaceClock(namespace);
|
|
4482
|
+
selectedClock.set(point.value.clock.now);
|
|
4042
4483
|
if (point.value.clock.frozen)
|
|
4043
|
-
|
|
4484
|
+
selectedClock.freeze();
|
|
4044
4485
|
else
|
|
4045
|
-
|
|
4486
|
+
selectedClock.unfreeze();
|
|
4046
4487
|
}
|
|
4047
4488
|
return namespace;
|
|
4048
4489
|
}
|
|
@@ -4100,11 +4541,12 @@ var createRuntime = (options) => {
|
|
|
4100
4541
|
const storage = ensureBranch(namespace, branchName);
|
|
4101
4542
|
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
4102
4543
|
captured.set(storage, point.value.snapshot);
|
|
4103
|
-
|
|
4544
|
+
const selectedClock = namespaceClock(namespace);
|
|
4545
|
+
selectedClock.set(point.value.clock.now);
|
|
4104
4546
|
if (point.value.clock.frozen)
|
|
4105
|
-
|
|
4547
|
+
selectedClock.freeze();
|
|
4106
4548
|
else
|
|
4107
|
-
|
|
4549
|
+
selectedClock.unfreeze();
|
|
4108
4550
|
(branchRngs.get(storage) ?? rng).setState(point.value.rngState);
|
|
4109
4551
|
};
|
|
4110
4552
|
const reset = async (name = DEFAULT_NAMESPACE) => {
|
|
@@ -4112,10 +4554,21 @@ var createRuntime = (options) => {
|
|
|
4112
4554
|
options.webhooks?.clear();
|
|
4113
4555
|
for (const each of instances.values())
|
|
4114
4556
|
await each.reset();
|
|
4557
|
+
for (const each of branchRngs.values())
|
|
4558
|
+
each.reset();
|
|
4115
4559
|
timelines.clear();
|
|
4560
|
+
for (const storage of branchStorage.values()) {
|
|
4561
|
+
instances.delete(storage);
|
|
4562
|
+
branchRngs.delete(storage);
|
|
4563
|
+
namespaceForStorage.delete(storage);
|
|
4564
|
+
}
|
|
4116
4565
|
branchStorage.clear();
|
|
4117
|
-
branchRngs.clear();
|
|
4118
4566
|
captured.clear();
|
|
4567
|
+
faults.clear();
|
|
4568
|
+
clock.reset();
|
|
4569
|
+
rng.reset();
|
|
4570
|
+
for (const each of namespaceClocks.values())
|
|
4571
|
+
each.reset();
|
|
4119
4572
|
return;
|
|
4120
4573
|
}
|
|
4121
4574
|
options.webhooks?.clear(name);
|
|
@@ -4133,11 +4586,18 @@ var createRuntime = (options) => {
|
|
|
4133
4586
|
else
|
|
4134
4587
|
clearNamespace(sqlite, storageNamespace(storage));
|
|
4135
4588
|
branchStorage.delete(mapping);
|
|
4589
|
+
instances.delete(storage);
|
|
4590
|
+
namespaceForStorage.delete(storage);
|
|
4136
4591
|
branchRngs.delete(storage);
|
|
4137
4592
|
captured.delete(storage);
|
|
4138
4593
|
}
|
|
4139
4594
|
timelines.delete(name);
|
|
4140
4595
|
captured.delete(name);
|
|
4596
|
+
faults.restore(name, { rules: [], rngState: createRng(options.seed ?? 0).state() });
|
|
4597
|
+
namespaceClock(name).reset();
|
|
4598
|
+
(branchRngs.get(name) ?? rng).reset();
|
|
4599
|
+
journal.clear(name);
|
|
4600
|
+
metrics.reset(name);
|
|
4141
4601
|
};
|
|
4142
4602
|
const snapshot = (name = DEFAULT_NAMESPACE) => {
|
|
4143
4603
|
return checkpoint(name, "main").value.snapshot;
|
|
@@ -4175,6 +4635,53 @@ var createRuntime = (options) => {
|
|
|
4175
4635
|
rng,
|
|
4176
4636
|
credentials,
|
|
4177
4637
|
webhooks: options.webhooks,
|
|
4638
|
+
isolateNamespaces() {
|
|
4639
|
+
if (publicNamespaces.size > 1)
|
|
4640
|
+
throw new Error("enable namespace isolation before creating named namespaces");
|
|
4641
|
+
isolatedNamespaces = true;
|
|
4642
|
+
faults.isolateNamespaces();
|
|
4643
|
+
},
|
|
4644
|
+
namespaceClock,
|
|
4645
|
+
namespaceOf(request) {
|
|
4646
|
+
const url = new URL(request.url);
|
|
4647
|
+
const prefixed = matchNamespacePath(url.pathname, adminPrefix);
|
|
4648
|
+
const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
|
|
4649
|
+
const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
|
|
4650
|
+
if (isAdminPath(path, adminPrefix))
|
|
4651
|
+
return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
|
|
4652
|
+
if (selected !== void 0)
|
|
4653
|
+
return selected;
|
|
4654
|
+
const credential = options.credential?.(request);
|
|
4655
|
+
return (credential !== void 0 ? credentials.get(credential) : void 0) ?? DEFAULT_NAMESPACE;
|
|
4656
|
+
},
|
|
4657
|
+
fleetSnapshot(namespace) {
|
|
4658
|
+
instance(namespace);
|
|
4659
|
+
const handle = {};
|
|
4660
|
+
fleetPoints.set(handle, {
|
|
4661
|
+
namespace,
|
|
4662
|
+
state: capture(namespace),
|
|
4663
|
+
faults: faults.snapshot(namespace),
|
|
4664
|
+
webhooks: options.webhooks?.snapshot(namespace)
|
|
4665
|
+
});
|
|
4666
|
+
return handle;
|
|
4667
|
+
},
|
|
4668
|
+
fleetRestore(handle, namespace) {
|
|
4669
|
+
const point = typeof handle === "object" && handle !== null ? fleetPoints.get(handle) : void 0;
|
|
4670
|
+
if (!point || point.namespace !== namespace)
|
|
4671
|
+
throw new Error("invalid fleet checkpoint");
|
|
4672
|
+
restoreNamespace(sqlite, storageNamespace(namespace), point.state.snapshot);
|
|
4673
|
+
captured.set(namespace, point.state.snapshot);
|
|
4674
|
+
const selectedClock = namespaceClock(namespace);
|
|
4675
|
+
selectedClock.freeze();
|
|
4676
|
+
selectedClock.set(point.state.clock.now);
|
|
4677
|
+
if (!point.state.clock.frozen)
|
|
4678
|
+
selectedClock.unfreeze();
|
|
4679
|
+
(branchRngs.get(namespace) ?? rng).setState(point.state.rngState);
|
|
4680
|
+
faults.restore(namespace, point.faults);
|
|
4681
|
+
if (point.webhooks !== void 0)
|
|
4682
|
+
options.webhooks?.restore(namespace, point.webhooks);
|
|
4683
|
+
timelines.delete(namespace);
|
|
4684
|
+
},
|
|
4178
4685
|
applyPreset: (name, namespace = DEFAULT_NAMESPACE, overrides = {}) => {
|
|
4179
4686
|
const preset = options.presets?.[name];
|
|
4180
4687
|
if (!preset)
|
|
@@ -4206,7 +4713,7 @@ var createRuntime = (options) => {
|
|
|
4206
4713
|
state: (namespace = DEFAULT_NAMESPACE) => inspectState(stateScope(namespace)),
|
|
4207
4714
|
fetch: async (incoming) => {
|
|
4208
4715
|
let request = incoming;
|
|
4209
|
-
const prefixed =
|
|
4716
|
+
const prefixed = matchNamespacePath(new URL(request.url).pathname, adminPrefix);
|
|
4210
4717
|
if (prefixed) {
|
|
4211
4718
|
const url2 = new URL(request.url);
|
|
4212
4719
|
url2.pathname = prefixed[2] ?? "/";
|
|
@@ -4222,7 +4729,15 @@ var createRuntime = (options) => {
|
|
|
4222
4729
|
signal: request.signal
|
|
4223
4730
|
});
|
|
4224
4731
|
}
|
|
4225
|
-
|
|
4732
|
+
const internalUrl = new URL(request.url);
|
|
4733
|
+
const internalPath = isAdminPath(internalUrl.pathname, adminPrefix) ? `${ADMIN_PREFIX}${internalUrl.pathname.slice(adminPrefix.length)}` : "";
|
|
4734
|
+
const internal = internalMatch(internalPath);
|
|
4735
|
+
const obsoleteInternalPath = adminPrefix !== ADMIN_PREFIX && internalMatch(internalUrl.pathname) && !internal;
|
|
4736
|
+
if (internal) {
|
|
4737
|
+
internalUrl.pathname = internalPath;
|
|
4738
|
+
request = new Request(internalUrl, request);
|
|
4739
|
+
}
|
|
4740
|
+
let namespace = internal ? internalUrl.searchParams.get("namespace") ?? control.namespaceOf(request) : control.namespaceOf(request);
|
|
4226
4741
|
if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
4227
4742
|
const credential = options.credential(request);
|
|
4228
4743
|
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
@@ -4242,7 +4757,9 @@ var createRuntime = (options) => {
|
|
|
4242
4757
|
return copy;
|
|
4243
4758
|
}
|
|
4244
4759
|
};
|
|
4245
|
-
|
|
4760
|
+
if (obsoleteInternalPath)
|
|
4761
|
+
return stamp(adminFail(404, "internal transport is only available under adminPrefix"));
|
|
4762
|
+
const handled = internal ? void 0 : await control.handle(request);
|
|
4246
4763
|
if (handled)
|
|
4247
4764
|
return stamp(handled);
|
|
4248
4765
|
const started = monotonicNow();
|
|
@@ -4283,7 +4800,7 @@ var createRuntime = (options) => {
|
|
|
4283
4800
|
type: "mockingbird_admin",
|
|
4284
4801
|
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
4285
4802
|
}
|
|
4286
|
-
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
4803
|
+
}), { status: 400, headers: { "content-type": "application/json; charset=utf-8" } }));
|
|
4287
4804
|
}
|
|
4288
4805
|
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
4289
4806
|
log(400);
|
|
@@ -4350,6 +4867,7 @@ var createRuntime = (options) => {
|
|
|
4350
4867
|
startedAt: wallNow(),
|
|
4351
4868
|
wallNow,
|
|
4352
4869
|
clock,
|
|
4870
|
+
namespaceClock,
|
|
4353
4871
|
faults,
|
|
4354
4872
|
metrics,
|
|
4355
4873
|
journal,
|
|
@@ -4399,15 +4917,16 @@ var createRuntime = (options) => {
|
|
|
4399
4917
|
...options.admin?.(runtime) ?? {},
|
|
4400
4918
|
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
4401
4919
|
// maps `presets`, and a service route must not replace that list with a record.
|
|
4402
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
4920
|
+
...options.presets ? presetRoutes(options.presets, runtime, adminPrefix) : {},
|
|
4403
4921
|
...stateAdminRoutes({
|
|
4404
4922
|
open: stateScope,
|
|
4405
4923
|
afterWrite: (namespace) => {
|
|
4406
4924
|
checkpoint(namespace, "main");
|
|
4407
4925
|
}
|
|
4408
4926
|
}),
|
|
4409
|
-
...adminUiRoutes(options.name, options.adminUi)
|
|
4927
|
+
...adminUiRoutes(options.name, options.adminUi, adminPrefix)
|
|
4410
4928
|
},
|
|
4929
|
+
adminPrefix,
|
|
4411
4930
|
adminKey: options.adminKey
|
|
4412
4931
|
});
|
|
4413
4932
|
return defineMock(runtime);
|
|
@@ -4421,7 +4940,10 @@ var mutableResponse = (response) => {
|
|
|
4421
4940
|
return new Response(response.body, response);
|
|
4422
4941
|
}
|
|
4423
4942
|
};
|
|
4424
|
-
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
4943
|
+
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
4944
|
+
status,
|
|
4945
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4946
|
+
});
|
|
4425
4947
|
var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
|
|
4426
4948
|
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4427
4949
|
var credentialRoutes = (registry) => ({
|
|
@@ -4473,7 +4995,7 @@ var credentialRoutes = (registry) => ({
|
|
|
4473
4995
|
return adminJson(200, { status: "ok" });
|
|
4474
4996
|
}
|
|
4475
4997
|
});
|
|
4476
|
-
var presetRoutes = (presets, runtime) => ({
|
|
4998
|
+
var presetRoutes = (presets, runtime, adminPrefix) => ({
|
|
4477
4999
|
"GET /faults/presets": () => {
|
|
4478
5000
|
const body = {
|
|
4479
5001
|
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
@@ -4483,7 +5005,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
4483
5005
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
4484
5006
|
const name = params.name;
|
|
4485
5007
|
if (!presets[name])
|
|
4486
|
-
return adminFail(404, `no fault preset ${name}; GET /
|
|
5008
|
+
return adminFail(404, `no fault preset ${name}; GET ${adminPrefix}/faults/presets`);
|
|
4487
5009
|
const overrides = isObject(body) ? body : {};
|
|
4488
5010
|
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
4489
5011
|
}
|
|
@@ -5129,6 +5651,7 @@ var createRuntime2 = (options = {}) => createRuntime({
|
|
|
5129
5651
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
5130
5652
|
...options.clock ? { clock: options.clock } : {},
|
|
5131
5653
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
5654
|
+
...options.adminPrefix !== void 0 ? { adminPrefix: options.adminPrefix } : {},
|
|
5132
5655
|
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
5133
5656
|
...options.onLog ? { onLog: options.onLog } : {},
|
|
5134
5657
|
credential: accessKeyCredential,
|
|
@@ -5923,6 +6446,9 @@ var SpeechAPI = class {
|
|
|
5923
6446
|
};
|
|
5924
6447
|
|
|
5925
6448
|
export {
|
|
6449
|
+
createClock,
|
|
6450
|
+
resolveAdminPrefix,
|
|
6451
|
+
parseDuration,
|
|
5926
6452
|
MS_PER_CHARACTER,
|
|
5927
6453
|
durationFor,
|
|
5928
6454
|
pcmTone,
|
|
@@ -5951,4 +6477,4 @@ export {
|
|
|
5951
6477
|
accessKeyCredential,
|
|
5952
6478
|
SpeechAPI
|
|
5953
6479
|
};
|
|
5954
|
-
//# sourceMappingURL=chunk-
|
|
6480
|
+
//# sourceMappingURL=chunk-RTZSXNQY.js.map
|