@crvouga/mockingbird-service-formbricks 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 +18 -0
- package/dist/{chunk-S2LM6VC4.js → chunk-2YLOIK2J.js} +822 -258
- package/dist/chunk-2YLOIK2J.js.map +7 -0
- package/dist/chunk-QQG3NBRB.js +858 -0
- package/dist/chunk-QQG3NBRB.js.map +7 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +34 -4
- package/dist/index.js +1 -1
- package/dist/server.d.ts +64 -4
- package/dist/server.js +2 -2
- package/openapi.yaml +555 -0
- package/package.json +29 -4
- package/dist/chunk-S2LM6VC4.js.map +0 -7
- package/dist/chunk-W53FW56G.js +0 -371
- package/dist/chunk-W53FW56G.js.map +0 -7
|
@@ -110,13 +110,31 @@ var Collection = class {
|
|
|
110
110
|
};
|
|
111
111
|
|
|
112
112
|
// ../core/dist/control.js
|
|
113
|
-
var HEALTH_PATH = "/health";
|
|
114
113
|
var ADMIN_PREFIX = "/__admin";
|
|
114
|
+
var resolveAdminPrefix = (prefix = ADMIN_PREFIX) => {
|
|
115
|
+
if (!/^\/[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*$/.test(prefix)) {
|
|
116
|
+
throw new Error("adminPrefix must be an absolute non-root path of literal segments, without a trailing slash");
|
|
117
|
+
}
|
|
118
|
+
return prefix;
|
|
119
|
+
};
|
|
120
|
+
var isAdminPath = (path, prefix) => path === prefix || path.startsWith(`${prefix}/`);
|
|
121
|
+
var matchNamespacePath = (path, prefix) => /^\/ns\/([^/]+)(\/.*)?$/.exec(isAdminPath(path, prefix) ? path.slice(prefix.length) : "");
|
|
122
|
+
var assertAdminPrefixAvailable = (prefix, paths) => {
|
|
123
|
+
const reserved = prefix.split("/").slice(1);
|
|
124
|
+
for (const path of paths) {
|
|
125
|
+
const segments = path.split("/").slice(1);
|
|
126
|
+
if (segments.length < reserved.length || /^\{[^}]+\}$/.test(segments[0] ?? ""))
|
|
127
|
+
continue;
|
|
128
|
+
if (reserved.every((part, i) => segments[i] === part || /^\{[^}]+\}$/.test(segments[i] ?? ""))) {
|
|
129
|
+
throw new Error(`vendor route ${path} collides with adminPrefix ${prefix}; choose a different adminPrefix`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
115
133
|
var ADMIN_KEY_HEADER = "x-mockingbird-admin-key";
|
|
116
134
|
var NAMESPACE_HEADER = "x-mockingbird-namespace";
|
|
117
135
|
var json = (status, body) => new Response(JSON.stringify(body), {
|
|
118
136
|
status,
|
|
119
|
-
headers: { "content-type": "application/json" }
|
|
137
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
120
138
|
});
|
|
121
139
|
var adminError = (status, message) => json(status, { error: { type: "mockingbird_admin", message } });
|
|
122
140
|
var UNITS = {
|
|
@@ -168,11 +186,25 @@ var readJson = async (request) => {
|
|
|
168
186
|
};
|
|
169
187
|
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
170
188
|
var createControlPlane = (context) => {
|
|
189
|
+
const adminPrefix = resolveAdminPrefix(context.adminPrefix);
|
|
171
190
|
const snapshots = /* @__PURE__ */ new Map();
|
|
172
191
|
let snapshotCounter = 0;
|
|
173
192
|
const headerNamespace = (request) => request.headers.get(NAMESPACE_HEADER) ?? context.defaultNamespace;
|
|
174
193
|
const adminNamespace = (request, url) => url.searchParams.get("namespace") ?? headerNamespace(request);
|
|
194
|
+
const health = () => {
|
|
195
|
+
return json(200, {
|
|
196
|
+
...context.describe(),
|
|
197
|
+
status: "ok",
|
|
198
|
+
service: context.name,
|
|
199
|
+
uptimeMs: context.wallNow() - context.startedAt,
|
|
200
|
+
clock: context.clock.state(),
|
|
201
|
+
namespaces: context.namespaces().length,
|
|
202
|
+
adminPrefix,
|
|
203
|
+
adminUi: `${adminPrefix}/ui`
|
|
204
|
+
});
|
|
205
|
+
};
|
|
175
206
|
const builtin = {
|
|
207
|
+
"GET /health": health,
|
|
176
208
|
"GET /": () => json(200, {
|
|
177
209
|
service: context.name,
|
|
178
210
|
routes: [...Object.keys(builtin), ...Object.keys(context.routes)].sort()
|
|
@@ -183,29 +215,30 @@ var createControlPlane = (context) => {
|
|
|
183
215
|
return json(200, { status: "ok", reset: target === "*" ? context.namespaces() : [target] });
|
|
184
216
|
},
|
|
185
217
|
"GET /namespaces": () => json(200, { default: context.defaultNamespace, namespaces: context.namespaces() }),
|
|
186
|
-
"GET /clock": () => json(200, context.clock.state()),
|
|
187
|
-
"POST /clock": ({ body }) => {
|
|
218
|
+
"GET /clock": ({ namespace }) => json(200, (context.namespaceClock?.(namespace) ?? context.clock).state()),
|
|
219
|
+
"POST /clock": ({ body, namespace }) => {
|
|
220
|
+
const clock = context.namespaceClock?.(namespace) ?? context.clock;
|
|
188
221
|
if (!isRecord(body))
|
|
189
222
|
return adminError(400, "expected a JSON object");
|
|
190
223
|
if (body.reset === true)
|
|
191
|
-
|
|
224
|
+
clock.reset();
|
|
192
225
|
if (body.freeze === true)
|
|
193
|
-
|
|
226
|
+
clock.freeze();
|
|
194
227
|
if (body.set !== void 0) {
|
|
195
228
|
const instant = parseInstant(body.set);
|
|
196
229
|
if (instant === void 0)
|
|
197
230
|
return adminError(400, "set: expected epoch ms or ISO-8601");
|
|
198
|
-
|
|
231
|
+
clock.set(instant);
|
|
199
232
|
}
|
|
200
233
|
if (body.advance !== void 0) {
|
|
201
234
|
const delta = parseDuration(body.advance);
|
|
202
235
|
if (delta === void 0)
|
|
203
236
|
return adminError(400, 'advance: expected ms or "15m"-style');
|
|
204
|
-
|
|
237
|
+
clock.advance(delta);
|
|
205
238
|
}
|
|
206
239
|
if (body.freeze === false)
|
|
207
|
-
|
|
208
|
-
return json(200,
|
|
240
|
+
clock.unfreeze();
|
|
241
|
+
return json(200, clock.state());
|
|
209
242
|
},
|
|
210
243
|
"GET /faults": () => json(200, { faults: context.faults.list() }),
|
|
211
244
|
"POST /faults": ({ body, namespace }) => {
|
|
@@ -338,7 +371,14 @@ var createControlPlane = (context) => {
|
|
|
338
371
|
return json(200, { status: "ok" });
|
|
339
372
|
}
|
|
340
373
|
};
|
|
341
|
-
const
|
|
374
|
+
for (const key of Object.keys(context.routes)) {
|
|
375
|
+
if (Object.hasOwn(builtin, key))
|
|
376
|
+
throw new Error(`admin route ${key} overrides a shared route`);
|
|
377
|
+
const path = key.slice(key.indexOf(" ") + 1);
|
|
378
|
+
if (isAdminPath(path, "/ns") || isAdminPath(path, "/blobs"))
|
|
379
|
+
throw new Error(`admin route ${key} overlaps an internal transport`);
|
|
380
|
+
}
|
|
381
|
+
const routes = [...Object.entries(builtin), ...Object.entries(context.routes)].map(([key, handler]) => {
|
|
342
382
|
const space = key.indexOf(" ");
|
|
343
383
|
return { method: key.slice(0, space), pattern: key.slice(space + 1), handler };
|
|
344
384
|
});
|
|
@@ -346,21 +386,13 @@ var createControlPlane = (context) => {
|
|
|
346
386
|
namespaceOf: headerNamespace,
|
|
347
387
|
async handle(request) {
|
|
348
388
|
const url = new URL(request.url);
|
|
349
|
-
if (url.pathname ===
|
|
350
|
-
return
|
|
351
|
-
status: "ok",
|
|
352
|
-
service: context.name,
|
|
353
|
-
uptimeMs: context.wallNow() - context.startedAt,
|
|
354
|
-
clock: context.clock.state(),
|
|
355
|
-
namespaces: context.namespaces().length,
|
|
356
|
-
adminUi: `${ADMIN_PREFIX}/ui`,
|
|
357
|
-
...context.describe()
|
|
358
|
-
});
|
|
389
|
+
if (url.pathname === `${adminPrefix}/health` && request.method === "GET") {
|
|
390
|
+
return health();
|
|
359
391
|
}
|
|
360
|
-
if (url.pathname
|
|
392
|
+
if (!isAdminPath(url.pathname, adminPrefix)) {
|
|
361
393
|
return void 0;
|
|
362
394
|
}
|
|
363
|
-
const path = url.pathname.slice(
|
|
395
|
+
const path = url.pathname.slice(adminPrefix.length) || "/";
|
|
364
396
|
const shell = request.method === "GET" && (path === "/ui" || path === "/ui/");
|
|
365
397
|
if (!shell && context.adminKey !== void 0 && request.headers.get(ADMIN_KEY_HEADER) !== context.adminKey) {
|
|
366
398
|
return adminError(401, `missing or wrong ${ADMIN_KEY_HEADER}`);
|
|
@@ -368,7 +400,12 @@ var createControlPlane = (context) => {
|
|
|
368
400
|
for (const route of routes) {
|
|
369
401
|
if (route.method !== request.method)
|
|
370
402
|
continue;
|
|
371
|
-
|
|
403
|
+
let params;
|
|
404
|
+
try {
|
|
405
|
+
params = matchRoute(route.pattern, path);
|
|
406
|
+
} catch {
|
|
407
|
+
return adminError(400, "invalid path encoding");
|
|
408
|
+
}
|
|
372
409
|
if (!params)
|
|
373
410
|
continue;
|
|
374
411
|
let body;
|
|
@@ -377,15 +414,18 @@ var createControlPlane = (context) => {
|
|
|
377
414
|
} catch {
|
|
378
415
|
return adminError(400, "request body is not valid JSON");
|
|
379
416
|
}
|
|
417
|
+
const namespace = adminNamespace(request, url);
|
|
418
|
+
if (!/^[A-Za-z0-9_.-]{1,64}$/.test(namespace))
|
|
419
|
+
return adminError(400, "invalid namespace");
|
|
380
420
|
return route.handler({
|
|
381
421
|
request,
|
|
382
422
|
url,
|
|
383
423
|
params,
|
|
384
|
-
namespace
|
|
424
|
+
namespace,
|
|
385
425
|
body
|
|
386
426
|
});
|
|
387
427
|
}
|
|
388
|
-
return adminError(404, `no admin route ${request.method} ${path}; GET ${
|
|
428
|
+
return adminError(404, `no admin route ${request.method} ${path}; GET ${adminPrefix} lists them`);
|
|
389
429
|
}
|
|
390
430
|
};
|
|
391
431
|
};
|
|
@@ -467,7 +507,33 @@ var faultResponse = (rule) => {
|
|
|
467
507
|
};
|
|
468
508
|
var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) => {
|
|
469
509
|
const entries = [];
|
|
510
|
+
let isolated = false;
|
|
511
|
+
const scopedRngs = /* @__PURE__ */ new Map();
|
|
512
|
+
const random = (namespace) => {
|
|
513
|
+
if (!isolated || namespace === "default")
|
|
514
|
+
return rng;
|
|
515
|
+
let scoped = scopedRngs.get(namespace);
|
|
516
|
+
if (!scoped) {
|
|
517
|
+
scoped = createRng(rng.seed);
|
|
518
|
+
scopedRngs.set(namespace, scoped);
|
|
519
|
+
}
|
|
520
|
+
return scoped;
|
|
521
|
+
};
|
|
470
522
|
return {
|
|
523
|
+
isolateNamespaces() {
|
|
524
|
+
isolated = true;
|
|
525
|
+
},
|
|
526
|
+
snapshot(namespace) {
|
|
527
|
+
return {
|
|
528
|
+
rules: structuredClone(entries.map((entry, position) => ({
|
|
529
|
+
...entry.rule,
|
|
530
|
+
remaining: entry.remaining,
|
|
531
|
+
hits: entry.hits,
|
|
532
|
+
position
|
|
533
|
+
})).filter((entry) => entry.namespace === namespace)),
|
|
534
|
+
rngState: random(namespace).state()
|
|
535
|
+
};
|
|
536
|
+
},
|
|
471
537
|
add(rule) {
|
|
472
538
|
const existing = entries.findIndex((e) => e.rule.id === rule.id);
|
|
473
539
|
const entry = { rule, remaining: rule.count ?? null, hits: 0 };
|
|
@@ -487,6 +553,16 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
|
|
|
487
553
|
},
|
|
488
554
|
clear() {
|
|
489
555
|
entries.length = 0;
|
|
556
|
+
rng.reset();
|
|
557
|
+
scopedRngs.clear();
|
|
558
|
+
},
|
|
559
|
+
restore(namespace, state) {
|
|
560
|
+
for (let i = entries.length - 1; i >= 0; i--)
|
|
561
|
+
if (entries[i]?.rule.namespace === namespace)
|
|
562
|
+
entries.splice(i, 1);
|
|
563
|
+
for (const { remaining, hits, position, ...rule } of state.rules)
|
|
564
|
+
entries.splice(Math.min(position, entries.length), 0, { rule, remaining, hits });
|
|
565
|
+
random(namespace).setState(state.rngState);
|
|
490
566
|
},
|
|
491
567
|
async take(candidate) {
|
|
492
568
|
const hits = [];
|
|
@@ -496,7 +572,7 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
|
|
|
496
572
|
if (!matches(entry.rule, candidate))
|
|
497
573
|
continue;
|
|
498
574
|
const rate = entry.rule.rate ?? 1;
|
|
499
|
-
if (
|
|
575
|
+
if (random(candidate.namespace).next() >= rate)
|
|
500
576
|
continue;
|
|
501
577
|
entry.hits++;
|
|
502
578
|
if (entry.remaining !== null)
|
|
@@ -873,7 +949,8 @@ var annotateResponse = (response, extra) => {
|
|
|
873
949
|
var responseNotes = (response) => notes.get(response);
|
|
874
950
|
|
|
875
951
|
// ../core/dist/metrics.js
|
|
876
|
-
var createMetrics = () => {
|
|
952
|
+
var createMetrics = (partition = true) => {
|
|
953
|
+
const namespaces = /* @__PURE__ */ new Map();
|
|
877
954
|
let requests = 0;
|
|
878
955
|
let faults = 0;
|
|
879
956
|
let totalDurationMs = 0;
|
|
@@ -881,6 +958,14 @@ var createMetrics = () => {
|
|
|
881
958
|
const unmatched = /* @__PURE__ */ new Map();
|
|
882
959
|
return {
|
|
883
960
|
record(entry) {
|
|
961
|
+
if (partition) {
|
|
962
|
+
let scoped = namespaces.get(entry.namespace);
|
|
963
|
+
if (!scoped) {
|
|
964
|
+
scoped = createMetrics(false);
|
|
965
|
+
namespaces.set(entry.namespace, scoped);
|
|
966
|
+
}
|
|
967
|
+
scoped.record(entry);
|
|
968
|
+
}
|
|
884
969
|
requests++;
|
|
885
970
|
totalDurationMs += entry.durationMs;
|
|
886
971
|
if (entry.faultId !== void 0)
|
|
@@ -892,7 +977,7 @@ var createMetrics = () => {
|
|
|
892
977
|
unmatched.set(route, (unmatched.get(route) ?? 0) + 1);
|
|
893
978
|
}
|
|
894
979
|
},
|
|
895
|
-
report: () => ({
|
|
980
|
+
report: (namespace) => namespace !== void 0 ? (namespaces.get(namespace) ?? createMetrics(false)).report() : {
|
|
896
981
|
requests,
|
|
897
982
|
byOperation: Object.fromEntries([...byOperation].sort(([a], [b]) => a.localeCompare(b))),
|
|
898
983
|
unmatched: [...unmatched].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([route, count]) => {
|
|
@@ -901,8 +986,13 @@ var createMetrics = () => {
|
|
|
901
986
|
}),
|
|
902
987
|
faults,
|
|
903
988
|
totalDurationMs
|
|
904
|
-
}
|
|
905
|
-
reset() {
|
|
989
|
+
},
|
|
990
|
+
reset(namespace) {
|
|
991
|
+
if (namespace !== void 0) {
|
|
992
|
+
namespaces.delete(namespace);
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
namespaces.clear();
|
|
906
996
|
requests = 0;
|
|
907
997
|
faults = 0;
|
|
908
998
|
totalDurationMs = 0;
|
|
@@ -1515,6 +1605,34 @@ dialog:not([open]) {
|
|
|
1515
1605
|
function isRecord2(value) {
|
|
1516
1606
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1517
1607
|
}
|
|
1608
|
+
function highlightJsonText(text) {
|
|
1609
|
+
const escapeToken = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
1610
|
+
switch (char) {
|
|
1611
|
+
case "&":
|
|
1612
|
+
return "&";
|
|
1613
|
+
case "<":
|
|
1614
|
+
return "<";
|
|
1615
|
+
case ">":
|
|
1616
|
+
return ">";
|
|
1617
|
+
case '"':
|
|
1618
|
+
return """;
|
|
1619
|
+
default:
|
|
1620
|
+
return "'";
|
|
1621
|
+
}
|
|
1622
|
+
});
|
|
1623
|
+
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;
|
|
1624
|
+
let cursor = 0;
|
|
1625
|
+
let html = "";
|
|
1626
|
+
for (const match of text.matchAll(token)) {
|
|
1627
|
+
const index = match.index ?? 0;
|
|
1628
|
+
const value = match[0];
|
|
1629
|
+
html += escapeToken(text.slice(cursor, index));
|
|
1630
|
+
const kind = value.startsWith('"') ? value.trimEnd().endsWith(":") ? "key" : "string" : value === "true" || value === "false" ? "boolean" : value === "null" ? "null" : "number";
|
|
1631
|
+
html += `<span class="json-${kind}">${escapeToken(value)}</span>`;
|
|
1632
|
+
cursor = index + value.length;
|
|
1633
|
+
}
|
|
1634
|
+
return html + escapeToken(text.slice(cursor));
|
|
1635
|
+
}
|
|
1518
1636
|
function mountAdminBrand(doc, host, service, catalog) {
|
|
1519
1637
|
const clip = (value, max) => {
|
|
1520
1638
|
if (typeof value !== "string")
|
|
@@ -1876,14 +1994,16 @@ function bootAdmin(config) {
|
|
|
1876
1994
|
const state = {
|
|
1877
1995
|
namespace: "default",
|
|
1878
1996
|
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
1879
|
-
view: "overview",
|
|
1997
|
+
view: ["overview", "state", "clock", "faults", "journal", "routes"].includes(location.hash.slice(1)) ? location.hash.slice(1) : "overview",
|
|
1880
1998
|
collection: "",
|
|
1881
1999
|
shape: null,
|
|
1882
2000
|
route: null,
|
|
2001
|
+
routes: [],
|
|
1883
2002
|
editing: null
|
|
1884
2003
|
};
|
|
1885
2004
|
const standard = new Set(config.standardRoutes);
|
|
1886
2005
|
const banner = el("banner", HTMLElement);
|
|
2006
|
+
const bannerMessage = el("banner-message", HTMLElement);
|
|
1887
2007
|
const keyInput = el("admin-key", HTMLInputElement);
|
|
1888
2008
|
keyInput.value = state.key;
|
|
1889
2009
|
const params = new URLSearchParams(location.search);
|
|
@@ -1894,18 +2014,21 @@ function bootAdmin(config) {
|
|
|
1894
2014
|
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
1895
2015
|
params.delete("key");
|
|
1896
2016
|
const next = params.toString();
|
|
1897
|
-
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
2017
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : "") + location.hash);
|
|
1898
2018
|
}
|
|
1899
2019
|
const queryNamespace = params.get("namespace");
|
|
1900
2020
|
if (queryNamespace)
|
|
1901
2021
|
state.namespace = queryNamespace;
|
|
1902
2022
|
const showError = (error2) => {
|
|
1903
|
-
|
|
2023
|
+
bannerMessage.textContent = error2 instanceof Error ? error2.message : String(error2);
|
|
1904
2024
|
banner.classList.add("show");
|
|
2025
|
+
banner.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
1905
2026
|
};
|
|
1906
2027
|
const clearError = () => {
|
|
1907
2028
|
banner.classList.remove("show");
|
|
2029
|
+
bannerMessage.textContent = "";
|
|
1908
2030
|
};
|
|
2031
|
+
el("banner-dismiss", HTMLButtonElement).addEventListener("click", clearError);
|
|
1909
2032
|
const api = async (method, path, body) => {
|
|
1910
2033
|
const headers = { accept: "application/json" };
|
|
1911
2034
|
if (state.key !== "")
|
|
@@ -1949,16 +2072,21 @@ function bootAdmin(config) {
|
|
|
1949
2072
|
namespace: () => state.namespace
|
|
1950
2073
|
};
|
|
1951
2074
|
const isPanelScript = (value) => typeof value === "function";
|
|
2075
|
+
const themeButton = el("theme", HTMLButtonElement);
|
|
1952
2076
|
const setTheme = (theme) => {
|
|
1953
2077
|
if (theme)
|
|
1954
2078
|
document.documentElement.dataset.theme = theme;
|
|
1955
2079
|
else
|
|
1956
2080
|
delete document.documentElement.dataset.theme;
|
|
2081
|
+
const dark = theme ? theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2082
|
+
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>';
|
|
2083
|
+
const label = dark ? "Use light theme" : "Use dark theme";
|
|
2084
|
+
themeButton.setAttribute("aria-label", label);
|
|
2085
|
+
themeButton.title = label;
|
|
1957
2086
|
};
|
|
1958
2087
|
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
2088
|
+
setTheme(storedTheme === "dark" || storedTheme === "light" ? storedTheme : null);
|
|
2089
|
+
themeButton.addEventListener("click", () => {
|
|
1962
2090
|
const current = document.documentElement.dataset.theme;
|
|
1963
2091
|
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
1964
2092
|
const next = dark ? "light" : "dark";
|
|
@@ -1977,6 +2105,7 @@ function bootAdmin(config) {
|
|
|
1977
2105
|
continue;
|
|
1978
2106
|
section.hidden = section.id !== `view-${view}`;
|
|
1979
2107
|
}
|
|
2108
|
+
history.replaceState(null, "", `${location.pathname}${location.search}#${encodeURIComponent(view)}`);
|
|
1980
2109
|
};
|
|
1981
2110
|
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
1982
2111
|
const target = event.target;
|
|
@@ -1991,7 +2120,7 @@ function bootAdmin(config) {
|
|
|
1991
2120
|
show(view);
|
|
1992
2121
|
refresh().catch(showError);
|
|
1993
2122
|
});
|
|
1994
|
-
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>`;
|
|
2123
|
+
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>`;
|
|
1995
2124
|
const table = (columns, rows) => {
|
|
1996
2125
|
if (rows.length === 0)
|
|
1997
2126
|
return '<p class="empty">Nothing here yet.</p>';
|
|
@@ -2001,16 +2130,22 @@ function bootAdmin(config) {
|
|
|
2001
2130
|
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
2002
2131
|
return `<tr${attrs}>${cells}</tr>`;
|
|
2003
2132
|
}).join("");
|
|
2004
|
-
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
2133
|
+
return `<div class="table-shell"><table><thead>${head}</thead><tbody>${body}</tbody></table></div>`;
|
|
2005
2134
|
};
|
|
2006
2135
|
const preview = (value) => {
|
|
2007
2136
|
const text = JSON.stringify(value) ?? "null";
|
|
2008
|
-
|
|
2137
|
+
const clipped = text.length > 180 ? `${text.slice(0, 180)}\u2026` : text;
|
|
2138
|
+
return `<code class="json-inline">${highlightJsonText(clipped)}</code>`;
|
|
2009
2139
|
};
|
|
2010
2140
|
const when = (epoch) => {
|
|
2011
2141
|
const date = new Date(epoch);
|
|
2012
2142
|
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
2013
2143
|
};
|
|
2144
|
+
const journalWhen = (value) => {
|
|
2145
|
+
const numeric = value.trim() === "" ? Number.NaN : Number(value);
|
|
2146
|
+
const epoch = Number.isFinite(numeric) ? numeric : Date.parse(value);
|
|
2147
|
+
return Number.isFinite(epoch) ? when(epoch) : value;
|
|
2148
|
+
};
|
|
2014
2149
|
const clockCard = (epoch) => {
|
|
2015
2150
|
const date = new Date(epoch);
|
|
2016
2151
|
if (Number.isNaN(date.getTime()))
|
|
@@ -2130,7 +2265,7 @@ function bootAdmin(config) {
|
|
|
2130
2265
|
};
|
|
2131
2266
|
const renderOverview = async () => {
|
|
2132
2267
|
const [health, shape, clock, journal] = await Promise.all([
|
|
2133
|
-
originGet(
|
|
2268
|
+
originGet(`${config.adminPrefix}/health`),
|
|
2134
2269
|
api("GET", "/state"),
|
|
2135
2270
|
api("GET", "/clock"),
|
|
2136
2271
|
api("GET", "/requests?limit=8")
|
|
@@ -2147,7 +2282,7 @@ function bootAdmin(config) {
|
|
|
2147
2282
|
},
|
|
2148
2283
|
{
|
|
2149
2284
|
k: "Status",
|
|
2150
|
-
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
2285
|
+
v: `<span class="badge ok">${esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")}</span>`
|
|
2151
2286
|
},
|
|
2152
2287
|
{ k: "Namespace", v: esc(state.namespace) },
|
|
2153
2288
|
{ k: "Collections", v: String(decoded.collections.length) },
|
|
@@ -2234,26 +2369,46 @@ function bootAdmin(config) {
|
|
|
2234
2369
|
const renderJournal = async () => {
|
|
2235
2370
|
const journal = await api("GET", "/requests?limit=100");
|
|
2236
2371
|
el("journal", HTMLElement).innerHTML = table([
|
|
2237
|
-
{ label: "When", cell: (row) => esc(row.at) },
|
|
2238
|
-
{
|
|
2372
|
+
{ label: "When", cell: (row) => esc(journalWhen(row.at)) },
|
|
2373
|
+
{
|
|
2374
|
+
label: "Method",
|
|
2375
|
+
cell: (row) => `<span class="method">${esc(row.method)}</span>`
|
|
2376
|
+
},
|
|
2239
2377
|
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2240
|
-
{
|
|
2378
|
+
{
|
|
2379
|
+
label: "Status",
|
|
2380
|
+
cell: (row) => `<span class="badge ${row.status.startsWith("2") ? "ok" : row.status.startsWith("4") || row.status.startsWith("5") ? "warn" : ""}">${esc(row.status)}</span>`
|
|
2381
|
+
},
|
|
2241
2382
|
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
2242
2383
|
], decodeJournal(journal));
|
|
2243
2384
|
};
|
|
2244
|
-
const
|
|
2245
|
-
const
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2385
|
+
const paintRoutes = () => {
|
|
2386
|
+
const query = el("route-search", HTMLInputElement).value.trim().toLocaleLowerCase();
|
|
2387
|
+
const routes = state.routes.filter((route) => route.toLocaleLowerCase().includes(query));
|
|
2388
|
+
el("route-count", HTMLElement).textContent = `${routes.length} of ${state.routes.length}`;
|
|
2389
|
+
if (state.routes.length === 0) {
|
|
2390
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No admin routes are available.</p>';
|
|
2391
|
+
return;
|
|
2392
|
+
}
|
|
2393
|
+
if (routes.length === 0) {
|
|
2394
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No routes match this filter.</p>';
|
|
2395
|
+
return;
|
|
2396
|
+
}
|
|
2249
2397
|
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2250
2398
|
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2251
2399
|
const parts = route.split(" ");
|
|
2252
2400
|
const method = parts[0] ?? "";
|
|
2253
2401
|
const path = parts.slice(1).join(" ");
|
|
2254
|
-
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>`;
|
|
2402
|
+
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>`;
|
|
2255
2403
|
}).join("");
|
|
2256
2404
|
};
|
|
2405
|
+
const renderRoutes = async () => {
|
|
2406
|
+
const data = await api("GET", "/");
|
|
2407
|
+
if (!isRecord2(data))
|
|
2408
|
+
throw new Error("Route list was not an object");
|
|
2409
|
+
state.routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
2410
|
+
paintRoutes();
|
|
2411
|
+
};
|
|
2257
2412
|
const renderers = {
|
|
2258
2413
|
overview: renderOverview,
|
|
2259
2414
|
state: renderState,
|
|
@@ -2262,19 +2417,50 @@ function bootAdmin(config) {
|
|
|
2262
2417
|
journal: renderJournal,
|
|
2263
2418
|
routes: renderRoutes
|
|
2264
2419
|
};
|
|
2420
|
+
let refreshToken = 0;
|
|
2265
2421
|
async function refresh() {
|
|
2422
|
+
const token = ++refreshToken;
|
|
2266
2423
|
clearError();
|
|
2424
|
+
const main = el("views", HTMLElement);
|
|
2425
|
+
const sync = el("sync-status", HTMLElement);
|
|
2426
|
+
main.setAttribute("aria-busy", "true");
|
|
2427
|
+
sync.classList.add("busy");
|
|
2428
|
+
sync.textContent = "Updating";
|
|
2267
2429
|
const render = renderers[state.view];
|
|
2268
|
-
|
|
2269
|
-
|
|
2430
|
+
try {
|
|
2431
|
+
if (render)
|
|
2432
|
+
await render();
|
|
2433
|
+
if (token === refreshToken)
|
|
2434
|
+
sync.textContent = "Up to date";
|
|
2435
|
+
} finally {
|
|
2436
|
+
if (token === refreshToken) {
|
|
2437
|
+
main.removeAttribute("aria-busy");
|
|
2438
|
+
sync.classList.remove("busy");
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2270
2441
|
}
|
|
2271
2442
|
const delay = (ms) => new Promise((resolve) => {
|
|
2272
2443
|
window.setTimeout(resolve, ms);
|
|
2273
2444
|
});
|
|
2274
|
-
const confirmAction = async (button, pending, done, work) => {
|
|
2445
|
+
const confirmAction = async (button, pending, done, work, confirmLabel) => {
|
|
2275
2446
|
if (button.dataset.pending === "1")
|
|
2276
2447
|
return;
|
|
2277
2448
|
const label = button.textContent ?? "";
|
|
2449
|
+
if (confirmLabel !== void 0 && button.dataset.confirming !== "1") {
|
|
2450
|
+
button.dataset.confirming = "1";
|
|
2451
|
+
button.classList.add("confirming");
|
|
2452
|
+
button.textContent = confirmLabel;
|
|
2453
|
+
window.setTimeout(() => {
|
|
2454
|
+
if (button.dataset.confirming !== "1")
|
|
2455
|
+
return;
|
|
2456
|
+
button.textContent = label;
|
|
2457
|
+
button.classList.remove("confirming");
|
|
2458
|
+
delete button.dataset.confirming;
|
|
2459
|
+
}, 3e3);
|
|
2460
|
+
return;
|
|
2461
|
+
}
|
|
2462
|
+
button.classList.remove("confirming");
|
|
2463
|
+
delete button.dataset.confirming;
|
|
2278
2464
|
const width = button.offsetWidth;
|
|
2279
2465
|
button.dataset.pending = "1";
|
|
2280
2466
|
button.classList.add("pressed");
|
|
@@ -2295,6 +2481,65 @@ function bootAdmin(config) {
|
|
|
2295
2481
|
delete button.dataset.pending;
|
|
2296
2482
|
}
|
|
2297
2483
|
};
|
|
2484
|
+
const wireJsonEditor = (textareaId, highlightId, statusId, shellId, allowEmpty) => {
|
|
2485
|
+
const textarea = el(textareaId, HTMLTextAreaElement);
|
|
2486
|
+
const highlight = el(highlightId, HTMLElement);
|
|
2487
|
+
const status = el(statusId, HTMLElement);
|
|
2488
|
+
const shell = el(shellId, HTMLElement);
|
|
2489
|
+
const sync = () => {
|
|
2490
|
+
const text = textarea.value;
|
|
2491
|
+
highlight.innerHTML = `${highlightJsonText(text)}
|
|
2492
|
+
`;
|
|
2493
|
+
let valid = true;
|
|
2494
|
+
let message = "Valid JSON";
|
|
2495
|
+
if (allowEmpty && text.trim() === "")
|
|
2496
|
+
message = "No body";
|
|
2497
|
+
else {
|
|
2498
|
+
try {
|
|
2499
|
+
readJson2(text);
|
|
2500
|
+
} catch {
|
|
2501
|
+
valid = false;
|
|
2502
|
+
message = "Invalid JSON";
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
shell.classList.toggle("invalid", !valid);
|
|
2506
|
+
status.classList.toggle("invalid", !valid);
|
|
2507
|
+
status.textContent = message;
|
|
2508
|
+
textarea.setAttribute("aria-invalid", valid ? "false" : "true");
|
|
2509
|
+
};
|
|
2510
|
+
const syncScroll = () => {
|
|
2511
|
+
const preview2 = highlight.parentElement;
|
|
2512
|
+
if (!(preview2 instanceof HTMLElement))
|
|
2513
|
+
return;
|
|
2514
|
+
preview2.scrollTop = textarea.scrollTop;
|
|
2515
|
+
preview2.scrollLeft = textarea.scrollLeft;
|
|
2516
|
+
};
|
|
2517
|
+
textarea.addEventListener("input", sync);
|
|
2518
|
+
textarea.addEventListener("scroll", syncScroll);
|
|
2519
|
+
sync();
|
|
2520
|
+
return sync;
|
|
2521
|
+
};
|
|
2522
|
+
const syncRouteEditor = wireJsonEditor("route-body", "route-highlight", "route-json-status", "route-editor", true);
|
|
2523
|
+
const syncRecordEditor = wireJsonEditor("editor-value", "editor-highlight", "editor-json-status", "record-editor", false);
|
|
2524
|
+
for (const button of document.querySelectorAll("[data-format]")) {
|
|
2525
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2526
|
+
continue;
|
|
2527
|
+
button.addEventListener("click", () => {
|
|
2528
|
+
const target = button.dataset.format;
|
|
2529
|
+
if (target === void 0)
|
|
2530
|
+
return;
|
|
2531
|
+
const textarea = document.getElementById(target);
|
|
2532
|
+
if (!(textarea instanceof HTMLTextAreaElement) || textarea.value.trim() === "")
|
|
2533
|
+
return;
|
|
2534
|
+
try {
|
|
2535
|
+
textarea.value = JSON.stringify(readJson2(textarea.value), null, 2) ?? "";
|
|
2536
|
+
textarea.dispatchEvent(new Event("input"));
|
|
2537
|
+
textarea.focus();
|
|
2538
|
+
} catch {
|
|
2539
|
+
showError(new Error("Fix the JSON before formatting it"));
|
|
2540
|
+
}
|
|
2541
|
+
});
|
|
2542
|
+
}
|
|
2298
2543
|
document.addEventListener("click", (event) => {
|
|
2299
2544
|
const target = event.target;
|
|
2300
2545
|
if (!(target instanceof Element))
|
|
@@ -2367,6 +2612,7 @@ function bootAdmin(config) {
|
|
|
2367
2612
|
} else {
|
|
2368
2613
|
editorValue.value = "{\n \n}";
|
|
2369
2614
|
}
|
|
2615
|
+
syncRecordEditor();
|
|
2370
2616
|
editor.showModal();
|
|
2371
2617
|
}
|
|
2372
2618
|
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -2396,10 +2642,10 @@ function bootAdmin(config) {
|
|
|
2396
2642
|
const id = state.editing;
|
|
2397
2643
|
if (!id)
|
|
2398
2644
|
return;
|
|
2399
|
-
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2645
|
+
confirmAction(editorDelete, "Deleting...", "Deleted", () => api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2400
2646
|
editor.close();
|
|
2401
2647
|
return renderState();
|
|
2402
|
-
}).catch(showError);
|
|
2648
|
+
}), "Confirm delete").catch(showError);
|
|
2403
2649
|
});
|
|
2404
2650
|
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2405
2651
|
if (!(button instanceof HTMLButtonElement))
|
|
@@ -2445,10 +2691,10 @@ function bootAdmin(config) {
|
|
|
2445
2691
|
}).then(() => renderFaults()).catch(showError);
|
|
2446
2692
|
});
|
|
2447
2693
|
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2448
|
-
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
|
|
2694
|
+
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults()), "Confirm clear").catch(showError);
|
|
2449
2695
|
});
|
|
2450
2696
|
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2451
|
-
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
|
|
2697
|
+
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal()), "Confirm clear").catch(showError);
|
|
2452
2698
|
});
|
|
2453
2699
|
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2454
2700
|
const target = event.target;
|
|
@@ -2462,9 +2708,15 @@ function bootAdmin(config) {
|
|
|
2462
2708
|
return;
|
|
2463
2709
|
state.route = route;
|
|
2464
2710
|
el("route-title", HTMLElement).textContent = route;
|
|
2711
|
+
el("route-send", HTMLButtonElement).disabled = false;
|
|
2465
2712
|
const method = route.split(" ")[0] ?? "";
|
|
2466
|
-
el("route-body", HTMLTextAreaElement)
|
|
2713
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
2714
|
+
routeBody.disabled = method === "GET" || method === "DELETE";
|
|
2715
|
+
el("route-editor", HTMLElement).hidden = routeBody.disabled;
|
|
2716
|
+
el("route-json-status", HTMLElement).parentElement?.toggleAttribute("hidden", routeBody.disabled);
|
|
2717
|
+
syncRouteEditor();
|
|
2467
2718
|
});
|
|
2719
|
+
el("route-search", HTMLInputElement).addEventListener("input", paintRoutes);
|
|
2468
2720
|
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2469
2721
|
event.preventDefault();
|
|
2470
2722
|
const send = async () => {
|
|
@@ -2486,7 +2738,8 @@ function bootAdmin(config) {
|
|
|
2486
2738
|
}
|
|
2487
2739
|
}
|
|
2488
2740
|
const data = await api(method, concrete, body);
|
|
2489
|
-
|
|
2741
|
+
const text = JSON.stringify(data, null, 2) ?? "null";
|
|
2742
|
+
el("route-result", HTMLElement).innerHTML = `<code>${highlightJsonText(text)}</code>`;
|
|
2490
2743
|
};
|
|
2491
2744
|
send().catch(showError);
|
|
2492
2745
|
});
|
|
@@ -2604,11 +2857,13 @@ function bootAdmin(config) {
|
|
|
2604
2857
|
views.append(section);
|
|
2605
2858
|
mountSqlExplorer(body, panelApi);
|
|
2606
2859
|
}
|
|
2860
|
+
show(document.getElementById(`view-${state.view}`) === null ? "overview" : state.view);
|
|
2607
2861
|
return refresh();
|
|
2608
2862
|
}).catch(showError);
|
|
2609
2863
|
}
|
|
2610
2864
|
var clientPrelude = [
|
|
2611
2865
|
isRecord2,
|
|
2866
|
+
highlightJsonText,
|
|
2612
2867
|
mountAdminBrand,
|
|
2613
2868
|
startAdminBrand,
|
|
2614
2869
|
faultPresetList,
|
|
@@ -2631,6 +2886,7 @@ ${entry}(${payload});
|
|
|
2631
2886
|
var defineMock = (runtime) => runtime;
|
|
2632
2887
|
var STANDARD_ADMIN_ROUTES = [
|
|
2633
2888
|
"GET /",
|
|
2889
|
+
"GET /health",
|
|
2634
2890
|
"POST /reset",
|
|
2635
2891
|
"GET /namespaces",
|
|
2636
2892
|
"GET /clock",
|
|
@@ -2710,11 +2966,11 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
2710
2966
|
}
|
|
2711
2967
|
});
|
|
2712
2968
|
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
2713
|
-
var renderAdminDocument = (service) => {
|
|
2969
|
+
var renderAdminDocument = (service, adminPrefix = ADMIN_PREFIX) => {
|
|
2714
2970
|
const name = escapeHtml(service);
|
|
2715
2971
|
const script = adminClientSource({
|
|
2716
2972
|
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
2717
|
-
adminPrefix
|
|
2973
|
+
adminPrefix,
|
|
2718
2974
|
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
2719
2975
|
brandsUrl: ADMIN_BRANDS_URL,
|
|
2720
2976
|
service
|
|
@@ -2731,75 +2987,99 @@ var renderAdminDocument = (service) => {
|
|
|
2731
2987
|
${CSS_RESET}
|
|
2732
2988
|
:root {
|
|
2733
2989
|
color-scheme: light;
|
|
2734
|
-
--bg: #
|
|
2990
|
+
--bg: #f7f8fb;
|
|
2735
2991
|
--bg-elev: #ffffff;
|
|
2736
|
-
--bg-muted: #
|
|
2737
|
-
--bg-inset: #
|
|
2738
|
-
--line: #
|
|
2739
|
-
--line-strong: #
|
|
2740
|
-
--ink: #
|
|
2741
|
-
--muted: #
|
|
2742
|
-
--faint: #
|
|
2743
|
-
--accent: #
|
|
2992
|
+
--bg-muted: #f1f3f7;
|
|
2993
|
+
--bg-inset: #f8f9fc;
|
|
2994
|
+
--line: #e4e7ec;
|
|
2995
|
+
--line-strong: #cfd4dc;
|
|
2996
|
+
--ink: #101828;
|
|
2997
|
+
--muted: #475467;
|
|
2998
|
+
--faint: #667085;
|
|
2999
|
+
--accent: #6941c6;
|
|
3000
|
+
--accent-hover: #5933b4;
|
|
2744
3001
|
--accent-ink: #ffffff;
|
|
2745
|
-
--accent-soft: #
|
|
2746
|
-
--ok: #
|
|
2747
|
-
--ok-soft: #
|
|
2748
|
-
--warn: #
|
|
2749
|
-
--warn-soft: #
|
|
2750
|
-
--err: #
|
|
2751
|
-
--err-soft: #
|
|
2752
|
-
--
|
|
3002
|
+
--accent-soft: #f4f0ff;
|
|
3003
|
+
--ok: #067647;
|
|
3004
|
+
--ok-soft: #ecfdf3;
|
|
3005
|
+
--warn: #b54708;
|
|
3006
|
+
--warn-soft: #fffaeb;
|
|
3007
|
+
--err: #b42318;
|
|
3008
|
+
--err-soft: #fef3f2;
|
|
3009
|
+
--code-key: #6941c6;
|
|
3010
|
+
--code-string: #067647;
|
|
3011
|
+
--code-number: #175cd3;
|
|
3012
|
+
--code-boolean: #c11574;
|
|
3013
|
+
--code-null: #667085;
|
|
3014
|
+
--shadow-xs: 0 1px 2px rgb(16 24 40 / 0.05);
|
|
3015
|
+
--shadow-sm: 0 1px 3px rgb(16 24 40 / 0.08), 0 1px 2px rgb(16 24 40 / 0.04);
|
|
3016
|
+
--shadow-lg: 0 18px 48px rgb(16 24 40 / 0.16), 0 4px 12px rgb(16 24 40 / 0.08);
|
|
2753
3017
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2754
3018
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
2755
|
-
--radius:
|
|
2756
|
-
--header:
|
|
2757
|
-
--nav:
|
|
3019
|
+
--radius: 12px;
|
|
3020
|
+
--header: 72px;
|
|
3021
|
+
--nav: 224px;
|
|
2758
3022
|
}
|
|
2759
3023
|
:root[data-theme="dark"] {
|
|
2760
3024
|
color-scheme: dark;
|
|
2761
|
-
--bg: #
|
|
2762
|
-
--bg-elev: #
|
|
2763
|
-
--bg-muted: #
|
|
2764
|
-
--bg-inset: #
|
|
2765
|
-
--line: #
|
|
2766
|
-
--line-strong: #
|
|
2767
|
-
--ink: #
|
|
2768
|
-
--muted: #
|
|
2769
|
-
--faint: #
|
|
2770
|
-
--accent: #
|
|
2771
|
-
--accent-
|
|
2772
|
-
--accent-
|
|
2773
|
-
--
|
|
2774
|
-
--ok
|
|
2775
|
-
--
|
|
2776
|
-
--warn
|
|
2777
|
-
--
|
|
2778
|
-
--err
|
|
2779
|
-
--
|
|
3025
|
+
--bg: #0c0e14;
|
|
3026
|
+
--bg-elev: #151821;
|
|
3027
|
+
--bg-muted: #20232e;
|
|
3028
|
+
--bg-inset: #11141c;
|
|
3029
|
+
--line: #2b303d;
|
|
3030
|
+
--line-strong: #414858;
|
|
3031
|
+
--ink: #f5f7fa;
|
|
3032
|
+
--muted: #b6bdc9;
|
|
3033
|
+
--faint: #8d96a7;
|
|
3034
|
+
--accent: #9e77ed;
|
|
3035
|
+
--accent-hover: #b692f6;
|
|
3036
|
+
--accent-ink: #ffffff;
|
|
3037
|
+
--accent-soft: #2b2142;
|
|
3038
|
+
--ok: #75e0a7;
|
|
3039
|
+
--ok-soft: #102a20;
|
|
3040
|
+
--warn: #fec84b;
|
|
3041
|
+
--warn-soft: #302611;
|
|
3042
|
+
--err: #fda29b;
|
|
3043
|
+
--err-soft: #321817;
|
|
3044
|
+
--code-key: #c3a6ff;
|
|
3045
|
+
--code-string: #75e0a7;
|
|
3046
|
+
--code-number: #84adff;
|
|
3047
|
+
--code-boolean: #f9a8d4;
|
|
3048
|
+
--code-null: #98a2b3;
|
|
3049
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3050
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3051
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2780
3052
|
}
|
|
2781
3053
|
@media (prefers-color-scheme: dark) {
|
|
2782
3054
|
:root:not([data-theme="light"]) {
|
|
2783
3055
|
color-scheme: dark;
|
|
2784
|
-
--bg: #
|
|
2785
|
-
--bg-elev: #
|
|
2786
|
-
--bg-muted: #
|
|
2787
|
-
--bg-inset: #
|
|
2788
|
-
--line: #
|
|
2789
|
-
--line-strong: #
|
|
2790
|
-
--ink: #
|
|
2791
|
-
--muted: #
|
|
2792
|
-
--faint: #
|
|
2793
|
-
--accent: #
|
|
2794
|
-
--accent-
|
|
2795
|
-
--accent-
|
|
2796
|
-
--
|
|
2797
|
-
--ok
|
|
2798
|
-
--
|
|
2799
|
-
--warn
|
|
2800
|
-
--
|
|
2801
|
-
--err
|
|
2802
|
-
--
|
|
3056
|
+
--bg: #0c0e14;
|
|
3057
|
+
--bg-elev: #151821;
|
|
3058
|
+
--bg-muted: #20232e;
|
|
3059
|
+
--bg-inset: #11141c;
|
|
3060
|
+
--line: #2b303d;
|
|
3061
|
+
--line-strong: #414858;
|
|
3062
|
+
--ink: #f5f7fa;
|
|
3063
|
+
--muted: #b6bdc9;
|
|
3064
|
+
--faint: #8d96a7;
|
|
3065
|
+
--accent: #9e77ed;
|
|
3066
|
+
--accent-hover: #b692f6;
|
|
3067
|
+
--accent-ink: #ffffff;
|
|
3068
|
+
--accent-soft: #2b2142;
|
|
3069
|
+
--ok: #75e0a7;
|
|
3070
|
+
--ok-soft: #102a20;
|
|
3071
|
+
--warn: #fec84b;
|
|
3072
|
+
--warn-soft: #302611;
|
|
3073
|
+
--err: #fda29b;
|
|
3074
|
+
--err-soft: #321817;
|
|
3075
|
+
--code-key: #c3a6ff;
|
|
3076
|
+
--code-string: #75e0a7;
|
|
3077
|
+
--code-number: #84adff;
|
|
3078
|
+
--code-boolean: #f9a8d4;
|
|
3079
|
+
--code-null: #98a2b3;
|
|
3080
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3081
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3082
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2803
3083
|
}
|
|
2804
3084
|
}
|
|
2805
3085
|
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
@@ -2817,13 +3097,17 @@ var renderAdminDocument = (service) => {
|
|
|
2817
3097
|
padding-right: env(safe-area-inset-right);
|
|
2818
3098
|
-webkit-text-size-adjust: 100%;
|
|
2819
3099
|
}
|
|
2820
|
-
|
|
3100
|
+
::selection { background: color-mix(in srgb, var(--accent) 28%, transparent); }
|
|
3101
|
+
:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px; }
|
|
3102
|
+
button, input, select, textarea { transition: border-color 140ms ease, box-shadow 140ms ease, transform 100ms ease; }
|
|
3103
|
+
button:disabled, input:disabled, select:disabled, textarea:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
2821
3104
|
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
2822
3105
|
header.top {
|
|
2823
|
-
position: sticky; top: 0; z-index:
|
|
3106
|
+
position: sticky; top: 0; z-index: 20;
|
|
2824
3107
|
display: flex; align-items: center; gap: 12px;
|
|
2825
|
-
min-height: var(--header); padding:
|
|
2826
|
-
background: var(--bg-elev);
|
|
3108
|
+
min-height: var(--header); padding: 12px 20px;
|
|
3109
|
+
background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
|
|
3110
|
+
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
2827
3111
|
border-bottom: 1px solid var(--line);
|
|
2828
3112
|
min-width: 0; max-width: 100%;
|
|
2829
3113
|
}
|
|
@@ -2831,12 +3115,21 @@ var renderAdminDocument = (service) => {
|
|
|
2831
3115
|
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
2832
3116
|
min-width: 0; max-width: 100%; margin-right: auto;
|
|
2833
3117
|
}
|
|
2834
|
-
.brand-
|
|
2835
|
-
.brand-
|
|
3118
|
+
.brand-lockup { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
3119
|
+
.brand-mark {
|
|
3120
|
+
width: 38px; height: 38px; display: grid; place-items: center; flex: none;
|
|
3121
|
+
border-radius: 11px; color: #fff;
|
|
3122
|
+
background: linear-gradient(145deg, #7f56d9 0%, #53389e 100%);
|
|
3123
|
+
box-shadow: 0 6px 16px rgb(105 65 198 / 0.24), inset 0 1px 0 rgb(255 255 255 / 0.25);
|
|
3124
|
+
}
|
|
3125
|
+
.brand-mark svg { width: 22px; height: 22px; }
|
|
3126
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; line-height: 1.25; }
|
|
3127
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
2836
3128
|
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
2837
3129
|
.vendor {
|
|
2838
3130
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
2839
3131
|
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
3132
|
+
padding: 5px 8px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-inset);
|
|
2840
3133
|
}
|
|
2841
3134
|
.vendor[hidden] { display: none; }
|
|
2842
3135
|
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
@@ -2848,12 +3141,17 @@ var renderAdminDocument = (service) => {
|
|
|
2848
3141
|
.vendor-sep { color: var(--faint); }
|
|
2849
3142
|
.vendor-link { color: inherit; text-decoration: none; }
|
|
2850
3143
|
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
2851
|
-
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items:
|
|
2852
|
-
|
|
3144
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
|
|
3145
|
+
.sync-status { align-self: center; display: flex; align-items: center; gap: 6px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3146
|
+
.sync-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
|
|
3147
|
+
.sync-status.busy::before { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); animation: pulse 1.2s ease-in-out infinite; }
|
|
3148
|
+
label.field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; font-weight: 600; color: var(--faint); }
|
|
2853
3149
|
label.field input, label.field select, .controls input[type="password"] {
|
|
2854
|
-
background-color: var(--bg-
|
|
2855
|
-
padding:
|
|
3150
|
+
background-color: var(--bg-elev); border: 1px solid var(--line-strong); border-radius: 9px;
|
|
3151
|
+
padding: 8px 10px; min-height: 38px; line-height: 1.2; box-shadow: var(--shadow-xs);
|
|
2856
3152
|
}
|
|
3153
|
+
label.field input:hover, label.field select:hover, textarea:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--line-strong)); }
|
|
3154
|
+
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; }
|
|
2857
3155
|
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
2858
3156
|
the text fields in the same form. Paint it like those fields. */
|
|
2859
3157
|
label.field select {
|
|
@@ -2870,12 +3168,20 @@ var renderAdminDocument = (service) => {
|
|
|
2870
3168
|
dialog .field > textarea { width: 100%; }
|
|
2871
3169
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
2872
3170
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
.btn
|
|
3171
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
|
3172
|
+
border-radius: 9px; min-height: 38px; padding: 0 13px; border: 1px solid var(--line-strong);
|
|
3173
|
+
background: var(--bg-elev); color: var(--ink); font-size: 13px; font-weight: 600; line-height: 1;
|
|
3174
|
+
box-shadow: var(--shadow-xs);
|
|
3175
|
+
}
|
|
3176
|
+
.icon-btn { width: 38px; padding: 0; }
|
|
3177
|
+
.icon-btn svg, .btn svg, .btn-primary svg { width: 16px; height: 16px; }
|
|
3178
|
+
.icon-btn:hover, .btn:hover { background: var(--bg-muted); border-color: var(--line-strong); }
|
|
3179
|
+
.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; }
|
|
3180
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
3181
|
+
.btn-quiet { background: transparent; border-color: transparent; box-shadow: none; }
|
|
3182
|
+
.btn-quiet:hover { background: var(--bg-muted); }
|
|
3183
|
+
.btn-danger { color: var(--err); border-color: color-mix(in srgb, var(--err) 25%, var(--line)); }
|
|
3184
|
+
.btn-danger:hover, .btn-danger.confirming { background: var(--err-soft); border-color: color-mix(in srgb, var(--err) 45%, var(--line)); }
|
|
2879
3185
|
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
2880
3186
|
so the press itself has to stay visible. */
|
|
2881
3187
|
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
@@ -2891,93 +3197,159 @@ var renderAdminDocument = (service) => {
|
|
|
2891
3197
|
flex: 1 0 auto; min-width: 0;
|
|
2892
3198
|
}
|
|
2893
3199
|
nav.side {
|
|
2894
|
-
|
|
3200
|
+
position: sticky; top: var(--header); align-self: start; height: calc(100vh - var(--header));
|
|
3201
|
+
border-right: 1px solid var(--line); padding: 18px 12px;
|
|
2895
3202
|
display: flex; flex-direction: column; gap: 4px;
|
|
2896
|
-
background: var(--bg-elev);
|
|
3203
|
+
background: var(--bg-elev); overflow-y: auto;
|
|
2897
3204
|
}
|
|
3205
|
+
.nav-label { padding: 0 10px 8px; color: var(--faint); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
|
|
2898
3206
|
nav.side button {
|
|
3207
|
+
position: relative; display: flex; align-items: center; gap: 10px; width: 100%;
|
|
2899
3208
|
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
2900
|
-
padding: 9px 10px; min-height:
|
|
2901
|
-
}
|
|
2902
|
-
nav.side button
|
|
2903
|
-
|
|
2904
|
-
.
|
|
2905
|
-
.
|
|
2906
|
-
.
|
|
3209
|
+
padding: 9px 10px; min-height: 42px; color: var(--muted); font-size: 13px; font-weight: 550;
|
|
3210
|
+
}
|
|
3211
|
+
nav.side button:hover { background: var(--bg-muted); color: var(--ink); }
|
|
3212
|
+
nav.side button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 650; }
|
|
3213
|
+
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); }
|
|
3214
|
+
.nav-icon { width: 18px; height: 18px; display: grid; place-items: center; flex: none; }
|
|
3215
|
+
.nav-icon svg { width: 18px; height: 18px; }
|
|
3216
|
+
main { padding: 32px clamp(20px, 4vw, 48px) 64px; max-width: 1320px; width: 100%; min-width: 0; }
|
|
3217
|
+
main[aria-busy="true"] .view { opacity: 0.62; }
|
|
3218
|
+
.view { animation: view-in 180ms ease-out; }
|
|
3219
|
+
.view[hidden] { display: none; }
|
|
3220
|
+
.view-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
|
|
3221
|
+
.view h2 { margin: 0 0 5px; font-size: clamp(24px, 3vw, 30px); line-height: 1.2; letter-spacing: -0.035em; }
|
|
3222
|
+
.view h3 { margin: 24px 0 10px; font-size: 15px; letter-spacing: -0.01em; }
|
|
3223
|
+
.lede { margin: 0; color: var(--muted); max-width: 72ch; font-size: 14px; }
|
|
3224
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 175px), 1fr)); gap: 14px; }
|
|
2907
3225
|
.card {
|
|
2908
3226
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
2909
|
-
padding:
|
|
3227
|
+
padding: 18px; box-shadow: var(--shadow-sm);
|
|
2910
3228
|
}
|
|
2911
|
-
.card
|
|
2912
|
-
.card
|
|
3229
|
+
.stat-card { position: relative; overflow: hidden; }
|
|
3230
|
+
.stat-card::after { content: ""; position: absolute; inset: 0 0 auto; height: 2px; background: linear-gradient(90deg, var(--accent), transparent 78%); opacity: 0.7; }
|
|
3231
|
+
.card .k { color: var(--faint); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
3232
|
+
.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; }
|
|
2913
3233
|
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
2914
|
-
.
|
|
3234
|
+
.section-card { background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-xs); overflow: hidden; }
|
|
3235
|
+
.section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
|
|
3236
|
+
.section-title h3 { margin: 0; }
|
|
3237
|
+
.split { display: grid; grid-template-columns: minmax(0, 250px) minmax(0, 1fr); gap: 18px; align-items: start; }
|
|
3238
|
+
#collections { padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); box-shadow: var(--shadow-xs); }
|
|
2915
3239
|
.collection {
|
|
2916
3240
|
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
2917
|
-
border-radius:
|
|
3241
|
+
border-radius: 9px; padding: 10px 11px; min-height: 48px; color: var(--ink); cursor: pointer;
|
|
2918
3242
|
}
|
|
2919
|
-
.collection
|
|
3243
|
+
.collection:hover { background: var(--bg-muted); }
|
|
3244
|
+
.collection[aria-current="true"] { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 18%, var(--line)); color: var(--accent); }
|
|
2920
3245
|
.collection small { display: block; color: var(--faint); }
|
|
2921
3246
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
2922
|
-
.chip {
|
|
2923
|
-
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
2924
|
-
border-radius: 999px; padding: 4px 8px;
|
|
2925
|
-
}
|
|
3247
|
+
.chip { font-family: var(--mono); font-size: 11px; background: var(--bg-muted); border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; }
|
|
2926
3248
|
.chip i { color: var(--faint); font-style: normal; }
|
|
2927
3249
|
.sql-layout {
|
|
2928
3250
|
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
2929
3251
|
}
|
|
2930
|
-
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
2931
|
-
.sql-tables button { text-align: left; }
|
|
3252
|
+
.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); }
|
|
3253
|
+
.sql-tables button { text-align: left; border: 0; border-radius: 8px; padding: 9px 10px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
3254
|
+
.sql-tables button:hover, .sql-tables button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); }
|
|
2932
3255
|
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
2933
|
-
table { width: 100%; border-
|
|
2934
|
-
|
|
2935
|
-
th {
|
|
3256
|
+
.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); }
|
|
3257
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
3258
|
+
th, td { text-align: left; padding: 11px 13px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
3259
|
+
th { color: var(--faint); background: var(--bg-inset); font-weight: 650; font-size: 11px; text-transform: uppercase; letter-spacing: 0.045em; }
|
|
3260
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
2936
3261
|
tr.clickable { cursor: pointer; }
|
|
2937
|
-
tr.clickable:hover { background: var(--
|
|
3262
|
+
tr.clickable:hover { background: var(--accent-soft); }
|
|
2938
3263
|
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
2939
3264
|
pre {
|
|
2940
3265
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
2941
|
-
background: var(--bg-inset); border-radius: 10px; padding:
|
|
2942
|
-
}
|
|
2943
|
-
.
|
|
3266
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 10px; padding: 14px; font-size: 12px; line-height: 1.65;
|
|
3267
|
+
}
|
|
3268
|
+
.json-key { color: var(--code-key); }
|
|
3269
|
+
.json-string { color: var(--code-string); }
|
|
3270
|
+
.json-number { color: var(--code-number); }
|
|
3271
|
+
.json-boolean { color: var(--code-boolean); }
|
|
3272
|
+
.json-null { color: var(--code-null); font-style: italic; }
|
|
3273
|
+
.json-inline { display: block; max-width: 44rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
3274
|
+
.json-view { position: relative; max-height: 26rem; overflow: auto; }
|
|
3275
|
+
.json-editor { position: relative; min-height: 190px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--bg-inset); overflow: hidden; }
|
|
3276
|
+
.json-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); }
|
|
3277
|
+
.json-editor.invalid { border-color: var(--err); }
|
|
3278
|
+
.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; }
|
|
3279
|
+
.json-editor pre { position: absolute; inset: 0; pointer-events: none; background: transparent; }
|
|
3280
|
+
.json-editor textarea { position: relative; z-index: 1; resize: vertical; background: transparent; color: transparent; caret-color: var(--ink); -webkit-text-fill-color: transparent; }
|
|
3281
|
+
.json-editor textarea::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); }
|
|
3282
|
+
.editor-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; }
|
|
3283
|
+
.json-status { display: inline-flex; align-items: center; gap: 6px; color: var(--ok); font-size: 11px; }
|
|
3284
|
+
.json-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
3285
|
+
.json-status.invalid { color: var(--err); }
|
|
3286
|
+
.format-json { border: 0; padding: 3px 5px; background: transparent; color: var(--accent); font-size: 11px; font-weight: 650; cursor: pointer; }
|
|
3287
|
+
.row-actions { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 14px 0; }
|
|
2944
3288
|
.banner {
|
|
2945
|
-
display: none;
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
3289
|
+
display: none; position: sticky; top: calc(var(--header) + 12px); z-index: 15;
|
|
3290
|
+
align-items: center; gap: 10px; margin: 0 0 16px; padding: 11px 12px 11px 14px;
|
|
3291
|
+
border: 1px solid color-mix(in srgb, var(--err) 30%, var(--line)); border-radius: 10px;
|
|
3292
|
+
background: var(--err-soft); color: var(--err); box-shadow: var(--shadow-sm); font-size: 13px;
|
|
3293
|
+
}
|
|
3294
|
+
.banner.show { display: flex; }
|
|
3295
|
+
.banner span { flex: 1; }
|
|
3296
|
+
.banner button { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 7px; background: transparent; color: currentColor; cursor: pointer; }
|
|
3297
|
+
.banner button:hover { background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
3298
|
+
.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); }
|
|
2950
3299
|
dialog {
|
|
2951
|
-
border: 1px solid var(--line); border-radius:
|
|
2952
|
-
color: var(--ink); width: min(
|
|
2953
|
-
}
|
|
2954
|
-
dialog
|
|
2955
|
-
dialog
|
|
3300
|
+
border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--bg-elev);
|
|
3301
|
+
color: var(--ink); width: min(720px, calc(100% - 24px)); max-width: calc(100vw - 24px); box-shadow: var(--shadow-lg);
|
|
3302
|
+
}
|
|
3303
|
+
dialog[open] { animation: dialog-in 160ms ease-out; }
|
|
3304
|
+
dialog::backdrop { background: rgb(12 14 20 / 0.58); backdrop-filter: blur(3px); }
|
|
3305
|
+
dialog form, .dialog-body { padding: 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
3306
|
+
.dialog-heading { display: flex; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
|
3307
|
+
.dialog-heading strong { font-size: 18px; letter-spacing: -0.02em; }
|
|
3308
|
+
.dialog-heading span { color: var(--muted); font-size: 12px; }
|
|
2956
3309
|
textarea {
|
|
2957
3310
|
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
2958
3311
|
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
2959
3312
|
font-family: var(--mono); font-size: 12px;
|
|
2960
3313
|
}
|
|
2961
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
2962
|
-
.
|
|
3314
|
+
.dialog-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding-top: 4px; }
|
|
3315
|
+
.dialog-actions .btn-danger { margin-right: auto; }
|
|
3316
|
+
.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); }
|
|
2963
3317
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
2964
3318
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
2965
3319
|
.status-2 { color: var(--ok); }
|
|
2966
3320
|
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
2967
3321
|
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
3322
|
+
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
3323
|
+
.form-grid .wide { grid-column: span 2; }
|
|
2968
3324
|
.route {
|
|
2969
|
-
display: grid; grid-template-columns:
|
|
2970
|
-
padding:
|
|
2971
|
-
}
|
|
2972
|
-
.
|
|
3325
|
+
display: grid; grid-template-columns: 84px minmax(0, 1fr) auto; gap: 10px; align-items: center;
|
|
3326
|
+
padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--bg-elev);
|
|
3327
|
+
}
|
|
3328
|
+
.route:first-child { border-radius: var(--radius) var(--radius) 0 0; }
|
|
3329
|
+
.route:last-child { border-bottom: 0; border-radius: 0 0 var(--radius) var(--radius); }
|
|
3330
|
+
.route-tools { display: flex; align-items: flex-end; gap: 12px; margin-bottom: 12px; }
|
|
3331
|
+
.route-tools .field { flex: 1; }
|
|
3332
|
+
.route-tools input { width: 100%; }
|
|
3333
|
+
.route-count { padding-bottom: 10px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3334
|
+
#route-list { max-height: 34rem; border: 1px solid var(--line); border-radius: var(--radius); overflow: auto; box-shadow: var(--shadow-xs); }
|
|
3335
|
+
.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; }
|
|
3336
|
+
@keyframes view-in { from { transform: translateY(3px); } to { transform: translateY(0); } }
|
|
3337
|
+
@keyframes dialog-in { from { transform: translateY(8px) scale(0.99); } to { transform: translateY(0) scale(1); } }
|
|
3338
|
+
@keyframes pulse { 50% { opacity: 0.45; } }
|
|
3339
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
|
|
3340
|
+
@media (forced-colors: active) { .json-editor pre { display: none; } .json-editor textarea { color: CanvasText; -webkit-text-fill-color: CanvasText; } }
|
|
2973
3341
|
@media (max-width: 800px) {
|
|
3342
|
+
:root { --header: 0px; }
|
|
2974
3343
|
header.top {
|
|
2975
|
-
align-items: stretch; flex-direction: column;
|
|
2976
|
-
padding
|
|
3344
|
+
position: relative; align-items: stretch; flex-direction: column;
|
|
3345
|
+
padding: max(12px, env(safe-area-inset-top)) 14px 12px;
|
|
2977
3346
|
}
|
|
2978
3347
|
input, select, textarea { font-size: 16px; }
|
|
2979
3348
|
.brand, .vendor, .controls { width: 100%; }
|
|
3349
|
+
.brand { gap: 10px; }
|
|
3350
|
+
.vendor { order: 2; border-radius: 9px; }
|
|
2980
3351
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
3352
|
+
.sync-status { display: none; }
|
|
2981
3353
|
/* The layout fills the screen. A single-column grid stretches every auto
|
|
2982
3354
|
row, so the tab bar grew with the leftover height and the buttons floated
|
|
2983
3355
|
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
@@ -2987,14 +3359,23 @@ var renderAdminDocument = (service) => {
|
|
|
2987
3359
|
align-content: start;
|
|
2988
3360
|
}
|
|
2989
3361
|
nav.side {
|
|
2990
|
-
align-self: start; height: auto;
|
|
3362
|
+
position: sticky; top: 0; z-index: 12; align-self: start; height: auto;
|
|
2991
3363
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
2992
3364
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
2993
|
-
padding: 8px;
|
|
3365
|
+
padding: 8px; box-shadow: var(--shadow-xs);
|
|
2994
3366
|
}
|
|
2995
|
-
nav
|
|
3367
|
+
.nav-label { display: none; }
|
|
3368
|
+
nav.side button { width: auto; white-space: nowrap; flex: none; }
|
|
3369
|
+
nav.side button[aria-current="true"]::before { inset: auto 9px -8px; width: auto; height: 3px; border-radius: 3px 3px 0 0; }
|
|
2996
3370
|
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
2997
|
-
|
|
3371
|
+
.form-grid { grid-template-columns: minmax(0, 1fr); }
|
|
3372
|
+
.form-grid .wide { grid-column: auto; }
|
|
3373
|
+
main { padding: 22px 14px 48px; }
|
|
3374
|
+
.view-head { margin-bottom: 18px; }
|
|
3375
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
3376
|
+
.card { padding: 14px; }
|
|
3377
|
+
.card .v { font-size: 20px; }
|
|
3378
|
+
.table-shell { border: 0; background: transparent; box-shadow: none; overflow: visible; }
|
|
2998
3379
|
table, thead, tbody, tr, th, td { display: block; }
|
|
2999
3380
|
thead { display: none; }
|
|
3000
3381
|
tr {
|
|
@@ -3009,50 +3390,63 @@ var renderAdminDocument = (service) => {
|
|
|
3009
3390
|
.route { grid-template-columns: 72px 1fr; }
|
|
3010
3391
|
.route button { grid-column: 1 / -1; }
|
|
3011
3392
|
}
|
|
3393
|
+
@media (max-width: 460px) {
|
|
3394
|
+
.controls label.field { min-width: calc(50% - 4px); }
|
|
3395
|
+
.controls .icon-btn, .controls .btn { flex: 1; }
|
|
3396
|
+
.grid { grid-template-columns: minmax(0, 1fr); }
|
|
3397
|
+
.dialog-actions { flex-wrap: wrap; }
|
|
3398
|
+
.dialog-actions button { flex: 1; }
|
|
3399
|
+
.dialog-actions .btn-danger { flex-basis: 100%; margin-right: 0; }
|
|
3400
|
+
}
|
|
3012
3401
|
</style>
|
|
3013
3402
|
</head>
|
|
3014
3403
|
<body>
|
|
3015
3404
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
3016
3405
|
<header class="top">
|
|
3017
3406
|
<div class="brand">
|
|
3018
|
-
<div class="brand-
|
|
3019
|
-
<
|
|
3020
|
-
|
|
3407
|
+
<div class="brand-lockup">
|
|
3408
|
+
<div class="brand-mark" aria-hidden="true">
|
|
3409
|
+
<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>
|
|
3410
|
+
</div>
|
|
3411
|
+
<div class="brand-id">
|
|
3412
|
+
<strong>${name}</strong>
|
|
3413
|
+
<span>Mock admin</span>
|
|
3414
|
+
</div>
|
|
3021
3415
|
</div>
|
|
3022
3416
|
<div class="vendor" id="vendor" hidden></div>
|
|
3023
3417
|
</div>
|
|
3024
3418
|
<div class="controls">
|
|
3419
|
+
<span class="sync-status" id="sync-status" role="status" aria-live="polite">Ready</span>
|
|
3025
3420
|
<label class="field">Namespace
|
|
3026
3421
|
<select id="namespace" aria-label="Namespace"></select>
|
|
3027
3422
|
</label>
|
|
3028
3423
|
<label class="field">Admin key
|
|
3029
3424
|
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
3030
3425
|
</label>
|
|
3031
|
-
<button class="icon-btn" id="theme" type="button" aria-label="
|
|
3032
|
-
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
3426
|
+
<button class="icon-btn" id="theme" type="button" aria-label="Use dark theme" title="Use dark theme"></button>
|
|
3427
|
+
<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>
|
|
3033
3428
|
</div>
|
|
3034
3429
|
</header>
|
|
3035
3430
|
<div class="layout">
|
|
3036
3431
|
<nav class="side" id="nav" aria-label="Admin sections">
|
|
3037
|
-
<
|
|
3038
|
-
<button type="button" data-view="
|
|
3039
|
-
<button type="button" data-view="
|
|
3040
|
-
<button type="button" data-view="
|
|
3041
|
-
<button type="button" data-view="
|
|
3042
|
-
<button type="button" data-view="
|
|
3432
|
+
<span class="nav-label">Workspace</span>
|
|
3433
|
+
<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>
|
|
3434
|
+
<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>
|
|
3435
|
+
<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>
|
|
3436
|
+
<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>
|
|
3437
|
+
<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>
|
|
3438
|
+
<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>
|
|
3043
3439
|
</nav>
|
|
3044
3440
|
<main id="views">
|
|
3045
|
-
<
|
|
3441
|
+
<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>
|
|
3046
3442
|
<section class="view" id="view-overview">
|
|
3047
|
-
<h2>Overview</h2>
|
|
3048
|
-
<p class="lede">Health, clock, and the collections this namespace is holding.</p>
|
|
3443
|
+
<div class="view-head"><div><h2>Overview</h2><p class="lede">Health, clock, and the collections this namespace is holding.</p></div></div>
|
|
3049
3444
|
<div class="grid" id="overview-cards"></div>
|
|
3050
3445
|
<h3>Recent requests</h3>
|
|
3051
3446
|
<div id="overview-requests"></div>
|
|
3052
3447
|
</section>
|
|
3053
3448
|
<section class="view" id="view-state" hidden>
|
|
3054
|
-
<h2>State</h2>
|
|
3055
|
-
<p class="lede">Every collection in this namespace. Declared fields stay visible before any row exists; stored rows add whatever else they contain.</p>
|
|
3449
|
+
<div class="view-head"><div><h2>State</h2><p class="lede">Inspect and edit every collection in this namespace.</p></div></div>
|
|
3056
3450
|
<div class="split">
|
|
3057
3451
|
<div id="collections"></div>
|
|
3058
3452
|
<div>
|
|
@@ -3065,8 +3459,7 @@ var renderAdminDocument = (service) => {
|
|
|
3065
3459
|
</div>
|
|
3066
3460
|
</section>
|
|
3067
3461
|
<section class="view" id="view-clock" hidden>
|
|
3068
|
-
<h2>Clock</h2>
|
|
3069
|
-
<p class="lede">The clock every timestamp in this mock reads.</p>
|
|
3462
|
+
<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>
|
|
3070
3463
|
<div class="card" id="clock-readout"></div>
|
|
3071
3464
|
<div class="row-actions">
|
|
3072
3465
|
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
@@ -3085,21 +3478,22 @@ var renderAdminDocument = (service) => {
|
|
|
3085
3478
|
</form>
|
|
3086
3479
|
</section>
|
|
3087
3480
|
<section class="view" id="view-faults" hidden>
|
|
3088
|
-
<h2>Faults</h2>
|
|
3089
|
-
<p class="lede">Inject a response, a delay, or a named preset. Rules apply to this namespace.</p>
|
|
3481
|
+
<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>
|
|
3090
3482
|
<form id="fault-form" class="card stack">
|
|
3091
|
-
<
|
|
3092
|
-
<
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
<
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
<
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
<
|
|
3102
|
-
|
|
3483
|
+
<div class="form-grid">
|
|
3484
|
+
<label class="field wide">Preset
|
|
3485
|
+
<select id="fault-preset"><option value="">Custom rule</option></select>
|
|
3486
|
+
</label>
|
|
3487
|
+
<label class="field">Status
|
|
3488
|
+
<input id="fault-status" inputmode="numeric" placeholder="503">
|
|
3489
|
+
</label>
|
|
3490
|
+
<label class="field wide">Path prefix
|
|
3491
|
+
<input id="fault-path" placeholder="/v1">
|
|
3492
|
+
</label>
|
|
3493
|
+
<label class="field">Method
|
|
3494
|
+
<input id="fault-method" placeholder="GET">
|
|
3495
|
+
</label>
|
|
3496
|
+
</div>
|
|
3103
3497
|
<div class="dialog-actions">
|
|
3104
3498
|
<button class="btn-primary" type="submit">Add fault</button>
|
|
3105
3499
|
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
@@ -3108,26 +3502,31 @@ var renderAdminDocument = (service) => {
|
|
|
3108
3502
|
<div id="fault-list"></div>
|
|
3109
3503
|
</section>
|
|
3110
3504
|
<section class="view" id="view-journal" hidden>
|
|
3111
|
-
<h2>Journal</h2>
|
|
3112
|
-
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
3113
|
-
<div class="row-actions">
|
|
3114
|
-
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
3115
|
-
</div>
|
|
3505
|
+
<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>
|
|
3116
3506
|
<div id="journal"></div>
|
|
3117
3507
|
</section>
|
|
3118
3508
|
<section class="view" id="view-routes" hidden>
|
|
3119
|
-
<h2>Routes</h2>
|
|
3120
|
-
<
|
|
3509
|
+
<div class="view-head"><div><h2>Routes</h2><p class="lede">Explore shared controls and service-specific admin endpoints.</p></div></div>
|
|
3510
|
+
<div class="route-tools">
|
|
3511
|
+
<label class="field">Filter routes
|
|
3512
|
+
<input id="route-search" type="search" placeholder="Method, path, or extension" autocomplete="off">
|
|
3513
|
+
</label>
|
|
3514
|
+
<span class="route-count" id="route-count"></span>
|
|
3515
|
+
</div>
|
|
3121
3516
|
<div id="route-list"></div>
|
|
3122
|
-
<form id="route-form" class="card stack">
|
|
3517
|
+
<form id="route-form" class="card stack" style="margin-top: 18px">
|
|
3123
3518
|
<strong id="route-title">Select a route</strong>
|
|
3124
3519
|
<label class="field">JSON body
|
|
3125
|
-
<
|
|
3520
|
+
<div class="json-editor" id="route-editor" hidden>
|
|
3521
|
+
<pre aria-hidden="true"><code id="route-highlight"></code></pre>
|
|
3522
|
+
<textarea id="route-body" placeholder="{ }" spellcheck="false" aria-describedby="route-json-status" disabled></textarea>
|
|
3523
|
+
</div>
|
|
3524
|
+
<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>
|
|
3126
3525
|
</label>
|
|
3127
3526
|
<div class="dialog-actions">
|
|
3128
|
-
<button class="btn-primary" type="submit">Send</button>
|
|
3527
|
+
<button class="btn-primary" id="route-send" type="submit" disabled>Send request</button>
|
|
3129
3528
|
</div>
|
|
3130
|
-
<pre id="route-result">Response will show here.</pre>
|
|
3529
|
+
<pre class="json-view" id="route-result"><code>Response will show here.</code></pre>
|
|
3131
3530
|
</form>
|
|
3132
3531
|
</section>
|
|
3133
3532
|
</main>
|
|
@@ -3135,12 +3534,16 @@ var renderAdminDocument = (service) => {
|
|
|
3135
3534
|
</div>
|
|
3136
3535
|
<dialog id="editor">
|
|
3137
3536
|
<form method="dialog" id="editor-form">
|
|
3138
|
-
<strong id="editor-title">Record</strong>
|
|
3537
|
+
<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>
|
|
3139
3538
|
<label class="field">Id
|
|
3140
3539
|
<input id="editor-id" required>
|
|
3141
3540
|
</label>
|
|
3142
3541
|
<label class="field">JSON value
|
|
3143
|
-
<
|
|
3542
|
+
<div class="json-editor" id="record-editor">
|
|
3543
|
+
<pre aria-hidden="true"><code id="editor-highlight"></code></pre>
|
|
3544
|
+
<textarea id="editor-value" spellcheck="false" aria-describedby="editor-json-status"></textarea>
|
|
3545
|
+
</div>
|
|
3546
|
+
<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>
|
|
3144
3547
|
</label>
|
|
3145
3548
|
<div class="dialog-actions">
|
|
3146
3549
|
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
@@ -3182,8 +3585,8 @@ var describeExtension = (extension) => {
|
|
|
3182
3585
|
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
3183
3586
|
};
|
|
3184
3587
|
};
|
|
3185
|
-
var adminUiRoutes = (service, ui) => {
|
|
3186
|
-
const shell = () => renderAdminDocument(service);
|
|
3588
|
+
var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
3589
|
+
const shell = () => renderAdminDocument(service, adminPrefix);
|
|
3187
3590
|
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
3188
3591
|
return {
|
|
3189
3592
|
"GET /ui": () => htmlResponse(document3()),
|
|
@@ -3800,7 +4203,7 @@ var stateAdminRoutes = (routes) => {
|
|
|
3800
4203
|
};
|
|
3801
4204
|
|
|
3802
4205
|
// ../core/dist/version.js
|
|
3803
|
-
var PACKAGE_VERSION = true ? "
|
|
4206
|
+
var PACKAGE_VERSION = true ? "2.0.0" : UNRELEASED_VERSION;
|
|
3804
4207
|
|
|
3805
4208
|
// ../core/dist/signing.js
|
|
3806
4209
|
var encoder = new TextEncoder();
|
|
@@ -3893,7 +4296,9 @@ var createWebhookHub = (options) => {
|
|
|
3893
4296
|
const faults = /* @__PURE__ */ new Map();
|
|
3894
4297
|
const held = /* @__PURE__ */ new Map();
|
|
3895
4298
|
const inFlight = /* @__PURE__ */ new Set();
|
|
4299
|
+
const controllers = /* @__PURE__ */ new Map();
|
|
3896
4300
|
let immediate = Promise.resolve();
|
|
4301
|
+
const snapshots = /* @__PURE__ */ new WeakMap();
|
|
3897
4302
|
const track = (work) => {
|
|
3898
4303
|
inFlight.add(work);
|
|
3899
4304
|
void work.finally(() => inFlight.delete(work));
|
|
@@ -3914,6 +4319,7 @@ var createWebhookHub = (options) => {
|
|
|
3914
4319
|
responseBody: null
|
|
3915
4320
|
};
|
|
3916
4321
|
const controller = new AbortController();
|
|
4322
|
+
controllers.set(delivery.id, controller);
|
|
3917
4323
|
const timer = scheduleTimer(() => controller.abort(), timeoutMs);
|
|
3918
4324
|
try {
|
|
3919
4325
|
const signed = await options.signer({
|
|
@@ -3943,6 +4349,7 @@ var createWebhookHub = (options) => {
|
|
|
3943
4349
|
} catch (error2) {
|
|
3944
4350
|
record.error = controller.signal.aborted ? `timed out after ${timeoutMs}ms` : error2 instanceof Error ? error2.message : String(error2);
|
|
3945
4351
|
} finally {
|
|
4352
|
+
controllers.delete(delivery.id);
|
|
3946
4353
|
cancel(timer);
|
|
3947
4354
|
record.durationMs = now() - started;
|
|
3948
4355
|
delivery.attempts.push(record);
|
|
@@ -3960,6 +4367,8 @@ var createWebhookHub = (options) => {
|
|
|
3960
4367
|
if (!payloads.has(delivery.id))
|
|
3961
4368
|
return;
|
|
3962
4369
|
await attempt(delivery).then((ok) => {
|
|
4370
|
+
if (!payloads.has(delivery.id))
|
|
4371
|
+
return;
|
|
3963
4372
|
if (ok)
|
|
3964
4373
|
delivery.state = "delivered";
|
|
3965
4374
|
else
|
|
@@ -4021,6 +4430,37 @@ var createWebhookHub = (options) => {
|
|
|
4021
4430
|
fanOut(message);
|
|
4022
4431
|
};
|
|
4023
4432
|
const hub = {
|
|
4433
|
+
pending(namespace) {
|
|
4434
|
+
return inFlight.size + (held.get(namespace)?.length ?? 0) + [...deliveries.values()].filter((d) => d.namespace === namespace && d.state === "pending").length;
|
|
4435
|
+
},
|
|
4436
|
+
snapshot(namespace) {
|
|
4437
|
+
if (hub.pending(namespace) > 0)
|
|
4438
|
+
throw new Error("webhooks are not quiescent");
|
|
4439
|
+
const handle = {};
|
|
4440
|
+
snapshots.set(handle, structuredClone({
|
|
4441
|
+
namespace,
|
|
4442
|
+
endpoints: own.get(namespace) ?? [],
|
|
4443
|
+
messages: messages.filter((m) => m.namespace === namespace),
|
|
4444
|
+
deliveries: [...deliveries].filter(([, d]) => d.namespace === namespace),
|
|
4445
|
+
payloads: [...payloads].filter(([, p]) => p.message.namespace === namespace),
|
|
4446
|
+
faults: faults.get(namespace) ?? []
|
|
4447
|
+
}));
|
|
4448
|
+
return handle;
|
|
4449
|
+
},
|
|
4450
|
+
restore(namespace, handle) {
|
|
4451
|
+
const point = typeof handle === "object" && handle !== null ? snapshots.get(handle) : void 0;
|
|
4452
|
+
if (!point || point.namespace !== namespace || hub.pending(namespace) > 0)
|
|
4453
|
+
throw new Error("invalid webhook restore");
|
|
4454
|
+
hub.clear(namespace);
|
|
4455
|
+
const copied = structuredClone(point);
|
|
4456
|
+
own.set(namespace, copied.endpoints);
|
|
4457
|
+
messages.push(...copied.messages);
|
|
4458
|
+
for (const [id2, delivery] of copied.deliveries)
|
|
4459
|
+
deliveries.set(id2, delivery);
|
|
4460
|
+
for (const [id2, payload] of copied.payloads)
|
|
4461
|
+
payloads.set(id2, payload);
|
|
4462
|
+
faults.set(namespace, copied.faults);
|
|
4463
|
+
},
|
|
4024
4464
|
publish(input) {
|
|
4025
4465
|
const contentType = input.contentType ?? (input.form ? "application/x-www-form-urlencoded" : "application/json");
|
|
4026
4466
|
const body = typeof input.body === "string" ? input.body : input.form ? new URLSearchParams(input.form).toString() : JSON.stringify(input.body);
|
|
@@ -4111,6 +4551,7 @@ var createWebhookHub = (options) => {
|
|
|
4111
4551
|
for (const [id2, delivery] of deliveries) {
|
|
4112
4552
|
if (namespace !== void 0 && delivery.namespace !== namespace)
|
|
4113
4553
|
continue;
|
|
4554
|
+
controllers.get(id2)?.abort();
|
|
4114
4555
|
const timer = pending.get(id2);
|
|
4115
4556
|
if (timer !== void 0)
|
|
4116
4557
|
cancel(timer);
|
|
@@ -4135,7 +4576,10 @@ var createWebhookHub = (options) => {
|
|
|
4135
4576
|
};
|
|
4136
4577
|
return hub;
|
|
4137
4578
|
};
|
|
4138
|
-
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4579
|
+
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4580
|
+
status,
|
|
4581
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4582
|
+
});
|
|
4139
4583
|
var adminError3 = (status, message) => json4(status, { error: { type: "mockingbird_admin", message } });
|
|
4140
4584
|
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4141
4585
|
var parseEndpoint = (value) => {
|
|
@@ -4243,7 +4687,6 @@ var AT_HEADER = "x-mockingbird-at";
|
|
|
4243
4687
|
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
4244
4688
|
var DEFAULT_NAMESPACE = "default";
|
|
4245
4689
|
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4246
|
-
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
4247
4690
|
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4248
4691
|
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
4249
4692
|
var effects = /* @__PURE__ */ new WeakMap();
|
|
@@ -4284,6 +4727,22 @@ var operationMatcher = (document3) => {
|
|
|
4284
4727
|
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
4285
4728
|
};
|
|
4286
4729
|
var createRuntime = (options) => {
|
|
4730
|
+
const adminPrefix = resolveAdminPrefix(options.adminPrefix);
|
|
4731
|
+
const internalPaths = Object.entries(options.document?.paths ?? {}).filter(([, item]) => item["x-mockingbird-internal"] === true).map(([path]) => path);
|
|
4732
|
+
for (const path of internalPaths) {
|
|
4733
|
+
if (!path.startsWith(`${ADMIN_PREFIX}/blobs/`))
|
|
4734
|
+
throw new Error(`internal transport ${path} must live under ${ADMIN_PREFIX}/blobs/`);
|
|
4735
|
+
}
|
|
4736
|
+
assertAdminPrefixAvailable(adminPrefix, Object.keys(options.document?.paths ?? {}).filter((path) => !internalPaths.includes(path)));
|
|
4737
|
+
const internalMatch = (path) => {
|
|
4738
|
+
for (const pattern of internalPaths) {
|
|
4739
|
+
const actual = path.split("/");
|
|
4740
|
+
const expected = pattern.split("/");
|
|
4741
|
+
if (actual.length === expected.length && expected.every((part, i) => part.startsWith("{") || part === actual[i]))
|
|
4742
|
+
return pattern;
|
|
4743
|
+
}
|
|
4744
|
+
return void 0;
|
|
4745
|
+
};
|
|
4287
4746
|
assertAdminUi(options.adminUi);
|
|
4288
4747
|
const sqlite = bootSqlite(options.sqlite);
|
|
4289
4748
|
const clock = options.clock ?? createClock();
|
|
@@ -4298,6 +4757,20 @@ var createRuntime = (options) => {
|
|
|
4298
4757
|
const instances = /* @__PURE__ */ new Map();
|
|
4299
4758
|
const publicNamespaces = /* @__PURE__ */ new Set();
|
|
4300
4759
|
const branchRngs = /* @__PURE__ */ new Map();
|
|
4760
|
+
const namespaceClocks = /* @__PURE__ */ new Map();
|
|
4761
|
+
const namespaceForStorage = /* @__PURE__ */ new Map();
|
|
4762
|
+
let isolatedNamespaces = false;
|
|
4763
|
+
const namespaceClock = (name = DEFAULT_NAMESPACE) => {
|
|
4764
|
+
if (!isolatedNamespaces || name === DEFAULT_NAMESPACE)
|
|
4765
|
+
return clock;
|
|
4766
|
+
let found = namespaceClocks.get(name);
|
|
4767
|
+
if (!found) {
|
|
4768
|
+
found = createClock();
|
|
4769
|
+
namespaceClocks.set(name, found);
|
|
4770
|
+
}
|
|
4771
|
+
return found;
|
|
4772
|
+
};
|
|
4773
|
+
const fleetPoints = /* @__PURE__ */ new WeakMap();
|
|
4301
4774
|
const timelines = /* @__PURE__ */ new Map();
|
|
4302
4775
|
const branchStorage = /* @__PURE__ */ new Map();
|
|
4303
4776
|
const captured = /* @__PURE__ */ new Map();
|
|
@@ -4311,17 +4784,22 @@ var createRuntime = (options) => {
|
|
|
4311
4784
|
if (!NAMESPACE_PATTERN.test(key) || !NAMESPACE_PATTERN.test(publicNamespace)) {
|
|
4312
4785
|
throw new RangeError(`namespace must match ${NAMESPACE_PATTERN}: ${JSON.stringify(publicNamespace)}`);
|
|
4313
4786
|
}
|
|
4787
|
+
const createdContextRng = isolatedRng ?? (isolatedNamespaces && key !== DEFAULT_NAMESPACE ? createRng(options.seed ?? 0) : rng);
|
|
4314
4788
|
const created = options.create({
|
|
4315
4789
|
namespace: storageNamespace(key),
|
|
4316
4790
|
publicNamespace,
|
|
4791
|
+
adminPrefix,
|
|
4317
4792
|
sqlite,
|
|
4318
|
-
clock,
|
|
4319
|
-
rng:
|
|
4793
|
+
clock: namespaceClock(publicNamespace),
|
|
4794
|
+
rng: createdContextRng
|
|
4320
4795
|
});
|
|
4321
4796
|
instances.set(key, created);
|
|
4797
|
+
namespaceForStorage.set(key, publicNamespace);
|
|
4322
4798
|
publicNamespaces.add(publicNamespace);
|
|
4323
4799
|
if (isolatedRng)
|
|
4324
4800
|
branchRngs.set(key, isolatedRng);
|
|
4801
|
+
else if (isolatedNamespaces && key !== DEFAULT_NAMESPACE)
|
|
4802
|
+
branchRngs.set(key, createdContextRng);
|
|
4325
4803
|
return created;
|
|
4326
4804
|
};
|
|
4327
4805
|
const instance = (name = DEFAULT_NAMESPACE) => instanceFor(name);
|
|
@@ -4339,7 +4817,7 @@ var createRuntime = (options) => {
|
|
|
4339
4817
|
captured.set(storage, snapshot2);
|
|
4340
4818
|
return Object.freeze({
|
|
4341
4819
|
snapshot: snapshot2,
|
|
4342
|
-
clock: Object.freeze(
|
|
4820
|
+
clock: Object.freeze(namespaceClock(namespaceForStorage.get(storage) ?? storage).state()),
|
|
4343
4821
|
rngState: (branchRngs.get(storage) ?? rng).state()
|
|
4344
4822
|
});
|
|
4345
4823
|
};
|
|
@@ -4349,7 +4827,7 @@ var createRuntime = (options) => {
|
|
|
4349
4827
|
return found;
|
|
4350
4828
|
instance(name);
|
|
4351
4829
|
found = new Timeline({
|
|
4352
|
-
now:
|
|
4830
|
+
now: namespaceClock(name).now,
|
|
4353
4831
|
...options.maxCheckpoints !== void 0 ? { maxCheckpoints: options.maxCheckpoints } : {}
|
|
4354
4832
|
});
|
|
4355
4833
|
found.commit(capture(name));
|
|
@@ -4376,12 +4854,13 @@ var createRuntime = (options) => {
|
|
|
4376
4854
|
const point = history2.checkout("main", at);
|
|
4377
4855
|
restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
|
|
4378
4856
|
captured.set(namespace, point.value.snapshot);
|
|
4379
|
-
rng.setState(point.value.rngState);
|
|
4380
|
-
|
|
4857
|
+
(branchRngs.get(namespace) ?? rng).setState(point.value.rngState);
|
|
4858
|
+
const selectedClock = namespaceClock(namespace);
|
|
4859
|
+
selectedClock.set(point.value.clock.now);
|
|
4381
4860
|
if (point.value.clock.frozen)
|
|
4382
|
-
|
|
4861
|
+
selectedClock.freeze();
|
|
4383
4862
|
else
|
|
4384
|
-
|
|
4863
|
+
selectedClock.unfreeze();
|
|
4385
4864
|
}
|
|
4386
4865
|
return namespace;
|
|
4387
4866
|
}
|
|
@@ -4439,11 +4918,12 @@ var createRuntime = (options) => {
|
|
|
4439
4918
|
const storage = ensureBranch(namespace, branchName);
|
|
4440
4919
|
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
4441
4920
|
captured.set(storage, point.value.snapshot);
|
|
4442
|
-
|
|
4921
|
+
const selectedClock = namespaceClock(namespace);
|
|
4922
|
+
selectedClock.set(point.value.clock.now);
|
|
4443
4923
|
if (point.value.clock.frozen)
|
|
4444
|
-
|
|
4924
|
+
selectedClock.freeze();
|
|
4445
4925
|
else
|
|
4446
|
-
|
|
4926
|
+
selectedClock.unfreeze();
|
|
4447
4927
|
(branchRngs.get(storage) ?? rng).setState(point.value.rngState);
|
|
4448
4928
|
};
|
|
4449
4929
|
const reset = async (name = DEFAULT_NAMESPACE) => {
|
|
@@ -4451,10 +4931,21 @@ var createRuntime = (options) => {
|
|
|
4451
4931
|
options.webhooks?.clear();
|
|
4452
4932
|
for (const each of instances.values())
|
|
4453
4933
|
await each.reset();
|
|
4934
|
+
for (const each of branchRngs.values())
|
|
4935
|
+
each.reset();
|
|
4454
4936
|
timelines.clear();
|
|
4937
|
+
for (const storage of branchStorage.values()) {
|
|
4938
|
+
instances.delete(storage);
|
|
4939
|
+
branchRngs.delete(storage);
|
|
4940
|
+
namespaceForStorage.delete(storage);
|
|
4941
|
+
}
|
|
4455
4942
|
branchStorage.clear();
|
|
4456
|
-
branchRngs.clear();
|
|
4457
4943
|
captured.clear();
|
|
4944
|
+
faults.clear();
|
|
4945
|
+
clock.reset();
|
|
4946
|
+
rng.reset();
|
|
4947
|
+
for (const each of namespaceClocks.values())
|
|
4948
|
+
each.reset();
|
|
4458
4949
|
return;
|
|
4459
4950
|
}
|
|
4460
4951
|
options.webhooks?.clear(name);
|
|
@@ -4472,11 +4963,18 @@ var createRuntime = (options) => {
|
|
|
4472
4963
|
else
|
|
4473
4964
|
clearNamespace(sqlite, storageNamespace(storage));
|
|
4474
4965
|
branchStorage.delete(mapping);
|
|
4966
|
+
instances.delete(storage);
|
|
4967
|
+
namespaceForStorage.delete(storage);
|
|
4475
4968
|
branchRngs.delete(storage);
|
|
4476
4969
|
captured.delete(storage);
|
|
4477
4970
|
}
|
|
4478
4971
|
timelines.delete(name);
|
|
4479
4972
|
captured.delete(name);
|
|
4973
|
+
faults.restore(name, { rules: [], rngState: createRng(options.seed ?? 0).state() });
|
|
4974
|
+
namespaceClock(name).reset();
|
|
4975
|
+
(branchRngs.get(name) ?? rng).reset();
|
|
4976
|
+
journal.clear(name);
|
|
4977
|
+
metrics.reset(name);
|
|
4480
4978
|
};
|
|
4481
4979
|
const snapshot = (name = DEFAULT_NAMESPACE) => {
|
|
4482
4980
|
return checkpoint(name, "main").value.snapshot;
|
|
@@ -4514,6 +5012,53 @@ var createRuntime = (options) => {
|
|
|
4514
5012
|
rng,
|
|
4515
5013
|
credentials,
|
|
4516
5014
|
webhooks: options.webhooks,
|
|
5015
|
+
isolateNamespaces() {
|
|
5016
|
+
if (publicNamespaces.size > 1)
|
|
5017
|
+
throw new Error("enable namespace isolation before creating named namespaces");
|
|
5018
|
+
isolatedNamespaces = true;
|
|
5019
|
+
faults.isolateNamespaces();
|
|
5020
|
+
},
|
|
5021
|
+
namespaceClock,
|
|
5022
|
+
namespaceOf(request) {
|
|
5023
|
+
const url = new URL(request.url);
|
|
5024
|
+
const prefixed = matchNamespacePath(url.pathname, adminPrefix);
|
|
5025
|
+
const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
|
|
5026
|
+
const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
|
|
5027
|
+
if (isAdminPath(path, adminPrefix))
|
|
5028
|
+
return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
|
|
5029
|
+
if (selected !== void 0)
|
|
5030
|
+
return selected;
|
|
5031
|
+
const credential = options.credential?.(request);
|
|
5032
|
+
return (credential !== void 0 ? credentials.get(credential) : void 0) ?? DEFAULT_NAMESPACE;
|
|
5033
|
+
},
|
|
5034
|
+
fleetSnapshot(namespace) {
|
|
5035
|
+
instance(namespace);
|
|
5036
|
+
const handle = {};
|
|
5037
|
+
fleetPoints.set(handle, {
|
|
5038
|
+
namespace,
|
|
5039
|
+
state: capture(namespace),
|
|
5040
|
+
faults: faults.snapshot(namespace),
|
|
5041
|
+
webhooks: options.webhooks?.snapshot(namespace)
|
|
5042
|
+
});
|
|
5043
|
+
return handle;
|
|
5044
|
+
},
|
|
5045
|
+
fleetRestore(handle, namespace) {
|
|
5046
|
+
const point = typeof handle === "object" && handle !== null ? fleetPoints.get(handle) : void 0;
|
|
5047
|
+
if (!point || point.namespace !== namespace)
|
|
5048
|
+
throw new Error("invalid fleet checkpoint");
|
|
5049
|
+
restoreNamespace(sqlite, storageNamespace(namespace), point.state.snapshot);
|
|
5050
|
+
captured.set(namespace, point.state.snapshot);
|
|
5051
|
+
const selectedClock = namespaceClock(namespace);
|
|
5052
|
+
selectedClock.freeze();
|
|
5053
|
+
selectedClock.set(point.state.clock.now);
|
|
5054
|
+
if (!point.state.clock.frozen)
|
|
5055
|
+
selectedClock.unfreeze();
|
|
5056
|
+
(branchRngs.get(namespace) ?? rng).setState(point.state.rngState);
|
|
5057
|
+
faults.restore(namespace, point.faults);
|
|
5058
|
+
if (point.webhooks !== void 0)
|
|
5059
|
+
options.webhooks?.restore(namespace, point.webhooks);
|
|
5060
|
+
timelines.delete(namespace);
|
|
5061
|
+
},
|
|
4517
5062
|
applyPreset: (name, namespace = DEFAULT_NAMESPACE, overrides = {}) => {
|
|
4518
5063
|
const preset = options.presets?.[name];
|
|
4519
5064
|
if (!preset)
|
|
@@ -4545,7 +5090,7 @@ var createRuntime = (options) => {
|
|
|
4545
5090
|
state: (namespace = DEFAULT_NAMESPACE) => inspectState(stateScope(namespace)),
|
|
4546
5091
|
fetch: async (incoming) => {
|
|
4547
5092
|
let request = incoming;
|
|
4548
|
-
const prefixed =
|
|
5093
|
+
const prefixed = matchNamespacePath(new URL(request.url).pathname, adminPrefix);
|
|
4549
5094
|
if (prefixed) {
|
|
4550
5095
|
const url2 = new URL(request.url);
|
|
4551
5096
|
url2.pathname = prefixed[2] ?? "/";
|
|
@@ -4561,7 +5106,15 @@ var createRuntime = (options) => {
|
|
|
4561
5106
|
signal: request.signal
|
|
4562
5107
|
});
|
|
4563
5108
|
}
|
|
4564
|
-
|
|
5109
|
+
const internalUrl = new URL(request.url);
|
|
5110
|
+
const internalPath = isAdminPath(internalUrl.pathname, adminPrefix) ? `${ADMIN_PREFIX}${internalUrl.pathname.slice(adminPrefix.length)}` : "";
|
|
5111
|
+
const internal = internalMatch(internalPath);
|
|
5112
|
+
const obsoleteInternalPath = adminPrefix !== ADMIN_PREFIX && internalMatch(internalUrl.pathname) && !internal;
|
|
5113
|
+
if (internal) {
|
|
5114
|
+
internalUrl.pathname = internalPath;
|
|
5115
|
+
request = new Request(internalUrl, request);
|
|
5116
|
+
}
|
|
5117
|
+
let namespace = internal ? internalUrl.searchParams.get("namespace") ?? control.namespaceOf(request) : control.namespaceOf(request);
|
|
4565
5118
|
if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
4566
5119
|
const credential = options.credential(request);
|
|
4567
5120
|
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
@@ -4581,7 +5134,9 @@ var createRuntime = (options) => {
|
|
|
4581
5134
|
return copy;
|
|
4582
5135
|
}
|
|
4583
5136
|
};
|
|
4584
|
-
|
|
5137
|
+
if (obsoleteInternalPath)
|
|
5138
|
+
return stamp(adminFail(404, "internal transport is only available under adminPrefix"));
|
|
5139
|
+
const handled = internal ? void 0 : await control.handle(request);
|
|
4585
5140
|
if (handled)
|
|
4586
5141
|
return stamp(handled);
|
|
4587
5142
|
const started = monotonicNow();
|
|
@@ -4622,7 +5177,7 @@ var createRuntime = (options) => {
|
|
|
4622
5177
|
type: "mockingbird_admin",
|
|
4623
5178
|
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
4624
5179
|
}
|
|
4625
|
-
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
5180
|
+
}), { status: 400, headers: { "content-type": "application/json; charset=utf-8" } }));
|
|
4626
5181
|
}
|
|
4627
5182
|
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
4628
5183
|
log(400);
|
|
@@ -4689,6 +5244,7 @@ var createRuntime = (options) => {
|
|
|
4689
5244
|
startedAt: wallNow(),
|
|
4690
5245
|
wallNow,
|
|
4691
5246
|
clock,
|
|
5247
|
+
namespaceClock,
|
|
4692
5248
|
faults,
|
|
4693
5249
|
metrics,
|
|
4694
5250
|
journal,
|
|
@@ -4738,15 +5294,16 @@ var createRuntime = (options) => {
|
|
|
4738
5294
|
...options.admin?.(runtime) ?? {},
|
|
4739
5295
|
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
4740
5296
|
// maps `presets`, and a service route must not replace that list with a record.
|
|
4741
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
5297
|
+
...options.presets ? presetRoutes(options.presets, runtime, adminPrefix) : {},
|
|
4742
5298
|
...stateAdminRoutes({
|
|
4743
5299
|
open: stateScope,
|
|
4744
5300
|
afterWrite: (namespace) => {
|
|
4745
5301
|
checkpoint(namespace, "main");
|
|
4746
5302
|
}
|
|
4747
5303
|
}),
|
|
4748
|
-
...adminUiRoutes(options.name, options.adminUi)
|
|
5304
|
+
...adminUiRoutes(options.name, options.adminUi, adminPrefix)
|
|
4749
5305
|
},
|
|
5306
|
+
adminPrefix,
|
|
4750
5307
|
adminKey: options.adminKey
|
|
4751
5308
|
});
|
|
4752
5309
|
return defineMock(runtime);
|
|
@@ -4760,7 +5317,10 @@ var mutableResponse = (response) => {
|
|
|
4760
5317
|
return new Response(response.body, response);
|
|
4761
5318
|
}
|
|
4762
5319
|
};
|
|
4763
|
-
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5320
|
+
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5321
|
+
status,
|
|
5322
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
5323
|
+
});
|
|
4764
5324
|
var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
|
|
4765
5325
|
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4766
5326
|
var credentialRoutes = (registry) => ({
|
|
@@ -4812,7 +5372,7 @@ var credentialRoutes = (registry) => ({
|
|
|
4812
5372
|
return adminJson(200, { status: "ok" });
|
|
4813
5373
|
}
|
|
4814
5374
|
});
|
|
4815
|
-
var presetRoutes = (presets, runtime) => ({
|
|
5375
|
+
var presetRoutes = (presets, runtime, adminPrefix) => ({
|
|
4816
5376
|
"GET /faults/presets": () => {
|
|
4817
5377
|
const body = {
|
|
4818
5378
|
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
@@ -4822,7 +5382,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
4822
5382
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
4823
5383
|
const name = params.name;
|
|
4824
5384
|
if (!presets[name])
|
|
4825
|
-
return adminFail(404, `no fault preset ${name}; GET /
|
|
5385
|
+
return adminFail(404, `no fault preset ${name}; GET ${adminPrefix}/faults/presets`);
|
|
4826
5386
|
const overrides = isObject(body) ? body : {};
|
|
4827
5387
|
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
4828
5388
|
}
|
|
@@ -5931,6 +6491,7 @@ var createRuntime2 = (options = {}) => {
|
|
|
5931
6491
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
5932
6492
|
...options.clock ? { clock: options.clock } : {},
|
|
5933
6493
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
6494
|
+
...options.adminPrefix !== void 0 ? { adminPrefix: options.adminPrefix } : {},
|
|
5934
6495
|
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
5935
6496
|
...options.onLog ? { onLog: options.onLog } : {},
|
|
5936
6497
|
credential: formbricksCredential,
|
|
@@ -6560,6 +7121,9 @@ var FormbricksAPI = class {
|
|
|
6560
7121
|
};
|
|
6561
7122
|
|
|
6562
7123
|
export {
|
|
7124
|
+
createClock,
|
|
7125
|
+
resolveAdminPrefix,
|
|
7126
|
+
parseDuration,
|
|
6563
7127
|
document2 as document,
|
|
6564
7128
|
operationIds,
|
|
6565
7129
|
supportedOperationIds,
|
|
@@ -6576,4 +7140,4 @@ export {
|
|
|
6576
7140
|
formbricksError,
|
|
6577
7141
|
FormbricksAPI
|
|
6578
7142
|
};
|
|
6579
|
-
//# sourceMappingURL=chunk-
|
|
7143
|
+
//# sourceMappingURL=chunk-2YLOIK2J.js.map
|