@crvouga/mockingbird-service-rxvortex 1.8.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/DISCOVERY.md +55 -0
- package/README.md +1 -1
- package/SUPPORT.md +15 -0
- package/dist/chunk-6OVK7IUG.js +909 -0
- package/dist/chunk-6OVK7IUG.js.map +7 -0
- package/dist/{chunk-CNQMYSGI.js → chunk-MYXNLMD7.js} +822 -258
- package/dist/chunk-MYXNLMD7.js.map +7 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +34 -4
- package/dist/index.js +1 -1
- package/dist/server.d.ts +64 -4
- package/dist/server.js +2 -2
- package/openapi.yaml +579 -0
- package/package.json +29 -4
- package/dist/chunk-CNQMYSGI.js.map +0 -7
- package/dist/chunk-U5XXCFRO.js +0 -422
- package/dist/chunk-U5XXCFRO.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)
|
|
@@ -1071,7 +1147,8 @@ var annotateResponse = (response, extra) => {
|
|
|
1071
1147
|
var responseNotes = (response) => notes.get(response);
|
|
1072
1148
|
|
|
1073
1149
|
// ../core/dist/metrics.js
|
|
1074
|
-
var createMetrics = () => {
|
|
1150
|
+
var createMetrics = (partition = true) => {
|
|
1151
|
+
const namespaces = /* @__PURE__ */ new Map();
|
|
1075
1152
|
let requests = 0;
|
|
1076
1153
|
let faults = 0;
|
|
1077
1154
|
let totalDurationMs = 0;
|
|
@@ -1079,6 +1156,14 @@ var createMetrics = () => {
|
|
|
1079
1156
|
const unmatched = /* @__PURE__ */ new Map();
|
|
1080
1157
|
return {
|
|
1081
1158
|
record(entry) {
|
|
1159
|
+
if (partition) {
|
|
1160
|
+
let scoped = namespaces.get(entry.namespace);
|
|
1161
|
+
if (!scoped) {
|
|
1162
|
+
scoped = createMetrics(false);
|
|
1163
|
+
namespaces.set(entry.namespace, scoped);
|
|
1164
|
+
}
|
|
1165
|
+
scoped.record(entry);
|
|
1166
|
+
}
|
|
1082
1167
|
requests++;
|
|
1083
1168
|
totalDurationMs += entry.durationMs;
|
|
1084
1169
|
if (entry.faultId !== void 0)
|
|
@@ -1090,7 +1175,7 @@ var createMetrics = () => {
|
|
|
1090
1175
|
unmatched.set(route, (unmatched.get(route) ?? 0) + 1);
|
|
1091
1176
|
}
|
|
1092
1177
|
},
|
|
1093
|
-
report: () => ({
|
|
1178
|
+
report: (namespace) => namespace !== void 0 ? (namespaces.get(namespace) ?? createMetrics(false)).report() : {
|
|
1094
1179
|
requests,
|
|
1095
1180
|
byOperation: Object.fromEntries([...byOperation].sort(([a], [b]) => a.localeCompare(b))),
|
|
1096
1181
|
unmatched: [...unmatched].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([route, count]) => {
|
|
@@ -1099,8 +1184,13 @@ var createMetrics = () => {
|
|
|
1099
1184
|
}),
|
|
1100
1185
|
faults,
|
|
1101
1186
|
totalDurationMs
|
|
1102
|
-
}
|
|
1103
|
-
reset() {
|
|
1187
|
+
},
|
|
1188
|
+
reset(namespace) {
|
|
1189
|
+
if (namespace !== void 0) {
|
|
1190
|
+
namespaces.delete(namespace);
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
namespaces.clear();
|
|
1104
1194
|
requests = 0;
|
|
1105
1195
|
faults = 0;
|
|
1106
1196
|
totalDurationMs = 0;
|
|
@@ -1713,6 +1803,34 @@ dialog:not([open]) {
|
|
|
1713
1803
|
function isRecord2(value) {
|
|
1714
1804
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1715
1805
|
}
|
|
1806
|
+
function highlightJsonText(text) {
|
|
1807
|
+
const escapeToken = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
1808
|
+
switch (char) {
|
|
1809
|
+
case "&":
|
|
1810
|
+
return "&";
|
|
1811
|
+
case "<":
|
|
1812
|
+
return "<";
|
|
1813
|
+
case ">":
|
|
1814
|
+
return ">";
|
|
1815
|
+
case '"':
|
|
1816
|
+
return """;
|
|
1817
|
+
default:
|
|
1818
|
+
return "'";
|
|
1819
|
+
}
|
|
1820
|
+
});
|
|
1821
|
+
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;
|
|
1822
|
+
let cursor = 0;
|
|
1823
|
+
let html = "";
|
|
1824
|
+
for (const match of text.matchAll(token)) {
|
|
1825
|
+
const index = match.index ?? 0;
|
|
1826
|
+
const value = match[0];
|
|
1827
|
+
html += escapeToken(text.slice(cursor, index));
|
|
1828
|
+
const kind = value.startsWith('"') ? value.trimEnd().endsWith(":") ? "key" : "string" : value === "true" || value === "false" ? "boolean" : value === "null" ? "null" : "number";
|
|
1829
|
+
html += `<span class="json-${kind}">${escapeToken(value)}</span>`;
|
|
1830
|
+
cursor = index + value.length;
|
|
1831
|
+
}
|
|
1832
|
+
return html + escapeToken(text.slice(cursor));
|
|
1833
|
+
}
|
|
1716
1834
|
function mountAdminBrand(doc, host, service, catalog) {
|
|
1717
1835
|
const clip = (value, max) => {
|
|
1718
1836
|
if (typeof value !== "string")
|
|
@@ -2074,14 +2192,16 @@ function bootAdmin(config) {
|
|
|
2074
2192
|
const state = {
|
|
2075
2193
|
namespace: "default",
|
|
2076
2194
|
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
2077
|
-
view: "overview",
|
|
2195
|
+
view: ["overview", "state", "clock", "faults", "journal", "routes"].includes(location.hash.slice(1)) ? location.hash.slice(1) : "overview",
|
|
2078
2196
|
collection: "",
|
|
2079
2197
|
shape: null,
|
|
2080
2198
|
route: null,
|
|
2199
|
+
routes: [],
|
|
2081
2200
|
editing: null
|
|
2082
2201
|
};
|
|
2083
2202
|
const standard = new Set(config.standardRoutes);
|
|
2084
2203
|
const banner = el("banner", HTMLElement);
|
|
2204
|
+
const bannerMessage = el("banner-message", HTMLElement);
|
|
2085
2205
|
const keyInput = el("admin-key", HTMLInputElement);
|
|
2086
2206
|
keyInput.value = state.key;
|
|
2087
2207
|
const params = new URLSearchParams(location.search);
|
|
@@ -2092,18 +2212,21 @@ function bootAdmin(config) {
|
|
|
2092
2212
|
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
2093
2213
|
params.delete("key");
|
|
2094
2214
|
const next = params.toString();
|
|
2095
|
-
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
2215
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : "") + location.hash);
|
|
2096
2216
|
}
|
|
2097
2217
|
const queryNamespace = params.get("namespace");
|
|
2098
2218
|
if (queryNamespace)
|
|
2099
2219
|
state.namespace = queryNamespace;
|
|
2100
2220
|
const showError = (error) => {
|
|
2101
|
-
|
|
2221
|
+
bannerMessage.textContent = error instanceof Error ? error.message : String(error);
|
|
2102
2222
|
banner.classList.add("show");
|
|
2223
|
+
banner.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
2103
2224
|
};
|
|
2104
2225
|
const clearError = () => {
|
|
2105
2226
|
banner.classList.remove("show");
|
|
2227
|
+
bannerMessage.textContent = "";
|
|
2106
2228
|
};
|
|
2229
|
+
el("banner-dismiss", HTMLButtonElement).addEventListener("click", clearError);
|
|
2107
2230
|
const api = async (method, path, body) => {
|
|
2108
2231
|
const headers = { accept: "application/json" };
|
|
2109
2232
|
if (state.key !== "")
|
|
@@ -2147,16 +2270,21 @@ function bootAdmin(config) {
|
|
|
2147
2270
|
namespace: () => state.namespace
|
|
2148
2271
|
};
|
|
2149
2272
|
const isPanelScript = (value) => typeof value === "function";
|
|
2273
|
+
const themeButton = el("theme", HTMLButtonElement);
|
|
2150
2274
|
const setTheme = (theme) => {
|
|
2151
2275
|
if (theme)
|
|
2152
2276
|
document.documentElement.dataset.theme = theme;
|
|
2153
2277
|
else
|
|
2154
2278
|
delete document.documentElement.dataset.theme;
|
|
2279
|
+
const dark = theme ? theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2280
|
+
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>';
|
|
2281
|
+
const label = dark ? "Use light theme" : "Use dark theme";
|
|
2282
|
+
themeButton.setAttribute("aria-label", label);
|
|
2283
|
+
themeButton.title = label;
|
|
2155
2284
|
};
|
|
2156
2285
|
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
2286
|
+
setTheme(storedTheme === "dark" || storedTheme === "light" ? storedTheme : null);
|
|
2287
|
+
themeButton.addEventListener("click", () => {
|
|
2160
2288
|
const current = document.documentElement.dataset.theme;
|
|
2161
2289
|
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2162
2290
|
const next = dark ? "light" : "dark";
|
|
@@ -2175,6 +2303,7 @@ function bootAdmin(config) {
|
|
|
2175
2303
|
continue;
|
|
2176
2304
|
section.hidden = section.id !== `view-${view}`;
|
|
2177
2305
|
}
|
|
2306
|
+
history.replaceState(null, "", `${location.pathname}${location.search}#${encodeURIComponent(view)}`);
|
|
2178
2307
|
};
|
|
2179
2308
|
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
2180
2309
|
const target = event.target;
|
|
@@ -2189,7 +2318,7 @@ function bootAdmin(config) {
|
|
|
2189
2318
|
show(view);
|
|
2190
2319
|
refresh().catch(showError);
|
|
2191
2320
|
});
|
|
2192
|
-
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>`;
|
|
2321
|
+
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>`;
|
|
2193
2322
|
const table = (columns, rows) => {
|
|
2194
2323
|
if (rows.length === 0)
|
|
2195
2324
|
return '<p class="empty">Nothing here yet.</p>';
|
|
@@ -2199,16 +2328,22 @@ function bootAdmin(config) {
|
|
|
2199
2328
|
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
2200
2329
|
return `<tr${attrs}>${cells}</tr>`;
|
|
2201
2330
|
}).join("");
|
|
2202
|
-
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
2331
|
+
return `<div class="table-shell"><table><thead>${head}</thead><tbody>${body}</tbody></table></div>`;
|
|
2203
2332
|
};
|
|
2204
2333
|
const preview = (value) => {
|
|
2205
2334
|
const text = JSON.stringify(value) ?? "null";
|
|
2206
|
-
|
|
2335
|
+
const clipped = text.length > 180 ? `${text.slice(0, 180)}\u2026` : text;
|
|
2336
|
+
return `<code class="json-inline">${highlightJsonText(clipped)}</code>`;
|
|
2207
2337
|
};
|
|
2208
2338
|
const when = (epoch) => {
|
|
2209
2339
|
const date = new Date(epoch);
|
|
2210
2340
|
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
2211
2341
|
};
|
|
2342
|
+
const journalWhen = (value) => {
|
|
2343
|
+
const numeric = value.trim() === "" ? Number.NaN : Number(value);
|
|
2344
|
+
const epoch = Number.isFinite(numeric) ? numeric : Date.parse(value);
|
|
2345
|
+
return Number.isFinite(epoch) ? when(epoch) : value;
|
|
2346
|
+
};
|
|
2212
2347
|
const clockCard = (epoch) => {
|
|
2213
2348
|
const date = new Date(epoch);
|
|
2214
2349
|
if (Number.isNaN(date.getTime()))
|
|
@@ -2328,7 +2463,7 @@ function bootAdmin(config) {
|
|
|
2328
2463
|
};
|
|
2329
2464
|
const renderOverview = async () => {
|
|
2330
2465
|
const [health, shape, clock, journal] = await Promise.all([
|
|
2331
|
-
originGet(
|
|
2466
|
+
originGet(`${config.adminPrefix}/health`),
|
|
2332
2467
|
api("GET", "/state"),
|
|
2333
2468
|
api("GET", "/clock"),
|
|
2334
2469
|
api("GET", "/requests?limit=8")
|
|
@@ -2345,7 +2480,7 @@ function bootAdmin(config) {
|
|
|
2345
2480
|
},
|
|
2346
2481
|
{
|
|
2347
2482
|
k: "Status",
|
|
2348
|
-
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
2483
|
+
v: `<span class="badge ok">${esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")}</span>`
|
|
2349
2484
|
},
|
|
2350
2485
|
{ k: "Namespace", v: esc(state.namespace) },
|
|
2351
2486
|
{ k: "Collections", v: String(decoded.collections.length) },
|
|
@@ -2432,26 +2567,46 @@ function bootAdmin(config) {
|
|
|
2432
2567
|
const renderJournal = async () => {
|
|
2433
2568
|
const journal = await api("GET", "/requests?limit=100");
|
|
2434
2569
|
el("journal", HTMLElement).innerHTML = table([
|
|
2435
|
-
{ label: "When", cell: (row) => esc(row.at) },
|
|
2436
|
-
{
|
|
2570
|
+
{ label: "When", cell: (row) => esc(journalWhen(row.at)) },
|
|
2571
|
+
{
|
|
2572
|
+
label: "Method",
|
|
2573
|
+
cell: (row) => `<span class="method">${esc(row.method)}</span>`
|
|
2574
|
+
},
|
|
2437
2575
|
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2438
|
-
{
|
|
2576
|
+
{
|
|
2577
|
+
label: "Status",
|
|
2578
|
+
cell: (row) => `<span class="badge ${row.status.startsWith("2") ? "ok" : row.status.startsWith("4") || row.status.startsWith("5") ? "warn" : ""}">${esc(row.status)}</span>`
|
|
2579
|
+
},
|
|
2439
2580
|
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
2440
2581
|
], decodeJournal(journal));
|
|
2441
2582
|
};
|
|
2442
|
-
const
|
|
2443
|
-
const
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2583
|
+
const paintRoutes = () => {
|
|
2584
|
+
const query = el("route-search", HTMLInputElement).value.trim().toLocaleLowerCase();
|
|
2585
|
+
const routes = state.routes.filter((route) => route.toLocaleLowerCase().includes(query));
|
|
2586
|
+
el("route-count", HTMLElement).textContent = `${routes.length} of ${state.routes.length}`;
|
|
2587
|
+
if (state.routes.length === 0) {
|
|
2588
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No admin routes are available.</p>';
|
|
2589
|
+
return;
|
|
2590
|
+
}
|
|
2591
|
+
if (routes.length === 0) {
|
|
2592
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No routes match this filter.</p>';
|
|
2593
|
+
return;
|
|
2594
|
+
}
|
|
2447
2595
|
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2448
2596
|
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2449
2597
|
const parts = route.split(" ");
|
|
2450
2598
|
const method = parts[0] ?? "";
|
|
2451
2599
|
const path = parts.slice(1).join(" ");
|
|
2452
|
-
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>`;
|
|
2600
|
+
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>`;
|
|
2453
2601
|
}).join("");
|
|
2454
2602
|
};
|
|
2603
|
+
const renderRoutes = async () => {
|
|
2604
|
+
const data = await api("GET", "/");
|
|
2605
|
+
if (!isRecord2(data))
|
|
2606
|
+
throw new Error("Route list was not an object");
|
|
2607
|
+
state.routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
2608
|
+
paintRoutes();
|
|
2609
|
+
};
|
|
2455
2610
|
const renderers = {
|
|
2456
2611
|
overview: renderOverview,
|
|
2457
2612
|
state: renderState,
|
|
@@ -2460,19 +2615,50 @@ function bootAdmin(config) {
|
|
|
2460
2615
|
journal: renderJournal,
|
|
2461
2616
|
routes: renderRoutes
|
|
2462
2617
|
};
|
|
2618
|
+
let refreshToken = 0;
|
|
2463
2619
|
async function refresh() {
|
|
2620
|
+
const token = ++refreshToken;
|
|
2464
2621
|
clearError();
|
|
2622
|
+
const main = el("views", HTMLElement);
|
|
2623
|
+
const sync = el("sync-status", HTMLElement);
|
|
2624
|
+
main.setAttribute("aria-busy", "true");
|
|
2625
|
+
sync.classList.add("busy");
|
|
2626
|
+
sync.textContent = "Updating";
|
|
2465
2627
|
const render = renderers[state.view];
|
|
2466
|
-
|
|
2467
|
-
|
|
2628
|
+
try {
|
|
2629
|
+
if (render)
|
|
2630
|
+
await render();
|
|
2631
|
+
if (token === refreshToken)
|
|
2632
|
+
sync.textContent = "Up to date";
|
|
2633
|
+
} finally {
|
|
2634
|
+
if (token === refreshToken) {
|
|
2635
|
+
main.removeAttribute("aria-busy");
|
|
2636
|
+
sync.classList.remove("busy");
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2468
2639
|
}
|
|
2469
2640
|
const delay = (ms) => new Promise((resolve) => {
|
|
2470
2641
|
window.setTimeout(resolve, ms);
|
|
2471
2642
|
});
|
|
2472
|
-
const confirmAction = async (button, pending, done, work) => {
|
|
2643
|
+
const confirmAction = async (button, pending, done, work, confirmLabel) => {
|
|
2473
2644
|
if (button.dataset.pending === "1")
|
|
2474
2645
|
return;
|
|
2475
2646
|
const label = button.textContent ?? "";
|
|
2647
|
+
if (confirmLabel !== void 0 && button.dataset.confirming !== "1") {
|
|
2648
|
+
button.dataset.confirming = "1";
|
|
2649
|
+
button.classList.add("confirming");
|
|
2650
|
+
button.textContent = confirmLabel;
|
|
2651
|
+
window.setTimeout(() => {
|
|
2652
|
+
if (button.dataset.confirming !== "1")
|
|
2653
|
+
return;
|
|
2654
|
+
button.textContent = label;
|
|
2655
|
+
button.classList.remove("confirming");
|
|
2656
|
+
delete button.dataset.confirming;
|
|
2657
|
+
}, 3e3);
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2660
|
+
button.classList.remove("confirming");
|
|
2661
|
+
delete button.dataset.confirming;
|
|
2476
2662
|
const width = button.offsetWidth;
|
|
2477
2663
|
button.dataset.pending = "1";
|
|
2478
2664
|
button.classList.add("pressed");
|
|
@@ -2493,6 +2679,65 @@ function bootAdmin(config) {
|
|
|
2493
2679
|
delete button.dataset.pending;
|
|
2494
2680
|
}
|
|
2495
2681
|
};
|
|
2682
|
+
const wireJsonEditor = (textareaId, highlightId, statusId, shellId, allowEmpty) => {
|
|
2683
|
+
const textarea = el(textareaId, HTMLTextAreaElement);
|
|
2684
|
+
const highlight = el(highlightId, HTMLElement);
|
|
2685
|
+
const status = el(statusId, HTMLElement);
|
|
2686
|
+
const shell = el(shellId, HTMLElement);
|
|
2687
|
+
const sync = () => {
|
|
2688
|
+
const text = textarea.value;
|
|
2689
|
+
highlight.innerHTML = `${highlightJsonText(text)}
|
|
2690
|
+
`;
|
|
2691
|
+
let valid = true;
|
|
2692
|
+
let message = "Valid JSON";
|
|
2693
|
+
if (allowEmpty && text.trim() === "")
|
|
2694
|
+
message = "No body";
|
|
2695
|
+
else {
|
|
2696
|
+
try {
|
|
2697
|
+
readJson2(text);
|
|
2698
|
+
} catch {
|
|
2699
|
+
valid = false;
|
|
2700
|
+
message = "Invalid JSON";
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
shell.classList.toggle("invalid", !valid);
|
|
2704
|
+
status.classList.toggle("invalid", !valid);
|
|
2705
|
+
status.textContent = message;
|
|
2706
|
+
textarea.setAttribute("aria-invalid", valid ? "false" : "true");
|
|
2707
|
+
};
|
|
2708
|
+
const syncScroll = () => {
|
|
2709
|
+
const preview2 = highlight.parentElement;
|
|
2710
|
+
if (!(preview2 instanceof HTMLElement))
|
|
2711
|
+
return;
|
|
2712
|
+
preview2.scrollTop = textarea.scrollTop;
|
|
2713
|
+
preview2.scrollLeft = textarea.scrollLeft;
|
|
2714
|
+
};
|
|
2715
|
+
textarea.addEventListener("input", sync);
|
|
2716
|
+
textarea.addEventListener("scroll", syncScroll);
|
|
2717
|
+
sync();
|
|
2718
|
+
return sync;
|
|
2719
|
+
};
|
|
2720
|
+
const syncRouteEditor = wireJsonEditor("route-body", "route-highlight", "route-json-status", "route-editor", true);
|
|
2721
|
+
const syncRecordEditor = wireJsonEditor("editor-value", "editor-highlight", "editor-json-status", "record-editor", false);
|
|
2722
|
+
for (const button of document.querySelectorAll("[data-format]")) {
|
|
2723
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2724
|
+
continue;
|
|
2725
|
+
button.addEventListener("click", () => {
|
|
2726
|
+
const target = button.dataset.format;
|
|
2727
|
+
if (target === void 0)
|
|
2728
|
+
return;
|
|
2729
|
+
const textarea = document.getElementById(target);
|
|
2730
|
+
if (!(textarea instanceof HTMLTextAreaElement) || textarea.value.trim() === "")
|
|
2731
|
+
return;
|
|
2732
|
+
try {
|
|
2733
|
+
textarea.value = JSON.stringify(readJson2(textarea.value), null, 2) ?? "";
|
|
2734
|
+
textarea.dispatchEvent(new Event("input"));
|
|
2735
|
+
textarea.focus();
|
|
2736
|
+
} catch {
|
|
2737
|
+
showError(new Error("Fix the JSON before formatting it"));
|
|
2738
|
+
}
|
|
2739
|
+
});
|
|
2740
|
+
}
|
|
2496
2741
|
document.addEventListener("click", (event) => {
|
|
2497
2742
|
const target = event.target;
|
|
2498
2743
|
if (!(target instanceof Element))
|
|
@@ -2565,6 +2810,7 @@ function bootAdmin(config) {
|
|
|
2565
2810
|
} else {
|
|
2566
2811
|
editorValue.value = "{\n \n}";
|
|
2567
2812
|
}
|
|
2813
|
+
syncRecordEditor();
|
|
2568
2814
|
editor.showModal();
|
|
2569
2815
|
}
|
|
2570
2816
|
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -2594,10 +2840,10 @@ function bootAdmin(config) {
|
|
|
2594
2840
|
const id = state.editing;
|
|
2595
2841
|
if (!id)
|
|
2596
2842
|
return;
|
|
2597
|
-
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2843
|
+
confirmAction(editorDelete, "Deleting...", "Deleted", () => api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2598
2844
|
editor.close();
|
|
2599
2845
|
return renderState();
|
|
2600
|
-
}).catch(showError);
|
|
2846
|
+
}), "Confirm delete").catch(showError);
|
|
2601
2847
|
});
|
|
2602
2848
|
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2603
2849
|
if (!(button instanceof HTMLButtonElement))
|
|
@@ -2643,10 +2889,10 @@ function bootAdmin(config) {
|
|
|
2643
2889
|
}).then(() => renderFaults()).catch(showError);
|
|
2644
2890
|
});
|
|
2645
2891
|
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2646
|
-
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
|
|
2892
|
+
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults()), "Confirm clear").catch(showError);
|
|
2647
2893
|
});
|
|
2648
2894
|
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2649
|
-
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
|
|
2895
|
+
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal()), "Confirm clear").catch(showError);
|
|
2650
2896
|
});
|
|
2651
2897
|
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2652
2898
|
const target = event.target;
|
|
@@ -2660,9 +2906,15 @@ function bootAdmin(config) {
|
|
|
2660
2906
|
return;
|
|
2661
2907
|
state.route = route;
|
|
2662
2908
|
el("route-title", HTMLElement).textContent = route;
|
|
2909
|
+
el("route-send", HTMLButtonElement).disabled = false;
|
|
2663
2910
|
const method = route.split(" ")[0] ?? "";
|
|
2664
|
-
el("route-body", HTMLTextAreaElement)
|
|
2911
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
2912
|
+
routeBody.disabled = method === "GET" || method === "DELETE";
|
|
2913
|
+
el("route-editor", HTMLElement).hidden = routeBody.disabled;
|
|
2914
|
+
el("route-json-status", HTMLElement).parentElement?.toggleAttribute("hidden", routeBody.disabled);
|
|
2915
|
+
syncRouteEditor();
|
|
2665
2916
|
});
|
|
2917
|
+
el("route-search", HTMLInputElement).addEventListener("input", paintRoutes);
|
|
2666
2918
|
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2667
2919
|
event.preventDefault();
|
|
2668
2920
|
const send = async () => {
|
|
@@ -2684,7 +2936,8 @@ function bootAdmin(config) {
|
|
|
2684
2936
|
}
|
|
2685
2937
|
}
|
|
2686
2938
|
const data = await api(method, concrete, body);
|
|
2687
|
-
|
|
2939
|
+
const text = JSON.stringify(data, null, 2) ?? "null";
|
|
2940
|
+
el("route-result", HTMLElement).innerHTML = `<code>${highlightJsonText(text)}</code>`;
|
|
2688
2941
|
};
|
|
2689
2942
|
send().catch(showError);
|
|
2690
2943
|
});
|
|
@@ -2802,11 +3055,13 @@ function bootAdmin(config) {
|
|
|
2802
3055
|
views.append(section);
|
|
2803
3056
|
mountSqlExplorer(body, panelApi);
|
|
2804
3057
|
}
|
|
3058
|
+
show(document.getElementById(`view-${state.view}`) === null ? "overview" : state.view);
|
|
2805
3059
|
return refresh();
|
|
2806
3060
|
}).catch(showError);
|
|
2807
3061
|
}
|
|
2808
3062
|
var clientPrelude = [
|
|
2809
3063
|
isRecord2,
|
|
3064
|
+
highlightJsonText,
|
|
2810
3065
|
mountAdminBrand,
|
|
2811
3066
|
startAdminBrand,
|
|
2812
3067
|
faultPresetList,
|
|
@@ -2829,6 +3084,7 @@ ${entry}(${payload});
|
|
|
2829
3084
|
var defineMock = (runtime) => runtime;
|
|
2830
3085
|
var STANDARD_ADMIN_ROUTES = [
|
|
2831
3086
|
"GET /",
|
|
3087
|
+
"GET /health",
|
|
2832
3088
|
"POST /reset",
|
|
2833
3089
|
"GET /namespaces",
|
|
2834
3090
|
"GET /clock",
|
|
@@ -2908,11 +3164,11 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
2908
3164
|
}
|
|
2909
3165
|
});
|
|
2910
3166
|
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
2911
|
-
var renderAdminDocument = (service) => {
|
|
3167
|
+
var renderAdminDocument = (service, adminPrefix = ADMIN_PREFIX) => {
|
|
2912
3168
|
const name = escapeHtml(service);
|
|
2913
3169
|
const script = adminClientSource({
|
|
2914
3170
|
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
2915
|
-
adminPrefix
|
|
3171
|
+
adminPrefix,
|
|
2916
3172
|
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
2917
3173
|
brandsUrl: ADMIN_BRANDS_URL,
|
|
2918
3174
|
service
|
|
@@ -2929,75 +3185,99 @@ var renderAdminDocument = (service) => {
|
|
|
2929
3185
|
${CSS_RESET}
|
|
2930
3186
|
:root {
|
|
2931
3187
|
color-scheme: light;
|
|
2932
|
-
--bg: #
|
|
3188
|
+
--bg: #f7f8fb;
|
|
2933
3189
|
--bg-elev: #ffffff;
|
|
2934
|
-
--bg-muted: #
|
|
2935
|
-
--bg-inset: #
|
|
2936
|
-
--line: #
|
|
2937
|
-
--line-strong: #
|
|
2938
|
-
--ink: #
|
|
2939
|
-
--muted: #
|
|
2940
|
-
--faint: #
|
|
2941
|
-
--accent: #
|
|
3190
|
+
--bg-muted: #f1f3f7;
|
|
3191
|
+
--bg-inset: #f8f9fc;
|
|
3192
|
+
--line: #e4e7ec;
|
|
3193
|
+
--line-strong: #cfd4dc;
|
|
3194
|
+
--ink: #101828;
|
|
3195
|
+
--muted: #475467;
|
|
3196
|
+
--faint: #667085;
|
|
3197
|
+
--accent: #6941c6;
|
|
3198
|
+
--accent-hover: #5933b4;
|
|
2942
3199
|
--accent-ink: #ffffff;
|
|
2943
|
-
--accent-soft: #
|
|
2944
|
-
--ok: #
|
|
2945
|
-
--ok-soft: #
|
|
2946
|
-
--warn: #
|
|
2947
|
-
--warn-soft: #
|
|
2948
|
-
--err: #
|
|
2949
|
-
--err-soft: #
|
|
2950
|
-
--
|
|
3200
|
+
--accent-soft: #f4f0ff;
|
|
3201
|
+
--ok: #067647;
|
|
3202
|
+
--ok-soft: #ecfdf3;
|
|
3203
|
+
--warn: #b54708;
|
|
3204
|
+
--warn-soft: #fffaeb;
|
|
3205
|
+
--err: #b42318;
|
|
3206
|
+
--err-soft: #fef3f2;
|
|
3207
|
+
--code-key: #6941c6;
|
|
3208
|
+
--code-string: #067647;
|
|
3209
|
+
--code-number: #175cd3;
|
|
3210
|
+
--code-boolean: #c11574;
|
|
3211
|
+
--code-null: #667085;
|
|
3212
|
+
--shadow-xs: 0 1px 2px rgb(16 24 40 / 0.05);
|
|
3213
|
+
--shadow-sm: 0 1px 3px rgb(16 24 40 / 0.08), 0 1px 2px rgb(16 24 40 / 0.04);
|
|
3214
|
+
--shadow-lg: 0 18px 48px rgb(16 24 40 / 0.16), 0 4px 12px rgb(16 24 40 / 0.08);
|
|
2951
3215
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2952
3216
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
2953
|
-
--radius:
|
|
2954
|
-
--header:
|
|
2955
|
-
--nav:
|
|
3217
|
+
--radius: 12px;
|
|
3218
|
+
--header: 72px;
|
|
3219
|
+
--nav: 224px;
|
|
2956
3220
|
}
|
|
2957
3221
|
:root[data-theme="dark"] {
|
|
2958
3222
|
color-scheme: dark;
|
|
2959
|
-
--bg: #
|
|
2960
|
-
--bg-elev: #
|
|
2961
|
-
--bg-muted: #
|
|
2962
|
-
--bg-inset: #
|
|
2963
|
-
--line: #
|
|
2964
|
-
--line-strong: #
|
|
2965
|
-
--ink: #
|
|
2966
|
-
--muted: #
|
|
2967
|
-
--faint: #
|
|
2968
|
-
--accent: #
|
|
2969
|
-
--accent-
|
|
2970
|
-
--accent-
|
|
2971
|
-
--
|
|
2972
|
-
--ok
|
|
2973
|
-
--
|
|
2974
|
-
--warn
|
|
2975
|
-
--
|
|
2976
|
-
--err
|
|
2977
|
-
--
|
|
3223
|
+
--bg: #0c0e14;
|
|
3224
|
+
--bg-elev: #151821;
|
|
3225
|
+
--bg-muted: #20232e;
|
|
3226
|
+
--bg-inset: #11141c;
|
|
3227
|
+
--line: #2b303d;
|
|
3228
|
+
--line-strong: #414858;
|
|
3229
|
+
--ink: #f5f7fa;
|
|
3230
|
+
--muted: #b6bdc9;
|
|
3231
|
+
--faint: #8d96a7;
|
|
3232
|
+
--accent: #9e77ed;
|
|
3233
|
+
--accent-hover: #b692f6;
|
|
3234
|
+
--accent-ink: #ffffff;
|
|
3235
|
+
--accent-soft: #2b2142;
|
|
3236
|
+
--ok: #75e0a7;
|
|
3237
|
+
--ok-soft: #102a20;
|
|
3238
|
+
--warn: #fec84b;
|
|
3239
|
+
--warn-soft: #302611;
|
|
3240
|
+
--err: #fda29b;
|
|
3241
|
+
--err-soft: #321817;
|
|
3242
|
+
--code-key: #c3a6ff;
|
|
3243
|
+
--code-string: #75e0a7;
|
|
3244
|
+
--code-number: #84adff;
|
|
3245
|
+
--code-boolean: #f9a8d4;
|
|
3246
|
+
--code-null: #98a2b3;
|
|
3247
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3248
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3249
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2978
3250
|
}
|
|
2979
3251
|
@media (prefers-color-scheme: dark) {
|
|
2980
3252
|
:root:not([data-theme="light"]) {
|
|
2981
3253
|
color-scheme: dark;
|
|
2982
|
-
--bg: #
|
|
2983
|
-
--bg-elev: #
|
|
2984
|
-
--bg-muted: #
|
|
2985
|
-
--bg-inset: #
|
|
2986
|
-
--line: #
|
|
2987
|
-
--line-strong: #
|
|
2988
|
-
--ink: #
|
|
2989
|
-
--muted: #
|
|
2990
|
-
--faint: #
|
|
2991
|
-
--accent: #
|
|
2992
|
-
--accent-
|
|
2993
|
-
--accent-
|
|
2994
|
-
--
|
|
2995
|
-
--ok
|
|
2996
|
-
--
|
|
2997
|
-
--warn
|
|
2998
|
-
--
|
|
2999
|
-
--err
|
|
3000
|
-
--
|
|
3254
|
+
--bg: #0c0e14;
|
|
3255
|
+
--bg-elev: #151821;
|
|
3256
|
+
--bg-muted: #20232e;
|
|
3257
|
+
--bg-inset: #11141c;
|
|
3258
|
+
--line: #2b303d;
|
|
3259
|
+
--line-strong: #414858;
|
|
3260
|
+
--ink: #f5f7fa;
|
|
3261
|
+
--muted: #b6bdc9;
|
|
3262
|
+
--faint: #8d96a7;
|
|
3263
|
+
--accent: #9e77ed;
|
|
3264
|
+
--accent-hover: #b692f6;
|
|
3265
|
+
--accent-ink: #ffffff;
|
|
3266
|
+
--accent-soft: #2b2142;
|
|
3267
|
+
--ok: #75e0a7;
|
|
3268
|
+
--ok-soft: #102a20;
|
|
3269
|
+
--warn: #fec84b;
|
|
3270
|
+
--warn-soft: #302611;
|
|
3271
|
+
--err: #fda29b;
|
|
3272
|
+
--err-soft: #321817;
|
|
3273
|
+
--code-key: #c3a6ff;
|
|
3274
|
+
--code-string: #75e0a7;
|
|
3275
|
+
--code-number: #84adff;
|
|
3276
|
+
--code-boolean: #f9a8d4;
|
|
3277
|
+
--code-null: #98a2b3;
|
|
3278
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3279
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3280
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
3001
3281
|
}
|
|
3002
3282
|
}
|
|
3003
3283
|
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
@@ -3015,13 +3295,17 @@ var renderAdminDocument = (service) => {
|
|
|
3015
3295
|
padding-right: env(safe-area-inset-right);
|
|
3016
3296
|
-webkit-text-size-adjust: 100%;
|
|
3017
3297
|
}
|
|
3018
|
-
|
|
3298
|
+
::selection { background: color-mix(in srgb, var(--accent) 28%, transparent); }
|
|
3299
|
+
:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px; }
|
|
3300
|
+
button, input, select, textarea { transition: border-color 140ms ease, box-shadow 140ms ease, transform 100ms ease; }
|
|
3301
|
+
button:disabled, input:disabled, select:disabled, textarea:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
3019
3302
|
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
3020
3303
|
header.top {
|
|
3021
|
-
position: sticky; top: 0; z-index:
|
|
3304
|
+
position: sticky; top: 0; z-index: 20;
|
|
3022
3305
|
display: flex; align-items: center; gap: 12px;
|
|
3023
|
-
min-height: var(--header); padding:
|
|
3024
|
-
background: var(--bg-elev);
|
|
3306
|
+
min-height: var(--header); padding: 12px 20px;
|
|
3307
|
+
background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
|
|
3308
|
+
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
3025
3309
|
border-bottom: 1px solid var(--line);
|
|
3026
3310
|
min-width: 0; max-width: 100%;
|
|
3027
3311
|
}
|
|
@@ -3029,12 +3313,21 @@ var renderAdminDocument = (service) => {
|
|
|
3029
3313
|
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
3030
3314
|
min-width: 0; max-width: 100%; margin-right: auto;
|
|
3031
3315
|
}
|
|
3032
|
-
.brand-
|
|
3033
|
-
.brand-
|
|
3316
|
+
.brand-lockup { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
3317
|
+
.brand-mark {
|
|
3318
|
+
width: 38px; height: 38px; display: grid; place-items: center; flex: none;
|
|
3319
|
+
border-radius: 11px; color: #fff;
|
|
3320
|
+
background: linear-gradient(145deg, #7f56d9 0%, #53389e 100%);
|
|
3321
|
+
box-shadow: 0 6px 16px rgb(105 65 198 / 0.24), inset 0 1px 0 rgb(255 255 255 / 0.25);
|
|
3322
|
+
}
|
|
3323
|
+
.brand-mark svg { width: 22px; height: 22px; }
|
|
3324
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; line-height: 1.25; }
|
|
3325
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
3034
3326
|
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
3035
3327
|
.vendor {
|
|
3036
3328
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
3037
3329
|
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
3330
|
+
padding: 5px 8px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-inset);
|
|
3038
3331
|
}
|
|
3039
3332
|
.vendor[hidden] { display: none; }
|
|
3040
3333
|
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
@@ -3046,12 +3339,17 @@ var renderAdminDocument = (service) => {
|
|
|
3046
3339
|
.vendor-sep { color: var(--faint); }
|
|
3047
3340
|
.vendor-link { color: inherit; text-decoration: none; }
|
|
3048
3341
|
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
3049
|
-
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items:
|
|
3050
|
-
|
|
3342
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
|
|
3343
|
+
.sync-status { align-self: center; display: flex; align-items: center; gap: 6px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3344
|
+
.sync-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
|
|
3345
|
+
.sync-status.busy::before { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); animation: pulse 1.2s ease-in-out infinite; }
|
|
3346
|
+
label.field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; font-weight: 600; color: var(--faint); }
|
|
3051
3347
|
label.field input, label.field select, .controls input[type="password"] {
|
|
3052
|
-
background-color: var(--bg-
|
|
3053
|
-
padding:
|
|
3348
|
+
background-color: var(--bg-elev); border: 1px solid var(--line-strong); border-radius: 9px;
|
|
3349
|
+
padding: 8px 10px; min-height: 38px; line-height: 1.2; box-shadow: var(--shadow-xs);
|
|
3054
3350
|
}
|
|
3351
|
+
label.field input:hover, label.field select:hover, textarea:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--line-strong)); }
|
|
3352
|
+
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; }
|
|
3055
3353
|
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
3056
3354
|
the text fields in the same form. Paint it like those fields. */
|
|
3057
3355
|
label.field select {
|
|
@@ -3068,12 +3366,20 @@ var renderAdminDocument = (service) => {
|
|
|
3068
3366
|
dialog .field > textarea { width: 100%; }
|
|
3069
3367
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
3070
3368
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
.btn
|
|
3369
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
|
3370
|
+
border-radius: 9px; min-height: 38px; padding: 0 13px; border: 1px solid var(--line-strong);
|
|
3371
|
+
background: var(--bg-elev); color: var(--ink); font-size: 13px; font-weight: 600; line-height: 1;
|
|
3372
|
+
box-shadow: var(--shadow-xs);
|
|
3373
|
+
}
|
|
3374
|
+
.icon-btn { width: 38px; padding: 0; }
|
|
3375
|
+
.icon-btn svg, .btn svg, .btn-primary svg { width: 16px; height: 16px; }
|
|
3376
|
+
.icon-btn:hover, .btn:hover { background: var(--bg-muted); border-color: var(--line-strong); }
|
|
3377
|
+
.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; }
|
|
3378
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
3379
|
+
.btn-quiet { background: transparent; border-color: transparent; box-shadow: none; }
|
|
3380
|
+
.btn-quiet:hover { background: var(--bg-muted); }
|
|
3381
|
+
.btn-danger { color: var(--err); border-color: color-mix(in srgb, var(--err) 25%, var(--line)); }
|
|
3382
|
+
.btn-danger:hover, .btn-danger.confirming { background: var(--err-soft); border-color: color-mix(in srgb, var(--err) 45%, var(--line)); }
|
|
3077
3383
|
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
3078
3384
|
so the press itself has to stay visible. */
|
|
3079
3385
|
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
@@ -3089,93 +3395,159 @@ var renderAdminDocument = (service) => {
|
|
|
3089
3395
|
flex: 1 0 auto; min-width: 0;
|
|
3090
3396
|
}
|
|
3091
3397
|
nav.side {
|
|
3092
|
-
|
|
3398
|
+
position: sticky; top: var(--header); align-self: start; height: calc(100vh - var(--header));
|
|
3399
|
+
border-right: 1px solid var(--line); padding: 18px 12px;
|
|
3093
3400
|
display: flex; flex-direction: column; gap: 4px;
|
|
3094
|
-
background: var(--bg-elev);
|
|
3401
|
+
background: var(--bg-elev); overflow-y: auto;
|
|
3095
3402
|
}
|
|
3403
|
+
.nav-label { padding: 0 10px 8px; color: var(--faint); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
|
|
3096
3404
|
nav.side button {
|
|
3405
|
+
position: relative; display: flex; align-items: center; gap: 10px; width: 100%;
|
|
3097
3406
|
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
3098
|
-
padding: 9px 10px; min-height:
|
|
3099
|
-
}
|
|
3100
|
-
nav.side button
|
|
3101
|
-
|
|
3102
|
-
.
|
|
3103
|
-
.
|
|
3104
|
-
.
|
|
3407
|
+
padding: 9px 10px; min-height: 42px; color: var(--muted); font-size: 13px; font-weight: 550;
|
|
3408
|
+
}
|
|
3409
|
+
nav.side button:hover { background: var(--bg-muted); color: var(--ink); }
|
|
3410
|
+
nav.side button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 650; }
|
|
3411
|
+
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); }
|
|
3412
|
+
.nav-icon { width: 18px; height: 18px; display: grid; place-items: center; flex: none; }
|
|
3413
|
+
.nav-icon svg { width: 18px; height: 18px; }
|
|
3414
|
+
main { padding: 32px clamp(20px, 4vw, 48px) 64px; max-width: 1320px; width: 100%; min-width: 0; }
|
|
3415
|
+
main[aria-busy="true"] .view { opacity: 0.62; }
|
|
3416
|
+
.view { animation: view-in 180ms ease-out; }
|
|
3417
|
+
.view[hidden] { display: none; }
|
|
3418
|
+
.view-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
|
|
3419
|
+
.view h2 { margin: 0 0 5px; font-size: clamp(24px, 3vw, 30px); line-height: 1.2; letter-spacing: -0.035em; }
|
|
3420
|
+
.view h3 { margin: 24px 0 10px; font-size: 15px; letter-spacing: -0.01em; }
|
|
3421
|
+
.lede { margin: 0; color: var(--muted); max-width: 72ch; font-size: 14px; }
|
|
3422
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 175px), 1fr)); gap: 14px; }
|
|
3105
3423
|
.card {
|
|
3106
3424
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
3107
|
-
padding:
|
|
3425
|
+
padding: 18px; box-shadow: var(--shadow-sm);
|
|
3108
3426
|
}
|
|
3109
|
-
.card
|
|
3110
|
-
.card
|
|
3427
|
+
.stat-card { position: relative; overflow: hidden; }
|
|
3428
|
+
.stat-card::after { content: ""; position: absolute; inset: 0 0 auto; height: 2px; background: linear-gradient(90deg, var(--accent), transparent 78%); opacity: 0.7; }
|
|
3429
|
+
.card .k { color: var(--faint); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
3430
|
+
.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; }
|
|
3111
3431
|
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
3112
|
-
.
|
|
3432
|
+
.section-card { background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-xs); overflow: hidden; }
|
|
3433
|
+
.section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
|
|
3434
|
+
.section-title h3 { margin: 0; }
|
|
3435
|
+
.split { display: grid; grid-template-columns: minmax(0, 250px) minmax(0, 1fr); gap: 18px; align-items: start; }
|
|
3436
|
+
#collections { padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); box-shadow: var(--shadow-xs); }
|
|
3113
3437
|
.collection {
|
|
3114
3438
|
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
3115
|
-
border-radius:
|
|
3439
|
+
border-radius: 9px; padding: 10px 11px; min-height: 48px; color: var(--ink); cursor: pointer;
|
|
3116
3440
|
}
|
|
3117
|
-
.collection
|
|
3441
|
+
.collection:hover { background: var(--bg-muted); }
|
|
3442
|
+
.collection[aria-current="true"] { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 18%, var(--line)); color: var(--accent); }
|
|
3118
3443
|
.collection small { display: block; color: var(--faint); }
|
|
3119
3444
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
3120
|
-
.chip {
|
|
3121
|
-
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
3122
|
-
border-radius: 999px; padding: 4px 8px;
|
|
3123
|
-
}
|
|
3445
|
+
.chip { font-family: var(--mono); font-size: 11px; background: var(--bg-muted); border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; }
|
|
3124
3446
|
.chip i { color: var(--faint); font-style: normal; }
|
|
3125
3447
|
.sql-layout {
|
|
3126
3448
|
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
3127
3449
|
}
|
|
3128
|
-
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
3129
|
-
.sql-tables button { text-align: left; }
|
|
3450
|
+
.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); }
|
|
3451
|
+
.sql-tables button { text-align: left; border: 0; border-radius: 8px; padding: 9px 10px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
3452
|
+
.sql-tables button:hover, .sql-tables button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); }
|
|
3130
3453
|
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
3131
|
-
table { width: 100%; border-
|
|
3132
|
-
|
|
3133
|
-
th {
|
|
3454
|
+
.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); }
|
|
3455
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
3456
|
+
th, td { text-align: left; padding: 11px 13px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
3457
|
+
th { color: var(--faint); background: var(--bg-inset); font-weight: 650; font-size: 11px; text-transform: uppercase; letter-spacing: 0.045em; }
|
|
3458
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
3134
3459
|
tr.clickable { cursor: pointer; }
|
|
3135
|
-
tr.clickable:hover { background: var(--
|
|
3460
|
+
tr.clickable:hover { background: var(--accent-soft); }
|
|
3136
3461
|
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
3137
3462
|
pre {
|
|
3138
3463
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
3139
|
-
background: var(--bg-inset); border-radius: 10px; padding:
|
|
3140
|
-
}
|
|
3141
|
-
.
|
|
3464
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 10px; padding: 14px; font-size: 12px; line-height: 1.65;
|
|
3465
|
+
}
|
|
3466
|
+
.json-key { color: var(--code-key); }
|
|
3467
|
+
.json-string { color: var(--code-string); }
|
|
3468
|
+
.json-number { color: var(--code-number); }
|
|
3469
|
+
.json-boolean { color: var(--code-boolean); }
|
|
3470
|
+
.json-null { color: var(--code-null); font-style: italic; }
|
|
3471
|
+
.json-inline { display: block; max-width: 44rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
3472
|
+
.json-view { position: relative; max-height: 26rem; overflow: auto; }
|
|
3473
|
+
.json-editor { position: relative; min-height: 190px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--bg-inset); overflow: hidden; }
|
|
3474
|
+
.json-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); }
|
|
3475
|
+
.json-editor.invalid { border-color: var(--err); }
|
|
3476
|
+
.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; }
|
|
3477
|
+
.json-editor pre { position: absolute; inset: 0; pointer-events: none; background: transparent; }
|
|
3478
|
+
.json-editor textarea { position: relative; z-index: 1; resize: vertical; background: transparent; color: transparent; caret-color: var(--ink); -webkit-text-fill-color: transparent; }
|
|
3479
|
+
.json-editor textarea::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); }
|
|
3480
|
+
.editor-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; }
|
|
3481
|
+
.json-status { display: inline-flex; align-items: center; gap: 6px; color: var(--ok); font-size: 11px; }
|
|
3482
|
+
.json-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
3483
|
+
.json-status.invalid { color: var(--err); }
|
|
3484
|
+
.format-json { border: 0; padding: 3px 5px; background: transparent; color: var(--accent); font-size: 11px; font-weight: 650; cursor: pointer; }
|
|
3485
|
+
.row-actions { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 14px 0; }
|
|
3142
3486
|
.banner {
|
|
3143
|
-
display: none;
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3487
|
+
display: none; position: sticky; top: calc(var(--header) + 12px); z-index: 15;
|
|
3488
|
+
align-items: center; gap: 10px; margin: 0 0 16px; padding: 11px 12px 11px 14px;
|
|
3489
|
+
border: 1px solid color-mix(in srgb, var(--err) 30%, var(--line)); border-radius: 10px;
|
|
3490
|
+
background: var(--err-soft); color: var(--err); box-shadow: var(--shadow-sm); font-size: 13px;
|
|
3491
|
+
}
|
|
3492
|
+
.banner.show { display: flex; }
|
|
3493
|
+
.banner span { flex: 1; }
|
|
3494
|
+
.banner button { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 7px; background: transparent; color: currentColor; cursor: pointer; }
|
|
3495
|
+
.banner button:hover { background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
3496
|
+
.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); }
|
|
3148
3497
|
dialog {
|
|
3149
|
-
border: 1px solid var(--line); border-radius:
|
|
3150
|
-
color: var(--ink); width: min(
|
|
3151
|
-
}
|
|
3152
|
-
dialog
|
|
3153
|
-
dialog
|
|
3498
|
+
border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--bg-elev);
|
|
3499
|
+
color: var(--ink); width: min(720px, calc(100% - 24px)); max-width: calc(100vw - 24px); box-shadow: var(--shadow-lg);
|
|
3500
|
+
}
|
|
3501
|
+
dialog[open] { animation: dialog-in 160ms ease-out; }
|
|
3502
|
+
dialog::backdrop { background: rgb(12 14 20 / 0.58); backdrop-filter: blur(3px); }
|
|
3503
|
+
dialog form, .dialog-body { padding: 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
3504
|
+
.dialog-heading { display: flex; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
|
3505
|
+
.dialog-heading strong { font-size: 18px; letter-spacing: -0.02em; }
|
|
3506
|
+
.dialog-heading span { color: var(--muted); font-size: 12px; }
|
|
3154
3507
|
textarea {
|
|
3155
3508
|
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
3156
3509
|
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
3157
3510
|
font-family: var(--mono); font-size: 12px;
|
|
3158
3511
|
}
|
|
3159
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
3160
|
-
.
|
|
3512
|
+
.dialog-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding-top: 4px; }
|
|
3513
|
+
.dialog-actions .btn-danger { margin-right: auto; }
|
|
3514
|
+
.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); }
|
|
3161
3515
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
3162
3516
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
3163
3517
|
.status-2 { color: var(--ok); }
|
|
3164
3518
|
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
3165
3519
|
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
3520
|
+
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
3521
|
+
.form-grid .wide { grid-column: span 2; }
|
|
3166
3522
|
.route {
|
|
3167
|
-
display: grid; grid-template-columns:
|
|
3168
|
-
padding:
|
|
3169
|
-
}
|
|
3170
|
-
.
|
|
3523
|
+
display: grid; grid-template-columns: 84px minmax(0, 1fr) auto; gap: 10px; align-items: center;
|
|
3524
|
+
padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--bg-elev);
|
|
3525
|
+
}
|
|
3526
|
+
.route:first-child { border-radius: var(--radius) var(--radius) 0 0; }
|
|
3527
|
+
.route:last-child { border-bottom: 0; border-radius: 0 0 var(--radius) var(--radius); }
|
|
3528
|
+
.route-tools { display: flex; align-items: flex-end; gap: 12px; margin-bottom: 12px; }
|
|
3529
|
+
.route-tools .field { flex: 1; }
|
|
3530
|
+
.route-tools input { width: 100%; }
|
|
3531
|
+
.route-count { padding-bottom: 10px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3532
|
+
#route-list { max-height: 34rem; border: 1px solid var(--line); border-radius: var(--radius); overflow: auto; box-shadow: var(--shadow-xs); }
|
|
3533
|
+
.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; }
|
|
3534
|
+
@keyframes view-in { from { transform: translateY(3px); } to { transform: translateY(0); } }
|
|
3535
|
+
@keyframes dialog-in { from { transform: translateY(8px) scale(0.99); } to { transform: translateY(0) scale(1); } }
|
|
3536
|
+
@keyframes pulse { 50% { opacity: 0.45; } }
|
|
3537
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
|
|
3538
|
+
@media (forced-colors: active) { .json-editor pre { display: none; } .json-editor textarea { color: CanvasText; -webkit-text-fill-color: CanvasText; } }
|
|
3171
3539
|
@media (max-width: 800px) {
|
|
3540
|
+
:root { --header: 0px; }
|
|
3172
3541
|
header.top {
|
|
3173
|
-
align-items: stretch; flex-direction: column;
|
|
3174
|
-
padding
|
|
3542
|
+
position: relative; align-items: stretch; flex-direction: column;
|
|
3543
|
+
padding: max(12px, env(safe-area-inset-top)) 14px 12px;
|
|
3175
3544
|
}
|
|
3176
3545
|
input, select, textarea { font-size: 16px; }
|
|
3177
3546
|
.brand, .vendor, .controls { width: 100%; }
|
|
3547
|
+
.brand { gap: 10px; }
|
|
3548
|
+
.vendor { order: 2; border-radius: 9px; }
|
|
3178
3549
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
3550
|
+
.sync-status { display: none; }
|
|
3179
3551
|
/* The layout fills the screen. A single-column grid stretches every auto
|
|
3180
3552
|
row, so the tab bar grew with the leftover height and the buttons floated
|
|
3181
3553
|
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
@@ -3185,14 +3557,23 @@ var renderAdminDocument = (service) => {
|
|
|
3185
3557
|
align-content: start;
|
|
3186
3558
|
}
|
|
3187
3559
|
nav.side {
|
|
3188
|
-
align-self: start; height: auto;
|
|
3560
|
+
position: sticky; top: 0; z-index: 12; align-self: start; height: auto;
|
|
3189
3561
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
3190
3562
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
3191
|
-
padding: 8px;
|
|
3563
|
+
padding: 8px; box-shadow: var(--shadow-xs);
|
|
3192
3564
|
}
|
|
3193
|
-
nav
|
|
3565
|
+
.nav-label { display: none; }
|
|
3566
|
+
nav.side button { width: auto; white-space: nowrap; flex: none; }
|
|
3567
|
+
nav.side button[aria-current="true"]::before { inset: auto 9px -8px; width: auto; height: 3px; border-radius: 3px 3px 0 0; }
|
|
3194
3568
|
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
3195
|
-
|
|
3569
|
+
.form-grid { grid-template-columns: minmax(0, 1fr); }
|
|
3570
|
+
.form-grid .wide { grid-column: auto; }
|
|
3571
|
+
main { padding: 22px 14px 48px; }
|
|
3572
|
+
.view-head { margin-bottom: 18px; }
|
|
3573
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
3574
|
+
.card { padding: 14px; }
|
|
3575
|
+
.card .v { font-size: 20px; }
|
|
3576
|
+
.table-shell { border: 0; background: transparent; box-shadow: none; overflow: visible; }
|
|
3196
3577
|
table, thead, tbody, tr, th, td { display: block; }
|
|
3197
3578
|
thead { display: none; }
|
|
3198
3579
|
tr {
|
|
@@ -3207,50 +3588,63 @@ var renderAdminDocument = (service) => {
|
|
|
3207
3588
|
.route { grid-template-columns: 72px 1fr; }
|
|
3208
3589
|
.route button { grid-column: 1 / -1; }
|
|
3209
3590
|
}
|
|
3591
|
+
@media (max-width: 460px) {
|
|
3592
|
+
.controls label.field { min-width: calc(50% - 4px); }
|
|
3593
|
+
.controls .icon-btn, .controls .btn { flex: 1; }
|
|
3594
|
+
.grid { grid-template-columns: minmax(0, 1fr); }
|
|
3595
|
+
.dialog-actions { flex-wrap: wrap; }
|
|
3596
|
+
.dialog-actions button { flex: 1; }
|
|
3597
|
+
.dialog-actions .btn-danger { flex-basis: 100%; margin-right: 0; }
|
|
3598
|
+
}
|
|
3210
3599
|
</style>
|
|
3211
3600
|
</head>
|
|
3212
3601
|
<body>
|
|
3213
3602
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
3214
3603
|
<header class="top">
|
|
3215
3604
|
<div class="brand">
|
|
3216
|
-
<div class="brand-
|
|
3217
|
-
<
|
|
3218
|
-
|
|
3605
|
+
<div class="brand-lockup">
|
|
3606
|
+
<div class="brand-mark" aria-hidden="true">
|
|
3607
|
+
<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>
|
|
3608
|
+
</div>
|
|
3609
|
+
<div class="brand-id">
|
|
3610
|
+
<strong>${name}</strong>
|
|
3611
|
+
<span>Mock admin</span>
|
|
3612
|
+
</div>
|
|
3219
3613
|
</div>
|
|
3220
3614
|
<div class="vendor" id="vendor" hidden></div>
|
|
3221
3615
|
</div>
|
|
3222
3616
|
<div class="controls">
|
|
3617
|
+
<span class="sync-status" id="sync-status" role="status" aria-live="polite">Ready</span>
|
|
3223
3618
|
<label class="field">Namespace
|
|
3224
3619
|
<select id="namespace" aria-label="Namespace"></select>
|
|
3225
3620
|
</label>
|
|
3226
3621
|
<label class="field">Admin key
|
|
3227
3622
|
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
3228
3623
|
</label>
|
|
3229
|
-
<button class="icon-btn" id="theme" type="button" aria-label="
|
|
3230
|
-
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
3624
|
+
<button class="icon-btn" id="theme" type="button" aria-label="Use dark theme" title="Use dark theme"></button>
|
|
3625
|
+
<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>
|
|
3231
3626
|
</div>
|
|
3232
3627
|
</header>
|
|
3233
3628
|
<div class="layout">
|
|
3234
3629
|
<nav class="side" id="nav" aria-label="Admin sections">
|
|
3235
|
-
<
|
|
3236
|
-
<button type="button" data-view="
|
|
3237
|
-
<button type="button" data-view="
|
|
3238
|
-
<button type="button" data-view="
|
|
3239
|
-
<button type="button" data-view="
|
|
3240
|
-
<button type="button" data-view="
|
|
3630
|
+
<span class="nav-label">Workspace</span>
|
|
3631
|
+
<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>
|
|
3632
|
+
<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>
|
|
3633
|
+
<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>
|
|
3634
|
+
<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>
|
|
3635
|
+
<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>
|
|
3636
|
+
<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>
|
|
3241
3637
|
</nav>
|
|
3242
3638
|
<main id="views">
|
|
3243
|
-
<
|
|
3639
|
+
<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>
|
|
3244
3640
|
<section class="view" id="view-overview">
|
|
3245
|
-
<h2>Overview</h2>
|
|
3246
|
-
<p class="lede">Health, clock, and the collections this namespace is holding.</p>
|
|
3641
|
+
<div class="view-head"><div><h2>Overview</h2><p class="lede">Health, clock, and the collections this namespace is holding.</p></div></div>
|
|
3247
3642
|
<div class="grid" id="overview-cards"></div>
|
|
3248
3643
|
<h3>Recent requests</h3>
|
|
3249
3644
|
<div id="overview-requests"></div>
|
|
3250
3645
|
</section>
|
|
3251
3646
|
<section class="view" id="view-state" hidden>
|
|
3252
|
-
<h2>State</h2>
|
|
3253
|
-
<p class="lede">Every collection in this namespace. Declared fields stay visible before any row exists; stored rows add whatever else they contain.</p>
|
|
3647
|
+
<div class="view-head"><div><h2>State</h2><p class="lede">Inspect and edit every collection in this namespace.</p></div></div>
|
|
3254
3648
|
<div class="split">
|
|
3255
3649
|
<div id="collections"></div>
|
|
3256
3650
|
<div>
|
|
@@ -3263,8 +3657,7 @@ var renderAdminDocument = (service) => {
|
|
|
3263
3657
|
</div>
|
|
3264
3658
|
</section>
|
|
3265
3659
|
<section class="view" id="view-clock" hidden>
|
|
3266
|
-
<h2>Clock</h2>
|
|
3267
|
-
<p class="lede">The clock every timestamp in this mock reads.</p>
|
|
3660
|
+
<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>
|
|
3268
3661
|
<div class="card" id="clock-readout"></div>
|
|
3269
3662
|
<div class="row-actions">
|
|
3270
3663
|
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
@@ -3283,21 +3676,22 @@ var renderAdminDocument = (service) => {
|
|
|
3283
3676
|
</form>
|
|
3284
3677
|
</section>
|
|
3285
3678
|
<section class="view" id="view-faults" hidden>
|
|
3286
|
-
<h2>Faults</h2>
|
|
3287
|
-
<p class="lede">Inject a response, a delay, or a named preset. Rules apply to this namespace.</p>
|
|
3679
|
+
<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>
|
|
3288
3680
|
<form id="fault-form" class="card stack">
|
|
3289
|
-
<
|
|
3290
|
-
<
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
<
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
<
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
<
|
|
3300
|
-
|
|
3681
|
+
<div class="form-grid">
|
|
3682
|
+
<label class="field wide">Preset
|
|
3683
|
+
<select id="fault-preset"><option value="">Custom rule</option></select>
|
|
3684
|
+
</label>
|
|
3685
|
+
<label class="field">Status
|
|
3686
|
+
<input id="fault-status" inputmode="numeric" placeholder="503">
|
|
3687
|
+
</label>
|
|
3688
|
+
<label class="field wide">Path prefix
|
|
3689
|
+
<input id="fault-path" placeholder="/v1">
|
|
3690
|
+
</label>
|
|
3691
|
+
<label class="field">Method
|
|
3692
|
+
<input id="fault-method" placeholder="GET">
|
|
3693
|
+
</label>
|
|
3694
|
+
</div>
|
|
3301
3695
|
<div class="dialog-actions">
|
|
3302
3696
|
<button class="btn-primary" type="submit">Add fault</button>
|
|
3303
3697
|
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
@@ -3306,26 +3700,31 @@ var renderAdminDocument = (service) => {
|
|
|
3306
3700
|
<div id="fault-list"></div>
|
|
3307
3701
|
</section>
|
|
3308
3702
|
<section class="view" id="view-journal" hidden>
|
|
3309
|
-
<h2>Journal</h2>
|
|
3310
|
-
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
3311
|
-
<div class="row-actions">
|
|
3312
|
-
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
3313
|
-
</div>
|
|
3703
|
+
<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>
|
|
3314
3704
|
<div id="journal"></div>
|
|
3315
3705
|
</section>
|
|
3316
3706
|
<section class="view" id="view-routes" hidden>
|
|
3317
|
-
<h2>Routes</h2>
|
|
3318
|
-
<
|
|
3707
|
+
<div class="view-head"><div><h2>Routes</h2><p class="lede">Explore shared controls and service-specific admin endpoints.</p></div></div>
|
|
3708
|
+
<div class="route-tools">
|
|
3709
|
+
<label class="field">Filter routes
|
|
3710
|
+
<input id="route-search" type="search" placeholder="Method, path, or extension" autocomplete="off">
|
|
3711
|
+
</label>
|
|
3712
|
+
<span class="route-count" id="route-count"></span>
|
|
3713
|
+
</div>
|
|
3319
3714
|
<div id="route-list"></div>
|
|
3320
|
-
<form id="route-form" class="card stack">
|
|
3715
|
+
<form id="route-form" class="card stack" style="margin-top: 18px">
|
|
3321
3716
|
<strong id="route-title">Select a route</strong>
|
|
3322
3717
|
<label class="field">JSON body
|
|
3323
|
-
<
|
|
3718
|
+
<div class="json-editor" id="route-editor" hidden>
|
|
3719
|
+
<pre aria-hidden="true"><code id="route-highlight"></code></pre>
|
|
3720
|
+
<textarea id="route-body" placeholder="{ }" spellcheck="false" aria-describedby="route-json-status" disabled></textarea>
|
|
3721
|
+
</div>
|
|
3722
|
+
<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>
|
|
3324
3723
|
</label>
|
|
3325
3724
|
<div class="dialog-actions">
|
|
3326
|
-
<button class="btn-primary" type="submit">Send</button>
|
|
3725
|
+
<button class="btn-primary" id="route-send" type="submit" disabled>Send request</button>
|
|
3327
3726
|
</div>
|
|
3328
|
-
<pre id="route-result">Response will show here.</pre>
|
|
3727
|
+
<pre class="json-view" id="route-result"><code>Response will show here.</code></pre>
|
|
3329
3728
|
</form>
|
|
3330
3729
|
</section>
|
|
3331
3730
|
</main>
|
|
@@ -3333,12 +3732,16 @@ var renderAdminDocument = (service) => {
|
|
|
3333
3732
|
</div>
|
|
3334
3733
|
<dialog id="editor">
|
|
3335
3734
|
<form method="dialog" id="editor-form">
|
|
3336
|
-
<strong id="editor-title">Record</strong>
|
|
3735
|
+
<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>
|
|
3337
3736
|
<label class="field">Id
|
|
3338
3737
|
<input id="editor-id" required>
|
|
3339
3738
|
</label>
|
|
3340
3739
|
<label class="field">JSON value
|
|
3341
|
-
<
|
|
3740
|
+
<div class="json-editor" id="record-editor">
|
|
3741
|
+
<pre aria-hidden="true"><code id="editor-highlight"></code></pre>
|
|
3742
|
+
<textarea id="editor-value" spellcheck="false" aria-describedby="editor-json-status"></textarea>
|
|
3743
|
+
</div>
|
|
3744
|
+
<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>
|
|
3342
3745
|
</label>
|
|
3343
3746
|
<div class="dialog-actions">
|
|
3344
3747
|
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
@@ -3380,8 +3783,8 @@ var describeExtension = (extension) => {
|
|
|
3380
3783
|
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
3381
3784
|
};
|
|
3382
3785
|
};
|
|
3383
|
-
var adminUiRoutes = (service, ui) => {
|
|
3384
|
-
const shell = () => renderAdminDocument(service);
|
|
3786
|
+
var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
3787
|
+
const shell = () => renderAdminDocument(service, adminPrefix);
|
|
3385
3788
|
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
3386
3789
|
return {
|
|
3387
3790
|
"GET /ui": () => htmlResponse(document3()),
|
|
@@ -4082,7 +4485,7 @@ var stateAdminRoutes = (routes) => {
|
|
|
4082
4485
|
};
|
|
4083
4486
|
|
|
4084
4487
|
// ../core/dist/version.js
|
|
4085
|
-
var PACKAGE_VERSION = true ? "
|
|
4488
|
+
var PACKAGE_VERSION = true ? "2.0.0" : UNRELEASED_VERSION;
|
|
4086
4489
|
|
|
4087
4490
|
// ../core/dist/signing.js
|
|
4088
4491
|
var encoder = new TextEncoder();
|
|
@@ -4175,7 +4578,9 @@ var createWebhookHub = (options) => {
|
|
|
4175
4578
|
const faults = /* @__PURE__ */ new Map();
|
|
4176
4579
|
const held = /* @__PURE__ */ new Map();
|
|
4177
4580
|
const inFlight = /* @__PURE__ */ new Set();
|
|
4581
|
+
const controllers = /* @__PURE__ */ new Map();
|
|
4178
4582
|
let immediate = Promise.resolve();
|
|
4583
|
+
const snapshots = /* @__PURE__ */ new WeakMap();
|
|
4179
4584
|
const track = (work) => {
|
|
4180
4585
|
inFlight.add(work);
|
|
4181
4586
|
void work.finally(() => inFlight.delete(work));
|
|
@@ -4196,6 +4601,7 @@ var createWebhookHub = (options) => {
|
|
|
4196
4601
|
responseBody: null
|
|
4197
4602
|
};
|
|
4198
4603
|
const controller = new AbortController();
|
|
4604
|
+
controllers.set(delivery.id, controller);
|
|
4199
4605
|
const timer = scheduleTimer(() => controller.abort(), timeoutMs);
|
|
4200
4606
|
try {
|
|
4201
4607
|
const signed = await options.signer({
|
|
@@ -4225,6 +4631,7 @@ var createWebhookHub = (options) => {
|
|
|
4225
4631
|
} catch (error) {
|
|
4226
4632
|
record2.error = controller.signal.aborted ? `timed out after ${timeoutMs}ms` : error instanceof Error ? error.message : String(error);
|
|
4227
4633
|
} finally {
|
|
4634
|
+
controllers.delete(delivery.id);
|
|
4228
4635
|
cancel(timer);
|
|
4229
4636
|
record2.durationMs = now() - started;
|
|
4230
4637
|
delivery.attempts.push(record2);
|
|
@@ -4242,6 +4649,8 @@ var createWebhookHub = (options) => {
|
|
|
4242
4649
|
if (!payloads.has(delivery.id))
|
|
4243
4650
|
return;
|
|
4244
4651
|
await attempt(delivery).then((ok) => {
|
|
4652
|
+
if (!payloads.has(delivery.id))
|
|
4653
|
+
return;
|
|
4245
4654
|
if (ok)
|
|
4246
4655
|
delivery.state = "delivered";
|
|
4247
4656
|
else
|
|
@@ -4303,6 +4712,37 @@ var createWebhookHub = (options) => {
|
|
|
4303
4712
|
fanOut(message);
|
|
4304
4713
|
};
|
|
4305
4714
|
const hub = {
|
|
4715
|
+
pending(namespace) {
|
|
4716
|
+
return inFlight.size + (held.get(namespace)?.length ?? 0) + [...deliveries.values()].filter((d) => d.namespace === namespace && d.state === "pending").length;
|
|
4717
|
+
},
|
|
4718
|
+
snapshot(namespace) {
|
|
4719
|
+
if (hub.pending(namespace) > 0)
|
|
4720
|
+
throw new Error("webhooks are not quiescent");
|
|
4721
|
+
const handle = {};
|
|
4722
|
+
snapshots.set(handle, structuredClone({
|
|
4723
|
+
namespace,
|
|
4724
|
+
endpoints: own.get(namespace) ?? [],
|
|
4725
|
+
messages: messages.filter((m) => m.namespace === namespace),
|
|
4726
|
+
deliveries: [...deliveries].filter(([, d]) => d.namespace === namespace),
|
|
4727
|
+
payloads: [...payloads].filter(([, p]) => p.message.namespace === namespace),
|
|
4728
|
+
faults: faults.get(namespace) ?? []
|
|
4729
|
+
}));
|
|
4730
|
+
return handle;
|
|
4731
|
+
},
|
|
4732
|
+
restore(namespace, handle) {
|
|
4733
|
+
const point = typeof handle === "object" && handle !== null ? snapshots.get(handle) : void 0;
|
|
4734
|
+
if (!point || point.namespace !== namespace || hub.pending(namespace) > 0)
|
|
4735
|
+
throw new Error("invalid webhook restore");
|
|
4736
|
+
hub.clear(namespace);
|
|
4737
|
+
const copied = structuredClone(point);
|
|
4738
|
+
own.set(namespace, copied.endpoints);
|
|
4739
|
+
messages.push(...copied.messages);
|
|
4740
|
+
for (const [id2, delivery] of copied.deliveries)
|
|
4741
|
+
deliveries.set(id2, delivery);
|
|
4742
|
+
for (const [id2, payload] of copied.payloads)
|
|
4743
|
+
payloads.set(id2, payload);
|
|
4744
|
+
faults.set(namespace, copied.faults);
|
|
4745
|
+
},
|
|
4306
4746
|
publish(input) {
|
|
4307
4747
|
const contentType = input.contentType ?? (input.form ? "application/x-www-form-urlencoded" : "application/json");
|
|
4308
4748
|
const body = typeof input.body === "string" ? input.body : input.form ? new URLSearchParams(input.form).toString() : JSON.stringify(input.body);
|
|
@@ -4393,6 +4833,7 @@ var createWebhookHub = (options) => {
|
|
|
4393
4833
|
for (const [id2, delivery] of deliveries) {
|
|
4394
4834
|
if (namespace !== void 0 && delivery.namespace !== namespace)
|
|
4395
4835
|
continue;
|
|
4836
|
+
controllers.get(id2)?.abort();
|
|
4396
4837
|
const timer = pending.get(id2);
|
|
4397
4838
|
if (timer !== void 0)
|
|
4398
4839
|
cancel(timer);
|
|
@@ -4417,7 +4858,10 @@ var createWebhookHub = (options) => {
|
|
|
4417
4858
|
};
|
|
4418
4859
|
return hub;
|
|
4419
4860
|
};
|
|
4420
|
-
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4861
|
+
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4862
|
+
status,
|
|
4863
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4864
|
+
});
|
|
4421
4865
|
var adminError3 = (status, message) => json4(status, { error: { type: "mockingbird_admin", message } });
|
|
4422
4866
|
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4423
4867
|
var parseEndpoint = (value) => {
|
|
@@ -4525,7 +4969,6 @@ var AT_HEADER = "x-mockingbird-at";
|
|
|
4525
4969
|
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
4526
4970
|
var DEFAULT_NAMESPACE = "default";
|
|
4527
4971
|
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4528
|
-
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
4529
4972
|
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4530
4973
|
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
4531
4974
|
var effects = /* @__PURE__ */ new WeakMap();
|
|
@@ -4566,6 +5009,22 @@ var operationMatcher = (document3) => {
|
|
|
4566
5009
|
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
4567
5010
|
};
|
|
4568
5011
|
var createRuntime = (options) => {
|
|
5012
|
+
const adminPrefix = resolveAdminPrefix(options.adminPrefix);
|
|
5013
|
+
const internalPaths = Object.entries(options.document?.paths ?? {}).filter(([, item]) => item["x-mockingbird-internal"] === true).map(([path]) => path);
|
|
5014
|
+
for (const path of internalPaths) {
|
|
5015
|
+
if (!path.startsWith(`${ADMIN_PREFIX}/blobs/`))
|
|
5016
|
+
throw new Error(`internal transport ${path} must live under ${ADMIN_PREFIX}/blobs/`);
|
|
5017
|
+
}
|
|
5018
|
+
assertAdminPrefixAvailable(adminPrefix, Object.keys(options.document?.paths ?? {}).filter((path) => !internalPaths.includes(path)));
|
|
5019
|
+
const internalMatch = (path) => {
|
|
5020
|
+
for (const pattern of internalPaths) {
|
|
5021
|
+
const actual = path.split("/");
|
|
5022
|
+
const expected = pattern.split("/");
|
|
5023
|
+
if (actual.length === expected.length && expected.every((part, i) => part.startsWith("{") || part === actual[i]))
|
|
5024
|
+
return pattern;
|
|
5025
|
+
}
|
|
5026
|
+
return void 0;
|
|
5027
|
+
};
|
|
4569
5028
|
assertAdminUi(options.adminUi);
|
|
4570
5029
|
const sqlite = bootSqlite(options.sqlite);
|
|
4571
5030
|
const clock = options.clock ?? createClock();
|
|
@@ -4580,6 +5039,20 @@ var createRuntime = (options) => {
|
|
|
4580
5039
|
const instances = /* @__PURE__ */ new Map();
|
|
4581
5040
|
const publicNamespaces = /* @__PURE__ */ new Set();
|
|
4582
5041
|
const branchRngs = /* @__PURE__ */ new Map();
|
|
5042
|
+
const namespaceClocks = /* @__PURE__ */ new Map();
|
|
5043
|
+
const namespaceForStorage = /* @__PURE__ */ new Map();
|
|
5044
|
+
let isolatedNamespaces = false;
|
|
5045
|
+
const namespaceClock = (name = DEFAULT_NAMESPACE) => {
|
|
5046
|
+
if (!isolatedNamespaces || name === DEFAULT_NAMESPACE)
|
|
5047
|
+
return clock;
|
|
5048
|
+
let found = namespaceClocks.get(name);
|
|
5049
|
+
if (!found) {
|
|
5050
|
+
found = createClock();
|
|
5051
|
+
namespaceClocks.set(name, found);
|
|
5052
|
+
}
|
|
5053
|
+
return found;
|
|
5054
|
+
};
|
|
5055
|
+
const fleetPoints = /* @__PURE__ */ new WeakMap();
|
|
4583
5056
|
const timelines = /* @__PURE__ */ new Map();
|
|
4584
5057
|
const branchStorage = /* @__PURE__ */ new Map();
|
|
4585
5058
|
const captured = /* @__PURE__ */ new Map();
|
|
@@ -4593,17 +5066,22 @@ var createRuntime = (options) => {
|
|
|
4593
5066
|
if (!NAMESPACE_PATTERN.test(key) || !NAMESPACE_PATTERN.test(publicNamespace)) {
|
|
4594
5067
|
throw new RangeError(`namespace must match ${NAMESPACE_PATTERN}: ${JSON.stringify(publicNamespace)}`);
|
|
4595
5068
|
}
|
|
5069
|
+
const createdContextRng = isolatedRng ?? (isolatedNamespaces && key !== DEFAULT_NAMESPACE ? createRng(options.seed ?? 0) : rng);
|
|
4596
5070
|
const created = options.create({
|
|
4597
5071
|
namespace: storageNamespace(key),
|
|
4598
5072
|
publicNamespace,
|
|
5073
|
+
adminPrefix,
|
|
4599
5074
|
sqlite,
|
|
4600
|
-
clock,
|
|
4601
|
-
rng:
|
|
5075
|
+
clock: namespaceClock(publicNamespace),
|
|
5076
|
+
rng: createdContextRng
|
|
4602
5077
|
});
|
|
4603
5078
|
instances.set(key, created);
|
|
5079
|
+
namespaceForStorage.set(key, publicNamespace);
|
|
4604
5080
|
publicNamespaces.add(publicNamespace);
|
|
4605
5081
|
if (isolatedRng)
|
|
4606
5082
|
branchRngs.set(key, isolatedRng);
|
|
5083
|
+
else if (isolatedNamespaces && key !== DEFAULT_NAMESPACE)
|
|
5084
|
+
branchRngs.set(key, createdContextRng);
|
|
4607
5085
|
return created;
|
|
4608
5086
|
};
|
|
4609
5087
|
const instance = (name = DEFAULT_NAMESPACE) => instanceFor(name);
|
|
@@ -4621,7 +5099,7 @@ var createRuntime = (options) => {
|
|
|
4621
5099
|
captured.set(storage, snapshot2);
|
|
4622
5100
|
return Object.freeze({
|
|
4623
5101
|
snapshot: snapshot2,
|
|
4624
|
-
clock: Object.freeze(
|
|
5102
|
+
clock: Object.freeze(namespaceClock(namespaceForStorage.get(storage) ?? storage).state()),
|
|
4625
5103
|
rngState: (branchRngs.get(storage) ?? rng).state()
|
|
4626
5104
|
});
|
|
4627
5105
|
};
|
|
@@ -4631,7 +5109,7 @@ var createRuntime = (options) => {
|
|
|
4631
5109
|
return found;
|
|
4632
5110
|
instance(name);
|
|
4633
5111
|
found = new Timeline({
|
|
4634
|
-
now:
|
|
5112
|
+
now: namespaceClock(name).now,
|
|
4635
5113
|
...options.maxCheckpoints !== void 0 ? { maxCheckpoints: options.maxCheckpoints } : {}
|
|
4636
5114
|
});
|
|
4637
5115
|
found.commit(capture(name));
|
|
@@ -4658,12 +5136,13 @@ var createRuntime = (options) => {
|
|
|
4658
5136
|
const point = history2.checkout("main", at);
|
|
4659
5137
|
restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
|
|
4660
5138
|
captured.set(namespace, point.value.snapshot);
|
|
4661
|
-
rng.setState(point.value.rngState);
|
|
4662
|
-
|
|
5139
|
+
(branchRngs.get(namespace) ?? rng).setState(point.value.rngState);
|
|
5140
|
+
const selectedClock = namespaceClock(namespace);
|
|
5141
|
+
selectedClock.set(point.value.clock.now);
|
|
4663
5142
|
if (point.value.clock.frozen)
|
|
4664
|
-
|
|
5143
|
+
selectedClock.freeze();
|
|
4665
5144
|
else
|
|
4666
|
-
|
|
5145
|
+
selectedClock.unfreeze();
|
|
4667
5146
|
}
|
|
4668
5147
|
return namespace;
|
|
4669
5148
|
}
|
|
@@ -4721,11 +5200,12 @@ var createRuntime = (options) => {
|
|
|
4721
5200
|
const storage = ensureBranch(namespace, branchName);
|
|
4722
5201
|
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
4723
5202
|
captured.set(storage, point.value.snapshot);
|
|
4724
|
-
|
|
5203
|
+
const selectedClock = namespaceClock(namespace);
|
|
5204
|
+
selectedClock.set(point.value.clock.now);
|
|
4725
5205
|
if (point.value.clock.frozen)
|
|
4726
|
-
|
|
5206
|
+
selectedClock.freeze();
|
|
4727
5207
|
else
|
|
4728
|
-
|
|
5208
|
+
selectedClock.unfreeze();
|
|
4729
5209
|
(branchRngs.get(storage) ?? rng).setState(point.value.rngState);
|
|
4730
5210
|
};
|
|
4731
5211
|
const reset = async (name = DEFAULT_NAMESPACE) => {
|
|
@@ -4733,10 +5213,21 @@ var createRuntime = (options) => {
|
|
|
4733
5213
|
options.webhooks?.clear();
|
|
4734
5214
|
for (const each of instances.values())
|
|
4735
5215
|
await each.reset();
|
|
5216
|
+
for (const each of branchRngs.values())
|
|
5217
|
+
each.reset();
|
|
4736
5218
|
timelines.clear();
|
|
5219
|
+
for (const storage of branchStorage.values()) {
|
|
5220
|
+
instances.delete(storage);
|
|
5221
|
+
branchRngs.delete(storage);
|
|
5222
|
+
namespaceForStorage.delete(storage);
|
|
5223
|
+
}
|
|
4737
5224
|
branchStorage.clear();
|
|
4738
|
-
branchRngs.clear();
|
|
4739
5225
|
captured.clear();
|
|
5226
|
+
faults.clear();
|
|
5227
|
+
clock.reset();
|
|
5228
|
+
rng.reset();
|
|
5229
|
+
for (const each of namespaceClocks.values())
|
|
5230
|
+
each.reset();
|
|
4740
5231
|
return;
|
|
4741
5232
|
}
|
|
4742
5233
|
options.webhooks?.clear(name);
|
|
@@ -4754,11 +5245,18 @@ var createRuntime = (options) => {
|
|
|
4754
5245
|
else
|
|
4755
5246
|
clearNamespace(sqlite, storageNamespace(storage));
|
|
4756
5247
|
branchStorage.delete(mapping);
|
|
5248
|
+
instances.delete(storage);
|
|
5249
|
+
namespaceForStorage.delete(storage);
|
|
4757
5250
|
branchRngs.delete(storage);
|
|
4758
5251
|
captured.delete(storage);
|
|
4759
5252
|
}
|
|
4760
5253
|
timelines.delete(name);
|
|
4761
5254
|
captured.delete(name);
|
|
5255
|
+
faults.restore(name, { rules: [], rngState: createRng(options.seed ?? 0).state() });
|
|
5256
|
+
namespaceClock(name).reset();
|
|
5257
|
+
(branchRngs.get(name) ?? rng).reset();
|
|
5258
|
+
journal.clear(name);
|
|
5259
|
+
metrics.reset(name);
|
|
4762
5260
|
};
|
|
4763
5261
|
const snapshot = (name = DEFAULT_NAMESPACE) => {
|
|
4764
5262
|
return checkpoint(name, "main").value.snapshot;
|
|
@@ -4796,6 +5294,53 @@ var createRuntime = (options) => {
|
|
|
4796
5294
|
rng,
|
|
4797
5295
|
credentials,
|
|
4798
5296
|
webhooks: options.webhooks,
|
|
5297
|
+
isolateNamespaces() {
|
|
5298
|
+
if (publicNamespaces.size > 1)
|
|
5299
|
+
throw new Error("enable namespace isolation before creating named namespaces");
|
|
5300
|
+
isolatedNamespaces = true;
|
|
5301
|
+
faults.isolateNamespaces();
|
|
5302
|
+
},
|
|
5303
|
+
namespaceClock,
|
|
5304
|
+
namespaceOf(request) {
|
|
5305
|
+
const url = new URL(request.url);
|
|
5306
|
+
const prefixed = matchNamespacePath(url.pathname, adminPrefix);
|
|
5307
|
+
const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
|
|
5308
|
+
const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
|
|
5309
|
+
if (isAdminPath(path, adminPrefix))
|
|
5310
|
+
return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
|
|
5311
|
+
if (selected !== void 0)
|
|
5312
|
+
return selected;
|
|
5313
|
+
const credential = options.credential?.(request);
|
|
5314
|
+
return (credential !== void 0 ? credentials.get(credential) : void 0) ?? DEFAULT_NAMESPACE;
|
|
5315
|
+
},
|
|
5316
|
+
fleetSnapshot(namespace) {
|
|
5317
|
+
instance(namespace);
|
|
5318
|
+
const handle = {};
|
|
5319
|
+
fleetPoints.set(handle, {
|
|
5320
|
+
namespace,
|
|
5321
|
+
state: capture(namespace),
|
|
5322
|
+
faults: faults.snapshot(namespace),
|
|
5323
|
+
webhooks: options.webhooks?.snapshot(namespace)
|
|
5324
|
+
});
|
|
5325
|
+
return handle;
|
|
5326
|
+
},
|
|
5327
|
+
fleetRestore(handle, namespace) {
|
|
5328
|
+
const point = typeof handle === "object" && handle !== null ? fleetPoints.get(handle) : void 0;
|
|
5329
|
+
if (!point || point.namespace !== namespace)
|
|
5330
|
+
throw new Error("invalid fleet checkpoint");
|
|
5331
|
+
restoreNamespace(sqlite, storageNamespace(namespace), point.state.snapshot);
|
|
5332
|
+
captured.set(namespace, point.state.snapshot);
|
|
5333
|
+
const selectedClock = namespaceClock(namespace);
|
|
5334
|
+
selectedClock.freeze();
|
|
5335
|
+
selectedClock.set(point.state.clock.now);
|
|
5336
|
+
if (!point.state.clock.frozen)
|
|
5337
|
+
selectedClock.unfreeze();
|
|
5338
|
+
(branchRngs.get(namespace) ?? rng).setState(point.state.rngState);
|
|
5339
|
+
faults.restore(namespace, point.faults);
|
|
5340
|
+
if (point.webhooks !== void 0)
|
|
5341
|
+
options.webhooks?.restore(namespace, point.webhooks);
|
|
5342
|
+
timelines.delete(namespace);
|
|
5343
|
+
},
|
|
4799
5344
|
applyPreset: (name, namespace = DEFAULT_NAMESPACE, overrides = {}) => {
|
|
4800
5345
|
const preset = options.presets?.[name];
|
|
4801
5346
|
if (!preset)
|
|
@@ -4827,7 +5372,7 @@ var createRuntime = (options) => {
|
|
|
4827
5372
|
state: (namespace = DEFAULT_NAMESPACE) => inspectState(stateScope(namespace)),
|
|
4828
5373
|
fetch: async (incoming) => {
|
|
4829
5374
|
let request = incoming;
|
|
4830
|
-
const prefixed =
|
|
5375
|
+
const prefixed = matchNamespacePath(new URL(request.url).pathname, adminPrefix);
|
|
4831
5376
|
if (prefixed) {
|
|
4832
5377
|
const url2 = new URL(request.url);
|
|
4833
5378
|
url2.pathname = prefixed[2] ?? "/";
|
|
@@ -4843,7 +5388,15 @@ var createRuntime = (options) => {
|
|
|
4843
5388
|
signal: request.signal
|
|
4844
5389
|
});
|
|
4845
5390
|
}
|
|
4846
|
-
|
|
5391
|
+
const internalUrl = new URL(request.url);
|
|
5392
|
+
const internalPath = isAdminPath(internalUrl.pathname, adminPrefix) ? `${ADMIN_PREFIX}${internalUrl.pathname.slice(adminPrefix.length)}` : "";
|
|
5393
|
+
const internal = internalMatch(internalPath);
|
|
5394
|
+
const obsoleteInternalPath = adminPrefix !== ADMIN_PREFIX && internalMatch(internalUrl.pathname) && !internal;
|
|
5395
|
+
if (internal) {
|
|
5396
|
+
internalUrl.pathname = internalPath;
|
|
5397
|
+
request = new Request(internalUrl, request);
|
|
5398
|
+
}
|
|
5399
|
+
let namespace = internal ? internalUrl.searchParams.get("namespace") ?? control.namespaceOf(request) : control.namespaceOf(request);
|
|
4847
5400
|
if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
4848
5401
|
const credential = options.credential(request);
|
|
4849
5402
|
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
@@ -4863,7 +5416,9 @@ var createRuntime = (options) => {
|
|
|
4863
5416
|
return copy;
|
|
4864
5417
|
}
|
|
4865
5418
|
};
|
|
4866
|
-
|
|
5419
|
+
if (obsoleteInternalPath)
|
|
5420
|
+
return stamp(adminFail(404, "internal transport is only available under adminPrefix"));
|
|
5421
|
+
const handled = internal ? void 0 : await control.handle(request);
|
|
4867
5422
|
if (handled)
|
|
4868
5423
|
return stamp(handled);
|
|
4869
5424
|
const started = monotonicNow();
|
|
@@ -4904,7 +5459,7 @@ var createRuntime = (options) => {
|
|
|
4904
5459
|
type: "mockingbird_admin",
|
|
4905
5460
|
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
4906
5461
|
}
|
|
4907
|
-
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
5462
|
+
}), { status: 400, headers: { "content-type": "application/json; charset=utf-8" } }));
|
|
4908
5463
|
}
|
|
4909
5464
|
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
4910
5465
|
log(400);
|
|
@@ -4971,6 +5526,7 @@ var createRuntime = (options) => {
|
|
|
4971
5526
|
startedAt: wallNow(),
|
|
4972
5527
|
wallNow,
|
|
4973
5528
|
clock,
|
|
5529
|
+
namespaceClock,
|
|
4974
5530
|
faults,
|
|
4975
5531
|
metrics,
|
|
4976
5532
|
journal,
|
|
@@ -5020,15 +5576,16 @@ var createRuntime = (options) => {
|
|
|
5020
5576
|
...options.admin?.(runtime) ?? {},
|
|
5021
5577
|
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
5022
5578
|
// maps `presets`, and a service route must not replace that list with a record.
|
|
5023
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
5579
|
+
...options.presets ? presetRoutes(options.presets, runtime, adminPrefix) : {},
|
|
5024
5580
|
...stateAdminRoutes({
|
|
5025
5581
|
open: stateScope,
|
|
5026
5582
|
afterWrite: (namespace) => {
|
|
5027
5583
|
checkpoint(namespace, "main");
|
|
5028
5584
|
}
|
|
5029
5585
|
}),
|
|
5030
|
-
...adminUiRoutes(options.name, options.adminUi)
|
|
5586
|
+
...adminUiRoutes(options.name, options.adminUi, adminPrefix)
|
|
5031
5587
|
},
|
|
5588
|
+
adminPrefix,
|
|
5032
5589
|
adminKey: options.adminKey
|
|
5033
5590
|
});
|
|
5034
5591
|
return defineMock(runtime);
|
|
@@ -5042,7 +5599,10 @@ var mutableResponse = (response) => {
|
|
|
5042
5599
|
return new Response(response.body, response);
|
|
5043
5600
|
}
|
|
5044
5601
|
};
|
|
5045
|
-
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5602
|
+
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5603
|
+
status,
|
|
5604
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
5605
|
+
});
|
|
5046
5606
|
var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
|
|
5047
5607
|
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5048
5608
|
var credentialRoutes = (registry) => ({
|
|
@@ -5094,7 +5654,7 @@ var credentialRoutes = (registry) => ({
|
|
|
5094
5654
|
return adminJson(200, { status: "ok" });
|
|
5095
5655
|
}
|
|
5096
5656
|
});
|
|
5097
|
-
var presetRoutes = (presets, runtime) => ({
|
|
5657
|
+
var presetRoutes = (presets, runtime, adminPrefix) => ({
|
|
5098
5658
|
"GET /faults/presets": () => {
|
|
5099
5659
|
const body = {
|
|
5100
5660
|
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
@@ -5104,7 +5664,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
5104
5664
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
5105
5665
|
const name = params.name;
|
|
5106
5666
|
if (!presets[name])
|
|
5107
|
-
return adminFail(404, `no fault preset ${name}; GET /
|
|
5667
|
+
return adminFail(404, `no fault preset ${name}; GET ${adminPrefix}/faults/presets`);
|
|
5108
5668
|
const overrides = isObject(body) ? body : {};
|
|
5109
5669
|
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
5110
5670
|
}
|
|
@@ -5497,6 +6057,7 @@ var createRuntime2 = (options = {}) => {
|
|
|
5497
6057
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
5498
6058
|
...options.clock ? { clock: options.clock } : {},
|
|
5499
6059
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
6060
|
+
...options.adminPrefix !== void 0 ? { adminPrefix: options.adminPrefix } : {},
|
|
5500
6061
|
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
5501
6062
|
...options.onLog ? { onLog: options.onLog } : {},
|
|
5502
6063
|
credential: tokenCredential,
|
|
@@ -5892,6 +6453,9 @@ var RxVortexAPI = class {
|
|
|
5892
6453
|
};
|
|
5893
6454
|
|
|
5894
6455
|
export {
|
|
6456
|
+
createClock,
|
|
6457
|
+
resolveAdminPrefix,
|
|
6458
|
+
parseDuration,
|
|
5895
6459
|
document2 as document,
|
|
5896
6460
|
operationIds,
|
|
5897
6461
|
supportedOperationIds,
|
|
@@ -5905,4 +6469,4 @@ export {
|
|
|
5905
6469
|
tokenCredential,
|
|
5906
6470
|
RxVortexAPI
|
|
5907
6471
|
};
|
|
5908
|
-
//# sourceMappingURL=chunk-
|
|
6472
|
+
//# sourceMappingURL=chunk-MYXNLMD7.js.map
|