@crvouga/mockingbird-service-posthog 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 +3 -3
- package/SUPPORT.md +26 -0
- package/dist/chunk-FM7IAA7L.js +847 -0
- package/dist/chunk-FM7IAA7L.js.map +7 -0
- package/dist/{chunk-ZRB53DZ6.js → chunk-NRFKITRM.js} +785 -259
- package/dist/chunk-NRFKITRM.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 +1017 -0
- package/package.json +29 -4
- package/dist/chunk-FWRNARH5.js +0 -360
- package/dist/chunk-FWRNARH5.js.map +0 -7
- package/dist/chunk-ZRB53DZ6.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
|
};
|
|
@@ -474,7 +514,33 @@ var faultResponse = (rule) => {
|
|
|
474
514
|
};
|
|
475
515
|
var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) => {
|
|
476
516
|
const entries = [];
|
|
517
|
+
let isolated = false;
|
|
518
|
+
const scopedRngs = /* @__PURE__ */ new Map();
|
|
519
|
+
const random = (namespace) => {
|
|
520
|
+
if (!isolated || namespace === "default")
|
|
521
|
+
return rng;
|
|
522
|
+
let scoped = scopedRngs.get(namespace);
|
|
523
|
+
if (!scoped) {
|
|
524
|
+
scoped = createRng(rng.seed);
|
|
525
|
+
scopedRngs.set(namespace, scoped);
|
|
526
|
+
}
|
|
527
|
+
return scoped;
|
|
528
|
+
};
|
|
477
529
|
return {
|
|
530
|
+
isolateNamespaces() {
|
|
531
|
+
isolated = true;
|
|
532
|
+
},
|
|
533
|
+
snapshot(namespace) {
|
|
534
|
+
return {
|
|
535
|
+
rules: structuredClone(entries.map((entry, position) => ({
|
|
536
|
+
...entry.rule,
|
|
537
|
+
remaining: entry.remaining,
|
|
538
|
+
hits: entry.hits,
|
|
539
|
+
position
|
|
540
|
+
})).filter((entry) => entry.namespace === namespace)),
|
|
541
|
+
rngState: random(namespace).state()
|
|
542
|
+
};
|
|
543
|
+
},
|
|
478
544
|
add(rule) {
|
|
479
545
|
const existing = entries.findIndex((e) => e.rule.id === rule.id);
|
|
480
546
|
const entry = { rule, remaining: rule.count ?? null, hits: 0 };
|
|
@@ -494,6 +560,16 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
|
|
|
494
560
|
},
|
|
495
561
|
clear() {
|
|
496
562
|
entries.length = 0;
|
|
563
|
+
rng.reset();
|
|
564
|
+
scopedRngs.clear();
|
|
565
|
+
},
|
|
566
|
+
restore(namespace, state) {
|
|
567
|
+
for (let i = entries.length - 1; i >= 0; i--)
|
|
568
|
+
if (entries[i]?.rule.namespace === namespace)
|
|
569
|
+
entries.splice(i, 1);
|
|
570
|
+
for (const { remaining, hits, position, ...rule } of state.rules)
|
|
571
|
+
entries.splice(Math.min(position, entries.length), 0, { rule, remaining, hits });
|
|
572
|
+
random(namespace).setState(state.rngState);
|
|
497
573
|
},
|
|
498
574
|
async take(candidate) {
|
|
499
575
|
const hits = [];
|
|
@@ -503,7 +579,7 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
|
|
|
503
579
|
if (!matches(entry.rule, candidate))
|
|
504
580
|
continue;
|
|
505
581
|
const rate = entry.rule.rate ?? 1;
|
|
506
|
-
if (
|
|
582
|
+
if (random(candidate.namespace).next() >= rate)
|
|
507
583
|
continue;
|
|
508
584
|
entry.hits++;
|
|
509
585
|
if (entry.remaining !== null)
|
|
@@ -880,7 +956,8 @@ var annotateResponse = (response, extra) => {
|
|
|
880
956
|
var responseNotes = (response) => notes.get(response);
|
|
881
957
|
|
|
882
958
|
// ../core/dist/metrics.js
|
|
883
|
-
var createMetrics = () => {
|
|
959
|
+
var createMetrics = (partition = true) => {
|
|
960
|
+
const namespaces = /* @__PURE__ */ new Map();
|
|
884
961
|
let requests = 0;
|
|
885
962
|
let faults = 0;
|
|
886
963
|
let totalDurationMs = 0;
|
|
@@ -888,6 +965,14 @@ var createMetrics = () => {
|
|
|
888
965
|
const unmatched = /* @__PURE__ */ new Map();
|
|
889
966
|
return {
|
|
890
967
|
record(entry) {
|
|
968
|
+
if (partition) {
|
|
969
|
+
let scoped = namespaces.get(entry.namespace);
|
|
970
|
+
if (!scoped) {
|
|
971
|
+
scoped = createMetrics(false);
|
|
972
|
+
namespaces.set(entry.namespace, scoped);
|
|
973
|
+
}
|
|
974
|
+
scoped.record(entry);
|
|
975
|
+
}
|
|
891
976
|
requests++;
|
|
892
977
|
totalDurationMs += entry.durationMs;
|
|
893
978
|
if (entry.faultId !== void 0)
|
|
@@ -899,7 +984,7 @@ var createMetrics = () => {
|
|
|
899
984
|
unmatched.set(route, (unmatched.get(route) ?? 0) + 1);
|
|
900
985
|
}
|
|
901
986
|
},
|
|
902
|
-
report: () => ({
|
|
987
|
+
report: (namespace) => namespace !== void 0 ? (namespaces.get(namespace) ?? createMetrics(false)).report() : {
|
|
903
988
|
requests,
|
|
904
989
|
byOperation: Object.fromEntries([...byOperation].sort(([a], [b]) => a.localeCompare(b))),
|
|
905
990
|
unmatched: [...unmatched].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([route, count]) => {
|
|
@@ -908,8 +993,13 @@ var createMetrics = () => {
|
|
|
908
993
|
}),
|
|
909
994
|
faults,
|
|
910
995
|
totalDurationMs
|
|
911
|
-
}
|
|
912
|
-
reset() {
|
|
996
|
+
},
|
|
997
|
+
reset(namespace) {
|
|
998
|
+
if (namespace !== void 0) {
|
|
999
|
+
namespaces.delete(namespace);
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
namespaces.clear();
|
|
913
1003
|
requests = 0;
|
|
914
1004
|
faults = 0;
|
|
915
1005
|
totalDurationMs = 0;
|
|
@@ -1530,6 +1620,34 @@ dialog:not([open]) {
|
|
|
1530
1620
|
function isRecord2(value) {
|
|
1531
1621
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1532
1622
|
}
|
|
1623
|
+
function highlightJsonText(text) {
|
|
1624
|
+
const escapeToken = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
1625
|
+
switch (char) {
|
|
1626
|
+
case "&":
|
|
1627
|
+
return "&";
|
|
1628
|
+
case "<":
|
|
1629
|
+
return "<";
|
|
1630
|
+
case ">":
|
|
1631
|
+
return ">";
|
|
1632
|
+
case '"':
|
|
1633
|
+
return """;
|
|
1634
|
+
default:
|
|
1635
|
+
return "'";
|
|
1636
|
+
}
|
|
1637
|
+
});
|
|
1638
|
+
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;
|
|
1639
|
+
let cursor = 0;
|
|
1640
|
+
let html = "";
|
|
1641
|
+
for (const match of text.matchAll(token)) {
|
|
1642
|
+
const index = match.index ?? 0;
|
|
1643
|
+
const value = match[0];
|
|
1644
|
+
html += escapeToken(text.slice(cursor, index));
|
|
1645
|
+
const kind = value.startsWith('"') ? value.trimEnd().endsWith(":") ? "key" : "string" : value === "true" || value === "false" ? "boolean" : value === "null" ? "null" : "number";
|
|
1646
|
+
html += `<span class="json-${kind}">${escapeToken(value)}</span>`;
|
|
1647
|
+
cursor = index + value.length;
|
|
1648
|
+
}
|
|
1649
|
+
return html + escapeToken(text.slice(cursor));
|
|
1650
|
+
}
|
|
1533
1651
|
function mountAdminBrand(doc, host, service, catalog) {
|
|
1534
1652
|
const clip = (value, max) => {
|
|
1535
1653
|
if (typeof value !== "string")
|
|
@@ -1891,14 +2009,16 @@ function bootAdmin(config) {
|
|
|
1891
2009
|
const state = {
|
|
1892
2010
|
namespace: "default",
|
|
1893
2011
|
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
1894
|
-
view: "overview",
|
|
2012
|
+
view: ["overview", "state", "clock", "faults", "journal", "routes"].includes(location.hash.slice(1)) ? location.hash.slice(1) : "overview",
|
|
1895
2013
|
collection: "",
|
|
1896
2014
|
shape: null,
|
|
1897
2015
|
route: null,
|
|
2016
|
+
routes: [],
|
|
1898
2017
|
editing: null
|
|
1899
2018
|
};
|
|
1900
2019
|
const standard = new Set(config.standardRoutes);
|
|
1901
2020
|
const banner = el("banner", HTMLElement);
|
|
2021
|
+
const bannerMessage = el("banner-message", HTMLElement);
|
|
1902
2022
|
const keyInput = el("admin-key", HTMLInputElement);
|
|
1903
2023
|
keyInput.value = state.key;
|
|
1904
2024
|
const params = new URLSearchParams(location.search);
|
|
@@ -1909,18 +2029,21 @@ function bootAdmin(config) {
|
|
|
1909
2029
|
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
1910
2030
|
params.delete("key");
|
|
1911
2031
|
const next = params.toString();
|
|
1912
|
-
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
2032
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : "") + location.hash);
|
|
1913
2033
|
}
|
|
1914
2034
|
const queryNamespace = params.get("namespace");
|
|
1915
2035
|
if (queryNamespace)
|
|
1916
2036
|
state.namespace = queryNamespace;
|
|
1917
2037
|
const showError = (error2) => {
|
|
1918
|
-
|
|
2038
|
+
bannerMessage.textContent = error2 instanceof Error ? error2.message : String(error2);
|
|
1919
2039
|
banner.classList.add("show");
|
|
2040
|
+
banner.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
1920
2041
|
};
|
|
1921
2042
|
const clearError = () => {
|
|
1922
2043
|
banner.classList.remove("show");
|
|
2044
|
+
bannerMessage.textContent = "";
|
|
1923
2045
|
};
|
|
2046
|
+
el("banner-dismiss", HTMLButtonElement).addEventListener("click", clearError);
|
|
1924
2047
|
const api = async (method, path, body) => {
|
|
1925
2048
|
const headers = { accept: "application/json" };
|
|
1926
2049
|
if (state.key !== "")
|
|
@@ -1964,16 +2087,21 @@ function bootAdmin(config) {
|
|
|
1964
2087
|
namespace: () => state.namespace
|
|
1965
2088
|
};
|
|
1966
2089
|
const isPanelScript = (value) => typeof value === "function";
|
|
2090
|
+
const themeButton = el("theme", HTMLButtonElement);
|
|
1967
2091
|
const setTheme = (theme) => {
|
|
1968
2092
|
if (theme)
|
|
1969
2093
|
document.documentElement.dataset.theme = theme;
|
|
1970
2094
|
else
|
|
1971
2095
|
delete document.documentElement.dataset.theme;
|
|
2096
|
+
const dark = theme ? theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2097
|
+
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>';
|
|
2098
|
+
const label = dark ? "Use light theme" : "Use dark theme";
|
|
2099
|
+
themeButton.setAttribute("aria-label", label);
|
|
2100
|
+
themeButton.title = label;
|
|
1972
2101
|
};
|
|
1973
2102
|
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
2103
|
+
setTheme(storedTheme === "dark" || storedTheme === "light" ? storedTheme : null);
|
|
2104
|
+
themeButton.addEventListener("click", () => {
|
|
1977
2105
|
const current = document.documentElement.dataset.theme;
|
|
1978
2106
|
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
1979
2107
|
const next = dark ? "light" : "dark";
|
|
@@ -1992,6 +2120,7 @@ function bootAdmin(config) {
|
|
|
1992
2120
|
continue;
|
|
1993
2121
|
section.hidden = section.id !== `view-${view}`;
|
|
1994
2122
|
}
|
|
2123
|
+
history.replaceState(null, "", `${location.pathname}${location.search}#${encodeURIComponent(view)}`);
|
|
1995
2124
|
};
|
|
1996
2125
|
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
1997
2126
|
const target = event.target;
|
|
@@ -2006,7 +2135,7 @@ function bootAdmin(config) {
|
|
|
2006
2135
|
show(view);
|
|
2007
2136
|
refresh().catch(showError);
|
|
2008
2137
|
});
|
|
2009
|
-
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>`;
|
|
2138
|
+
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>`;
|
|
2010
2139
|
const table = (columns, rows) => {
|
|
2011
2140
|
if (rows.length === 0)
|
|
2012
2141
|
return '<p class="empty">Nothing here yet.</p>';
|
|
@@ -2016,16 +2145,22 @@ function bootAdmin(config) {
|
|
|
2016
2145
|
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
2017
2146
|
return `<tr${attrs}>${cells}</tr>`;
|
|
2018
2147
|
}).join("");
|
|
2019
|
-
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
2148
|
+
return `<div class="table-shell"><table><thead>${head}</thead><tbody>${body}</tbody></table></div>`;
|
|
2020
2149
|
};
|
|
2021
2150
|
const preview = (value) => {
|
|
2022
2151
|
const text = JSON.stringify(value) ?? "null";
|
|
2023
|
-
|
|
2152
|
+
const clipped = text.length > 180 ? `${text.slice(0, 180)}\u2026` : text;
|
|
2153
|
+
return `<code class="json-inline">${highlightJsonText(clipped)}</code>`;
|
|
2024
2154
|
};
|
|
2025
2155
|
const when = (epoch) => {
|
|
2026
2156
|
const date = new Date(epoch);
|
|
2027
2157
|
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
2028
2158
|
};
|
|
2159
|
+
const journalWhen = (value) => {
|
|
2160
|
+
const numeric = value.trim() === "" ? Number.NaN : Number(value);
|
|
2161
|
+
const epoch = Number.isFinite(numeric) ? numeric : Date.parse(value);
|
|
2162
|
+
return Number.isFinite(epoch) ? when(epoch) : value;
|
|
2163
|
+
};
|
|
2029
2164
|
const clockCard = (epoch) => {
|
|
2030
2165
|
const date = new Date(epoch);
|
|
2031
2166
|
if (Number.isNaN(date.getTime()))
|
|
@@ -2145,7 +2280,7 @@ function bootAdmin(config) {
|
|
|
2145
2280
|
};
|
|
2146
2281
|
const renderOverview = async () => {
|
|
2147
2282
|
const [health, shape, clock, journal] = await Promise.all([
|
|
2148
|
-
originGet(
|
|
2283
|
+
originGet(`${config.adminPrefix}/health`),
|
|
2149
2284
|
api("GET", "/state"),
|
|
2150
2285
|
api("GET", "/clock"),
|
|
2151
2286
|
api("GET", "/requests?limit=8")
|
|
@@ -2162,7 +2297,7 @@ function bootAdmin(config) {
|
|
|
2162
2297
|
},
|
|
2163
2298
|
{
|
|
2164
2299
|
k: "Status",
|
|
2165
|
-
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
2300
|
+
v: `<span class="badge ok">${esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")}</span>`
|
|
2166
2301
|
},
|
|
2167
2302
|
{ k: "Namespace", v: esc(state.namespace) },
|
|
2168
2303
|
{ k: "Collections", v: String(decoded2.collections.length) },
|
|
@@ -2249,26 +2384,46 @@ function bootAdmin(config) {
|
|
|
2249
2384
|
const renderJournal = async () => {
|
|
2250
2385
|
const journal = await api("GET", "/requests?limit=100");
|
|
2251
2386
|
el("journal", HTMLElement).innerHTML = table([
|
|
2252
|
-
{ label: "When", cell: (row) => esc(row.at) },
|
|
2253
|
-
{
|
|
2387
|
+
{ label: "When", cell: (row) => esc(journalWhen(row.at)) },
|
|
2388
|
+
{
|
|
2389
|
+
label: "Method",
|
|
2390
|
+
cell: (row) => `<span class="method">${esc(row.method)}</span>`
|
|
2391
|
+
},
|
|
2254
2392
|
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2255
|
-
{
|
|
2393
|
+
{
|
|
2394
|
+
label: "Status",
|
|
2395
|
+
cell: (row) => `<span class="badge ${row.status.startsWith("2") ? "ok" : row.status.startsWith("4") || row.status.startsWith("5") ? "warn" : ""}">${esc(row.status)}</span>`
|
|
2396
|
+
},
|
|
2256
2397
|
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
2257
2398
|
], decodeJournal(journal));
|
|
2258
2399
|
};
|
|
2259
|
-
const
|
|
2260
|
-
const
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2400
|
+
const paintRoutes = () => {
|
|
2401
|
+
const query = el("route-search", HTMLInputElement).value.trim().toLocaleLowerCase();
|
|
2402
|
+
const routes = state.routes.filter((route) => route.toLocaleLowerCase().includes(query));
|
|
2403
|
+
el("route-count", HTMLElement).textContent = `${routes.length} of ${state.routes.length}`;
|
|
2404
|
+
if (state.routes.length === 0) {
|
|
2405
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No admin routes are available.</p>';
|
|
2406
|
+
return;
|
|
2407
|
+
}
|
|
2408
|
+
if (routes.length === 0) {
|
|
2409
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No routes match this filter.</p>';
|
|
2410
|
+
return;
|
|
2411
|
+
}
|
|
2264
2412
|
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2265
2413
|
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2266
2414
|
const parts = route.split(" ");
|
|
2267
2415
|
const method = parts[0] ?? "";
|
|
2268
2416
|
const path = parts.slice(1).join(" ");
|
|
2269
|
-
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>`;
|
|
2417
|
+
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>`;
|
|
2270
2418
|
}).join("");
|
|
2271
2419
|
};
|
|
2420
|
+
const renderRoutes = async () => {
|
|
2421
|
+
const data = await api("GET", "/");
|
|
2422
|
+
if (!isRecord2(data))
|
|
2423
|
+
throw new Error("Route list was not an object");
|
|
2424
|
+
state.routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
2425
|
+
paintRoutes();
|
|
2426
|
+
};
|
|
2272
2427
|
const renderers = {
|
|
2273
2428
|
overview: renderOverview,
|
|
2274
2429
|
state: renderState,
|
|
@@ -2277,19 +2432,50 @@ function bootAdmin(config) {
|
|
|
2277
2432
|
journal: renderJournal,
|
|
2278
2433
|
routes: renderRoutes
|
|
2279
2434
|
};
|
|
2435
|
+
let refreshToken = 0;
|
|
2280
2436
|
async function refresh() {
|
|
2437
|
+
const token = ++refreshToken;
|
|
2281
2438
|
clearError();
|
|
2439
|
+
const main = el("views", HTMLElement);
|
|
2440
|
+
const sync = el("sync-status", HTMLElement);
|
|
2441
|
+
main.setAttribute("aria-busy", "true");
|
|
2442
|
+
sync.classList.add("busy");
|
|
2443
|
+
sync.textContent = "Updating";
|
|
2282
2444
|
const render = renderers[state.view];
|
|
2283
|
-
|
|
2284
|
-
|
|
2445
|
+
try {
|
|
2446
|
+
if (render)
|
|
2447
|
+
await render();
|
|
2448
|
+
if (token === refreshToken)
|
|
2449
|
+
sync.textContent = "Up to date";
|
|
2450
|
+
} finally {
|
|
2451
|
+
if (token === refreshToken) {
|
|
2452
|
+
main.removeAttribute("aria-busy");
|
|
2453
|
+
sync.classList.remove("busy");
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2285
2456
|
}
|
|
2286
2457
|
const delay = (ms) => new Promise((resolve) => {
|
|
2287
2458
|
window.setTimeout(resolve, ms);
|
|
2288
2459
|
});
|
|
2289
|
-
const confirmAction = async (button, pending, done, work) => {
|
|
2460
|
+
const confirmAction = async (button, pending, done, work, confirmLabel) => {
|
|
2290
2461
|
if (button.dataset.pending === "1")
|
|
2291
2462
|
return;
|
|
2292
2463
|
const label = button.textContent ?? "";
|
|
2464
|
+
if (confirmLabel !== void 0 && button.dataset.confirming !== "1") {
|
|
2465
|
+
button.dataset.confirming = "1";
|
|
2466
|
+
button.classList.add("confirming");
|
|
2467
|
+
button.textContent = confirmLabel;
|
|
2468
|
+
window.setTimeout(() => {
|
|
2469
|
+
if (button.dataset.confirming !== "1")
|
|
2470
|
+
return;
|
|
2471
|
+
button.textContent = label;
|
|
2472
|
+
button.classList.remove("confirming");
|
|
2473
|
+
delete button.dataset.confirming;
|
|
2474
|
+
}, 3e3);
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
button.classList.remove("confirming");
|
|
2478
|
+
delete button.dataset.confirming;
|
|
2293
2479
|
const width = button.offsetWidth;
|
|
2294
2480
|
button.dataset.pending = "1";
|
|
2295
2481
|
button.classList.add("pressed");
|
|
@@ -2310,6 +2496,65 @@ function bootAdmin(config) {
|
|
|
2310
2496
|
delete button.dataset.pending;
|
|
2311
2497
|
}
|
|
2312
2498
|
};
|
|
2499
|
+
const wireJsonEditor = (textareaId, highlightId, statusId, shellId, allowEmpty) => {
|
|
2500
|
+
const textarea = el(textareaId, HTMLTextAreaElement);
|
|
2501
|
+
const highlight = el(highlightId, HTMLElement);
|
|
2502
|
+
const status = el(statusId, HTMLElement);
|
|
2503
|
+
const shell = el(shellId, HTMLElement);
|
|
2504
|
+
const sync = () => {
|
|
2505
|
+
const text = textarea.value;
|
|
2506
|
+
highlight.innerHTML = `${highlightJsonText(text)}
|
|
2507
|
+
`;
|
|
2508
|
+
let valid = true;
|
|
2509
|
+
let message = "Valid JSON";
|
|
2510
|
+
if (allowEmpty && text.trim() === "")
|
|
2511
|
+
message = "No body";
|
|
2512
|
+
else {
|
|
2513
|
+
try {
|
|
2514
|
+
readJson2(text);
|
|
2515
|
+
} catch {
|
|
2516
|
+
valid = false;
|
|
2517
|
+
message = "Invalid JSON";
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
shell.classList.toggle("invalid", !valid);
|
|
2521
|
+
status.classList.toggle("invalid", !valid);
|
|
2522
|
+
status.textContent = message;
|
|
2523
|
+
textarea.setAttribute("aria-invalid", valid ? "false" : "true");
|
|
2524
|
+
};
|
|
2525
|
+
const syncScroll = () => {
|
|
2526
|
+
const preview2 = highlight.parentElement;
|
|
2527
|
+
if (!(preview2 instanceof HTMLElement))
|
|
2528
|
+
return;
|
|
2529
|
+
preview2.scrollTop = textarea.scrollTop;
|
|
2530
|
+
preview2.scrollLeft = textarea.scrollLeft;
|
|
2531
|
+
};
|
|
2532
|
+
textarea.addEventListener("input", sync);
|
|
2533
|
+
textarea.addEventListener("scroll", syncScroll);
|
|
2534
|
+
sync();
|
|
2535
|
+
return sync;
|
|
2536
|
+
};
|
|
2537
|
+
const syncRouteEditor = wireJsonEditor("route-body", "route-highlight", "route-json-status", "route-editor", true);
|
|
2538
|
+
const syncRecordEditor = wireJsonEditor("editor-value", "editor-highlight", "editor-json-status", "record-editor", false);
|
|
2539
|
+
for (const button of document.querySelectorAll("[data-format]")) {
|
|
2540
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2541
|
+
continue;
|
|
2542
|
+
button.addEventListener("click", () => {
|
|
2543
|
+
const target = button.dataset.format;
|
|
2544
|
+
if (target === void 0)
|
|
2545
|
+
return;
|
|
2546
|
+
const textarea = document.getElementById(target);
|
|
2547
|
+
if (!(textarea instanceof HTMLTextAreaElement) || textarea.value.trim() === "")
|
|
2548
|
+
return;
|
|
2549
|
+
try {
|
|
2550
|
+
textarea.value = JSON.stringify(readJson2(textarea.value), null, 2) ?? "";
|
|
2551
|
+
textarea.dispatchEvent(new Event("input"));
|
|
2552
|
+
textarea.focus();
|
|
2553
|
+
} catch {
|
|
2554
|
+
showError(new Error("Fix the JSON before formatting it"));
|
|
2555
|
+
}
|
|
2556
|
+
});
|
|
2557
|
+
}
|
|
2313
2558
|
document.addEventListener("click", (event) => {
|
|
2314
2559
|
const target = event.target;
|
|
2315
2560
|
if (!(target instanceof Element))
|
|
@@ -2382,6 +2627,7 @@ function bootAdmin(config) {
|
|
|
2382
2627
|
} else {
|
|
2383
2628
|
editorValue.value = "{\n \n}";
|
|
2384
2629
|
}
|
|
2630
|
+
syncRecordEditor();
|
|
2385
2631
|
editor.showModal();
|
|
2386
2632
|
}
|
|
2387
2633
|
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -2411,10 +2657,10 @@ function bootAdmin(config) {
|
|
|
2411
2657
|
const id = state.editing;
|
|
2412
2658
|
if (!id)
|
|
2413
2659
|
return;
|
|
2414
|
-
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2660
|
+
confirmAction(editorDelete, "Deleting...", "Deleted", () => api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2415
2661
|
editor.close();
|
|
2416
2662
|
return renderState();
|
|
2417
|
-
}).catch(showError);
|
|
2663
|
+
}), "Confirm delete").catch(showError);
|
|
2418
2664
|
});
|
|
2419
2665
|
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2420
2666
|
if (!(button instanceof HTMLButtonElement))
|
|
@@ -2460,10 +2706,10 @@ function bootAdmin(config) {
|
|
|
2460
2706
|
}).then(() => renderFaults()).catch(showError);
|
|
2461
2707
|
});
|
|
2462
2708
|
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2463
|
-
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
|
|
2709
|
+
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults()), "Confirm clear").catch(showError);
|
|
2464
2710
|
});
|
|
2465
2711
|
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2466
|
-
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
|
|
2712
|
+
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal()), "Confirm clear").catch(showError);
|
|
2467
2713
|
});
|
|
2468
2714
|
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2469
2715
|
const target = event.target;
|
|
@@ -2477,9 +2723,15 @@ function bootAdmin(config) {
|
|
|
2477
2723
|
return;
|
|
2478
2724
|
state.route = route;
|
|
2479
2725
|
el("route-title", HTMLElement).textContent = route;
|
|
2726
|
+
el("route-send", HTMLButtonElement).disabled = false;
|
|
2480
2727
|
const method = route.split(" ")[0] ?? "";
|
|
2481
|
-
el("route-body", HTMLTextAreaElement)
|
|
2728
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
2729
|
+
routeBody.disabled = method === "GET" || method === "DELETE";
|
|
2730
|
+
el("route-editor", HTMLElement).hidden = routeBody.disabled;
|
|
2731
|
+
el("route-json-status", HTMLElement).parentElement?.toggleAttribute("hidden", routeBody.disabled);
|
|
2732
|
+
syncRouteEditor();
|
|
2482
2733
|
});
|
|
2734
|
+
el("route-search", HTMLInputElement).addEventListener("input", paintRoutes);
|
|
2483
2735
|
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2484
2736
|
event.preventDefault();
|
|
2485
2737
|
const send = async () => {
|
|
@@ -2501,7 +2753,8 @@ function bootAdmin(config) {
|
|
|
2501
2753
|
}
|
|
2502
2754
|
}
|
|
2503
2755
|
const data = await api(method, concrete, body);
|
|
2504
|
-
|
|
2756
|
+
const text = JSON.stringify(data, null, 2) ?? "null";
|
|
2757
|
+
el("route-result", HTMLElement).innerHTML = `<code>${highlightJsonText(text)}</code>`;
|
|
2505
2758
|
};
|
|
2506
2759
|
send().catch(showError);
|
|
2507
2760
|
});
|
|
@@ -2619,11 +2872,13 @@ function bootAdmin(config) {
|
|
|
2619
2872
|
views.append(section);
|
|
2620
2873
|
mountSqlExplorer(body, panelApi);
|
|
2621
2874
|
}
|
|
2875
|
+
show(document.getElementById(`view-${state.view}`) === null ? "overview" : state.view);
|
|
2622
2876
|
return refresh();
|
|
2623
2877
|
}).catch(showError);
|
|
2624
2878
|
}
|
|
2625
2879
|
var clientPrelude = [
|
|
2626
2880
|
isRecord2,
|
|
2881
|
+
highlightJsonText,
|
|
2627
2882
|
mountAdminBrand,
|
|
2628
2883
|
startAdminBrand,
|
|
2629
2884
|
faultPresetList,
|
|
@@ -2646,6 +2901,7 @@ ${entry}(${payload});
|
|
|
2646
2901
|
var defineMock = (runtime) => runtime;
|
|
2647
2902
|
var STANDARD_ADMIN_ROUTES = [
|
|
2648
2903
|
"GET /",
|
|
2904
|
+
"GET /health",
|
|
2649
2905
|
"POST /reset",
|
|
2650
2906
|
"GET /namespaces",
|
|
2651
2907
|
"GET /clock",
|
|
@@ -2725,11 +2981,11 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
2725
2981
|
}
|
|
2726
2982
|
});
|
|
2727
2983
|
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
2728
|
-
var renderAdminDocument = (service) => {
|
|
2984
|
+
var renderAdminDocument = (service, adminPrefix = ADMIN_PREFIX) => {
|
|
2729
2985
|
const name = escapeHtml(service);
|
|
2730
2986
|
const script = adminClientSource({
|
|
2731
2987
|
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
2732
|
-
adminPrefix
|
|
2988
|
+
adminPrefix,
|
|
2733
2989
|
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
2734
2990
|
brandsUrl: ADMIN_BRANDS_URL,
|
|
2735
2991
|
service
|
|
@@ -2746,75 +3002,99 @@ var renderAdminDocument = (service) => {
|
|
|
2746
3002
|
${CSS_RESET}
|
|
2747
3003
|
:root {
|
|
2748
3004
|
color-scheme: light;
|
|
2749
|
-
--bg: #
|
|
3005
|
+
--bg: #f7f8fb;
|
|
2750
3006
|
--bg-elev: #ffffff;
|
|
2751
|
-
--bg-muted: #
|
|
2752
|
-
--bg-inset: #
|
|
2753
|
-
--line: #
|
|
2754
|
-
--line-strong: #
|
|
2755
|
-
--ink: #
|
|
2756
|
-
--muted: #
|
|
2757
|
-
--faint: #
|
|
2758
|
-
--accent: #
|
|
3007
|
+
--bg-muted: #f1f3f7;
|
|
3008
|
+
--bg-inset: #f8f9fc;
|
|
3009
|
+
--line: #e4e7ec;
|
|
3010
|
+
--line-strong: #cfd4dc;
|
|
3011
|
+
--ink: #101828;
|
|
3012
|
+
--muted: #475467;
|
|
3013
|
+
--faint: #667085;
|
|
3014
|
+
--accent: #6941c6;
|
|
3015
|
+
--accent-hover: #5933b4;
|
|
2759
3016
|
--accent-ink: #ffffff;
|
|
2760
|
-
--accent-soft: #
|
|
2761
|
-
--ok: #
|
|
2762
|
-
--ok-soft: #
|
|
2763
|
-
--warn: #
|
|
2764
|
-
--warn-soft: #
|
|
2765
|
-
--err: #
|
|
2766
|
-
--err-soft: #
|
|
2767
|
-
--
|
|
3017
|
+
--accent-soft: #f4f0ff;
|
|
3018
|
+
--ok: #067647;
|
|
3019
|
+
--ok-soft: #ecfdf3;
|
|
3020
|
+
--warn: #b54708;
|
|
3021
|
+
--warn-soft: #fffaeb;
|
|
3022
|
+
--err: #b42318;
|
|
3023
|
+
--err-soft: #fef3f2;
|
|
3024
|
+
--code-key: #6941c6;
|
|
3025
|
+
--code-string: #067647;
|
|
3026
|
+
--code-number: #175cd3;
|
|
3027
|
+
--code-boolean: #c11574;
|
|
3028
|
+
--code-null: #667085;
|
|
3029
|
+
--shadow-xs: 0 1px 2px rgb(16 24 40 / 0.05);
|
|
3030
|
+
--shadow-sm: 0 1px 3px rgb(16 24 40 / 0.08), 0 1px 2px rgb(16 24 40 / 0.04);
|
|
3031
|
+
--shadow-lg: 0 18px 48px rgb(16 24 40 / 0.16), 0 4px 12px rgb(16 24 40 / 0.08);
|
|
2768
3032
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2769
3033
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
2770
|
-
--radius:
|
|
2771
|
-
--header:
|
|
2772
|
-
--nav:
|
|
3034
|
+
--radius: 12px;
|
|
3035
|
+
--header: 72px;
|
|
3036
|
+
--nav: 224px;
|
|
2773
3037
|
}
|
|
2774
3038
|
:root[data-theme="dark"] {
|
|
2775
3039
|
color-scheme: dark;
|
|
2776
|
-
--bg: #
|
|
2777
|
-
--bg-elev: #
|
|
2778
|
-
--bg-muted: #
|
|
2779
|
-
--bg-inset: #
|
|
2780
|
-
--line: #
|
|
2781
|
-
--line-strong: #
|
|
2782
|
-
--ink: #
|
|
2783
|
-
--muted: #
|
|
2784
|
-
--faint: #
|
|
2785
|
-
--accent: #
|
|
2786
|
-
--accent-
|
|
2787
|
-
--accent-
|
|
2788
|
-
--
|
|
2789
|
-
--ok
|
|
2790
|
-
--
|
|
2791
|
-
--warn
|
|
2792
|
-
--
|
|
2793
|
-
--err
|
|
2794
|
-
--
|
|
3040
|
+
--bg: #0c0e14;
|
|
3041
|
+
--bg-elev: #151821;
|
|
3042
|
+
--bg-muted: #20232e;
|
|
3043
|
+
--bg-inset: #11141c;
|
|
3044
|
+
--line: #2b303d;
|
|
3045
|
+
--line-strong: #414858;
|
|
3046
|
+
--ink: #f5f7fa;
|
|
3047
|
+
--muted: #b6bdc9;
|
|
3048
|
+
--faint: #8d96a7;
|
|
3049
|
+
--accent: #9e77ed;
|
|
3050
|
+
--accent-hover: #b692f6;
|
|
3051
|
+
--accent-ink: #ffffff;
|
|
3052
|
+
--accent-soft: #2b2142;
|
|
3053
|
+
--ok: #75e0a7;
|
|
3054
|
+
--ok-soft: #102a20;
|
|
3055
|
+
--warn: #fec84b;
|
|
3056
|
+
--warn-soft: #302611;
|
|
3057
|
+
--err: #fda29b;
|
|
3058
|
+
--err-soft: #321817;
|
|
3059
|
+
--code-key: #c3a6ff;
|
|
3060
|
+
--code-string: #75e0a7;
|
|
3061
|
+
--code-number: #84adff;
|
|
3062
|
+
--code-boolean: #f9a8d4;
|
|
3063
|
+
--code-null: #98a2b3;
|
|
3064
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3065
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3066
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2795
3067
|
}
|
|
2796
3068
|
@media (prefers-color-scheme: dark) {
|
|
2797
3069
|
:root:not([data-theme="light"]) {
|
|
2798
3070
|
color-scheme: dark;
|
|
2799
|
-
--bg: #
|
|
2800
|
-
--bg-elev: #
|
|
2801
|
-
--bg-muted: #
|
|
2802
|
-
--bg-inset: #
|
|
2803
|
-
--line: #
|
|
2804
|
-
--line-strong: #
|
|
2805
|
-
--ink: #
|
|
2806
|
-
--muted: #
|
|
2807
|
-
--faint: #
|
|
2808
|
-
--accent: #
|
|
2809
|
-
--accent-
|
|
2810
|
-
--accent-
|
|
2811
|
-
--
|
|
2812
|
-
--ok
|
|
2813
|
-
--
|
|
2814
|
-
--warn
|
|
2815
|
-
--
|
|
2816
|
-
--err
|
|
2817
|
-
--
|
|
3071
|
+
--bg: #0c0e14;
|
|
3072
|
+
--bg-elev: #151821;
|
|
3073
|
+
--bg-muted: #20232e;
|
|
3074
|
+
--bg-inset: #11141c;
|
|
3075
|
+
--line: #2b303d;
|
|
3076
|
+
--line-strong: #414858;
|
|
3077
|
+
--ink: #f5f7fa;
|
|
3078
|
+
--muted: #b6bdc9;
|
|
3079
|
+
--faint: #8d96a7;
|
|
3080
|
+
--accent: #9e77ed;
|
|
3081
|
+
--accent-hover: #b692f6;
|
|
3082
|
+
--accent-ink: #ffffff;
|
|
3083
|
+
--accent-soft: #2b2142;
|
|
3084
|
+
--ok: #75e0a7;
|
|
3085
|
+
--ok-soft: #102a20;
|
|
3086
|
+
--warn: #fec84b;
|
|
3087
|
+
--warn-soft: #302611;
|
|
3088
|
+
--err: #fda29b;
|
|
3089
|
+
--err-soft: #321817;
|
|
3090
|
+
--code-key: #c3a6ff;
|
|
3091
|
+
--code-string: #75e0a7;
|
|
3092
|
+
--code-number: #84adff;
|
|
3093
|
+
--code-boolean: #f9a8d4;
|
|
3094
|
+
--code-null: #98a2b3;
|
|
3095
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3096
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3097
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2818
3098
|
}
|
|
2819
3099
|
}
|
|
2820
3100
|
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
@@ -2832,13 +3112,17 @@ var renderAdminDocument = (service) => {
|
|
|
2832
3112
|
padding-right: env(safe-area-inset-right);
|
|
2833
3113
|
-webkit-text-size-adjust: 100%;
|
|
2834
3114
|
}
|
|
2835
|
-
|
|
3115
|
+
::selection { background: color-mix(in srgb, var(--accent) 28%, transparent); }
|
|
3116
|
+
:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px; }
|
|
3117
|
+
button, input, select, textarea { transition: border-color 140ms ease, box-shadow 140ms ease, transform 100ms ease; }
|
|
3118
|
+
button:disabled, input:disabled, select:disabled, textarea:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
2836
3119
|
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
2837
3120
|
header.top {
|
|
2838
|
-
position: sticky; top: 0; z-index:
|
|
3121
|
+
position: sticky; top: 0; z-index: 20;
|
|
2839
3122
|
display: flex; align-items: center; gap: 12px;
|
|
2840
|
-
min-height: var(--header); padding:
|
|
2841
|
-
background: var(--bg-elev);
|
|
3123
|
+
min-height: var(--header); padding: 12px 20px;
|
|
3124
|
+
background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
|
|
3125
|
+
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
2842
3126
|
border-bottom: 1px solid var(--line);
|
|
2843
3127
|
min-width: 0; max-width: 100%;
|
|
2844
3128
|
}
|
|
@@ -2846,12 +3130,21 @@ var renderAdminDocument = (service) => {
|
|
|
2846
3130
|
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
2847
3131
|
min-width: 0; max-width: 100%; margin-right: auto;
|
|
2848
3132
|
}
|
|
2849
|
-
.brand-
|
|
2850
|
-
.brand-
|
|
3133
|
+
.brand-lockup { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
3134
|
+
.brand-mark {
|
|
3135
|
+
width: 38px; height: 38px; display: grid; place-items: center; flex: none;
|
|
3136
|
+
border-radius: 11px; color: #fff;
|
|
3137
|
+
background: linear-gradient(145deg, #7f56d9 0%, #53389e 100%);
|
|
3138
|
+
box-shadow: 0 6px 16px rgb(105 65 198 / 0.24), inset 0 1px 0 rgb(255 255 255 / 0.25);
|
|
3139
|
+
}
|
|
3140
|
+
.brand-mark svg { width: 22px; height: 22px; }
|
|
3141
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; line-height: 1.25; }
|
|
3142
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
2851
3143
|
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
2852
3144
|
.vendor {
|
|
2853
3145
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
2854
3146
|
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
3147
|
+
padding: 5px 8px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-inset);
|
|
2855
3148
|
}
|
|
2856
3149
|
.vendor[hidden] { display: none; }
|
|
2857
3150
|
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
@@ -2863,12 +3156,17 @@ var renderAdminDocument = (service) => {
|
|
|
2863
3156
|
.vendor-sep { color: var(--faint); }
|
|
2864
3157
|
.vendor-link { color: inherit; text-decoration: none; }
|
|
2865
3158
|
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
2866
|
-
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items:
|
|
2867
|
-
|
|
3159
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
|
|
3160
|
+
.sync-status { align-self: center; display: flex; align-items: center; gap: 6px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3161
|
+
.sync-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
|
|
3162
|
+
.sync-status.busy::before { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); animation: pulse 1.2s ease-in-out infinite; }
|
|
3163
|
+
label.field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; font-weight: 600; color: var(--faint); }
|
|
2868
3164
|
label.field input, label.field select, .controls input[type="password"] {
|
|
2869
|
-
background-color: var(--bg-
|
|
2870
|
-
padding:
|
|
3165
|
+
background-color: var(--bg-elev); border: 1px solid var(--line-strong); border-radius: 9px;
|
|
3166
|
+
padding: 8px 10px; min-height: 38px; line-height: 1.2; box-shadow: var(--shadow-xs);
|
|
2871
3167
|
}
|
|
3168
|
+
label.field input:hover, label.field select:hover, textarea:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--line-strong)); }
|
|
3169
|
+
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; }
|
|
2872
3170
|
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
2873
3171
|
the text fields in the same form. Paint it like those fields. */
|
|
2874
3172
|
label.field select {
|
|
@@ -2885,12 +3183,20 @@ var renderAdminDocument = (service) => {
|
|
|
2885
3183
|
dialog .field > textarea { width: 100%; }
|
|
2886
3184
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
2887
3185
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
.btn
|
|
3186
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
|
3187
|
+
border-radius: 9px; min-height: 38px; padding: 0 13px; border: 1px solid var(--line-strong);
|
|
3188
|
+
background: var(--bg-elev); color: var(--ink); font-size: 13px; font-weight: 600; line-height: 1;
|
|
3189
|
+
box-shadow: var(--shadow-xs);
|
|
3190
|
+
}
|
|
3191
|
+
.icon-btn { width: 38px; padding: 0; }
|
|
3192
|
+
.icon-btn svg, .btn svg, .btn-primary svg { width: 16px; height: 16px; }
|
|
3193
|
+
.icon-btn:hover, .btn:hover { background: var(--bg-muted); border-color: var(--line-strong); }
|
|
3194
|
+
.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; }
|
|
3195
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
3196
|
+
.btn-quiet { background: transparent; border-color: transparent; box-shadow: none; }
|
|
3197
|
+
.btn-quiet:hover { background: var(--bg-muted); }
|
|
3198
|
+
.btn-danger { color: var(--err); border-color: color-mix(in srgb, var(--err) 25%, var(--line)); }
|
|
3199
|
+
.btn-danger:hover, .btn-danger.confirming { background: var(--err-soft); border-color: color-mix(in srgb, var(--err) 45%, var(--line)); }
|
|
2894
3200
|
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
2895
3201
|
so the press itself has to stay visible. */
|
|
2896
3202
|
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
@@ -2906,93 +3212,159 @@ var renderAdminDocument = (service) => {
|
|
|
2906
3212
|
flex: 1 0 auto; min-width: 0;
|
|
2907
3213
|
}
|
|
2908
3214
|
nav.side {
|
|
2909
|
-
|
|
3215
|
+
position: sticky; top: var(--header); align-self: start; height: calc(100vh - var(--header));
|
|
3216
|
+
border-right: 1px solid var(--line); padding: 18px 12px;
|
|
2910
3217
|
display: flex; flex-direction: column; gap: 4px;
|
|
2911
|
-
background: var(--bg-elev);
|
|
3218
|
+
background: var(--bg-elev); overflow-y: auto;
|
|
2912
3219
|
}
|
|
3220
|
+
.nav-label { padding: 0 10px 8px; color: var(--faint); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
|
|
2913
3221
|
nav.side button {
|
|
3222
|
+
position: relative; display: flex; align-items: center; gap: 10px; width: 100%;
|
|
2914
3223
|
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
2915
|
-
padding: 9px 10px; min-height:
|
|
2916
|
-
}
|
|
2917
|
-
nav.side button
|
|
2918
|
-
|
|
2919
|
-
.
|
|
2920
|
-
.
|
|
2921
|
-
.
|
|
3224
|
+
padding: 9px 10px; min-height: 42px; color: var(--muted); font-size: 13px; font-weight: 550;
|
|
3225
|
+
}
|
|
3226
|
+
nav.side button:hover { background: var(--bg-muted); color: var(--ink); }
|
|
3227
|
+
nav.side button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 650; }
|
|
3228
|
+
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); }
|
|
3229
|
+
.nav-icon { width: 18px; height: 18px; display: grid; place-items: center; flex: none; }
|
|
3230
|
+
.nav-icon svg { width: 18px; height: 18px; }
|
|
3231
|
+
main { padding: 32px clamp(20px, 4vw, 48px) 64px; max-width: 1320px; width: 100%; min-width: 0; }
|
|
3232
|
+
main[aria-busy="true"] .view { opacity: 0.62; }
|
|
3233
|
+
.view { animation: view-in 180ms ease-out; }
|
|
3234
|
+
.view[hidden] { display: none; }
|
|
3235
|
+
.view-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
|
|
3236
|
+
.view h2 { margin: 0 0 5px; font-size: clamp(24px, 3vw, 30px); line-height: 1.2; letter-spacing: -0.035em; }
|
|
3237
|
+
.view h3 { margin: 24px 0 10px; font-size: 15px; letter-spacing: -0.01em; }
|
|
3238
|
+
.lede { margin: 0; color: var(--muted); max-width: 72ch; font-size: 14px; }
|
|
3239
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 175px), 1fr)); gap: 14px; }
|
|
2922
3240
|
.card {
|
|
2923
3241
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
2924
|
-
padding:
|
|
3242
|
+
padding: 18px; box-shadow: var(--shadow-sm);
|
|
2925
3243
|
}
|
|
2926
|
-
.card
|
|
2927
|
-
.card
|
|
3244
|
+
.stat-card { position: relative; overflow: hidden; }
|
|
3245
|
+
.stat-card::after { content: ""; position: absolute; inset: 0 0 auto; height: 2px; background: linear-gradient(90deg, var(--accent), transparent 78%); opacity: 0.7; }
|
|
3246
|
+
.card .k { color: var(--faint); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
3247
|
+
.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; }
|
|
2928
3248
|
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
2929
|
-
.
|
|
3249
|
+
.section-card { background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-xs); overflow: hidden; }
|
|
3250
|
+
.section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
|
|
3251
|
+
.section-title h3 { margin: 0; }
|
|
3252
|
+
.split { display: grid; grid-template-columns: minmax(0, 250px) minmax(0, 1fr); gap: 18px; align-items: start; }
|
|
3253
|
+
#collections { padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); box-shadow: var(--shadow-xs); }
|
|
2930
3254
|
.collection {
|
|
2931
3255
|
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
2932
|
-
border-radius:
|
|
3256
|
+
border-radius: 9px; padding: 10px 11px; min-height: 48px; color: var(--ink); cursor: pointer;
|
|
2933
3257
|
}
|
|
2934
|
-
.collection
|
|
3258
|
+
.collection:hover { background: var(--bg-muted); }
|
|
3259
|
+
.collection[aria-current="true"] { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 18%, var(--line)); color: var(--accent); }
|
|
2935
3260
|
.collection small { display: block; color: var(--faint); }
|
|
2936
3261
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
2937
|
-
.chip {
|
|
2938
|
-
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
2939
|
-
border-radius: 999px; padding: 4px 8px;
|
|
2940
|
-
}
|
|
3262
|
+
.chip { font-family: var(--mono); font-size: 11px; background: var(--bg-muted); border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; }
|
|
2941
3263
|
.chip i { color: var(--faint); font-style: normal; }
|
|
2942
3264
|
.sql-layout {
|
|
2943
3265
|
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
2944
3266
|
}
|
|
2945
|
-
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
2946
|
-
.sql-tables button { text-align: left; }
|
|
3267
|
+
.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); }
|
|
3268
|
+
.sql-tables button { text-align: left; border: 0; border-radius: 8px; padding: 9px 10px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
3269
|
+
.sql-tables button:hover, .sql-tables button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); }
|
|
2947
3270
|
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
2948
|
-
table { width: 100%; border-
|
|
2949
|
-
|
|
2950
|
-
th {
|
|
3271
|
+
.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); }
|
|
3272
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
3273
|
+
th, td { text-align: left; padding: 11px 13px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
3274
|
+
th { color: var(--faint); background: var(--bg-inset); font-weight: 650; font-size: 11px; text-transform: uppercase; letter-spacing: 0.045em; }
|
|
3275
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
2951
3276
|
tr.clickable { cursor: pointer; }
|
|
2952
|
-
tr.clickable:hover { background: var(--
|
|
3277
|
+
tr.clickable:hover { background: var(--accent-soft); }
|
|
2953
3278
|
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
2954
3279
|
pre {
|
|
2955
3280
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
2956
|
-
background: var(--bg-inset); border-radius: 10px; padding:
|
|
2957
|
-
}
|
|
2958
|
-
.
|
|
3281
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 10px; padding: 14px; font-size: 12px; line-height: 1.65;
|
|
3282
|
+
}
|
|
3283
|
+
.json-key { color: var(--code-key); }
|
|
3284
|
+
.json-string { color: var(--code-string); }
|
|
3285
|
+
.json-number { color: var(--code-number); }
|
|
3286
|
+
.json-boolean { color: var(--code-boolean); }
|
|
3287
|
+
.json-null { color: var(--code-null); font-style: italic; }
|
|
3288
|
+
.json-inline { display: block; max-width: 44rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
3289
|
+
.json-view { position: relative; max-height: 26rem; overflow: auto; }
|
|
3290
|
+
.json-editor { position: relative; min-height: 190px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--bg-inset); overflow: hidden; }
|
|
3291
|
+
.json-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); }
|
|
3292
|
+
.json-editor.invalid { border-color: var(--err); }
|
|
3293
|
+
.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; }
|
|
3294
|
+
.json-editor pre { position: absolute; inset: 0; pointer-events: none; background: transparent; }
|
|
3295
|
+
.json-editor textarea { position: relative; z-index: 1; resize: vertical; background: transparent; color: transparent; caret-color: var(--ink); -webkit-text-fill-color: transparent; }
|
|
3296
|
+
.json-editor textarea::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); }
|
|
3297
|
+
.editor-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; }
|
|
3298
|
+
.json-status { display: inline-flex; align-items: center; gap: 6px; color: var(--ok); font-size: 11px; }
|
|
3299
|
+
.json-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
3300
|
+
.json-status.invalid { color: var(--err); }
|
|
3301
|
+
.format-json { border: 0; padding: 3px 5px; background: transparent; color: var(--accent); font-size: 11px; font-weight: 650; cursor: pointer; }
|
|
3302
|
+
.row-actions { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 14px 0; }
|
|
2959
3303
|
.banner {
|
|
2960
|
-
display: none;
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
3304
|
+
display: none; position: sticky; top: calc(var(--header) + 12px); z-index: 15;
|
|
3305
|
+
align-items: center; gap: 10px; margin: 0 0 16px; padding: 11px 12px 11px 14px;
|
|
3306
|
+
border: 1px solid color-mix(in srgb, var(--err) 30%, var(--line)); border-radius: 10px;
|
|
3307
|
+
background: var(--err-soft); color: var(--err); box-shadow: var(--shadow-sm); font-size: 13px;
|
|
3308
|
+
}
|
|
3309
|
+
.banner.show { display: flex; }
|
|
3310
|
+
.banner span { flex: 1; }
|
|
3311
|
+
.banner button { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 7px; background: transparent; color: currentColor; cursor: pointer; }
|
|
3312
|
+
.banner button:hover { background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
3313
|
+
.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); }
|
|
2965
3314
|
dialog {
|
|
2966
|
-
border: 1px solid var(--line); border-radius:
|
|
2967
|
-
color: var(--ink); width: min(
|
|
2968
|
-
}
|
|
2969
|
-
dialog
|
|
2970
|
-
dialog
|
|
3315
|
+
border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--bg-elev);
|
|
3316
|
+
color: var(--ink); width: min(720px, calc(100% - 24px)); max-width: calc(100vw - 24px); box-shadow: var(--shadow-lg);
|
|
3317
|
+
}
|
|
3318
|
+
dialog[open] { animation: dialog-in 160ms ease-out; }
|
|
3319
|
+
dialog::backdrop { background: rgb(12 14 20 / 0.58); backdrop-filter: blur(3px); }
|
|
3320
|
+
dialog form, .dialog-body { padding: 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
3321
|
+
.dialog-heading { display: flex; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
|
3322
|
+
.dialog-heading strong { font-size: 18px; letter-spacing: -0.02em; }
|
|
3323
|
+
.dialog-heading span { color: var(--muted); font-size: 12px; }
|
|
2971
3324
|
textarea {
|
|
2972
3325
|
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
2973
3326
|
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
2974
3327
|
font-family: var(--mono); font-size: 12px;
|
|
2975
3328
|
}
|
|
2976
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
2977
|
-
.
|
|
3329
|
+
.dialog-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding-top: 4px; }
|
|
3330
|
+
.dialog-actions .btn-danger { margin-right: auto; }
|
|
3331
|
+
.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); }
|
|
2978
3332
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
2979
3333
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
2980
3334
|
.status-2 { color: var(--ok); }
|
|
2981
3335
|
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
2982
3336
|
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
3337
|
+
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
3338
|
+
.form-grid .wide { grid-column: span 2; }
|
|
2983
3339
|
.route {
|
|
2984
|
-
display: grid; grid-template-columns:
|
|
2985
|
-
padding:
|
|
2986
|
-
}
|
|
2987
|
-
.
|
|
3340
|
+
display: grid; grid-template-columns: 84px minmax(0, 1fr) auto; gap: 10px; align-items: center;
|
|
3341
|
+
padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--bg-elev);
|
|
3342
|
+
}
|
|
3343
|
+
.route:first-child { border-radius: var(--radius) var(--radius) 0 0; }
|
|
3344
|
+
.route:last-child { border-bottom: 0; border-radius: 0 0 var(--radius) var(--radius); }
|
|
3345
|
+
.route-tools { display: flex; align-items: flex-end; gap: 12px; margin-bottom: 12px; }
|
|
3346
|
+
.route-tools .field { flex: 1; }
|
|
3347
|
+
.route-tools input { width: 100%; }
|
|
3348
|
+
.route-count { padding-bottom: 10px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3349
|
+
#route-list { max-height: 34rem; border: 1px solid var(--line); border-radius: var(--radius); overflow: auto; box-shadow: var(--shadow-xs); }
|
|
3350
|
+
.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; }
|
|
3351
|
+
@keyframes view-in { from { transform: translateY(3px); } to { transform: translateY(0); } }
|
|
3352
|
+
@keyframes dialog-in { from { transform: translateY(8px) scale(0.99); } to { transform: translateY(0) scale(1); } }
|
|
3353
|
+
@keyframes pulse { 50% { opacity: 0.45; } }
|
|
3354
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
|
|
3355
|
+
@media (forced-colors: active) { .json-editor pre { display: none; } .json-editor textarea { color: CanvasText; -webkit-text-fill-color: CanvasText; } }
|
|
2988
3356
|
@media (max-width: 800px) {
|
|
3357
|
+
:root { --header: 0px; }
|
|
2989
3358
|
header.top {
|
|
2990
|
-
align-items: stretch; flex-direction: column;
|
|
2991
|
-
padding
|
|
3359
|
+
position: relative; align-items: stretch; flex-direction: column;
|
|
3360
|
+
padding: max(12px, env(safe-area-inset-top)) 14px 12px;
|
|
2992
3361
|
}
|
|
2993
3362
|
input, select, textarea { font-size: 16px; }
|
|
2994
3363
|
.brand, .vendor, .controls { width: 100%; }
|
|
3364
|
+
.brand { gap: 10px; }
|
|
3365
|
+
.vendor { order: 2; border-radius: 9px; }
|
|
2995
3366
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
3367
|
+
.sync-status { display: none; }
|
|
2996
3368
|
/* The layout fills the screen. A single-column grid stretches every auto
|
|
2997
3369
|
row, so the tab bar grew with the leftover height and the buttons floated
|
|
2998
3370
|
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
@@ -3002,14 +3374,23 @@ var renderAdminDocument = (service) => {
|
|
|
3002
3374
|
align-content: start;
|
|
3003
3375
|
}
|
|
3004
3376
|
nav.side {
|
|
3005
|
-
align-self: start; height: auto;
|
|
3377
|
+
position: sticky; top: 0; z-index: 12; align-self: start; height: auto;
|
|
3006
3378
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
3007
3379
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
3008
|
-
padding: 8px;
|
|
3380
|
+
padding: 8px; box-shadow: var(--shadow-xs);
|
|
3009
3381
|
}
|
|
3010
|
-
nav
|
|
3382
|
+
.nav-label { display: none; }
|
|
3383
|
+
nav.side button { width: auto; white-space: nowrap; flex: none; }
|
|
3384
|
+
nav.side button[aria-current="true"]::before { inset: auto 9px -8px; width: auto; height: 3px; border-radius: 3px 3px 0 0; }
|
|
3011
3385
|
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
3012
|
-
|
|
3386
|
+
.form-grid { grid-template-columns: minmax(0, 1fr); }
|
|
3387
|
+
.form-grid .wide { grid-column: auto; }
|
|
3388
|
+
main { padding: 22px 14px 48px; }
|
|
3389
|
+
.view-head { margin-bottom: 18px; }
|
|
3390
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
3391
|
+
.card { padding: 14px; }
|
|
3392
|
+
.card .v { font-size: 20px; }
|
|
3393
|
+
.table-shell { border: 0; background: transparent; box-shadow: none; overflow: visible; }
|
|
3013
3394
|
table, thead, tbody, tr, th, td { display: block; }
|
|
3014
3395
|
thead { display: none; }
|
|
3015
3396
|
tr {
|
|
@@ -3024,50 +3405,63 @@ var renderAdminDocument = (service) => {
|
|
|
3024
3405
|
.route { grid-template-columns: 72px 1fr; }
|
|
3025
3406
|
.route button { grid-column: 1 / -1; }
|
|
3026
3407
|
}
|
|
3408
|
+
@media (max-width: 460px) {
|
|
3409
|
+
.controls label.field { min-width: calc(50% - 4px); }
|
|
3410
|
+
.controls .icon-btn, .controls .btn { flex: 1; }
|
|
3411
|
+
.grid { grid-template-columns: minmax(0, 1fr); }
|
|
3412
|
+
.dialog-actions { flex-wrap: wrap; }
|
|
3413
|
+
.dialog-actions button { flex: 1; }
|
|
3414
|
+
.dialog-actions .btn-danger { flex-basis: 100%; margin-right: 0; }
|
|
3415
|
+
}
|
|
3027
3416
|
</style>
|
|
3028
3417
|
</head>
|
|
3029
3418
|
<body>
|
|
3030
3419
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
3031
3420
|
<header class="top">
|
|
3032
3421
|
<div class="brand">
|
|
3033
|
-
<div class="brand-
|
|
3034
|
-
<
|
|
3035
|
-
|
|
3422
|
+
<div class="brand-lockup">
|
|
3423
|
+
<div class="brand-mark" aria-hidden="true">
|
|
3424
|
+
<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>
|
|
3425
|
+
</div>
|
|
3426
|
+
<div class="brand-id">
|
|
3427
|
+
<strong>${name}</strong>
|
|
3428
|
+
<span>Mock admin</span>
|
|
3429
|
+
</div>
|
|
3036
3430
|
</div>
|
|
3037
3431
|
<div class="vendor" id="vendor" hidden></div>
|
|
3038
3432
|
</div>
|
|
3039
3433
|
<div class="controls">
|
|
3434
|
+
<span class="sync-status" id="sync-status" role="status" aria-live="polite">Ready</span>
|
|
3040
3435
|
<label class="field">Namespace
|
|
3041
3436
|
<select id="namespace" aria-label="Namespace"></select>
|
|
3042
3437
|
</label>
|
|
3043
3438
|
<label class="field">Admin key
|
|
3044
3439
|
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
3045
3440
|
</label>
|
|
3046
|
-
<button class="icon-btn" id="theme" type="button" aria-label="
|
|
3047
|
-
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
3441
|
+
<button class="icon-btn" id="theme" type="button" aria-label="Use dark theme" title="Use dark theme"></button>
|
|
3442
|
+
<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>
|
|
3048
3443
|
</div>
|
|
3049
3444
|
</header>
|
|
3050
3445
|
<div class="layout">
|
|
3051
3446
|
<nav class="side" id="nav" aria-label="Admin sections">
|
|
3052
|
-
<
|
|
3053
|
-
<button type="button" data-view="
|
|
3054
|
-
<button type="button" data-view="
|
|
3055
|
-
<button type="button" data-view="
|
|
3056
|
-
<button type="button" data-view="
|
|
3057
|
-
<button type="button" data-view="
|
|
3447
|
+
<span class="nav-label">Workspace</span>
|
|
3448
|
+
<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>
|
|
3449
|
+
<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>
|
|
3450
|
+
<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>
|
|
3451
|
+
<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>
|
|
3452
|
+
<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>
|
|
3453
|
+
<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>
|
|
3058
3454
|
</nav>
|
|
3059
3455
|
<main id="views">
|
|
3060
|
-
<
|
|
3456
|
+
<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>
|
|
3061
3457
|
<section class="view" id="view-overview">
|
|
3062
|
-
<h2>Overview</h2>
|
|
3063
|
-
<p class="lede">Health, clock, and the collections this namespace is holding.</p>
|
|
3458
|
+
<div class="view-head"><div><h2>Overview</h2><p class="lede">Health, clock, and the collections this namespace is holding.</p></div></div>
|
|
3064
3459
|
<div class="grid" id="overview-cards"></div>
|
|
3065
3460
|
<h3>Recent requests</h3>
|
|
3066
3461
|
<div id="overview-requests"></div>
|
|
3067
3462
|
</section>
|
|
3068
3463
|
<section class="view" id="view-state" hidden>
|
|
3069
|
-
<h2>State</h2>
|
|
3070
|
-
<p class="lede">Every collection in this namespace. Declared fields stay visible before any row exists; stored rows add whatever else they contain.</p>
|
|
3464
|
+
<div class="view-head"><div><h2>State</h2><p class="lede">Inspect and edit every collection in this namespace.</p></div></div>
|
|
3071
3465
|
<div class="split">
|
|
3072
3466
|
<div id="collections"></div>
|
|
3073
3467
|
<div>
|
|
@@ -3080,8 +3474,7 @@ var renderAdminDocument = (service) => {
|
|
|
3080
3474
|
</div>
|
|
3081
3475
|
</section>
|
|
3082
3476
|
<section class="view" id="view-clock" hidden>
|
|
3083
|
-
<h2>Clock</h2>
|
|
3084
|
-
<p class="lede">The clock every timestamp in this mock reads.</p>
|
|
3477
|
+
<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>
|
|
3085
3478
|
<div class="card" id="clock-readout"></div>
|
|
3086
3479
|
<div class="row-actions">
|
|
3087
3480
|
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
@@ -3100,21 +3493,22 @@ var renderAdminDocument = (service) => {
|
|
|
3100
3493
|
</form>
|
|
3101
3494
|
</section>
|
|
3102
3495
|
<section class="view" id="view-faults" hidden>
|
|
3103
|
-
<h2>Faults</h2>
|
|
3104
|
-
<p class="lede">Inject a response, a delay, or a named preset. Rules apply to this namespace.</p>
|
|
3496
|
+
<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>
|
|
3105
3497
|
<form id="fault-form" class="card stack">
|
|
3106
|
-
<
|
|
3107
|
-
<
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
<
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
<
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
<
|
|
3117
|
-
|
|
3498
|
+
<div class="form-grid">
|
|
3499
|
+
<label class="field wide">Preset
|
|
3500
|
+
<select id="fault-preset"><option value="">Custom rule</option></select>
|
|
3501
|
+
</label>
|
|
3502
|
+
<label class="field">Status
|
|
3503
|
+
<input id="fault-status" inputmode="numeric" placeholder="503">
|
|
3504
|
+
</label>
|
|
3505
|
+
<label class="field wide">Path prefix
|
|
3506
|
+
<input id="fault-path" placeholder="/v1">
|
|
3507
|
+
</label>
|
|
3508
|
+
<label class="field">Method
|
|
3509
|
+
<input id="fault-method" placeholder="GET">
|
|
3510
|
+
</label>
|
|
3511
|
+
</div>
|
|
3118
3512
|
<div class="dialog-actions">
|
|
3119
3513
|
<button class="btn-primary" type="submit">Add fault</button>
|
|
3120
3514
|
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
@@ -3123,26 +3517,31 @@ var renderAdminDocument = (service) => {
|
|
|
3123
3517
|
<div id="fault-list"></div>
|
|
3124
3518
|
</section>
|
|
3125
3519
|
<section class="view" id="view-journal" hidden>
|
|
3126
|
-
<h2>Journal</h2>
|
|
3127
|
-
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
3128
|
-
<div class="row-actions">
|
|
3129
|
-
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
3130
|
-
</div>
|
|
3520
|
+
<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>
|
|
3131
3521
|
<div id="journal"></div>
|
|
3132
3522
|
</section>
|
|
3133
3523
|
<section class="view" id="view-routes" hidden>
|
|
3134
|
-
<h2>Routes</h2>
|
|
3135
|
-
<
|
|
3524
|
+
<div class="view-head"><div><h2>Routes</h2><p class="lede">Explore shared controls and service-specific admin endpoints.</p></div></div>
|
|
3525
|
+
<div class="route-tools">
|
|
3526
|
+
<label class="field">Filter routes
|
|
3527
|
+
<input id="route-search" type="search" placeholder="Method, path, or extension" autocomplete="off">
|
|
3528
|
+
</label>
|
|
3529
|
+
<span class="route-count" id="route-count"></span>
|
|
3530
|
+
</div>
|
|
3136
3531
|
<div id="route-list"></div>
|
|
3137
|
-
<form id="route-form" class="card stack">
|
|
3532
|
+
<form id="route-form" class="card stack" style="margin-top: 18px">
|
|
3138
3533
|
<strong id="route-title">Select a route</strong>
|
|
3139
3534
|
<label class="field">JSON body
|
|
3140
|
-
<
|
|
3535
|
+
<div class="json-editor" id="route-editor" hidden>
|
|
3536
|
+
<pre aria-hidden="true"><code id="route-highlight"></code></pre>
|
|
3537
|
+
<textarea id="route-body" placeholder="{ }" spellcheck="false" aria-describedby="route-json-status" disabled></textarea>
|
|
3538
|
+
</div>
|
|
3539
|
+
<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>
|
|
3141
3540
|
</label>
|
|
3142
3541
|
<div class="dialog-actions">
|
|
3143
|
-
<button class="btn-primary" type="submit">Send</button>
|
|
3542
|
+
<button class="btn-primary" id="route-send" type="submit" disabled>Send request</button>
|
|
3144
3543
|
</div>
|
|
3145
|
-
<pre id="route-result">Response will show here.</pre>
|
|
3544
|
+
<pre class="json-view" id="route-result"><code>Response will show here.</code></pre>
|
|
3146
3545
|
</form>
|
|
3147
3546
|
</section>
|
|
3148
3547
|
</main>
|
|
@@ -3150,12 +3549,16 @@ var renderAdminDocument = (service) => {
|
|
|
3150
3549
|
</div>
|
|
3151
3550
|
<dialog id="editor">
|
|
3152
3551
|
<form method="dialog" id="editor-form">
|
|
3153
|
-
<strong id="editor-title">Record</strong>
|
|
3552
|
+
<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>
|
|
3154
3553
|
<label class="field">Id
|
|
3155
3554
|
<input id="editor-id" required>
|
|
3156
3555
|
</label>
|
|
3157
3556
|
<label class="field">JSON value
|
|
3158
|
-
<
|
|
3557
|
+
<div class="json-editor" id="record-editor">
|
|
3558
|
+
<pre aria-hidden="true"><code id="editor-highlight"></code></pre>
|
|
3559
|
+
<textarea id="editor-value" spellcheck="false" aria-describedby="editor-json-status"></textarea>
|
|
3560
|
+
</div>
|
|
3561
|
+
<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>
|
|
3159
3562
|
</label>
|
|
3160
3563
|
<div class="dialog-actions">
|
|
3161
3564
|
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
@@ -3197,8 +3600,8 @@ var describeExtension = (extension) => {
|
|
|
3197
3600
|
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
3198
3601
|
};
|
|
3199
3602
|
};
|
|
3200
|
-
var adminUiRoutes = (service, ui) => {
|
|
3201
|
-
const shell = () => renderAdminDocument(service);
|
|
3603
|
+
var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
3604
|
+
const shell = () => renderAdminDocument(service, adminPrefix);
|
|
3202
3605
|
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
3203
3606
|
return {
|
|
3204
3607
|
"GET /ui": () => htmlResponse(document3()),
|
|
@@ -3815,14 +4218,17 @@ var stateAdminRoutes = (routes) => {
|
|
|
3815
4218
|
};
|
|
3816
4219
|
|
|
3817
4220
|
// ../core/dist/version.js
|
|
3818
|
-
var PACKAGE_VERSION = true ? "
|
|
4221
|
+
var PACKAGE_VERSION = true ? "2.0.0" : UNRELEASED_VERSION;
|
|
3819
4222
|
|
|
3820
4223
|
// ../core/dist/signing.js
|
|
3821
4224
|
var encoder = new TextEncoder();
|
|
3822
4225
|
var fromBase64 = (value) => Uint8Array.from(atob(value), (char) => char.charCodeAt(0));
|
|
3823
4226
|
|
|
3824
4227
|
// ../core/dist/webhooks.js
|
|
3825
|
-
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4228
|
+
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4229
|
+
status,
|
|
4230
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4231
|
+
});
|
|
3826
4232
|
var adminError3 = (status, message) => json4(status, { error: { type: "mockingbird_admin", message } });
|
|
3827
4233
|
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3828
4234
|
var parseEndpoint = (value) => {
|
|
@@ -3930,7 +4336,6 @@ var AT_HEADER = "x-mockingbird-at";
|
|
|
3930
4336
|
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
3931
4337
|
var DEFAULT_NAMESPACE = "default";
|
|
3932
4338
|
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
3933
|
-
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
3934
4339
|
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
3935
4340
|
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
3936
4341
|
var effects = /* @__PURE__ */ new WeakMap();
|
|
@@ -3970,6 +4375,22 @@ var operationMatcher = (document3) => {
|
|
|
3970
4375
|
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
3971
4376
|
};
|
|
3972
4377
|
var createRuntime = (options) => {
|
|
4378
|
+
const adminPrefix = resolveAdminPrefix(options.adminPrefix);
|
|
4379
|
+
const internalPaths = Object.entries(options.document?.paths ?? {}).filter(([, item]) => item["x-mockingbird-internal"] === true).map(([path]) => path);
|
|
4380
|
+
for (const path of internalPaths) {
|
|
4381
|
+
if (!path.startsWith(`${ADMIN_PREFIX}/blobs/`))
|
|
4382
|
+
throw new Error(`internal transport ${path} must live under ${ADMIN_PREFIX}/blobs/`);
|
|
4383
|
+
}
|
|
4384
|
+
assertAdminPrefixAvailable(adminPrefix, Object.keys(options.document?.paths ?? {}).filter((path) => !internalPaths.includes(path)));
|
|
4385
|
+
const internalMatch = (path) => {
|
|
4386
|
+
for (const pattern of internalPaths) {
|
|
4387
|
+
const actual = path.split("/");
|
|
4388
|
+
const expected = pattern.split("/");
|
|
4389
|
+
if (actual.length === expected.length && expected.every((part, i) => part.startsWith("{") || part === actual[i]))
|
|
4390
|
+
return pattern;
|
|
4391
|
+
}
|
|
4392
|
+
return void 0;
|
|
4393
|
+
};
|
|
3973
4394
|
assertAdminUi(options.adminUi);
|
|
3974
4395
|
const sqlite = bootSqlite(options.sqlite);
|
|
3975
4396
|
const clock = options.clock ?? createClock();
|
|
@@ -3984,6 +4405,20 @@ var createRuntime = (options) => {
|
|
|
3984
4405
|
const instances = /* @__PURE__ */ new Map();
|
|
3985
4406
|
const publicNamespaces = /* @__PURE__ */ new Set();
|
|
3986
4407
|
const branchRngs = /* @__PURE__ */ new Map();
|
|
4408
|
+
const namespaceClocks = /* @__PURE__ */ new Map();
|
|
4409
|
+
const namespaceForStorage = /* @__PURE__ */ new Map();
|
|
4410
|
+
let isolatedNamespaces = false;
|
|
4411
|
+
const namespaceClock = (name = DEFAULT_NAMESPACE) => {
|
|
4412
|
+
if (!isolatedNamespaces || name === DEFAULT_NAMESPACE)
|
|
4413
|
+
return clock;
|
|
4414
|
+
let found = namespaceClocks.get(name);
|
|
4415
|
+
if (!found) {
|
|
4416
|
+
found = createClock();
|
|
4417
|
+
namespaceClocks.set(name, found);
|
|
4418
|
+
}
|
|
4419
|
+
return found;
|
|
4420
|
+
};
|
|
4421
|
+
const fleetPoints = /* @__PURE__ */ new WeakMap();
|
|
3987
4422
|
const timelines = /* @__PURE__ */ new Map();
|
|
3988
4423
|
const branchStorage = /* @__PURE__ */ new Map();
|
|
3989
4424
|
const captured = /* @__PURE__ */ new Map();
|
|
@@ -3997,17 +4432,22 @@ var createRuntime = (options) => {
|
|
|
3997
4432
|
if (!NAMESPACE_PATTERN.test(key) || !NAMESPACE_PATTERN.test(publicNamespace)) {
|
|
3998
4433
|
throw new RangeError(`namespace must match ${NAMESPACE_PATTERN}: ${JSON.stringify(publicNamespace)}`);
|
|
3999
4434
|
}
|
|
4435
|
+
const createdContextRng = isolatedRng ?? (isolatedNamespaces && key !== DEFAULT_NAMESPACE ? createRng(options.seed ?? 0) : rng);
|
|
4000
4436
|
const created = options.create({
|
|
4001
4437
|
namespace: storageNamespace(key),
|
|
4002
4438
|
publicNamespace,
|
|
4439
|
+
adminPrefix,
|
|
4003
4440
|
sqlite,
|
|
4004
|
-
clock,
|
|
4005
|
-
rng:
|
|
4441
|
+
clock: namespaceClock(publicNamespace),
|
|
4442
|
+
rng: createdContextRng
|
|
4006
4443
|
});
|
|
4007
4444
|
instances.set(key, created);
|
|
4445
|
+
namespaceForStorage.set(key, publicNamespace);
|
|
4008
4446
|
publicNamespaces.add(publicNamespace);
|
|
4009
4447
|
if (isolatedRng)
|
|
4010
4448
|
branchRngs.set(key, isolatedRng);
|
|
4449
|
+
else if (isolatedNamespaces && key !== DEFAULT_NAMESPACE)
|
|
4450
|
+
branchRngs.set(key, createdContextRng);
|
|
4011
4451
|
return created;
|
|
4012
4452
|
};
|
|
4013
4453
|
const instance = (name = DEFAULT_NAMESPACE) => instanceFor(name);
|
|
@@ -4025,7 +4465,7 @@ var createRuntime = (options) => {
|
|
|
4025
4465
|
captured.set(storage, snapshot2);
|
|
4026
4466
|
return Object.freeze({
|
|
4027
4467
|
snapshot: snapshot2,
|
|
4028
|
-
clock: Object.freeze(
|
|
4468
|
+
clock: Object.freeze(namespaceClock(namespaceForStorage.get(storage) ?? storage).state()),
|
|
4029
4469
|
rngState: (branchRngs.get(storage) ?? rng).state()
|
|
4030
4470
|
});
|
|
4031
4471
|
};
|
|
@@ -4035,7 +4475,7 @@ var createRuntime = (options) => {
|
|
|
4035
4475
|
return found;
|
|
4036
4476
|
instance(name);
|
|
4037
4477
|
found = new Timeline({
|
|
4038
|
-
now:
|
|
4478
|
+
now: namespaceClock(name).now,
|
|
4039
4479
|
...options.maxCheckpoints !== void 0 ? { maxCheckpoints: options.maxCheckpoints } : {}
|
|
4040
4480
|
});
|
|
4041
4481
|
found.commit(capture(name));
|
|
@@ -4062,12 +4502,13 @@ var createRuntime = (options) => {
|
|
|
4062
4502
|
const point = history2.checkout("main", at);
|
|
4063
4503
|
restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
|
|
4064
4504
|
captured.set(namespace, point.value.snapshot);
|
|
4065
|
-
rng.setState(point.value.rngState);
|
|
4066
|
-
|
|
4505
|
+
(branchRngs.get(namespace) ?? rng).setState(point.value.rngState);
|
|
4506
|
+
const selectedClock = namespaceClock(namespace);
|
|
4507
|
+
selectedClock.set(point.value.clock.now);
|
|
4067
4508
|
if (point.value.clock.frozen)
|
|
4068
|
-
|
|
4509
|
+
selectedClock.freeze();
|
|
4069
4510
|
else
|
|
4070
|
-
|
|
4511
|
+
selectedClock.unfreeze();
|
|
4071
4512
|
}
|
|
4072
4513
|
return namespace;
|
|
4073
4514
|
}
|
|
@@ -4125,11 +4566,12 @@ var createRuntime = (options) => {
|
|
|
4125
4566
|
const storage = ensureBranch(namespace, branchName);
|
|
4126
4567
|
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
4127
4568
|
captured.set(storage, point.value.snapshot);
|
|
4128
|
-
|
|
4569
|
+
const selectedClock = namespaceClock(namespace);
|
|
4570
|
+
selectedClock.set(point.value.clock.now);
|
|
4129
4571
|
if (point.value.clock.frozen)
|
|
4130
|
-
|
|
4572
|
+
selectedClock.freeze();
|
|
4131
4573
|
else
|
|
4132
|
-
|
|
4574
|
+
selectedClock.unfreeze();
|
|
4133
4575
|
(branchRngs.get(storage) ?? rng).setState(point.value.rngState);
|
|
4134
4576
|
};
|
|
4135
4577
|
const reset = async (name = DEFAULT_NAMESPACE) => {
|
|
@@ -4137,10 +4579,21 @@ var createRuntime = (options) => {
|
|
|
4137
4579
|
options.webhooks?.clear();
|
|
4138
4580
|
for (const each of instances.values())
|
|
4139
4581
|
await each.reset();
|
|
4582
|
+
for (const each of branchRngs.values())
|
|
4583
|
+
each.reset();
|
|
4140
4584
|
timelines.clear();
|
|
4585
|
+
for (const storage of branchStorage.values()) {
|
|
4586
|
+
instances.delete(storage);
|
|
4587
|
+
branchRngs.delete(storage);
|
|
4588
|
+
namespaceForStorage.delete(storage);
|
|
4589
|
+
}
|
|
4141
4590
|
branchStorage.clear();
|
|
4142
|
-
branchRngs.clear();
|
|
4143
4591
|
captured.clear();
|
|
4592
|
+
faults.clear();
|
|
4593
|
+
clock.reset();
|
|
4594
|
+
rng.reset();
|
|
4595
|
+
for (const each of namespaceClocks.values())
|
|
4596
|
+
each.reset();
|
|
4144
4597
|
return;
|
|
4145
4598
|
}
|
|
4146
4599
|
options.webhooks?.clear(name);
|
|
@@ -4158,11 +4611,18 @@ var createRuntime = (options) => {
|
|
|
4158
4611
|
else
|
|
4159
4612
|
clearNamespace(sqlite, storageNamespace(storage));
|
|
4160
4613
|
branchStorage.delete(mapping);
|
|
4614
|
+
instances.delete(storage);
|
|
4615
|
+
namespaceForStorage.delete(storage);
|
|
4161
4616
|
branchRngs.delete(storage);
|
|
4162
4617
|
captured.delete(storage);
|
|
4163
4618
|
}
|
|
4164
4619
|
timelines.delete(name);
|
|
4165
4620
|
captured.delete(name);
|
|
4621
|
+
faults.restore(name, { rules: [], rngState: createRng(options.seed ?? 0).state() });
|
|
4622
|
+
namespaceClock(name).reset();
|
|
4623
|
+
(branchRngs.get(name) ?? rng).reset();
|
|
4624
|
+
journal.clear(name);
|
|
4625
|
+
metrics.reset(name);
|
|
4166
4626
|
};
|
|
4167
4627
|
const snapshot = (name = DEFAULT_NAMESPACE) => {
|
|
4168
4628
|
return checkpoint(name, "main").value.snapshot;
|
|
@@ -4200,6 +4660,53 @@ var createRuntime = (options) => {
|
|
|
4200
4660
|
rng,
|
|
4201
4661
|
credentials,
|
|
4202
4662
|
webhooks: options.webhooks,
|
|
4663
|
+
isolateNamespaces() {
|
|
4664
|
+
if (publicNamespaces.size > 1)
|
|
4665
|
+
throw new Error("enable namespace isolation before creating named namespaces");
|
|
4666
|
+
isolatedNamespaces = true;
|
|
4667
|
+
faults.isolateNamespaces();
|
|
4668
|
+
},
|
|
4669
|
+
namespaceClock,
|
|
4670
|
+
namespaceOf(request) {
|
|
4671
|
+
const url = new URL(request.url);
|
|
4672
|
+
const prefixed = matchNamespacePath(url.pathname, adminPrefix);
|
|
4673
|
+
const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
|
|
4674
|
+
const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
|
|
4675
|
+
if (isAdminPath(path, adminPrefix))
|
|
4676
|
+
return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
|
|
4677
|
+
if (selected !== void 0)
|
|
4678
|
+
return selected;
|
|
4679
|
+
const credential = options.credential?.(request);
|
|
4680
|
+
return (credential !== void 0 ? credentials.get(credential) : void 0) ?? DEFAULT_NAMESPACE;
|
|
4681
|
+
},
|
|
4682
|
+
fleetSnapshot(namespace) {
|
|
4683
|
+
instance(namespace);
|
|
4684
|
+
const handle = {};
|
|
4685
|
+
fleetPoints.set(handle, {
|
|
4686
|
+
namespace,
|
|
4687
|
+
state: capture(namespace),
|
|
4688
|
+
faults: faults.snapshot(namespace),
|
|
4689
|
+
webhooks: options.webhooks?.snapshot(namespace)
|
|
4690
|
+
});
|
|
4691
|
+
return handle;
|
|
4692
|
+
},
|
|
4693
|
+
fleetRestore(handle, namespace) {
|
|
4694
|
+
const point = typeof handle === "object" && handle !== null ? fleetPoints.get(handle) : void 0;
|
|
4695
|
+
if (!point || point.namespace !== namespace)
|
|
4696
|
+
throw new Error("invalid fleet checkpoint");
|
|
4697
|
+
restoreNamespace(sqlite, storageNamespace(namespace), point.state.snapshot);
|
|
4698
|
+
captured.set(namespace, point.state.snapshot);
|
|
4699
|
+
const selectedClock = namespaceClock(namespace);
|
|
4700
|
+
selectedClock.freeze();
|
|
4701
|
+
selectedClock.set(point.state.clock.now);
|
|
4702
|
+
if (!point.state.clock.frozen)
|
|
4703
|
+
selectedClock.unfreeze();
|
|
4704
|
+
(branchRngs.get(namespace) ?? rng).setState(point.state.rngState);
|
|
4705
|
+
faults.restore(namespace, point.faults);
|
|
4706
|
+
if (point.webhooks !== void 0)
|
|
4707
|
+
options.webhooks?.restore(namespace, point.webhooks);
|
|
4708
|
+
timelines.delete(namespace);
|
|
4709
|
+
},
|
|
4203
4710
|
applyPreset: (name, namespace = DEFAULT_NAMESPACE, overrides = {}) => {
|
|
4204
4711
|
const preset = options.presets?.[name];
|
|
4205
4712
|
if (!preset)
|
|
@@ -4231,7 +4738,7 @@ var createRuntime = (options) => {
|
|
|
4231
4738
|
state: (namespace = DEFAULT_NAMESPACE) => inspectState(stateScope(namespace)),
|
|
4232
4739
|
fetch: async (incoming) => {
|
|
4233
4740
|
let request = incoming;
|
|
4234
|
-
const prefixed =
|
|
4741
|
+
const prefixed = matchNamespacePath(new URL(request.url).pathname, adminPrefix);
|
|
4235
4742
|
if (prefixed) {
|
|
4236
4743
|
const url2 = new URL(request.url);
|
|
4237
4744
|
url2.pathname = prefixed[2] ?? "/";
|
|
@@ -4247,7 +4754,15 @@ var createRuntime = (options) => {
|
|
|
4247
4754
|
signal: request.signal
|
|
4248
4755
|
});
|
|
4249
4756
|
}
|
|
4250
|
-
|
|
4757
|
+
const internalUrl = new URL(request.url);
|
|
4758
|
+
const internalPath = isAdminPath(internalUrl.pathname, adminPrefix) ? `${ADMIN_PREFIX}${internalUrl.pathname.slice(adminPrefix.length)}` : "";
|
|
4759
|
+
const internal = internalMatch(internalPath);
|
|
4760
|
+
const obsoleteInternalPath = adminPrefix !== ADMIN_PREFIX && internalMatch(internalUrl.pathname) && !internal;
|
|
4761
|
+
if (internal) {
|
|
4762
|
+
internalUrl.pathname = internalPath;
|
|
4763
|
+
request = new Request(internalUrl, request);
|
|
4764
|
+
}
|
|
4765
|
+
let namespace = internal ? internalUrl.searchParams.get("namespace") ?? control.namespaceOf(request) : control.namespaceOf(request);
|
|
4251
4766
|
if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
4252
4767
|
const credential = options.credential(request);
|
|
4253
4768
|
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
@@ -4267,7 +4782,9 @@ var createRuntime = (options) => {
|
|
|
4267
4782
|
return copy;
|
|
4268
4783
|
}
|
|
4269
4784
|
};
|
|
4270
|
-
|
|
4785
|
+
if (obsoleteInternalPath)
|
|
4786
|
+
return stamp(adminFail(404, "internal transport is only available under adminPrefix"));
|
|
4787
|
+
const handled = internal ? void 0 : await control.handle(request);
|
|
4271
4788
|
if (handled)
|
|
4272
4789
|
return stamp(handled);
|
|
4273
4790
|
const started = monotonicNow();
|
|
@@ -4308,7 +4825,7 @@ var createRuntime = (options) => {
|
|
|
4308
4825
|
type: "mockingbird_admin",
|
|
4309
4826
|
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
4310
4827
|
}
|
|
4311
|
-
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
4828
|
+
}), { status: 400, headers: { "content-type": "application/json; charset=utf-8" } }));
|
|
4312
4829
|
}
|
|
4313
4830
|
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
4314
4831
|
log(400);
|
|
@@ -4375,6 +4892,7 @@ var createRuntime = (options) => {
|
|
|
4375
4892
|
startedAt: wallNow(),
|
|
4376
4893
|
wallNow,
|
|
4377
4894
|
clock,
|
|
4895
|
+
namespaceClock,
|
|
4378
4896
|
faults,
|
|
4379
4897
|
metrics,
|
|
4380
4898
|
journal,
|
|
@@ -4424,15 +4942,16 @@ var createRuntime = (options) => {
|
|
|
4424
4942
|
...options.admin?.(runtime) ?? {},
|
|
4425
4943
|
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
4426
4944
|
// maps `presets`, and a service route must not replace that list with a record.
|
|
4427
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
4945
|
+
...options.presets ? presetRoutes(options.presets, runtime, adminPrefix) : {},
|
|
4428
4946
|
...stateAdminRoutes({
|
|
4429
4947
|
open: stateScope,
|
|
4430
4948
|
afterWrite: (namespace) => {
|
|
4431
4949
|
checkpoint(namespace, "main");
|
|
4432
4950
|
}
|
|
4433
4951
|
}),
|
|
4434
|
-
...adminUiRoutes(options.name, options.adminUi)
|
|
4952
|
+
...adminUiRoutes(options.name, options.adminUi, adminPrefix)
|
|
4435
4953
|
},
|
|
4954
|
+
adminPrefix,
|
|
4436
4955
|
adminKey: options.adminKey
|
|
4437
4956
|
});
|
|
4438
4957
|
return defineMock(runtime);
|
|
@@ -4446,7 +4965,10 @@ var mutableResponse = (response) => {
|
|
|
4446
4965
|
return new Response(response.body, response);
|
|
4447
4966
|
}
|
|
4448
4967
|
};
|
|
4449
|
-
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
4968
|
+
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
4969
|
+
status,
|
|
4970
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4971
|
+
});
|
|
4450
4972
|
var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
|
|
4451
4973
|
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4452
4974
|
var credentialRoutes = (registry) => ({
|
|
@@ -4498,7 +5020,7 @@ var credentialRoutes = (registry) => ({
|
|
|
4498
5020
|
return adminJson(200, { status: "ok" });
|
|
4499
5021
|
}
|
|
4500
5022
|
});
|
|
4501
|
-
var presetRoutes = (presets, runtime) => ({
|
|
5023
|
+
var presetRoutes = (presets, runtime, adminPrefix) => ({
|
|
4502
5024
|
"GET /faults/presets": () => {
|
|
4503
5025
|
const body = {
|
|
4504
5026
|
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
@@ -4508,7 +5030,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
4508
5030
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
4509
5031
|
const name = params.name;
|
|
4510
5032
|
if (!presets[name])
|
|
4511
|
-
return adminFail(404, `no fault preset ${name}; GET /
|
|
5033
|
+
return adminFail(404, `no fault preset ${name}; GET ${adminPrefix}/faults/presets`);
|
|
4512
5034
|
const overrides = isObject(body) ? body : {};
|
|
4513
5035
|
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
4514
5036
|
}
|
|
@@ -5121,6 +5643,7 @@ var createRuntime2 = (options = {}) => {
|
|
|
5121
5643
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
5122
5644
|
...options.clock ? { clock: options.clock } : {},
|
|
5123
5645
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
5646
|
+
...options.adminPrefix !== void 0 ? { adminPrefix: options.adminPrefix } : {},
|
|
5124
5647
|
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
5125
5648
|
...options.onLog ? { onLog: options.onLog } : {},
|
|
5126
5649
|
credential: (request) => tokens.get(request),
|
|
@@ -5138,7 +5661,7 @@ var createRuntime2 = (options = {}) => {
|
|
|
5138
5661
|
const inner = runtime.fetch;
|
|
5139
5662
|
const fetch2 = async (request) => {
|
|
5140
5663
|
const path = new URL(request.url).pathname;
|
|
5141
|
-
if (!request.headers.has(NAMESPACE_HEADER) && !path.
|
|
5664
|
+
if (!request.headers.has(NAMESPACE_HEADER) && !(path === (options.adminPrefix ?? "/__admin") || path.startsWith(`${options.adminPrefix ?? "/__admin"}/`))) {
|
|
5142
5665
|
const token = await requestToken(request, path) ?? bearerToken(request);
|
|
5143
5666
|
if (token !== void 0) tokens.set(request, token);
|
|
5144
5667
|
}
|
|
@@ -5469,6 +5992,9 @@ var malformedBody = () => ({
|
|
|
5469
5992
|
});
|
|
5470
5993
|
|
|
5471
5994
|
export {
|
|
5995
|
+
createClock,
|
|
5996
|
+
resolveAdminPrefix,
|
|
5997
|
+
parseDuration,
|
|
5472
5998
|
decodePostHogBody,
|
|
5473
5999
|
tokenFromBody,
|
|
5474
6000
|
evaluateFlag,
|
|
@@ -5486,4 +6012,4 @@ export {
|
|
|
5486
6012
|
scrubProperties,
|
|
5487
6013
|
PostHogAPI
|
|
5488
6014
|
};
|
|
5489
|
-
//# sourceMappingURL=chunk-
|
|
6015
|
+
//# sourceMappingURL=chunk-NRFKITRM.js.map
|