@crvouga/mockingbird-service-flex 1.7.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/DISCOVERY.md +55 -0
- package/README.md +3 -3
- package/SUPPORT.md +24 -0
- package/dist/{chunk-QPJYRGYD.js → chunk-E3W2D3EY.js} +827 -260
- package/dist/chunk-E3W2D3EY.js.map +7 -0
- package/dist/chunk-PWSGISMP.js +871 -0
- package/dist/chunk-PWSGISMP.js.map +7 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +37 -5
- package/dist/index.js +1 -1
- package/dist/server.d.ts +67 -5
- package/dist/server.js +2 -2
- package/openapi.yaml +1180 -0
- package/package.json +28 -3
- package/dist/chunk-NWE7UY5H.js +0 -384
- package/dist/chunk-NWE7UY5H.js.map +0 -7
- package/dist/chunk-QPJYRGYD.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), sleep2 = (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), sleep2 = (ms) => new Promise((res
|
|
|
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), sleep2 = (ms) => new Promise((res
|
|
|
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)
|
|
@@ -1131,7 +1207,8 @@ var annotateResponse = (response, extra) => {
|
|
|
1131
1207
|
var responseNotes = (response) => notes.get(response);
|
|
1132
1208
|
|
|
1133
1209
|
// ../core/dist/metrics.js
|
|
1134
|
-
var createMetrics = () => {
|
|
1210
|
+
var createMetrics = (partition = true) => {
|
|
1211
|
+
const namespaces = /* @__PURE__ */ new Map();
|
|
1135
1212
|
let requests = 0;
|
|
1136
1213
|
let faults = 0;
|
|
1137
1214
|
let totalDurationMs = 0;
|
|
@@ -1139,6 +1216,14 @@ var createMetrics = () => {
|
|
|
1139
1216
|
const unmatched = /* @__PURE__ */ new Map();
|
|
1140
1217
|
return {
|
|
1141
1218
|
record(entry) {
|
|
1219
|
+
if (partition) {
|
|
1220
|
+
let scoped = namespaces.get(entry.namespace);
|
|
1221
|
+
if (!scoped) {
|
|
1222
|
+
scoped = createMetrics(false);
|
|
1223
|
+
namespaces.set(entry.namespace, scoped);
|
|
1224
|
+
}
|
|
1225
|
+
scoped.record(entry);
|
|
1226
|
+
}
|
|
1142
1227
|
requests++;
|
|
1143
1228
|
totalDurationMs += entry.durationMs;
|
|
1144
1229
|
if (entry.faultId !== void 0)
|
|
@@ -1150,7 +1235,7 @@ var createMetrics = () => {
|
|
|
1150
1235
|
unmatched.set(route, (unmatched.get(route) ?? 0) + 1);
|
|
1151
1236
|
}
|
|
1152
1237
|
},
|
|
1153
|
-
report: () => ({
|
|
1238
|
+
report: (namespace) => namespace !== void 0 ? (namespaces.get(namespace) ?? createMetrics(false)).report() : {
|
|
1154
1239
|
requests,
|
|
1155
1240
|
byOperation: Object.fromEntries([...byOperation].sort(([a], [b]) => a.localeCompare(b))),
|
|
1156
1241
|
unmatched: [...unmatched].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([route, count]) => {
|
|
@@ -1159,8 +1244,13 @@ var createMetrics = () => {
|
|
|
1159
1244
|
}),
|
|
1160
1245
|
faults,
|
|
1161
1246
|
totalDurationMs
|
|
1162
|
-
}
|
|
1163
|
-
reset() {
|
|
1247
|
+
},
|
|
1248
|
+
reset(namespace) {
|
|
1249
|
+
if (namespace !== void 0) {
|
|
1250
|
+
namespaces.delete(namespace);
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
namespaces.clear();
|
|
1164
1254
|
requests = 0;
|
|
1165
1255
|
faults = 0;
|
|
1166
1256
|
totalDurationMs = 0;
|
|
@@ -1773,6 +1863,34 @@ dialog:not([open]) {
|
|
|
1773
1863
|
function isRecord2(value) {
|
|
1774
1864
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1775
1865
|
}
|
|
1866
|
+
function highlightJsonText(text) {
|
|
1867
|
+
const escapeToken = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
1868
|
+
switch (char) {
|
|
1869
|
+
case "&":
|
|
1870
|
+
return "&";
|
|
1871
|
+
case "<":
|
|
1872
|
+
return "<";
|
|
1873
|
+
case ">":
|
|
1874
|
+
return ">";
|
|
1875
|
+
case '"':
|
|
1876
|
+
return """;
|
|
1877
|
+
default:
|
|
1878
|
+
return "'";
|
|
1879
|
+
}
|
|
1880
|
+
});
|
|
1881
|
+
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;
|
|
1882
|
+
let cursor = 0;
|
|
1883
|
+
let html2 = "";
|
|
1884
|
+
for (const match of text.matchAll(token)) {
|
|
1885
|
+
const index = match.index ?? 0;
|
|
1886
|
+
const value = match[0];
|
|
1887
|
+
html2 += escapeToken(text.slice(cursor, index));
|
|
1888
|
+
const kind = value.startsWith('"') ? value.trimEnd().endsWith(":") ? "key" : "string" : value === "true" || value === "false" ? "boolean" : value === "null" ? "null" : "number";
|
|
1889
|
+
html2 += `<span class="json-${kind}">${escapeToken(value)}</span>`;
|
|
1890
|
+
cursor = index + value.length;
|
|
1891
|
+
}
|
|
1892
|
+
return html2 + escapeToken(text.slice(cursor));
|
|
1893
|
+
}
|
|
1776
1894
|
function mountAdminBrand(doc, host, service, catalog) {
|
|
1777
1895
|
const clip = (value, max) => {
|
|
1778
1896
|
if (typeof value !== "string")
|
|
@@ -2134,14 +2252,16 @@ function bootAdmin(config) {
|
|
|
2134
2252
|
const state = {
|
|
2135
2253
|
namespace: "default",
|
|
2136
2254
|
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
2137
|
-
view: "overview",
|
|
2255
|
+
view: ["overview", "state", "clock", "faults", "journal", "routes"].includes(location.hash.slice(1)) ? location.hash.slice(1) : "overview",
|
|
2138
2256
|
collection: "",
|
|
2139
2257
|
shape: null,
|
|
2140
2258
|
route: null,
|
|
2259
|
+
routes: [],
|
|
2141
2260
|
editing: null
|
|
2142
2261
|
};
|
|
2143
2262
|
const standard = new Set(config.standardRoutes);
|
|
2144
2263
|
const banner = el("banner", HTMLElement);
|
|
2264
|
+
const bannerMessage = el("banner-message", HTMLElement);
|
|
2145
2265
|
const keyInput = el("admin-key", HTMLInputElement);
|
|
2146
2266
|
keyInput.value = state.key;
|
|
2147
2267
|
const params = new URLSearchParams(location.search);
|
|
@@ -2152,18 +2272,21 @@ function bootAdmin(config) {
|
|
|
2152
2272
|
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
2153
2273
|
params.delete("key");
|
|
2154
2274
|
const next = params.toString();
|
|
2155
|
-
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
2275
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : "") + location.hash);
|
|
2156
2276
|
}
|
|
2157
2277
|
const queryNamespace = params.get("namespace");
|
|
2158
2278
|
if (queryNamespace)
|
|
2159
2279
|
state.namespace = queryNamespace;
|
|
2160
2280
|
const showError = (error2) => {
|
|
2161
|
-
|
|
2281
|
+
bannerMessage.textContent = error2 instanceof Error ? error2.message : String(error2);
|
|
2162
2282
|
banner.classList.add("show");
|
|
2283
|
+
banner.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
2163
2284
|
};
|
|
2164
2285
|
const clearError = () => {
|
|
2165
2286
|
banner.classList.remove("show");
|
|
2287
|
+
bannerMessage.textContent = "";
|
|
2166
2288
|
};
|
|
2289
|
+
el("banner-dismiss", HTMLButtonElement).addEventListener("click", clearError);
|
|
2167
2290
|
const api = async (method, path, body) => {
|
|
2168
2291
|
const headers = { accept: "application/json" };
|
|
2169
2292
|
if (state.key !== "")
|
|
@@ -2207,16 +2330,21 @@ function bootAdmin(config) {
|
|
|
2207
2330
|
namespace: () => state.namespace
|
|
2208
2331
|
};
|
|
2209
2332
|
const isPanelScript = (value) => typeof value === "function";
|
|
2333
|
+
const themeButton = el("theme", HTMLButtonElement);
|
|
2210
2334
|
const setTheme = (theme) => {
|
|
2211
2335
|
if (theme)
|
|
2212
2336
|
document.documentElement.dataset.theme = theme;
|
|
2213
2337
|
else
|
|
2214
2338
|
delete document.documentElement.dataset.theme;
|
|
2339
|
+
const dark = theme ? theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2340
|
+
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>';
|
|
2341
|
+
const label = dark ? "Use light theme" : "Use dark theme";
|
|
2342
|
+
themeButton.setAttribute("aria-label", label);
|
|
2343
|
+
themeButton.title = label;
|
|
2215
2344
|
};
|
|
2216
2345
|
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
2346
|
+
setTheme(storedTheme === "dark" || storedTheme === "light" ? storedTheme : null);
|
|
2347
|
+
themeButton.addEventListener("click", () => {
|
|
2220
2348
|
const current = document.documentElement.dataset.theme;
|
|
2221
2349
|
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2222
2350
|
const next = dark ? "light" : "dark";
|
|
@@ -2235,6 +2363,7 @@ function bootAdmin(config) {
|
|
|
2235
2363
|
continue;
|
|
2236
2364
|
section.hidden = section.id !== `view-${view}`;
|
|
2237
2365
|
}
|
|
2366
|
+
history.replaceState(null, "", `${location.pathname}${location.search}#${encodeURIComponent(view)}`);
|
|
2238
2367
|
};
|
|
2239
2368
|
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
2240
2369
|
const target = event.target;
|
|
@@ -2249,7 +2378,7 @@ function bootAdmin(config) {
|
|
|
2249
2378
|
show(view);
|
|
2250
2379
|
refresh().catch(showError);
|
|
2251
2380
|
});
|
|
2252
|
-
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>`;
|
|
2381
|
+
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>`;
|
|
2253
2382
|
const table = (columns, rows) => {
|
|
2254
2383
|
if (rows.length === 0)
|
|
2255
2384
|
return '<p class="empty">Nothing here yet.</p>';
|
|
@@ -2259,16 +2388,22 @@ function bootAdmin(config) {
|
|
|
2259
2388
|
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
2260
2389
|
return `<tr${attrs}>${cells}</tr>`;
|
|
2261
2390
|
}).join("");
|
|
2262
|
-
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
2391
|
+
return `<div class="table-shell"><table><thead>${head}</thead><tbody>${body}</tbody></table></div>`;
|
|
2263
2392
|
};
|
|
2264
2393
|
const preview = (value) => {
|
|
2265
2394
|
const text = JSON.stringify(value) ?? "null";
|
|
2266
|
-
|
|
2395
|
+
const clipped = text.length > 180 ? `${text.slice(0, 180)}\u2026` : text;
|
|
2396
|
+
return `<code class="json-inline">${highlightJsonText(clipped)}</code>`;
|
|
2267
2397
|
};
|
|
2268
2398
|
const when = (epoch) => {
|
|
2269
2399
|
const date = new Date(epoch);
|
|
2270
2400
|
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
2271
2401
|
};
|
|
2402
|
+
const journalWhen = (value) => {
|
|
2403
|
+
const numeric = value.trim() === "" ? Number.NaN : Number(value);
|
|
2404
|
+
const epoch = Number.isFinite(numeric) ? numeric : Date.parse(value);
|
|
2405
|
+
return Number.isFinite(epoch) ? when(epoch) : value;
|
|
2406
|
+
};
|
|
2272
2407
|
const clockCard = (epoch) => {
|
|
2273
2408
|
const date = new Date(epoch);
|
|
2274
2409
|
if (Number.isNaN(date.getTime()))
|
|
@@ -2388,7 +2523,7 @@ function bootAdmin(config) {
|
|
|
2388
2523
|
};
|
|
2389
2524
|
const renderOverview = async () => {
|
|
2390
2525
|
const [health, shape, clock, journal] = await Promise.all([
|
|
2391
|
-
originGet(
|
|
2526
|
+
originGet(`${config.adminPrefix}/health`),
|
|
2392
2527
|
api("GET", "/state"),
|
|
2393
2528
|
api("GET", "/clock"),
|
|
2394
2529
|
api("GET", "/requests?limit=8")
|
|
@@ -2405,7 +2540,7 @@ function bootAdmin(config) {
|
|
|
2405
2540
|
},
|
|
2406
2541
|
{
|
|
2407
2542
|
k: "Status",
|
|
2408
|
-
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
2543
|
+
v: `<span class="badge ok">${esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")}</span>`
|
|
2409
2544
|
},
|
|
2410
2545
|
{ k: "Namespace", v: esc(state.namespace) },
|
|
2411
2546
|
{ k: "Collections", v: String(decoded.collections.length) },
|
|
@@ -2492,26 +2627,46 @@ function bootAdmin(config) {
|
|
|
2492
2627
|
const renderJournal = async () => {
|
|
2493
2628
|
const journal = await api("GET", "/requests?limit=100");
|
|
2494
2629
|
el("journal", HTMLElement).innerHTML = table([
|
|
2495
|
-
{ label: "When", cell: (row) => esc(row.at) },
|
|
2496
|
-
{
|
|
2630
|
+
{ label: "When", cell: (row) => esc(journalWhen(row.at)) },
|
|
2631
|
+
{
|
|
2632
|
+
label: "Method",
|
|
2633
|
+
cell: (row) => `<span class="method">${esc(row.method)}</span>`
|
|
2634
|
+
},
|
|
2497
2635
|
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2498
|
-
{
|
|
2636
|
+
{
|
|
2637
|
+
label: "Status",
|
|
2638
|
+
cell: (row) => `<span class="badge ${row.status.startsWith("2") ? "ok" : row.status.startsWith("4") || row.status.startsWith("5") ? "warn" : ""}">${esc(row.status)}</span>`
|
|
2639
|
+
},
|
|
2499
2640
|
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
2500
2641
|
], decodeJournal(journal));
|
|
2501
2642
|
};
|
|
2502
|
-
const
|
|
2503
|
-
const
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2643
|
+
const paintRoutes = () => {
|
|
2644
|
+
const query = el("route-search", HTMLInputElement).value.trim().toLocaleLowerCase();
|
|
2645
|
+
const routes = state.routes.filter((route) => route.toLocaleLowerCase().includes(query));
|
|
2646
|
+
el("route-count", HTMLElement).textContent = `${routes.length} of ${state.routes.length}`;
|
|
2647
|
+
if (state.routes.length === 0) {
|
|
2648
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No admin routes are available.</p>';
|
|
2649
|
+
return;
|
|
2650
|
+
}
|
|
2651
|
+
if (routes.length === 0) {
|
|
2652
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No routes match this filter.</p>';
|
|
2653
|
+
return;
|
|
2654
|
+
}
|
|
2507
2655
|
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2508
2656
|
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2509
2657
|
const parts = route.split(" ");
|
|
2510
2658
|
const method = parts[0] ?? "";
|
|
2511
2659
|
const path = parts.slice(1).join(" ");
|
|
2512
|
-
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>`;
|
|
2660
|
+
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>`;
|
|
2513
2661
|
}).join("");
|
|
2514
2662
|
};
|
|
2663
|
+
const renderRoutes = async () => {
|
|
2664
|
+
const data = await api("GET", "/");
|
|
2665
|
+
if (!isRecord2(data))
|
|
2666
|
+
throw new Error("Route list was not an object");
|
|
2667
|
+
state.routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
2668
|
+
paintRoutes();
|
|
2669
|
+
};
|
|
2515
2670
|
const renderers = {
|
|
2516
2671
|
overview: renderOverview,
|
|
2517
2672
|
state: renderState,
|
|
@@ -2520,19 +2675,50 @@ function bootAdmin(config) {
|
|
|
2520
2675
|
journal: renderJournal,
|
|
2521
2676
|
routes: renderRoutes
|
|
2522
2677
|
};
|
|
2678
|
+
let refreshToken = 0;
|
|
2523
2679
|
async function refresh() {
|
|
2680
|
+
const token = ++refreshToken;
|
|
2524
2681
|
clearError();
|
|
2682
|
+
const main = el("views", HTMLElement);
|
|
2683
|
+
const sync = el("sync-status", HTMLElement);
|
|
2684
|
+
main.setAttribute("aria-busy", "true");
|
|
2685
|
+
sync.classList.add("busy");
|
|
2686
|
+
sync.textContent = "Updating";
|
|
2525
2687
|
const render = renderers[state.view];
|
|
2526
|
-
|
|
2527
|
-
|
|
2688
|
+
try {
|
|
2689
|
+
if (render)
|
|
2690
|
+
await render();
|
|
2691
|
+
if (token === refreshToken)
|
|
2692
|
+
sync.textContent = "Up to date";
|
|
2693
|
+
} finally {
|
|
2694
|
+
if (token === refreshToken) {
|
|
2695
|
+
main.removeAttribute("aria-busy");
|
|
2696
|
+
sync.classList.remove("busy");
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2528
2699
|
}
|
|
2529
2700
|
const delay = (ms) => new Promise((resolve) => {
|
|
2530
2701
|
window.setTimeout(resolve, ms);
|
|
2531
2702
|
});
|
|
2532
|
-
const confirmAction = async (button, pending, done, work) => {
|
|
2703
|
+
const confirmAction = async (button, pending, done, work, confirmLabel) => {
|
|
2533
2704
|
if (button.dataset.pending === "1")
|
|
2534
2705
|
return;
|
|
2535
2706
|
const label = button.textContent ?? "";
|
|
2707
|
+
if (confirmLabel !== void 0 && button.dataset.confirming !== "1") {
|
|
2708
|
+
button.dataset.confirming = "1";
|
|
2709
|
+
button.classList.add("confirming");
|
|
2710
|
+
button.textContent = confirmLabel;
|
|
2711
|
+
window.setTimeout(() => {
|
|
2712
|
+
if (button.dataset.confirming !== "1")
|
|
2713
|
+
return;
|
|
2714
|
+
button.textContent = label;
|
|
2715
|
+
button.classList.remove("confirming");
|
|
2716
|
+
delete button.dataset.confirming;
|
|
2717
|
+
}, 3e3);
|
|
2718
|
+
return;
|
|
2719
|
+
}
|
|
2720
|
+
button.classList.remove("confirming");
|
|
2721
|
+
delete button.dataset.confirming;
|
|
2536
2722
|
const width = button.offsetWidth;
|
|
2537
2723
|
button.dataset.pending = "1";
|
|
2538
2724
|
button.classList.add("pressed");
|
|
@@ -2553,6 +2739,65 @@ function bootAdmin(config) {
|
|
|
2553
2739
|
delete button.dataset.pending;
|
|
2554
2740
|
}
|
|
2555
2741
|
};
|
|
2742
|
+
const wireJsonEditor = (textareaId, highlightId, statusId, shellId, allowEmpty) => {
|
|
2743
|
+
const textarea = el(textareaId, HTMLTextAreaElement);
|
|
2744
|
+
const highlight = el(highlightId, HTMLElement);
|
|
2745
|
+
const status = el(statusId, HTMLElement);
|
|
2746
|
+
const shell = el(shellId, HTMLElement);
|
|
2747
|
+
const sync = () => {
|
|
2748
|
+
const text = textarea.value;
|
|
2749
|
+
highlight.innerHTML = `${highlightJsonText(text)}
|
|
2750
|
+
`;
|
|
2751
|
+
let valid = true;
|
|
2752
|
+
let message = "Valid JSON";
|
|
2753
|
+
if (allowEmpty && text.trim() === "")
|
|
2754
|
+
message = "No body";
|
|
2755
|
+
else {
|
|
2756
|
+
try {
|
|
2757
|
+
readJson2(text);
|
|
2758
|
+
} catch {
|
|
2759
|
+
valid = false;
|
|
2760
|
+
message = "Invalid JSON";
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
shell.classList.toggle("invalid", !valid);
|
|
2764
|
+
status.classList.toggle("invalid", !valid);
|
|
2765
|
+
status.textContent = message;
|
|
2766
|
+
textarea.setAttribute("aria-invalid", valid ? "false" : "true");
|
|
2767
|
+
};
|
|
2768
|
+
const syncScroll = () => {
|
|
2769
|
+
const preview2 = highlight.parentElement;
|
|
2770
|
+
if (!(preview2 instanceof HTMLElement))
|
|
2771
|
+
return;
|
|
2772
|
+
preview2.scrollTop = textarea.scrollTop;
|
|
2773
|
+
preview2.scrollLeft = textarea.scrollLeft;
|
|
2774
|
+
};
|
|
2775
|
+
textarea.addEventListener("input", sync);
|
|
2776
|
+
textarea.addEventListener("scroll", syncScroll);
|
|
2777
|
+
sync();
|
|
2778
|
+
return sync;
|
|
2779
|
+
};
|
|
2780
|
+
const syncRouteEditor = wireJsonEditor("route-body", "route-highlight", "route-json-status", "route-editor", true);
|
|
2781
|
+
const syncRecordEditor = wireJsonEditor("editor-value", "editor-highlight", "editor-json-status", "record-editor", false);
|
|
2782
|
+
for (const button of document.querySelectorAll("[data-format]")) {
|
|
2783
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2784
|
+
continue;
|
|
2785
|
+
button.addEventListener("click", () => {
|
|
2786
|
+
const target = button.dataset.format;
|
|
2787
|
+
if (target === void 0)
|
|
2788
|
+
return;
|
|
2789
|
+
const textarea = document.getElementById(target);
|
|
2790
|
+
if (!(textarea instanceof HTMLTextAreaElement) || textarea.value.trim() === "")
|
|
2791
|
+
return;
|
|
2792
|
+
try {
|
|
2793
|
+
textarea.value = JSON.stringify(readJson2(textarea.value), null, 2) ?? "";
|
|
2794
|
+
textarea.dispatchEvent(new Event("input"));
|
|
2795
|
+
textarea.focus();
|
|
2796
|
+
} catch {
|
|
2797
|
+
showError(new Error("Fix the JSON before formatting it"));
|
|
2798
|
+
}
|
|
2799
|
+
});
|
|
2800
|
+
}
|
|
2556
2801
|
document.addEventListener("click", (event) => {
|
|
2557
2802
|
const target = event.target;
|
|
2558
2803
|
if (!(target instanceof Element))
|
|
@@ -2625,6 +2870,7 @@ function bootAdmin(config) {
|
|
|
2625
2870
|
} else {
|
|
2626
2871
|
editorValue.value = "{\n \n}";
|
|
2627
2872
|
}
|
|
2873
|
+
syncRecordEditor();
|
|
2628
2874
|
editor.showModal();
|
|
2629
2875
|
}
|
|
2630
2876
|
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -2654,10 +2900,10 @@ function bootAdmin(config) {
|
|
|
2654
2900
|
const id = state.editing;
|
|
2655
2901
|
if (!id)
|
|
2656
2902
|
return;
|
|
2657
|
-
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2903
|
+
confirmAction(editorDelete, "Deleting...", "Deleted", () => api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2658
2904
|
editor.close();
|
|
2659
2905
|
return renderState();
|
|
2660
|
-
}).catch(showError);
|
|
2906
|
+
}), "Confirm delete").catch(showError);
|
|
2661
2907
|
});
|
|
2662
2908
|
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2663
2909
|
if (!(button instanceof HTMLButtonElement))
|
|
@@ -2703,10 +2949,10 @@ function bootAdmin(config) {
|
|
|
2703
2949
|
}).then(() => renderFaults()).catch(showError);
|
|
2704
2950
|
});
|
|
2705
2951
|
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2706
|
-
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
|
|
2952
|
+
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults()), "Confirm clear").catch(showError);
|
|
2707
2953
|
});
|
|
2708
2954
|
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2709
|
-
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
|
|
2955
|
+
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal()), "Confirm clear").catch(showError);
|
|
2710
2956
|
});
|
|
2711
2957
|
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2712
2958
|
const target = event.target;
|
|
@@ -2720,9 +2966,15 @@ function bootAdmin(config) {
|
|
|
2720
2966
|
return;
|
|
2721
2967
|
state.route = route;
|
|
2722
2968
|
el("route-title", HTMLElement).textContent = route;
|
|
2969
|
+
el("route-send", HTMLButtonElement).disabled = false;
|
|
2723
2970
|
const method = route.split(" ")[0] ?? "";
|
|
2724
|
-
el("route-body", HTMLTextAreaElement)
|
|
2971
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
2972
|
+
routeBody.disabled = method === "GET" || method === "DELETE";
|
|
2973
|
+
el("route-editor", HTMLElement).hidden = routeBody.disabled;
|
|
2974
|
+
el("route-json-status", HTMLElement).parentElement?.toggleAttribute("hidden", routeBody.disabled);
|
|
2975
|
+
syncRouteEditor();
|
|
2725
2976
|
});
|
|
2977
|
+
el("route-search", HTMLInputElement).addEventListener("input", paintRoutes);
|
|
2726
2978
|
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2727
2979
|
event.preventDefault();
|
|
2728
2980
|
const send = async () => {
|
|
@@ -2744,7 +2996,8 @@ function bootAdmin(config) {
|
|
|
2744
2996
|
}
|
|
2745
2997
|
}
|
|
2746
2998
|
const data = await api(method, concrete, body);
|
|
2747
|
-
|
|
2999
|
+
const text = JSON.stringify(data, null, 2) ?? "null";
|
|
3000
|
+
el("route-result", HTMLElement).innerHTML = `<code>${highlightJsonText(text)}</code>`;
|
|
2748
3001
|
};
|
|
2749
3002
|
send().catch(showError);
|
|
2750
3003
|
});
|
|
@@ -2862,11 +3115,13 @@ function bootAdmin(config) {
|
|
|
2862
3115
|
views.append(section);
|
|
2863
3116
|
mountSqlExplorer(body, panelApi);
|
|
2864
3117
|
}
|
|
3118
|
+
show(document.getElementById(`view-${state.view}`) === null ? "overview" : state.view);
|
|
2865
3119
|
return refresh();
|
|
2866
3120
|
}).catch(showError);
|
|
2867
3121
|
}
|
|
2868
3122
|
var clientPrelude = [
|
|
2869
3123
|
isRecord2,
|
|
3124
|
+
highlightJsonText,
|
|
2870
3125
|
mountAdminBrand,
|
|
2871
3126
|
startAdminBrand,
|
|
2872
3127
|
faultPresetList,
|
|
@@ -2889,6 +3144,7 @@ ${entry}(${payload});
|
|
|
2889
3144
|
var defineMock = (runtime) => runtime;
|
|
2890
3145
|
var STANDARD_ADMIN_ROUTES = [
|
|
2891
3146
|
"GET /",
|
|
3147
|
+
"GET /health",
|
|
2892
3148
|
"POST /reset",
|
|
2893
3149
|
"GET /namespaces",
|
|
2894
3150
|
"GET /clock",
|
|
@@ -2968,11 +3224,11 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
2968
3224
|
}
|
|
2969
3225
|
});
|
|
2970
3226
|
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
2971
|
-
var renderAdminDocument = (service) => {
|
|
3227
|
+
var renderAdminDocument = (service, adminPrefix = ADMIN_PREFIX) => {
|
|
2972
3228
|
const name = escapeHtml(service);
|
|
2973
3229
|
const script = adminClientSource({
|
|
2974
3230
|
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
2975
|
-
adminPrefix
|
|
3231
|
+
adminPrefix,
|
|
2976
3232
|
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
2977
3233
|
brandsUrl: ADMIN_BRANDS_URL,
|
|
2978
3234
|
service
|
|
@@ -2989,75 +3245,99 @@ var renderAdminDocument = (service) => {
|
|
|
2989
3245
|
${CSS_RESET}
|
|
2990
3246
|
:root {
|
|
2991
3247
|
color-scheme: light;
|
|
2992
|
-
--bg: #
|
|
3248
|
+
--bg: #f7f8fb;
|
|
2993
3249
|
--bg-elev: #ffffff;
|
|
2994
|
-
--bg-muted: #
|
|
2995
|
-
--bg-inset: #
|
|
2996
|
-
--line: #
|
|
2997
|
-
--line-strong: #
|
|
2998
|
-
--ink: #
|
|
2999
|
-
--muted: #
|
|
3000
|
-
--faint: #
|
|
3001
|
-
--accent: #
|
|
3250
|
+
--bg-muted: #f1f3f7;
|
|
3251
|
+
--bg-inset: #f8f9fc;
|
|
3252
|
+
--line: #e4e7ec;
|
|
3253
|
+
--line-strong: #cfd4dc;
|
|
3254
|
+
--ink: #101828;
|
|
3255
|
+
--muted: #475467;
|
|
3256
|
+
--faint: #667085;
|
|
3257
|
+
--accent: #6941c6;
|
|
3258
|
+
--accent-hover: #5933b4;
|
|
3002
3259
|
--accent-ink: #ffffff;
|
|
3003
|
-
--accent-soft: #
|
|
3004
|
-
--ok: #
|
|
3005
|
-
--ok-soft: #
|
|
3006
|
-
--warn: #
|
|
3007
|
-
--warn-soft: #
|
|
3008
|
-
--err: #
|
|
3009
|
-
--err-soft: #
|
|
3010
|
-
--
|
|
3260
|
+
--accent-soft: #f4f0ff;
|
|
3261
|
+
--ok: #067647;
|
|
3262
|
+
--ok-soft: #ecfdf3;
|
|
3263
|
+
--warn: #b54708;
|
|
3264
|
+
--warn-soft: #fffaeb;
|
|
3265
|
+
--err: #b42318;
|
|
3266
|
+
--err-soft: #fef3f2;
|
|
3267
|
+
--code-key: #6941c6;
|
|
3268
|
+
--code-string: #067647;
|
|
3269
|
+
--code-number: #175cd3;
|
|
3270
|
+
--code-boolean: #c11574;
|
|
3271
|
+
--code-null: #667085;
|
|
3272
|
+
--shadow-xs: 0 1px 2px rgb(16 24 40 / 0.05);
|
|
3273
|
+
--shadow-sm: 0 1px 3px rgb(16 24 40 / 0.08), 0 1px 2px rgb(16 24 40 / 0.04);
|
|
3274
|
+
--shadow-lg: 0 18px 48px rgb(16 24 40 / 0.16), 0 4px 12px rgb(16 24 40 / 0.08);
|
|
3011
3275
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
3012
3276
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
3013
|
-
--radius:
|
|
3014
|
-
--header:
|
|
3015
|
-
--nav:
|
|
3277
|
+
--radius: 12px;
|
|
3278
|
+
--header: 72px;
|
|
3279
|
+
--nav: 224px;
|
|
3016
3280
|
}
|
|
3017
3281
|
:root[data-theme="dark"] {
|
|
3018
3282
|
color-scheme: dark;
|
|
3019
|
-
--bg: #
|
|
3020
|
-
--bg-elev: #
|
|
3021
|
-
--bg-muted: #
|
|
3022
|
-
--bg-inset: #
|
|
3023
|
-
--line: #
|
|
3024
|
-
--line-strong: #
|
|
3025
|
-
--ink: #
|
|
3026
|
-
--muted: #
|
|
3027
|
-
--faint: #
|
|
3028
|
-
--accent: #
|
|
3029
|
-
--accent-
|
|
3030
|
-
--accent-
|
|
3031
|
-
--
|
|
3032
|
-
--ok
|
|
3033
|
-
--
|
|
3034
|
-
--warn
|
|
3035
|
-
--
|
|
3036
|
-
--err
|
|
3037
|
-
--
|
|
3283
|
+
--bg: #0c0e14;
|
|
3284
|
+
--bg-elev: #151821;
|
|
3285
|
+
--bg-muted: #20232e;
|
|
3286
|
+
--bg-inset: #11141c;
|
|
3287
|
+
--line: #2b303d;
|
|
3288
|
+
--line-strong: #414858;
|
|
3289
|
+
--ink: #f5f7fa;
|
|
3290
|
+
--muted: #b6bdc9;
|
|
3291
|
+
--faint: #8d96a7;
|
|
3292
|
+
--accent: #9e77ed;
|
|
3293
|
+
--accent-hover: #b692f6;
|
|
3294
|
+
--accent-ink: #ffffff;
|
|
3295
|
+
--accent-soft: #2b2142;
|
|
3296
|
+
--ok: #75e0a7;
|
|
3297
|
+
--ok-soft: #102a20;
|
|
3298
|
+
--warn: #fec84b;
|
|
3299
|
+
--warn-soft: #302611;
|
|
3300
|
+
--err: #fda29b;
|
|
3301
|
+
--err-soft: #321817;
|
|
3302
|
+
--code-key: #c3a6ff;
|
|
3303
|
+
--code-string: #75e0a7;
|
|
3304
|
+
--code-number: #84adff;
|
|
3305
|
+
--code-boolean: #f9a8d4;
|
|
3306
|
+
--code-null: #98a2b3;
|
|
3307
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3308
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3309
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
3038
3310
|
}
|
|
3039
3311
|
@media (prefers-color-scheme: dark) {
|
|
3040
3312
|
:root:not([data-theme="light"]) {
|
|
3041
3313
|
color-scheme: dark;
|
|
3042
|
-
--bg: #
|
|
3043
|
-
--bg-elev: #
|
|
3044
|
-
--bg-muted: #
|
|
3045
|
-
--bg-inset: #
|
|
3046
|
-
--line: #
|
|
3047
|
-
--line-strong: #
|
|
3048
|
-
--ink: #
|
|
3049
|
-
--muted: #
|
|
3050
|
-
--faint: #
|
|
3051
|
-
--accent: #
|
|
3052
|
-
--accent-
|
|
3053
|
-
--accent-
|
|
3054
|
-
--
|
|
3055
|
-
--ok
|
|
3056
|
-
--
|
|
3057
|
-
--warn
|
|
3058
|
-
--
|
|
3059
|
-
--err
|
|
3060
|
-
--
|
|
3314
|
+
--bg: #0c0e14;
|
|
3315
|
+
--bg-elev: #151821;
|
|
3316
|
+
--bg-muted: #20232e;
|
|
3317
|
+
--bg-inset: #11141c;
|
|
3318
|
+
--line: #2b303d;
|
|
3319
|
+
--line-strong: #414858;
|
|
3320
|
+
--ink: #f5f7fa;
|
|
3321
|
+
--muted: #b6bdc9;
|
|
3322
|
+
--faint: #8d96a7;
|
|
3323
|
+
--accent: #9e77ed;
|
|
3324
|
+
--accent-hover: #b692f6;
|
|
3325
|
+
--accent-ink: #ffffff;
|
|
3326
|
+
--accent-soft: #2b2142;
|
|
3327
|
+
--ok: #75e0a7;
|
|
3328
|
+
--ok-soft: #102a20;
|
|
3329
|
+
--warn: #fec84b;
|
|
3330
|
+
--warn-soft: #302611;
|
|
3331
|
+
--err: #fda29b;
|
|
3332
|
+
--err-soft: #321817;
|
|
3333
|
+
--code-key: #c3a6ff;
|
|
3334
|
+
--code-string: #75e0a7;
|
|
3335
|
+
--code-number: #84adff;
|
|
3336
|
+
--code-boolean: #f9a8d4;
|
|
3337
|
+
--code-null: #98a2b3;
|
|
3338
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3339
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3340
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
3061
3341
|
}
|
|
3062
3342
|
}
|
|
3063
3343
|
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
@@ -3075,13 +3355,17 @@ var renderAdminDocument = (service) => {
|
|
|
3075
3355
|
padding-right: env(safe-area-inset-right);
|
|
3076
3356
|
-webkit-text-size-adjust: 100%;
|
|
3077
3357
|
}
|
|
3078
|
-
|
|
3358
|
+
::selection { background: color-mix(in srgb, var(--accent) 28%, transparent); }
|
|
3359
|
+
:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px; }
|
|
3360
|
+
button, input, select, textarea { transition: border-color 140ms ease, box-shadow 140ms ease, transform 100ms ease; }
|
|
3361
|
+
button:disabled, input:disabled, select:disabled, textarea:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
3079
3362
|
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
3080
3363
|
header.top {
|
|
3081
|
-
position: sticky; top: 0; z-index:
|
|
3364
|
+
position: sticky; top: 0; z-index: 20;
|
|
3082
3365
|
display: flex; align-items: center; gap: 12px;
|
|
3083
|
-
min-height: var(--header); padding:
|
|
3084
|
-
background: var(--bg-elev);
|
|
3366
|
+
min-height: var(--header); padding: 12px 20px;
|
|
3367
|
+
background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
|
|
3368
|
+
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
3085
3369
|
border-bottom: 1px solid var(--line);
|
|
3086
3370
|
min-width: 0; max-width: 100%;
|
|
3087
3371
|
}
|
|
@@ -3089,12 +3373,21 @@ var renderAdminDocument = (service) => {
|
|
|
3089
3373
|
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
3090
3374
|
min-width: 0; max-width: 100%; margin-right: auto;
|
|
3091
3375
|
}
|
|
3092
|
-
.brand-
|
|
3093
|
-
.brand-
|
|
3376
|
+
.brand-lockup { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
3377
|
+
.brand-mark {
|
|
3378
|
+
width: 38px; height: 38px; display: grid; place-items: center; flex: none;
|
|
3379
|
+
border-radius: 11px; color: #fff;
|
|
3380
|
+
background: linear-gradient(145deg, #7f56d9 0%, #53389e 100%);
|
|
3381
|
+
box-shadow: 0 6px 16px rgb(105 65 198 / 0.24), inset 0 1px 0 rgb(255 255 255 / 0.25);
|
|
3382
|
+
}
|
|
3383
|
+
.brand-mark svg { width: 22px; height: 22px; }
|
|
3384
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; line-height: 1.25; }
|
|
3385
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
3094
3386
|
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
3095
3387
|
.vendor {
|
|
3096
3388
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
3097
3389
|
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
3390
|
+
padding: 5px 8px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-inset);
|
|
3098
3391
|
}
|
|
3099
3392
|
.vendor[hidden] { display: none; }
|
|
3100
3393
|
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
@@ -3106,12 +3399,17 @@ var renderAdminDocument = (service) => {
|
|
|
3106
3399
|
.vendor-sep { color: var(--faint); }
|
|
3107
3400
|
.vendor-link { color: inherit; text-decoration: none; }
|
|
3108
3401
|
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
3109
|
-
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items:
|
|
3110
|
-
|
|
3402
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
|
|
3403
|
+
.sync-status { align-self: center; display: flex; align-items: center; gap: 6px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3404
|
+
.sync-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
|
|
3405
|
+
.sync-status.busy::before { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); animation: pulse 1.2s ease-in-out infinite; }
|
|
3406
|
+
label.field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; font-weight: 600; color: var(--faint); }
|
|
3111
3407
|
label.field input, label.field select, .controls input[type="password"] {
|
|
3112
|
-
background-color: var(--bg-
|
|
3113
|
-
padding:
|
|
3408
|
+
background-color: var(--bg-elev); border: 1px solid var(--line-strong); border-radius: 9px;
|
|
3409
|
+
padding: 8px 10px; min-height: 38px; line-height: 1.2; box-shadow: var(--shadow-xs);
|
|
3114
3410
|
}
|
|
3411
|
+
label.field input:hover, label.field select:hover, textarea:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--line-strong)); }
|
|
3412
|
+
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; }
|
|
3115
3413
|
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
3116
3414
|
the text fields in the same form. Paint it like those fields. */
|
|
3117
3415
|
label.field select {
|
|
@@ -3128,12 +3426,20 @@ var renderAdminDocument = (service) => {
|
|
|
3128
3426
|
dialog .field > textarea { width: 100%; }
|
|
3129
3427
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
3130
3428
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
.btn
|
|
3429
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
|
3430
|
+
border-radius: 9px; min-height: 38px; padding: 0 13px; border: 1px solid var(--line-strong);
|
|
3431
|
+
background: var(--bg-elev); color: var(--ink); font-size: 13px; font-weight: 600; line-height: 1;
|
|
3432
|
+
box-shadow: var(--shadow-xs);
|
|
3433
|
+
}
|
|
3434
|
+
.icon-btn { width: 38px; padding: 0; }
|
|
3435
|
+
.icon-btn svg, .btn svg, .btn-primary svg { width: 16px; height: 16px; }
|
|
3436
|
+
.icon-btn:hover, .btn:hover { background: var(--bg-muted); border-color: var(--line-strong); }
|
|
3437
|
+
.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; }
|
|
3438
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
3439
|
+
.btn-quiet { background: transparent; border-color: transparent; box-shadow: none; }
|
|
3440
|
+
.btn-quiet:hover { background: var(--bg-muted); }
|
|
3441
|
+
.btn-danger { color: var(--err); border-color: color-mix(in srgb, var(--err) 25%, var(--line)); }
|
|
3442
|
+
.btn-danger:hover, .btn-danger.confirming { background: var(--err-soft); border-color: color-mix(in srgb, var(--err) 45%, var(--line)); }
|
|
3137
3443
|
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
3138
3444
|
so the press itself has to stay visible. */
|
|
3139
3445
|
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
@@ -3149,93 +3455,159 @@ var renderAdminDocument = (service) => {
|
|
|
3149
3455
|
flex: 1 0 auto; min-width: 0;
|
|
3150
3456
|
}
|
|
3151
3457
|
nav.side {
|
|
3152
|
-
|
|
3458
|
+
position: sticky; top: var(--header); align-self: start; height: calc(100vh - var(--header));
|
|
3459
|
+
border-right: 1px solid var(--line); padding: 18px 12px;
|
|
3153
3460
|
display: flex; flex-direction: column; gap: 4px;
|
|
3154
|
-
background: var(--bg-elev);
|
|
3461
|
+
background: var(--bg-elev); overflow-y: auto;
|
|
3155
3462
|
}
|
|
3463
|
+
.nav-label { padding: 0 10px 8px; color: var(--faint); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
|
|
3156
3464
|
nav.side button {
|
|
3465
|
+
position: relative; display: flex; align-items: center; gap: 10px; width: 100%;
|
|
3157
3466
|
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
3158
|
-
padding: 9px 10px; min-height:
|
|
3159
|
-
}
|
|
3160
|
-
nav.side button
|
|
3161
|
-
|
|
3162
|
-
.
|
|
3163
|
-
.
|
|
3164
|
-
.
|
|
3467
|
+
padding: 9px 10px; min-height: 42px; color: var(--muted); font-size: 13px; font-weight: 550;
|
|
3468
|
+
}
|
|
3469
|
+
nav.side button:hover { background: var(--bg-muted); color: var(--ink); }
|
|
3470
|
+
nav.side button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 650; }
|
|
3471
|
+
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); }
|
|
3472
|
+
.nav-icon { width: 18px; height: 18px; display: grid; place-items: center; flex: none; }
|
|
3473
|
+
.nav-icon svg { width: 18px; height: 18px; }
|
|
3474
|
+
main { padding: 32px clamp(20px, 4vw, 48px) 64px; max-width: 1320px; width: 100%; min-width: 0; }
|
|
3475
|
+
main[aria-busy="true"] .view { opacity: 0.62; }
|
|
3476
|
+
.view { animation: view-in 180ms ease-out; }
|
|
3477
|
+
.view[hidden] { display: none; }
|
|
3478
|
+
.view-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
|
|
3479
|
+
.view h2 { margin: 0 0 5px; font-size: clamp(24px, 3vw, 30px); line-height: 1.2; letter-spacing: -0.035em; }
|
|
3480
|
+
.view h3 { margin: 24px 0 10px; font-size: 15px; letter-spacing: -0.01em; }
|
|
3481
|
+
.lede { margin: 0; color: var(--muted); max-width: 72ch; font-size: 14px; }
|
|
3482
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 175px), 1fr)); gap: 14px; }
|
|
3165
3483
|
.card {
|
|
3166
3484
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
3167
|
-
padding:
|
|
3485
|
+
padding: 18px; box-shadow: var(--shadow-sm);
|
|
3168
3486
|
}
|
|
3169
|
-
.card
|
|
3170
|
-
.card
|
|
3487
|
+
.stat-card { position: relative; overflow: hidden; }
|
|
3488
|
+
.stat-card::after { content: ""; position: absolute; inset: 0 0 auto; height: 2px; background: linear-gradient(90deg, var(--accent), transparent 78%); opacity: 0.7; }
|
|
3489
|
+
.card .k { color: var(--faint); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
3490
|
+
.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; }
|
|
3171
3491
|
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
3172
|
-
.
|
|
3492
|
+
.section-card { background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-xs); overflow: hidden; }
|
|
3493
|
+
.section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
|
|
3494
|
+
.section-title h3 { margin: 0; }
|
|
3495
|
+
.split { display: grid; grid-template-columns: minmax(0, 250px) minmax(0, 1fr); gap: 18px; align-items: start; }
|
|
3496
|
+
#collections { padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); box-shadow: var(--shadow-xs); }
|
|
3173
3497
|
.collection {
|
|
3174
3498
|
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
3175
|
-
border-radius:
|
|
3499
|
+
border-radius: 9px; padding: 10px 11px; min-height: 48px; color: var(--ink); cursor: pointer;
|
|
3176
3500
|
}
|
|
3177
|
-
.collection
|
|
3501
|
+
.collection:hover { background: var(--bg-muted); }
|
|
3502
|
+
.collection[aria-current="true"] { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 18%, var(--line)); color: var(--accent); }
|
|
3178
3503
|
.collection small { display: block; color: var(--faint); }
|
|
3179
3504
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
3180
|
-
.chip {
|
|
3181
|
-
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
3182
|
-
border-radius: 999px; padding: 4px 8px;
|
|
3183
|
-
}
|
|
3505
|
+
.chip { font-family: var(--mono); font-size: 11px; background: var(--bg-muted); border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; }
|
|
3184
3506
|
.chip i { color: var(--faint); font-style: normal; }
|
|
3185
3507
|
.sql-layout {
|
|
3186
3508
|
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
3187
3509
|
}
|
|
3188
|
-
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
3189
|
-
.sql-tables button { text-align: left; }
|
|
3510
|
+
.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); }
|
|
3511
|
+
.sql-tables button { text-align: left; border: 0; border-radius: 8px; padding: 9px 10px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
3512
|
+
.sql-tables button:hover, .sql-tables button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); }
|
|
3190
3513
|
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
3191
|
-
table { width: 100%; border-
|
|
3192
|
-
|
|
3193
|
-
th {
|
|
3514
|
+
.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); }
|
|
3515
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
3516
|
+
th, td { text-align: left; padding: 11px 13px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
3517
|
+
th { color: var(--faint); background: var(--bg-inset); font-weight: 650; font-size: 11px; text-transform: uppercase; letter-spacing: 0.045em; }
|
|
3518
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
3194
3519
|
tr.clickable { cursor: pointer; }
|
|
3195
|
-
tr.clickable:hover { background: var(--
|
|
3520
|
+
tr.clickable:hover { background: var(--accent-soft); }
|
|
3196
3521
|
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
3197
3522
|
pre {
|
|
3198
3523
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
3199
|
-
background: var(--bg-inset); border-radius: 10px; padding:
|
|
3200
|
-
}
|
|
3201
|
-
.
|
|
3524
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 10px; padding: 14px; font-size: 12px; line-height: 1.65;
|
|
3525
|
+
}
|
|
3526
|
+
.json-key { color: var(--code-key); }
|
|
3527
|
+
.json-string { color: var(--code-string); }
|
|
3528
|
+
.json-number { color: var(--code-number); }
|
|
3529
|
+
.json-boolean { color: var(--code-boolean); }
|
|
3530
|
+
.json-null { color: var(--code-null); font-style: italic; }
|
|
3531
|
+
.json-inline { display: block; max-width: 44rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
3532
|
+
.json-view { position: relative; max-height: 26rem; overflow: auto; }
|
|
3533
|
+
.json-editor { position: relative; min-height: 190px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--bg-inset); overflow: hidden; }
|
|
3534
|
+
.json-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); }
|
|
3535
|
+
.json-editor.invalid { border-color: var(--err); }
|
|
3536
|
+
.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; }
|
|
3537
|
+
.json-editor pre { position: absolute; inset: 0; pointer-events: none; background: transparent; }
|
|
3538
|
+
.json-editor textarea { position: relative; z-index: 1; resize: vertical; background: transparent; color: transparent; caret-color: var(--ink); -webkit-text-fill-color: transparent; }
|
|
3539
|
+
.json-editor textarea::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); }
|
|
3540
|
+
.editor-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; }
|
|
3541
|
+
.json-status { display: inline-flex; align-items: center; gap: 6px; color: var(--ok); font-size: 11px; }
|
|
3542
|
+
.json-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
3543
|
+
.json-status.invalid { color: var(--err); }
|
|
3544
|
+
.format-json { border: 0; padding: 3px 5px; background: transparent; color: var(--accent); font-size: 11px; font-weight: 650; cursor: pointer; }
|
|
3545
|
+
.row-actions { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 14px 0; }
|
|
3202
3546
|
.banner {
|
|
3203
|
-
display: none;
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3547
|
+
display: none; position: sticky; top: calc(var(--header) + 12px); z-index: 15;
|
|
3548
|
+
align-items: center; gap: 10px; margin: 0 0 16px; padding: 11px 12px 11px 14px;
|
|
3549
|
+
border: 1px solid color-mix(in srgb, var(--err) 30%, var(--line)); border-radius: 10px;
|
|
3550
|
+
background: var(--err-soft); color: var(--err); box-shadow: var(--shadow-sm); font-size: 13px;
|
|
3551
|
+
}
|
|
3552
|
+
.banner.show { display: flex; }
|
|
3553
|
+
.banner span { flex: 1; }
|
|
3554
|
+
.banner button { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 7px; background: transparent; color: currentColor; cursor: pointer; }
|
|
3555
|
+
.banner button:hover { background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
3556
|
+
.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); }
|
|
3208
3557
|
dialog {
|
|
3209
|
-
border: 1px solid var(--line); border-radius:
|
|
3210
|
-
color: var(--ink); width: min(
|
|
3211
|
-
}
|
|
3212
|
-
dialog
|
|
3213
|
-
dialog
|
|
3558
|
+
border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--bg-elev);
|
|
3559
|
+
color: var(--ink); width: min(720px, calc(100% - 24px)); max-width: calc(100vw - 24px); box-shadow: var(--shadow-lg);
|
|
3560
|
+
}
|
|
3561
|
+
dialog[open] { animation: dialog-in 160ms ease-out; }
|
|
3562
|
+
dialog::backdrop { background: rgb(12 14 20 / 0.58); backdrop-filter: blur(3px); }
|
|
3563
|
+
dialog form, .dialog-body { padding: 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
3564
|
+
.dialog-heading { display: flex; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
|
3565
|
+
.dialog-heading strong { font-size: 18px; letter-spacing: -0.02em; }
|
|
3566
|
+
.dialog-heading span { color: var(--muted); font-size: 12px; }
|
|
3214
3567
|
textarea {
|
|
3215
3568
|
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
3216
3569
|
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
3217
3570
|
font-family: var(--mono); font-size: 12px;
|
|
3218
3571
|
}
|
|
3219
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
3220
|
-
.
|
|
3572
|
+
.dialog-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding-top: 4px; }
|
|
3573
|
+
.dialog-actions .btn-danger { margin-right: auto; }
|
|
3574
|
+
.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); }
|
|
3221
3575
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
3222
3576
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
3223
3577
|
.status-2 { color: var(--ok); }
|
|
3224
3578
|
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
3225
3579
|
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
3580
|
+
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
3581
|
+
.form-grid .wide { grid-column: span 2; }
|
|
3226
3582
|
.route {
|
|
3227
|
-
display: grid; grid-template-columns:
|
|
3228
|
-
padding:
|
|
3229
|
-
}
|
|
3230
|
-
.
|
|
3583
|
+
display: grid; grid-template-columns: 84px minmax(0, 1fr) auto; gap: 10px; align-items: center;
|
|
3584
|
+
padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--bg-elev);
|
|
3585
|
+
}
|
|
3586
|
+
.route:first-child { border-radius: var(--radius) var(--radius) 0 0; }
|
|
3587
|
+
.route:last-child { border-bottom: 0; border-radius: 0 0 var(--radius) var(--radius); }
|
|
3588
|
+
.route-tools { display: flex; align-items: flex-end; gap: 12px; margin-bottom: 12px; }
|
|
3589
|
+
.route-tools .field { flex: 1; }
|
|
3590
|
+
.route-tools input { width: 100%; }
|
|
3591
|
+
.route-count { padding-bottom: 10px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3592
|
+
#route-list { max-height: 34rem; border: 1px solid var(--line); border-radius: var(--radius); overflow: auto; box-shadow: var(--shadow-xs); }
|
|
3593
|
+
.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; }
|
|
3594
|
+
@keyframes view-in { from { transform: translateY(3px); } to { transform: translateY(0); } }
|
|
3595
|
+
@keyframes dialog-in { from { transform: translateY(8px) scale(0.99); } to { transform: translateY(0) scale(1); } }
|
|
3596
|
+
@keyframes pulse { 50% { opacity: 0.45; } }
|
|
3597
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
|
|
3598
|
+
@media (forced-colors: active) { .json-editor pre { display: none; } .json-editor textarea { color: CanvasText; -webkit-text-fill-color: CanvasText; } }
|
|
3231
3599
|
@media (max-width: 800px) {
|
|
3600
|
+
:root { --header: 0px; }
|
|
3232
3601
|
header.top {
|
|
3233
|
-
align-items: stretch; flex-direction: column;
|
|
3234
|
-
padding
|
|
3602
|
+
position: relative; align-items: stretch; flex-direction: column;
|
|
3603
|
+
padding: max(12px, env(safe-area-inset-top)) 14px 12px;
|
|
3235
3604
|
}
|
|
3236
3605
|
input, select, textarea { font-size: 16px; }
|
|
3237
3606
|
.brand, .vendor, .controls { width: 100%; }
|
|
3607
|
+
.brand { gap: 10px; }
|
|
3608
|
+
.vendor { order: 2; border-radius: 9px; }
|
|
3238
3609
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
3610
|
+
.sync-status { display: none; }
|
|
3239
3611
|
/* The layout fills the screen. A single-column grid stretches every auto
|
|
3240
3612
|
row, so the tab bar grew with the leftover height and the buttons floated
|
|
3241
3613
|
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
@@ -3245,14 +3617,23 @@ var renderAdminDocument = (service) => {
|
|
|
3245
3617
|
align-content: start;
|
|
3246
3618
|
}
|
|
3247
3619
|
nav.side {
|
|
3248
|
-
align-self: start; height: auto;
|
|
3620
|
+
position: sticky; top: 0; z-index: 12; align-self: start; height: auto;
|
|
3249
3621
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
3250
3622
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
3251
|
-
padding: 8px;
|
|
3623
|
+
padding: 8px; box-shadow: var(--shadow-xs);
|
|
3252
3624
|
}
|
|
3253
|
-
nav
|
|
3625
|
+
.nav-label { display: none; }
|
|
3626
|
+
nav.side button { width: auto; white-space: nowrap; flex: none; }
|
|
3627
|
+
nav.side button[aria-current="true"]::before { inset: auto 9px -8px; width: auto; height: 3px; border-radius: 3px 3px 0 0; }
|
|
3254
3628
|
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
3255
|
-
|
|
3629
|
+
.form-grid { grid-template-columns: minmax(0, 1fr); }
|
|
3630
|
+
.form-grid .wide { grid-column: auto; }
|
|
3631
|
+
main { padding: 22px 14px 48px; }
|
|
3632
|
+
.view-head { margin-bottom: 18px; }
|
|
3633
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
3634
|
+
.card { padding: 14px; }
|
|
3635
|
+
.card .v { font-size: 20px; }
|
|
3636
|
+
.table-shell { border: 0; background: transparent; box-shadow: none; overflow: visible; }
|
|
3256
3637
|
table, thead, tbody, tr, th, td { display: block; }
|
|
3257
3638
|
thead { display: none; }
|
|
3258
3639
|
tr {
|
|
@@ -3267,50 +3648,63 @@ var renderAdminDocument = (service) => {
|
|
|
3267
3648
|
.route { grid-template-columns: 72px 1fr; }
|
|
3268
3649
|
.route button { grid-column: 1 / -1; }
|
|
3269
3650
|
}
|
|
3651
|
+
@media (max-width: 460px) {
|
|
3652
|
+
.controls label.field { min-width: calc(50% - 4px); }
|
|
3653
|
+
.controls .icon-btn, .controls .btn { flex: 1; }
|
|
3654
|
+
.grid { grid-template-columns: minmax(0, 1fr); }
|
|
3655
|
+
.dialog-actions { flex-wrap: wrap; }
|
|
3656
|
+
.dialog-actions button { flex: 1; }
|
|
3657
|
+
.dialog-actions .btn-danger { flex-basis: 100%; margin-right: 0; }
|
|
3658
|
+
}
|
|
3270
3659
|
</style>
|
|
3271
3660
|
</head>
|
|
3272
3661
|
<body>
|
|
3273
3662
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
3274
3663
|
<header class="top">
|
|
3275
3664
|
<div class="brand">
|
|
3276
|
-
<div class="brand-
|
|
3277
|
-
<
|
|
3278
|
-
|
|
3665
|
+
<div class="brand-lockup">
|
|
3666
|
+
<div class="brand-mark" aria-hidden="true">
|
|
3667
|
+
<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>
|
|
3668
|
+
</div>
|
|
3669
|
+
<div class="brand-id">
|
|
3670
|
+
<strong>${name}</strong>
|
|
3671
|
+
<span>Mock admin</span>
|
|
3672
|
+
</div>
|
|
3279
3673
|
</div>
|
|
3280
3674
|
<div class="vendor" id="vendor" hidden></div>
|
|
3281
3675
|
</div>
|
|
3282
3676
|
<div class="controls">
|
|
3677
|
+
<span class="sync-status" id="sync-status" role="status" aria-live="polite">Ready</span>
|
|
3283
3678
|
<label class="field">Namespace
|
|
3284
3679
|
<select id="namespace" aria-label="Namespace"></select>
|
|
3285
3680
|
</label>
|
|
3286
3681
|
<label class="field">Admin key
|
|
3287
3682
|
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
3288
3683
|
</label>
|
|
3289
|
-
<button class="icon-btn" id="theme" type="button" aria-label="
|
|
3290
|
-
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
3684
|
+
<button class="icon-btn" id="theme" type="button" aria-label="Use dark theme" title="Use dark theme"></button>
|
|
3685
|
+
<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>
|
|
3291
3686
|
</div>
|
|
3292
3687
|
</header>
|
|
3293
3688
|
<div class="layout">
|
|
3294
3689
|
<nav class="side" id="nav" aria-label="Admin sections">
|
|
3295
|
-
<
|
|
3296
|
-
<button type="button" data-view="
|
|
3297
|
-
<button type="button" data-view="
|
|
3298
|
-
<button type="button" data-view="
|
|
3299
|
-
<button type="button" data-view="
|
|
3300
|
-
<button type="button" data-view="
|
|
3690
|
+
<span class="nav-label">Workspace</span>
|
|
3691
|
+
<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>
|
|
3692
|
+
<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>
|
|
3693
|
+
<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>
|
|
3694
|
+
<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>
|
|
3695
|
+
<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>
|
|
3696
|
+
<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>
|
|
3301
3697
|
</nav>
|
|
3302
3698
|
<main id="views">
|
|
3303
|
-
<
|
|
3699
|
+
<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>
|
|
3304
3700
|
<section class="view" id="view-overview">
|
|
3305
|
-
<h2>Overview</h2>
|
|
3306
|
-
<p class="lede">Health, clock, and the collections this namespace is holding.</p>
|
|
3701
|
+
<div class="view-head"><div><h2>Overview</h2><p class="lede">Health, clock, and the collections this namespace is holding.</p></div></div>
|
|
3307
3702
|
<div class="grid" id="overview-cards"></div>
|
|
3308
3703
|
<h3>Recent requests</h3>
|
|
3309
3704
|
<div id="overview-requests"></div>
|
|
3310
3705
|
</section>
|
|
3311
3706
|
<section class="view" id="view-state" hidden>
|
|
3312
|
-
<h2>State</h2>
|
|
3313
|
-
<p class="lede">Every collection in this namespace. Declared fields stay visible before any row exists; stored rows add whatever else they contain.</p>
|
|
3707
|
+
<div class="view-head"><div><h2>State</h2><p class="lede">Inspect and edit every collection in this namespace.</p></div></div>
|
|
3314
3708
|
<div class="split">
|
|
3315
3709
|
<div id="collections"></div>
|
|
3316
3710
|
<div>
|
|
@@ -3323,8 +3717,7 @@ var renderAdminDocument = (service) => {
|
|
|
3323
3717
|
</div>
|
|
3324
3718
|
</section>
|
|
3325
3719
|
<section class="view" id="view-clock" hidden>
|
|
3326
|
-
<h2>Clock</h2>
|
|
3327
|
-
<p class="lede">The clock every timestamp in this mock reads.</p>
|
|
3720
|
+
<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>
|
|
3328
3721
|
<div class="card" id="clock-readout"></div>
|
|
3329
3722
|
<div class="row-actions">
|
|
3330
3723
|
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
@@ -3343,21 +3736,22 @@ var renderAdminDocument = (service) => {
|
|
|
3343
3736
|
</form>
|
|
3344
3737
|
</section>
|
|
3345
3738
|
<section class="view" id="view-faults" hidden>
|
|
3346
|
-
<h2>Faults</h2>
|
|
3347
|
-
<p class="lede">Inject a response, a delay, or a named preset. Rules apply to this namespace.</p>
|
|
3739
|
+
<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>
|
|
3348
3740
|
<form id="fault-form" class="card stack">
|
|
3349
|
-
<
|
|
3350
|
-
<
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
<
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
<
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
<
|
|
3360
|
-
|
|
3741
|
+
<div class="form-grid">
|
|
3742
|
+
<label class="field wide">Preset
|
|
3743
|
+
<select id="fault-preset"><option value="">Custom rule</option></select>
|
|
3744
|
+
</label>
|
|
3745
|
+
<label class="field">Status
|
|
3746
|
+
<input id="fault-status" inputmode="numeric" placeholder="503">
|
|
3747
|
+
</label>
|
|
3748
|
+
<label class="field wide">Path prefix
|
|
3749
|
+
<input id="fault-path" placeholder="/v1">
|
|
3750
|
+
</label>
|
|
3751
|
+
<label class="field">Method
|
|
3752
|
+
<input id="fault-method" placeholder="GET">
|
|
3753
|
+
</label>
|
|
3754
|
+
</div>
|
|
3361
3755
|
<div class="dialog-actions">
|
|
3362
3756
|
<button class="btn-primary" type="submit">Add fault</button>
|
|
3363
3757
|
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
@@ -3366,26 +3760,31 @@ var renderAdminDocument = (service) => {
|
|
|
3366
3760
|
<div id="fault-list"></div>
|
|
3367
3761
|
</section>
|
|
3368
3762
|
<section class="view" id="view-journal" hidden>
|
|
3369
|
-
<h2>Journal</h2>
|
|
3370
|
-
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
3371
|
-
<div class="row-actions">
|
|
3372
|
-
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
3373
|
-
</div>
|
|
3763
|
+
<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>
|
|
3374
3764
|
<div id="journal"></div>
|
|
3375
3765
|
</section>
|
|
3376
3766
|
<section class="view" id="view-routes" hidden>
|
|
3377
|
-
<h2>Routes</h2>
|
|
3378
|
-
<
|
|
3767
|
+
<div class="view-head"><div><h2>Routes</h2><p class="lede">Explore shared controls and service-specific admin endpoints.</p></div></div>
|
|
3768
|
+
<div class="route-tools">
|
|
3769
|
+
<label class="field">Filter routes
|
|
3770
|
+
<input id="route-search" type="search" placeholder="Method, path, or extension" autocomplete="off">
|
|
3771
|
+
</label>
|
|
3772
|
+
<span class="route-count" id="route-count"></span>
|
|
3773
|
+
</div>
|
|
3379
3774
|
<div id="route-list"></div>
|
|
3380
|
-
<form id="route-form" class="card stack">
|
|
3775
|
+
<form id="route-form" class="card stack" style="margin-top: 18px">
|
|
3381
3776
|
<strong id="route-title">Select a route</strong>
|
|
3382
3777
|
<label class="field">JSON body
|
|
3383
|
-
<
|
|
3778
|
+
<div class="json-editor" id="route-editor" hidden>
|
|
3779
|
+
<pre aria-hidden="true"><code id="route-highlight"></code></pre>
|
|
3780
|
+
<textarea id="route-body" placeholder="{ }" spellcheck="false" aria-describedby="route-json-status" disabled></textarea>
|
|
3781
|
+
</div>
|
|
3782
|
+
<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>
|
|
3384
3783
|
</label>
|
|
3385
3784
|
<div class="dialog-actions">
|
|
3386
|
-
<button class="btn-primary" type="submit">Send</button>
|
|
3785
|
+
<button class="btn-primary" id="route-send" type="submit" disabled>Send request</button>
|
|
3387
3786
|
</div>
|
|
3388
|
-
<pre id="route-result">Response will show here.</pre>
|
|
3787
|
+
<pre class="json-view" id="route-result"><code>Response will show here.</code></pre>
|
|
3389
3788
|
</form>
|
|
3390
3789
|
</section>
|
|
3391
3790
|
</main>
|
|
@@ -3393,12 +3792,16 @@ var renderAdminDocument = (service) => {
|
|
|
3393
3792
|
</div>
|
|
3394
3793
|
<dialog id="editor">
|
|
3395
3794
|
<form method="dialog" id="editor-form">
|
|
3396
|
-
<strong id="editor-title">Record</strong>
|
|
3795
|
+
<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>
|
|
3397
3796
|
<label class="field">Id
|
|
3398
3797
|
<input id="editor-id" required>
|
|
3399
3798
|
</label>
|
|
3400
3799
|
<label class="field">JSON value
|
|
3401
|
-
<
|
|
3800
|
+
<div class="json-editor" id="record-editor">
|
|
3801
|
+
<pre aria-hidden="true"><code id="editor-highlight"></code></pre>
|
|
3802
|
+
<textarea id="editor-value" spellcheck="false" aria-describedby="editor-json-status"></textarea>
|
|
3803
|
+
</div>
|
|
3804
|
+
<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>
|
|
3402
3805
|
</label>
|
|
3403
3806
|
<div class="dialog-actions">
|
|
3404
3807
|
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
@@ -3440,8 +3843,8 @@ var describeExtension = (extension) => {
|
|
|
3440
3843
|
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
3441
3844
|
};
|
|
3442
3845
|
};
|
|
3443
|
-
var adminUiRoutes = (service, ui) => {
|
|
3444
|
-
const shell = () => renderAdminDocument(service);
|
|
3846
|
+
var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
3847
|
+
const shell = () => renderAdminDocument(service, adminPrefix);
|
|
3445
3848
|
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
3446
3849
|
return {
|
|
3447
3850
|
"GET /ui": () => htmlResponse(document3()),
|
|
@@ -4132,7 +4535,7 @@ var stateAdminRoutes = (routes) => {
|
|
|
4132
4535
|
};
|
|
4133
4536
|
|
|
4134
4537
|
// ../core/dist/version.js
|
|
4135
|
-
var PACKAGE_VERSION = true ? "
|
|
4538
|
+
var PACKAGE_VERSION = true ? "2.0.0" : UNRELEASED_VERSION;
|
|
4136
4539
|
|
|
4137
4540
|
// ../core/dist/signing.js
|
|
4138
4541
|
var encoder = new TextEncoder();
|
|
@@ -4225,7 +4628,9 @@ var createWebhookHub = (options) => {
|
|
|
4225
4628
|
const faults = /* @__PURE__ */ new Map();
|
|
4226
4629
|
const held = /* @__PURE__ */ new Map();
|
|
4227
4630
|
const inFlight2 = /* @__PURE__ */ new Set();
|
|
4631
|
+
const controllers = /* @__PURE__ */ new Map();
|
|
4228
4632
|
let immediate = Promise.resolve();
|
|
4633
|
+
const snapshots = /* @__PURE__ */ new WeakMap();
|
|
4229
4634
|
const track = (work) => {
|
|
4230
4635
|
inFlight2.add(work);
|
|
4231
4636
|
void work.finally(() => inFlight2.delete(work));
|
|
@@ -4246,6 +4651,7 @@ var createWebhookHub = (options) => {
|
|
|
4246
4651
|
responseBody: null
|
|
4247
4652
|
};
|
|
4248
4653
|
const controller = new AbortController();
|
|
4654
|
+
controllers.set(delivery.id, controller);
|
|
4249
4655
|
const timer = scheduleTimer(() => controller.abort(), timeoutMs);
|
|
4250
4656
|
try {
|
|
4251
4657
|
const signed = await options.signer({
|
|
@@ -4275,6 +4681,7 @@ var createWebhookHub = (options) => {
|
|
|
4275
4681
|
} catch (error2) {
|
|
4276
4682
|
record.error = controller.signal.aborted ? `timed out after ${timeoutMs}ms` : error2 instanceof Error ? error2.message : String(error2);
|
|
4277
4683
|
} finally {
|
|
4684
|
+
controllers.delete(delivery.id);
|
|
4278
4685
|
cancel(timer);
|
|
4279
4686
|
record.durationMs = now() - started;
|
|
4280
4687
|
delivery.attempts.push(record);
|
|
@@ -4292,6 +4699,8 @@ var createWebhookHub = (options) => {
|
|
|
4292
4699
|
if (!payloads.has(delivery.id))
|
|
4293
4700
|
return;
|
|
4294
4701
|
await attempt(delivery).then((ok) => {
|
|
4702
|
+
if (!payloads.has(delivery.id))
|
|
4703
|
+
return;
|
|
4295
4704
|
if (ok)
|
|
4296
4705
|
delivery.state = "delivered";
|
|
4297
4706
|
else
|
|
@@ -4353,6 +4762,37 @@ var createWebhookHub = (options) => {
|
|
|
4353
4762
|
fanOut(message);
|
|
4354
4763
|
};
|
|
4355
4764
|
const hub = {
|
|
4765
|
+
pending(namespace) {
|
|
4766
|
+
return inFlight2.size + (held.get(namespace)?.length ?? 0) + [...deliveries.values()].filter((d) => d.namespace === namespace && d.state === "pending").length;
|
|
4767
|
+
},
|
|
4768
|
+
snapshot(namespace) {
|
|
4769
|
+
if (hub.pending(namespace) > 0)
|
|
4770
|
+
throw new Error("webhooks are not quiescent");
|
|
4771
|
+
const handle = {};
|
|
4772
|
+
snapshots.set(handle, structuredClone({
|
|
4773
|
+
namespace,
|
|
4774
|
+
endpoints: own.get(namespace) ?? [],
|
|
4775
|
+
messages: messages.filter((m) => m.namespace === namespace),
|
|
4776
|
+
deliveries: [...deliveries].filter(([, d]) => d.namespace === namespace),
|
|
4777
|
+
payloads: [...payloads].filter(([, p]) => p.message.namespace === namespace),
|
|
4778
|
+
faults: faults.get(namespace) ?? []
|
|
4779
|
+
}));
|
|
4780
|
+
return handle;
|
|
4781
|
+
},
|
|
4782
|
+
restore(namespace, handle) {
|
|
4783
|
+
const point = typeof handle === "object" && handle !== null ? snapshots.get(handle) : void 0;
|
|
4784
|
+
if (!point || point.namespace !== namespace || hub.pending(namespace) > 0)
|
|
4785
|
+
throw new Error("invalid webhook restore");
|
|
4786
|
+
hub.clear(namespace);
|
|
4787
|
+
const copied = structuredClone(point);
|
|
4788
|
+
own.set(namespace, copied.endpoints);
|
|
4789
|
+
messages.push(...copied.messages);
|
|
4790
|
+
for (const [id2, delivery] of copied.deliveries)
|
|
4791
|
+
deliveries.set(id2, delivery);
|
|
4792
|
+
for (const [id2, payload] of copied.payloads)
|
|
4793
|
+
payloads.set(id2, payload);
|
|
4794
|
+
faults.set(namespace, copied.faults);
|
|
4795
|
+
},
|
|
4356
4796
|
publish(input) {
|
|
4357
4797
|
const contentType = input.contentType ?? (input.form ? "application/x-www-form-urlencoded" : "application/json");
|
|
4358
4798
|
const body = typeof input.body === "string" ? input.body : input.form ? new URLSearchParams(input.form).toString() : JSON.stringify(input.body);
|
|
@@ -4443,6 +4883,7 @@ var createWebhookHub = (options) => {
|
|
|
4443
4883
|
for (const [id2, delivery] of deliveries) {
|
|
4444
4884
|
if (namespace !== void 0 && delivery.namespace !== namespace)
|
|
4445
4885
|
continue;
|
|
4886
|
+
controllers.get(id2)?.abort();
|
|
4446
4887
|
const timer = pending.get(id2);
|
|
4447
4888
|
if (timer !== void 0)
|
|
4448
4889
|
cancel(timer);
|
|
@@ -4467,7 +4908,10 @@ var createWebhookHub = (options) => {
|
|
|
4467
4908
|
};
|
|
4468
4909
|
return hub;
|
|
4469
4910
|
};
|
|
4470
|
-
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4911
|
+
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4912
|
+
status,
|
|
4913
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4914
|
+
});
|
|
4471
4915
|
var adminError3 = (status, message) => json4(status, { error: { type: "mockingbird_admin", message } });
|
|
4472
4916
|
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4473
4917
|
var parseEndpoint = (value) => {
|
|
@@ -4575,7 +5019,6 @@ var AT_HEADER = "x-mockingbird-at";
|
|
|
4575
5019
|
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
4576
5020
|
var DEFAULT_NAMESPACE = "default";
|
|
4577
5021
|
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4578
|
-
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
4579
5022
|
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4580
5023
|
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
4581
5024
|
var effects = /* @__PURE__ */ new WeakMap();
|
|
@@ -4616,6 +5059,22 @@ var operationMatcher = (document3) => {
|
|
|
4616
5059
|
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
4617
5060
|
};
|
|
4618
5061
|
var createRuntime = (options) => {
|
|
5062
|
+
const adminPrefix = resolveAdminPrefix(options.adminPrefix);
|
|
5063
|
+
const internalPaths = Object.entries(options.document?.paths ?? {}).filter(([, item]) => item["x-mockingbird-internal"] === true).map(([path]) => path);
|
|
5064
|
+
for (const path of internalPaths) {
|
|
5065
|
+
if (!path.startsWith(`${ADMIN_PREFIX}/blobs/`))
|
|
5066
|
+
throw new Error(`internal transport ${path} must live under ${ADMIN_PREFIX}/blobs/`);
|
|
5067
|
+
}
|
|
5068
|
+
assertAdminPrefixAvailable(adminPrefix, Object.keys(options.document?.paths ?? {}).filter((path) => !internalPaths.includes(path)));
|
|
5069
|
+
const internalMatch = (path) => {
|
|
5070
|
+
for (const pattern of internalPaths) {
|
|
5071
|
+
const actual = path.split("/");
|
|
5072
|
+
const expected = pattern.split("/");
|
|
5073
|
+
if (actual.length === expected.length && expected.every((part, i) => part.startsWith("{") || part === actual[i]))
|
|
5074
|
+
return pattern;
|
|
5075
|
+
}
|
|
5076
|
+
return void 0;
|
|
5077
|
+
};
|
|
4619
5078
|
assertAdminUi(options.adminUi);
|
|
4620
5079
|
const sqlite = bootSqlite(options.sqlite);
|
|
4621
5080
|
const clock = options.clock ?? createClock();
|
|
@@ -4630,6 +5089,20 @@ var createRuntime = (options) => {
|
|
|
4630
5089
|
const instances = /* @__PURE__ */ new Map();
|
|
4631
5090
|
const publicNamespaces = /* @__PURE__ */ new Set();
|
|
4632
5091
|
const branchRngs = /* @__PURE__ */ new Map();
|
|
5092
|
+
const namespaceClocks = /* @__PURE__ */ new Map();
|
|
5093
|
+
const namespaceForStorage = /* @__PURE__ */ new Map();
|
|
5094
|
+
let isolatedNamespaces = false;
|
|
5095
|
+
const namespaceClock = (name = DEFAULT_NAMESPACE) => {
|
|
5096
|
+
if (!isolatedNamespaces || name === DEFAULT_NAMESPACE)
|
|
5097
|
+
return clock;
|
|
5098
|
+
let found = namespaceClocks.get(name);
|
|
5099
|
+
if (!found) {
|
|
5100
|
+
found = createClock();
|
|
5101
|
+
namespaceClocks.set(name, found);
|
|
5102
|
+
}
|
|
5103
|
+
return found;
|
|
5104
|
+
};
|
|
5105
|
+
const fleetPoints = /* @__PURE__ */ new WeakMap();
|
|
4633
5106
|
const timelines = /* @__PURE__ */ new Map();
|
|
4634
5107
|
const branchStorage = /* @__PURE__ */ new Map();
|
|
4635
5108
|
const captured = /* @__PURE__ */ new Map();
|
|
@@ -4643,17 +5116,22 @@ var createRuntime = (options) => {
|
|
|
4643
5116
|
if (!NAMESPACE_PATTERN.test(key) || !NAMESPACE_PATTERN.test(publicNamespace)) {
|
|
4644
5117
|
throw new RangeError(`namespace must match ${NAMESPACE_PATTERN}: ${JSON.stringify(publicNamespace)}`);
|
|
4645
5118
|
}
|
|
5119
|
+
const createdContextRng = isolatedRng ?? (isolatedNamespaces && key !== DEFAULT_NAMESPACE ? createRng(options.seed ?? 0) : rng);
|
|
4646
5120
|
const created = options.create({
|
|
4647
5121
|
namespace: storageNamespace(key),
|
|
4648
5122
|
publicNamespace,
|
|
5123
|
+
adminPrefix,
|
|
4649
5124
|
sqlite,
|
|
4650
|
-
clock,
|
|
4651
|
-
rng:
|
|
5125
|
+
clock: namespaceClock(publicNamespace),
|
|
5126
|
+
rng: createdContextRng
|
|
4652
5127
|
});
|
|
4653
5128
|
instances.set(key, created);
|
|
5129
|
+
namespaceForStorage.set(key, publicNamespace);
|
|
4654
5130
|
publicNamespaces.add(publicNamespace);
|
|
4655
5131
|
if (isolatedRng)
|
|
4656
5132
|
branchRngs.set(key, isolatedRng);
|
|
5133
|
+
else if (isolatedNamespaces && key !== DEFAULT_NAMESPACE)
|
|
5134
|
+
branchRngs.set(key, createdContextRng);
|
|
4657
5135
|
return created;
|
|
4658
5136
|
};
|
|
4659
5137
|
const instance = (name = DEFAULT_NAMESPACE) => instanceFor(name);
|
|
@@ -4671,7 +5149,7 @@ var createRuntime = (options) => {
|
|
|
4671
5149
|
captured.set(storage, snapshot2);
|
|
4672
5150
|
return Object.freeze({
|
|
4673
5151
|
snapshot: snapshot2,
|
|
4674
|
-
clock: Object.freeze(
|
|
5152
|
+
clock: Object.freeze(namespaceClock(namespaceForStorage.get(storage) ?? storage).state()),
|
|
4675
5153
|
rngState: (branchRngs.get(storage) ?? rng).state()
|
|
4676
5154
|
});
|
|
4677
5155
|
};
|
|
@@ -4681,7 +5159,7 @@ var createRuntime = (options) => {
|
|
|
4681
5159
|
return found;
|
|
4682
5160
|
instance(name);
|
|
4683
5161
|
found = new Timeline({
|
|
4684
|
-
now:
|
|
5162
|
+
now: namespaceClock(name).now,
|
|
4685
5163
|
...options.maxCheckpoints !== void 0 ? { maxCheckpoints: options.maxCheckpoints } : {}
|
|
4686
5164
|
});
|
|
4687
5165
|
found.commit(capture(name));
|
|
@@ -4708,12 +5186,13 @@ var createRuntime = (options) => {
|
|
|
4708
5186
|
const point = history2.checkout("main", at);
|
|
4709
5187
|
restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
|
|
4710
5188
|
captured.set(namespace, point.value.snapshot);
|
|
4711
|
-
rng.setState(point.value.rngState);
|
|
4712
|
-
|
|
5189
|
+
(branchRngs.get(namespace) ?? rng).setState(point.value.rngState);
|
|
5190
|
+
const selectedClock = namespaceClock(namespace);
|
|
5191
|
+
selectedClock.set(point.value.clock.now);
|
|
4713
5192
|
if (point.value.clock.frozen)
|
|
4714
|
-
|
|
5193
|
+
selectedClock.freeze();
|
|
4715
5194
|
else
|
|
4716
|
-
|
|
5195
|
+
selectedClock.unfreeze();
|
|
4717
5196
|
}
|
|
4718
5197
|
return namespace;
|
|
4719
5198
|
}
|
|
@@ -4771,11 +5250,12 @@ var createRuntime = (options) => {
|
|
|
4771
5250
|
const storage = ensureBranch(namespace, branchName);
|
|
4772
5251
|
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
4773
5252
|
captured.set(storage, point.value.snapshot);
|
|
4774
|
-
|
|
5253
|
+
const selectedClock = namespaceClock(namespace);
|
|
5254
|
+
selectedClock.set(point.value.clock.now);
|
|
4775
5255
|
if (point.value.clock.frozen)
|
|
4776
|
-
|
|
5256
|
+
selectedClock.freeze();
|
|
4777
5257
|
else
|
|
4778
|
-
|
|
5258
|
+
selectedClock.unfreeze();
|
|
4779
5259
|
(branchRngs.get(storage) ?? rng).setState(point.value.rngState);
|
|
4780
5260
|
};
|
|
4781
5261
|
const reset = async (name = DEFAULT_NAMESPACE) => {
|
|
@@ -4783,10 +5263,21 @@ var createRuntime = (options) => {
|
|
|
4783
5263
|
options.webhooks?.clear();
|
|
4784
5264
|
for (const each of instances.values())
|
|
4785
5265
|
await each.reset();
|
|
5266
|
+
for (const each of branchRngs.values())
|
|
5267
|
+
each.reset();
|
|
4786
5268
|
timelines.clear();
|
|
5269
|
+
for (const storage of branchStorage.values()) {
|
|
5270
|
+
instances.delete(storage);
|
|
5271
|
+
branchRngs.delete(storage);
|
|
5272
|
+
namespaceForStorage.delete(storage);
|
|
5273
|
+
}
|
|
4787
5274
|
branchStorage.clear();
|
|
4788
|
-
branchRngs.clear();
|
|
4789
5275
|
captured.clear();
|
|
5276
|
+
faults.clear();
|
|
5277
|
+
clock.reset();
|
|
5278
|
+
rng.reset();
|
|
5279
|
+
for (const each of namespaceClocks.values())
|
|
5280
|
+
each.reset();
|
|
4790
5281
|
return;
|
|
4791
5282
|
}
|
|
4792
5283
|
options.webhooks?.clear(name);
|
|
@@ -4804,11 +5295,18 @@ var createRuntime = (options) => {
|
|
|
4804
5295
|
else
|
|
4805
5296
|
clearNamespace(sqlite, storageNamespace(storage));
|
|
4806
5297
|
branchStorage.delete(mapping);
|
|
5298
|
+
instances.delete(storage);
|
|
5299
|
+
namespaceForStorage.delete(storage);
|
|
4807
5300
|
branchRngs.delete(storage);
|
|
4808
5301
|
captured.delete(storage);
|
|
4809
5302
|
}
|
|
4810
5303
|
timelines.delete(name);
|
|
4811
5304
|
captured.delete(name);
|
|
5305
|
+
faults.restore(name, { rules: [], rngState: createRng(options.seed ?? 0).state() });
|
|
5306
|
+
namespaceClock(name).reset();
|
|
5307
|
+
(branchRngs.get(name) ?? rng).reset();
|
|
5308
|
+
journal.clear(name);
|
|
5309
|
+
metrics.reset(name);
|
|
4812
5310
|
};
|
|
4813
5311
|
const snapshot = (name = DEFAULT_NAMESPACE) => {
|
|
4814
5312
|
return checkpoint(name, "main").value.snapshot;
|
|
@@ -4846,6 +5344,53 @@ var createRuntime = (options) => {
|
|
|
4846
5344
|
rng,
|
|
4847
5345
|
credentials,
|
|
4848
5346
|
webhooks: options.webhooks,
|
|
5347
|
+
isolateNamespaces() {
|
|
5348
|
+
if (publicNamespaces.size > 1)
|
|
5349
|
+
throw new Error("enable namespace isolation before creating named namespaces");
|
|
5350
|
+
isolatedNamespaces = true;
|
|
5351
|
+
faults.isolateNamespaces();
|
|
5352
|
+
},
|
|
5353
|
+
namespaceClock,
|
|
5354
|
+
namespaceOf(request) {
|
|
5355
|
+
const url = new URL(request.url);
|
|
5356
|
+
const prefixed = matchNamespacePath(url.pathname, adminPrefix);
|
|
5357
|
+
const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
|
|
5358
|
+
const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
|
|
5359
|
+
if (isAdminPath(path, adminPrefix))
|
|
5360
|
+
return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
|
|
5361
|
+
if (selected !== void 0)
|
|
5362
|
+
return selected;
|
|
5363
|
+
const credential = options.credential?.(request);
|
|
5364
|
+
return (credential !== void 0 ? credentials.get(credential) : void 0) ?? DEFAULT_NAMESPACE;
|
|
5365
|
+
},
|
|
5366
|
+
fleetSnapshot(namespace) {
|
|
5367
|
+
instance(namespace);
|
|
5368
|
+
const handle = {};
|
|
5369
|
+
fleetPoints.set(handle, {
|
|
5370
|
+
namespace,
|
|
5371
|
+
state: capture(namespace),
|
|
5372
|
+
faults: faults.snapshot(namespace),
|
|
5373
|
+
webhooks: options.webhooks?.snapshot(namespace)
|
|
5374
|
+
});
|
|
5375
|
+
return handle;
|
|
5376
|
+
},
|
|
5377
|
+
fleetRestore(handle, namespace) {
|
|
5378
|
+
const point = typeof handle === "object" && handle !== null ? fleetPoints.get(handle) : void 0;
|
|
5379
|
+
if (!point || point.namespace !== namespace)
|
|
5380
|
+
throw new Error("invalid fleet checkpoint");
|
|
5381
|
+
restoreNamespace(sqlite, storageNamespace(namespace), point.state.snapshot);
|
|
5382
|
+
captured.set(namespace, point.state.snapshot);
|
|
5383
|
+
const selectedClock = namespaceClock(namespace);
|
|
5384
|
+
selectedClock.freeze();
|
|
5385
|
+
selectedClock.set(point.state.clock.now);
|
|
5386
|
+
if (!point.state.clock.frozen)
|
|
5387
|
+
selectedClock.unfreeze();
|
|
5388
|
+
(branchRngs.get(namespace) ?? rng).setState(point.state.rngState);
|
|
5389
|
+
faults.restore(namespace, point.faults);
|
|
5390
|
+
if (point.webhooks !== void 0)
|
|
5391
|
+
options.webhooks?.restore(namespace, point.webhooks);
|
|
5392
|
+
timelines.delete(namespace);
|
|
5393
|
+
},
|
|
4849
5394
|
applyPreset: (name, namespace = DEFAULT_NAMESPACE, overrides = {}) => {
|
|
4850
5395
|
const preset = options.presets?.[name];
|
|
4851
5396
|
if (!preset)
|
|
@@ -4877,7 +5422,7 @@ var createRuntime = (options) => {
|
|
|
4877
5422
|
state: (namespace = DEFAULT_NAMESPACE) => inspectState(stateScope(namespace)),
|
|
4878
5423
|
fetch: async (incoming) => {
|
|
4879
5424
|
let request = incoming;
|
|
4880
|
-
const prefixed =
|
|
5425
|
+
const prefixed = matchNamespacePath(new URL(request.url).pathname, adminPrefix);
|
|
4881
5426
|
if (prefixed) {
|
|
4882
5427
|
const url2 = new URL(request.url);
|
|
4883
5428
|
url2.pathname = prefixed[2] ?? "/";
|
|
@@ -4893,7 +5438,15 @@ var createRuntime = (options) => {
|
|
|
4893
5438
|
signal: request.signal
|
|
4894
5439
|
});
|
|
4895
5440
|
}
|
|
4896
|
-
|
|
5441
|
+
const internalUrl = new URL(request.url);
|
|
5442
|
+
const internalPath = isAdminPath(internalUrl.pathname, adminPrefix) ? `${ADMIN_PREFIX}${internalUrl.pathname.slice(adminPrefix.length)}` : "";
|
|
5443
|
+
const internal = internalMatch(internalPath);
|
|
5444
|
+
const obsoleteInternalPath = adminPrefix !== ADMIN_PREFIX && internalMatch(internalUrl.pathname) && !internal;
|
|
5445
|
+
if (internal) {
|
|
5446
|
+
internalUrl.pathname = internalPath;
|
|
5447
|
+
request = new Request(internalUrl, request);
|
|
5448
|
+
}
|
|
5449
|
+
let namespace = internal ? internalUrl.searchParams.get("namespace") ?? control.namespaceOf(request) : control.namespaceOf(request);
|
|
4897
5450
|
if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
4898
5451
|
const credential = options.credential(request);
|
|
4899
5452
|
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
@@ -4913,7 +5466,9 @@ var createRuntime = (options) => {
|
|
|
4913
5466
|
return copy;
|
|
4914
5467
|
}
|
|
4915
5468
|
};
|
|
4916
|
-
|
|
5469
|
+
if (obsoleteInternalPath)
|
|
5470
|
+
return stamp(adminFail(404, "internal transport is only available under adminPrefix"));
|
|
5471
|
+
const handled = internal ? void 0 : await control.handle(request);
|
|
4917
5472
|
if (handled)
|
|
4918
5473
|
return stamp(handled);
|
|
4919
5474
|
const started = monotonicNow();
|
|
@@ -4954,7 +5509,7 @@ var createRuntime = (options) => {
|
|
|
4954
5509
|
type: "mockingbird_admin",
|
|
4955
5510
|
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
4956
5511
|
}
|
|
4957
|
-
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
5512
|
+
}), { status: 400, headers: { "content-type": "application/json; charset=utf-8" } }));
|
|
4958
5513
|
}
|
|
4959
5514
|
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
4960
5515
|
log(400);
|
|
@@ -5021,6 +5576,7 @@ var createRuntime = (options) => {
|
|
|
5021
5576
|
startedAt: wallNow(),
|
|
5022
5577
|
wallNow,
|
|
5023
5578
|
clock,
|
|
5579
|
+
namespaceClock,
|
|
5024
5580
|
faults,
|
|
5025
5581
|
metrics,
|
|
5026
5582
|
journal,
|
|
@@ -5070,15 +5626,16 @@ var createRuntime = (options) => {
|
|
|
5070
5626
|
...options.admin?.(runtime) ?? {},
|
|
5071
5627
|
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
5072
5628
|
// maps `presets`, and a service route must not replace that list with a record.
|
|
5073
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
5629
|
+
...options.presets ? presetRoutes(options.presets, runtime, adminPrefix) : {},
|
|
5074
5630
|
...stateAdminRoutes({
|
|
5075
5631
|
open: stateScope,
|
|
5076
5632
|
afterWrite: (namespace) => {
|
|
5077
5633
|
checkpoint(namespace, "main");
|
|
5078
5634
|
}
|
|
5079
5635
|
}),
|
|
5080
|
-
...adminUiRoutes(options.name, options.adminUi)
|
|
5636
|
+
...adminUiRoutes(options.name, options.adminUi, adminPrefix)
|
|
5081
5637
|
},
|
|
5638
|
+
adminPrefix,
|
|
5082
5639
|
adminKey: options.adminKey
|
|
5083
5640
|
});
|
|
5084
5641
|
return defineMock(runtime);
|
|
@@ -5092,7 +5649,10 @@ var mutableResponse = (response) => {
|
|
|
5092
5649
|
return new Response(response.body, response);
|
|
5093
5650
|
}
|
|
5094
5651
|
};
|
|
5095
|
-
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5652
|
+
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5653
|
+
status,
|
|
5654
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
5655
|
+
});
|
|
5096
5656
|
var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
|
|
5097
5657
|
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5098
5658
|
var credentialRoutes = (registry) => ({
|
|
@@ -5144,7 +5704,7 @@ var credentialRoutes = (registry) => ({
|
|
|
5144
5704
|
return adminJson(200, { status: "ok" });
|
|
5145
5705
|
}
|
|
5146
5706
|
});
|
|
5147
|
-
var presetRoutes = (presets, runtime) => ({
|
|
5707
|
+
var presetRoutes = (presets, runtime, adminPrefix) => ({
|
|
5148
5708
|
"GET /faults/presets": () => {
|
|
5149
5709
|
const body = {
|
|
5150
5710
|
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
@@ -5154,7 +5714,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
5154
5714
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
5155
5715
|
const name = params.name;
|
|
5156
5716
|
if (!presets[name])
|
|
5157
|
-
return adminFail(404, `no fault preset ${name}; GET /
|
|
5717
|
+
return adminFail(404, `no fault preset ${name}; GET ${adminPrefix}/faults/presets`);
|
|
5158
5718
|
const overrides = isObject(body) ? body : {};
|
|
5159
5719
|
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
5160
5720
|
}
|
|
@@ -6334,15 +6894,17 @@ var createRuntime2 = (options = {}) => {
|
|
|
6334
6894
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
6335
6895
|
...options.clock ? { clock: options.clock } : {},
|
|
6336
6896
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
6897
|
+
...options.adminPrefix !== void 0 ? { adminPrefix: options.adminPrefix } : {},
|
|
6337
6898
|
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
6338
6899
|
...options.onLog ? { onLog: options.onLog } : {},
|
|
6339
6900
|
credential: bearerToken,
|
|
6340
6901
|
presets: FLEX_PRESETS,
|
|
6341
6902
|
webhooks: hub,
|
|
6342
|
-
create: ({ sqlite, namespace, publicNamespace, clock }) => new FlexAPI({
|
|
6903
|
+
create: ({ sqlite, namespace, publicNamespace, adminPrefix, clock }) => new FlexAPI({
|
|
6343
6904
|
sqlite,
|
|
6344
6905
|
namespace,
|
|
6345
6906
|
publicNamespace,
|
|
6907
|
+
adminPrefix,
|
|
6346
6908
|
now: clock.now,
|
|
6347
6909
|
...options.products ? { products: options.products } : {},
|
|
6348
6910
|
...options.settings ? { settings: options.settings } : {},
|
|
@@ -6443,12 +7005,14 @@ var FlexAPI = class {
|
|
|
6443
7005
|
service;
|
|
6444
7006
|
idempotency;
|
|
6445
7007
|
now;
|
|
7008
|
+
adminPrefix;
|
|
6446
7009
|
publicNamespace;
|
|
6447
7010
|
onEvent;
|
|
6448
7011
|
constructor(options = {}) {
|
|
6449
7012
|
const sqlite = bootSqlite(options.sqlite);
|
|
6450
7013
|
const namespace = options.namespace ?? FLEX_NAMESPACE;
|
|
6451
7014
|
this.now = options.now ?? (() => Date.now());
|
|
7015
|
+
this.adminPrefix = options.adminPrefix ?? "/__admin";
|
|
6452
7016
|
this.publicNamespace = options.publicNamespace ?? "default";
|
|
6453
7017
|
this.onEvent = options.onEvent;
|
|
6454
7018
|
this.state = new FlexState(sqlite, namespace, {
|
|
@@ -6617,7 +7181,7 @@ var FlexAPI = class {
|
|
|
6617
7181
|
pageBase(context) {
|
|
6618
7182
|
const configured = this.state.current().publicUrl;
|
|
6619
7183
|
if (configured) return configured.replace(/\/$/, "");
|
|
6620
|
-
const prefix = this.publicNamespace === "default" ? "" :
|
|
7184
|
+
const prefix = this.publicNamespace === "default" ? "" : `${this.adminPrefix}/ns/${encodeURIComponent(this.publicNamespace)}`;
|
|
6621
7185
|
return `${context.url.origin}${prefix}`;
|
|
6622
7186
|
}
|
|
6623
7187
|
async createSession(context) {
|
|
@@ -7242,6 +7806,9 @@ var FlexAPI = class {
|
|
|
7242
7806
|
var isNextActionType = (value) => typeof value === "string" && NEXT_ACTION_TYPES.includes(value);
|
|
7243
7807
|
|
|
7244
7808
|
export {
|
|
7809
|
+
createClock,
|
|
7810
|
+
resolveAdminPrefix,
|
|
7811
|
+
parseDuration,
|
|
7245
7812
|
document2 as document,
|
|
7246
7813
|
operationIds,
|
|
7247
7814
|
supportedOperationIds,
|
|
@@ -7264,4 +7831,4 @@ export {
|
|
|
7264
7831
|
FlexAPI,
|
|
7265
7832
|
isNextActionType
|
|
7266
7833
|
};
|
|
7267
|
-
//# sourceMappingURL=chunk-
|
|
7834
|
+
//# sourceMappingURL=chunk-E3W2D3EY.js.map
|