@crvouga/mockingbird-service-fullscript 1.7.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/DISCOVERY.md +55 -0
- package/README.md +1 -1
- package/SUPPORT.md +20 -0
- package/dist/chunk-EZGGE5JC.js +871 -0
- package/dist/chunk-EZGGE5JC.js.map +7 -0
- package/dist/{chunk-VPJ64KU3.js → chunk-R7LD4QOX.js} +825 -260
- package/dist/chunk-R7LD4QOX.js.map +7 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +36 -5
- package/dist/index.js +1 -1
- package/dist/server.d.ts +66 -5
- package/dist/server.js +2 -2
- package/openapi.yaml +776 -0
- package/package.json +29 -4
- package/dist/chunk-DI3IPCT2.js +0 -384
- package/dist/chunk-DI3IPCT2.js.map +0 -7
- package/dist/chunk-VPJ64KU3.js.map +0 -7
|
@@ -110,13 +110,31 @@ var Collection = class {
|
|
|
110
110
|
};
|
|
111
111
|
|
|
112
112
|
// ../core/dist/control.js
|
|
113
|
-
var HEALTH_PATH = "/health";
|
|
114
113
|
var ADMIN_PREFIX = "/__admin";
|
|
114
|
+
var resolveAdminPrefix = (prefix = ADMIN_PREFIX) => {
|
|
115
|
+
if (!/^\/[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*$/.test(prefix)) {
|
|
116
|
+
throw new Error("adminPrefix must be an absolute non-root path of literal segments, without a trailing slash");
|
|
117
|
+
}
|
|
118
|
+
return prefix;
|
|
119
|
+
};
|
|
120
|
+
var isAdminPath = (path, prefix) => path === prefix || path.startsWith(`${prefix}/`);
|
|
121
|
+
var matchNamespacePath = (path, prefix) => /^\/ns\/([^/]+)(\/.*)?$/.exec(isAdminPath(path, prefix) ? path.slice(prefix.length) : "");
|
|
122
|
+
var assertAdminPrefixAvailable = (prefix, paths) => {
|
|
123
|
+
const reserved = prefix.split("/").slice(1);
|
|
124
|
+
for (const path of paths) {
|
|
125
|
+
const segments = path.split("/").slice(1);
|
|
126
|
+
if (segments.length < reserved.length || /^\{[^}]+\}$/.test(segments[0] ?? ""))
|
|
127
|
+
continue;
|
|
128
|
+
if (reserved.every((part, i) => segments[i] === part || /^\{[^}]+\}$/.test(segments[i] ?? ""))) {
|
|
129
|
+
throw new Error(`vendor route ${path} collides with adminPrefix ${prefix}; choose a different adminPrefix`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
115
133
|
var ADMIN_KEY_HEADER = "x-mockingbird-admin-key";
|
|
116
134
|
var NAMESPACE_HEADER = "x-mockingbird-namespace";
|
|
117
135
|
var json = (status, body) => new Response(JSON.stringify(body), {
|
|
118
136
|
status,
|
|
119
|
-
headers: { "content-type": "application/json" }
|
|
137
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
120
138
|
});
|
|
121
139
|
var adminError = (status, message) => json(status, { error: { type: "mockingbird_admin", message } });
|
|
122
140
|
var UNITS = {
|
|
@@ -168,11 +186,25 @@ var readJson = async (request) => {
|
|
|
168
186
|
};
|
|
169
187
|
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
170
188
|
var createControlPlane = (context) => {
|
|
189
|
+
const adminPrefix = resolveAdminPrefix(context.adminPrefix);
|
|
171
190
|
const snapshots = /* @__PURE__ */ new Map();
|
|
172
191
|
let snapshotCounter = 0;
|
|
173
192
|
const headerNamespace = (request) => request.headers.get(NAMESPACE_HEADER) ?? context.defaultNamespace;
|
|
174
193
|
const adminNamespace = (request, url) => url.searchParams.get("namespace") ?? headerNamespace(request);
|
|
194
|
+
const health = () => {
|
|
195
|
+
return json(200, {
|
|
196
|
+
...context.describe(),
|
|
197
|
+
status: "ok",
|
|
198
|
+
service: context.name,
|
|
199
|
+
uptimeMs: context.wallNow() - context.startedAt,
|
|
200
|
+
clock: context.clock.state(),
|
|
201
|
+
namespaces: context.namespaces().length,
|
|
202
|
+
adminPrefix,
|
|
203
|
+
adminUi: `${adminPrefix}/ui`
|
|
204
|
+
});
|
|
205
|
+
};
|
|
175
206
|
const builtin = {
|
|
207
|
+
"GET /health": health,
|
|
176
208
|
"GET /": () => json(200, {
|
|
177
209
|
service: context.name,
|
|
178
210
|
routes: [...Object.keys(builtin), ...Object.keys(context.routes)].sort()
|
|
@@ -183,29 +215,30 @@ var createControlPlane = (context) => {
|
|
|
183
215
|
return json(200, { status: "ok", reset: target === "*" ? context.namespaces() : [target] });
|
|
184
216
|
},
|
|
185
217
|
"GET /namespaces": () => json(200, { default: context.defaultNamespace, namespaces: context.namespaces() }),
|
|
186
|
-
"GET /clock": () => json(200, context.clock.state()),
|
|
187
|
-
"POST /clock": ({ body }) => {
|
|
218
|
+
"GET /clock": ({ namespace }) => json(200, (context.namespaceClock?.(namespace) ?? context.clock).state()),
|
|
219
|
+
"POST /clock": ({ body, namespace }) => {
|
|
220
|
+
const clock = context.namespaceClock?.(namespace) ?? context.clock;
|
|
188
221
|
if (!isRecord(body))
|
|
189
222
|
return adminError(400, "expected a JSON object");
|
|
190
223
|
if (body.reset === true)
|
|
191
|
-
|
|
224
|
+
clock.reset();
|
|
192
225
|
if (body.freeze === true)
|
|
193
|
-
|
|
226
|
+
clock.freeze();
|
|
194
227
|
if (body.set !== void 0) {
|
|
195
228
|
const instant = parseInstant(body.set);
|
|
196
229
|
if (instant === void 0)
|
|
197
230
|
return adminError(400, "set: expected epoch ms or ISO-8601");
|
|
198
|
-
|
|
231
|
+
clock.set(instant);
|
|
199
232
|
}
|
|
200
233
|
if (body.advance !== void 0) {
|
|
201
234
|
const delta = parseDuration(body.advance);
|
|
202
235
|
if (delta === void 0)
|
|
203
236
|
return adminError(400, 'advance: expected ms or "15m"-style');
|
|
204
|
-
|
|
237
|
+
clock.advance(delta);
|
|
205
238
|
}
|
|
206
239
|
if (body.freeze === false)
|
|
207
|
-
|
|
208
|
-
return json(200,
|
|
240
|
+
clock.unfreeze();
|
|
241
|
+
return json(200, clock.state());
|
|
209
242
|
},
|
|
210
243
|
"GET /faults": () => json(200, { faults: context.faults.list() }),
|
|
211
244
|
"POST /faults": ({ body, namespace }) => {
|
|
@@ -338,7 +371,14 @@ var createControlPlane = (context) => {
|
|
|
338
371
|
return json(200, { status: "ok" });
|
|
339
372
|
}
|
|
340
373
|
};
|
|
341
|
-
const
|
|
374
|
+
for (const key of Object.keys(context.routes)) {
|
|
375
|
+
if (Object.hasOwn(builtin, key))
|
|
376
|
+
throw new Error(`admin route ${key} overrides a shared route`);
|
|
377
|
+
const path = key.slice(key.indexOf(" ") + 1);
|
|
378
|
+
if (isAdminPath(path, "/ns") || isAdminPath(path, "/blobs"))
|
|
379
|
+
throw new Error(`admin route ${key} overlaps an internal transport`);
|
|
380
|
+
}
|
|
381
|
+
const routes = [...Object.entries(builtin), ...Object.entries(context.routes)].map(([key, handler]) => {
|
|
342
382
|
const space = key.indexOf(" ");
|
|
343
383
|
return { method: key.slice(0, space), pattern: key.slice(space + 1), handler };
|
|
344
384
|
});
|
|
@@ -346,21 +386,13 @@ var createControlPlane = (context) => {
|
|
|
346
386
|
namespaceOf: headerNamespace,
|
|
347
387
|
async handle(request) {
|
|
348
388
|
const url = new URL(request.url);
|
|
349
|
-
if (url.pathname ===
|
|
350
|
-
return
|
|
351
|
-
status: "ok",
|
|
352
|
-
service: context.name,
|
|
353
|
-
uptimeMs: context.wallNow() - context.startedAt,
|
|
354
|
-
clock: context.clock.state(),
|
|
355
|
-
namespaces: context.namespaces().length,
|
|
356
|
-
adminUi: `${ADMIN_PREFIX}/ui`,
|
|
357
|
-
...context.describe()
|
|
358
|
-
});
|
|
389
|
+
if (url.pathname === `${adminPrefix}/health` && request.method === "GET") {
|
|
390
|
+
return health();
|
|
359
391
|
}
|
|
360
|
-
if (url.pathname
|
|
392
|
+
if (!isAdminPath(url.pathname, adminPrefix)) {
|
|
361
393
|
return void 0;
|
|
362
394
|
}
|
|
363
|
-
const path = url.pathname.slice(
|
|
395
|
+
const path = url.pathname.slice(adminPrefix.length) || "/";
|
|
364
396
|
const shell = request.method === "GET" && (path === "/ui" || path === "/ui/");
|
|
365
397
|
if (!shell && context.adminKey !== void 0 && request.headers.get(ADMIN_KEY_HEADER) !== context.adminKey) {
|
|
366
398
|
return adminError(401, `missing or wrong ${ADMIN_KEY_HEADER}`);
|
|
@@ -368,7 +400,12 @@ var createControlPlane = (context) => {
|
|
|
368
400
|
for (const route of routes) {
|
|
369
401
|
if (route.method !== request.method)
|
|
370
402
|
continue;
|
|
371
|
-
|
|
403
|
+
let params;
|
|
404
|
+
try {
|
|
405
|
+
params = matchRoute(route.pattern, path);
|
|
406
|
+
} catch {
|
|
407
|
+
return adminError(400, "invalid path encoding");
|
|
408
|
+
}
|
|
372
409
|
if (!params)
|
|
373
410
|
continue;
|
|
374
411
|
let body;
|
|
@@ -377,15 +414,18 @@ var createControlPlane = (context) => {
|
|
|
377
414
|
} catch {
|
|
378
415
|
return adminError(400, "request body is not valid JSON");
|
|
379
416
|
}
|
|
417
|
+
const namespace = adminNamespace(request, url);
|
|
418
|
+
if (!/^[A-Za-z0-9_.-]{1,64}$/.test(namespace))
|
|
419
|
+
return adminError(400, "invalid namespace");
|
|
380
420
|
return route.handler({
|
|
381
421
|
request,
|
|
382
422
|
url,
|
|
383
423
|
params,
|
|
384
|
-
namespace
|
|
424
|
+
namespace,
|
|
385
425
|
body
|
|
386
426
|
});
|
|
387
427
|
}
|
|
388
|
-
return adminError(404, `no admin route ${request.method} ${path}; GET ${
|
|
428
|
+
return adminError(404, `no admin route ${request.method} ${path}; GET ${adminPrefix} lists them`);
|
|
389
429
|
}
|
|
390
430
|
};
|
|
391
431
|
};
|
|
@@ -474,7 +514,33 @@ var faultResponse = (rule) => {
|
|
|
474
514
|
};
|
|
475
515
|
var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) => {
|
|
476
516
|
const entries = [];
|
|
517
|
+
let isolated = false;
|
|
518
|
+
const scopedRngs = /* @__PURE__ */ new Map();
|
|
519
|
+
const random = (namespace) => {
|
|
520
|
+
if (!isolated || namespace === "default")
|
|
521
|
+
return rng;
|
|
522
|
+
let scoped = scopedRngs.get(namespace);
|
|
523
|
+
if (!scoped) {
|
|
524
|
+
scoped = createRng(rng.seed);
|
|
525
|
+
scopedRngs.set(namespace, scoped);
|
|
526
|
+
}
|
|
527
|
+
return scoped;
|
|
528
|
+
};
|
|
477
529
|
return {
|
|
530
|
+
isolateNamespaces() {
|
|
531
|
+
isolated = true;
|
|
532
|
+
},
|
|
533
|
+
snapshot(namespace) {
|
|
534
|
+
return {
|
|
535
|
+
rules: structuredClone(entries.map((entry, position) => ({
|
|
536
|
+
...entry.rule,
|
|
537
|
+
remaining: entry.remaining,
|
|
538
|
+
hits: entry.hits,
|
|
539
|
+
position
|
|
540
|
+
})).filter((entry) => entry.namespace === namespace)),
|
|
541
|
+
rngState: random(namespace).state()
|
|
542
|
+
};
|
|
543
|
+
},
|
|
478
544
|
add(rule) {
|
|
479
545
|
const existing = entries.findIndex((e) => e.rule.id === rule.id);
|
|
480
546
|
const entry = { rule, remaining: rule.count ?? null, hits: 0 };
|
|
@@ -494,6 +560,16 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
|
|
|
494
560
|
},
|
|
495
561
|
clear() {
|
|
496
562
|
entries.length = 0;
|
|
563
|
+
rng.reset();
|
|
564
|
+
scopedRngs.clear();
|
|
565
|
+
},
|
|
566
|
+
restore(namespace, state) {
|
|
567
|
+
for (let i = entries.length - 1; i >= 0; i--)
|
|
568
|
+
if (entries[i]?.rule.namespace === namespace)
|
|
569
|
+
entries.splice(i, 1);
|
|
570
|
+
for (const { remaining, hits, position, ...rule } of state.rules)
|
|
571
|
+
entries.splice(Math.min(position, entries.length), 0, { rule, remaining, hits });
|
|
572
|
+
random(namespace).setState(state.rngState);
|
|
497
573
|
},
|
|
498
574
|
async take(candidate) {
|
|
499
575
|
const hits = [];
|
|
@@ -503,7 +579,7 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
|
|
|
503
579
|
if (!matches(entry.rule, candidate))
|
|
504
580
|
continue;
|
|
505
581
|
const rate = entry.rule.rate ?? 1;
|
|
506
|
-
if (
|
|
582
|
+
if (random(candidate.namespace).next() >= rate)
|
|
507
583
|
continue;
|
|
508
584
|
entry.hits++;
|
|
509
585
|
if (entry.remaining !== null)
|
|
@@ -1093,7 +1169,8 @@ var annotateResponse = (response, extra) => {
|
|
|
1093
1169
|
var responseNotes = (response) => notes.get(response);
|
|
1094
1170
|
|
|
1095
1171
|
// ../core/dist/metrics.js
|
|
1096
|
-
var createMetrics = () => {
|
|
1172
|
+
var createMetrics = (partition = true) => {
|
|
1173
|
+
const namespaces = /* @__PURE__ */ new Map();
|
|
1097
1174
|
let requests = 0;
|
|
1098
1175
|
let faults = 0;
|
|
1099
1176
|
let totalDurationMs = 0;
|
|
@@ -1101,6 +1178,14 @@ var createMetrics = () => {
|
|
|
1101
1178
|
const unmatched = /* @__PURE__ */ new Map();
|
|
1102
1179
|
return {
|
|
1103
1180
|
record(entry) {
|
|
1181
|
+
if (partition) {
|
|
1182
|
+
let scoped = namespaces.get(entry.namespace);
|
|
1183
|
+
if (!scoped) {
|
|
1184
|
+
scoped = createMetrics(false);
|
|
1185
|
+
namespaces.set(entry.namespace, scoped);
|
|
1186
|
+
}
|
|
1187
|
+
scoped.record(entry);
|
|
1188
|
+
}
|
|
1104
1189
|
requests++;
|
|
1105
1190
|
totalDurationMs += entry.durationMs;
|
|
1106
1191
|
if (entry.faultId !== void 0)
|
|
@@ -1112,7 +1197,7 @@ var createMetrics = () => {
|
|
|
1112
1197
|
unmatched.set(route, (unmatched.get(route) ?? 0) + 1);
|
|
1113
1198
|
}
|
|
1114
1199
|
},
|
|
1115
|
-
report: () => ({
|
|
1200
|
+
report: (namespace) => namespace !== void 0 ? (namespaces.get(namespace) ?? createMetrics(false)).report() : {
|
|
1116
1201
|
requests,
|
|
1117
1202
|
byOperation: Object.fromEntries([...byOperation].sort(([a], [b]) => a.localeCompare(b))),
|
|
1118
1203
|
unmatched: [...unmatched].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([route, count]) => {
|
|
@@ -1121,8 +1206,13 @@ var createMetrics = () => {
|
|
|
1121
1206
|
}),
|
|
1122
1207
|
faults,
|
|
1123
1208
|
totalDurationMs
|
|
1124
|
-
}
|
|
1125
|
-
reset() {
|
|
1209
|
+
},
|
|
1210
|
+
reset(namespace) {
|
|
1211
|
+
if (namespace !== void 0) {
|
|
1212
|
+
namespaces.delete(namespace);
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
namespaces.clear();
|
|
1126
1216
|
requests = 0;
|
|
1127
1217
|
faults = 0;
|
|
1128
1218
|
totalDurationMs = 0;
|
|
@@ -1735,6 +1825,34 @@ dialog:not([open]) {
|
|
|
1735
1825
|
function isRecord2(value) {
|
|
1736
1826
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1737
1827
|
}
|
|
1828
|
+
function highlightJsonText(text) {
|
|
1829
|
+
const escapeToken = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
1830
|
+
switch (char) {
|
|
1831
|
+
case "&":
|
|
1832
|
+
return "&";
|
|
1833
|
+
case "<":
|
|
1834
|
+
return "<";
|
|
1835
|
+
case ">":
|
|
1836
|
+
return ">";
|
|
1837
|
+
case '"':
|
|
1838
|
+
return """;
|
|
1839
|
+
default:
|
|
1840
|
+
return "'";
|
|
1841
|
+
}
|
|
1842
|
+
});
|
|
1843
|
+
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;
|
|
1844
|
+
let cursor = 0;
|
|
1845
|
+
let html = "";
|
|
1846
|
+
for (const match of text.matchAll(token)) {
|
|
1847
|
+
const index = match.index ?? 0;
|
|
1848
|
+
const value = match[0];
|
|
1849
|
+
html += escapeToken(text.slice(cursor, index));
|
|
1850
|
+
const kind = value.startsWith('"') ? value.trimEnd().endsWith(":") ? "key" : "string" : value === "true" || value === "false" ? "boolean" : value === "null" ? "null" : "number";
|
|
1851
|
+
html += `<span class="json-${kind}">${escapeToken(value)}</span>`;
|
|
1852
|
+
cursor = index + value.length;
|
|
1853
|
+
}
|
|
1854
|
+
return html + escapeToken(text.slice(cursor));
|
|
1855
|
+
}
|
|
1738
1856
|
function mountAdminBrand(doc, host, service, catalog) {
|
|
1739
1857
|
const clip = (value, max) => {
|
|
1740
1858
|
if (typeof value !== "string")
|
|
@@ -2096,14 +2214,16 @@ function bootAdmin(config) {
|
|
|
2096
2214
|
const state = {
|
|
2097
2215
|
namespace: "default",
|
|
2098
2216
|
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
2099
|
-
view: "overview",
|
|
2217
|
+
view: ["overview", "state", "clock", "faults", "journal", "routes"].includes(location.hash.slice(1)) ? location.hash.slice(1) : "overview",
|
|
2100
2218
|
collection: "",
|
|
2101
2219
|
shape: null,
|
|
2102
2220
|
route: null,
|
|
2221
|
+
routes: [],
|
|
2103
2222
|
editing: null
|
|
2104
2223
|
};
|
|
2105
2224
|
const standard = new Set(config.standardRoutes);
|
|
2106
2225
|
const banner = el("banner", HTMLElement);
|
|
2226
|
+
const bannerMessage = el("banner-message", HTMLElement);
|
|
2107
2227
|
const keyInput = el("admin-key", HTMLInputElement);
|
|
2108
2228
|
keyInput.value = state.key;
|
|
2109
2229
|
const params = new URLSearchParams(location.search);
|
|
@@ -2114,18 +2234,21 @@ function bootAdmin(config) {
|
|
|
2114
2234
|
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
2115
2235
|
params.delete("key");
|
|
2116
2236
|
const next = params.toString();
|
|
2117
|
-
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
2237
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : "") + location.hash);
|
|
2118
2238
|
}
|
|
2119
2239
|
const queryNamespace = params.get("namespace");
|
|
2120
2240
|
if (queryNamespace)
|
|
2121
2241
|
state.namespace = queryNamespace;
|
|
2122
2242
|
const showError = (error) => {
|
|
2123
|
-
|
|
2243
|
+
bannerMessage.textContent = error instanceof Error ? error.message : String(error);
|
|
2124
2244
|
banner.classList.add("show");
|
|
2245
|
+
banner.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
2125
2246
|
};
|
|
2126
2247
|
const clearError = () => {
|
|
2127
2248
|
banner.classList.remove("show");
|
|
2249
|
+
bannerMessage.textContent = "";
|
|
2128
2250
|
};
|
|
2251
|
+
el("banner-dismiss", HTMLButtonElement).addEventListener("click", clearError);
|
|
2129
2252
|
const api = async (method, path, body) => {
|
|
2130
2253
|
const headers = { accept: "application/json" };
|
|
2131
2254
|
if (state.key !== "")
|
|
@@ -2169,16 +2292,21 @@ function bootAdmin(config) {
|
|
|
2169
2292
|
namespace: () => state.namespace
|
|
2170
2293
|
};
|
|
2171
2294
|
const isPanelScript = (value) => typeof value === "function";
|
|
2295
|
+
const themeButton = el("theme", HTMLButtonElement);
|
|
2172
2296
|
const setTheme = (theme) => {
|
|
2173
2297
|
if (theme)
|
|
2174
2298
|
document.documentElement.dataset.theme = theme;
|
|
2175
2299
|
else
|
|
2176
2300
|
delete document.documentElement.dataset.theme;
|
|
2301
|
+
const dark = theme ? theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2302
|
+
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>';
|
|
2303
|
+
const label = dark ? "Use light theme" : "Use dark theme";
|
|
2304
|
+
themeButton.setAttribute("aria-label", label);
|
|
2305
|
+
themeButton.title = label;
|
|
2177
2306
|
};
|
|
2178
2307
|
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
2308
|
+
setTheme(storedTheme === "dark" || storedTheme === "light" ? storedTheme : null);
|
|
2309
|
+
themeButton.addEventListener("click", () => {
|
|
2182
2310
|
const current = document.documentElement.dataset.theme;
|
|
2183
2311
|
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2184
2312
|
const next = dark ? "light" : "dark";
|
|
@@ -2197,6 +2325,7 @@ function bootAdmin(config) {
|
|
|
2197
2325
|
continue;
|
|
2198
2326
|
section.hidden = section.id !== `view-${view}`;
|
|
2199
2327
|
}
|
|
2328
|
+
history.replaceState(null, "", `${location.pathname}${location.search}#${encodeURIComponent(view)}`);
|
|
2200
2329
|
};
|
|
2201
2330
|
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
2202
2331
|
const target = event.target;
|
|
@@ -2211,7 +2340,7 @@ function bootAdmin(config) {
|
|
|
2211
2340
|
show(view);
|
|
2212
2341
|
refresh().catch(showError);
|
|
2213
2342
|
});
|
|
2214
|
-
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>`;
|
|
2343
|
+
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>`;
|
|
2215
2344
|
const table = (columns, rows) => {
|
|
2216
2345
|
if (rows.length === 0)
|
|
2217
2346
|
return '<p class="empty">Nothing here yet.</p>';
|
|
@@ -2221,16 +2350,22 @@ function bootAdmin(config) {
|
|
|
2221
2350
|
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
2222
2351
|
return `<tr${attrs}>${cells}</tr>`;
|
|
2223
2352
|
}).join("");
|
|
2224
|
-
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
2353
|
+
return `<div class="table-shell"><table><thead>${head}</thead><tbody>${body}</tbody></table></div>`;
|
|
2225
2354
|
};
|
|
2226
2355
|
const preview = (value) => {
|
|
2227
2356
|
const text = JSON.stringify(value) ?? "null";
|
|
2228
|
-
|
|
2357
|
+
const clipped = text.length > 180 ? `${text.slice(0, 180)}\u2026` : text;
|
|
2358
|
+
return `<code class="json-inline">${highlightJsonText(clipped)}</code>`;
|
|
2229
2359
|
};
|
|
2230
2360
|
const when = (epoch) => {
|
|
2231
2361
|
const date = new Date(epoch);
|
|
2232
2362
|
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
2233
2363
|
};
|
|
2364
|
+
const journalWhen = (value) => {
|
|
2365
|
+
const numeric = value.trim() === "" ? Number.NaN : Number(value);
|
|
2366
|
+
const epoch = Number.isFinite(numeric) ? numeric : Date.parse(value);
|
|
2367
|
+
return Number.isFinite(epoch) ? when(epoch) : value;
|
|
2368
|
+
};
|
|
2234
2369
|
const clockCard = (epoch) => {
|
|
2235
2370
|
const date = new Date(epoch);
|
|
2236
2371
|
if (Number.isNaN(date.getTime()))
|
|
@@ -2350,7 +2485,7 @@ function bootAdmin(config) {
|
|
|
2350
2485
|
};
|
|
2351
2486
|
const renderOverview = async () => {
|
|
2352
2487
|
const [health, shape, clock, journal] = await Promise.all([
|
|
2353
|
-
originGet(
|
|
2488
|
+
originGet(`${config.adminPrefix}/health`),
|
|
2354
2489
|
api("GET", "/state"),
|
|
2355
2490
|
api("GET", "/clock"),
|
|
2356
2491
|
api("GET", "/requests?limit=8")
|
|
@@ -2367,7 +2502,7 @@ function bootAdmin(config) {
|
|
|
2367
2502
|
},
|
|
2368
2503
|
{
|
|
2369
2504
|
k: "Status",
|
|
2370
|
-
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
2505
|
+
v: `<span class="badge ok">${esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")}</span>`
|
|
2371
2506
|
},
|
|
2372
2507
|
{ k: "Namespace", v: esc(state.namespace) },
|
|
2373
2508
|
{ k: "Collections", v: String(decoded.collections.length) },
|
|
@@ -2454,26 +2589,46 @@ function bootAdmin(config) {
|
|
|
2454
2589
|
const renderJournal = async () => {
|
|
2455
2590
|
const journal = await api("GET", "/requests?limit=100");
|
|
2456
2591
|
el("journal", HTMLElement).innerHTML = table([
|
|
2457
|
-
{ label: "When", cell: (row) => esc(row.at) },
|
|
2458
|
-
{
|
|
2592
|
+
{ label: "When", cell: (row) => esc(journalWhen(row.at)) },
|
|
2593
|
+
{
|
|
2594
|
+
label: "Method",
|
|
2595
|
+
cell: (row) => `<span class="method">${esc(row.method)}</span>`
|
|
2596
|
+
},
|
|
2459
2597
|
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2460
|
-
{
|
|
2598
|
+
{
|
|
2599
|
+
label: "Status",
|
|
2600
|
+
cell: (row) => `<span class="badge ${row.status.startsWith("2") ? "ok" : row.status.startsWith("4") || row.status.startsWith("5") ? "warn" : ""}">${esc(row.status)}</span>`
|
|
2601
|
+
},
|
|
2461
2602
|
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
2462
2603
|
], decodeJournal(journal));
|
|
2463
2604
|
};
|
|
2464
|
-
const
|
|
2465
|
-
const
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2605
|
+
const paintRoutes = () => {
|
|
2606
|
+
const query = el("route-search", HTMLInputElement).value.trim().toLocaleLowerCase();
|
|
2607
|
+
const routes = state.routes.filter((route) => route.toLocaleLowerCase().includes(query));
|
|
2608
|
+
el("route-count", HTMLElement).textContent = `${routes.length} of ${state.routes.length}`;
|
|
2609
|
+
if (state.routes.length === 0) {
|
|
2610
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No admin routes are available.</p>';
|
|
2611
|
+
return;
|
|
2612
|
+
}
|
|
2613
|
+
if (routes.length === 0) {
|
|
2614
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No routes match this filter.</p>';
|
|
2615
|
+
return;
|
|
2616
|
+
}
|
|
2469
2617
|
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2470
2618
|
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2471
2619
|
const parts = route.split(" ");
|
|
2472
2620
|
const method = parts[0] ?? "";
|
|
2473
2621
|
const path = parts.slice(1).join(" ");
|
|
2474
|
-
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>`;
|
|
2622
|
+
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>`;
|
|
2475
2623
|
}).join("");
|
|
2476
2624
|
};
|
|
2625
|
+
const renderRoutes = async () => {
|
|
2626
|
+
const data = await api("GET", "/");
|
|
2627
|
+
if (!isRecord2(data))
|
|
2628
|
+
throw new Error("Route list was not an object");
|
|
2629
|
+
state.routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
2630
|
+
paintRoutes();
|
|
2631
|
+
};
|
|
2477
2632
|
const renderers = {
|
|
2478
2633
|
overview: renderOverview,
|
|
2479
2634
|
state: renderState,
|
|
@@ -2482,19 +2637,50 @@ function bootAdmin(config) {
|
|
|
2482
2637
|
journal: renderJournal,
|
|
2483
2638
|
routes: renderRoutes
|
|
2484
2639
|
};
|
|
2640
|
+
let refreshToken = 0;
|
|
2485
2641
|
async function refresh() {
|
|
2642
|
+
const token = ++refreshToken;
|
|
2486
2643
|
clearError();
|
|
2644
|
+
const main = el("views", HTMLElement);
|
|
2645
|
+
const sync = el("sync-status", HTMLElement);
|
|
2646
|
+
main.setAttribute("aria-busy", "true");
|
|
2647
|
+
sync.classList.add("busy");
|
|
2648
|
+
sync.textContent = "Updating";
|
|
2487
2649
|
const render = renderers[state.view];
|
|
2488
|
-
|
|
2489
|
-
|
|
2650
|
+
try {
|
|
2651
|
+
if (render)
|
|
2652
|
+
await render();
|
|
2653
|
+
if (token === refreshToken)
|
|
2654
|
+
sync.textContent = "Up to date";
|
|
2655
|
+
} finally {
|
|
2656
|
+
if (token === refreshToken) {
|
|
2657
|
+
main.removeAttribute("aria-busy");
|
|
2658
|
+
sync.classList.remove("busy");
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2490
2661
|
}
|
|
2491
2662
|
const delay = (ms) => new Promise((resolve) => {
|
|
2492
2663
|
window.setTimeout(resolve, ms);
|
|
2493
2664
|
});
|
|
2494
|
-
const confirmAction = async (button, pending, done, work) => {
|
|
2665
|
+
const confirmAction = async (button, pending, done, work, confirmLabel) => {
|
|
2495
2666
|
if (button.dataset.pending === "1")
|
|
2496
2667
|
return;
|
|
2497
2668
|
const label = button.textContent ?? "";
|
|
2669
|
+
if (confirmLabel !== void 0 && button.dataset.confirming !== "1") {
|
|
2670
|
+
button.dataset.confirming = "1";
|
|
2671
|
+
button.classList.add("confirming");
|
|
2672
|
+
button.textContent = confirmLabel;
|
|
2673
|
+
window.setTimeout(() => {
|
|
2674
|
+
if (button.dataset.confirming !== "1")
|
|
2675
|
+
return;
|
|
2676
|
+
button.textContent = label;
|
|
2677
|
+
button.classList.remove("confirming");
|
|
2678
|
+
delete button.dataset.confirming;
|
|
2679
|
+
}, 3e3);
|
|
2680
|
+
return;
|
|
2681
|
+
}
|
|
2682
|
+
button.classList.remove("confirming");
|
|
2683
|
+
delete button.dataset.confirming;
|
|
2498
2684
|
const width = button.offsetWidth;
|
|
2499
2685
|
button.dataset.pending = "1";
|
|
2500
2686
|
button.classList.add("pressed");
|
|
@@ -2515,6 +2701,65 @@ function bootAdmin(config) {
|
|
|
2515
2701
|
delete button.dataset.pending;
|
|
2516
2702
|
}
|
|
2517
2703
|
};
|
|
2704
|
+
const wireJsonEditor = (textareaId, highlightId, statusId, shellId, allowEmpty) => {
|
|
2705
|
+
const textarea = el(textareaId, HTMLTextAreaElement);
|
|
2706
|
+
const highlight = el(highlightId, HTMLElement);
|
|
2707
|
+
const status = el(statusId, HTMLElement);
|
|
2708
|
+
const shell = el(shellId, HTMLElement);
|
|
2709
|
+
const sync = () => {
|
|
2710
|
+
const text = textarea.value;
|
|
2711
|
+
highlight.innerHTML = `${highlightJsonText(text)}
|
|
2712
|
+
`;
|
|
2713
|
+
let valid = true;
|
|
2714
|
+
let message = "Valid JSON";
|
|
2715
|
+
if (allowEmpty && text.trim() === "")
|
|
2716
|
+
message = "No body";
|
|
2717
|
+
else {
|
|
2718
|
+
try {
|
|
2719
|
+
readJson2(text);
|
|
2720
|
+
} catch {
|
|
2721
|
+
valid = false;
|
|
2722
|
+
message = "Invalid JSON";
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
shell.classList.toggle("invalid", !valid);
|
|
2726
|
+
status.classList.toggle("invalid", !valid);
|
|
2727
|
+
status.textContent = message;
|
|
2728
|
+
textarea.setAttribute("aria-invalid", valid ? "false" : "true");
|
|
2729
|
+
};
|
|
2730
|
+
const syncScroll = () => {
|
|
2731
|
+
const preview2 = highlight.parentElement;
|
|
2732
|
+
if (!(preview2 instanceof HTMLElement))
|
|
2733
|
+
return;
|
|
2734
|
+
preview2.scrollTop = textarea.scrollTop;
|
|
2735
|
+
preview2.scrollLeft = textarea.scrollLeft;
|
|
2736
|
+
};
|
|
2737
|
+
textarea.addEventListener("input", sync);
|
|
2738
|
+
textarea.addEventListener("scroll", syncScroll);
|
|
2739
|
+
sync();
|
|
2740
|
+
return sync;
|
|
2741
|
+
};
|
|
2742
|
+
const syncRouteEditor = wireJsonEditor("route-body", "route-highlight", "route-json-status", "route-editor", true);
|
|
2743
|
+
const syncRecordEditor = wireJsonEditor("editor-value", "editor-highlight", "editor-json-status", "record-editor", false);
|
|
2744
|
+
for (const button of document.querySelectorAll("[data-format]")) {
|
|
2745
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2746
|
+
continue;
|
|
2747
|
+
button.addEventListener("click", () => {
|
|
2748
|
+
const target = button.dataset.format;
|
|
2749
|
+
if (target === void 0)
|
|
2750
|
+
return;
|
|
2751
|
+
const textarea = document.getElementById(target);
|
|
2752
|
+
if (!(textarea instanceof HTMLTextAreaElement) || textarea.value.trim() === "")
|
|
2753
|
+
return;
|
|
2754
|
+
try {
|
|
2755
|
+
textarea.value = JSON.stringify(readJson2(textarea.value), null, 2) ?? "";
|
|
2756
|
+
textarea.dispatchEvent(new Event("input"));
|
|
2757
|
+
textarea.focus();
|
|
2758
|
+
} catch {
|
|
2759
|
+
showError(new Error("Fix the JSON before formatting it"));
|
|
2760
|
+
}
|
|
2761
|
+
});
|
|
2762
|
+
}
|
|
2518
2763
|
document.addEventListener("click", (event) => {
|
|
2519
2764
|
const target = event.target;
|
|
2520
2765
|
if (!(target instanceof Element))
|
|
@@ -2587,6 +2832,7 @@ function bootAdmin(config) {
|
|
|
2587
2832
|
} else {
|
|
2588
2833
|
editorValue.value = "{\n \n}";
|
|
2589
2834
|
}
|
|
2835
|
+
syncRecordEditor();
|
|
2590
2836
|
editor.showModal();
|
|
2591
2837
|
}
|
|
2592
2838
|
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -2616,10 +2862,10 @@ function bootAdmin(config) {
|
|
|
2616
2862
|
const id = state.editing;
|
|
2617
2863
|
if (!id)
|
|
2618
2864
|
return;
|
|
2619
|
-
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2865
|
+
confirmAction(editorDelete, "Deleting...", "Deleted", () => api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2620
2866
|
editor.close();
|
|
2621
2867
|
return renderState();
|
|
2622
|
-
}).catch(showError);
|
|
2868
|
+
}), "Confirm delete").catch(showError);
|
|
2623
2869
|
});
|
|
2624
2870
|
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2625
2871
|
if (!(button instanceof HTMLButtonElement))
|
|
@@ -2665,10 +2911,10 @@ function bootAdmin(config) {
|
|
|
2665
2911
|
}).then(() => renderFaults()).catch(showError);
|
|
2666
2912
|
});
|
|
2667
2913
|
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2668
|
-
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
|
|
2914
|
+
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults()), "Confirm clear").catch(showError);
|
|
2669
2915
|
});
|
|
2670
2916
|
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2671
|
-
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
|
|
2917
|
+
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal()), "Confirm clear").catch(showError);
|
|
2672
2918
|
});
|
|
2673
2919
|
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2674
2920
|
const target = event.target;
|
|
@@ -2682,9 +2928,15 @@ function bootAdmin(config) {
|
|
|
2682
2928
|
return;
|
|
2683
2929
|
state.route = route;
|
|
2684
2930
|
el("route-title", HTMLElement).textContent = route;
|
|
2931
|
+
el("route-send", HTMLButtonElement).disabled = false;
|
|
2685
2932
|
const method = route.split(" ")[0] ?? "";
|
|
2686
|
-
el("route-body", HTMLTextAreaElement)
|
|
2933
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
2934
|
+
routeBody.disabled = method === "GET" || method === "DELETE";
|
|
2935
|
+
el("route-editor", HTMLElement).hidden = routeBody.disabled;
|
|
2936
|
+
el("route-json-status", HTMLElement).parentElement?.toggleAttribute("hidden", routeBody.disabled);
|
|
2937
|
+
syncRouteEditor();
|
|
2687
2938
|
});
|
|
2939
|
+
el("route-search", HTMLInputElement).addEventListener("input", paintRoutes);
|
|
2688
2940
|
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2689
2941
|
event.preventDefault();
|
|
2690
2942
|
const send = async () => {
|
|
@@ -2706,7 +2958,8 @@ function bootAdmin(config) {
|
|
|
2706
2958
|
}
|
|
2707
2959
|
}
|
|
2708
2960
|
const data = await api(method, concrete, body);
|
|
2709
|
-
|
|
2961
|
+
const text = JSON.stringify(data, null, 2) ?? "null";
|
|
2962
|
+
el("route-result", HTMLElement).innerHTML = `<code>${highlightJsonText(text)}</code>`;
|
|
2710
2963
|
};
|
|
2711
2964
|
send().catch(showError);
|
|
2712
2965
|
});
|
|
@@ -2824,11 +3077,13 @@ function bootAdmin(config) {
|
|
|
2824
3077
|
views.append(section);
|
|
2825
3078
|
mountSqlExplorer(body, panelApi);
|
|
2826
3079
|
}
|
|
3080
|
+
show(document.getElementById(`view-${state.view}`) === null ? "overview" : state.view);
|
|
2827
3081
|
return refresh();
|
|
2828
3082
|
}).catch(showError);
|
|
2829
3083
|
}
|
|
2830
3084
|
var clientPrelude = [
|
|
2831
3085
|
isRecord2,
|
|
3086
|
+
highlightJsonText,
|
|
2832
3087
|
mountAdminBrand,
|
|
2833
3088
|
startAdminBrand,
|
|
2834
3089
|
faultPresetList,
|
|
@@ -2851,6 +3106,7 @@ ${entry}(${payload});
|
|
|
2851
3106
|
var defineMock = (runtime) => runtime;
|
|
2852
3107
|
var STANDARD_ADMIN_ROUTES = [
|
|
2853
3108
|
"GET /",
|
|
3109
|
+
"GET /health",
|
|
2854
3110
|
"POST /reset",
|
|
2855
3111
|
"GET /namespaces",
|
|
2856
3112
|
"GET /clock",
|
|
@@ -2930,11 +3186,11 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
2930
3186
|
}
|
|
2931
3187
|
});
|
|
2932
3188
|
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
2933
|
-
var renderAdminDocument = (service) => {
|
|
3189
|
+
var renderAdminDocument = (service, adminPrefix = ADMIN_PREFIX) => {
|
|
2934
3190
|
const name = escapeHtml(service);
|
|
2935
3191
|
const script = adminClientSource({
|
|
2936
3192
|
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
2937
|
-
adminPrefix
|
|
3193
|
+
adminPrefix,
|
|
2938
3194
|
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
2939
3195
|
brandsUrl: ADMIN_BRANDS_URL,
|
|
2940
3196
|
service
|
|
@@ -2951,75 +3207,99 @@ var renderAdminDocument = (service) => {
|
|
|
2951
3207
|
${CSS_RESET}
|
|
2952
3208
|
:root {
|
|
2953
3209
|
color-scheme: light;
|
|
2954
|
-
--bg: #
|
|
3210
|
+
--bg: #f7f8fb;
|
|
2955
3211
|
--bg-elev: #ffffff;
|
|
2956
|
-
--bg-muted: #
|
|
2957
|
-
--bg-inset: #
|
|
2958
|
-
--line: #
|
|
2959
|
-
--line-strong: #
|
|
2960
|
-
--ink: #
|
|
2961
|
-
--muted: #
|
|
2962
|
-
--faint: #
|
|
2963
|
-
--accent: #
|
|
3212
|
+
--bg-muted: #f1f3f7;
|
|
3213
|
+
--bg-inset: #f8f9fc;
|
|
3214
|
+
--line: #e4e7ec;
|
|
3215
|
+
--line-strong: #cfd4dc;
|
|
3216
|
+
--ink: #101828;
|
|
3217
|
+
--muted: #475467;
|
|
3218
|
+
--faint: #667085;
|
|
3219
|
+
--accent: #6941c6;
|
|
3220
|
+
--accent-hover: #5933b4;
|
|
2964
3221
|
--accent-ink: #ffffff;
|
|
2965
|
-
--accent-soft: #
|
|
2966
|
-
--ok: #
|
|
2967
|
-
--ok-soft: #
|
|
2968
|
-
--warn: #
|
|
2969
|
-
--warn-soft: #
|
|
2970
|
-
--err: #
|
|
2971
|
-
--err-soft: #
|
|
2972
|
-
--
|
|
3222
|
+
--accent-soft: #f4f0ff;
|
|
3223
|
+
--ok: #067647;
|
|
3224
|
+
--ok-soft: #ecfdf3;
|
|
3225
|
+
--warn: #b54708;
|
|
3226
|
+
--warn-soft: #fffaeb;
|
|
3227
|
+
--err: #b42318;
|
|
3228
|
+
--err-soft: #fef3f2;
|
|
3229
|
+
--code-key: #6941c6;
|
|
3230
|
+
--code-string: #067647;
|
|
3231
|
+
--code-number: #175cd3;
|
|
3232
|
+
--code-boolean: #c11574;
|
|
3233
|
+
--code-null: #667085;
|
|
3234
|
+
--shadow-xs: 0 1px 2px rgb(16 24 40 / 0.05);
|
|
3235
|
+
--shadow-sm: 0 1px 3px rgb(16 24 40 / 0.08), 0 1px 2px rgb(16 24 40 / 0.04);
|
|
3236
|
+
--shadow-lg: 0 18px 48px rgb(16 24 40 / 0.16), 0 4px 12px rgb(16 24 40 / 0.08);
|
|
2973
3237
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2974
3238
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
2975
|
-
--radius:
|
|
2976
|
-
--header:
|
|
2977
|
-
--nav:
|
|
3239
|
+
--radius: 12px;
|
|
3240
|
+
--header: 72px;
|
|
3241
|
+
--nav: 224px;
|
|
2978
3242
|
}
|
|
2979
3243
|
:root[data-theme="dark"] {
|
|
2980
3244
|
color-scheme: dark;
|
|
2981
|
-
--bg: #
|
|
2982
|
-
--bg-elev: #
|
|
2983
|
-
--bg-muted: #
|
|
2984
|
-
--bg-inset: #
|
|
2985
|
-
--line: #
|
|
2986
|
-
--line-strong: #
|
|
2987
|
-
--ink: #
|
|
2988
|
-
--muted: #
|
|
2989
|
-
--faint: #
|
|
2990
|
-
--accent: #
|
|
2991
|
-
--accent-
|
|
2992
|
-
--accent-
|
|
2993
|
-
--
|
|
2994
|
-
--ok
|
|
2995
|
-
--
|
|
2996
|
-
--warn
|
|
2997
|
-
--
|
|
2998
|
-
--err
|
|
2999
|
-
--
|
|
3245
|
+
--bg: #0c0e14;
|
|
3246
|
+
--bg-elev: #151821;
|
|
3247
|
+
--bg-muted: #20232e;
|
|
3248
|
+
--bg-inset: #11141c;
|
|
3249
|
+
--line: #2b303d;
|
|
3250
|
+
--line-strong: #414858;
|
|
3251
|
+
--ink: #f5f7fa;
|
|
3252
|
+
--muted: #b6bdc9;
|
|
3253
|
+
--faint: #8d96a7;
|
|
3254
|
+
--accent: #9e77ed;
|
|
3255
|
+
--accent-hover: #b692f6;
|
|
3256
|
+
--accent-ink: #ffffff;
|
|
3257
|
+
--accent-soft: #2b2142;
|
|
3258
|
+
--ok: #75e0a7;
|
|
3259
|
+
--ok-soft: #102a20;
|
|
3260
|
+
--warn: #fec84b;
|
|
3261
|
+
--warn-soft: #302611;
|
|
3262
|
+
--err: #fda29b;
|
|
3263
|
+
--err-soft: #321817;
|
|
3264
|
+
--code-key: #c3a6ff;
|
|
3265
|
+
--code-string: #75e0a7;
|
|
3266
|
+
--code-number: #84adff;
|
|
3267
|
+
--code-boolean: #f9a8d4;
|
|
3268
|
+
--code-null: #98a2b3;
|
|
3269
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3270
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3271
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
3000
3272
|
}
|
|
3001
3273
|
@media (prefers-color-scheme: dark) {
|
|
3002
3274
|
:root:not([data-theme="light"]) {
|
|
3003
3275
|
color-scheme: dark;
|
|
3004
|
-
--bg: #
|
|
3005
|
-
--bg-elev: #
|
|
3006
|
-
--bg-muted: #
|
|
3007
|
-
--bg-inset: #
|
|
3008
|
-
--line: #
|
|
3009
|
-
--line-strong: #
|
|
3010
|
-
--ink: #
|
|
3011
|
-
--muted: #
|
|
3012
|
-
--faint: #
|
|
3013
|
-
--accent: #
|
|
3014
|
-
--accent-
|
|
3015
|
-
--accent-
|
|
3016
|
-
--
|
|
3017
|
-
--ok
|
|
3018
|
-
--
|
|
3019
|
-
--warn
|
|
3020
|
-
--
|
|
3021
|
-
--err
|
|
3022
|
-
--
|
|
3276
|
+
--bg: #0c0e14;
|
|
3277
|
+
--bg-elev: #151821;
|
|
3278
|
+
--bg-muted: #20232e;
|
|
3279
|
+
--bg-inset: #11141c;
|
|
3280
|
+
--line: #2b303d;
|
|
3281
|
+
--line-strong: #414858;
|
|
3282
|
+
--ink: #f5f7fa;
|
|
3283
|
+
--muted: #b6bdc9;
|
|
3284
|
+
--faint: #8d96a7;
|
|
3285
|
+
--accent: #9e77ed;
|
|
3286
|
+
--accent-hover: #b692f6;
|
|
3287
|
+
--accent-ink: #ffffff;
|
|
3288
|
+
--accent-soft: #2b2142;
|
|
3289
|
+
--ok: #75e0a7;
|
|
3290
|
+
--ok-soft: #102a20;
|
|
3291
|
+
--warn: #fec84b;
|
|
3292
|
+
--warn-soft: #302611;
|
|
3293
|
+
--err: #fda29b;
|
|
3294
|
+
--err-soft: #321817;
|
|
3295
|
+
--code-key: #c3a6ff;
|
|
3296
|
+
--code-string: #75e0a7;
|
|
3297
|
+
--code-number: #84adff;
|
|
3298
|
+
--code-boolean: #f9a8d4;
|
|
3299
|
+
--code-null: #98a2b3;
|
|
3300
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3301
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3302
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
3023
3303
|
}
|
|
3024
3304
|
}
|
|
3025
3305
|
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
@@ -3037,13 +3317,17 @@ var renderAdminDocument = (service) => {
|
|
|
3037
3317
|
padding-right: env(safe-area-inset-right);
|
|
3038
3318
|
-webkit-text-size-adjust: 100%;
|
|
3039
3319
|
}
|
|
3040
|
-
|
|
3320
|
+
::selection { background: color-mix(in srgb, var(--accent) 28%, transparent); }
|
|
3321
|
+
:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px; }
|
|
3322
|
+
button, input, select, textarea { transition: border-color 140ms ease, box-shadow 140ms ease, transform 100ms ease; }
|
|
3323
|
+
button:disabled, input:disabled, select:disabled, textarea:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
3041
3324
|
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
3042
3325
|
header.top {
|
|
3043
|
-
position: sticky; top: 0; z-index:
|
|
3326
|
+
position: sticky; top: 0; z-index: 20;
|
|
3044
3327
|
display: flex; align-items: center; gap: 12px;
|
|
3045
|
-
min-height: var(--header); padding:
|
|
3046
|
-
background: var(--bg-elev);
|
|
3328
|
+
min-height: var(--header); padding: 12px 20px;
|
|
3329
|
+
background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
|
|
3330
|
+
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
3047
3331
|
border-bottom: 1px solid var(--line);
|
|
3048
3332
|
min-width: 0; max-width: 100%;
|
|
3049
3333
|
}
|
|
@@ -3051,12 +3335,21 @@ var renderAdminDocument = (service) => {
|
|
|
3051
3335
|
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
3052
3336
|
min-width: 0; max-width: 100%; margin-right: auto;
|
|
3053
3337
|
}
|
|
3054
|
-
.brand-
|
|
3055
|
-
.brand-
|
|
3338
|
+
.brand-lockup { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
3339
|
+
.brand-mark {
|
|
3340
|
+
width: 38px; height: 38px; display: grid; place-items: center; flex: none;
|
|
3341
|
+
border-radius: 11px; color: #fff;
|
|
3342
|
+
background: linear-gradient(145deg, #7f56d9 0%, #53389e 100%);
|
|
3343
|
+
box-shadow: 0 6px 16px rgb(105 65 198 / 0.24), inset 0 1px 0 rgb(255 255 255 / 0.25);
|
|
3344
|
+
}
|
|
3345
|
+
.brand-mark svg { width: 22px; height: 22px; }
|
|
3346
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; line-height: 1.25; }
|
|
3347
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
3056
3348
|
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
3057
3349
|
.vendor {
|
|
3058
3350
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
3059
3351
|
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
3352
|
+
padding: 5px 8px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-inset);
|
|
3060
3353
|
}
|
|
3061
3354
|
.vendor[hidden] { display: none; }
|
|
3062
3355
|
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
@@ -3068,12 +3361,17 @@ var renderAdminDocument = (service) => {
|
|
|
3068
3361
|
.vendor-sep { color: var(--faint); }
|
|
3069
3362
|
.vendor-link { color: inherit; text-decoration: none; }
|
|
3070
3363
|
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
3071
|
-
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items:
|
|
3072
|
-
|
|
3364
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
|
|
3365
|
+
.sync-status { align-self: center; display: flex; align-items: center; gap: 6px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3366
|
+
.sync-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
|
|
3367
|
+
.sync-status.busy::before { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); animation: pulse 1.2s ease-in-out infinite; }
|
|
3368
|
+
label.field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; font-weight: 600; color: var(--faint); }
|
|
3073
3369
|
label.field input, label.field select, .controls input[type="password"] {
|
|
3074
|
-
background-color: var(--bg-
|
|
3075
|
-
padding:
|
|
3370
|
+
background-color: var(--bg-elev); border: 1px solid var(--line-strong); border-radius: 9px;
|
|
3371
|
+
padding: 8px 10px; min-height: 38px; line-height: 1.2; box-shadow: var(--shadow-xs);
|
|
3076
3372
|
}
|
|
3373
|
+
label.field input:hover, label.field select:hover, textarea:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--line-strong)); }
|
|
3374
|
+
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; }
|
|
3077
3375
|
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
3078
3376
|
the text fields in the same form. Paint it like those fields. */
|
|
3079
3377
|
label.field select {
|
|
@@ -3090,12 +3388,20 @@ var renderAdminDocument = (service) => {
|
|
|
3090
3388
|
dialog .field > textarea { width: 100%; }
|
|
3091
3389
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
3092
3390
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
.btn
|
|
3391
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
|
3392
|
+
border-radius: 9px; min-height: 38px; padding: 0 13px; border: 1px solid var(--line-strong);
|
|
3393
|
+
background: var(--bg-elev); color: var(--ink); font-size: 13px; font-weight: 600; line-height: 1;
|
|
3394
|
+
box-shadow: var(--shadow-xs);
|
|
3395
|
+
}
|
|
3396
|
+
.icon-btn { width: 38px; padding: 0; }
|
|
3397
|
+
.icon-btn svg, .btn svg, .btn-primary svg { width: 16px; height: 16px; }
|
|
3398
|
+
.icon-btn:hover, .btn:hover { background: var(--bg-muted); border-color: var(--line-strong); }
|
|
3399
|
+
.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; }
|
|
3400
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
3401
|
+
.btn-quiet { background: transparent; border-color: transparent; box-shadow: none; }
|
|
3402
|
+
.btn-quiet:hover { background: var(--bg-muted); }
|
|
3403
|
+
.btn-danger { color: var(--err); border-color: color-mix(in srgb, var(--err) 25%, var(--line)); }
|
|
3404
|
+
.btn-danger:hover, .btn-danger.confirming { background: var(--err-soft); border-color: color-mix(in srgb, var(--err) 45%, var(--line)); }
|
|
3099
3405
|
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
3100
3406
|
so the press itself has to stay visible. */
|
|
3101
3407
|
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
@@ -3111,93 +3417,159 @@ var renderAdminDocument = (service) => {
|
|
|
3111
3417
|
flex: 1 0 auto; min-width: 0;
|
|
3112
3418
|
}
|
|
3113
3419
|
nav.side {
|
|
3114
|
-
|
|
3420
|
+
position: sticky; top: var(--header); align-self: start; height: calc(100vh - var(--header));
|
|
3421
|
+
border-right: 1px solid var(--line); padding: 18px 12px;
|
|
3115
3422
|
display: flex; flex-direction: column; gap: 4px;
|
|
3116
|
-
background: var(--bg-elev);
|
|
3423
|
+
background: var(--bg-elev); overflow-y: auto;
|
|
3117
3424
|
}
|
|
3425
|
+
.nav-label { padding: 0 10px 8px; color: var(--faint); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
|
|
3118
3426
|
nav.side button {
|
|
3427
|
+
position: relative; display: flex; align-items: center; gap: 10px; width: 100%;
|
|
3119
3428
|
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
3120
|
-
padding: 9px 10px; min-height:
|
|
3121
|
-
}
|
|
3122
|
-
nav.side button
|
|
3123
|
-
|
|
3124
|
-
.
|
|
3125
|
-
.
|
|
3126
|
-
.
|
|
3429
|
+
padding: 9px 10px; min-height: 42px; color: var(--muted); font-size: 13px; font-weight: 550;
|
|
3430
|
+
}
|
|
3431
|
+
nav.side button:hover { background: var(--bg-muted); color: var(--ink); }
|
|
3432
|
+
nav.side button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 650; }
|
|
3433
|
+
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); }
|
|
3434
|
+
.nav-icon { width: 18px; height: 18px; display: grid; place-items: center; flex: none; }
|
|
3435
|
+
.nav-icon svg { width: 18px; height: 18px; }
|
|
3436
|
+
main { padding: 32px clamp(20px, 4vw, 48px) 64px; max-width: 1320px; width: 100%; min-width: 0; }
|
|
3437
|
+
main[aria-busy="true"] .view { opacity: 0.62; }
|
|
3438
|
+
.view { animation: view-in 180ms ease-out; }
|
|
3439
|
+
.view[hidden] { display: none; }
|
|
3440
|
+
.view-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
|
|
3441
|
+
.view h2 { margin: 0 0 5px; font-size: clamp(24px, 3vw, 30px); line-height: 1.2; letter-spacing: -0.035em; }
|
|
3442
|
+
.view h3 { margin: 24px 0 10px; font-size: 15px; letter-spacing: -0.01em; }
|
|
3443
|
+
.lede { margin: 0; color: var(--muted); max-width: 72ch; font-size: 14px; }
|
|
3444
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 175px), 1fr)); gap: 14px; }
|
|
3127
3445
|
.card {
|
|
3128
3446
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
3129
|
-
padding:
|
|
3447
|
+
padding: 18px; box-shadow: var(--shadow-sm);
|
|
3130
3448
|
}
|
|
3131
|
-
.card
|
|
3132
|
-
.card
|
|
3449
|
+
.stat-card { position: relative; overflow: hidden; }
|
|
3450
|
+
.stat-card::after { content: ""; position: absolute; inset: 0 0 auto; height: 2px; background: linear-gradient(90deg, var(--accent), transparent 78%); opacity: 0.7; }
|
|
3451
|
+
.card .k { color: var(--faint); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
3452
|
+
.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; }
|
|
3133
3453
|
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
3134
|
-
.
|
|
3454
|
+
.section-card { background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-xs); overflow: hidden; }
|
|
3455
|
+
.section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
|
|
3456
|
+
.section-title h3 { margin: 0; }
|
|
3457
|
+
.split { display: grid; grid-template-columns: minmax(0, 250px) minmax(0, 1fr); gap: 18px; align-items: start; }
|
|
3458
|
+
#collections { padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); box-shadow: var(--shadow-xs); }
|
|
3135
3459
|
.collection {
|
|
3136
3460
|
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
3137
|
-
border-radius:
|
|
3461
|
+
border-radius: 9px; padding: 10px 11px; min-height: 48px; color: var(--ink); cursor: pointer;
|
|
3138
3462
|
}
|
|
3139
|
-
.collection
|
|
3463
|
+
.collection:hover { background: var(--bg-muted); }
|
|
3464
|
+
.collection[aria-current="true"] { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 18%, var(--line)); color: var(--accent); }
|
|
3140
3465
|
.collection small { display: block; color: var(--faint); }
|
|
3141
3466
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
3142
|
-
.chip {
|
|
3143
|
-
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
3144
|
-
border-radius: 999px; padding: 4px 8px;
|
|
3145
|
-
}
|
|
3467
|
+
.chip { font-family: var(--mono); font-size: 11px; background: var(--bg-muted); border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; }
|
|
3146
3468
|
.chip i { color: var(--faint); font-style: normal; }
|
|
3147
3469
|
.sql-layout {
|
|
3148
3470
|
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
3149
3471
|
}
|
|
3150
|
-
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
3151
|
-
.sql-tables button { text-align: left; }
|
|
3472
|
+
.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); }
|
|
3473
|
+
.sql-tables button { text-align: left; border: 0; border-radius: 8px; padding: 9px 10px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
3474
|
+
.sql-tables button:hover, .sql-tables button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); }
|
|
3152
3475
|
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
3153
|
-
table { width: 100%; border-
|
|
3154
|
-
|
|
3155
|
-
th {
|
|
3476
|
+
.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); }
|
|
3477
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
3478
|
+
th, td { text-align: left; padding: 11px 13px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
3479
|
+
th { color: var(--faint); background: var(--bg-inset); font-weight: 650; font-size: 11px; text-transform: uppercase; letter-spacing: 0.045em; }
|
|
3480
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
3156
3481
|
tr.clickable { cursor: pointer; }
|
|
3157
|
-
tr.clickable:hover { background: var(--
|
|
3482
|
+
tr.clickable:hover { background: var(--accent-soft); }
|
|
3158
3483
|
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
3159
3484
|
pre {
|
|
3160
3485
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
3161
|
-
background: var(--bg-inset); border-radius: 10px; padding:
|
|
3162
|
-
}
|
|
3163
|
-
.
|
|
3486
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 10px; padding: 14px; font-size: 12px; line-height: 1.65;
|
|
3487
|
+
}
|
|
3488
|
+
.json-key { color: var(--code-key); }
|
|
3489
|
+
.json-string { color: var(--code-string); }
|
|
3490
|
+
.json-number { color: var(--code-number); }
|
|
3491
|
+
.json-boolean { color: var(--code-boolean); }
|
|
3492
|
+
.json-null { color: var(--code-null); font-style: italic; }
|
|
3493
|
+
.json-inline { display: block; max-width: 44rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
3494
|
+
.json-view { position: relative; max-height: 26rem; overflow: auto; }
|
|
3495
|
+
.json-editor { position: relative; min-height: 190px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--bg-inset); overflow: hidden; }
|
|
3496
|
+
.json-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); }
|
|
3497
|
+
.json-editor.invalid { border-color: var(--err); }
|
|
3498
|
+
.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; }
|
|
3499
|
+
.json-editor pre { position: absolute; inset: 0; pointer-events: none; background: transparent; }
|
|
3500
|
+
.json-editor textarea { position: relative; z-index: 1; resize: vertical; background: transparent; color: transparent; caret-color: var(--ink); -webkit-text-fill-color: transparent; }
|
|
3501
|
+
.json-editor textarea::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); }
|
|
3502
|
+
.editor-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; }
|
|
3503
|
+
.json-status { display: inline-flex; align-items: center; gap: 6px; color: var(--ok); font-size: 11px; }
|
|
3504
|
+
.json-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
3505
|
+
.json-status.invalid { color: var(--err); }
|
|
3506
|
+
.format-json { border: 0; padding: 3px 5px; background: transparent; color: var(--accent); font-size: 11px; font-weight: 650; cursor: pointer; }
|
|
3507
|
+
.row-actions { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 14px 0; }
|
|
3164
3508
|
.banner {
|
|
3165
|
-
display: none;
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3509
|
+
display: none; position: sticky; top: calc(var(--header) + 12px); z-index: 15;
|
|
3510
|
+
align-items: center; gap: 10px; margin: 0 0 16px; padding: 11px 12px 11px 14px;
|
|
3511
|
+
border: 1px solid color-mix(in srgb, var(--err) 30%, var(--line)); border-radius: 10px;
|
|
3512
|
+
background: var(--err-soft); color: var(--err); box-shadow: var(--shadow-sm); font-size: 13px;
|
|
3513
|
+
}
|
|
3514
|
+
.banner.show { display: flex; }
|
|
3515
|
+
.banner span { flex: 1; }
|
|
3516
|
+
.banner button { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 7px; background: transparent; color: currentColor; cursor: pointer; }
|
|
3517
|
+
.banner button:hover { background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
3518
|
+
.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); }
|
|
3170
3519
|
dialog {
|
|
3171
|
-
border: 1px solid var(--line); border-radius:
|
|
3172
|
-
color: var(--ink); width: min(
|
|
3173
|
-
}
|
|
3174
|
-
dialog
|
|
3175
|
-
dialog
|
|
3520
|
+
border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--bg-elev);
|
|
3521
|
+
color: var(--ink); width: min(720px, calc(100% - 24px)); max-width: calc(100vw - 24px); box-shadow: var(--shadow-lg);
|
|
3522
|
+
}
|
|
3523
|
+
dialog[open] { animation: dialog-in 160ms ease-out; }
|
|
3524
|
+
dialog::backdrop { background: rgb(12 14 20 / 0.58); backdrop-filter: blur(3px); }
|
|
3525
|
+
dialog form, .dialog-body { padding: 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
3526
|
+
.dialog-heading { display: flex; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
|
3527
|
+
.dialog-heading strong { font-size: 18px; letter-spacing: -0.02em; }
|
|
3528
|
+
.dialog-heading span { color: var(--muted); font-size: 12px; }
|
|
3176
3529
|
textarea {
|
|
3177
3530
|
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
3178
3531
|
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
3179
3532
|
font-family: var(--mono); font-size: 12px;
|
|
3180
3533
|
}
|
|
3181
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
3182
|
-
.
|
|
3534
|
+
.dialog-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding-top: 4px; }
|
|
3535
|
+
.dialog-actions .btn-danger { margin-right: auto; }
|
|
3536
|
+
.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); }
|
|
3183
3537
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
3184
3538
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
3185
3539
|
.status-2 { color: var(--ok); }
|
|
3186
3540
|
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
3187
3541
|
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
3542
|
+
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
3543
|
+
.form-grid .wide { grid-column: span 2; }
|
|
3188
3544
|
.route {
|
|
3189
|
-
display: grid; grid-template-columns:
|
|
3190
|
-
padding:
|
|
3191
|
-
}
|
|
3192
|
-
.
|
|
3545
|
+
display: grid; grid-template-columns: 84px minmax(0, 1fr) auto; gap: 10px; align-items: center;
|
|
3546
|
+
padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--bg-elev);
|
|
3547
|
+
}
|
|
3548
|
+
.route:first-child { border-radius: var(--radius) var(--radius) 0 0; }
|
|
3549
|
+
.route:last-child { border-bottom: 0; border-radius: 0 0 var(--radius) var(--radius); }
|
|
3550
|
+
.route-tools { display: flex; align-items: flex-end; gap: 12px; margin-bottom: 12px; }
|
|
3551
|
+
.route-tools .field { flex: 1; }
|
|
3552
|
+
.route-tools input { width: 100%; }
|
|
3553
|
+
.route-count { padding-bottom: 10px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3554
|
+
#route-list { max-height: 34rem; border: 1px solid var(--line); border-radius: var(--radius); overflow: auto; box-shadow: var(--shadow-xs); }
|
|
3555
|
+
.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; }
|
|
3556
|
+
@keyframes view-in { from { transform: translateY(3px); } to { transform: translateY(0); } }
|
|
3557
|
+
@keyframes dialog-in { from { transform: translateY(8px) scale(0.99); } to { transform: translateY(0) scale(1); } }
|
|
3558
|
+
@keyframes pulse { 50% { opacity: 0.45; } }
|
|
3559
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
|
|
3560
|
+
@media (forced-colors: active) { .json-editor pre { display: none; } .json-editor textarea { color: CanvasText; -webkit-text-fill-color: CanvasText; } }
|
|
3193
3561
|
@media (max-width: 800px) {
|
|
3562
|
+
:root { --header: 0px; }
|
|
3194
3563
|
header.top {
|
|
3195
|
-
align-items: stretch; flex-direction: column;
|
|
3196
|
-
padding
|
|
3564
|
+
position: relative; align-items: stretch; flex-direction: column;
|
|
3565
|
+
padding: max(12px, env(safe-area-inset-top)) 14px 12px;
|
|
3197
3566
|
}
|
|
3198
3567
|
input, select, textarea { font-size: 16px; }
|
|
3199
3568
|
.brand, .vendor, .controls { width: 100%; }
|
|
3569
|
+
.brand { gap: 10px; }
|
|
3570
|
+
.vendor { order: 2; border-radius: 9px; }
|
|
3200
3571
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
3572
|
+
.sync-status { display: none; }
|
|
3201
3573
|
/* The layout fills the screen. A single-column grid stretches every auto
|
|
3202
3574
|
row, so the tab bar grew with the leftover height and the buttons floated
|
|
3203
3575
|
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
@@ -3207,14 +3579,23 @@ var renderAdminDocument = (service) => {
|
|
|
3207
3579
|
align-content: start;
|
|
3208
3580
|
}
|
|
3209
3581
|
nav.side {
|
|
3210
|
-
align-self: start; height: auto;
|
|
3582
|
+
position: sticky; top: 0; z-index: 12; align-self: start; height: auto;
|
|
3211
3583
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
3212
3584
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
3213
|
-
padding: 8px;
|
|
3585
|
+
padding: 8px; box-shadow: var(--shadow-xs);
|
|
3214
3586
|
}
|
|
3215
|
-
nav
|
|
3587
|
+
.nav-label { display: none; }
|
|
3588
|
+
nav.side button { width: auto; white-space: nowrap; flex: none; }
|
|
3589
|
+
nav.side button[aria-current="true"]::before { inset: auto 9px -8px; width: auto; height: 3px; border-radius: 3px 3px 0 0; }
|
|
3216
3590
|
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
3217
|
-
|
|
3591
|
+
.form-grid { grid-template-columns: minmax(0, 1fr); }
|
|
3592
|
+
.form-grid .wide { grid-column: auto; }
|
|
3593
|
+
main { padding: 22px 14px 48px; }
|
|
3594
|
+
.view-head { margin-bottom: 18px; }
|
|
3595
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
3596
|
+
.card { padding: 14px; }
|
|
3597
|
+
.card .v { font-size: 20px; }
|
|
3598
|
+
.table-shell { border: 0; background: transparent; box-shadow: none; overflow: visible; }
|
|
3218
3599
|
table, thead, tbody, tr, th, td { display: block; }
|
|
3219
3600
|
thead { display: none; }
|
|
3220
3601
|
tr {
|
|
@@ -3229,50 +3610,63 @@ var renderAdminDocument = (service) => {
|
|
|
3229
3610
|
.route { grid-template-columns: 72px 1fr; }
|
|
3230
3611
|
.route button { grid-column: 1 / -1; }
|
|
3231
3612
|
}
|
|
3613
|
+
@media (max-width: 460px) {
|
|
3614
|
+
.controls label.field { min-width: calc(50% - 4px); }
|
|
3615
|
+
.controls .icon-btn, .controls .btn { flex: 1; }
|
|
3616
|
+
.grid { grid-template-columns: minmax(0, 1fr); }
|
|
3617
|
+
.dialog-actions { flex-wrap: wrap; }
|
|
3618
|
+
.dialog-actions button { flex: 1; }
|
|
3619
|
+
.dialog-actions .btn-danger { flex-basis: 100%; margin-right: 0; }
|
|
3620
|
+
}
|
|
3232
3621
|
</style>
|
|
3233
3622
|
</head>
|
|
3234
3623
|
<body>
|
|
3235
3624
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
3236
3625
|
<header class="top">
|
|
3237
3626
|
<div class="brand">
|
|
3238
|
-
<div class="brand-
|
|
3239
|
-
<
|
|
3240
|
-
|
|
3627
|
+
<div class="brand-lockup">
|
|
3628
|
+
<div class="brand-mark" aria-hidden="true">
|
|
3629
|
+
<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>
|
|
3630
|
+
</div>
|
|
3631
|
+
<div class="brand-id">
|
|
3632
|
+
<strong>${name}</strong>
|
|
3633
|
+
<span>Mock admin</span>
|
|
3634
|
+
</div>
|
|
3241
3635
|
</div>
|
|
3242
3636
|
<div class="vendor" id="vendor" hidden></div>
|
|
3243
3637
|
</div>
|
|
3244
3638
|
<div class="controls">
|
|
3639
|
+
<span class="sync-status" id="sync-status" role="status" aria-live="polite">Ready</span>
|
|
3245
3640
|
<label class="field">Namespace
|
|
3246
3641
|
<select id="namespace" aria-label="Namespace"></select>
|
|
3247
3642
|
</label>
|
|
3248
3643
|
<label class="field">Admin key
|
|
3249
3644
|
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
3250
3645
|
</label>
|
|
3251
|
-
<button class="icon-btn" id="theme" type="button" aria-label="
|
|
3252
|
-
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
3646
|
+
<button class="icon-btn" id="theme" type="button" aria-label="Use dark theme" title="Use dark theme"></button>
|
|
3647
|
+
<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>
|
|
3253
3648
|
</div>
|
|
3254
3649
|
</header>
|
|
3255
3650
|
<div class="layout">
|
|
3256
3651
|
<nav class="side" id="nav" aria-label="Admin sections">
|
|
3257
|
-
<
|
|
3258
|
-
<button type="button" data-view="
|
|
3259
|
-
<button type="button" data-view="
|
|
3260
|
-
<button type="button" data-view="
|
|
3261
|
-
<button type="button" data-view="
|
|
3262
|
-
<button type="button" data-view="
|
|
3652
|
+
<span class="nav-label">Workspace</span>
|
|
3653
|
+
<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>
|
|
3654
|
+
<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>
|
|
3655
|
+
<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>
|
|
3656
|
+
<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>
|
|
3657
|
+
<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>
|
|
3658
|
+
<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>
|
|
3263
3659
|
</nav>
|
|
3264
3660
|
<main id="views">
|
|
3265
|
-
<
|
|
3661
|
+
<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>
|
|
3266
3662
|
<section class="view" id="view-overview">
|
|
3267
|
-
<h2>Overview</h2>
|
|
3268
|
-
<p class="lede">Health, clock, and the collections this namespace is holding.</p>
|
|
3663
|
+
<div class="view-head"><div><h2>Overview</h2><p class="lede">Health, clock, and the collections this namespace is holding.</p></div></div>
|
|
3269
3664
|
<div class="grid" id="overview-cards"></div>
|
|
3270
3665
|
<h3>Recent requests</h3>
|
|
3271
3666
|
<div id="overview-requests"></div>
|
|
3272
3667
|
</section>
|
|
3273
3668
|
<section class="view" id="view-state" hidden>
|
|
3274
|
-
<h2>State</h2>
|
|
3275
|
-
<p class="lede">Every collection in this namespace. Declared fields stay visible before any row exists; stored rows add whatever else they contain.</p>
|
|
3669
|
+
<div class="view-head"><div><h2>State</h2><p class="lede">Inspect and edit every collection in this namespace.</p></div></div>
|
|
3276
3670
|
<div class="split">
|
|
3277
3671
|
<div id="collections"></div>
|
|
3278
3672
|
<div>
|
|
@@ -3285,8 +3679,7 @@ var renderAdminDocument = (service) => {
|
|
|
3285
3679
|
</div>
|
|
3286
3680
|
</section>
|
|
3287
3681
|
<section class="view" id="view-clock" hidden>
|
|
3288
|
-
<h2>Clock</h2>
|
|
3289
|
-
<p class="lede">The clock every timestamp in this mock reads.</p>
|
|
3682
|
+
<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>
|
|
3290
3683
|
<div class="card" id="clock-readout"></div>
|
|
3291
3684
|
<div class="row-actions">
|
|
3292
3685
|
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
@@ -3305,21 +3698,22 @@ var renderAdminDocument = (service) => {
|
|
|
3305
3698
|
</form>
|
|
3306
3699
|
</section>
|
|
3307
3700
|
<section class="view" id="view-faults" hidden>
|
|
3308
|
-
<h2>Faults</h2>
|
|
3309
|
-
<p class="lede">Inject a response, a delay, or a named preset. Rules apply to this namespace.</p>
|
|
3701
|
+
<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>
|
|
3310
3702
|
<form id="fault-form" class="card stack">
|
|
3311
|
-
<
|
|
3312
|
-
<
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
<
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
<
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
<
|
|
3322
|
-
|
|
3703
|
+
<div class="form-grid">
|
|
3704
|
+
<label class="field wide">Preset
|
|
3705
|
+
<select id="fault-preset"><option value="">Custom rule</option></select>
|
|
3706
|
+
</label>
|
|
3707
|
+
<label class="field">Status
|
|
3708
|
+
<input id="fault-status" inputmode="numeric" placeholder="503">
|
|
3709
|
+
</label>
|
|
3710
|
+
<label class="field wide">Path prefix
|
|
3711
|
+
<input id="fault-path" placeholder="/v1">
|
|
3712
|
+
</label>
|
|
3713
|
+
<label class="field">Method
|
|
3714
|
+
<input id="fault-method" placeholder="GET">
|
|
3715
|
+
</label>
|
|
3716
|
+
</div>
|
|
3323
3717
|
<div class="dialog-actions">
|
|
3324
3718
|
<button class="btn-primary" type="submit">Add fault</button>
|
|
3325
3719
|
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
@@ -3328,26 +3722,31 @@ var renderAdminDocument = (service) => {
|
|
|
3328
3722
|
<div id="fault-list"></div>
|
|
3329
3723
|
</section>
|
|
3330
3724
|
<section class="view" id="view-journal" hidden>
|
|
3331
|
-
<h2>Journal</h2>
|
|
3332
|
-
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
3333
|
-
<div class="row-actions">
|
|
3334
|
-
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
3335
|
-
</div>
|
|
3725
|
+
<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>
|
|
3336
3726
|
<div id="journal"></div>
|
|
3337
3727
|
</section>
|
|
3338
3728
|
<section class="view" id="view-routes" hidden>
|
|
3339
|
-
<h2>Routes</h2>
|
|
3340
|
-
<
|
|
3729
|
+
<div class="view-head"><div><h2>Routes</h2><p class="lede">Explore shared controls and service-specific admin endpoints.</p></div></div>
|
|
3730
|
+
<div class="route-tools">
|
|
3731
|
+
<label class="field">Filter routes
|
|
3732
|
+
<input id="route-search" type="search" placeholder="Method, path, or extension" autocomplete="off">
|
|
3733
|
+
</label>
|
|
3734
|
+
<span class="route-count" id="route-count"></span>
|
|
3735
|
+
</div>
|
|
3341
3736
|
<div id="route-list"></div>
|
|
3342
|
-
<form id="route-form" class="card stack">
|
|
3737
|
+
<form id="route-form" class="card stack" style="margin-top: 18px">
|
|
3343
3738
|
<strong id="route-title">Select a route</strong>
|
|
3344
3739
|
<label class="field">JSON body
|
|
3345
|
-
<
|
|
3740
|
+
<div class="json-editor" id="route-editor" hidden>
|
|
3741
|
+
<pre aria-hidden="true"><code id="route-highlight"></code></pre>
|
|
3742
|
+
<textarea id="route-body" placeholder="{ }" spellcheck="false" aria-describedby="route-json-status" disabled></textarea>
|
|
3743
|
+
</div>
|
|
3744
|
+
<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>
|
|
3346
3745
|
</label>
|
|
3347
3746
|
<div class="dialog-actions">
|
|
3348
|
-
<button class="btn-primary" type="submit">Send</button>
|
|
3747
|
+
<button class="btn-primary" id="route-send" type="submit" disabled>Send request</button>
|
|
3349
3748
|
</div>
|
|
3350
|
-
<pre id="route-result">Response will show here.</pre>
|
|
3749
|
+
<pre class="json-view" id="route-result"><code>Response will show here.</code></pre>
|
|
3351
3750
|
</form>
|
|
3352
3751
|
</section>
|
|
3353
3752
|
</main>
|
|
@@ -3355,12 +3754,16 @@ var renderAdminDocument = (service) => {
|
|
|
3355
3754
|
</div>
|
|
3356
3755
|
<dialog id="editor">
|
|
3357
3756
|
<form method="dialog" id="editor-form">
|
|
3358
|
-
<strong id="editor-title">Record</strong>
|
|
3757
|
+
<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>
|
|
3359
3758
|
<label class="field">Id
|
|
3360
3759
|
<input id="editor-id" required>
|
|
3361
3760
|
</label>
|
|
3362
3761
|
<label class="field">JSON value
|
|
3363
|
-
<
|
|
3762
|
+
<div class="json-editor" id="record-editor">
|
|
3763
|
+
<pre aria-hidden="true"><code id="editor-highlight"></code></pre>
|
|
3764
|
+
<textarea id="editor-value" spellcheck="false" aria-describedby="editor-json-status"></textarea>
|
|
3765
|
+
</div>
|
|
3766
|
+
<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>
|
|
3364
3767
|
</label>
|
|
3365
3768
|
<div class="dialog-actions">
|
|
3366
3769
|
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
@@ -3402,8 +3805,8 @@ var describeExtension = (extension) => {
|
|
|
3402
3805
|
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
3403
3806
|
};
|
|
3404
3807
|
};
|
|
3405
|
-
var adminUiRoutes = (service, ui) => {
|
|
3406
|
-
const shell = () => renderAdminDocument(service);
|
|
3808
|
+
var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
3809
|
+
const shell = () => renderAdminDocument(service, adminPrefix);
|
|
3407
3810
|
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
3408
3811
|
return {
|
|
3409
3812
|
"GET /ui": () => htmlResponse(document3()),
|
|
@@ -4094,7 +4497,7 @@ var stateAdminRoutes = (routes) => {
|
|
|
4094
4497
|
};
|
|
4095
4498
|
|
|
4096
4499
|
// ../core/dist/version.js
|
|
4097
|
-
var PACKAGE_VERSION = true ? "
|
|
4500
|
+
var PACKAGE_VERSION = true ? "2.0.0" : UNRELEASED_VERSION;
|
|
4098
4501
|
|
|
4099
4502
|
// ../core/dist/signing.js
|
|
4100
4503
|
var encoder = new TextEncoder();
|
|
@@ -4187,7 +4590,9 @@ var createWebhookHub = (options) => {
|
|
|
4187
4590
|
const faults = /* @__PURE__ */ new Map();
|
|
4188
4591
|
const held = /* @__PURE__ */ new Map();
|
|
4189
4592
|
const inFlight = /* @__PURE__ */ new Set();
|
|
4593
|
+
const controllers = /* @__PURE__ */ new Map();
|
|
4190
4594
|
let immediate = Promise.resolve();
|
|
4595
|
+
const snapshots = /* @__PURE__ */ new WeakMap();
|
|
4191
4596
|
const track = (work) => {
|
|
4192
4597
|
inFlight.add(work);
|
|
4193
4598
|
void work.finally(() => inFlight.delete(work));
|
|
@@ -4208,6 +4613,7 @@ var createWebhookHub = (options) => {
|
|
|
4208
4613
|
responseBody: null
|
|
4209
4614
|
};
|
|
4210
4615
|
const controller = new AbortController();
|
|
4616
|
+
controllers.set(delivery.id, controller);
|
|
4211
4617
|
const timer = scheduleTimer(() => controller.abort(), timeoutMs);
|
|
4212
4618
|
try {
|
|
4213
4619
|
const signed = await options.signer({
|
|
@@ -4237,6 +4643,7 @@ var createWebhookHub = (options) => {
|
|
|
4237
4643
|
} catch (error) {
|
|
4238
4644
|
record.error = controller.signal.aborted ? `timed out after ${timeoutMs}ms` : error instanceof Error ? error.message : String(error);
|
|
4239
4645
|
} finally {
|
|
4646
|
+
controllers.delete(delivery.id);
|
|
4240
4647
|
cancel(timer);
|
|
4241
4648
|
record.durationMs = now() - started;
|
|
4242
4649
|
delivery.attempts.push(record);
|
|
@@ -4254,6 +4661,8 @@ var createWebhookHub = (options) => {
|
|
|
4254
4661
|
if (!payloads.has(delivery.id))
|
|
4255
4662
|
return;
|
|
4256
4663
|
await attempt(delivery).then((ok2) => {
|
|
4664
|
+
if (!payloads.has(delivery.id))
|
|
4665
|
+
return;
|
|
4257
4666
|
if (ok2)
|
|
4258
4667
|
delivery.state = "delivered";
|
|
4259
4668
|
else
|
|
@@ -4315,6 +4724,37 @@ var createWebhookHub = (options) => {
|
|
|
4315
4724
|
fanOut(message);
|
|
4316
4725
|
};
|
|
4317
4726
|
const hub = {
|
|
4727
|
+
pending(namespace) {
|
|
4728
|
+
return inFlight.size + (held.get(namespace)?.length ?? 0) + [...deliveries.values()].filter((d) => d.namespace === namespace && d.state === "pending").length;
|
|
4729
|
+
},
|
|
4730
|
+
snapshot(namespace) {
|
|
4731
|
+
if (hub.pending(namespace) > 0)
|
|
4732
|
+
throw new Error("webhooks are not quiescent");
|
|
4733
|
+
const handle = {};
|
|
4734
|
+
snapshots.set(handle, structuredClone({
|
|
4735
|
+
namespace,
|
|
4736
|
+
endpoints: own.get(namespace) ?? [],
|
|
4737
|
+
messages: messages.filter((m) => m.namespace === namespace),
|
|
4738
|
+
deliveries: [...deliveries].filter(([, d]) => d.namespace === namespace),
|
|
4739
|
+
payloads: [...payloads].filter(([, p]) => p.message.namespace === namespace),
|
|
4740
|
+
faults: faults.get(namespace) ?? []
|
|
4741
|
+
}));
|
|
4742
|
+
return handle;
|
|
4743
|
+
},
|
|
4744
|
+
restore(namespace, handle) {
|
|
4745
|
+
const point = typeof handle === "object" && handle !== null ? snapshots.get(handle) : void 0;
|
|
4746
|
+
if (!point || point.namespace !== namespace || hub.pending(namespace) > 0)
|
|
4747
|
+
throw new Error("invalid webhook restore");
|
|
4748
|
+
hub.clear(namespace);
|
|
4749
|
+
const copied = structuredClone(point);
|
|
4750
|
+
own.set(namespace, copied.endpoints);
|
|
4751
|
+
messages.push(...copied.messages);
|
|
4752
|
+
for (const [id2, delivery] of copied.deliveries)
|
|
4753
|
+
deliveries.set(id2, delivery);
|
|
4754
|
+
for (const [id2, payload] of copied.payloads)
|
|
4755
|
+
payloads.set(id2, payload);
|
|
4756
|
+
faults.set(namespace, copied.faults);
|
|
4757
|
+
},
|
|
4318
4758
|
publish(input) {
|
|
4319
4759
|
const contentType = input.contentType ?? (input.form ? "application/x-www-form-urlencoded" : "application/json");
|
|
4320
4760
|
const body = typeof input.body === "string" ? input.body : input.form ? new URLSearchParams(input.form).toString() : JSON.stringify(input.body);
|
|
@@ -4405,6 +4845,7 @@ var createWebhookHub = (options) => {
|
|
|
4405
4845
|
for (const [id2, delivery] of deliveries) {
|
|
4406
4846
|
if (namespace !== void 0 && delivery.namespace !== namespace)
|
|
4407
4847
|
continue;
|
|
4848
|
+
controllers.get(id2)?.abort();
|
|
4408
4849
|
const timer = pending.get(id2);
|
|
4409
4850
|
if (timer !== void 0)
|
|
4410
4851
|
cancel(timer);
|
|
@@ -4429,7 +4870,10 @@ var createWebhookHub = (options) => {
|
|
|
4429
4870
|
};
|
|
4430
4871
|
return hub;
|
|
4431
4872
|
};
|
|
4432
|
-
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4873
|
+
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4874
|
+
status,
|
|
4875
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4876
|
+
});
|
|
4433
4877
|
var adminError3 = (status, message) => json4(status, { error: { type: "mockingbird_admin", message } });
|
|
4434
4878
|
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4435
4879
|
var parseEndpoint = (value) => {
|
|
@@ -4537,7 +4981,6 @@ var AT_HEADER = "x-mockingbird-at";
|
|
|
4537
4981
|
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
4538
4982
|
var DEFAULT_NAMESPACE = "default";
|
|
4539
4983
|
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4540
|
-
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
4541
4984
|
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4542
4985
|
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
4543
4986
|
var effects = /* @__PURE__ */ new WeakMap();
|
|
@@ -4578,6 +5021,22 @@ var operationMatcher = (document3) => {
|
|
|
4578
5021
|
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
4579
5022
|
};
|
|
4580
5023
|
var createRuntime = (options) => {
|
|
5024
|
+
const adminPrefix = resolveAdminPrefix(options.adminPrefix);
|
|
5025
|
+
const internalPaths = Object.entries(options.document?.paths ?? {}).filter(([, item]) => item["x-mockingbird-internal"] === true).map(([path]) => path);
|
|
5026
|
+
for (const path of internalPaths) {
|
|
5027
|
+
if (!path.startsWith(`${ADMIN_PREFIX}/blobs/`))
|
|
5028
|
+
throw new Error(`internal transport ${path} must live under ${ADMIN_PREFIX}/blobs/`);
|
|
5029
|
+
}
|
|
5030
|
+
assertAdminPrefixAvailable(adminPrefix, Object.keys(options.document?.paths ?? {}).filter((path) => !internalPaths.includes(path)));
|
|
5031
|
+
const internalMatch = (path) => {
|
|
5032
|
+
for (const pattern of internalPaths) {
|
|
5033
|
+
const actual = path.split("/");
|
|
5034
|
+
const expected = pattern.split("/");
|
|
5035
|
+
if (actual.length === expected.length && expected.every((part, i) => part.startsWith("{") || part === actual[i]))
|
|
5036
|
+
return pattern;
|
|
5037
|
+
}
|
|
5038
|
+
return void 0;
|
|
5039
|
+
};
|
|
4581
5040
|
assertAdminUi(options.adminUi);
|
|
4582
5041
|
const sqlite = bootSqlite(options.sqlite);
|
|
4583
5042
|
const clock = options.clock ?? createClock();
|
|
@@ -4592,6 +5051,20 @@ var createRuntime = (options) => {
|
|
|
4592
5051
|
const instances = /* @__PURE__ */ new Map();
|
|
4593
5052
|
const publicNamespaces = /* @__PURE__ */ new Set();
|
|
4594
5053
|
const branchRngs = /* @__PURE__ */ new Map();
|
|
5054
|
+
const namespaceClocks = /* @__PURE__ */ new Map();
|
|
5055
|
+
const namespaceForStorage = /* @__PURE__ */ new Map();
|
|
5056
|
+
let isolatedNamespaces = false;
|
|
5057
|
+
const namespaceClock = (name = DEFAULT_NAMESPACE) => {
|
|
5058
|
+
if (!isolatedNamespaces || name === DEFAULT_NAMESPACE)
|
|
5059
|
+
return clock;
|
|
5060
|
+
let found = namespaceClocks.get(name);
|
|
5061
|
+
if (!found) {
|
|
5062
|
+
found = createClock();
|
|
5063
|
+
namespaceClocks.set(name, found);
|
|
5064
|
+
}
|
|
5065
|
+
return found;
|
|
5066
|
+
};
|
|
5067
|
+
const fleetPoints = /* @__PURE__ */ new WeakMap();
|
|
4595
5068
|
const timelines = /* @__PURE__ */ new Map();
|
|
4596
5069
|
const branchStorage = /* @__PURE__ */ new Map();
|
|
4597
5070
|
const captured = /* @__PURE__ */ new Map();
|
|
@@ -4605,17 +5078,22 @@ var createRuntime = (options) => {
|
|
|
4605
5078
|
if (!NAMESPACE_PATTERN.test(key) || !NAMESPACE_PATTERN.test(publicNamespace)) {
|
|
4606
5079
|
throw new RangeError(`namespace must match ${NAMESPACE_PATTERN}: ${JSON.stringify(publicNamespace)}`);
|
|
4607
5080
|
}
|
|
5081
|
+
const createdContextRng = isolatedRng ?? (isolatedNamespaces && key !== DEFAULT_NAMESPACE ? createRng(options.seed ?? 0) : rng);
|
|
4608
5082
|
const created = options.create({
|
|
4609
5083
|
namespace: storageNamespace(key),
|
|
4610
5084
|
publicNamespace,
|
|
5085
|
+
adminPrefix,
|
|
4611
5086
|
sqlite,
|
|
4612
|
-
clock,
|
|
4613
|
-
rng:
|
|
5087
|
+
clock: namespaceClock(publicNamespace),
|
|
5088
|
+
rng: createdContextRng
|
|
4614
5089
|
});
|
|
4615
5090
|
instances.set(key, created);
|
|
5091
|
+
namespaceForStorage.set(key, publicNamespace);
|
|
4616
5092
|
publicNamespaces.add(publicNamespace);
|
|
4617
5093
|
if (isolatedRng)
|
|
4618
5094
|
branchRngs.set(key, isolatedRng);
|
|
5095
|
+
else if (isolatedNamespaces && key !== DEFAULT_NAMESPACE)
|
|
5096
|
+
branchRngs.set(key, createdContextRng);
|
|
4619
5097
|
return created;
|
|
4620
5098
|
};
|
|
4621
5099
|
const instance = (name = DEFAULT_NAMESPACE) => instanceFor(name);
|
|
@@ -4633,7 +5111,7 @@ var createRuntime = (options) => {
|
|
|
4633
5111
|
captured.set(storage, snapshot2);
|
|
4634
5112
|
return Object.freeze({
|
|
4635
5113
|
snapshot: snapshot2,
|
|
4636
|
-
clock: Object.freeze(
|
|
5114
|
+
clock: Object.freeze(namespaceClock(namespaceForStorage.get(storage) ?? storage).state()),
|
|
4637
5115
|
rngState: (branchRngs.get(storage) ?? rng).state()
|
|
4638
5116
|
});
|
|
4639
5117
|
};
|
|
@@ -4643,7 +5121,7 @@ var createRuntime = (options) => {
|
|
|
4643
5121
|
return found;
|
|
4644
5122
|
instance(name);
|
|
4645
5123
|
found = new Timeline({
|
|
4646
|
-
now:
|
|
5124
|
+
now: namespaceClock(name).now,
|
|
4647
5125
|
...options.maxCheckpoints !== void 0 ? { maxCheckpoints: options.maxCheckpoints } : {}
|
|
4648
5126
|
});
|
|
4649
5127
|
found.commit(capture(name));
|
|
@@ -4670,12 +5148,13 @@ var createRuntime = (options) => {
|
|
|
4670
5148
|
const point = history2.checkout("main", at);
|
|
4671
5149
|
restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
|
|
4672
5150
|
captured.set(namespace, point.value.snapshot);
|
|
4673
|
-
rng.setState(point.value.rngState);
|
|
4674
|
-
|
|
5151
|
+
(branchRngs.get(namespace) ?? rng).setState(point.value.rngState);
|
|
5152
|
+
const selectedClock = namespaceClock(namespace);
|
|
5153
|
+
selectedClock.set(point.value.clock.now);
|
|
4675
5154
|
if (point.value.clock.frozen)
|
|
4676
|
-
|
|
5155
|
+
selectedClock.freeze();
|
|
4677
5156
|
else
|
|
4678
|
-
|
|
5157
|
+
selectedClock.unfreeze();
|
|
4679
5158
|
}
|
|
4680
5159
|
return namespace;
|
|
4681
5160
|
}
|
|
@@ -4733,11 +5212,12 @@ var createRuntime = (options) => {
|
|
|
4733
5212
|
const storage = ensureBranch(namespace, branchName);
|
|
4734
5213
|
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
4735
5214
|
captured.set(storage, point.value.snapshot);
|
|
4736
|
-
|
|
5215
|
+
const selectedClock = namespaceClock(namespace);
|
|
5216
|
+
selectedClock.set(point.value.clock.now);
|
|
4737
5217
|
if (point.value.clock.frozen)
|
|
4738
|
-
|
|
5218
|
+
selectedClock.freeze();
|
|
4739
5219
|
else
|
|
4740
|
-
|
|
5220
|
+
selectedClock.unfreeze();
|
|
4741
5221
|
(branchRngs.get(storage) ?? rng).setState(point.value.rngState);
|
|
4742
5222
|
};
|
|
4743
5223
|
const reset = async (name = DEFAULT_NAMESPACE) => {
|
|
@@ -4745,10 +5225,21 @@ var createRuntime = (options) => {
|
|
|
4745
5225
|
options.webhooks?.clear();
|
|
4746
5226
|
for (const each of instances.values())
|
|
4747
5227
|
await each.reset();
|
|
5228
|
+
for (const each of branchRngs.values())
|
|
5229
|
+
each.reset();
|
|
4748
5230
|
timelines.clear();
|
|
5231
|
+
for (const storage of branchStorage.values()) {
|
|
5232
|
+
instances.delete(storage);
|
|
5233
|
+
branchRngs.delete(storage);
|
|
5234
|
+
namespaceForStorage.delete(storage);
|
|
5235
|
+
}
|
|
4749
5236
|
branchStorage.clear();
|
|
4750
|
-
branchRngs.clear();
|
|
4751
5237
|
captured.clear();
|
|
5238
|
+
faults.clear();
|
|
5239
|
+
clock.reset();
|
|
5240
|
+
rng.reset();
|
|
5241
|
+
for (const each of namespaceClocks.values())
|
|
5242
|
+
each.reset();
|
|
4752
5243
|
return;
|
|
4753
5244
|
}
|
|
4754
5245
|
options.webhooks?.clear(name);
|
|
@@ -4766,11 +5257,18 @@ var createRuntime = (options) => {
|
|
|
4766
5257
|
else
|
|
4767
5258
|
clearNamespace(sqlite, storageNamespace(storage));
|
|
4768
5259
|
branchStorage.delete(mapping);
|
|
5260
|
+
instances.delete(storage);
|
|
5261
|
+
namespaceForStorage.delete(storage);
|
|
4769
5262
|
branchRngs.delete(storage);
|
|
4770
5263
|
captured.delete(storage);
|
|
4771
5264
|
}
|
|
4772
5265
|
timelines.delete(name);
|
|
4773
5266
|
captured.delete(name);
|
|
5267
|
+
faults.restore(name, { rules: [], rngState: createRng(options.seed ?? 0).state() });
|
|
5268
|
+
namespaceClock(name).reset();
|
|
5269
|
+
(branchRngs.get(name) ?? rng).reset();
|
|
5270
|
+
journal.clear(name);
|
|
5271
|
+
metrics.reset(name);
|
|
4774
5272
|
};
|
|
4775
5273
|
const snapshot = (name = DEFAULT_NAMESPACE) => {
|
|
4776
5274
|
return checkpoint(name, "main").value.snapshot;
|
|
@@ -4808,6 +5306,53 @@ var createRuntime = (options) => {
|
|
|
4808
5306
|
rng,
|
|
4809
5307
|
credentials,
|
|
4810
5308
|
webhooks: options.webhooks,
|
|
5309
|
+
isolateNamespaces() {
|
|
5310
|
+
if (publicNamespaces.size > 1)
|
|
5311
|
+
throw new Error("enable namespace isolation before creating named namespaces");
|
|
5312
|
+
isolatedNamespaces = true;
|
|
5313
|
+
faults.isolateNamespaces();
|
|
5314
|
+
},
|
|
5315
|
+
namespaceClock,
|
|
5316
|
+
namespaceOf(request) {
|
|
5317
|
+
const url = new URL(request.url);
|
|
5318
|
+
const prefixed = matchNamespacePath(url.pathname, adminPrefix);
|
|
5319
|
+
const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
|
|
5320
|
+
const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
|
|
5321
|
+
if (isAdminPath(path, adminPrefix))
|
|
5322
|
+
return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
|
|
5323
|
+
if (selected !== void 0)
|
|
5324
|
+
return selected;
|
|
5325
|
+
const credential = options.credential?.(request);
|
|
5326
|
+
return (credential !== void 0 ? credentials.get(credential) : void 0) ?? DEFAULT_NAMESPACE;
|
|
5327
|
+
},
|
|
5328
|
+
fleetSnapshot(namespace) {
|
|
5329
|
+
instance(namespace);
|
|
5330
|
+
const handle = {};
|
|
5331
|
+
fleetPoints.set(handle, {
|
|
5332
|
+
namespace,
|
|
5333
|
+
state: capture(namespace),
|
|
5334
|
+
faults: faults.snapshot(namespace),
|
|
5335
|
+
webhooks: options.webhooks?.snapshot(namespace)
|
|
5336
|
+
});
|
|
5337
|
+
return handle;
|
|
5338
|
+
},
|
|
5339
|
+
fleetRestore(handle, namespace) {
|
|
5340
|
+
const point = typeof handle === "object" && handle !== null ? fleetPoints.get(handle) : void 0;
|
|
5341
|
+
if (!point || point.namespace !== namespace)
|
|
5342
|
+
throw new Error("invalid fleet checkpoint");
|
|
5343
|
+
restoreNamespace(sqlite, storageNamespace(namespace), point.state.snapshot);
|
|
5344
|
+
captured.set(namespace, point.state.snapshot);
|
|
5345
|
+
const selectedClock = namespaceClock(namespace);
|
|
5346
|
+
selectedClock.freeze();
|
|
5347
|
+
selectedClock.set(point.state.clock.now);
|
|
5348
|
+
if (!point.state.clock.frozen)
|
|
5349
|
+
selectedClock.unfreeze();
|
|
5350
|
+
(branchRngs.get(namespace) ?? rng).setState(point.state.rngState);
|
|
5351
|
+
faults.restore(namespace, point.faults);
|
|
5352
|
+
if (point.webhooks !== void 0)
|
|
5353
|
+
options.webhooks?.restore(namespace, point.webhooks);
|
|
5354
|
+
timelines.delete(namespace);
|
|
5355
|
+
},
|
|
4811
5356
|
applyPreset: (name, namespace = DEFAULT_NAMESPACE, overrides = {}) => {
|
|
4812
5357
|
const preset = options.presets?.[name];
|
|
4813
5358
|
if (!preset)
|
|
@@ -4839,7 +5384,7 @@ var createRuntime = (options) => {
|
|
|
4839
5384
|
state: (namespace = DEFAULT_NAMESPACE) => inspectState(stateScope(namespace)),
|
|
4840
5385
|
fetch: async (incoming) => {
|
|
4841
5386
|
let request = incoming;
|
|
4842
|
-
const prefixed =
|
|
5387
|
+
const prefixed = matchNamespacePath(new URL(request.url).pathname, adminPrefix);
|
|
4843
5388
|
if (prefixed) {
|
|
4844
5389
|
const url2 = new URL(request.url);
|
|
4845
5390
|
url2.pathname = prefixed[2] ?? "/";
|
|
@@ -4855,7 +5400,15 @@ var createRuntime = (options) => {
|
|
|
4855
5400
|
signal: request.signal
|
|
4856
5401
|
});
|
|
4857
5402
|
}
|
|
4858
|
-
|
|
5403
|
+
const internalUrl = new URL(request.url);
|
|
5404
|
+
const internalPath = isAdminPath(internalUrl.pathname, adminPrefix) ? `${ADMIN_PREFIX}${internalUrl.pathname.slice(adminPrefix.length)}` : "";
|
|
5405
|
+
const internal = internalMatch(internalPath);
|
|
5406
|
+
const obsoleteInternalPath = adminPrefix !== ADMIN_PREFIX && internalMatch(internalUrl.pathname) && !internal;
|
|
5407
|
+
if (internal) {
|
|
5408
|
+
internalUrl.pathname = internalPath;
|
|
5409
|
+
request = new Request(internalUrl, request);
|
|
5410
|
+
}
|
|
5411
|
+
let namespace = internal ? internalUrl.searchParams.get("namespace") ?? control.namespaceOf(request) : control.namespaceOf(request);
|
|
4859
5412
|
if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
4860
5413
|
const credential = options.credential(request);
|
|
4861
5414
|
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
@@ -4875,7 +5428,9 @@ var createRuntime = (options) => {
|
|
|
4875
5428
|
return copy;
|
|
4876
5429
|
}
|
|
4877
5430
|
};
|
|
4878
|
-
|
|
5431
|
+
if (obsoleteInternalPath)
|
|
5432
|
+
return stamp(adminFail(404, "internal transport is only available under adminPrefix"));
|
|
5433
|
+
const handled = internal ? void 0 : await control.handle(request);
|
|
4879
5434
|
if (handled)
|
|
4880
5435
|
return stamp(handled);
|
|
4881
5436
|
const started = monotonicNow();
|
|
@@ -4916,7 +5471,7 @@ var createRuntime = (options) => {
|
|
|
4916
5471
|
type: "mockingbird_admin",
|
|
4917
5472
|
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
4918
5473
|
}
|
|
4919
|
-
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
5474
|
+
}), { status: 400, headers: { "content-type": "application/json; charset=utf-8" } }));
|
|
4920
5475
|
}
|
|
4921
5476
|
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
4922
5477
|
log(400);
|
|
@@ -4983,6 +5538,7 @@ var createRuntime = (options) => {
|
|
|
4983
5538
|
startedAt: wallNow(),
|
|
4984
5539
|
wallNow,
|
|
4985
5540
|
clock,
|
|
5541
|
+
namespaceClock,
|
|
4986
5542
|
faults,
|
|
4987
5543
|
metrics,
|
|
4988
5544
|
journal,
|
|
@@ -5032,15 +5588,16 @@ var createRuntime = (options) => {
|
|
|
5032
5588
|
...options.admin?.(runtime) ?? {},
|
|
5033
5589
|
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
5034
5590
|
// maps `presets`, and a service route must not replace that list with a record.
|
|
5035
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
5591
|
+
...options.presets ? presetRoutes(options.presets, runtime, adminPrefix) : {},
|
|
5036
5592
|
...stateAdminRoutes({
|
|
5037
5593
|
open: stateScope,
|
|
5038
5594
|
afterWrite: (namespace) => {
|
|
5039
5595
|
checkpoint(namespace, "main");
|
|
5040
5596
|
}
|
|
5041
5597
|
}),
|
|
5042
|
-
...adminUiRoutes(options.name, options.adminUi)
|
|
5598
|
+
...adminUiRoutes(options.name, options.adminUi, adminPrefix)
|
|
5043
5599
|
},
|
|
5600
|
+
adminPrefix,
|
|
5044
5601
|
adminKey: options.adminKey
|
|
5045
5602
|
});
|
|
5046
5603
|
return defineMock(runtime);
|
|
@@ -5054,7 +5611,10 @@ var mutableResponse = (response) => {
|
|
|
5054
5611
|
return new Response(response.body, response);
|
|
5055
5612
|
}
|
|
5056
5613
|
};
|
|
5057
|
-
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5614
|
+
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5615
|
+
status,
|
|
5616
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
5617
|
+
});
|
|
5058
5618
|
var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
|
|
5059
5619
|
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5060
5620
|
var credentialRoutes = (registry) => ({
|
|
@@ -5106,7 +5666,7 @@ var credentialRoutes = (registry) => ({
|
|
|
5106
5666
|
return adminJson(200, { status: "ok" });
|
|
5107
5667
|
}
|
|
5108
5668
|
});
|
|
5109
|
-
var presetRoutes = (presets, runtime) => ({
|
|
5669
|
+
var presetRoutes = (presets, runtime, adminPrefix) => ({
|
|
5110
5670
|
"GET /faults/presets": () => {
|
|
5111
5671
|
const body = {
|
|
5112
5672
|
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
@@ -5116,7 +5676,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
5116
5676
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
5117
5677
|
const name = params.name;
|
|
5118
5678
|
if (!presets[name])
|
|
5119
|
-
return adminFail(404, `no fault preset ${name}; GET /
|
|
5679
|
+
return adminFail(404, `no fault preset ${name}; GET ${adminPrefix}/faults/presets`);
|
|
5120
5680
|
const overrides = isObject(body) ? body : {};
|
|
5121
5681
|
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
5122
5682
|
}
|
|
@@ -5462,15 +6022,17 @@ var createRuntime2 = (options = {}) => {
|
|
|
5462
6022
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
5463
6023
|
...options.clock ? { clock: options.clock } : {},
|
|
5464
6024
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
6025
|
+
...options.adminPrefix !== void 0 ? { adminPrefix: options.adminPrefix } : {},
|
|
5465
6026
|
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
5466
6027
|
...options.onLog ? { onLog: options.onLog } : {},
|
|
5467
6028
|
credential: tokenCredential,
|
|
5468
6029
|
presets: FULLSCRIPT_PRESETS,
|
|
5469
6030
|
webhooks: hub,
|
|
5470
|
-
create: ({ sqlite, namespace, publicNamespace, clock }) => new FullscriptAPI({
|
|
6031
|
+
create: ({ sqlite, namespace, publicNamespace, adminPrefix, clock }) => new FullscriptAPI({
|
|
5471
6032
|
sqlite,
|
|
5472
6033
|
namespace,
|
|
5473
6034
|
publicNamespace,
|
|
6035
|
+
adminPrefix,
|
|
5474
6036
|
now: clock.now,
|
|
5475
6037
|
...options.settings ? { settings: options.settings } : {},
|
|
5476
6038
|
...options.orders ? { orders: options.orders } : {},
|
|
@@ -5578,7 +6140,7 @@ var FullscriptAPI = class {
|
|
|
5578
6140
|
const namespace = options.namespace ?? FULLSCRIPT_NAMESPACE;
|
|
5579
6141
|
this.now = options.now ?? (() => Date.now());
|
|
5580
6142
|
this.onEvent = options.onEvent;
|
|
5581
|
-
this.prefix = options.publicNamespace && options.publicNamespace !== "default" ?
|
|
6143
|
+
this.prefix = options.publicNamespace && options.publicNamespace !== "default" ? `${options.adminPrefix ?? "/__admin"}/ns/${encodeURIComponent(options.publicNamespace)}` : "";
|
|
5582
6144
|
this.state = new FullscriptState(sqlite, namespace, options.settings ?? {});
|
|
5583
6145
|
const handlers = defineOperations({
|
|
5584
6146
|
OAuthToken: (context) => this.token(context),
|
|
@@ -6000,6 +6562,9 @@ var FullscriptAPI = class {
|
|
|
6000
6562
|
};
|
|
6001
6563
|
|
|
6002
6564
|
export {
|
|
6565
|
+
createClock,
|
|
6566
|
+
resolveAdminPrefix,
|
|
6567
|
+
parseDuration,
|
|
6003
6568
|
document2 as document,
|
|
6004
6569
|
operationIds,
|
|
6005
6570
|
supportedOperationIds,
|
|
@@ -6020,4 +6585,4 @@ export {
|
|
|
6020
6585
|
envelope,
|
|
6021
6586
|
FullscriptAPI
|
|
6022
6587
|
};
|
|
6023
|
-
//# sourceMappingURL=chunk-
|
|
6588
|
+
//# sourceMappingURL=chunk-R7LD4QOX.js.map
|