@crvouga/mockingbird-service-resend 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 +38 -0
- package/DISCOVERY.md +55 -0
- package/README.md +2 -2
- package/SUPPORT.md +15 -0
- package/dist/{chunk-ESQWM4LS.js → chunk-AVLXDCB3.js} +853 -280
- package/dist/chunk-AVLXDCB3.js.map +7 -0
- package/dist/chunk-K5P64BHJ.js +861 -0
- package/dist/chunk-K5P64BHJ.js.map +7 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +39 -5
- package/dist/index.js +1 -1
- package/dist/server.d.ts +69 -5
- package/dist/server.js +2 -2
- package/openapi.yaml +495 -0
- package/package.json +29 -4
- package/dist/chunk-ESQWM4LS.js.map +0 -7
- package/dist/chunk-WJ3RNVO6.js +0 -373
- package/dist/chunk-WJ3RNVO6.js.map +0 -7
|
@@ -110,13 +110,31 @@ var Collection = class {
|
|
|
110
110
|
};
|
|
111
111
|
|
|
112
112
|
// ../core/dist/control.js
|
|
113
|
-
var HEALTH_PATH = "/health";
|
|
114
113
|
var ADMIN_PREFIX = "/__admin";
|
|
114
|
+
var resolveAdminPrefix = (prefix = ADMIN_PREFIX) => {
|
|
115
|
+
if (!/^\/[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*$/.test(prefix)) {
|
|
116
|
+
throw new Error("adminPrefix must be an absolute non-root path of literal segments, without a trailing slash");
|
|
117
|
+
}
|
|
118
|
+
return prefix;
|
|
119
|
+
};
|
|
120
|
+
var isAdminPath = (path, prefix) => path === prefix || path.startsWith(`${prefix}/`);
|
|
121
|
+
var matchNamespacePath = (path, prefix) => /^\/ns\/([^/]+)(\/.*)?$/.exec(isAdminPath(path, prefix) ? path.slice(prefix.length) : "");
|
|
122
|
+
var assertAdminPrefixAvailable = (prefix, paths) => {
|
|
123
|
+
const reserved = prefix.split("/").slice(1);
|
|
124
|
+
for (const path of paths) {
|
|
125
|
+
const segments = path.split("/").slice(1);
|
|
126
|
+
if (segments.length < reserved.length || /^\{[^}]+\}$/.test(segments[0] ?? ""))
|
|
127
|
+
continue;
|
|
128
|
+
if (reserved.every((part, i) => segments[i] === part || /^\{[^}]+\}$/.test(segments[i] ?? ""))) {
|
|
129
|
+
throw new Error(`vendor route ${path} collides with adminPrefix ${prefix}; choose a different adminPrefix`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
115
133
|
var ADMIN_KEY_HEADER = "x-mockingbird-admin-key";
|
|
116
134
|
var NAMESPACE_HEADER = "x-mockingbird-namespace";
|
|
117
135
|
var json = (status, body) => new Response(JSON.stringify(body), {
|
|
118
136
|
status,
|
|
119
|
-
headers: { "content-type": "application/json" }
|
|
137
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
120
138
|
});
|
|
121
139
|
var adminError = (status, message) => json(status, { error: { type: "mockingbird_admin", message } });
|
|
122
140
|
var UNITS = {
|
|
@@ -168,11 +186,25 @@ var readJson = async (request) => {
|
|
|
168
186
|
};
|
|
169
187
|
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
170
188
|
var createControlPlane = (context) => {
|
|
189
|
+
const adminPrefix = resolveAdminPrefix(context.adminPrefix);
|
|
171
190
|
const snapshots = /* @__PURE__ */ new Map();
|
|
172
191
|
let snapshotCounter = 0;
|
|
173
192
|
const headerNamespace = (request) => request.headers.get(NAMESPACE_HEADER) ?? context.defaultNamespace;
|
|
174
193
|
const adminNamespace = (request, url) => url.searchParams.get("namespace") ?? headerNamespace(request);
|
|
194
|
+
const health = () => {
|
|
195
|
+
return json(200, {
|
|
196
|
+
...context.describe(),
|
|
197
|
+
status: "ok",
|
|
198
|
+
service: context.name,
|
|
199
|
+
uptimeMs: context.wallNow() - context.startedAt,
|
|
200
|
+
clock: context.clock.state(),
|
|
201
|
+
namespaces: context.namespaces().length,
|
|
202
|
+
adminPrefix,
|
|
203
|
+
adminUi: `${adminPrefix}/ui`
|
|
204
|
+
});
|
|
205
|
+
};
|
|
175
206
|
const builtin = {
|
|
207
|
+
"GET /health": health,
|
|
176
208
|
"GET /": () => json(200, {
|
|
177
209
|
service: context.name,
|
|
178
210
|
routes: [...Object.keys(builtin), ...Object.keys(context.routes)].sort()
|
|
@@ -183,29 +215,30 @@ var createControlPlane = (context) => {
|
|
|
183
215
|
return json(200, { status: "ok", reset: target === "*" ? context.namespaces() : [target] });
|
|
184
216
|
},
|
|
185
217
|
"GET /namespaces": () => json(200, { default: context.defaultNamespace, namespaces: context.namespaces() }),
|
|
186
|
-
"GET /clock": () => json(200, context.clock.state()),
|
|
187
|
-
"POST /clock": ({ body }) => {
|
|
218
|
+
"GET /clock": ({ namespace }) => json(200, (context.namespaceClock?.(namespace) ?? context.clock).state()),
|
|
219
|
+
"POST /clock": ({ body, namespace }) => {
|
|
220
|
+
const clock = context.namespaceClock?.(namespace) ?? context.clock;
|
|
188
221
|
if (!isRecord(body))
|
|
189
222
|
return adminError(400, "expected a JSON object");
|
|
190
223
|
if (body.reset === true)
|
|
191
|
-
|
|
224
|
+
clock.reset();
|
|
192
225
|
if (body.freeze === true)
|
|
193
|
-
|
|
226
|
+
clock.freeze();
|
|
194
227
|
if (body.set !== void 0) {
|
|
195
228
|
const instant = parseInstant(body.set);
|
|
196
229
|
if (instant === void 0)
|
|
197
230
|
return adminError(400, "set: expected epoch ms or ISO-8601");
|
|
198
|
-
|
|
231
|
+
clock.set(instant);
|
|
199
232
|
}
|
|
200
233
|
if (body.advance !== void 0) {
|
|
201
234
|
const delta = parseDuration(body.advance);
|
|
202
235
|
if (delta === void 0)
|
|
203
236
|
return adminError(400, 'advance: expected ms or "15m"-style');
|
|
204
|
-
|
|
237
|
+
clock.advance(delta);
|
|
205
238
|
}
|
|
206
239
|
if (body.freeze === false)
|
|
207
|
-
|
|
208
|
-
return json(200,
|
|
240
|
+
clock.unfreeze();
|
|
241
|
+
return json(200, clock.state());
|
|
209
242
|
},
|
|
210
243
|
"GET /faults": () => json(200, { faults: context.faults.list() }),
|
|
211
244
|
"POST /faults": ({ body, namespace }) => {
|
|
@@ -338,7 +371,14 @@ var createControlPlane = (context) => {
|
|
|
338
371
|
return json(200, { status: "ok" });
|
|
339
372
|
}
|
|
340
373
|
};
|
|
341
|
-
const
|
|
374
|
+
for (const key of Object.keys(context.routes)) {
|
|
375
|
+
if (Object.hasOwn(builtin, key))
|
|
376
|
+
throw new Error(`admin route ${key} overrides a shared route`);
|
|
377
|
+
const path = key.slice(key.indexOf(" ") + 1);
|
|
378
|
+
if (isAdminPath(path, "/ns") || isAdminPath(path, "/blobs"))
|
|
379
|
+
throw new Error(`admin route ${key} overlaps an internal transport`);
|
|
380
|
+
}
|
|
381
|
+
const routes = [...Object.entries(builtin), ...Object.entries(context.routes)].map(([key, handler]) => {
|
|
342
382
|
const space = key.indexOf(" ");
|
|
343
383
|
return { method: key.slice(0, space), pattern: key.slice(space + 1), handler };
|
|
344
384
|
});
|
|
@@ -346,21 +386,13 @@ var createControlPlane = (context) => {
|
|
|
346
386
|
namespaceOf: headerNamespace,
|
|
347
387
|
async handle(request) {
|
|
348
388
|
const url = new URL(request.url);
|
|
349
|
-
if (url.pathname ===
|
|
350
|
-
return
|
|
351
|
-
status: "ok",
|
|
352
|
-
service: context.name,
|
|
353
|
-
uptimeMs: context.wallNow() - context.startedAt,
|
|
354
|
-
clock: context.clock.state(),
|
|
355
|
-
namespaces: context.namespaces().length,
|
|
356
|
-
adminUi: `${ADMIN_PREFIX}/ui`,
|
|
357
|
-
...context.describe()
|
|
358
|
-
});
|
|
389
|
+
if (url.pathname === `${adminPrefix}/health` && request.method === "GET") {
|
|
390
|
+
return health();
|
|
359
391
|
}
|
|
360
|
-
if (url.pathname
|
|
392
|
+
if (!isAdminPath(url.pathname, adminPrefix)) {
|
|
361
393
|
return void 0;
|
|
362
394
|
}
|
|
363
|
-
const path = url.pathname.slice(
|
|
395
|
+
const path = url.pathname.slice(adminPrefix.length) || "/";
|
|
364
396
|
const shell = request.method === "GET" && (path === "/ui" || path === "/ui/");
|
|
365
397
|
if (!shell && context.adminKey !== void 0 && request.headers.get(ADMIN_KEY_HEADER) !== context.adminKey) {
|
|
366
398
|
return adminError(401, `missing or wrong ${ADMIN_KEY_HEADER}`);
|
|
@@ -368,7 +400,12 @@ var createControlPlane = (context) => {
|
|
|
368
400
|
for (const route of routes) {
|
|
369
401
|
if (route.method !== request.method)
|
|
370
402
|
continue;
|
|
371
|
-
|
|
403
|
+
let params;
|
|
404
|
+
try {
|
|
405
|
+
params = matchRoute(route.pattern, path);
|
|
406
|
+
} catch {
|
|
407
|
+
return adminError(400, "invalid path encoding");
|
|
408
|
+
}
|
|
372
409
|
if (!params)
|
|
373
410
|
continue;
|
|
374
411
|
let body;
|
|
@@ -377,15 +414,18 @@ var createControlPlane = (context) => {
|
|
|
377
414
|
} catch {
|
|
378
415
|
return adminError(400, "request body is not valid JSON");
|
|
379
416
|
}
|
|
417
|
+
const namespace = adminNamespace(request, url);
|
|
418
|
+
if (!/^[A-Za-z0-9_.-]{1,64}$/.test(namespace))
|
|
419
|
+
return adminError(400, "invalid namespace");
|
|
380
420
|
return route.handler({
|
|
381
421
|
request,
|
|
382
422
|
url,
|
|
383
423
|
params,
|
|
384
|
-
namespace
|
|
424
|
+
namespace,
|
|
385
425
|
body
|
|
386
426
|
});
|
|
387
427
|
}
|
|
388
|
-
return adminError(404, `no admin route ${request.method} ${path}; GET ${
|
|
428
|
+
return adminError(404, `no admin route ${request.method} ${path}; GET ${adminPrefix} lists them`);
|
|
389
429
|
}
|
|
390
430
|
};
|
|
391
431
|
};
|
|
@@ -474,7 +514,33 @@ var faultResponse = (rule) => {
|
|
|
474
514
|
};
|
|
475
515
|
var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) => {
|
|
476
516
|
const entries = [];
|
|
517
|
+
let isolated = false;
|
|
518
|
+
const scopedRngs = /* @__PURE__ */ new Map();
|
|
519
|
+
const random = (namespace) => {
|
|
520
|
+
if (!isolated || namespace === "default")
|
|
521
|
+
return rng;
|
|
522
|
+
let scoped = scopedRngs.get(namespace);
|
|
523
|
+
if (!scoped) {
|
|
524
|
+
scoped = createRng(rng.seed);
|
|
525
|
+
scopedRngs.set(namespace, scoped);
|
|
526
|
+
}
|
|
527
|
+
return scoped;
|
|
528
|
+
};
|
|
477
529
|
return {
|
|
530
|
+
isolateNamespaces() {
|
|
531
|
+
isolated = true;
|
|
532
|
+
},
|
|
533
|
+
snapshot(namespace) {
|
|
534
|
+
return {
|
|
535
|
+
rules: structuredClone(entries.map((entry, position) => ({
|
|
536
|
+
...entry.rule,
|
|
537
|
+
remaining: entry.remaining,
|
|
538
|
+
hits: entry.hits,
|
|
539
|
+
position
|
|
540
|
+
})).filter((entry) => entry.namespace === namespace)),
|
|
541
|
+
rngState: random(namespace).state()
|
|
542
|
+
};
|
|
543
|
+
},
|
|
478
544
|
add(rule) {
|
|
479
545
|
const existing = entries.findIndex((e) => e.rule.id === rule.id);
|
|
480
546
|
const entry = { rule, remaining: rule.count ?? null, hits: 0 };
|
|
@@ -494,6 +560,16 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
|
|
|
494
560
|
},
|
|
495
561
|
clear() {
|
|
496
562
|
entries.length = 0;
|
|
563
|
+
rng.reset();
|
|
564
|
+
scopedRngs.clear();
|
|
565
|
+
},
|
|
566
|
+
restore(namespace, state) {
|
|
567
|
+
for (let i = entries.length - 1; i >= 0; i--)
|
|
568
|
+
if (entries[i]?.rule.namespace === namespace)
|
|
569
|
+
entries.splice(i, 1);
|
|
570
|
+
for (const { remaining, hits, position, ...rule } of state.rules)
|
|
571
|
+
entries.splice(Math.min(position, entries.length), 0, { rule, remaining, hits });
|
|
572
|
+
random(namespace).setState(state.rngState);
|
|
497
573
|
},
|
|
498
574
|
async take(candidate) {
|
|
499
575
|
const hits = [];
|
|
@@ -503,7 +579,7 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
|
|
|
503
579
|
if (!matches(entry.rule, candidate))
|
|
504
580
|
continue;
|
|
505
581
|
const rate = entry.rule.rate ?? 1;
|
|
506
|
-
if (
|
|
582
|
+
if (random(candidate.namespace).next() >= rate)
|
|
507
583
|
continue;
|
|
508
584
|
entry.hits++;
|
|
509
585
|
if (entry.remaining !== null)
|
|
@@ -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;
|
|
@@ -1204,7 +1294,10 @@ var OutboxStore = class {
|
|
|
1204
1294
|
return query.limit !== void 0 ? matched.slice(-Math.max(0, query.limit)) : matched;
|
|
1205
1295
|
}
|
|
1206
1296
|
};
|
|
1207
|
-
var json2 = (status, body) => new Response(JSON.stringify(body), {
|
|
1297
|
+
var json2 = (status, body) => new Response(JSON.stringify(body), {
|
|
1298
|
+
status,
|
|
1299
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
1300
|
+
});
|
|
1208
1301
|
var parseSince = (value) => {
|
|
1209
1302
|
if (value === null)
|
|
1210
1303
|
return void 0;
|
|
@@ -1850,6 +1943,34 @@ dialog:not([open]) {
|
|
|
1850
1943
|
function isRecord2(value) {
|
|
1851
1944
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1852
1945
|
}
|
|
1946
|
+
function highlightJsonText(text) {
|
|
1947
|
+
const escapeToken = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
1948
|
+
switch (char) {
|
|
1949
|
+
case "&":
|
|
1950
|
+
return "&";
|
|
1951
|
+
case "<":
|
|
1952
|
+
return "<";
|
|
1953
|
+
case ">":
|
|
1954
|
+
return ">";
|
|
1955
|
+
case '"':
|
|
1956
|
+
return """;
|
|
1957
|
+
default:
|
|
1958
|
+
return "'";
|
|
1959
|
+
}
|
|
1960
|
+
});
|
|
1961
|
+
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;
|
|
1962
|
+
let cursor = 0;
|
|
1963
|
+
let html = "";
|
|
1964
|
+
for (const match of text.matchAll(token)) {
|
|
1965
|
+
const index = match.index ?? 0;
|
|
1966
|
+
const value = match[0];
|
|
1967
|
+
html += escapeToken(text.slice(cursor, index));
|
|
1968
|
+
const kind = value.startsWith('"') ? value.trimEnd().endsWith(":") ? "key" : "string" : value === "true" || value === "false" ? "boolean" : value === "null" ? "null" : "number";
|
|
1969
|
+
html += `<span class="json-${kind}">${escapeToken(value)}</span>`;
|
|
1970
|
+
cursor = index + value.length;
|
|
1971
|
+
}
|
|
1972
|
+
return html + escapeToken(text.slice(cursor));
|
|
1973
|
+
}
|
|
1853
1974
|
function mountAdminBrand(doc, host, service, catalog) {
|
|
1854
1975
|
const clip = (value, max) => {
|
|
1855
1976
|
if (typeof value !== "string")
|
|
@@ -2211,14 +2332,16 @@ function bootAdmin(config) {
|
|
|
2211
2332
|
const state = {
|
|
2212
2333
|
namespace: "default",
|
|
2213
2334
|
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
2214
|
-
view: "overview",
|
|
2335
|
+
view: ["overview", "state", "clock", "faults", "journal", "routes"].includes(location.hash.slice(1)) ? location.hash.slice(1) : "overview",
|
|
2215
2336
|
collection: "",
|
|
2216
2337
|
shape: null,
|
|
2217
2338
|
route: null,
|
|
2339
|
+
routes: [],
|
|
2218
2340
|
editing: null
|
|
2219
2341
|
};
|
|
2220
2342
|
const standard = new Set(config.standardRoutes);
|
|
2221
2343
|
const banner = el("banner", HTMLElement);
|
|
2344
|
+
const bannerMessage = el("banner-message", HTMLElement);
|
|
2222
2345
|
const keyInput = el("admin-key", HTMLInputElement);
|
|
2223
2346
|
keyInput.value = state.key;
|
|
2224
2347
|
const params = new URLSearchParams(location.search);
|
|
@@ -2229,18 +2352,21 @@ function bootAdmin(config) {
|
|
|
2229
2352
|
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
2230
2353
|
params.delete("key");
|
|
2231
2354
|
const next = params.toString();
|
|
2232
|
-
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
2355
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : "") + location.hash);
|
|
2233
2356
|
}
|
|
2234
2357
|
const queryNamespace = params.get("namespace");
|
|
2235
2358
|
if (queryNamespace)
|
|
2236
2359
|
state.namespace = queryNamespace;
|
|
2237
2360
|
const showError = (error2) => {
|
|
2238
|
-
|
|
2361
|
+
bannerMessage.textContent = error2 instanceof Error ? error2.message : String(error2);
|
|
2239
2362
|
banner.classList.add("show");
|
|
2363
|
+
banner.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
2240
2364
|
};
|
|
2241
2365
|
const clearError = () => {
|
|
2242
2366
|
banner.classList.remove("show");
|
|
2367
|
+
bannerMessage.textContent = "";
|
|
2243
2368
|
};
|
|
2369
|
+
el("banner-dismiss", HTMLButtonElement).addEventListener("click", clearError);
|
|
2244
2370
|
const api = async (method, path, body) => {
|
|
2245
2371
|
const headers = { accept: "application/json" };
|
|
2246
2372
|
if (state.key !== "")
|
|
@@ -2284,16 +2410,21 @@ function bootAdmin(config) {
|
|
|
2284
2410
|
namespace: () => state.namespace
|
|
2285
2411
|
};
|
|
2286
2412
|
const isPanelScript = (value) => typeof value === "function";
|
|
2413
|
+
const themeButton = el("theme", HTMLButtonElement);
|
|
2287
2414
|
const setTheme = (theme) => {
|
|
2288
2415
|
if (theme)
|
|
2289
2416
|
document.documentElement.dataset.theme = theme;
|
|
2290
2417
|
else
|
|
2291
2418
|
delete document.documentElement.dataset.theme;
|
|
2419
|
+
const dark = theme ? theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2420
|
+
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>';
|
|
2421
|
+
const label = dark ? "Use light theme" : "Use dark theme";
|
|
2422
|
+
themeButton.setAttribute("aria-label", label);
|
|
2423
|
+
themeButton.title = label;
|
|
2292
2424
|
};
|
|
2293
2425
|
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
2426
|
+
setTheme(storedTheme === "dark" || storedTheme === "light" ? storedTheme : null);
|
|
2427
|
+
themeButton.addEventListener("click", () => {
|
|
2297
2428
|
const current = document.documentElement.dataset.theme;
|
|
2298
2429
|
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2299
2430
|
const next = dark ? "light" : "dark";
|
|
@@ -2312,6 +2443,7 @@ function bootAdmin(config) {
|
|
|
2312
2443
|
continue;
|
|
2313
2444
|
section.hidden = section.id !== `view-${view}`;
|
|
2314
2445
|
}
|
|
2446
|
+
history.replaceState(null, "", `${location.pathname}${location.search}#${encodeURIComponent(view)}`);
|
|
2315
2447
|
};
|
|
2316
2448
|
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
2317
2449
|
const target = event.target;
|
|
@@ -2326,7 +2458,7 @@ function bootAdmin(config) {
|
|
|
2326
2458
|
show(view);
|
|
2327
2459
|
refresh().catch(showError);
|
|
2328
2460
|
});
|
|
2329
|
-
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>`;
|
|
2461
|
+
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>`;
|
|
2330
2462
|
const table = (columns, rows) => {
|
|
2331
2463
|
if (rows.length === 0)
|
|
2332
2464
|
return '<p class="empty">Nothing here yet.</p>';
|
|
@@ -2336,16 +2468,22 @@ function bootAdmin(config) {
|
|
|
2336
2468
|
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
2337
2469
|
return `<tr${attrs}>${cells}</tr>`;
|
|
2338
2470
|
}).join("");
|
|
2339
|
-
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
2471
|
+
return `<div class="table-shell"><table><thead>${head}</thead><tbody>${body}</tbody></table></div>`;
|
|
2340
2472
|
};
|
|
2341
2473
|
const preview = (value) => {
|
|
2342
2474
|
const text = JSON.stringify(value) ?? "null";
|
|
2343
|
-
|
|
2475
|
+
const clipped = text.length > 180 ? `${text.slice(0, 180)}\u2026` : text;
|
|
2476
|
+
return `<code class="json-inline">${highlightJsonText(clipped)}</code>`;
|
|
2344
2477
|
};
|
|
2345
2478
|
const when = (epoch) => {
|
|
2346
2479
|
const date = new Date(epoch);
|
|
2347
2480
|
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
2348
2481
|
};
|
|
2482
|
+
const journalWhen = (value) => {
|
|
2483
|
+
const numeric = value.trim() === "" ? Number.NaN : Number(value);
|
|
2484
|
+
const epoch = Number.isFinite(numeric) ? numeric : Date.parse(value);
|
|
2485
|
+
return Number.isFinite(epoch) ? when(epoch) : value;
|
|
2486
|
+
};
|
|
2349
2487
|
const clockCard = (epoch) => {
|
|
2350
2488
|
const date = new Date(epoch);
|
|
2351
2489
|
if (Number.isNaN(date.getTime()))
|
|
@@ -2465,7 +2603,7 @@ function bootAdmin(config) {
|
|
|
2465
2603
|
};
|
|
2466
2604
|
const renderOverview = async () => {
|
|
2467
2605
|
const [health, shape, clock, journal] = await Promise.all([
|
|
2468
|
-
originGet(
|
|
2606
|
+
originGet(`${config.adminPrefix}/health`),
|
|
2469
2607
|
api("GET", "/state"),
|
|
2470
2608
|
api("GET", "/clock"),
|
|
2471
2609
|
api("GET", "/requests?limit=8")
|
|
@@ -2482,7 +2620,7 @@ function bootAdmin(config) {
|
|
|
2482
2620
|
},
|
|
2483
2621
|
{
|
|
2484
2622
|
k: "Status",
|
|
2485
|
-
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
2623
|
+
v: `<span class="badge ok">${esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")}</span>`
|
|
2486
2624
|
},
|
|
2487
2625
|
{ k: "Namespace", v: esc(state.namespace) },
|
|
2488
2626
|
{ k: "Collections", v: String(decoded.collections.length) },
|
|
@@ -2569,26 +2707,46 @@ function bootAdmin(config) {
|
|
|
2569
2707
|
const renderJournal = async () => {
|
|
2570
2708
|
const journal = await api("GET", "/requests?limit=100");
|
|
2571
2709
|
el("journal", HTMLElement).innerHTML = table([
|
|
2572
|
-
{ label: "When", cell: (row) => esc(row.at) },
|
|
2573
|
-
{
|
|
2710
|
+
{ label: "When", cell: (row) => esc(journalWhen(row.at)) },
|
|
2711
|
+
{
|
|
2712
|
+
label: "Method",
|
|
2713
|
+
cell: (row) => `<span class="method">${esc(row.method)}</span>`
|
|
2714
|
+
},
|
|
2574
2715
|
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2575
|
-
{
|
|
2716
|
+
{
|
|
2717
|
+
label: "Status",
|
|
2718
|
+
cell: (row) => `<span class="badge ${row.status.startsWith("2") ? "ok" : row.status.startsWith("4") || row.status.startsWith("5") ? "warn" : ""}">${esc(row.status)}</span>`
|
|
2719
|
+
},
|
|
2576
2720
|
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
2577
2721
|
], decodeJournal(journal));
|
|
2578
2722
|
};
|
|
2579
|
-
const
|
|
2580
|
-
const
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2723
|
+
const paintRoutes = () => {
|
|
2724
|
+
const query = el("route-search", HTMLInputElement).value.trim().toLocaleLowerCase();
|
|
2725
|
+
const routes = state.routes.filter((route) => route.toLocaleLowerCase().includes(query));
|
|
2726
|
+
el("route-count", HTMLElement).textContent = `${routes.length} of ${state.routes.length}`;
|
|
2727
|
+
if (state.routes.length === 0) {
|
|
2728
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No admin routes are available.</p>';
|
|
2729
|
+
return;
|
|
2730
|
+
}
|
|
2731
|
+
if (routes.length === 0) {
|
|
2732
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No routes match this filter.</p>';
|
|
2733
|
+
return;
|
|
2734
|
+
}
|
|
2584
2735
|
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2585
2736
|
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2586
2737
|
const parts = route.split(" ");
|
|
2587
2738
|
const method = parts[0] ?? "";
|
|
2588
2739
|
const path = parts.slice(1).join(" ");
|
|
2589
|
-
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>`;
|
|
2740
|
+
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>`;
|
|
2590
2741
|
}).join("");
|
|
2591
2742
|
};
|
|
2743
|
+
const renderRoutes = async () => {
|
|
2744
|
+
const data = await api("GET", "/");
|
|
2745
|
+
if (!isRecord2(data))
|
|
2746
|
+
throw new Error("Route list was not an object");
|
|
2747
|
+
state.routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
2748
|
+
paintRoutes();
|
|
2749
|
+
};
|
|
2592
2750
|
const renderers = {
|
|
2593
2751
|
overview: renderOverview,
|
|
2594
2752
|
state: renderState,
|
|
@@ -2597,19 +2755,50 @@ function bootAdmin(config) {
|
|
|
2597
2755
|
journal: renderJournal,
|
|
2598
2756
|
routes: renderRoutes
|
|
2599
2757
|
};
|
|
2758
|
+
let refreshToken = 0;
|
|
2600
2759
|
async function refresh() {
|
|
2760
|
+
const token = ++refreshToken;
|
|
2601
2761
|
clearError();
|
|
2762
|
+
const main = el("views", HTMLElement);
|
|
2763
|
+
const sync = el("sync-status", HTMLElement);
|
|
2764
|
+
main.setAttribute("aria-busy", "true");
|
|
2765
|
+
sync.classList.add("busy");
|
|
2766
|
+
sync.textContent = "Updating";
|
|
2602
2767
|
const render = renderers[state.view];
|
|
2603
|
-
|
|
2604
|
-
|
|
2768
|
+
try {
|
|
2769
|
+
if (render)
|
|
2770
|
+
await render();
|
|
2771
|
+
if (token === refreshToken)
|
|
2772
|
+
sync.textContent = "Up to date";
|
|
2773
|
+
} finally {
|
|
2774
|
+
if (token === refreshToken) {
|
|
2775
|
+
main.removeAttribute("aria-busy");
|
|
2776
|
+
sync.classList.remove("busy");
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2605
2779
|
}
|
|
2606
2780
|
const delay = (ms) => new Promise((resolve) => {
|
|
2607
2781
|
window.setTimeout(resolve, ms);
|
|
2608
2782
|
});
|
|
2609
|
-
const confirmAction = async (button, pending, done, work) => {
|
|
2783
|
+
const confirmAction = async (button, pending, done, work, confirmLabel) => {
|
|
2610
2784
|
if (button.dataset.pending === "1")
|
|
2611
2785
|
return;
|
|
2612
2786
|
const label = button.textContent ?? "";
|
|
2787
|
+
if (confirmLabel !== void 0 && button.dataset.confirming !== "1") {
|
|
2788
|
+
button.dataset.confirming = "1";
|
|
2789
|
+
button.classList.add("confirming");
|
|
2790
|
+
button.textContent = confirmLabel;
|
|
2791
|
+
window.setTimeout(() => {
|
|
2792
|
+
if (button.dataset.confirming !== "1")
|
|
2793
|
+
return;
|
|
2794
|
+
button.textContent = label;
|
|
2795
|
+
button.classList.remove("confirming");
|
|
2796
|
+
delete button.dataset.confirming;
|
|
2797
|
+
}, 3e3);
|
|
2798
|
+
return;
|
|
2799
|
+
}
|
|
2800
|
+
button.classList.remove("confirming");
|
|
2801
|
+
delete button.dataset.confirming;
|
|
2613
2802
|
const width = button.offsetWidth;
|
|
2614
2803
|
button.dataset.pending = "1";
|
|
2615
2804
|
button.classList.add("pressed");
|
|
@@ -2630,6 +2819,65 @@ function bootAdmin(config) {
|
|
|
2630
2819
|
delete button.dataset.pending;
|
|
2631
2820
|
}
|
|
2632
2821
|
};
|
|
2822
|
+
const wireJsonEditor = (textareaId, highlightId, statusId, shellId, allowEmpty) => {
|
|
2823
|
+
const textarea = el(textareaId, HTMLTextAreaElement);
|
|
2824
|
+
const highlight = el(highlightId, HTMLElement);
|
|
2825
|
+
const status = el(statusId, HTMLElement);
|
|
2826
|
+
const shell = el(shellId, HTMLElement);
|
|
2827
|
+
const sync = () => {
|
|
2828
|
+
const text = textarea.value;
|
|
2829
|
+
highlight.innerHTML = `${highlightJsonText(text)}
|
|
2830
|
+
`;
|
|
2831
|
+
let valid = true;
|
|
2832
|
+
let message = "Valid JSON";
|
|
2833
|
+
if (allowEmpty && text.trim() === "")
|
|
2834
|
+
message = "No body";
|
|
2835
|
+
else {
|
|
2836
|
+
try {
|
|
2837
|
+
readJson2(text);
|
|
2838
|
+
} catch {
|
|
2839
|
+
valid = false;
|
|
2840
|
+
message = "Invalid JSON";
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
shell.classList.toggle("invalid", !valid);
|
|
2844
|
+
status.classList.toggle("invalid", !valid);
|
|
2845
|
+
status.textContent = message;
|
|
2846
|
+
textarea.setAttribute("aria-invalid", valid ? "false" : "true");
|
|
2847
|
+
};
|
|
2848
|
+
const syncScroll = () => {
|
|
2849
|
+
const preview2 = highlight.parentElement;
|
|
2850
|
+
if (!(preview2 instanceof HTMLElement))
|
|
2851
|
+
return;
|
|
2852
|
+
preview2.scrollTop = textarea.scrollTop;
|
|
2853
|
+
preview2.scrollLeft = textarea.scrollLeft;
|
|
2854
|
+
};
|
|
2855
|
+
textarea.addEventListener("input", sync);
|
|
2856
|
+
textarea.addEventListener("scroll", syncScroll);
|
|
2857
|
+
sync();
|
|
2858
|
+
return sync;
|
|
2859
|
+
};
|
|
2860
|
+
const syncRouteEditor = wireJsonEditor("route-body", "route-highlight", "route-json-status", "route-editor", true);
|
|
2861
|
+
const syncRecordEditor = wireJsonEditor("editor-value", "editor-highlight", "editor-json-status", "record-editor", false);
|
|
2862
|
+
for (const button of document.querySelectorAll("[data-format]")) {
|
|
2863
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2864
|
+
continue;
|
|
2865
|
+
button.addEventListener("click", () => {
|
|
2866
|
+
const target = button.dataset.format;
|
|
2867
|
+
if (target === void 0)
|
|
2868
|
+
return;
|
|
2869
|
+
const textarea = document.getElementById(target);
|
|
2870
|
+
if (!(textarea instanceof HTMLTextAreaElement) || textarea.value.trim() === "")
|
|
2871
|
+
return;
|
|
2872
|
+
try {
|
|
2873
|
+
textarea.value = JSON.stringify(readJson2(textarea.value), null, 2) ?? "";
|
|
2874
|
+
textarea.dispatchEvent(new Event("input"));
|
|
2875
|
+
textarea.focus();
|
|
2876
|
+
} catch {
|
|
2877
|
+
showError(new Error("Fix the JSON before formatting it"));
|
|
2878
|
+
}
|
|
2879
|
+
});
|
|
2880
|
+
}
|
|
2633
2881
|
document.addEventListener("click", (event) => {
|
|
2634
2882
|
const target = event.target;
|
|
2635
2883
|
if (!(target instanceof Element))
|
|
@@ -2702,6 +2950,7 @@ function bootAdmin(config) {
|
|
|
2702
2950
|
} else {
|
|
2703
2951
|
editorValue.value = "{\n \n}";
|
|
2704
2952
|
}
|
|
2953
|
+
syncRecordEditor();
|
|
2705
2954
|
editor.showModal();
|
|
2706
2955
|
}
|
|
2707
2956
|
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -2731,10 +2980,10 @@ function bootAdmin(config) {
|
|
|
2731
2980
|
const id = state.editing;
|
|
2732
2981
|
if (!id)
|
|
2733
2982
|
return;
|
|
2734
|
-
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2983
|
+
confirmAction(editorDelete, "Deleting...", "Deleted", () => api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2735
2984
|
editor.close();
|
|
2736
2985
|
return renderState();
|
|
2737
|
-
}).catch(showError);
|
|
2986
|
+
}), "Confirm delete").catch(showError);
|
|
2738
2987
|
});
|
|
2739
2988
|
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2740
2989
|
if (!(button instanceof HTMLButtonElement))
|
|
@@ -2780,10 +3029,10 @@ function bootAdmin(config) {
|
|
|
2780
3029
|
}).then(() => renderFaults()).catch(showError);
|
|
2781
3030
|
});
|
|
2782
3031
|
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2783
|
-
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
|
|
3032
|
+
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults()), "Confirm clear").catch(showError);
|
|
2784
3033
|
});
|
|
2785
3034
|
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2786
|
-
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
|
|
3035
|
+
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal()), "Confirm clear").catch(showError);
|
|
2787
3036
|
});
|
|
2788
3037
|
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2789
3038
|
const target = event.target;
|
|
@@ -2797,9 +3046,15 @@ function bootAdmin(config) {
|
|
|
2797
3046
|
return;
|
|
2798
3047
|
state.route = route;
|
|
2799
3048
|
el("route-title", HTMLElement).textContent = route;
|
|
3049
|
+
el("route-send", HTMLButtonElement).disabled = false;
|
|
2800
3050
|
const method = route.split(" ")[0] ?? "";
|
|
2801
|
-
el("route-body", HTMLTextAreaElement)
|
|
3051
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
3052
|
+
routeBody.disabled = method === "GET" || method === "DELETE";
|
|
3053
|
+
el("route-editor", HTMLElement).hidden = routeBody.disabled;
|
|
3054
|
+
el("route-json-status", HTMLElement).parentElement?.toggleAttribute("hidden", routeBody.disabled);
|
|
3055
|
+
syncRouteEditor();
|
|
2802
3056
|
});
|
|
3057
|
+
el("route-search", HTMLInputElement).addEventListener("input", paintRoutes);
|
|
2803
3058
|
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2804
3059
|
event.preventDefault();
|
|
2805
3060
|
const send = async () => {
|
|
@@ -2821,7 +3076,8 @@ function bootAdmin(config) {
|
|
|
2821
3076
|
}
|
|
2822
3077
|
}
|
|
2823
3078
|
const data = await api(method, concrete, body);
|
|
2824
|
-
|
|
3079
|
+
const text = JSON.stringify(data, null, 2) ?? "null";
|
|
3080
|
+
el("route-result", HTMLElement).innerHTML = `<code>${highlightJsonText(text)}</code>`;
|
|
2825
3081
|
};
|
|
2826
3082
|
send().catch(showError);
|
|
2827
3083
|
});
|
|
@@ -2939,11 +3195,13 @@ function bootAdmin(config) {
|
|
|
2939
3195
|
views.append(section);
|
|
2940
3196
|
mountSqlExplorer(body, panelApi);
|
|
2941
3197
|
}
|
|
3198
|
+
show(document.getElementById(`view-${state.view}`) === null ? "overview" : state.view);
|
|
2942
3199
|
return refresh();
|
|
2943
3200
|
}).catch(showError);
|
|
2944
3201
|
}
|
|
2945
3202
|
var clientPrelude = [
|
|
2946
3203
|
isRecord2,
|
|
3204
|
+
highlightJsonText,
|
|
2947
3205
|
mountAdminBrand,
|
|
2948
3206
|
startAdminBrand,
|
|
2949
3207
|
faultPresetList,
|
|
@@ -2966,6 +3224,7 @@ ${entry}(${payload});
|
|
|
2966
3224
|
var defineMock = (runtime) => runtime;
|
|
2967
3225
|
var STANDARD_ADMIN_ROUTES = [
|
|
2968
3226
|
"GET /",
|
|
3227
|
+
"GET /health",
|
|
2969
3228
|
"POST /reset",
|
|
2970
3229
|
"GET /namespaces",
|
|
2971
3230
|
"GET /clock",
|
|
@@ -3045,11 +3304,11 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
3045
3304
|
}
|
|
3046
3305
|
});
|
|
3047
3306
|
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
3048
|
-
var renderAdminDocument = (service) => {
|
|
3307
|
+
var renderAdminDocument = (service, adminPrefix = ADMIN_PREFIX) => {
|
|
3049
3308
|
const name = escapeHtml(service);
|
|
3050
3309
|
const script = adminClientSource({
|
|
3051
3310
|
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
3052
|
-
adminPrefix
|
|
3311
|
+
adminPrefix,
|
|
3053
3312
|
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
3054
3313
|
brandsUrl: ADMIN_BRANDS_URL,
|
|
3055
3314
|
service
|
|
@@ -3066,75 +3325,99 @@ var renderAdminDocument = (service) => {
|
|
|
3066
3325
|
${CSS_RESET}
|
|
3067
3326
|
:root {
|
|
3068
3327
|
color-scheme: light;
|
|
3069
|
-
--bg: #
|
|
3328
|
+
--bg: #f7f8fb;
|
|
3070
3329
|
--bg-elev: #ffffff;
|
|
3071
|
-
--bg-muted: #
|
|
3072
|
-
--bg-inset: #
|
|
3073
|
-
--line: #
|
|
3074
|
-
--line-strong: #
|
|
3075
|
-
--ink: #
|
|
3076
|
-
--muted: #
|
|
3077
|
-
--faint: #
|
|
3078
|
-
--accent: #
|
|
3330
|
+
--bg-muted: #f1f3f7;
|
|
3331
|
+
--bg-inset: #f8f9fc;
|
|
3332
|
+
--line: #e4e7ec;
|
|
3333
|
+
--line-strong: #cfd4dc;
|
|
3334
|
+
--ink: #101828;
|
|
3335
|
+
--muted: #475467;
|
|
3336
|
+
--faint: #667085;
|
|
3337
|
+
--accent: #6941c6;
|
|
3338
|
+
--accent-hover: #5933b4;
|
|
3079
3339
|
--accent-ink: #ffffff;
|
|
3080
|
-
--accent-soft: #
|
|
3081
|
-
--ok: #
|
|
3082
|
-
--ok-soft: #
|
|
3083
|
-
--warn: #
|
|
3084
|
-
--warn-soft: #
|
|
3085
|
-
--err: #
|
|
3086
|
-
--err-soft: #
|
|
3087
|
-
--
|
|
3340
|
+
--accent-soft: #f4f0ff;
|
|
3341
|
+
--ok: #067647;
|
|
3342
|
+
--ok-soft: #ecfdf3;
|
|
3343
|
+
--warn: #b54708;
|
|
3344
|
+
--warn-soft: #fffaeb;
|
|
3345
|
+
--err: #b42318;
|
|
3346
|
+
--err-soft: #fef3f2;
|
|
3347
|
+
--code-key: #6941c6;
|
|
3348
|
+
--code-string: #067647;
|
|
3349
|
+
--code-number: #175cd3;
|
|
3350
|
+
--code-boolean: #c11574;
|
|
3351
|
+
--code-null: #667085;
|
|
3352
|
+
--shadow-xs: 0 1px 2px rgb(16 24 40 / 0.05);
|
|
3353
|
+
--shadow-sm: 0 1px 3px rgb(16 24 40 / 0.08), 0 1px 2px rgb(16 24 40 / 0.04);
|
|
3354
|
+
--shadow-lg: 0 18px 48px rgb(16 24 40 / 0.16), 0 4px 12px rgb(16 24 40 / 0.08);
|
|
3088
3355
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
3089
3356
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
3090
|
-
--radius:
|
|
3091
|
-
--header:
|
|
3092
|
-
--nav:
|
|
3357
|
+
--radius: 12px;
|
|
3358
|
+
--header: 72px;
|
|
3359
|
+
--nav: 224px;
|
|
3093
3360
|
}
|
|
3094
3361
|
:root[data-theme="dark"] {
|
|
3095
3362
|
color-scheme: dark;
|
|
3096
|
-
--bg: #
|
|
3097
|
-
--bg-elev: #
|
|
3098
|
-
--bg-muted: #
|
|
3099
|
-
--bg-inset: #
|
|
3100
|
-
--line: #
|
|
3101
|
-
--line-strong: #
|
|
3102
|
-
--ink: #
|
|
3103
|
-
--muted: #
|
|
3104
|
-
--faint: #
|
|
3105
|
-
--accent: #
|
|
3106
|
-
--accent-
|
|
3107
|
-
--accent-
|
|
3108
|
-
--
|
|
3109
|
-
--ok
|
|
3110
|
-
--
|
|
3111
|
-
--warn
|
|
3112
|
-
--
|
|
3113
|
-
--err
|
|
3114
|
-
--
|
|
3363
|
+
--bg: #0c0e14;
|
|
3364
|
+
--bg-elev: #151821;
|
|
3365
|
+
--bg-muted: #20232e;
|
|
3366
|
+
--bg-inset: #11141c;
|
|
3367
|
+
--line: #2b303d;
|
|
3368
|
+
--line-strong: #414858;
|
|
3369
|
+
--ink: #f5f7fa;
|
|
3370
|
+
--muted: #b6bdc9;
|
|
3371
|
+
--faint: #8d96a7;
|
|
3372
|
+
--accent: #9e77ed;
|
|
3373
|
+
--accent-hover: #b692f6;
|
|
3374
|
+
--accent-ink: #ffffff;
|
|
3375
|
+
--accent-soft: #2b2142;
|
|
3376
|
+
--ok: #75e0a7;
|
|
3377
|
+
--ok-soft: #102a20;
|
|
3378
|
+
--warn: #fec84b;
|
|
3379
|
+
--warn-soft: #302611;
|
|
3380
|
+
--err: #fda29b;
|
|
3381
|
+
--err-soft: #321817;
|
|
3382
|
+
--code-key: #c3a6ff;
|
|
3383
|
+
--code-string: #75e0a7;
|
|
3384
|
+
--code-number: #84adff;
|
|
3385
|
+
--code-boolean: #f9a8d4;
|
|
3386
|
+
--code-null: #98a2b3;
|
|
3387
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3388
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3389
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
3115
3390
|
}
|
|
3116
3391
|
@media (prefers-color-scheme: dark) {
|
|
3117
3392
|
:root:not([data-theme="light"]) {
|
|
3118
3393
|
color-scheme: dark;
|
|
3119
|
-
--bg: #
|
|
3120
|
-
--bg-elev: #
|
|
3121
|
-
--bg-muted: #
|
|
3122
|
-
--bg-inset: #
|
|
3123
|
-
--line: #
|
|
3124
|
-
--line-strong: #
|
|
3125
|
-
--ink: #
|
|
3126
|
-
--muted: #
|
|
3127
|
-
--faint: #
|
|
3128
|
-
--accent: #
|
|
3129
|
-
--accent-
|
|
3130
|
-
--accent-
|
|
3131
|
-
--
|
|
3132
|
-
--ok
|
|
3133
|
-
--
|
|
3134
|
-
--warn
|
|
3135
|
-
--
|
|
3136
|
-
--err
|
|
3137
|
-
--
|
|
3394
|
+
--bg: #0c0e14;
|
|
3395
|
+
--bg-elev: #151821;
|
|
3396
|
+
--bg-muted: #20232e;
|
|
3397
|
+
--bg-inset: #11141c;
|
|
3398
|
+
--line: #2b303d;
|
|
3399
|
+
--line-strong: #414858;
|
|
3400
|
+
--ink: #f5f7fa;
|
|
3401
|
+
--muted: #b6bdc9;
|
|
3402
|
+
--faint: #8d96a7;
|
|
3403
|
+
--accent: #9e77ed;
|
|
3404
|
+
--accent-hover: #b692f6;
|
|
3405
|
+
--accent-ink: #ffffff;
|
|
3406
|
+
--accent-soft: #2b2142;
|
|
3407
|
+
--ok: #75e0a7;
|
|
3408
|
+
--ok-soft: #102a20;
|
|
3409
|
+
--warn: #fec84b;
|
|
3410
|
+
--warn-soft: #302611;
|
|
3411
|
+
--err: #fda29b;
|
|
3412
|
+
--err-soft: #321817;
|
|
3413
|
+
--code-key: #c3a6ff;
|
|
3414
|
+
--code-string: #75e0a7;
|
|
3415
|
+
--code-number: #84adff;
|
|
3416
|
+
--code-boolean: #f9a8d4;
|
|
3417
|
+
--code-null: #98a2b3;
|
|
3418
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3419
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3420
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
3138
3421
|
}
|
|
3139
3422
|
}
|
|
3140
3423
|
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
@@ -3152,13 +3435,17 @@ var renderAdminDocument = (service) => {
|
|
|
3152
3435
|
padding-right: env(safe-area-inset-right);
|
|
3153
3436
|
-webkit-text-size-adjust: 100%;
|
|
3154
3437
|
}
|
|
3155
|
-
|
|
3438
|
+
::selection { background: color-mix(in srgb, var(--accent) 28%, transparent); }
|
|
3439
|
+
:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px; }
|
|
3440
|
+
button, input, select, textarea { transition: border-color 140ms ease, box-shadow 140ms ease, transform 100ms ease; }
|
|
3441
|
+
button:disabled, input:disabled, select:disabled, textarea:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
3156
3442
|
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
3157
3443
|
header.top {
|
|
3158
|
-
position: sticky; top: 0; z-index:
|
|
3444
|
+
position: sticky; top: 0; z-index: 20;
|
|
3159
3445
|
display: flex; align-items: center; gap: 12px;
|
|
3160
|
-
min-height: var(--header); padding:
|
|
3161
|
-
background: var(--bg-elev);
|
|
3446
|
+
min-height: var(--header); padding: 12px 20px;
|
|
3447
|
+
background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
|
|
3448
|
+
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
3162
3449
|
border-bottom: 1px solid var(--line);
|
|
3163
3450
|
min-width: 0; max-width: 100%;
|
|
3164
3451
|
}
|
|
@@ -3166,12 +3453,21 @@ var renderAdminDocument = (service) => {
|
|
|
3166
3453
|
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
3167
3454
|
min-width: 0; max-width: 100%; margin-right: auto;
|
|
3168
3455
|
}
|
|
3169
|
-
.brand-
|
|
3170
|
-
.brand-
|
|
3456
|
+
.brand-lockup { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
3457
|
+
.brand-mark {
|
|
3458
|
+
width: 38px; height: 38px; display: grid; place-items: center; flex: none;
|
|
3459
|
+
border-radius: 11px; color: #fff;
|
|
3460
|
+
background: linear-gradient(145deg, #7f56d9 0%, #53389e 100%);
|
|
3461
|
+
box-shadow: 0 6px 16px rgb(105 65 198 / 0.24), inset 0 1px 0 rgb(255 255 255 / 0.25);
|
|
3462
|
+
}
|
|
3463
|
+
.brand-mark svg { width: 22px; height: 22px; }
|
|
3464
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; line-height: 1.25; }
|
|
3465
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
3171
3466
|
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
3172
3467
|
.vendor {
|
|
3173
3468
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
3174
3469
|
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
3470
|
+
padding: 5px 8px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-inset);
|
|
3175
3471
|
}
|
|
3176
3472
|
.vendor[hidden] { display: none; }
|
|
3177
3473
|
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
@@ -3183,12 +3479,17 @@ var renderAdminDocument = (service) => {
|
|
|
3183
3479
|
.vendor-sep { color: var(--faint); }
|
|
3184
3480
|
.vendor-link { color: inherit; text-decoration: none; }
|
|
3185
3481
|
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
3186
|
-
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items:
|
|
3187
|
-
|
|
3482
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
|
|
3483
|
+
.sync-status { align-self: center; display: flex; align-items: center; gap: 6px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3484
|
+
.sync-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
|
|
3485
|
+
.sync-status.busy::before { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); animation: pulse 1.2s ease-in-out infinite; }
|
|
3486
|
+
label.field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; font-weight: 600; color: var(--faint); }
|
|
3188
3487
|
label.field input, label.field select, .controls input[type="password"] {
|
|
3189
|
-
background-color: var(--bg-
|
|
3190
|
-
padding:
|
|
3488
|
+
background-color: var(--bg-elev); border: 1px solid var(--line-strong); border-radius: 9px;
|
|
3489
|
+
padding: 8px 10px; min-height: 38px; line-height: 1.2; box-shadow: var(--shadow-xs);
|
|
3191
3490
|
}
|
|
3491
|
+
label.field input:hover, label.field select:hover, textarea:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--line-strong)); }
|
|
3492
|
+
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; }
|
|
3192
3493
|
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
3193
3494
|
the text fields in the same form. Paint it like those fields. */
|
|
3194
3495
|
label.field select {
|
|
@@ -3205,12 +3506,20 @@ var renderAdminDocument = (service) => {
|
|
|
3205
3506
|
dialog .field > textarea { width: 100%; }
|
|
3206
3507
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
3207
3508
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
.btn
|
|
3509
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
|
3510
|
+
border-radius: 9px; min-height: 38px; padding: 0 13px; border: 1px solid var(--line-strong);
|
|
3511
|
+
background: var(--bg-elev); color: var(--ink); font-size: 13px; font-weight: 600; line-height: 1;
|
|
3512
|
+
box-shadow: var(--shadow-xs);
|
|
3513
|
+
}
|
|
3514
|
+
.icon-btn { width: 38px; padding: 0; }
|
|
3515
|
+
.icon-btn svg, .btn svg, .btn-primary svg { width: 16px; height: 16px; }
|
|
3516
|
+
.icon-btn:hover, .btn:hover { background: var(--bg-muted); border-color: var(--line-strong); }
|
|
3517
|
+
.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; }
|
|
3518
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
3519
|
+
.btn-quiet { background: transparent; border-color: transparent; box-shadow: none; }
|
|
3520
|
+
.btn-quiet:hover { background: var(--bg-muted); }
|
|
3521
|
+
.btn-danger { color: var(--err); border-color: color-mix(in srgb, var(--err) 25%, var(--line)); }
|
|
3522
|
+
.btn-danger:hover, .btn-danger.confirming { background: var(--err-soft); border-color: color-mix(in srgb, var(--err) 45%, var(--line)); }
|
|
3214
3523
|
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
3215
3524
|
so the press itself has to stay visible. */
|
|
3216
3525
|
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
@@ -3226,93 +3535,159 @@ var renderAdminDocument = (service) => {
|
|
|
3226
3535
|
flex: 1 0 auto; min-width: 0;
|
|
3227
3536
|
}
|
|
3228
3537
|
nav.side {
|
|
3229
|
-
|
|
3538
|
+
position: sticky; top: var(--header); align-self: start; height: calc(100vh - var(--header));
|
|
3539
|
+
border-right: 1px solid var(--line); padding: 18px 12px;
|
|
3230
3540
|
display: flex; flex-direction: column; gap: 4px;
|
|
3231
|
-
background: var(--bg-elev);
|
|
3541
|
+
background: var(--bg-elev); overflow-y: auto;
|
|
3232
3542
|
}
|
|
3543
|
+
.nav-label { padding: 0 10px 8px; color: var(--faint); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
|
|
3233
3544
|
nav.side button {
|
|
3545
|
+
position: relative; display: flex; align-items: center; gap: 10px; width: 100%;
|
|
3234
3546
|
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
3235
|
-
padding: 9px 10px; min-height:
|
|
3236
|
-
}
|
|
3237
|
-
nav.side button
|
|
3238
|
-
|
|
3239
|
-
.
|
|
3240
|
-
.
|
|
3241
|
-
.
|
|
3547
|
+
padding: 9px 10px; min-height: 42px; color: var(--muted); font-size: 13px; font-weight: 550;
|
|
3548
|
+
}
|
|
3549
|
+
nav.side button:hover { background: var(--bg-muted); color: var(--ink); }
|
|
3550
|
+
nav.side button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 650; }
|
|
3551
|
+
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); }
|
|
3552
|
+
.nav-icon { width: 18px; height: 18px; display: grid; place-items: center; flex: none; }
|
|
3553
|
+
.nav-icon svg { width: 18px; height: 18px; }
|
|
3554
|
+
main { padding: 32px clamp(20px, 4vw, 48px) 64px; max-width: 1320px; width: 100%; min-width: 0; }
|
|
3555
|
+
main[aria-busy="true"] .view { opacity: 0.62; }
|
|
3556
|
+
.view { animation: view-in 180ms ease-out; }
|
|
3557
|
+
.view[hidden] { display: none; }
|
|
3558
|
+
.view-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
|
|
3559
|
+
.view h2 { margin: 0 0 5px; font-size: clamp(24px, 3vw, 30px); line-height: 1.2; letter-spacing: -0.035em; }
|
|
3560
|
+
.view h3 { margin: 24px 0 10px; font-size: 15px; letter-spacing: -0.01em; }
|
|
3561
|
+
.lede { margin: 0; color: var(--muted); max-width: 72ch; font-size: 14px; }
|
|
3562
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 175px), 1fr)); gap: 14px; }
|
|
3242
3563
|
.card {
|
|
3243
3564
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
3244
|
-
padding:
|
|
3565
|
+
padding: 18px; box-shadow: var(--shadow-sm);
|
|
3245
3566
|
}
|
|
3246
|
-
.card
|
|
3247
|
-
.card
|
|
3567
|
+
.stat-card { position: relative; overflow: hidden; }
|
|
3568
|
+
.stat-card::after { content: ""; position: absolute; inset: 0 0 auto; height: 2px; background: linear-gradient(90deg, var(--accent), transparent 78%); opacity: 0.7; }
|
|
3569
|
+
.card .k { color: var(--faint); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
3570
|
+
.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; }
|
|
3248
3571
|
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
3249
|
-
.
|
|
3572
|
+
.section-card { background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-xs); overflow: hidden; }
|
|
3573
|
+
.section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
|
|
3574
|
+
.section-title h3 { margin: 0; }
|
|
3575
|
+
.split { display: grid; grid-template-columns: minmax(0, 250px) minmax(0, 1fr); gap: 18px; align-items: start; }
|
|
3576
|
+
#collections { padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); box-shadow: var(--shadow-xs); }
|
|
3250
3577
|
.collection {
|
|
3251
3578
|
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
3252
|
-
border-radius:
|
|
3579
|
+
border-radius: 9px; padding: 10px 11px; min-height: 48px; color: var(--ink); cursor: pointer;
|
|
3253
3580
|
}
|
|
3254
|
-
.collection
|
|
3581
|
+
.collection:hover { background: var(--bg-muted); }
|
|
3582
|
+
.collection[aria-current="true"] { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 18%, var(--line)); color: var(--accent); }
|
|
3255
3583
|
.collection small { display: block; color: var(--faint); }
|
|
3256
3584
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
3257
|
-
.chip {
|
|
3258
|
-
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
3259
|
-
border-radius: 999px; padding: 4px 8px;
|
|
3260
|
-
}
|
|
3585
|
+
.chip { font-family: var(--mono); font-size: 11px; background: var(--bg-muted); border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; }
|
|
3261
3586
|
.chip i { color: var(--faint); font-style: normal; }
|
|
3262
3587
|
.sql-layout {
|
|
3263
3588
|
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
3264
3589
|
}
|
|
3265
|
-
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
3266
|
-
.sql-tables button { text-align: left; }
|
|
3590
|
+
.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); }
|
|
3591
|
+
.sql-tables button { text-align: left; border: 0; border-radius: 8px; padding: 9px 10px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
3592
|
+
.sql-tables button:hover, .sql-tables button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); }
|
|
3267
3593
|
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
3268
|
-
table { width: 100%; border-
|
|
3269
|
-
|
|
3270
|
-
th {
|
|
3594
|
+
.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); }
|
|
3595
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
3596
|
+
th, td { text-align: left; padding: 11px 13px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
3597
|
+
th { color: var(--faint); background: var(--bg-inset); font-weight: 650; font-size: 11px; text-transform: uppercase; letter-spacing: 0.045em; }
|
|
3598
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
3271
3599
|
tr.clickable { cursor: pointer; }
|
|
3272
|
-
tr.clickable:hover { background: var(--
|
|
3600
|
+
tr.clickable:hover { background: var(--accent-soft); }
|
|
3273
3601
|
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
3274
3602
|
pre {
|
|
3275
3603
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
3276
|
-
background: var(--bg-inset); border-radius: 10px; padding:
|
|
3277
|
-
}
|
|
3278
|
-
.
|
|
3604
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 10px; padding: 14px; font-size: 12px; line-height: 1.65;
|
|
3605
|
+
}
|
|
3606
|
+
.json-key { color: var(--code-key); }
|
|
3607
|
+
.json-string { color: var(--code-string); }
|
|
3608
|
+
.json-number { color: var(--code-number); }
|
|
3609
|
+
.json-boolean { color: var(--code-boolean); }
|
|
3610
|
+
.json-null { color: var(--code-null); font-style: italic; }
|
|
3611
|
+
.json-inline { display: block; max-width: 44rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
3612
|
+
.json-view { position: relative; max-height: 26rem; overflow: auto; }
|
|
3613
|
+
.json-editor { position: relative; min-height: 190px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--bg-inset); overflow: hidden; }
|
|
3614
|
+
.json-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); }
|
|
3615
|
+
.json-editor.invalid { border-color: var(--err); }
|
|
3616
|
+
.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; }
|
|
3617
|
+
.json-editor pre { position: absolute; inset: 0; pointer-events: none; background: transparent; }
|
|
3618
|
+
.json-editor textarea { position: relative; z-index: 1; resize: vertical; background: transparent; color: transparent; caret-color: var(--ink); -webkit-text-fill-color: transparent; }
|
|
3619
|
+
.json-editor textarea::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); }
|
|
3620
|
+
.editor-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; }
|
|
3621
|
+
.json-status { display: inline-flex; align-items: center; gap: 6px; color: var(--ok); font-size: 11px; }
|
|
3622
|
+
.json-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
3623
|
+
.json-status.invalid { color: var(--err); }
|
|
3624
|
+
.format-json { border: 0; padding: 3px 5px; background: transparent; color: var(--accent); font-size: 11px; font-weight: 650; cursor: pointer; }
|
|
3625
|
+
.row-actions { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 14px 0; }
|
|
3279
3626
|
.banner {
|
|
3280
|
-
display: none;
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3627
|
+
display: none; position: sticky; top: calc(var(--header) + 12px); z-index: 15;
|
|
3628
|
+
align-items: center; gap: 10px; margin: 0 0 16px; padding: 11px 12px 11px 14px;
|
|
3629
|
+
border: 1px solid color-mix(in srgb, var(--err) 30%, var(--line)); border-radius: 10px;
|
|
3630
|
+
background: var(--err-soft); color: var(--err); box-shadow: var(--shadow-sm); font-size: 13px;
|
|
3631
|
+
}
|
|
3632
|
+
.banner.show { display: flex; }
|
|
3633
|
+
.banner span { flex: 1; }
|
|
3634
|
+
.banner button { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 7px; background: transparent; color: currentColor; cursor: pointer; }
|
|
3635
|
+
.banner button:hover { background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
3636
|
+
.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); }
|
|
3285
3637
|
dialog {
|
|
3286
|
-
border: 1px solid var(--line); border-radius:
|
|
3287
|
-
color: var(--ink); width: min(
|
|
3288
|
-
}
|
|
3289
|
-
dialog
|
|
3290
|
-
dialog
|
|
3638
|
+
border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--bg-elev);
|
|
3639
|
+
color: var(--ink); width: min(720px, calc(100% - 24px)); max-width: calc(100vw - 24px); box-shadow: var(--shadow-lg);
|
|
3640
|
+
}
|
|
3641
|
+
dialog[open] { animation: dialog-in 160ms ease-out; }
|
|
3642
|
+
dialog::backdrop { background: rgb(12 14 20 / 0.58); backdrop-filter: blur(3px); }
|
|
3643
|
+
dialog form, .dialog-body { padding: 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
3644
|
+
.dialog-heading { display: flex; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
|
3645
|
+
.dialog-heading strong { font-size: 18px; letter-spacing: -0.02em; }
|
|
3646
|
+
.dialog-heading span { color: var(--muted); font-size: 12px; }
|
|
3291
3647
|
textarea {
|
|
3292
3648
|
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
3293
3649
|
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
3294
3650
|
font-family: var(--mono); font-size: 12px;
|
|
3295
3651
|
}
|
|
3296
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
3297
|
-
.
|
|
3652
|
+
.dialog-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding-top: 4px; }
|
|
3653
|
+
.dialog-actions .btn-danger { margin-right: auto; }
|
|
3654
|
+
.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); }
|
|
3298
3655
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
3299
3656
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
3300
3657
|
.status-2 { color: var(--ok); }
|
|
3301
3658
|
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
3302
3659
|
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
3660
|
+
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
3661
|
+
.form-grid .wide { grid-column: span 2; }
|
|
3303
3662
|
.route {
|
|
3304
|
-
display: grid; grid-template-columns:
|
|
3305
|
-
padding:
|
|
3306
|
-
}
|
|
3307
|
-
.
|
|
3663
|
+
display: grid; grid-template-columns: 84px minmax(0, 1fr) auto; gap: 10px; align-items: center;
|
|
3664
|
+
padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--bg-elev);
|
|
3665
|
+
}
|
|
3666
|
+
.route:first-child { border-radius: var(--radius) var(--radius) 0 0; }
|
|
3667
|
+
.route:last-child { border-bottom: 0; border-radius: 0 0 var(--radius) var(--radius); }
|
|
3668
|
+
.route-tools { display: flex; align-items: flex-end; gap: 12px; margin-bottom: 12px; }
|
|
3669
|
+
.route-tools .field { flex: 1; }
|
|
3670
|
+
.route-tools input { width: 100%; }
|
|
3671
|
+
.route-count { padding-bottom: 10px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3672
|
+
#route-list { max-height: 34rem; border: 1px solid var(--line); border-radius: var(--radius); overflow: auto; box-shadow: var(--shadow-xs); }
|
|
3673
|
+
.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; }
|
|
3674
|
+
@keyframes view-in { from { transform: translateY(3px); } to { transform: translateY(0); } }
|
|
3675
|
+
@keyframes dialog-in { from { transform: translateY(8px) scale(0.99); } to { transform: translateY(0) scale(1); } }
|
|
3676
|
+
@keyframes pulse { 50% { opacity: 0.45; } }
|
|
3677
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
|
|
3678
|
+
@media (forced-colors: active) { .json-editor pre { display: none; } .json-editor textarea { color: CanvasText; -webkit-text-fill-color: CanvasText; } }
|
|
3308
3679
|
@media (max-width: 800px) {
|
|
3680
|
+
:root { --header: 0px; }
|
|
3309
3681
|
header.top {
|
|
3310
|
-
align-items: stretch; flex-direction: column;
|
|
3311
|
-
padding
|
|
3682
|
+
position: relative; align-items: stretch; flex-direction: column;
|
|
3683
|
+
padding: max(12px, env(safe-area-inset-top)) 14px 12px;
|
|
3312
3684
|
}
|
|
3313
3685
|
input, select, textarea { font-size: 16px; }
|
|
3314
3686
|
.brand, .vendor, .controls { width: 100%; }
|
|
3687
|
+
.brand { gap: 10px; }
|
|
3688
|
+
.vendor { order: 2; border-radius: 9px; }
|
|
3315
3689
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
3690
|
+
.sync-status { display: none; }
|
|
3316
3691
|
/* The layout fills the screen. A single-column grid stretches every auto
|
|
3317
3692
|
row, so the tab bar grew with the leftover height and the buttons floated
|
|
3318
3693
|
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
@@ -3322,14 +3697,23 @@ var renderAdminDocument = (service) => {
|
|
|
3322
3697
|
align-content: start;
|
|
3323
3698
|
}
|
|
3324
3699
|
nav.side {
|
|
3325
|
-
align-self: start; height: auto;
|
|
3700
|
+
position: sticky; top: 0; z-index: 12; align-self: start; height: auto;
|
|
3326
3701
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
3327
3702
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
3328
|
-
padding: 8px;
|
|
3703
|
+
padding: 8px; box-shadow: var(--shadow-xs);
|
|
3329
3704
|
}
|
|
3330
|
-
nav
|
|
3705
|
+
.nav-label { display: none; }
|
|
3706
|
+
nav.side button { width: auto; white-space: nowrap; flex: none; }
|
|
3707
|
+
nav.side button[aria-current="true"]::before { inset: auto 9px -8px; width: auto; height: 3px; border-radius: 3px 3px 0 0; }
|
|
3331
3708
|
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
3332
|
-
|
|
3709
|
+
.form-grid { grid-template-columns: minmax(0, 1fr); }
|
|
3710
|
+
.form-grid .wide { grid-column: auto; }
|
|
3711
|
+
main { padding: 22px 14px 48px; }
|
|
3712
|
+
.view-head { margin-bottom: 18px; }
|
|
3713
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
3714
|
+
.card { padding: 14px; }
|
|
3715
|
+
.card .v { font-size: 20px; }
|
|
3716
|
+
.table-shell { border: 0; background: transparent; box-shadow: none; overflow: visible; }
|
|
3333
3717
|
table, thead, tbody, tr, th, td { display: block; }
|
|
3334
3718
|
thead { display: none; }
|
|
3335
3719
|
tr {
|
|
@@ -3344,50 +3728,63 @@ var renderAdminDocument = (service) => {
|
|
|
3344
3728
|
.route { grid-template-columns: 72px 1fr; }
|
|
3345
3729
|
.route button { grid-column: 1 / -1; }
|
|
3346
3730
|
}
|
|
3731
|
+
@media (max-width: 460px) {
|
|
3732
|
+
.controls label.field { min-width: calc(50% - 4px); }
|
|
3733
|
+
.controls .icon-btn, .controls .btn { flex: 1; }
|
|
3734
|
+
.grid { grid-template-columns: minmax(0, 1fr); }
|
|
3735
|
+
.dialog-actions { flex-wrap: wrap; }
|
|
3736
|
+
.dialog-actions button { flex: 1; }
|
|
3737
|
+
.dialog-actions .btn-danger { flex-basis: 100%; margin-right: 0; }
|
|
3738
|
+
}
|
|
3347
3739
|
</style>
|
|
3348
3740
|
</head>
|
|
3349
3741
|
<body>
|
|
3350
3742
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
3351
3743
|
<header class="top">
|
|
3352
3744
|
<div class="brand">
|
|
3353
|
-
<div class="brand-
|
|
3354
|
-
<
|
|
3355
|
-
|
|
3745
|
+
<div class="brand-lockup">
|
|
3746
|
+
<div class="brand-mark" aria-hidden="true">
|
|
3747
|
+
<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>
|
|
3748
|
+
</div>
|
|
3749
|
+
<div class="brand-id">
|
|
3750
|
+
<strong>${name}</strong>
|
|
3751
|
+
<span>Mock admin</span>
|
|
3752
|
+
</div>
|
|
3356
3753
|
</div>
|
|
3357
3754
|
<div class="vendor" id="vendor" hidden></div>
|
|
3358
3755
|
</div>
|
|
3359
3756
|
<div class="controls">
|
|
3757
|
+
<span class="sync-status" id="sync-status" role="status" aria-live="polite">Ready</span>
|
|
3360
3758
|
<label class="field">Namespace
|
|
3361
3759
|
<select id="namespace" aria-label="Namespace"></select>
|
|
3362
3760
|
</label>
|
|
3363
3761
|
<label class="field">Admin key
|
|
3364
3762
|
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
3365
3763
|
</label>
|
|
3366
|
-
<button class="icon-btn" id="theme" type="button" aria-label="
|
|
3367
|
-
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
3764
|
+
<button class="icon-btn" id="theme" type="button" aria-label="Use dark theme" title="Use dark theme"></button>
|
|
3765
|
+
<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>
|
|
3368
3766
|
</div>
|
|
3369
3767
|
</header>
|
|
3370
3768
|
<div class="layout">
|
|
3371
3769
|
<nav class="side" id="nav" aria-label="Admin sections">
|
|
3372
|
-
<
|
|
3373
|
-
<button type="button" data-view="
|
|
3374
|
-
<button type="button" data-view="
|
|
3375
|
-
<button type="button" data-view="
|
|
3376
|
-
<button type="button" data-view="
|
|
3377
|
-
<button type="button" data-view="
|
|
3770
|
+
<span class="nav-label">Workspace</span>
|
|
3771
|
+
<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>
|
|
3772
|
+
<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>
|
|
3773
|
+
<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>
|
|
3774
|
+
<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>
|
|
3775
|
+
<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>
|
|
3776
|
+
<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>
|
|
3378
3777
|
</nav>
|
|
3379
3778
|
<main id="views">
|
|
3380
|
-
<
|
|
3779
|
+
<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>
|
|
3381
3780
|
<section class="view" id="view-overview">
|
|
3382
|
-
<h2>Overview</h2>
|
|
3383
|
-
<p class="lede">Health, clock, and the collections this namespace is holding.</p>
|
|
3781
|
+
<div class="view-head"><div><h2>Overview</h2><p class="lede">Health, clock, and the collections this namespace is holding.</p></div></div>
|
|
3384
3782
|
<div class="grid" id="overview-cards"></div>
|
|
3385
3783
|
<h3>Recent requests</h3>
|
|
3386
3784
|
<div id="overview-requests"></div>
|
|
3387
3785
|
</section>
|
|
3388
3786
|
<section class="view" id="view-state" hidden>
|
|
3389
|
-
<h2>State</h2>
|
|
3390
|
-
<p class="lede">Every collection in this namespace. Declared fields stay visible before any row exists; stored rows add whatever else they contain.</p>
|
|
3787
|
+
<div class="view-head"><div><h2>State</h2><p class="lede">Inspect and edit every collection in this namespace.</p></div></div>
|
|
3391
3788
|
<div class="split">
|
|
3392
3789
|
<div id="collections"></div>
|
|
3393
3790
|
<div>
|
|
@@ -3400,8 +3797,7 @@ var renderAdminDocument = (service) => {
|
|
|
3400
3797
|
</div>
|
|
3401
3798
|
</section>
|
|
3402
3799
|
<section class="view" id="view-clock" hidden>
|
|
3403
|
-
<h2>Clock</h2>
|
|
3404
|
-
<p class="lede">The clock every timestamp in this mock reads.</p>
|
|
3800
|
+
<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>
|
|
3405
3801
|
<div class="card" id="clock-readout"></div>
|
|
3406
3802
|
<div class="row-actions">
|
|
3407
3803
|
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
@@ -3420,21 +3816,22 @@ var renderAdminDocument = (service) => {
|
|
|
3420
3816
|
</form>
|
|
3421
3817
|
</section>
|
|
3422
3818
|
<section class="view" id="view-faults" hidden>
|
|
3423
|
-
<h2>Faults</h2>
|
|
3424
|
-
<p class="lede">Inject a response, a delay, or a named preset. Rules apply to this namespace.</p>
|
|
3819
|
+
<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>
|
|
3425
3820
|
<form id="fault-form" class="card stack">
|
|
3426
|
-
<
|
|
3427
|
-
<
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
<
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
<
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
<
|
|
3437
|
-
|
|
3821
|
+
<div class="form-grid">
|
|
3822
|
+
<label class="field wide">Preset
|
|
3823
|
+
<select id="fault-preset"><option value="">Custom rule</option></select>
|
|
3824
|
+
</label>
|
|
3825
|
+
<label class="field">Status
|
|
3826
|
+
<input id="fault-status" inputmode="numeric" placeholder="503">
|
|
3827
|
+
</label>
|
|
3828
|
+
<label class="field wide">Path prefix
|
|
3829
|
+
<input id="fault-path" placeholder="/v1">
|
|
3830
|
+
</label>
|
|
3831
|
+
<label class="field">Method
|
|
3832
|
+
<input id="fault-method" placeholder="GET">
|
|
3833
|
+
</label>
|
|
3834
|
+
</div>
|
|
3438
3835
|
<div class="dialog-actions">
|
|
3439
3836
|
<button class="btn-primary" type="submit">Add fault</button>
|
|
3440
3837
|
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
@@ -3443,26 +3840,31 @@ var renderAdminDocument = (service) => {
|
|
|
3443
3840
|
<div id="fault-list"></div>
|
|
3444
3841
|
</section>
|
|
3445
3842
|
<section class="view" id="view-journal" hidden>
|
|
3446
|
-
<h2>Journal</h2>
|
|
3447
|
-
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
3448
|
-
<div class="row-actions">
|
|
3449
|
-
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
3450
|
-
</div>
|
|
3843
|
+
<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>
|
|
3451
3844
|
<div id="journal"></div>
|
|
3452
3845
|
</section>
|
|
3453
3846
|
<section class="view" id="view-routes" hidden>
|
|
3454
|
-
<h2>Routes</h2>
|
|
3455
|
-
<
|
|
3847
|
+
<div class="view-head"><div><h2>Routes</h2><p class="lede">Explore shared controls and service-specific admin endpoints.</p></div></div>
|
|
3848
|
+
<div class="route-tools">
|
|
3849
|
+
<label class="field">Filter routes
|
|
3850
|
+
<input id="route-search" type="search" placeholder="Method, path, or extension" autocomplete="off">
|
|
3851
|
+
</label>
|
|
3852
|
+
<span class="route-count" id="route-count"></span>
|
|
3853
|
+
</div>
|
|
3456
3854
|
<div id="route-list"></div>
|
|
3457
|
-
<form id="route-form" class="card stack">
|
|
3855
|
+
<form id="route-form" class="card stack" style="margin-top: 18px">
|
|
3458
3856
|
<strong id="route-title">Select a route</strong>
|
|
3459
3857
|
<label class="field">JSON body
|
|
3460
|
-
<
|
|
3858
|
+
<div class="json-editor" id="route-editor" hidden>
|
|
3859
|
+
<pre aria-hidden="true"><code id="route-highlight"></code></pre>
|
|
3860
|
+
<textarea id="route-body" placeholder="{ }" spellcheck="false" aria-describedby="route-json-status" disabled></textarea>
|
|
3861
|
+
</div>
|
|
3862
|
+
<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>
|
|
3461
3863
|
</label>
|
|
3462
3864
|
<div class="dialog-actions">
|
|
3463
|
-
<button class="btn-primary" type="submit">Send</button>
|
|
3865
|
+
<button class="btn-primary" id="route-send" type="submit" disabled>Send request</button>
|
|
3464
3866
|
</div>
|
|
3465
|
-
<pre id="route-result">Response will show here.</pre>
|
|
3867
|
+
<pre class="json-view" id="route-result"><code>Response will show here.</code></pre>
|
|
3466
3868
|
</form>
|
|
3467
3869
|
</section>
|
|
3468
3870
|
</main>
|
|
@@ -3470,12 +3872,16 @@ var renderAdminDocument = (service) => {
|
|
|
3470
3872
|
</div>
|
|
3471
3873
|
<dialog id="editor">
|
|
3472
3874
|
<form method="dialog" id="editor-form">
|
|
3473
|
-
<strong id="editor-title">Record</strong>
|
|
3875
|
+
<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>
|
|
3474
3876
|
<label class="field">Id
|
|
3475
3877
|
<input id="editor-id" required>
|
|
3476
3878
|
</label>
|
|
3477
3879
|
<label class="field">JSON value
|
|
3478
|
-
<
|
|
3880
|
+
<div class="json-editor" id="record-editor">
|
|
3881
|
+
<pre aria-hidden="true"><code id="editor-highlight"></code></pre>
|
|
3882
|
+
<textarea id="editor-value" spellcheck="false" aria-describedby="editor-json-status"></textarea>
|
|
3883
|
+
</div>
|
|
3884
|
+
<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>
|
|
3479
3885
|
</label>
|
|
3480
3886
|
<div class="dialog-actions">
|
|
3481
3887
|
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
@@ -3517,8 +3923,8 @@ var describeExtension = (extension) => {
|
|
|
3517
3923
|
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
3518
3924
|
};
|
|
3519
3925
|
};
|
|
3520
|
-
var adminUiRoutes = (service, ui) => {
|
|
3521
|
-
const shell = () => renderAdminDocument(service);
|
|
3926
|
+
var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
3927
|
+
const shell = () => renderAdminDocument(service, adminPrefix);
|
|
3522
3928
|
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
3523
3929
|
return {
|
|
3524
3930
|
"GET /ui": () => htmlResponse(document3()),
|
|
@@ -4209,7 +4615,7 @@ var stateAdminRoutes = (routes) => {
|
|
|
4209
4615
|
};
|
|
4210
4616
|
|
|
4211
4617
|
// ../core/dist/version.js
|
|
4212
|
-
var PACKAGE_VERSION = true ? "
|
|
4618
|
+
var PACKAGE_VERSION = true ? "2.0.0" : UNRELEASED_VERSION;
|
|
4213
4619
|
|
|
4214
4620
|
// ../core/dist/signing.js
|
|
4215
4621
|
var encoder = new TextEncoder();
|
|
@@ -4302,7 +4708,9 @@ var createWebhookHub = (options) => {
|
|
|
4302
4708
|
const faults = /* @__PURE__ */ new Map();
|
|
4303
4709
|
const held = /* @__PURE__ */ new Map();
|
|
4304
4710
|
const inFlight2 = /* @__PURE__ */ new Set();
|
|
4711
|
+
const controllers = /* @__PURE__ */ new Map();
|
|
4305
4712
|
let immediate = Promise.resolve();
|
|
4713
|
+
const snapshots = /* @__PURE__ */ new WeakMap();
|
|
4306
4714
|
const track = (work) => {
|
|
4307
4715
|
inFlight2.add(work);
|
|
4308
4716
|
void work.finally(() => inFlight2.delete(work));
|
|
@@ -4323,6 +4731,7 @@ var createWebhookHub = (options) => {
|
|
|
4323
4731
|
responseBody: null
|
|
4324
4732
|
};
|
|
4325
4733
|
const controller = new AbortController();
|
|
4734
|
+
controllers.set(delivery.id, controller);
|
|
4326
4735
|
const timer = scheduleTimer(() => controller.abort(), timeoutMs);
|
|
4327
4736
|
try {
|
|
4328
4737
|
const signed = await options.signer({
|
|
@@ -4352,6 +4761,7 @@ var createWebhookHub = (options) => {
|
|
|
4352
4761
|
} catch (error2) {
|
|
4353
4762
|
record.error = controller.signal.aborted ? `timed out after ${timeoutMs}ms` : error2 instanceof Error ? error2.message : String(error2);
|
|
4354
4763
|
} finally {
|
|
4764
|
+
controllers.delete(delivery.id);
|
|
4355
4765
|
cancel(timer);
|
|
4356
4766
|
record.durationMs = now() - started;
|
|
4357
4767
|
delivery.attempts.push(record);
|
|
@@ -4369,6 +4779,8 @@ var createWebhookHub = (options) => {
|
|
|
4369
4779
|
if (!payloads.has(delivery.id))
|
|
4370
4780
|
return;
|
|
4371
4781
|
await attempt(delivery).then((ok) => {
|
|
4782
|
+
if (!payloads.has(delivery.id))
|
|
4783
|
+
return;
|
|
4372
4784
|
if (ok)
|
|
4373
4785
|
delivery.state = "delivered";
|
|
4374
4786
|
else
|
|
@@ -4430,6 +4842,37 @@ var createWebhookHub = (options) => {
|
|
|
4430
4842
|
fanOut(message);
|
|
4431
4843
|
};
|
|
4432
4844
|
const hub = {
|
|
4845
|
+
pending(namespace) {
|
|
4846
|
+
return inFlight2.size + (held.get(namespace)?.length ?? 0) + [...deliveries.values()].filter((d) => d.namespace === namespace && d.state === "pending").length;
|
|
4847
|
+
},
|
|
4848
|
+
snapshot(namespace) {
|
|
4849
|
+
if (hub.pending(namespace) > 0)
|
|
4850
|
+
throw new Error("webhooks are not quiescent");
|
|
4851
|
+
const handle = {};
|
|
4852
|
+
snapshots.set(handle, structuredClone({
|
|
4853
|
+
namespace,
|
|
4854
|
+
endpoints: own.get(namespace) ?? [],
|
|
4855
|
+
messages: messages.filter((m) => m.namespace === namespace),
|
|
4856
|
+
deliveries: [...deliveries].filter(([, d]) => d.namespace === namespace),
|
|
4857
|
+
payloads: [...payloads].filter(([, p]) => p.message.namespace === namespace),
|
|
4858
|
+
faults: faults.get(namespace) ?? []
|
|
4859
|
+
}));
|
|
4860
|
+
return handle;
|
|
4861
|
+
},
|
|
4862
|
+
restore(namespace, handle) {
|
|
4863
|
+
const point = typeof handle === "object" && handle !== null ? snapshots.get(handle) : void 0;
|
|
4864
|
+
if (!point || point.namespace !== namespace || hub.pending(namespace) > 0)
|
|
4865
|
+
throw new Error("invalid webhook restore");
|
|
4866
|
+
hub.clear(namespace);
|
|
4867
|
+
const copied = structuredClone(point);
|
|
4868
|
+
own.set(namespace, copied.endpoints);
|
|
4869
|
+
messages.push(...copied.messages);
|
|
4870
|
+
for (const [id2, delivery] of copied.deliveries)
|
|
4871
|
+
deliveries.set(id2, delivery);
|
|
4872
|
+
for (const [id2, payload] of copied.payloads)
|
|
4873
|
+
payloads.set(id2, payload);
|
|
4874
|
+
faults.set(namespace, copied.faults);
|
|
4875
|
+
},
|
|
4433
4876
|
publish(input) {
|
|
4434
4877
|
const contentType = input.contentType ?? (input.form ? "application/x-www-form-urlencoded" : "application/json");
|
|
4435
4878
|
const body = typeof input.body === "string" ? input.body : input.form ? new URLSearchParams(input.form).toString() : JSON.stringify(input.body);
|
|
@@ -4520,6 +4963,7 @@ var createWebhookHub = (options) => {
|
|
|
4520
4963
|
for (const [id2, delivery] of deliveries) {
|
|
4521
4964
|
if (namespace !== void 0 && delivery.namespace !== namespace)
|
|
4522
4965
|
continue;
|
|
4966
|
+
controllers.get(id2)?.abort();
|
|
4523
4967
|
const timer = pending.get(id2);
|
|
4524
4968
|
if (timer !== void 0)
|
|
4525
4969
|
cancel(timer);
|
|
@@ -4544,7 +4988,10 @@ var createWebhookHub = (options) => {
|
|
|
4544
4988
|
};
|
|
4545
4989
|
return hub;
|
|
4546
4990
|
};
|
|
4547
|
-
var json5 = (status, body) => new Response(JSON.stringify(body), {
|
|
4991
|
+
var json5 = (status, body) => new Response(JSON.stringify(body), {
|
|
4992
|
+
status,
|
|
4993
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4994
|
+
});
|
|
4548
4995
|
var adminError3 = (status, message) => json5(status, { error: { type: "mockingbird_admin", message } });
|
|
4549
4996
|
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4550
4997
|
var parseEndpoint = (value) => {
|
|
@@ -4652,7 +5099,6 @@ var AT_HEADER = "x-mockingbird-at";
|
|
|
4652
5099
|
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
4653
5100
|
var DEFAULT_NAMESPACE = "default";
|
|
4654
5101
|
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4655
|
-
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
4656
5102
|
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4657
5103
|
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
4658
5104
|
var effects = /* @__PURE__ */ new WeakMap();
|
|
@@ -4693,6 +5139,22 @@ var operationMatcher = (document3) => {
|
|
|
4693
5139
|
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
4694
5140
|
};
|
|
4695
5141
|
var createRuntime = (options) => {
|
|
5142
|
+
const adminPrefix = resolveAdminPrefix(options.adminPrefix);
|
|
5143
|
+
const internalPaths = Object.entries(options.document?.paths ?? {}).filter(([, item]) => item["x-mockingbird-internal"] === true).map(([path]) => path);
|
|
5144
|
+
for (const path of internalPaths) {
|
|
5145
|
+
if (!path.startsWith(`${ADMIN_PREFIX}/blobs/`))
|
|
5146
|
+
throw new Error(`internal transport ${path} must live under ${ADMIN_PREFIX}/blobs/`);
|
|
5147
|
+
}
|
|
5148
|
+
assertAdminPrefixAvailable(adminPrefix, Object.keys(options.document?.paths ?? {}).filter((path) => !internalPaths.includes(path)));
|
|
5149
|
+
const internalMatch = (path) => {
|
|
5150
|
+
for (const pattern of internalPaths) {
|
|
5151
|
+
const actual = path.split("/");
|
|
5152
|
+
const expected = pattern.split("/");
|
|
5153
|
+
if (actual.length === expected.length && expected.every((part, i) => part.startsWith("{") || part === actual[i]))
|
|
5154
|
+
return pattern;
|
|
5155
|
+
}
|
|
5156
|
+
return void 0;
|
|
5157
|
+
};
|
|
4696
5158
|
assertAdminUi(options.adminUi);
|
|
4697
5159
|
const sqlite = bootSqlite(options.sqlite);
|
|
4698
5160
|
const clock = options.clock ?? createClock();
|
|
@@ -4707,6 +5169,20 @@ var createRuntime = (options) => {
|
|
|
4707
5169
|
const instances = /* @__PURE__ */ new Map();
|
|
4708
5170
|
const publicNamespaces = /* @__PURE__ */ new Set();
|
|
4709
5171
|
const branchRngs = /* @__PURE__ */ new Map();
|
|
5172
|
+
const namespaceClocks = /* @__PURE__ */ new Map();
|
|
5173
|
+
const namespaceForStorage = /* @__PURE__ */ new Map();
|
|
5174
|
+
let isolatedNamespaces = false;
|
|
5175
|
+
const namespaceClock = (name = DEFAULT_NAMESPACE) => {
|
|
5176
|
+
if (!isolatedNamespaces || name === DEFAULT_NAMESPACE)
|
|
5177
|
+
return clock;
|
|
5178
|
+
let found = namespaceClocks.get(name);
|
|
5179
|
+
if (!found) {
|
|
5180
|
+
found = createClock();
|
|
5181
|
+
namespaceClocks.set(name, found);
|
|
5182
|
+
}
|
|
5183
|
+
return found;
|
|
5184
|
+
};
|
|
5185
|
+
const fleetPoints = /* @__PURE__ */ new WeakMap();
|
|
4710
5186
|
const timelines = /* @__PURE__ */ new Map();
|
|
4711
5187
|
const branchStorage = /* @__PURE__ */ new Map();
|
|
4712
5188
|
const captured = /* @__PURE__ */ new Map();
|
|
@@ -4720,17 +5196,22 @@ var createRuntime = (options) => {
|
|
|
4720
5196
|
if (!NAMESPACE_PATTERN.test(key) || !NAMESPACE_PATTERN.test(publicNamespace)) {
|
|
4721
5197
|
throw new RangeError(`namespace must match ${NAMESPACE_PATTERN}: ${JSON.stringify(publicNamespace)}`);
|
|
4722
5198
|
}
|
|
5199
|
+
const createdContextRng = isolatedRng ?? (isolatedNamespaces && key !== DEFAULT_NAMESPACE ? createRng(options.seed ?? 0) : rng);
|
|
4723
5200
|
const created = options.create({
|
|
4724
5201
|
namespace: storageNamespace(key),
|
|
4725
5202
|
publicNamespace,
|
|
5203
|
+
adminPrefix,
|
|
4726
5204
|
sqlite,
|
|
4727
|
-
clock,
|
|
4728
|
-
rng:
|
|
5205
|
+
clock: namespaceClock(publicNamespace),
|
|
5206
|
+
rng: createdContextRng
|
|
4729
5207
|
});
|
|
4730
5208
|
instances.set(key, created);
|
|
5209
|
+
namespaceForStorage.set(key, publicNamespace);
|
|
4731
5210
|
publicNamespaces.add(publicNamespace);
|
|
4732
5211
|
if (isolatedRng)
|
|
4733
5212
|
branchRngs.set(key, isolatedRng);
|
|
5213
|
+
else if (isolatedNamespaces && key !== DEFAULT_NAMESPACE)
|
|
5214
|
+
branchRngs.set(key, createdContextRng);
|
|
4734
5215
|
return created;
|
|
4735
5216
|
};
|
|
4736
5217
|
const instance = (name = DEFAULT_NAMESPACE) => instanceFor(name);
|
|
@@ -4748,7 +5229,7 @@ var createRuntime = (options) => {
|
|
|
4748
5229
|
captured.set(storage, snapshot2);
|
|
4749
5230
|
return Object.freeze({
|
|
4750
5231
|
snapshot: snapshot2,
|
|
4751
|
-
clock: Object.freeze(
|
|
5232
|
+
clock: Object.freeze(namespaceClock(namespaceForStorage.get(storage) ?? storage).state()),
|
|
4752
5233
|
rngState: (branchRngs.get(storage) ?? rng).state()
|
|
4753
5234
|
});
|
|
4754
5235
|
};
|
|
@@ -4758,7 +5239,7 @@ var createRuntime = (options) => {
|
|
|
4758
5239
|
return found;
|
|
4759
5240
|
instance(name);
|
|
4760
5241
|
found = new Timeline({
|
|
4761
|
-
now:
|
|
5242
|
+
now: namespaceClock(name).now,
|
|
4762
5243
|
...options.maxCheckpoints !== void 0 ? { maxCheckpoints: options.maxCheckpoints } : {}
|
|
4763
5244
|
});
|
|
4764
5245
|
found.commit(capture(name));
|
|
@@ -4785,12 +5266,13 @@ var createRuntime = (options) => {
|
|
|
4785
5266
|
const point = history2.checkout("main", at);
|
|
4786
5267
|
restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
|
|
4787
5268
|
captured.set(namespace, point.value.snapshot);
|
|
4788
|
-
rng.setState(point.value.rngState);
|
|
4789
|
-
|
|
5269
|
+
(branchRngs.get(namespace) ?? rng).setState(point.value.rngState);
|
|
5270
|
+
const selectedClock = namespaceClock(namespace);
|
|
5271
|
+
selectedClock.set(point.value.clock.now);
|
|
4790
5272
|
if (point.value.clock.frozen)
|
|
4791
|
-
|
|
5273
|
+
selectedClock.freeze();
|
|
4792
5274
|
else
|
|
4793
|
-
|
|
5275
|
+
selectedClock.unfreeze();
|
|
4794
5276
|
}
|
|
4795
5277
|
return namespace;
|
|
4796
5278
|
}
|
|
@@ -4848,11 +5330,12 @@ var createRuntime = (options) => {
|
|
|
4848
5330
|
const storage = ensureBranch(namespace, branchName);
|
|
4849
5331
|
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
4850
5332
|
captured.set(storage, point.value.snapshot);
|
|
4851
|
-
|
|
5333
|
+
const selectedClock = namespaceClock(namespace);
|
|
5334
|
+
selectedClock.set(point.value.clock.now);
|
|
4852
5335
|
if (point.value.clock.frozen)
|
|
4853
|
-
|
|
5336
|
+
selectedClock.freeze();
|
|
4854
5337
|
else
|
|
4855
|
-
|
|
5338
|
+
selectedClock.unfreeze();
|
|
4856
5339
|
(branchRngs.get(storage) ?? rng).setState(point.value.rngState);
|
|
4857
5340
|
};
|
|
4858
5341
|
const reset = async (name = DEFAULT_NAMESPACE) => {
|
|
@@ -4860,10 +5343,21 @@ var createRuntime = (options) => {
|
|
|
4860
5343
|
options.webhooks?.clear();
|
|
4861
5344
|
for (const each of instances.values())
|
|
4862
5345
|
await each.reset();
|
|
5346
|
+
for (const each of branchRngs.values())
|
|
5347
|
+
each.reset();
|
|
4863
5348
|
timelines.clear();
|
|
5349
|
+
for (const storage of branchStorage.values()) {
|
|
5350
|
+
instances.delete(storage);
|
|
5351
|
+
branchRngs.delete(storage);
|
|
5352
|
+
namespaceForStorage.delete(storage);
|
|
5353
|
+
}
|
|
4864
5354
|
branchStorage.clear();
|
|
4865
|
-
branchRngs.clear();
|
|
4866
5355
|
captured.clear();
|
|
5356
|
+
faults.clear();
|
|
5357
|
+
clock.reset();
|
|
5358
|
+
rng.reset();
|
|
5359
|
+
for (const each of namespaceClocks.values())
|
|
5360
|
+
each.reset();
|
|
4867
5361
|
return;
|
|
4868
5362
|
}
|
|
4869
5363
|
options.webhooks?.clear(name);
|
|
@@ -4881,11 +5375,18 @@ var createRuntime = (options) => {
|
|
|
4881
5375
|
else
|
|
4882
5376
|
clearNamespace(sqlite, storageNamespace(storage));
|
|
4883
5377
|
branchStorage.delete(mapping);
|
|
5378
|
+
instances.delete(storage);
|
|
5379
|
+
namespaceForStorage.delete(storage);
|
|
4884
5380
|
branchRngs.delete(storage);
|
|
4885
5381
|
captured.delete(storage);
|
|
4886
5382
|
}
|
|
4887
5383
|
timelines.delete(name);
|
|
4888
5384
|
captured.delete(name);
|
|
5385
|
+
faults.restore(name, { rules: [], rngState: createRng(options.seed ?? 0).state() });
|
|
5386
|
+
namespaceClock(name).reset();
|
|
5387
|
+
(branchRngs.get(name) ?? rng).reset();
|
|
5388
|
+
journal.clear(name);
|
|
5389
|
+
metrics.reset(name);
|
|
4889
5390
|
};
|
|
4890
5391
|
const snapshot = (name = DEFAULT_NAMESPACE) => {
|
|
4891
5392
|
return checkpoint(name, "main").value.snapshot;
|
|
@@ -4923,6 +5424,53 @@ var createRuntime = (options) => {
|
|
|
4923
5424
|
rng,
|
|
4924
5425
|
credentials,
|
|
4925
5426
|
webhooks: options.webhooks,
|
|
5427
|
+
isolateNamespaces() {
|
|
5428
|
+
if (publicNamespaces.size > 1)
|
|
5429
|
+
throw new Error("enable namespace isolation before creating named namespaces");
|
|
5430
|
+
isolatedNamespaces = true;
|
|
5431
|
+
faults.isolateNamespaces();
|
|
5432
|
+
},
|
|
5433
|
+
namespaceClock,
|
|
5434
|
+
namespaceOf(request) {
|
|
5435
|
+
const url = new URL(request.url);
|
|
5436
|
+
const prefixed = matchNamespacePath(url.pathname, adminPrefix);
|
|
5437
|
+
const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
|
|
5438
|
+
const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
|
|
5439
|
+
if (isAdminPath(path, adminPrefix))
|
|
5440
|
+
return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
|
|
5441
|
+
if (selected !== void 0)
|
|
5442
|
+
return selected;
|
|
5443
|
+
const credential = options.credential?.(request);
|
|
5444
|
+
return (credential !== void 0 ? credentials.get(credential) : void 0) ?? DEFAULT_NAMESPACE;
|
|
5445
|
+
},
|
|
5446
|
+
fleetSnapshot(namespace) {
|
|
5447
|
+
instance(namespace);
|
|
5448
|
+
const handle = {};
|
|
5449
|
+
fleetPoints.set(handle, {
|
|
5450
|
+
namespace,
|
|
5451
|
+
state: capture(namespace),
|
|
5452
|
+
faults: faults.snapshot(namespace),
|
|
5453
|
+
webhooks: options.webhooks?.snapshot(namespace)
|
|
5454
|
+
});
|
|
5455
|
+
return handle;
|
|
5456
|
+
},
|
|
5457
|
+
fleetRestore(handle, namespace) {
|
|
5458
|
+
const point = typeof handle === "object" && handle !== null ? fleetPoints.get(handle) : void 0;
|
|
5459
|
+
if (!point || point.namespace !== namespace)
|
|
5460
|
+
throw new Error("invalid fleet checkpoint");
|
|
5461
|
+
restoreNamespace(sqlite, storageNamespace(namespace), point.state.snapshot);
|
|
5462
|
+
captured.set(namespace, point.state.snapshot);
|
|
5463
|
+
const selectedClock = namespaceClock(namespace);
|
|
5464
|
+
selectedClock.freeze();
|
|
5465
|
+
selectedClock.set(point.state.clock.now);
|
|
5466
|
+
if (!point.state.clock.frozen)
|
|
5467
|
+
selectedClock.unfreeze();
|
|
5468
|
+
(branchRngs.get(namespace) ?? rng).setState(point.state.rngState);
|
|
5469
|
+
faults.restore(namespace, point.faults);
|
|
5470
|
+
if (point.webhooks !== void 0)
|
|
5471
|
+
options.webhooks?.restore(namespace, point.webhooks);
|
|
5472
|
+
timelines.delete(namespace);
|
|
5473
|
+
},
|
|
4926
5474
|
applyPreset: (name, namespace = DEFAULT_NAMESPACE, overrides = {}) => {
|
|
4927
5475
|
const preset = options.presets?.[name];
|
|
4928
5476
|
if (!preset)
|
|
@@ -4954,7 +5502,7 @@ var createRuntime = (options) => {
|
|
|
4954
5502
|
state: (namespace = DEFAULT_NAMESPACE) => inspectState(stateScope(namespace)),
|
|
4955
5503
|
fetch: async (incoming) => {
|
|
4956
5504
|
let request = incoming;
|
|
4957
|
-
const prefixed =
|
|
5505
|
+
const prefixed = matchNamespacePath(new URL(request.url).pathname, adminPrefix);
|
|
4958
5506
|
if (prefixed) {
|
|
4959
5507
|
const url2 = new URL(request.url);
|
|
4960
5508
|
url2.pathname = prefixed[2] ?? "/";
|
|
@@ -4970,7 +5518,15 @@ var createRuntime = (options) => {
|
|
|
4970
5518
|
signal: request.signal
|
|
4971
5519
|
});
|
|
4972
5520
|
}
|
|
4973
|
-
|
|
5521
|
+
const internalUrl = new URL(request.url);
|
|
5522
|
+
const internalPath = isAdminPath(internalUrl.pathname, adminPrefix) ? `${ADMIN_PREFIX}${internalUrl.pathname.slice(adminPrefix.length)}` : "";
|
|
5523
|
+
const internal = internalMatch(internalPath);
|
|
5524
|
+
const obsoleteInternalPath = adminPrefix !== ADMIN_PREFIX && internalMatch(internalUrl.pathname) && !internal;
|
|
5525
|
+
if (internal) {
|
|
5526
|
+
internalUrl.pathname = internalPath;
|
|
5527
|
+
request = new Request(internalUrl, request);
|
|
5528
|
+
}
|
|
5529
|
+
let namespace = internal ? internalUrl.searchParams.get("namespace") ?? control.namespaceOf(request) : control.namespaceOf(request);
|
|
4974
5530
|
if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
4975
5531
|
const credential = options.credential(request);
|
|
4976
5532
|
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
@@ -4990,7 +5546,9 @@ var createRuntime = (options) => {
|
|
|
4990
5546
|
return copy;
|
|
4991
5547
|
}
|
|
4992
5548
|
};
|
|
4993
|
-
|
|
5549
|
+
if (obsoleteInternalPath)
|
|
5550
|
+
return stamp(adminFail(404, "internal transport is only available under adminPrefix"));
|
|
5551
|
+
const handled = internal ? void 0 : await control.handle(request);
|
|
4994
5552
|
if (handled)
|
|
4995
5553
|
return stamp(handled);
|
|
4996
5554
|
const started = monotonicNow();
|
|
@@ -5031,7 +5589,7 @@ var createRuntime = (options) => {
|
|
|
5031
5589
|
type: "mockingbird_admin",
|
|
5032
5590
|
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
5033
5591
|
}
|
|
5034
|
-
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
5592
|
+
}), { status: 400, headers: { "content-type": "application/json; charset=utf-8" } }));
|
|
5035
5593
|
}
|
|
5036
5594
|
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
5037
5595
|
log(400);
|
|
@@ -5098,6 +5656,7 @@ var createRuntime = (options) => {
|
|
|
5098
5656
|
startedAt: wallNow(),
|
|
5099
5657
|
wallNow,
|
|
5100
5658
|
clock,
|
|
5659
|
+
namespaceClock,
|
|
5101
5660
|
faults,
|
|
5102
5661
|
metrics,
|
|
5103
5662
|
journal,
|
|
@@ -5147,15 +5706,16 @@ var createRuntime = (options) => {
|
|
|
5147
5706
|
...options.admin?.(runtime) ?? {},
|
|
5148
5707
|
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
5149
5708
|
// maps `presets`, and a service route must not replace that list with a record.
|
|
5150
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
5709
|
+
...options.presets ? presetRoutes(options.presets, runtime, adminPrefix) : {},
|
|
5151
5710
|
...stateAdminRoutes({
|
|
5152
5711
|
open: stateScope,
|
|
5153
5712
|
afterWrite: (namespace) => {
|
|
5154
5713
|
checkpoint(namespace, "main");
|
|
5155
5714
|
}
|
|
5156
5715
|
}),
|
|
5157
|
-
...adminUiRoutes(options.name, options.adminUi)
|
|
5716
|
+
...adminUiRoutes(options.name, options.adminUi, adminPrefix)
|
|
5158
5717
|
},
|
|
5718
|
+
adminPrefix,
|
|
5159
5719
|
adminKey: options.adminKey
|
|
5160
5720
|
});
|
|
5161
5721
|
return defineMock(runtime);
|
|
@@ -5169,7 +5729,10 @@ var mutableResponse = (response) => {
|
|
|
5169
5729
|
return new Response(response.body, response);
|
|
5170
5730
|
}
|
|
5171
5731
|
};
|
|
5172
|
-
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5732
|
+
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5733
|
+
status,
|
|
5734
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
5735
|
+
});
|
|
5173
5736
|
var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
|
|
5174
5737
|
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5175
5738
|
var credentialRoutes = (registry) => ({
|
|
@@ -5221,7 +5784,7 @@ var credentialRoutes = (registry) => ({
|
|
|
5221
5784
|
return adminJson(200, { status: "ok" });
|
|
5222
5785
|
}
|
|
5223
5786
|
});
|
|
5224
|
-
var presetRoutes = (presets, runtime) => ({
|
|
5787
|
+
var presetRoutes = (presets, runtime, adminPrefix) => ({
|
|
5225
5788
|
"GET /faults/presets": () => {
|
|
5226
5789
|
const body = {
|
|
5227
5790
|
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
@@ -5231,7 +5794,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
5231
5794
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
5232
5795
|
const name = params.name;
|
|
5233
5796
|
if (!presets[name])
|
|
5234
|
-
return adminFail(404, `no fault preset ${name}; GET /
|
|
5797
|
+
return adminFail(404, `no fault preset ${name}; GET ${adminPrefix}/faults/presets`);
|
|
5235
5798
|
const overrides = isObject(body) ? body : {};
|
|
5236
5799
|
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
5237
5800
|
}
|
|
@@ -5364,25 +5927,28 @@ var RESEND_PRESETS = {
|
|
|
5364
5927
|
var forwardToInbox = async (target, email, namespace) => {
|
|
5365
5928
|
const send = target.fetch ?? ((request) => fetch(request));
|
|
5366
5929
|
const response = await send(
|
|
5367
|
-
new Request(
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5930
|
+
new Request(
|
|
5931
|
+
`${target.url.replace(/\/$/, "")}${resolveAdminPrefix(target.adminPrefix)}/ingest`,
|
|
5932
|
+
{
|
|
5933
|
+
method: "POST",
|
|
5934
|
+
headers: {
|
|
5935
|
+
"content-type": "application/json",
|
|
5936
|
+
"x-mockingbird-namespace": namespace,
|
|
5937
|
+
...target.adminKey ? { "x-mockingbird-admin-key": target.adminKey } : {}
|
|
5938
|
+
},
|
|
5939
|
+
body: JSON.stringify({
|
|
5940
|
+
from: email.from,
|
|
5941
|
+
to: email.toHeader,
|
|
5942
|
+
cc: email.cc,
|
|
5943
|
+
bcc: email.bcc,
|
|
5944
|
+
subject: email.subject,
|
|
5945
|
+
html: email.html,
|
|
5946
|
+
text: email.text,
|
|
5947
|
+
headers: email.headers
|
|
5948
|
+
}),
|
|
5949
|
+
signal: AbortSignal.timeout(target.timeoutMs ?? 2e3)
|
|
5950
|
+
}
|
|
5951
|
+
)
|
|
5386
5952
|
);
|
|
5387
5953
|
await response.body?.cancel();
|
|
5388
5954
|
return response.ok;
|
|
@@ -5469,15 +6035,17 @@ var createRuntime2 = (options = {}) => {
|
|
|
5469
6035
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
5470
6036
|
...options.clock ? { clock: options.clock } : {},
|
|
5471
6037
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
6038
|
+
...options.adminPrefix !== void 0 ? { adminPrefix: options.adminPrefix } : {},
|
|
5472
6039
|
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
5473
6040
|
...options.onLog ? { onLog: options.onLog } : {},
|
|
5474
6041
|
credential: bearerToken,
|
|
5475
6042
|
presets: RESEND_PRESETS,
|
|
5476
6043
|
webhooks: hub,
|
|
5477
|
-
create: ({ sqlite, namespace, publicNamespace, clock }) => new ResendAPI({
|
|
6044
|
+
create: ({ sqlite, namespace, publicNamespace, adminPrefix, clock }) => new ResendAPI({
|
|
5478
6045
|
sqlite,
|
|
5479
6046
|
namespace,
|
|
5480
6047
|
publicNamespace,
|
|
6048
|
+
adminPrefix,
|
|
5481
6049
|
now: clock.now,
|
|
5482
6050
|
onOutcome: (event) => recordOutcome(event),
|
|
5483
6051
|
...target ? {
|
|
@@ -5589,6 +6157,7 @@ var ResendAPI = class {
|
|
|
5589
6157
|
state;
|
|
5590
6158
|
service;
|
|
5591
6159
|
now;
|
|
6160
|
+
adminPrefix;
|
|
5592
6161
|
publicNamespace;
|
|
5593
6162
|
onSent;
|
|
5594
6163
|
onOutcome;
|
|
@@ -5602,6 +6171,7 @@ var ResendAPI = class {
|
|
|
5602
6171
|
const sqlite = bootSqlite(options.sqlite);
|
|
5603
6172
|
const namespace = options.namespace ?? RESEND_NAMESPACE;
|
|
5604
6173
|
this.now = options.now ?? (() => Date.now());
|
|
6174
|
+
this.adminPrefix = options.adminPrefix ?? "/__admin";
|
|
5605
6175
|
this.publicNamespace = options.publicNamespace;
|
|
5606
6176
|
this.onSent = options.onSent;
|
|
5607
6177
|
this.onOutcome = options.onOutcome;
|
|
@@ -5853,7 +6423,7 @@ var ResendAPI = class {
|
|
|
5853
6423
|
};
|
|
5854
6424
|
}
|
|
5855
6425
|
downloadUrl(origin, attachmentId) {
|
|
5856
|
-
const prefix = this.publicNamespace && this.publicNamespace !== "default" ?
|
|
6426
|
+
const prefix = this.publicNamespace && this.publicNamespace !== "default" ? `${this.adminPrefix}/ns/${encodeURIComponent(this.publicNamespace)}` : "";
|
|
5857
6427
|
return `${origin}${prefix}/downloads/inbound/${attachmentId}`;
|
|
5858
6428
|
}
|
|
5859
6429
|
getReceived(context) {
|
|
@@ -5914,6 +6484,9 @@ var ResendAPI = class {
|
|
|
5914
6484
|
};
|
|
5915
6485
|
|
|
5916
6486
|
export {
|
|
6487
|
+
createClock,
|
|
6488
|
+
resolveAdminPrefix,
|
|
6489
|
+
parseDuration,
|
|
5917
6490
|
document2 as document,
|
|
5918
6491
|
operationIds,
|
|
5919
6492
|
supportedOperationIds,
|
|
@@ -5925,4 +6498,4 @@ export {
|
|
|
5925
6498
|
bareAddress,
|
|
5926
6499
|
ResendAPI
|
|
5927
6500
|
};
|
|
5928
|
-
//# sourceMappingURL=chunk-
|
|
6501
|
+
//# sourceMappingURL=chunk-AVLXDCB3.js.map
|