@crvouga/mockingbird-service-google-maps 1.8.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 +16 -0
- package/dist/chunk-DY4P32X4.js +850 -0
- package/dist/chunk-DY4P32X4.js.map +7 -0
- package/dist/{chunk-5R4HFNRA.js → chunk-O2FDQBMY.js} +789 -260
- package/dist/chunk-O2FDQBMY.js.map +7 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +37 -5
- package/dist/index.js +1 -1
- package/dist/server.d.ts +66 -4
- package/dist/server.js +2 -2
- package/openapi.yaml +747 -0
- package/package.json +29 -4
- package/dist/chunk-45IZ6HBU.js +0 -363
- package/dist/chunk-45IZ6HBU.js.map +0 -7
- package/dist/chunk-5R4HFNRA.js.map +0 -7
|
@@ -110,13 +110,31 @@ var Collection = class {
|
|
|
110
110
|
};
|
|
111
111
|
|
|
112
112
|
// ../core/dist/control.js
|
|
113
|
-
var HEALTH_PATH = "/health";
|
|
114
113
|
var ADMIN_PREFIX = "/__admin";
|
|
114
|
+
var resolveAdminPrefix = (prefix = ADMIN_PREFIX) => {
|
|
115
|
+
if (!/^\/[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*$/.test(prefix)) {
|
|
116
|
+
throw new Error("adminPrefix must be an absolute non-root path of literal segments, without a trailing slash");
|
|
117
|
+
}
|
|
118
|
+
return prefix;
|
|
119
|
+
};
|
|
120
|
+
var isAdminPath = (path, prefix) => path === prefix || path.startsWith(`${prefix}/`);
|
|
121
|
+
var matchNamespacePath = (path, prefix) => /^\/ns\/([^/]+)(\/.*)?$/.exec(isAdminPath(path, prefix) ? path.slice(prefix.length) : "");
|
|
122
|
+
var assertAdminPrefixAvailable = (prefix, paths) => {
|
|
123
|
+
const reserved = prefix.split("/").slice(1);
|
|
124
|
+
for (const path of paths) {
|
|
125
|
+
const segments = path.split("/").slice(1);
|
|
126
|
+
if (segments.length < reserved.length || /^\{[^}]+\}$/.test(segments[0] ?? ""))
|
|
127
|
+
continue;
|
|
128
|
+
if (reserved.every((part, i) => segments[i] === part || /^\{[^}]+\}$/.test(segments[i] ?? ""))) {
|
|
129
|
+
throw new Error(`vendor route ${path} collides with adminPrefix ${prefix}; choose a different adminPrefix`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
115
133
|
var ADMIN_KEY_HEADER = "x-mockingbird-admin-key";
|
|
116
134
|
var NAMESPACE_HEADER = "x-mockingbird-namespace";
|
|
117
135
|
var json = (status, body) => new Response(JSON.stringify(body), {
|
|
118
136
|
status,
|
|
119
|
-
headers: { "content-type": "application/json" }
|
|
137
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
120
138
|
});
|
|
121
139
|
var adminError = (status, message) => json(status, { error: { type: "mockingbird_admin", message } });
|
|
122
140
|
var UNITS = {
|
|
@@ -168,11 +186,25 @@ var readJson = async (request) => {
|
|
|
168
186
|
};
|
|
169
187
|
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
170
188
|
var createControlPlane = (context) => {
|
|
189
|
+
const adminPrefix = resolveAdminPrefix(context.adminPrefix);
|
|
171
190
|
const snapshots = /* @__PURE__ */ new Map();
|
|
172
191
|
let snapshotCounter = 0;
|
|
173
192
|
const headerNamespace = (request) => request.headers.get(NAMESPACE_HEADER) ?? context.defaultNamespace;
|
|
174
193
|
const adminNamespace = (request, url) => url.searchParams.get("namespace") ?? headerNamespace(request);
|
|
194
|
+
const health = () => {
|
|
195
|
+
return json(200, {
|
|
196
|
+
...context.describe(),
|
|
197
|
+
status: "ok",
|
|
198
|
+
service: context.name,
|
|
199
|
+
uptimeMs: context.wallNow() - context.startedAt,
|
|
200
|
+
clock: context.clock.state(),
|
|
201
|
+
namespaces: context.namespaces().length,
|
|
202
|
+
adminPrefix,
|
|
203
|
+
adminUi: `${adminPrefix}/ui`
|
|
204
|
+
});
|
|
205
|
+
};
|
|
175
206
|
const builtin = {
|
|
207
|
+
"GET /health": health,
|
|
176
208
|
"GET /": () => json(200, {
|
|
177
209
|
service: context.name,
|
|
178
210
|
routes: [...Object.keys(builtin), ...Object.keys(context.routes)].sort()
|
|
@@ -183,29 +215,30 @@ var createControlPlane = (context) => {
|
|
|
183
215
|
return json(200, { status: "ok", reset: target === "*" ? context.namespaces() : [target] });
|
|
184
216
|
},
|
|
185
217
|
"GET /namespaces": () => json(200, { default: context.defaultNamespace, namespaces: context.namespaces() }),
|
|
186
|
-
"GET /clock": () => json(200, context.clock.state()),
|
|
187
|
-
"POST /clock": ({ body }) => {
|
|
218
|
+
"GET /clock": ({ namespace }) => json(200, (context.namespaceClock?.(namespace) ?? context.clock).state()),
|
|
219
|
+
"POST /clock": ({ body, namespace }) => {
|
|
220
|
+
const clock = context.namespaceClock?.(namespace) ?? context.clock;
|
|
188
221
|
if (!isRecord(body))
|
|
189
222
|
return adminError(400, "expected a JSON object");
|
|
190
223
|
if (body.reset === true)
|
|
191
|
-
|
|
224
|
+
clock.reset();
|
|
192
225
|
if (body.freeze === true)
|
|
193
|
-
|
|
226
|
+
clock.freeze();
|
|
194
227
|
if (body.set !== void 0) {
|
|
195
228
|
const instant = parseInstant(body.set);
|
|
196
229
|
if (instant === void 0)
|
|
197
230
|
return adminError(400, "set: expected epoch ms or ISO-8601");
|
|
198
|
-
|
|
231
|
+
clock.set(instant);
|
|
199
232
|
}
|
|
200
233
|
if (body.advance !== void 0) {
|
|
201
234
|
const delta = parseDuration(body.advance);
|
|
202
235
|
if (delta === void 0)
|
|
203
236
|
return adminError(400, 'advance: expected ms or "15m"-style');
|
|
204
|
-
|
|
237
|
+
clock.advance(delta);
|
|
205
238
|
}
|
|
206
239
|
if (body.freeze === false)
|
|
207
|
-
|
|
208
|
-
return json(200,
|
|
240
|
+
clock.unfreeze();
|
|
241
|
+
return json(200, clock.state());
|
|
209
242
|
},
|
|
210
243
|
"GET /faults": () => json(200, { faults: context.faults.list() }),
|
|
211
244
|
"POST /faults": ({ body, namespace }) => {
|
|
@@ -338,7 +371,14 @@ var createControlPlane = (context) => {
|
|
|
338
371
|
return json(200, { status: "ok" });
|
|
339
372
|
}
|
|
340
373
|
};
|
|
341
|
-
const
|
|
374
|
+
for (const key of Object.keys(context.routes)) {
|
|
375
|
+
if (Object.hasOwn(builtin, key))
|
|
376
|
+
throw new Error(`admin route ${key} overrides a shared route`);
|
|
377
|
+
const path = key.slice(key.indexOf(" ") + 1);
|
|
378
|
+
if (isAdminPath(path, "/ns") || isAdminPath(path, "/blobs"))
|
|
379
|
+
throw new Error(`admin route ${key} overlaps an internal transport`);
|
|
380
|
+
}
|
|
381
|
+
const routes = [...Object.entries(builtin), ...Object.entries(context.routes)].map(([key, handler]) => {
|
|
342
382
|
const space = key.indexOf(" ");
|
|
343
383
|
return { method: key.slice(0, space), pattern: key.slice(space + 1), handler };
|
|
344
384
|
});
|
|
@@ -346,21 +386,13 @@ var createControlPlane = (context) => {
|
|
|
346
386
|
namespaceOf: headerNamespace,
|
|
347
387
|
async handle(request) {
|
|
348
388
|
const url = new URL(request.url);
|
|
349
|
-
if (url.pathname ===
|
|
350
|
-
return
|
|
351
|
-
status: "ok",
|
|
352
|
-
service: context.name,
|
|
353
|
-
uptimeMs: context.wallNow() - context.startedAt,
|
|
354
|
-
clock: context.clock.state(),
|
|
355
|
-
namespaces: context.namespaces().length,
|
|
356
|
-
adminUi: `${ADMIN_PREFIX}/ui`,
|
|
357
|
-
...context.describe()
|
|
358
|
-
});
|
|
389
|
+
if (url.pathname === `${adminPrefix}/health` && request.method === "GET") {
|
|
390
|
+
return health();
|
|
359
391
|
}
|
|
360
|
-
if (url.pathname
|
|
392
|
+
if (!isAdminPath(url.pathname, adminPrefix)) {
|
|
361
393
|
return void 0;
|
|
362
394
|
}
|
|
363
|
-
const path = url.pathname.slice(
|
|
395
|
+
const path = url.pathname.slice(adminPrefix.length) || "/";
|
|
364
396
|
const shell = request.method === "GET" && (path === "/ui" || path === "/ui/");
|
|
365
397
|
if (!shell && context.adminKey !== void 0 && request.headers.get(ADMIN_KEY_HEADER) !== context.adminKey) {
|
|
366
398
|
return adminError(401, `missing or wrong ${ADMIN_KEY_HEADER}`);
|
|
@@ -368,7 +400,12 @@ var createControlPlane = (context) => {
|
|
|
368
400
|
for (const route of routes) {
|
|
369
401
|
if (route.method !== request.method)
|
|
370
402
|
continue;
|
|
371
|
-
|
|
403
|
+
let params;
|
|
404
|
+
try {
|
|
405
|
+
params = matchRoute(route.pattern, path);
|
|
406
|
+
} catch {
|
|
407
|
+
return adminError(400, "invalid path encoding");
|
|
408
|
+
}
|
|
372
409
|
if (!params)
|
|
373
410
|
continue;
|
|
374
411
|
let body;
|
|
@@ -377,15 +414,18 @@ var createControlPlane = (context) => {
|
|
|
377
414
|
} catch {
|
|
378
415
|
return adminError(400, "request body is not valid JSON");
|
|
379
416
|
}
|
|
417
|
+
const namespace = adminNamespace(request, url);
|
|
418
|
+
if (!/^[A-Za-z0-9_.-]{1,64}$/.test(namespace))
|
|
419
|
+
return adminError(400, "invalid namespace");
|
|
380
420
|
return route.handler({
|
|
381
421
|
request,
|
|
382
422
|
url,
|
|
383
423
|
params,
|
|
384
|
-
namespace
|
|
424
|
+
namespace,
|
|
385
425
|
body
|
|
386
426
|
});
|
|
387
427
|
}
|
|
388
|
-
return adminError(404, `no admin route ${request.method} ${path}; GET ${
|
|
428
|
+
return adminError(404, `no admin route ${request.method} ${path}; GET ${adminPrefix} lists them`);
|
|
389
429
|
}
|
|
390
430
|
};
|
|
391
431
|
};
|
|
@@ -467,7 +507,33 @@ var faultResponse = (rule) => {
|
|
|
467
507
|
};
|
|
468
508
|
var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms))) => {
|
|
469
509
|
const entries = [];
|
|
510
|
+
let isolated = false;
|
|
511
|
+
const scopedRngs = /* @__PURE__ */ new Map();
|
|
512
|
+
const random = (namespace) => {
|
|
513
|
+
if (!isolated || namespace === "default")
|
|
514
|
+
return rng;
|
|
515
|
+
let scoped = scopedRngs.get(namespace);
|
|
516
|
+
if (!scoped) {
|
|
517
|
+
scoped = createRng(rng.seed);
|
|
518
|
+
scopedRngs.set(namespace, scoped);
|
|
519
|
+
}
|
|
520
|
+
return scoped;
|
|
521
|
+
};
|
|
470
522
|
return {
|
|
523
|
+
isolateNamespaces() {
|
|
524
|
+
isolated = true;
|
|
525
|
+
},
|
|
526
|
+
snapshot(namespace) {
|
|
527
|
+
return {
|
|
528
|
+
rules: structuredClone(entries.map((entry, position) => ({
|
|
529
|
+
...entry.rule,
|
|
530
|
+
remaining: entry.remaining,
|
|
531
|
+
hits: entry.hits,
|
|
532
|
+
position
|
|
533
|
+
})).filter((entry) => entry.namespace === namespace)),
|
|
534
|
+
rngState: random(namespace).state()
|
|
535
|
+
};
|
|
536
|
+
},
|
|
471
537
|
add(rule) {
|
|
472
538
|
const existing = entries.findIndex((e) => e.rule.id === rule.id);
|
|
473
539
|
const entry = { rule, remaining: rule.count ?? null, hits: 0 };
|
|
@@ -487,6 +553,16 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
|
|
|
487
553
|
},
|
|
488
554
|
clear() {
|
|
489
555
|
entries.length = 0;
|
|
556
|
+
rng.reset();
|
|
557
|
+
scopedRngs.clear();
|
|
558
|
+
},
|
|
559
|
+
restore(namespace, state) {
|
|
560
|
+
for (let i = entries.length - 1; i >= 0; i--)
|
|
561
|
+
if (entries[i]?.rule.namespace === namespace)
|
|
562
|
+
entries.splice(i, 1);
|
|
563
|
+
for (const { remaining, hits, position, ...rule } of state.rules)
|
|
564
|
+
entries.splice(Math.min(position, entries.length), 0, { rule, remaining, hits });
|
|
565
|
+
random(namespace).setState(state.rngState);
|
|
490
566
|
},
|
|
491
567
|
async take(candidate) {
|
|
492
568
|
const hits = [];
|
|
@@ -496,7 +572,7 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
|
|
|
496
572
|
if (!matches(entry.rule, candidate))
|
|
497
573
|
continue;
|
|
498
574
|
const rate = entry.rule.rate ?? 1;
|
|
499
|
-
if (
|
|
575
|
+
if (random(candidate.namespace).next() >= rate)
|
|
500
576
|
continue;
|
|
501
577
|
entry.hits++;
|
|
502
578
|
if (entry.remaining !== null)
|
|
@@ -854,7 +930,8 @@ var annotateResponse = (response, extra) => {
|
|
|
854
930
|
var responseNotes = (response) => notes.get(response);
|
|
855
931
|
|
|
856
932
|
// ../core/dist/metrics.js
|
|
857
|
-
var createMetrics = () => {
|
|
933
|
+
var createMetrics = (partition = true) => {
|
|
934
|
+
const namespaces = /* @__PURE__ */ new Map();
|
|
858
935
|
let requests = 0;
|
|
859
936
|
let faults = 0;
|
|
860
937
|
let totalDurationMs = 0;
|
|
@@ -862,6 +939,14 @@ var createMetrics = () => {
|
|
|
862
939
|
const unmatched = /* @__PURE__ */ new Map();
|
|
863
940
|
return {
|
|
864
941
|
record(entry) {
|
|
942
|
+
if (partition) {
|
|
943
|
+
let scoped = namespaces.get(entry.namespace);
|
|
944
|
+
if (!scoped) {
|
|
945
|
+
scoped = createMetrics(false);
|
|
946
|
+
namespaces.set(entry.namespace, scoped);
|
|
947
|
+
}
|
|
948
|
+
scoped.record(entry);
|
|
949
|
+
}
|
|
865
950
|
requests++;
|
|
866
951
|
totalDurationMs += entry.durationMs;
|
|
867
952
|
if (entry.faultId !== void 0)
|
|
@@ -873,7 +958,7 @@ var createMetrics = () => {
|
|
|
873
958
|
unmatched.set(route, (unmatched.get(route) ?? 0) + 1);
|
|
874
959
|
}
|
|
875
960
|
},
|
|
876
|
-
report: () => ({
|
|
961
|
+
report: (namespace) => namespace !== void 0 ? (namespaces.get(namespace) ?? createMetrics(false)).report() : {
|
|
877
962
|
requests,
|
|
878
963
|
byOperation: Object.fromEntries([...byOperation].sort(([a], [b]) => a.localeCompare(b))),
|
|
879
964
|
unmatched: [...unmatched].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([route, count]) => {
|
|
@@ -882,8 +967,13 @@ var createMetrics = () => {
|
|
|
882
967
|
}),
|
|
883
968
|
faults,
|
|
884
969
|
totalDurationMs
|
|
885
|
-
}
|
|
886
|
-
reset() {
|
|
970
|
+
},
|
|
971
|
+
reset(namespace) {
|
|
972
|
+
if (namespace !== void 0) {
|
|
973
|
+
namespaces.delete(namespace);
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
namespaces.clear();
|
|
887
977
|
requests = 0;
|
|
888
978
|
faults = 0;
|
|
889
979
|
totalDurationMs = 0;
|
|
@@ -1496,6 +1586,34 @@ dialog:not([open]) {
|
|
|
1496
1586
|
function isRecord2(value) {
|
|
1497
1587
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1498
1588
|
}
|
|
1589
|
+
function highlightJsonText(text2) {
|
|
1590
|
+
const escapeToken = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
1591
|
+
switch (char) {
|
|
1592
|
+
case "&":
|
|
1593
|
+
return "&";
|
|
1594
|
+
case "<":
|
|
1595
|
+
return "<";
|
|
1596
|
+
case ">":
|
|
1597
|
+
return ">";
|
|
1598
|
+
case '"':
|
|
1599
|
+
return """;
|
|
1600
|
+
default:
|
|
1601
|
+
return "'";
|
|
1602
|
+
}
|
|
1603
|
+
});
|
|
1604
|
+
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;
|
|
1605
|
+
let cursor = 0;
|
|
1606
|
+
let html = "";
|
|
1607
|
+
for (const match of text2.matchAll(token)) {
|
|
1608
|
+
const index = match.index ?? 0;
|
|
1609
|
+
const value = match[0];
|
|
1610
|
+
html += escapeToken(text2.slice(cursor, index));
|
|
1611
|
+
const kind = value.startsWith('"') ? value.trimEnd().endsWith(":") ? "key" : "string" : value === "true" || value === "false" ? "boolean" : value === "null" ? "null" : "number";
|
|
1612
|
+
html += `<span class="json-${kind}">${escapeToken(value)}</span>`;
|
|
1613
|
+
cursor = index + value.length;
|
|
1614
|
+
}
|
|
1615
|
+
return html + escapeToken(text2.slice(cursor));
|
|
1616
|
+
}
|
|
1499
1617
|
function mountAdminBrand(doc, host, service, catalog) {
|
|
1500
1618
|
const clip = (value, max) => {
|
|
1501
1619
|
if (typeof value !== "string")
|
|
@@ -1857,14 +1975,16 @@ function bootAdmin(config) {
|
|
|
1857
1975
|
const state = {
|
|
1858
1976
|
namespace: "default",
|
|
1859
1977
|
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
1860
|
-
view: "overview",
|
|
1978
|
+
view: ["overview", "state", "clock", "faults", "journal", "routes"].includes(location.hash.slice(1)) ? location.hash.slice(1) : "overview",
|
|
1861
1979
|
collection: "",
|
|
1862
1980
|
shape: null,
|
|
1863
1981
|
route: null,
|
|
1982
|
+
routes: [],
|
|
1864
1983
|
editing: null
|
|
1865
1984
|
};
|
|
1866
1985
|
const standard = new Set(config.standardRoutes);
|
|
1867
1986
|
const banner = el("banner", HTMLElement);
|
|
1987
|
+
const bannerMessage = el("banner-message", HTMLElement);
|
|
1868
1988
|
const keyInput = el("admin-key", HTMLInputElement);
|
|
1869
1989
|
keyInput.value = state.key;
|
|
1870
1990
|
const params = new URLSearchParams(location.search);
|
|
@@ -1875,18 +1995,21 @@ function bootAdmin(config) {
|
|
|
1875
1995
|
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
1876
1996
|
params.delete("key");
|
|
1877
1997
|
const next = params.toString();
|
|
1878
|
-
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
1998
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : "") + location.hash);
|
|
1879
1999
|
}
|
|
1880
2000
|
const queryNamespace = params.get("namespace");
|
|
1881
2001
|
if (queryNamespace)
|
|
1882
2002
|
state.namespace = queryNamespace;
|
|
1883
2003
|
const showError = (error) => {
|
|
1884
|
-
|
|
2004
|
+
bannerMessage.textContent = error instanceof Error ? error.message : String(error);
|
|
1885
2005
|
banner.classList.add("show");
|
|
2006
|
+
banner.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
1886
2007
|
};
|
|
1887
2008
|
const clearError = () => {
|
|
1888
2009
|
banner.classList.remove("show");
|
|
2010
|
+
bannerMessage.textContent = "";
|
|
1889
2011
|
};
|
|
2012
|
+
el("banner-dismiss", HTMLButtonElement).addEventListener("click", clearError);
|
|
1890
2013
|
const api = async (method, path, body) => {
|
|
1891
2014
|
const headers = { accept: "application/json" };
|
|
1892
2015
|
if (state.key !== "")
|
|
@@ -1930,16 +2053,21 @@ function bootAdmin(config) {
|
|
|
1930
2053
|
namespace: () => state.namespace
|
|
1931
2054
|
};
|
|
1932
2055
|
const isPanelScript = (value) => typeof value === "function";
|
|
2056
|
+
const themeButton = el("theme", HTMLButtonElement);
|
|
1933
2057
|
const setTheme = (theme) => {
|
|
1934
2058
|
if (theme)
|
|
1935
2059
|
document.documentElement.dataset.theme = theme;
|
|
1936
2060
|
else
|
|
1937
2061
|
delete document.documentElement.dataset.theme;
|
|
2062
|
+
const dark = theme ? theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2063
|
+
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>';
|
|
2064
|
+
const label = dark ? "Use light theme" : "Use dark theme";
|
|
2065
|
+
themeButton.setAttribute("aria-label", label);
|
|
2066
|
+
themeButton.title = label;
|
|
1938
2067
|
};
|
|
1939
2068
|
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
2069
|
+
setTheme(storedTheme === "dark" || storedTheme === "light" ? storedTheme : null);
|
|
2070
|
+
themeButton.addEventListener("click", () => {
|
|
1943
2071
|
const current = document.documentElement.dataset.theme;
|
|
1944
2072
|
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
1945
2073
|
const next = dark ? "light" : "dark";
|
|
@@ -1958,6 +2086,7 @@ function bootAdmin(config) {
|
|
|
1958
2086
|
continue;
|
|
1959
2087
|
section.hidden = section.id !== `view-${view}`;
|
|
1960
2088
|
}
|
|
2089
|
+
history.replaceState(null, "", `${location.pathname}${location.search}#${encodeURIComponent(view)}`);
|
|
1961
2090
|
};
|
|
1962
2091
|
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
1963
2092
|
const target = event.target;
|
|
@@ -1972,7 +2101,7 @@ function bootAdmin(config) {
|
|
|
1972
2101
|
show(view);
|
|
1973
2102
|
refresh().catch(showError);
|
|
1974
2103
|
});
|
|
1975
|
-
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>`;
|
|
2104
|
+
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>`;
|
|
1976
2105
|
const table = (columns, rows) => {
|
|
1977
2106
|
if (rows.length === 0)
|
|
1978
2107
|
return '<p class="empty">Nothing here yet.</p>';
|
|
@@ -1982,16 +2111,22 @@ function bootAdmin(config) {
|
|
|
1982
2111
|
const attrs = "attrs" in row2 && typeof row2.attrs === "string" ? row2.attrs : "";
|
|
1983
2112
|
return `<tr${attrs}>${cells}</tr>`;
|
|
1984
2113
|
}).join("");
|
|
1985
|
-
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
2114
|
+
return `<div class="table-shell"><table><thead>${head}</thead><tbody>${body}</tbody></table></div>`;
|
|
1986
2115
|
};
|
|
1987
2116
|
const preview = (value) => {
|
|
1988
2117
|
const text2 = JSON.stringify(value) ?? "null";
|
|
1989
|
-
|
|
2118
|
+
const clipped = text2.length > 180 ? `${text2.slice(0, 180)}\u2026` : text2;
|
|
2119
|
+
return `<code class="json-inline">${highlightJsonText(clipped)}</code>`;
|
|
1990
2120
|
};
|
|
1991
2121
|
const when = (epoch) => {
|
|
1992
2122
|
const date = new Date(epoch);
|
|
1993
2123
|
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
1994
2124
|
};
|
|
2125
|
+
const journalWhen = (value) => {
|
|
2126
|
+
const numeric = value.trim() === "" ? Number.NaN : Number(value);
|
|
2127
|
+
const epoch = Number.isFinite(numeric) ? numeric : Date.parse(value);
|
|
2128
|
+
return Number.isFinite(epoch) ? when(epoch) : value;
|
|
2129
|
+
};
|
|
1995
2130
|
const clockCard = (epoch) => {
|
|
1996
2131
|
const date = new Date(epoch);
|
|
1997
2132
|
if (Number.isNaN(date.getTime()))
|
|
@@ -2111,7 +2246,7 @@ function bootAdmin(config) {
|
|
|
2111
2246
|
};
|
|
2112
2247
|
const renderOverview = async () => {
|
|
2113
2248
|
const [health, shape, clock, journal] = await Promise.all([
|
|
2114
|
-
originGet(
|
|
2249
|
+
originGet(`${config.adminPrefix}/health`),
|
|
2115
2250
|
api("GET", "/state"),
|
|
2116
2251
|
api("GET", "/clock"),
|
|
2117
2252
|
api("GET", "/requests?limit=8")
|
|
@@ -2128,7 +2263,7 @@ function bootAdmin(config) {
|
|
|
2128
2263
|
},
|
|
2129
2264
|
{
|
|
2130
2265
|
k: "Status",
|
|
2131
|
-
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
2266
|
+
v: `<span class="badge ok">${esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")}</span>`
|
|
2132
2267
|
},
|
|
2133
2268
|
{ k: "Namespace", v: esc(state.namespace) },
|
|
2134
2269
|
{ k: "Collections", v: String(decoded.collections.length) },
|
|
@@ -2215,26 +2350,46 @@ function bootAdmin(config) {
|
|
|
2215
2350
|
const renderJournal = async () => {
|
|
2216
2351
|
const journal = await api("GET", "/requests?limit=100");
|
|
2217
2352
|
el("journal", HTMLElement).innerHTML = table([
|
|
2218
|
-
{ label: "When", cell: (row2) => esc(row2.at) },
|
|
2219
|
-
{
|
|
2353
|
+
{ label: "When", cell: (row2) => esc(journalWhen(row2.at)) },
|
|
2354
|
+
{
|
|
2355
|
+
label: "Method",
|
|
2356
|
+
cell: (row2) => `<span class="method">${esc(row2.method)}</span>`
|
|
2357
|
+
},
|
|
2220
2358
|
{ label: "Path", cell: (row2) => esc(row2.path) },
|
|
2221
|
-
{
|
|
2359
|
+
{
|
|
2360
|
+
label: "Status",
|
|
2361
|
+
cell: (row2) => `<span class="badge ${row2.status.startsWith("2") ? "ok" : row2.status.startsWith("4") || row2.status.startsWith("5") ? "warn" : ""}">${esc(row2.status)}</span>`
|
|
2362
|
+
},
|
|
2222
2363
|
{ label: "ms", cell: (row2) => esc(row2.durationMs) }
|
|
2223
2364
|
], decodeJournal(journal));
|
|
2224
2365
|
};
|
|
2225
|
-
const
|
|
2226
|
-
const
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2366
|
+
const paintRoutes = () => {
|
|
2367
|
+
const query = el("route-search", HTMLInputElement).value.trim().toLocaleLowerCase();
|
|
2368
|
+
const routes = state.routes.filter((route) => route.toLocaleLowerCase().includes(query));
|
|
2369
|
+
el("route-count", HTMLElement).textContent = `${routes.length} of ${state.routes.length}`;
|
|
2370
|
+
if (state.routes.length === 0) {
|
|
2371
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No admin routes are available.</p>';
|
|
2372
|
+
return;
|
|
2373
|
+
}
|
|
2374
|
+
if (routes.length === 0) {
|
|
2375
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No routes match this filter.</p>';
|
|
2376
|
+
return;
|
|
2377
|
+
}
|
|
2230
2378
|
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2231
2379
|
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2232
2380
|
const parts = route.split(" ");
|
|
2233
2381
|
const method = parts[0] ?? "";
|
|
2234
2382
|
const path = parts.slice(1).join(" ");
|
|
2235
|
-
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>`;
|
|
2383
|
+
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>`;
|
|
2236
2384
|
}).join("");
|
|
2237
2385
|
};
|
|
2386
|
+
const renderRoutes = async () => {
|
|
2387
|
+
const data = await api("GET", "/");
|
|
2388
|
+
if (!isRecord2(data))
|
|
2389
|
+
throw new Error("Route list was not an object");
|
|
2390
|
+
state.routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
2391
|
+
paintRoutes();
|
|
2392
|
+
};
|
|
2238
2393
|
const renderers = {
|
|
2239
2394
|
overview: renderOverview,
|
|
2240
2395
|
state: renderState,
|
|
@@ -2243,19 +2398,50 @@ function bootAdmin(config) {
|
|
|
2243
2398
|
journal: renderJournal,
|
|
2244
2399
|
routes: renderRoutes
|
|
2245
2400
|
};
|
|
2401
|
+
let refreshToken = 0;
|
|
2246
2402
|
async function refresh() {
|
|
2403
|
+
const token = ++refreshToken;
|
|
2247
2404
|
clearError();
|
|
2405
|
+
const main = el("views", HTMLElement);
|
|
2406
|
+
const sync = el("sync-status", HTMLElement);
|
|
2407
|
+
main.setAttribute("aria-busy", "true");
|
|
2408
|
+
sync.classList.add("busy");
|
|
2409
|
+
sync.textContent = "Updating";
|
|
2248
2410
|
const render = renderers[state.view];
|
|
2249
|
-
|
|
2250
|
-
|
|
2411
|
+
try {
|
|
2412
|
+
if (render)
|
|
2413
|
+
await render();
|
|
2414
|
+
if (token === refreshToken)
|
|
2415
|
+
sync.textContent = "Up to date";
|
|
2416
|
+
} finally {
|
|
2417
|
+
if (token === refreshToken) {
|
|
2418
|
+
main.removeAttribute("aria-busy");
|
|
2419
|
+
sync.classList.remove("busy");
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2251
2422
|
}
|
|
2252
2423
|
const delay = (ms) => new Promise((resolve2) => {
|
|
2253
2424
|
window.setTimeout(resolve2, ms);
|
|
2254
2425
|
});
|
|
2255
|
-
const confirmAction = async (button, pending, done, work) => {
|
|
2426
|
+
const confirmAction = async (button, pending, done, work, confirmLabel) => {
|
|
2256
2427
|
if (button.dataset.pending === "1")
|
|
2257
2428
|
return;
|
|
2258
2429
|
const label = button.textContent ?? "";
|
|
2430
|
+
if (confirmLabel !== void 0 && button.dataset.confirming !== "1") {
|
|
2431
|
+
button.dataset.confirming = "1";
|
|
2432
|
+
button.classList.add("confirming");
|
|
2433
|
+
button.textContent = confirmLabel;
|
|
2434
|
+
window.setTimeout(() => {
|
|
2435
|
+
if (button.dataset.confirming !== "1")
|
|
2436
|
+
return;
|
|
2437
|
+
button.textContent = label;
|
|
2438
|
+
button.classList.remove("confirming");
|
|
2439
|
+
delete button.dataset.confirming;
|
|
2440
|
+
}, 3e3);
|
|
2441
|
+
return;
|
|
2442
|
+
}
|
|
2443
|
+
button.classList.remove("confirming");
|
|
2444
|
+
delete button.dataset.confirming;
|
|
2259
2445
|
const width = button.offsetWidth;
|
|
2260
2446
|
button.dataset.pending = "1";
|
|
2261
2447
|
button.classList.add("pressed");
|
|
@@ -2276,6 +2462,65 @@ function bootAdmin(config) {
|
|
|
2276
2462
|
delete button.dataset.pending;
|
|
2277
2463
|
}
|
|
2278
2464
|
};
|
|
2465
|
+
const wireJsonEditor = (textareaId, highlightId, statusId, shellId, allowEmpty) => {
|
|
2466
|
+
const textarea = el(textareaId, HTMLTextAreaElement);
|
|
2467
|
+
const highlight = el(highlightId, HTMLElement);
|
|
2468
|
+
const status = el(statusId, HTMLElement);
|
|
2469
|
+
const shell = el(shellId, HTMLElement);
|
|
2470
|
+
const sync = () => {
|
|
2471
|
+
const text2 = textarea.value;
|
|
2472
|
+
highlight.innerHTML = `${highlightJsonText(text2)}
|
|
2473
|
+
`;
|
|
2474
|
+
let valid = true;
|
|
2475
|
+
let message = "Valid JSON";
|
|
2476
|
+
if (allowEmpty && text2.trim() === "")
|
|
2477
|
+
message = "No body";
|
|
2478
|
+
else {
|
|
2479
|
+
try {
|
|
2480
|
+
readJson2(text2);
|
|
2481
|
+
} catch {
|
|
2482
|
+
valid = false;
|
|
2483
|
+
message = "Invalid JSON";
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
shell.classList.toggle("invalid", !valid);
|
|
2487
|
+
status.classList.toggle("invalid", !valid);
|
|
2488
|
+
status.textContent = message;
|
|
2489
|
+
textarea.setAttribute("aria-invalid", valid ? "false" : "true");
|
|
2490
|
+
};
|
|
2491
|
+
const syncScroll = () => {
|
|
2492
|
+
const preview2 = highlight.parentElement;
|
|
2493
|
+
if (!(preview2 instanceof HTMLElement))
|
|
2494
|
+
return;
|
|
2495
|
+
preview2.scrollTop = textarea.scrollTop;
|
|
2496
|
+
preview2.scrollLeft = textarea.scrollLeft;
|
|
2497
|
+
};
|
|
2498
|
+
textarea.addEventListener("input", sync);
|
|
2499
|
+
textarea.addEventListener("scroll", syncScroll);
|
|
2500
|
+
sync();
|
|
2501
|
+
return sync;
|
|
2502
|
+
};
|
|
2503
|
+
const syncRouteEditor = wireJsonEditor("route-body", "route-highlight", "route-json-status", "route-editor", true);
|
|
2504
|
+
const syncRecordEditor = wireJsonEditor("editor-value", "editor-highlight", "editor-json-status", "record-editor", false);
|
|
2505
|
+
for (const button of document.querySelectorAll("[data-format]")) {
|
|
2506
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2507
|
+
continue;
|
|
2508
|
+
button.addEventListener("click", () => {
|
|
2509
|
+
const target = button.dataset.format;
|
|
2510
|
+
if (target === void 0)
|
|
2511
|
+
return;
|
|
2512
|
+
const textarea = document.getElementById(target);
|
|
2513
|
+
if (!(textarea instanceof HTMLTextAreaElement) || textarea.value.trim() === "")
|
|
2514
|
+
return;
|
|
2515
|
+
try {
|
|
2516
|
+
textarea.value = JSON.stringify(readJson2(textarea.value), null, 2) ?? "";
|
|
2517
|
+
textarea.dispatchEvent(new Event("input"));
|
|
2518
|
+
textarea.focus();
|
|
2519
|
+
} catch {
|
|
2520
|
+
showError(new Error("Fix the JSON before formatting it"));
|
|
2521
|
+
}
|
|
2522
|
+
});
|
|
2523
|
+
}
|
|
2279
2524
|
document.addEventListener("click", (event) => {
|
|
2280
2525
|
const target = event.target;
|
|
2281
2526
|
if (!(target instanceof Element))
|
|
@@ -2348,6 +2593,7 @@ function bootAdmin(config) {
|
|
|
2348
2593
|
} else {
|
|
2349
2594
|
editorValue.value = "{\n \n}";
|
|
2350
2595
|
}
|
|
2596
|
+
syncRecordEditor();
|
|
2351
2597
|
editor.showModal();
|
|
2352
2598
|
}
|
|
2353
2599
|
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -2377,10 +2623,10 @@ function bootAdmin(config) {
|
|
|
2377
2623
|
const id = state.editing;
|
|
2378
2624
|
if (!id)
|
|
2379
2625
|
return;
|
|
2380
|
-
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2626
|
+
confirmAction(editorDelete, "Deleting...", "Deleted", () => api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2381
2627
|
editor.close();
|
|
2382
2628
|
return renderState();
|
|
2383
|
-
}).catch(showError);
|
|
2629
|
+
}), "Confirm delete").catch(showError);
|
|
2384
2630
|
});
|
|
2385
2631
|
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2386
2632
|
if (!(button instanceof HTMLButtonElement))
|
|
@@ -2426,10 +2672,10 @@ function bootAdmin(config) {
|
|
|
2426
2672
|
}).then(() => renderFaults()).catch(showError);
|
|
2427
2673
|
});
|
|
2428
2674
|
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2429
|
-
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
|
|
2675
|
+
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults()), "Confirm clear").catch(showError);
|
|
2430
2676
|
});
|
|
2431
2677
|
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2432
|
-
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
|
|
2678
|
+
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal()), "Confirm clear").catch(showError);
|
|
2433
2679
|
});
|
|
2434
2680
|
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2435
2681
|
const target = event.target;
|
|
@@ -2443,9 +2689,15 @@ function bootAdmin(config) {
|
|
|
2443
2689
|
return;
|
|
2444
2690
|
state.route = route;
|
|
2445
2691
|
el("route-title", HTMLElement).textContent = route;
|
|
2692
|
+
el("route-send", HTMLButtonElement).disabled = false;
|
|
2446
2693
|
const method = route.split(" ")[0] ?? "";
|
|
2447
|
-
el("route-body", HTMLTextAreaElement)
|
|
2694
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
2695
|
+
routeBody.disabled = method === "GET" || method === "DELETE";
|
|
2696
|
+
el("route-editor", HTMLElement).hidden = routeBody.disabled;
|
|
2697
|
+
el("route-json-status", HTMLElement).parentElement?.toggleAttribute("hidden", routeBody.disabled);
|
|
2698
|
+
syncRouteEditor();
|
|
2448
2699
|
});
|
|
2700
|
+
el("route-search", HTMLInputElement).addEventListener("input", paintRoutes);
|
|
2449
2701
|
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2450
2702
|
event.preventDefault();
|
|
2451
2703
|
const send = async () => {
|
|
@@ -2467,7 +2719,8 @@ function bootAdmin(config) {
|
|
|
2467
2719
|
}
|
|
2468
2720
|
}
|
|
2469
2721
|
const data = await api(method, concrete, body);
|
|
2470
|
-
|
|
2722
|
+
const text2 = JSON.stringify(data, null, 2) ?? "null";
|
|
2723
|
+
el("route-result", HTMLElement).innerHTML = `<code>${highlightJsonText(text2)}</code>`;
|
|
2471
2724
|
};
|
|
2472
2725
|
send().catch(showError);
|
|
2473
2726
|
});
|
|
@@ -2585,11 +2838,13 @@ function bootAdmin(config) {
|
|
|
2585
2838
|
views.append(section);
|
|
2586
2839
|
mountSqlExplorer(body, panelApi);
|
|
2587
2840
|
}
|
|
2841
|
+
show(document.getElementById(`view-${state.view}`) === null ? "overview" : state.view);
|
|
2588
2842
|
return refresh();
|
|
2589
2843
|
}).catch(showError);
|
|
2590
2844
|
}
|
|
2591
2845
|
var clientPrelude = [
|
|
2592
2846
|
isRecord2,
|
|
2847
|
+
highlightJsonText,
|
|
2593
2848
|
mountAdminBrand,
|
|
2594
2849
|
startAdminBrand,
|
|
2595
2850
|
faultPresetList,
|
|
@@ -2612,6 +2867,7 @@ ${entry}(${payload});
|
|
|
2612
2867
|
var defineMock = (runtime) => runtime;
|
|
2613
2868
|
var STANDARD_ADMIN_ROUTES = [
|
|
2614
2869
|
"GET /",
|
|
2870
|
+
"GET /health",
|
|
2615
2871
|
"POST /reset",
|
|
2616
2872
|
"GET /namespaces",
|
|
2617
2873
|
"GET /clock",
|
|
@@ -2691,11 +2947,11 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
2691
2947
|
}
|
|
2692
2948
|
});
|
|
2693
2949
|
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
2694
|
-
var renderAdminDocument = (service) => {
|
|
2950
|
+
var renderAdminDocument = (service, adminPrefix = ADMIN_PREFIX) => {
|
|
2695
2951
|
const name = escapeHtml(service);
|
|
2696
2952
|
const script = adminClientSource({
|
|
2697
2953
|
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
2698
|
-
adminPrefix
|
|
2954
|
+
adminPrefix,
|
|
2699
2955
|
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
2700
2956
|
brandsUrl: ADMIN_BRANDS_URL,
|
|
2701
2957
|
service
|
|
@@ -2712,75 +2968,99 @@ var renderAdminDocument = (service) => {
|
|
|
2712
2968
|
${CSS_RESET}
|
|
2713
2969
|
:root {
|
|
2714
2970
|
color-scheme: light;
|
|
2715
|
-
--bg: #
|
|
2971
|
+
--bg: #f7f8fb;
|
|
2716
2972
|
--bg-elev: #ffffff;
|
|
2717
|
-
--bg-muted: #
|
|
2718
|
-
--bg-inset: #
|
|
2719
|
-
--line: #
|
|
2720
|
-
--line-strong: #
|
|
2721
|
-
--ink: #
|
|
2722
|
-
--muted: #
|
|
2723
|
-
--faint: #
|
|
2724
|
-
--accent: #
|
|
2973
|
+
--bg-muted: #f1f3f7;
|
|
2974
|
+
--bg-inset: #f8f9fc;
|
|
2975
|
+
--line: #e4e7ec;
|
|
2976
|
+
--line-strong: #cfd4dc;
|
|
2977
|
+
--ink: #101828;
|
|
2978
|
+
--muted: #475467;
|
|
2979
|
+
--faint: #667085;
|
|
2980
|
+
--accent: #6941c6;
|
|
2981
|
+
--accent-hover: #5933b4;
|
|
2725
2982
|
--accent-ink: #ffffff;
|
|
2726
|
-
--accent-soft: #
|
|
2727
|
-
--ok: #
|
|
2728
|
-
--ok-soft: #
|
|
2729
|
-
--warn: #
|
|
2730
|
-
--warn-soft: #
|
|
2731
|
-
--err: #
|
|
2732
|
-
--err-soft: #
|
|
2733
|
-
--
|
|
2983
|
+
--accent-soft: #f4f0ff;
|
|
2984
|
+
--ok: #067647;
|
|
2985
|
+
--ok-soft: #ecfdf3;
|
|
2986
|
+
--warn: #b54708;
|
|
2987
|
+
--warn-soft: #fffaeb;
|
|
2988
|
+
--err: #b42318;
|
|
2989
|
+
--err-soft: #fef3f2;
|
|
2990
|
+
--code-key: #6941c6;
|
|
2991
|
+
--code-string: #067647;
|
|
2992
|
+
--code-number: #175cd3;
|
|
2993
|
+
--code-boolean: #c11574;
|
|
2994
|
+
--code-null: #667085;
|
|
2995
|
+
--shadow-xs: 0 1px 2px rgb(16 24 40 / 0.05);
|
|
2996
|
+
--shadow-sm: 0 1px 3px rgb(16 24 40 / 0.08), 0 1px 2px rgb(16 24 40 / 0.04);
|
|
2997
|
+
--shadow-lg: 0 18px 48px rgb(16 24 40 / 0.16), 0 4px 12px rgb(16 24 40 / 0.08);
|
|
2734
2998
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2735
2999
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
2736
|
-
--radius:
|
|
2737
|
-
--header:
|
|
2738
|
-
--nav:
|
|
3000
|
+
--radius: 12px;
|
|
3001
|
+
--header: 72px;
|
|
3002
|
+
--nav: 224px;
|
|
2739
3003
|
}
|
|
2740
3004
|
:root[data-theme="dark"] {
|
|
2741
3005
|
color-scheme: dark;
|
|
2742
|
-
--bg: #
|
|
2743
|
-
--bg-elev: #
|
|
2744
|
-
--bg-muted: #
|
|
2745
|
-
--bg-inset: #
|
|
2746
|
-
--line: #
|
|
2747
|
-
--line-strong: #
|
|
2748
|
-
--ink: #
|
|
2749
|
-
--muted: #
|
|
2750
|
-
--faint: #
|
|
2751
|
-
--accent: #
|
|
2752
|
-
--accent-
|
|
2753
|
-
--accent-
|
|
2754
|
-
--
|
|
2755
|
-
--ok
|
|
2756
|
-
--
|
|
2757
|
-
--warn
|
|
2758
|
-
--
|
|
2759
|
-
--err
|
|
2760
|
-
--
|
|
3006
|
+
--bg: #0c0e14;
|
|
3007
|
+
--bg-elev: #151821;
|
|
3008
|
+
--bg-muted: #20232e;
|
|
3009
|
+
--bg-inset: #11141c;
|
|
3010
|
+
--line: #2b303d;
|
|
3011
|
+
--line-strong: #414858;
|
|
3012
|
+
--ink: #f5f7fa;
|
|
3013
|
+
--muted: #b6bdc9;
|
|
3014
|
+
--faint: #8d96a7;
|
|
3015
|
+
--accent: #9e77ed;
|
|
3016
|
+
--accent-hover: #b692f6;
|
|
3017
|
+
--accent-ink: #ffffff;
|
|
3018
|
+
--accent-soft: #2b2142;
|
|
3019
|
+
--ok: #75e0a7;
|
|
3020
|
+
--ok-soft: #102a20;
|
|
3021
|
+
--warn: #fec84b;
|
|
3022
|
+
--warn-soft: #302611;
|
|
3023
|
+
--err: #fda29b;
|
|
3024
|
+
--err-soft: #321817;
|
|
3025
|
+
--code-key: #c3a6ff;
|
|
3026
|
+
--code-string: #75e0a7;
|
|
3027
|
+
--code-number: #84adff;
|
|
3028
|
+
--code-boolean: #f9a8d4;
|
|
3029
|
+
--code-null: #98a2b3;
|
|
3030
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3031
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3032
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2761
3033
|
}
|
|
2762
3034
|
@media (prefers-color-scheme: dark) {
|
|
2763
3035
|
:root:not([data-theme="light"]) {
|
|
2764
3036
|
color-scheme: dark;
|
|
2765
|
-
--bg: #
|
|
2766
|
-
--bg-elev: #
|
|
2767
|
-
--bg-muted: #
|
|
2768
|
-
--bg-inset: #
|
|
2769
|
-
--line: #
|
|
2770
|
-
--line-strong: #
|
|
2771
|
-
--ink: #
|
|
2772
|
-
--muted: #
|
|
2773
|
-
--faint: #
|
|
2774
|
-
--accent: #
|
|
2775
|
-
--accent-
|
|
2776
|
-
--accent-
|
|
2777
|
-
--
|
|
2778
|
-
--ok
|
|
2779
|
-
--
|
|
2780
|
-
--warn
|
|
2781
|
-
--
|
|
2782
|
-
--err
|
|
2783
|
-
--
|
|
3037
|
+
--bg: #0c0e14;
|
|
3038
|
+
--bg-elev: #151821;
|
|
3039
|
+
--bg-muted: #20232e;
|
|
3040
|
+
--bg-inset: #11141c;
|
|
3041
|
+
--line: #2b303d;
|
|
3042
|
+
--line-strong: #414858;
|
|
3043
|
+
--ink: #f5f7fa;
|
|
3044
|
+
--muted: #b6bdc9;
|
|
3045
|
+
--faint: #8d96a7;
|
|
3046
|
+
--accent: #9e77ed;
|
|
3047
|
+
--accent-hover: #b692f6;
|
|
3048
|
+
--accent-ink: #ffffff;
|
|
3049
|
+
--accent-soft: #2b2142;
|
|
3050
|
+
--ok: #75e0a7;
|
|
3051
|
+
--ok-soft: #102a20;
|
|
3052
|
+
--warn: #fec84b;
|
|
3053
|
+
--warn-soft: #302611;
|
|
3054
|
+
--err: #fda29b;
|
|
3055
|
+
--err-soft: #321817;
|
|
3056
|
+
--code-key: #c3a6ff;
|
|
3057
|
+
--code-string: #75e0a7;
|
|
3058
|
+
--code-number: #84adff;
|
|
3059
|
+
--code-boolean: #f9a8d4;
|
|
3060
|
+
--code-null: #98a2b3;
|
|
3061
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3062
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3063
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2784
3064
|
}
|
|
2785
3065
|
}
|
|
2786
3066
|
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
@@ -2798,13 +3078,17 @@ var renderAdminDocument = (service) => {
|
|
|
2798
3078
|
padding-right: env(safe-area-inset-right);
|
|
2799
3079
|
-webkit-text-size-adjust: 100%;
|
|
2800
3080
|
}
|
|
2801
|
-
|
|
3081
|
+
::selection { background: color-mix(in srgb, var(--accent) 28%, transparent); }
|
|
3082
|
+
:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px; }
|
|
3083
|
+
button, input, select, textarea { transition: border-color 140ms ease, box-shadow 140ms ease, transform 100ms ease; }
|
|
3084
|
+
button:disabled, input:disabled, select:disabled, textarea:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
2802
3085
|
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
2803
3086
|
header.top {
|
|
2804
|
-
position: sticky; top: 0; z-index:
|
|
3087
|
+
position: sticky; top: 0; z-index: 20;
|
|
2805
3088
|
display: flex; align-items: center; gap: 12px;
|
|
2806
|
-
min-height: var(--header); padding:
|
|
2807
|
-
background: var(--bg-elev);
|
|
3089
|
+
min-height: var(--header); padding: 12px 20px;
|
|
3090
|
+
background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
|
|
3091
|
+
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
2808
3092
|
border-bottom: 1px solid var(--line);
|
|
2809
3093
|
min-width: 0; max-width: 100%;
|
|
2810
3094
|
}
|
|
@@ -2812,12 +3096,21 @@ var renderAdminDocument = (service) => {
|
|
|
2812
3096
|
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
2813
3097
|
min-width: 0; max-width: 100%; margin-right: auto;
|
|
2814
3098
|
}
|
|
2815
|
-
.brand-
|
|
2816
|
-
.brand-
|
|
3099
|
+
.brand-lockup { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
3100
|
+
.brand-mark {
|
|
3101
|
+
width: 38px; height: 38px; display: grid; place-items: center; flex: none;
|
|
3102
|
+
border-radius: 11px; color: #fff;
|
|
3103
|
+
background: linear-gradient(145deg, #7f56d9 0%, #53389e 100%);
|
|
3104
|
+
box-shadow: 0 6px 16px rgb(105 65 198 / 0.24), inset 0 1px 0 rgb(255 255 255 / 0.25);
|
|
3105
|
+
}
|
|
3106
|
+
.brand-mark svg { width: 22px; height: 22px; }
|
|
3107
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; line-height: 1.25; }
|
|
3108
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
2817
3109
|
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
2818
3110
|
.vendor {
|
|
2819
3111
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
2820
3112
|
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
3113
|
+
padding: 5px 8px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-inset);
|
|
2821
3114
|
}
|
|
2822
3115
|
.vendor[hidden] { display: none; }
|
|
2823
3116
|
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
@@ -2829,12 +3122,17 @@ var renderAdminDocument = (service) => {
|
|
|
2829
3122
|
.vendor-sep { color: var(--faint); }
|
|
2830
3123
|
.vendor-link { color: inherit; text-decoration: none; }
|
|
2831
3124
|
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
2832
|
-
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items:
|
|
2833
|
-
|
|
3125
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
|
|
3126
|
+
.sync-status { align-self: center; display: flex; align-items: center; gap: 6px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3127
|
+
.sync-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
|
|
3128
|
+
.sync-status.busy::before { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); animation: pulse 1.2s ease-in-out infinite; }
|
|
3129
|
+
label.field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; font-weight: 600; color: var(--faint); }
|
|
2834
3130
|
label.field input, label.field select, .controls input[type="password"] {
|
|
2835
|
-
background-color: var(--bg-
|
|
2836
|
-
padding:
|
|
3131
|
+
background-color: var(--bg-elev); border: 1px solid var(--line-strong); border-radius: 9px;
|
|
3132
|
+
padding: 8px 10px; min-height: 38px; line-height: 1.2; box-shadow: var(--shadow-xs);
|
|
2837
3133
|
}
|
|
3134
|
+
label.field input:hover, label.field select:hover, textarea:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--line-strong)); }
|
|
3135
|
+
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; }
|
|
2838
3136
|
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
2839
3137
|
the text fields in the same form. Paint it like those fields. */
|
|
2840
3138
|
label.field select {
|
|
@@ -2851,12 +3149,20 @@ var renderAdminDocument = (service) => {
|
|
|
2851
3149
|
dialog .field > textarea { width: 100%; }
|
|
2852
3150
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
2853
3151
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
.btn
|
|
3152
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
|
3153
|
+
border-radius: 9px; min-height: 38px; padding: 0 13px; border: 1px solid var(--line-strong);
|
|
3154
|
+
background: var(--bg-elev); color: var(--ink); font-size: 13px; font-weight: 600; line-height: 1;
|
|
3155
|
+
box-shadow: var(--shadow-xs);
|
|
3156
|
+
}
|
|
3157
|
+
.icon-btn { width: 38px; padding: 0; }
|
|
3158
|
+
.icon-btn svg, .btn svg, .btn-primary svg { width: 16px; height: 16px; }
|
|
3159
|
+
.icon-btn:hover, .btn:hover { background: var(--bg-muted); border-color: var(--line-strong); }
|
|
3160
|
+
.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; }
|
|
3161
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
3162
|
+
.btn-quiet { background: transparent; border-color: transparent; box-shadow: none; }
|
|
3163
|
+
.btn-quiet:hover { background: var(--bg-muted); }
|
|
3164
|
+
.btn-danger { color: var(--err); border-color: color-mix(in srgb, var(--err) 25%, var(--line)); }
|
|
3165
|
+
.btn-danger:hover, .btn-danger.confirming { background: var(--err-soft); border-color: color-mix(in srgb, var(--err) 45%, var(--line)); }
|
|
2860
3166
|
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
2861
3167
|
so the press itself has to stay visible. */
|
|
2862
3168
|
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
@@ -2872,93 +3178,159 @@ var renderAdminDocument = (service) => {
|
|
|
2872
3178
|
flex: 1 0 auto; min-width: 0;
|
|
2873
3179
|
}
|
|
2874
3180
|
nav.side {
|
|
2875
|
-
|
|
3181
|
+
position: sticky; top: var(--header); align-self: start; height: calc(100vh - var(--header));
|
|
3182
|
+
border-right: 1px solid var(--line); padding: 18px 12px;
|
|
2876
3183
|
display: flex; flex-direction: column; gap: 4px;
|
|
2877
|
-
background: var(--bg-elev);
|
|
3184
|
+
background: var(--bg-elev); overflow-y: auto;
|
|
2878
3185
|
}
|
|
3186
|
+
.nav-label { padding: 0 10px 8px; color: var(--faint); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
|
|
2879
3187
|
nav.side button {
|
|
3188
|
+
position: relative; display: flex; align-items: center; gap: 10px; width: 100%;
|
|
2880
3189
|
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
2881
|
-
padding: 9px 10px; min-height:
|
|
2882
|
-
}
|
|
2883
|
-
nav.side button
|
|
2884
|
-
|
|
2885
|
-
.
|
|
2886
|
-
.
|
|
2887
|
-
.
|
|
3190
|
+
padding: 9px 10px; min-height: 42px; color: var(--muted); font-size: 13px; font-weight: 550;
|
|
3191
|
+
}
|
|
3192
|
+
nav.side button:hover { background: var(--bg-muted); color: var(--ink); }
|
|
3193
|
+
nav.side button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 650; }
|
|
3194
|
+
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); }
|
|
3195
|
+
.nav-icon { width: 18px; height: 18px; display: grid; place-items: center; flex: none; }
|
|
3196
|
+
.nav-icon svg { width: 18px; height: 18px; }
|
|
3197
|
+
main { padding: 32px clamp(20px, 4vw, 48px) 64px; max-width: 1320px; width: 100%; min-width: 0; }
|
|
3198
|
+
main[aria-busy="true"] .view { opacity: 0.62; }
|
|
3199
|
+
.view { animation: view-in 180ms ease-out; }
|
|
3200
|
+
.view[hidden] { display: none; }
|
|
3201
|
+
.view-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
|
|
3202
|
+
.view h2 { margin: 0 0 5px; font-size: clamp(24px, 3vw, 30px); line-height: 1.2; letter-spacing: -0.035em; }
|
|
3203
|
+
.view h3 { margin: 24px 0 10px; font-size: 15px; letter-spacing: -0.01em; }
|
|
3204
|
+
.lede { margin: 0; color: var(--muted); max-width: 72ch; font-size: 14px; }
|
|
3205
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 175px), 1fr)); gap: 14px; }
|
|
2888
3206
|
.card {
|
|
2889
3207
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
2890
|
-
padding:
|
|
3208
|
+
padding: 18px; box-shadow: var(--shadow-sm);
|
|
2891
3209
|
}
|
|
2892
|
-
.card
|
|
2893
|
-
.card
|
|
3210
|
+
.stat-card { position: relative; overflow: hidden; }
|
|
3211
|
+
.stat-card::after { content: ""; position: absolute; inset: 0 0 auto; height: 2px; background: linear-gradient(90deg, var(--accent), transparent 78%); opacity: 0.7; }
|
|
3212
|
+
.card .k { color: var(--faint); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
3213
|
+
.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; }
|
|
2894
3214
|
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
2895
|
-
.
|
|
3215
|
+
.section-card { background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-xs); overflow: hidden; }
|
|
3216
|
+
.section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
|
|
3217
|
+
.section-title h3 { margin: 0; }
|
|
3218
|
+
.split { display: grid; grid-template-columns: minmax(0, 250px) minmax(0, 1fr); gap: 18px; align-items: start; }
|
|
3219
|
+
#collections { padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); box-shadow: var(--shadow-xs); }
|
|
2896
3220
|
.collection {
|
|
2897
3221
|
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
2898
|
-
border-radius:
|
|
3222
|
+
border-radius: 9px; padding: 10px 11px; min-height: 48px; color: var(--ink); cursor: pointer;
|
|
2899
3223
|
}
|
|
2900
|
-
.collection
|
|
3224
|
+
.collection:hover { background: var(--bg-muted); }
|
|
3225
|
+
.collection[aria-current="true"] { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 18%, var(--line)); color: var(--accent); }
|
|
2901
3226
|
.collection small { display: block; color: var(--faint); }
|
|
2902
3227
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
2903
|
-
.chip {
|
|
2904
|
-
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
2905
|
-
border-radius: 999px; padding: 4px 8px;
|
|
2906
|
-
}
|
|
3228
|
+
.chip { font-family: var(--mono); font-size: 11px; background: var(--bg-muted); border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; }
|
|
2907
3229
|
.chip i { color: var(--faint); font-style: normal; }
|
|
2908
3230
|
.sql-layout {
|
|
2909
3231
|
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
2910
3232
|
}
|
|
2911
|
-
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
2912
|
-
.sql-tables button { text-align: left; }
|
|
3233
|
+
.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); }
|
|
3234
|
+
.sql-tables button { text-align: left; border: 0; border-radius: 8px; padding: 9px 10px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
3235
|
+
.sql-tables button:hover, .sql-tables button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); }
|
|
2913
3236
|
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
2914
|
-
table { width: 100%; border-
|
|
2915
|
-
|
|
2916
|
-
th {
|
|
3237
|
+
.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); }
|
|
3238
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
3239
|
+
th, td { text-align: left; padding: 11px 13px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
3240
|
+
th { color: var(--faint); background: var(--bg-inset); font-weight: 650; font-size: 11px; text-transform: uppercase; letter-spacing: 0.045em; }
|
|
3241
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
2917
3242
|
tr.clickable { cursor: pointer; }
|
|
2918
|
-
tr.clickable:hover { background: var(--
|
|
3243
|
+
tr.clickable:hover { background: var(--accent-soft); }
|
|
2919
3244
|
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
2920
3245
|
pre {
|
|
2921
3246
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
2922
|
-
background: var(--bg-inset); border-radius: 10px; padding:
|
|
2923
|
-
}
|
|
2924
|
-
.
|
|
3247
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 10px; padding: 14px; font-size: 12px; line-height: 1.65;
|
|
3248
|
+
}
|
|
3249
|
+
.json-key { color: var(--code-key); }
|
|
3250
|
+
.json-string { color: var(--code-string); }
|
|
3251
|
+
.json-number { color: var(--code-number); }
|
|
3252
|
+
.json-boolean { color: var(--code-boolean); }
|
|
3253
|
+
.json-null { color: var(--code-null); font-style: italic; }
|
|
3254
|
+
.json-inline { display: block; max-width: 44rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
3255
|
+
.json-view { position: relative; max-height: 26rem; overflow: auto; }
|
|
3256
|
+
.json-editor { position: relative; min-height: 190px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--bg-inset); overflow: hidden; }
|
|
3257
|
+
.json-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); }
|
|
3258
|
+
.json-editor.invalid { border-color: var(--err); }
|
|
3259
|
+
.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; }
|
|
3260
|
+
.json-editor pre { position: absolute; inset: 0; pointer-events: none; background: transparent; }
|
|
3261
|
+
.json-editor textarea { position: relative; z-index: 1; resize: vertical; background: transparent; color: transparent; caret-color: var(--ink); -webkit-text-fill-color: transparent; }
|
|
3262
|
+
.json-editor textarea::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); }
|
|
3263
|
+
.editor-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; }
|
|
3264
|
+
.json-status { display: inline-flex; align-items: center; gap: 6px; color: var(--ok); font-size: 11px; }
|
|
3265
|
+
.json-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
3266
|
+
.json-status.invalid { color: var(--err); }
|
|
3267
|
+
.format-json { border: 0; padding: 3px 5px; background: transparent; color: var(--accent); font-size: 11px; font-weight: 650; cursor: pointer; }
|
|
3268
|
+
.row-actions { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 14px 0; }
|
|
2925
3269
|
.banner {
|
|
2926
|
-
display: none;
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
3270
|
+
display: none; position: sticky; top: calc(var(--header) + 12px); z-index: 15;
|
|
3271
|
+
align-items: center; gap: 10px; margin: 0 0 16px; padding: 11px 12px 11px 14px;
|
|
3272
|
+
border: 1px solid color-mix(in srgb, var(--err) 30%, var(--line)); border-radius: 10px;
|
|
3273
|
+
background: var(--err-soft); color: var(--err); box-shadow: var(--shadow-sm); font-size: 13px;
|
|
3274
|
+
}
|
|
3275
|
+
.banner.show { display: flex; }
|
|
3276
|
+
.banner span { flex: 1; }
|
|
3277
|
+
.banner button { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 7px; background: transparent; color: currentColor; cursor: pointer; }
|
|
3278
|
+
.banner button:hover { background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
3279
|
+
.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); }
|
|
2931
3280
|
dialog {
|
|
2932
|
-
border: 1px solid var(--line); border-radius:
|
|
2933
|
-
color: var(--ink); width: min(
|
|
2934
|
-
}
|
|
2935
|
-
dialog
|
|
2936
|
-
dialog
|
|
3281
|
+
border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--bg-elev);
|
|
3282
|
+
color: var(--ink); width: min(720px, calc(100% - 24px)); max-width: calc(100vw - 24px); box-shadow: var(--shadow-lg);
|
|
3283
|
+
}
|
|
3284
|
+
dialog[open] { animation: dialog-in 160ms ease-out; }
|
|
3285
|
+
dialog::backdrop { background: rgb(12 14 20 / 0.58); backdrop-filter: blur(3px); }
|
|
3286
|
+
dialog form, .dialog-body { padding: 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
3287
|
+
.dialog-heading { display: flex; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
|
3288
|
+
.dialog-heading strong { font-size: 18px; letter-spacing: -0.02em; }
|
|
3289
|
+
.dialog-heading span { color: var(--muted); font-size: 12px; }
|
|
2937
3290
|
textarea {
|
|
2938
3291
|
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
2939
3292
|
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
2940
3293
|
font-family: var(--mono); font-size: 12px;
|
|
2941
3294
|
}
|
|
2942
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
2943
|
-
.
|
|
3295
|
+
.dialog-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding-top: 4px; }
|
|
3296
|
+
.dialog-actions .btn-danger { margin-right: auto; }
|
|
3297
|
+
.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); }
|
|
2944
3298
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
2945
3299
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
2946
3300
|
.status-2 { color: var(--ok); }
|
|
2947
3301
|
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
2948
3302
|
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
3303
|
+
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
3304
|
+
.form-grid .wide { grid-column: span 2; }
|
|
2949
3305
|
.route {
|
|
2950
|
-
display: grid; grid-template-columns:
|
|
2951
|
-
padding:
|
|
2952
|
-
}
|
|
2953
|
-
.
|
|
3306
|
+
display: grid; grid-template-columns: 84px minmax(0, 1fr) auto; gap: 10px; align-items: center;
|
|
3307
|
+
padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--bg-elev);
|
|
3308
|
+
}
|
|
3309
|
+
.route:first-child { border-radius: var(--radius) var(--radius) 0 0; }
|
|
3310
|
+
.route:last-child { border-bottom: 0; border-radius: 0 0 var(--radius) var(--radius); }
|
|
3311
|
+
.route-tools { display: flex; align-items: flex-end; gap: 12px; margin-bottom: 12px; }
|
|
3312
|
+
.route-tools .field { flex: 1; }
|
|
3313
|
+
.route-tools input { width: 100%; }
|
|
3314
|
+
.route-count { padding-bottom: 10px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3315
|
+
#route-list { max-height: 34rem; border: 1px solid var(--line); border-radius: var(--radius); overflow: auto; box-shadow: var(--shadow-xs); }
|
|
3316
|
+
.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; }
|
|
3317
|
+
@keyframes view-in { from { transform: translateY(3px); } to { transform: translateY(0); } }
|
|
3318
|
+
@keyframes dialog-in { from { transform: translateY(8px) scale(0.99); } to { transform: translateY(0) scale(1); } }
|
|
3319
|
+
@keyframes pulse { 50% { opacity: 0.45; } }
|
|
3320
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
|
|
3321
|
+
@media (forced-colors: active) { .json-editor pre { display: none; } .json-editor textarea { color: CanvasText; -webkit-text-fill-color: CanvasText; } }
|
|
2954
3322
|
@media (max-width: 800px) {
|
|
3323
|
+
:root { --header: 0px; }
|
|
2955
3324
|
header.top {
|
|
2956
|
-
align-items: stretch; flex-direction: column;
|
|
2957
|
-
padding
|
|
3325
|
+
position: relative; align-items: stretch; flex-direction: column;
|
|
3326
|
+
padding: max(12px, env(safe-area-inset-top)) 14px 12px;
|
|
2958
3327
|
}
|
|
2959
3328
|
input, select, textarea { font-size: 16px; }
|
|
2960
3329
|
.brand, .vendor, .controls { width: 100%; }
|
|
3330
|
+
.brand { gap: 10px; }
|
|
3331
|
+
.vendor { order: 2; border-radius: 9px; }
|
|
2961
3332
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
3333
|
+
.sync-status { display: none; }
|
|
2962
3334
|
/* The layout fills the screen. A single-column grid stretches every auto
|
|
2963
3335
|
row, so the tab bar grew with the leftover height and the buttons floated
|
|
2964
3336
|
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
@@ -2968,14 +3340,23 @@ var renderAdminDocument = (service) => {
|
|
|
2968
3340
|
align-content: start;
|
|
2969
3341
|
}
|
|
2970
3342
|
nav.side {
|
|
2971
|
-
align-self: start; height: auto;
|
|
3343
|
+
position: sticky; top: 0; z-index: 12; align-self: start; height: auto;
|
|
2972
3344
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
2973
3345
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
2974
|
-
padding: 8px;
|
|
3346
|
+
padding: 8px; box-shadow: var(--shadow-xs);
|
|
2975
3347
|
}
|
|
2976
|
-
nav
|
|
3348
|
+
.nav-label { display: none; }
|
|
3349
|
+
nav.side button { width: auto; white-space: nowrap; flex: none; }
|
|
3350
|
+
nav.side button[aria-current="true"]::before { inset: auto 9px -8px; width: auto; height: 3px; border-radius: 3px 3px 0 0; }
|
|
2977
3351
|
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
2978
|
-
|
|
3352
|
+
.form-grid { grid-template-columns: minmax(0, 1fr); }
|
|
3353
|
+
.form-grid .wide { grid-column: auto; }
|
|
3354
|
+
main { padding: 22px 14px 48px; }
|
|
3355
|
+
.view-head { margin-bottom: 18px; }
|
|
3356
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
3357
|
+
.card { padding: 14px; }
|
|
3358
|
+
.card .v { font-size: 20px; }
|
|
3359
|
+
.table-shell { border: 0; background: transparent; box-shadow: none; overflow: visible; }
|
|
2979
3360
|
table, thead, tbody, tr, th, td { display: block; }
|
|
2980
3361
|
thead { display: none; }
|
|
2981
3362
|
tr {
|
|
@@ -2990,50 +3371,63 @@ var renderAdminDocument = (service) => {
|
|
|
2990
3371
|
.route { grid-template-columns: 72px 1fr; }
|
|
2991
3372
|
.route button { grid-column: 1 / -1; }
|
|
2992
3373
|
}
|
|
3374
|
+
@media (max-width: 460px) {
|
|
3375
|
+
.controls label.field { min-width: calc(50% - 4px); }
|
|
3376
|
+
.controls .icon-btn, .controls .btn { flex: 1; }
|
|
3377
|
+
.grid { grid-template-columns: minmax(0, 1fr); }
|
|
3378
|
+
.dialog-actions { flex-wrap: wrap; }
|
|
3379
|
+
.dialog-actions button { flex: 1; }
|
|
3380
|
+
.dialog-actions .btn-danger { flex-basis: 100%; margin-right: 0; }
|
|
3381
|
+
}
|
|
2993
3382
|
</style>
|
|
2994
3383
|
</head>
|
|
2995
3384
|
<body>
|
|
2996
3385
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
2997
3386
|
<header class="top">
|
|
2998
3387
|
<div class="brand">
|
|
2999
|
-
<div class="brand-
|
|
3000
|
-
<
|
|
3001
|
-
|
|
3388
|
+
<div class="brand-lockup">
|
|
3389
|
+
<div class="brand-mark" aria-hidden="true">
|
|
3390
|
+
<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>
|
|
3391
|
+
</div>
|
|
3392
|
+
<div class="brand-id">
|
|
3393
|
+
<strong>${name}</strong>
|
|
3394
|
+
<span>Mock admin</span>
|
|
3395
|
+
</div>
|
|
3002
3396
|
</div>
|
|
3003
3397
|
<div class="vendor" id="vendor" hidden></div>
|
|
3004
3398
|
</div>
|
|
3005
3399
|
<div class="controls">
|
|
3400
|
+
<span class="sync-status" id="sync-status" role="status" aria-live="polite">Ready</span>
|
|
3006
3401
|
<label class="field">Namespace
|
|
3007
3402
|
<select id="namespace" aria-label="Namespace"></select>
|
|
3008
3403
|
</label>
|
|
3009
3404
|
<label class="field">Admin key
|
|
3010
3405
|
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
3011
3406
|
</label>
|
|
3012
|
-
<button class="icon-btn" id="theme" type="button" aria-label="
|
|
3013
|
-
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
3407
|
+
<button class="icon-btn" id="theme" type="button" aria-label="Use dark theme" title="Use dark theme"></button>
|
|
3408
|
+
<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>
|
|
3014
3409
|
</div>
|
|
3015
3410
|
</header>
|
|
3016
3411
|
<div class="layout">
|
|
3017
3412
|
<nav class="side" id="nav" aria-label="Admin sections">
|
|
3018
|
-
<
|
|
3019
|
-
<button type="button" data-view="
|
|
3020
|
-
<button type="button" data-view="
|
|
3021
|
-
<button type="button" data-view="
|
|
3022
|
-
<button type="button" data-view="
|
|
3023
|
-
<button type="button" data-view="
|
|
3413
|
+
<span class="nav-label">Workspace</span>
|
|
3414
|
+
<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>
|
|
3415
|
+
<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>
|
|
3416
|
+
<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>
|
|
3417
|
+
<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>
|
|
3418
|
+
<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>
|
|
3419
|
+
<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>
|
|
3024
3420
|
</nav>
|
|
3025
3421
|
<main id="views">
|
|
3026
|
-
<
|
|
3422
|
+
<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>
|
|
3027
3423
|
<section class="view" id="view-overview">
|
|
3028
|
-
<h2>Overview</h2>
|
|
3029
|
-
<p class="lede">Health, clock, and the collections this namespace is holding.</p>
|
|
3424
|
+
<div class="view-head"><div><h2>Overview</h2><p class="lede">Health, clock, and the collections this namespace is holding.</p></div></div>
|
|
3030
3425
|
<div class="grid" id="overview-cards"></div>
|
|
3031
3426
|
<h3>Recent requests</h3>
|
|
3032
3427
|
<div id="overview-requests"></div>
|
|
3033
3428
|
</section>
|
|
3034
3429
|
<section class="view" id="view-state" hidden>
|
|
3035
|
-
<h2>State</h2>
|
|
3036
|
-
<p class="lede">Every collection in this namespace. Declared fields stay visible before any row exists; stored rows add whatever else they contain.</p>
|
|
3430
|
+
<div class="view-head"><div><h2>State</h2><p class="lede">Inspect and edit every collection in this namespace.</p></div></div>
|
|
3037
3431
|
<div class="split">
|
|
3038
3432
|
<div id="collections"></div>
|
|
3039
3433
|
<div>
|
|
@@ -3046,8 +3440,7 @@ var renderAdminDocument = (service) => {
|
|
|
3046
3440
|
</div>
|
|
3047
3441
|
</section>
|
|
3048
3442
|
<section class="view" id="view-clock" hidden>
|
|
3049
|
-
<h2>Clock</h2>
|
|
3050
|
-
<p class="lede">The clock every timestamp in this mock reads.</p>
|
|
3443
|
+
<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>
|
|
3051
3444
|
<div class="card" id="clock-readout"></div>
|
|
3052
3445
|
<div class="row-actions">
|
|
3053
3446
|
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
@@ -3066,21 +3459,22 @@ var renderAdminDocument = (service) => {
|
|
|
3066
3459
|
</form>
|
|
3067
3460
|
</section>
|
|
3068
3461
|
<section class="view" id="view-faults" hidden>
|
|
3069
|
-
<h2>Faults</h2>
|
|
3070
|
-
<p class="lede">Inject a response, a delay, or a named preset. Rules apply to this namespace.</p>
|
|
3462
|
+
<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>
|
|
3071
3463
|
<form id="fault-form" class="card stack">
|
|
3072
|
-
<
|
|
3073
|
-
<
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
<
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
<
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
<
|
|
3083
|
-
|
|
3464
|
+
<div class="form-grid">
|
|
3465
|
+
<label class="field wide">Preset
|
|
3466
|
+
<select id="fault-preset"><option value="">Custom rule</option></select>
|
|
3467
|
+
</label>
|
|
3468
|
+
<label class="field">Status
|
|
3469
|
+
<input id="fault-status" inputmode="numeric" placeholder="503">
|
|
3470
|
+
</label>
|
|
3471
|
+
<label class="field wide">Path prefix
|
|
3472
|
+
<input id="fault-path" placeholder="/v1">
|
|
3473
|
+
</label>
|
|
3474
|
+
<label class="field">Method
|
|
3475
|
+
<input id="fault-method" placeholder="GET">
|
|
3476
|
+
</label>
|
|
3477
|
+
</div>
|
|
3084
3478
|
<div class="dialog-actions">
|
|
3085
3479
|
<button class="btn-primary" type="submit">Add fault</button>
|
|
3086
3480
|
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
@@ -3089,26 +3483,31 @@ var renderAdminDocument = (service) => {
|
|
|
3089
3483
|
<div id="fault-list"></div>
|
|
3090
3484
|
</section>
|
|
3091
3485
|
<section class="view" id="view-journal" hidden>
|
|
3092
|
-
<h2>Journal</h2>
|
|
3093
|
-
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
3094
|
-
<div class="row-actions">
|
|
3095
|
-
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
3096
|
-
</div>
|
|
3486
|
+
<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>
|
|
3097
3487
|
<div id="journal"></div>
|
|
3098
3488
|
</section>
|
|
3099
3489
|
<section class="view" id="view-routes" hidden>
|
|
3100
|
-
<h2>Routes</h2>
|
|
3101
|
-
<
|
|
3490
|
+
<div class="view-head"><div><h2>Routes</h2><p class="lede">Explore shared controls and service-specific admin endpoints.</p></div></div>
|
|
3491
|
+
<div class="route-tools">
|
|
3492
|
+
<label class="field">Filter routes
|
|
3493
|
+
<input id="route-search" type="search" placeholder="Method, path, or extension" autocomplete="off">
|
|
3494
|
+
</label>
|
|
3495
|
+
<span class="route-count" id="route-count"></span>
|
|
3496
|
+
</div>
|
|
3102
3497
|
<div id="route-list"></div>
|
|
3103
|
-
<form id="route-form" class="card stack">
|
|
3498
|
+
<form id="route-form" class="card stack" style="margin-top: 18px">
|
|
3104
3499
|
<strong id="route-title">Select a route</strong>
|
|
3105
3500
|
<label class="field">JSON body
|
|
3106
|
-
<
|
|
3501
|
+
<div class="json-editor" id="route-editor" hidden>
|
|
3502
|
+
<pre aria-hidden="true"><code id="route-highlight"></code></pre>
|
|
3503
|
+
<textarea id="route-body" placeholder="{ }" spellcheck="false" aria-describedby="route-json-status" disabled></textarea>
|
|
3504
|
+
</div>
|
|
3505
|
+
<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>
|
|
3107
3506
|
</label>
|
|
3108
3507
|
<div class="dialog-actions">
|
|
3109
|
-
<button class="btn-primary" type="submit">Send</button>
|
|
3508
|
+
<button class="btn-primary" id="route-send" type="submit" disabled>Send request</button>
|
|
3110
3509
|
</div>
|
|
3111
|
-
<pre id="route-result">Response will show here.</pre>
|
|
3510
|
+
<pre class="json-view" id="route-result"><code>Response will show here.</code></pre>
|
|
3112
3511
|
</form>
|
|
3113
3512
|
</section>
|
|
3114
3513
|
</main>
|
|
@@ -3116,12 +3515,16 @@ var renderAdminDocument = (service) => {
|
|
|
3116
3515
|
</div>
|
|
3117
3516
|
<dialog id="editor">
|
|
3118
3517
|
<form method="dialog" id="editor-form">
|
|
3119
|
-
<strong id="editor-title">Record</strong>
|
|
3518
|
+
<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>
|
|
3120
3519
|
<label class="field">Id
|
|
3121
3520
|
<input id="editor-id" required>
|
|
3122
3521
|
</label>
|
|
3123
3522
|
<label class="field">JSON value
|
|
3124
|
-
<
|
|
3523
|
+
<div class="json-editor" id="record-editor">
|
|
3524
|
+
<pre aria-hidden="true"><code id="editor-highlight"></code></pre>
|
|
3525
|
+
<textarea id="editor-value" spellcheck="false" aria-describedby="editor-json-status"></textarea>
|
|
3526
|
+
</div>
|
|
3527
|
+
<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>
|
|
3125
3528
|
</label>
|
|
3126
3529
|
<div class="dialog-actions">
|
|
3127
3530
|
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
@@ -3163,8 +3566,8 @@ var describeExtension = (extension) => {
|
|
|
3163
3566
|
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
3164
3567
|
};
|
|
3165
3568
|
};
|
|
3166
|
-
var adminUiRoutes = (service, ui) => {
|
|
3167
|
-
const shell = () => renderAdminDocument(service);
|
|
3569
|
+
var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
3570
|
+
const shell = () => renderAdminDocument(service, adminPrefix);
|
|
3168
3571
|
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
3169
3572
|
return {
|
|
3170
3573
|
"GET /ui": () => htmlResponse(document3()),
|
|
@@ -3781,7 +4184,7 @@ var stateAdminRoutes = (routes) => {
|
|
|
3781
4184
|
};
|
|
3782
4185
|
|
|
3783
4186
|
// ../core/dist/version.js
|
|
3784
|
-
var PACKAGE_VERSION = true ? "
|
|
4187
|
+
var PACKAGE_VERSION = true ? "2.0.0" : UNRELEASED_VERSION;
|
|
3785
4188
|
|
|
3786
4189
|
// ../core/dist/signing.js
|
|
3787
4190
|
var encoder = new TextEncoder();
|
|
@@ -3795,7 +4198,10 @@ var toBase64 = (bytes) => {
|
|
|
3795
4198
|
var fromBase64 = (value) => Uint8Array.from(atob(value), (char) => char.charCodeAt(0));
|
|
3796
4199
|
|
|
3797
4200
|
// ../core/dist/webhooks.js
|
|
3798
|
-
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4201
|
+
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4202
|
+
status,
|
|
4203
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4204
|
+
});
|
|
3799
4205
|
var adminError3 = (status, message) => json4(status, { error: { type: "mockingbird_admin", message } });
|
|
3800
4206
|
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3801
4207
|
var parseEndpoint = (value) => {
|
|
@@ -3903,7 +4309,6 @@ var AT_HEADER = "x-mockingbird-at";
|
|
|
3903
4309
|
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
3904
4310
|
var DEFAULT_NAMESPACE = "default";
|
|
3905
4311
|
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
3906
|
-
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
3907
4312
|
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
3908
4313
|
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
3909
4314
|
var effects = /* @__PURE__ */ new WeakMap();
|
|
@@ -3944,6 +4349,22 @@ var operationMatcher = (document3) => {
|
|
|
3944
4349
|
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
3945
4350
|
};
|
|
3946
4351
|
var createRuntime = (options) => {
|
|
4352
|
+
const adminPrefix = resolveAdminPrefix(options.adminPrefix);
|
|
4353
|
+
const internalPaths = Object.entries(options.document?.paths ?? {}).filter(([, item]) => item["x-mockingbird-internal"] === true).map(([path]) => path);
|
|
4354
|
+
for (const path of internalPaths) {
|
|
4355
|
+
if (!path.startsWith(`${ADMIN_PREFIX}/blobs/`))
|
|
4356
|
+
throw new Error(`internal transport ${path} must live under ${ADMIN_PREFIX}/blobs/`);
|
|
4357
|
+
}
|
|
4358
|
+
assertAdminPrefixAvailable(adminPrefix, Object.keys(options.document?.paths ?? {}).filter((path) => !internalPaths.includes(path)));
|
|
4359
|
+
const internalMatch = (path) => {
|
|
4360
|
+
for (const pattern of internalPaths) {
|
|
4361
|
+
const actual = path.split("/");
|
|
4362
|
+
const expected = pattern.split("/");
|
|
4363
|
+
if (actual.length === expected.length && expected.every((part, i) => part.startsWith("{") || part === actual[i]))
|
|
4364
|
+
return pattern;
|
|
4365
|
+
}
|
|
4366
|
+
return void 0;
|
|
4367
|
+
};
|
|
3947
4368
|
assertAdminUi(options.adminUi);
|
|
3948
4369
|
const sqlite = bootSqlite(options.sqlite);
|
|
3949
4370
|
const clock = options.clock ?? createClock();
|
|
@@ -3958,6 +4379,20 @@ var createRuntime = (options) => {
|
|
|
3958
4379
|
const instances = /* @__PURE__ */ new Map();
|
|
3959
4380
|
const publicNamespaces = /* @__PURE__ */ new Set();
|
|
3960
4381
|
const branchRngs = /* @__PURE__ */ new Map();
|
|
4382
|
+
const namespaceClocks = /* @__PURE__ */ new Map();
|
|
4383
|
+
const namespaceForStorage = /* @__PURE__ */ new Map();
|
|
4384
|
+
let isolatedNamespaces = false;
|
|
4385
|
+
const namespaceClock = (name = DEFAULT_NAMESPACE) => {
|
|
4386
|
+
if (!isolatedNamespaces || name === DEFAULT_NAMESPACE)
|
|
4387
|
+
return clock;
|
|
4388
|
+
let found = namespaceClocks.get(name);
|
|
4389
|
+
if (!found) {
|
|
4390
|
+
found = createClock();
|
|
4391
|
+
namespaceClocks.set(name, found);
|
|
4392
|
+
}
|
|
4393
|
+
return found;
|
|
4394
|
+
};
|
|
4395
|
+
const fleetPoints = /* @__PURE__ */ new WeakMap();
|
|
3961
4396
|
const timelines = /* @__PURE__ */ new Map();
|
|
3962
4397
|
const branchStorage = /* @__PURE__ */ new Map();
|
|
3963
4398
|
const captured = /* @__PURE__ */ new Map();
|
|
@@ -3971,17 +4406,22 @@ var createRuntime = (options) => {
|
|
|
3971
4406
|
if (!NAMESPACE_PATTERN.test(key) || !NAMESPACE_PATTERN.test(publicNamespace)) {
|
|
3972
4407
|
throw new RangeError(`namespace must match ${NAMESPACE_PATTERN}: ${JSON.stringify(publicNamespace)}`);
|
|
3973
4408
|
}
|
|
4409
|
+
const createdContextRng = isolatedRng ?? (isolatedNamespaces && key !== DEFAULT_NAMESPACE ? createRng(options.seed ?? 0) : rng);
|
|
3974
4410
|
const created = options.create({
|
|
3975
4411
|
namespace: storageNamespace(key),
|
|
3976
4412
|
publicNamespace,
|
|
4413
|
+
adminPrefix,
|
|
3977
4414
|
sqlite,
|
|
3978
|
-
clock,
|
|
3979
|
-
rng:
|
|
4415
|
+
clock: namespaceClock(publicNamespace),
|
|
4416
|
+
rng: createdContextRng
|
|
3980
4417
|
});
|
|
3981
4418
|
instances.set(key, created);
|
|
4419
|
+
namespaceForStorage.set(key, publicNamespace);
|
|
3982
4420
|
publicNamespaces.add(publicNamespace);
|
|
3983
4421
|
if (isolatedRng)
|
|
3984
4422
|
branchRngs.set(key, isolatedRng);
|
|
4423
|
+
else if (isolatedNamespaces && key !== DEFAULT_NAMESPACE)
|
|
4424
|
+
branchRngs.set(key, createdContextRng);
|
|
3985
4425
|
return created;
|
|
3986
4426
|
};
|
|
3987
4427
|
const instance = (name = DEFAULT_NAMESPACE) => instanceFor(name);
|
|
@@ -3999,7 +4439,7 @@ var createRuntime = (options) => {
|
|
|
3999
4439
|
captured.set(storage, snapshot2);
|
|
4000
4440
|
return Object.freeze({
|
|
4001
4441
|
snapshot: snapshot2,
|
|
4002
|
-
clock: Object.freeze(
|
|
4442
|
+
clock: Object.freeze(namespaceClock(namespaceForStorage.get(storage) ?? storage).state()),
|
|
4003
4443
|
rngState: (branchRngs.get(storage) ?? rng).state()
|
|
4004
4444
|
});
|
|
4005
4445
|
};
|
|
@@ -4009,7 +4449,7 @@ var createRuntime = (options) => {
|
|
|
4009
4449
|
return found;
|
|
4010
4450
|
instance(name);
|
|
4011
4451
|
found = new Timeline({
|
|
4012
|
-
now:
|
|
4452
|
+
now: namespaceClock(name).now,
|
|
4013
4453
|
...options.maxCheckpoints !== void 0 ? { maxCheckpoints: options.maxCheckpoints } : {}
|
|
4014
4454
|
});
|
|
4015
4455
|
found.commit(capture(name));
|
|
@@ -4036,12 +4476,13 @@ var createRuntime = (options) => {
|
|
|
4036
4476
|
const point = history2.checkout("main", at);
|
|
4037
4477
|
restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
|
|
4038
4478
|
captured.set(namespace, point.value.snapshot);
|
|
4039
|
-
rng.setState(point.value.rngState);
|
|
4040
|
-
|
|
4479
|
+
(branchRngs.get(namespace) ?? rng).setState(point.value.rngState);
|
|
4480
|
+
const selectedClock = namespaceClock(namespace);
|
|
4481
|
+
selectedClock.set(point.value.clock.now);
|
|
4041
4482
|
if (point.value.clock.frozen)
|
|
4042
|
-
|
|
4483
|
+
selectedClock.freeze();
|
|
4043
4484
|
else
|
|
4044
|
-
|
|
4485
|
+
selectedClock.unfreeze();
|
|
4045
4486
|
}
|
|
4046
4487
|
return namespace;
|
|
4047
4488
|
}
|
|
@@ -4099,11 +4540,12 @@ var createRuntime = (options) => {
|
|
|
4099
4540
|
const storage = ensureBranch(namespace, branchName);
|
|
4100
4541
|
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
4101
4542
|
captured.set(storage, point.value.snapshot);
|
|
4102
|
-
|
|
4543
|
+
const selectedClock = namespaceClock(namespace);
|
|
4544
|
+
selectedClock.set(point.value.clock.now);
|
|
4103
4545
|
if (point.value.clock.frozen)
|
|
4104
|
-
|
|
4546
|
+
selectedClock.freeze();
|
|
4105
4547
|
else
|
|
4106
|
-
|
|
4548
|
+
selectedClock.unfreeze();
|
|
4107
4549
|
(branchRngs.get(storage) ?? rng).setState(point.value.rngState);
|
|
4108
4550
|
};
|
|
4109
4551
|
const reset = async (name = DEFAULT_NAMESPACE) => {
|
|
@@ -4111,10 +4553,21 @@ var createRuntime = (options) => {
|
|
|
4111
4553
|
options.webhooks?.clear();
|
|
4112
4554
|
for (const each of instances.values())
|
|
4113
4555
|
await each.reset();
|
|
4556
|
+
for (const each of branchRngs.values())
|
|
4557
|
+
each.reset();
|
|
4114
4558
|
timelines.clear();
|
|
4559
|
+
for (const storage of branchStorage.values()) {
|
|
4560
|
+
instances.delete(storage);
|
|
4561
|
+
branchRngs.delete(storage);
|
|
4562
|
+
namespaceForStorage.delete(storage);
|
|
4563
|
+
}
|
|
4115
4564
|
branchStorage.clear();
|
|
4116
|
-
branchRngs.clear();
|
|
4117
4565
|
captured.clear();
|
|
4566
|
+
faults.clear();
|
|
4567
|
+
clock.reset();
|
|
4568
|
+
rng.reset();
|
|
4569
|
+
for (const each of namespaceClocks.values())
|
|
4570
|
+
each.reset();
|
|
4118
4571
|
return;
|
|
4119
4572
|
}
|
|
4120
4573
|
options.webhooks?.clear(name);
|
|
@@ -4132,11 +4585,18 @@ var createRuntime = (options) => {
|
|
|
4132
4585
|
else
|
|
4133
4586
|
clearNamespace(sqlite, storageNamespace(storage));
|
|
4134
4587
|
branchStorage.delete(mapping);
|
|
4588
|
+
instances.delete(storage);
|
|
4589
|
+
namespaceForStorage.delete(storage);
|
|
4135
4590
|
branchRngs.delete(storage);
|
|
4136
4591
|
captured.delete(storage);
|
|
4137
4592
|
}
|
|
4138
4593
|
timelines.delete(name);
|
|
4139
4594
|
captured.delete(name);
|
|
4595
|
+
faults.restore(name, { rules: [], rngState: createRng(options.seed ?? 0).state() });
|
|
4596
|
+
namespaceClock(name).reset();
|
|
4597
|
+
(branchRngs.get(name) ?? rng).reset();
|
|
4598
|
+
journal.clear(name);
|
|
4599
|
+
metrics.reset(name);
|
|
4140
4600
|
};
|
|
4141
4601
|
const snapshot = (name = DEFAULT_NAMESPACE) => {
|
|
4142
4602
|
return checkpoint(name, "main").value.snapshot;
|
|
@@ -4174,6 +4634,53 @@ var createRuntime = (options) => {
|
|
|
4174
4634
|
rng,
|
|
4175
4635
|
credentials,
|
|
4176
4636
|
webhooks: options.webhooks,
|
|
4637
|
+
isolateNamespaces() {
|
|
4638
|
+
if (publicNamespaces.size > 1)
|
|
4639
|
+
throw new Error("enable namespace isolation before creating named namespaces");
|
|
4640
|
+
isolatedNamespaces = true;
|
|
4641
|
+
faults.isolateNamespaces();
|
|
4642
|
+
},
|
|
4643
|
+
namespaceClock,
|
|
4644
|
+
namespaceOf(request) {
|
|
4645
|
+
const url = new URL(request.url);
|
|
4646
|
+
const prefixed = matchNamespacePath(url.pathname, adminPrefix);
|
|
4647
|
+
const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
|
|
4648
|
+
const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
|
|
4649
|
+
if (isAdminPath(path, adminPrefix))
|
|
4650
|
+
return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
|
|
4651
|
+
if (selected !== void 0)
|
|
4652
|
+
return selected;
|
|
4653
|
+
const credential = options.credential?.(request);
|
|
4654
|
+
return (credential !== void 0 ? credentials.get(credential) : void 0) ?? DEFAULT_NAMESPACE;
|
|
4655
|
+
},
|
|
4656
|
+
fleetSnapshot(namespace) {
|
|
4657
|
+
instance(namespace);
|
|
4658
|
+
const handle = {};
|
|
4659
|
+
fleetPoints.set(handle, {
|
|
4660
|
+
namespace,
|
|
4661
|
+
state: capture(namespace),
|
|
4662
|
+
faults: faults.snapshot(namespace),
|
|
4663
|
+
webhooks: options.webhooks?.snapshot(namespace)
|
|
4664
|
+
});
|
|
4665
|
+
return handle;
|
|
4666
|
+
},
|
|
4667
|
+
fleetRestore(handle, namespace) {
|
|
4668
|
+
const point = typeof handle === "object" && handle !== null ? fleetPoints.get(handle) : void 0;
|
|
4669
|
+
if (!point || point.namespace !== namespace)
|
|
4670
|
+
throw new Error("invalid fleet checkpoint");
|
|
4671
|
+
restoreNamespace(sqlite, storageNamespace(namespace), point.state.snapshot);
|
|
4672
|
+
captured.set(namespace, point.state.snapshot);
|
|
4673
|
+
const selectedClock = namespaceClock(namespace);
|
|
4674
|
+
selectedClock.freeze();
|
|
4675
|
+
selectedClock.set(point.state.clock.now);
|
|
4676
|
+
if (!point.state.clock.frozen)
|
|
4677
|
+
selectedClock.unfreeze();
|
|
4678
|
+
(branchRngs.get(namespace) ?? rng).setState(point.state.rngState);
|
|
4679
|
+
faults.restore(namespace, point.faults);
|
|
4680
|
+
if (point.webhooks !== void 0)
|
|
4681
|
+
options.webhooks?.restore(namespace, point.webhooks);
|
|
4682
|
+
timelines.delete(namespace);
|
|
4683
|
+
},
|
|
4177
4684
|
applyPreset: (name, namespace = DEFAULT_NAMESPACE, overrides = {}) => {
|
|
4178
4685
|
const preset = options.presets?.[name];
|
|
4179
4686
|
if (!preset)
|
|
@@ -4205,7 +4712,7 @@ var createRuntime = (options) => {
|
|
|
4205
4712
|
state: (namespace = DEFAULT_NAMESPACE) => inspectState(stateScope(namespace)),
|
|
4206
4713
|
fetch: async (incoming) => {
|
|
4207
4714
|
let request = incoming;
|
|
4208
|
-
const prefixed =
|
|
4715
|
+
const prefixed = matchNamespacePath(new URL(request.url).pathname, adminPrefix);
|
|
4209
4716
|
if (prefixed) {
|
|
4210
4717
|
const url2 = new URL(request.url);
|
|
4211
4718
|
url2.pathname = prefixed[2] ?? "/";
|
|
@@ -4221,7 +4728,15 @@ var createRuntime = (options) => {
|
|
|
4221
4728
|
signal: request.signal
|
|
4222
4729
|
});
|
|
4223
4730
|
}
|
|
4224
|
-
|
|
4731
|
+
const internalUrl = new URL(request.url);
|
|
4732
|
+
const internalPath = isAdminPath(internalUrl.pathname, adminPrefix) ? `${ADMIN_PREFIX}${internalUrl.pathname.slice(adminPrefix.length)}` : "";
|
|
4733
|
+
const internal = internalMatch(internalPath);
|
|
4734
|
+
const obsoleteInternalPath = adminPrefix !== ADMIN_PREFIX && internalMatch(internalUrl.pathname) && !internal;
|
|
4735
|
+
if (internal) {
|
|
4736
|
+
internalUrl.pathname = internalPath;
|
|
4737
|
+
request = new Request(internalUrl, request);
|
|
4738
|
+
}
|
|
4739
|
+
let namespace = internal ? internalUrl.searchParams.get("namespace") ?? control.namespaceOf(request) : control.namespaceOf(request);
|
|
4225
4740
|
if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
4226
4741
|
const credential = options.credential(request);
|
|
4227
4742
|
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
@@ -4241,7 +4756,9 @@ var createRuntime = (options) => {
|
|
|
4241
4756
|
return copy;
|
|
4242
4757
|
}
|
|
4243
4758
|
};
|
|
4244
|
-
|
|
4759
|
+
if (obsoleteInternalPath)
|
|
4760
|
+
return stamp(adminFail(404, "internal transport is only available under adminPrefix"));
|
|
4761
|
+
const handled = internal ? void 0 : await control.handle(request);
|
|
4245
4762
|
if (handled)
|
|
4246
4763
|
return stamp(handled);
|
|
4247
4764
|
const started = monotonicNow();
|
|
@@ -4282,7 +4799,7 @@ var createRuntime = (options) => {
|
|
|
4282
4799
|
type: "mockingbird_admin",
|
|
4283
4800
|
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
4284
4801
|
}
|
|
4285
|
-
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
4802
|
+
}), { status: 400, headers: { "content-type": "application/json; charset=utf-8" } }));
|
|
4286
4803
|
}
|
|
4287
4804
|
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
4288
4805
|
log(400);
|
|
@@ -4349,6 +4866,7 @@ var createRuntime = (options) => {
|
|
|
4349
4866
|
startedAt: wallNow(),
|
|
4350
4867
|
wallNow,
|
|
4351
4868
|
clock,
|
|
4869
|
+
namespaceClock,
|
|
4352
4870
|
faults,
|
|
4353
4871
|
metrics,
|
|
4354
4872
|
journal,
|
|
@@ -4398,15 +4916,16 @@ var createRuntime = (options) => {
|
|
|
4398
4916
|
...options.admin?.(runtime) ?? {},
|
|
4399
4917
|
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
4400
4918
|
// maps `presets`, and a service route must not replace that list with a record.
|
|
4401
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
4919
|
+
...options.presets ? presetRoutes(options.presets, runtime, adminPrefix) : {},
|
|
4402
4920
|
...stateAdminRoutes({
|
|
4403
4921
|
open: stateScope,
|
|
4404
4922
|
afterWrite: (namespace) => {
|
|
4405
4923
|
checkpoint(namespace, "main");
|
|
4406
4924
|
}
|
|
4407
4925
|
}),
|
|
4408
|
-
...adminUiRoutes(options.name, options.adminUi)
|
|
4926
|
+
...adminUiRoutes(options.name, options.adminUi, adminPrefix)
|
|
4409
4927
|
},
|
|
4928
|
+
adminPrefix,
|
|
4410
4929
|
adminKey: options.adminKey
|
|
4411
4930
|
});
|
|
4412
4931
|
return defineMock(runtime);
|
|
@@ -4420,7 +4939,10 @@ var mutableResponse = (response) => {
|
|
|
4420
4939
|
return new Response(response.body, response);
|
|
4421
4940
|
}
|
|
4422
4941
|
};
|
|
4423
|
-
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
4942
|
+
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
4943
|
+
status,
|
|
4944
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4945
|
+
});
|
|
4424
4946
|
var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
|
|
4425
4947
|
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4426
4948
|
var credentialRoutes = (registry) => ({
|
|
@@ -4472,7 +4994,7 @@ var credentialRoutes = (registry) => ({
|
|
|
4472
4994
|
return adminJson(200, { status: "ok" });
|
|
4473
4995
|
}
|
|
4474
4996
|
});
|
|
4475
|
-
var presetRoutes = (presets, runtime) => ({
|
|
4997
|
+
var presetRoutes = (presets, runtime, adminPrefix) => ({
|
|
4476
4998
|
"GET /faults/presets": () => {
|
|
4477
4999
|
const body = {
|
|
4478
5000
|
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
@@ -4482,7 +5004,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
4482
5004
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
4483
5005
|
const name = params.name;
|
|
4484
5006
|
if (!presets[name])
|
|
4485
|
-
return adminFail(404, `no fault preset ${name}; GET /
|
|
5007
|
+
return adminFail(404, `no fault preset ${name}; GET ${adminPrefix}/faults/presets`);
|
|
4486
5008
|
const overrides = isObject(body) ? body : {};
|
|
4487
5009
|
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
4488
5010
|
}
|
|
@@ -6325,14 +6847,16 @@ var createRuntime2 = (options = {}) => createRuntime({
|
|
|
6325
6847
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
6326
6848
|
...options.clock ? { clock: options.clock } : {},
|
|
6327
6849
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
6850
|
+
...options.adminPrefix !== void 0 ? { adminPrefix: options.adminPrefix } : {},
|
|
6328
6851
|
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
6329
6852
|
...options.onLog ? { onLog: options.onLog } : {},
|
|
6330
6853
|
credential: keyCredential,
|
|
6331
6854
|
presets: GOOGLE_MAPS_PRESETS,
|
|
6332
|
-
create: ({ sqlite, namespace, publicNamespace, clock }) => new GoogleMapsAPI({
|
|
6855
|
+
create: ({ sqlite, namespace, publicNamespace, adminPrefix, clock }) => new GoogleMapsAPI({
|
|
6333
6856
|
sqlite,
|
|
6334
6857
|
namespace,
|
|
6335
6858
|
publicNamespace,
|
|
6859
|
+
adminPrefix,
|
|
6336
6860
|
now: clock.now,
|
|
6337
6861
|
...options.corpus ? { corpus: options.corpus } : {},
|
|
6338
6862
|
...options.settings ? { settings: options.settings } : {}
|
|
@@ -6383,10 +6907,12 @@ var GoogleMapsAPI = class {
|
|
|
6383
6907
|
sqlite;
|
|
6384
6908
|
state;
|
|
6385
6909
|
service;
|
|
6910
|
+
adminPrefix;
|
|
6386
6911
|
publicNamespace;
|
|
6387
6912
|
constructor(options = {}) {
|
|
6388
6913
|
const sqlite = bootSqlite(options.sqlite);
|
|
6389
6914
|
const namespace = options.namespace ?? GOOGLE_MAPS_NAMESPACE;
|
|
6915
|
+
this.adminPrefix = options.adminPrefix ?? "/__admin";
|
|
6390
6916
|
this.publicNamespace = options.publicNamespace ?? "default";
|
|
6391
6917
|
this.state = new GoogleMapsState(sqlite, namespace, {
|
|
6392
6918
|
corpus: options.corpus ?? [],
|
|
@@ -6605,7 +7131,7 @@ var GoogleMapsAPI = class {
|
|
|
6605
7131
|
const key = text(context.query.key) ?? "";
|
|
6606
7132
|
const settings = this.state.current();
|
|
6607
7133
|
const origin = (settings.publicUrl ?? context.url.origin).replace(/\/$/, "");
|
|
6608
|
-
const prefix = this.publicNamespace === "default" ? "" :
|
|
7134
|
+
const prefix = this.publicNamespace === "default" ? "" : `${this.adminPrefix}/ns/${encodeURIComponent(this.publicNamespace)}`;
|
|
6609
7135
|
const callback = text(context.query.callback) ?? null;
|
|
6610
7136
|
const safeCallback = callback && /^[A-Za-z_$][\w$.]*$/.test(callback) ? callback : null;
|
|
6611
7137
|
const authFailed = !key || settings.keys.length > 0 && !settings.keys.includes(key);
|
|
@@ -6620,6 +7146,9 @@ var GoogleMapsAPI = class {
|
|
|
6620
7146
|
};
|
|
6621
7147
|
|
|
6622
7148
|
export {
|
|
7149
|
+
createClock,
|
|
7150
|
+
resolveAdminPrefix,
|
|
7151
|
+
parseDuration,
|
|
6623
7152
|
document2 as document,
|
|
6624
7153
|
operationIds,
|
|
6625
7154
|
supportedOperationIds,
|
|
@@ -6639,4 +7168,4 @@ export {
|
|
|
6639
7168
|
keyCredential,
|
|
6640
7169
|
GoogleMapsAPI
|
|
6641
7170
|
};
|
|
6642
|
-
//# sourceMappingURL=chunk-
|
|
7171
|
+
//# sourceMappingURL=chunk-O2FDQBMY.js.map
|