@crvouga/mockingbird-service-easypost 1.7.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/DISCOVERY.md +55 -0
- package/README.md +1 -1
- package/SUPPORT.md +13 -0
- package/dist/{chunk-3TOBBMZD.js → chunk-32BNNR7Z.js} +784 -258
- package/dist/chunk-32BNNR7Z.js.map +7 -0
- package/dist/chunk-NFI2NJUO.js +838 -0
- package/dist/chunk-NFI2NJUO.js.map +7 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +34 -4
- package/dist/index.js +1 -1
- package/dist/server.d.ts +64 -4
- package/dist/server.js +2 -2
- package/openapi.yaml +286 -0
- package/package.json +29 -4
- package/dist/chunk-3TOBBMZD.js.map +0 -7
- package/dist/chunk-PLFXUF5M.js +0 -351
- package/dist/chunk-PLFXUF5M.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
|
};
|
|
@@ -485,7 +525,33 @@ var faultResponse = (rule) => {
|
|
|
485
525
|
};
|
|
486
526
|
var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) => {
|
|
487
527
|
const entries = [];
|
|
528
|
+
let isolated = false;
|
|
529
|
+
const scopedRngs = /* @__PURE__ */ new Map();
|
|
530
|
+
const random = (namespace) => {
|
|
531
|
+
if (!isolated || namespace === "default")
|
|
532
|
+
return rng;
|
|
533
|
+
let scoped = scopedRngs.get(namespace);
|
|
534
|
+
if (!scoped) {
|
|
535
|
+
scoped = createRng(rng.seed);
|
|
536
|
+
scopedRngs.set(namespace, scoped);
|
|
537
|
+
}
|
|
538
|
+
return scoped;
|
|
539
|
+
};
|
|
488
540
|
return {
|
|
541
|
+
isolateNamespaces() {
|
|
542
|
+
isolated = true;
|
|
543
|
+
},
|
|
544
|
+
snapshot(namespace) {
|
|
545
|
+
return {
|
|
546
|
+
rules: structuredClone(entries.map((entry, position) => ({
|
|
547
|
+
...entry.rule,
|
|
548
|
+
remaining: entry.remaining,
|
|
549
|
+
hits: entry.hits,
|
|
550
|
+
position
|
|
551
|
+
})).filter((entry) => entry.namespace === namespace)),
|
|
552
|
+
rngState: random(namespace).state()
|
|
553
|
+
};
|
|
554
|
+
},
|
|
489
555
|
add(rule) {
|
|
490
556
|
const existing = entries.findIndex((e) => e.rule.id === rule.id);
|
|
491
557
|
const entry = { rule, remaining: rule.count ?? null, hits: 0 };
|
|
@@ -505,6 +571,16 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
|
|
|
505
571
|
},
|
|
506
572
|
clear() {
|
|
507
573
|
entries.length = 0;
|
|
574
|
+
rng.reset();
|
|
575
|
+
scopedRngs.clear();
|
|
576
|
+
},
|
|
577
|
+
restore(namespace, state) {
|
|
578
|
+
for (let i = entries.length - 1; i >= 0; i--)
|
|
579
|
+
if (entries[i]?.rule.namespace === namespace)
|
|
580
|
+
entries.splice(i, 1);
|
|
581
|
+
for (const { remaining, hits, position, ...rule } of state.rules)
|
|
582
|
+
entries.splice(Math.min(position, entries.length), 0, { rule, remaining, hits });
|
|
583
|
+
random(namespace).setState(state.rngState);
|
|
508
584
|
},
|
|
509
585
|
async take(candidate) {
|
|
510
586
|
const hits = [];
|
|
@@ -514,7 +590,7 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
|
|
|
514
590
|
if (!matches(entry.rule, candidate))
|
|
515
591
|
continue;
|
|
516
592
|
const rate = entry.rule.rate ?? 1;
|
|
517
|
-
if (
|
|
593
|
+
if (random(candidate.namespace).next() >= rate)
|
|
518
594
|
continue;
|
|
519
595
|
entry.hits++;
|
|
520
596
|
if (entry.remaining !== null)
|
|
@@ -1123,7 +1199,8 @@ var annotateResponse = (response, extra) => {
|
|
|
1123
1199
|
var responseNotes = (response) => notes.get(response);
|
|
1124
1200
|
|
|
1125
1201
|
// ../core/dist/metrics.js
|
|
1126
|
-
var createMetrics = () => {
|
|
1202
|
+
var createMetrics = (partition = true) => {
|
|
1203
|
+
const namespaces = /* @__PURE__ */ new Map();
|
|
1127
1204
|
let requests = 0;
|
|
1128
1205
|
let faults = 0;
|
|
1129
1206
|
let totalDurationMs = 0;
|
|
@@ -1131,6 +1208,14 @@ var createMetrics = () => {
|
|
|
1131
1208
|
const unmatched = /* @__PURE__ */ new Map();
|
|
1132
1209
|
return {
|
|
1133
1210
|
record(entry) {
|
|
1211
|
+
if (partition) {
|
|
1212
|
+
let scoped = namespaces.get(entry.namespace);
|
|
1213
|
+
if (!scoped) {
|
|
1214
|
+
scoped = createMetrics(false);
|
|
1215
|
+
namespaces.set(entry.namespace, scoped);
|
|
1216
|
+
}
|
|
1217
|
+
scoped.record(entry);
|
|
1218
|
+
}
|
|
1134
1219
|
requests++;
|
|
1135
1220
|
totalDurationMs += entry.durationMs;
|
|
1136
1221
|
if (entry.faultId !== void 0)
|
|
@@ -1142,7 +1227,7 @@ var createMetrics = () => {
|
|
|
1142
1227
|
unmatched.set(route, (unmatched.get(route) ?? 0) + 1);
|
|
1143
1228
|
}
|
|
1144
1229
|
},
|
|
1145
|
-
report: () => ({
|
|
1230
|
+
report: (namespace) => namespace !== void 0 ? (namespaces.get(namespace) ?? createMetrics(false)).report() : {
|
|
1146
1231
|
requests,
|
|
1147
1232
|
byOperation: Object.fromEntries([...byOperation].sort(([a], [b]) => a.localeCompare(b))),
|
|
1148
1233
|
unmatched: [...unmatched].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([route, count]) => {
|
|
@@ -1151,8 +1236,13 @@ var createMetrics = () => {
|
|
|
1151
1236
|
}),
|
|
1152
1237
|
faults,
|
|
1153
1238
|
totalDurationMs
|
|
1154
|
-
}
|
|
1155
|
-
reset() {
|
|
1239
|
+
},
|
|
1240
|
+
reset(namespace) {
|
|
1241
|
+
if (namespace !== void 0) {
|
|
1242
|
+
namespaces.delete(namespace);
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
namespaces.clear();
|
|
1156
1246
|
requests = 0;
|
|
1157
1247
|
faults = 0;
|
|
1158
1248
|
totalDurationMs = 0;
|
|
@@ -1765,6 +1855,34 @@ dialog:not([open]) {
|
|
|
1765
1855
|
function isRecord2(value) {
|
|
1766
1856
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1767
1857
|
}
|
|
1858
|
+
function highlightJsonText(text) {
|
|
1859
|
+
const escapeToken = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
1860
|
+
switch (char) {
|
|
1861
|
+
case "&":
|
|
1862
|
+
return "&";
|
|
1863
|
+
case "<":
|
|
1864
|
+
return "<";
|
|
1865
|
+
case ">":
|
|
1866
|
+
return ">";
|
|
1867
|
+
case '"':
|
|
1868
|
+
return """;
|
|
1869
|
+
default:
|
|
1870
|
+
return "'";
|
|
1871
|
+
}
|
|
1872
|
+
});
|
|
1873
|
+
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;
|
|
1874
|
+
let cursor = 0;
|
|
1875
|
+
let html = "";
|
|
1876
|
+
for (const match of text.matchAll(token)) {
|
|
1877
|
+
const index = match.index ?? 0;
|
|
1878
|
+
const value = match[0];
|
|
1879
|
+
html += escapeToken(text.slice(cursor, index));
|
|
1880
|
+
const kind = value.startsWith('"') ? value.trimEnd().endsWith(":") ? "key" : "string" : value === "true" || value === "false" ? "boolean" : value === "null" ? "null" : "number";
|
|
1881
|
+
html += `<span class="json-${kind}">${escapeToken(value)}</span>`;
|
|
1882
|
+
cursor = index + value.length;
|
|
1883
|
+
}
|
|
1884
|
+
return html + escapeToken(text.slice(cursor));
|
|
1885
|
+
}
|
|
1768
1886
|
function mountAdminBrand(doc, host, service, catalog) {
|
|
1769
1887
|
const clip = (value, max) => {
|
|
1770
1888
|
if (typeof value !== "string")
|
|
@@ -2126,14 +2244,16 @@ function bootAdmin(config) {
|
|
|
2126
2244
|
const state = {
|
|
2127
2245
|
namespace: "default",
|
|
2128
2246
|
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
2129
|
-
view: "overview",
|
|
2247
|
+
view: ["overview", "state", "clock", "faults", "journal", "routes"].includes(location.hash.slice(1)) ? location.hash.slice(1) : "overview",
|
|
2130
2248
|
collection: "",
|
|
2131
2249
|
shape: null,
|
|
2132
2250
|
route: null,
|
|
2251
|
+
routes: [],
|
|
2133
2252
|
editing: null
|
|
2134
2253
|
};
|
|
2135
2254
|
const standard = new Set(config.standardRoutes);
|
|
2136
2255
|
const banner = el("banner", HTMLElement);
|
|
2256
|
+
const bannerMessage = el("banner-message", HTMLElement);
|
|
2137
2257
|
const keyInput = el("admin-key", HTMLInputElement);
|
|
2138
2258
|
keyInput.value = state.key;
|
|
2139
2259
|
const params = new URLSearchParams(location.search);
|
|
@@ -2144,18 +2264,21 @@ function bootAdmin(config) {
|
|
|
2144
2264
|
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
2145
2265
|
params.delete("key");
|
|
2146
2266
|
const next = params.toString();
|
|
2147
|
-
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
2267
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : "") + location.hash);
|
|
2148
2268
|
}
|
|
2149
2269
|
const queryNamespace = params.get("namespace");
|
|
2150
2270
|
if (queryNamespace)
|
|
2151
2271
|
state.namespace = queryNamespace;
|
|
2152
2272
|
const showError = (error) => {
|
|
2153
|
-
|
|
2273
|
+
bannerMessage.textContent = error instanceof Error ? error.message : String(error);
|
|
2154
2274
|
banner.classList.add("show");
|
|
2275
|
+
banner.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
2155
2276
|
};
|
|
2156
2277
|
const clearError = () => {
|
|
2157
2278
|
banner.classList.remove("show");
|
|
2279
|
+
bannerMessage.textContent = "";
|
|
2158
2280
|
};
|
|
2281
|
+
el("banner-dismiss", HTMLButtonElement).addEventListener("click", clearError);
|
|
2159
2282
|
const api = async (method, path, body) => {
|
|
2160
2283
|
const headers = { accept: "application/json" };
|
|
2161
2284
|
if (state.key !== "")
|
|
@@ -2199,16 +2322,21 @@ function bootAdmin(config) {
|
|
|
2199
2322
|
namespace: () => state.namespace
|
|
2200
2323
|
};
|
|
2201
2324
|
const isPanelScript = (value) => typeof value === "function";
|
|
2325
|
+
const themeButton = el("theme", HTMLButtonElement);
|
|
2202
2326
|
const setTheme = (theme) => {
|
|
2203
2327
|
if (theme)
|
|
2204
2328
|
document.documentElement.dataset.theme = theme;
|
|
2205
2329
|
else
|
|
2206
2330
|
delete document.documentElement.dataset.theme;
|
|
2331
|
+
const dark = theme ? theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2332
|
+
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>';
|
|
2333
|
+
const label = dark ? "Use light theme" : "Use dark theme";
|
|
2334
|
+
themeButton.setAttribute("aria-label", label);
|
|
2335
|
+
themeButton.title = label;
|
|
2207
2336
|
};
|
|
2208
2337
|
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
2338
|
+
setTheme(storedTheme === "dark" || storedTheme === "light" ? storedTheme : null);
|
|
2339
|
+
themeButton.addEventListener("click", () => {
|
|
2212
2340
|
const current = document.documentElement.dataset.theme;
|
|
2213
2341
|
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2214
2342
|
const next = dark ? "light" : "dark";
|
|
@@ -2227,6 +2355,7 @@ function bootAdmin(config) {
|
|
|
2227
2355
|
continue;
|
|
2228
2356
|
section.hidden = section.id !== `view-${view}`;
|
|
2229
2357
|
}
|
|
2358
|
+
history.replaceState(null, "", `${location.pathname}${location.search}#${encodeURIComponent(view)}`);
|
|
2230
2359
|
};
|
|
2231
2360
|
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
2232
2361
|
const target = event.target;
|
|
@@ -2241,7 +2370,7 @@ function bootAdmin(config) {
|
|
|
2241
2370
|
show(view);
|
|
2242
2371
|
refresh().catch(showError);
|
|
2243
2372
|
});
|
|
2244
|
-
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>`;
|
|
2373
|
+
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>`;
|
|
2245
2374
|
const table = (columns, rows) => {
|
|
2246
2375
|
if (rows.length === 0)
|
|
2247
2376
|
return '<p class="empty">Nothing here yet.</p>';
|
|
@@ -2251,16 +2380,22 @@ function bootAdmin(config) {
|
|
|
2251
2380
|
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
2252
2381
|
return `<tr${attrs}>${cells}</tr>`;
|
|
2253
2382
|
}).join("");
|
|
2254
|
-
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
2383
|
+
return `<div class="table-shell"><table><thead>${head}</thead><tbody>${body}</tbody></table></div>`;
|
|
2255
2384
|
};
|
|
2256
2385
|
const preview = (value) => {
|
|
2257
2386
|
const text = JSON.stringify(value) ?? "null";
|
|
2258
|
-
|
|
2387
|
+
const clipped = text.length > 180 ? `${text.slice(0, 180)}\u2026` : text;
|
|
2388
|
+
return `<code class="json-inline">${highlightJsonText(clipped)}</code>`;
|
|
2259
2389
|
};
|
|
2260
2390
|
const when = (epoch) => {
|
|
2261
2391
|
const date = new Date(epoch);
|
|
2262
2392
|
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
2263
2393
|
};
|
|
2394
|
+
const journalWhen = (value) => {
|
|
2395
|
+
const numeric = value.trim() === "" ? Number.NaN : Number(value);
|
|
2396
|
+
const epoch = Number.isFinite(numeric) ? numeric : Date.parse(value);
|
|
2397
|
+
return Number.isFinite(epoch) ? when(epoch) : value;
|
|
2398
|
+
};
|
|
2264
2399
|
const clockCard = (epoch) => {
|
|
2265
2400
|
const date = new Date(epoch);
|
|
2266
2401
|
if (Number.isNaN(date.getTime()))
|
|
@@ -2380,7 +2515,7 @@ function bootAdmin(config) {
|
|
|
2380
2515
|
};
|
|
2381
2516
|
const renderOverview = async () => {
|
|
2382
2517
|
const [health, shape, clock, journal] = await Promise.all([
|
|
2383
|
-
originGet(
|
|
2518
|
+
originGet(`${config.adminPrefix}/health`),
|
|
2384
2519
|
api("GET", "/state"),
|
|
2385
2520
|
api("GET", "/clock"),
|
|
2386
2521
|
api("GET", "/requests?limit=8")
|
|
@@ -2397,7 +2532,7 @@ function bootAdmin(config) {
|
|
|
2397
2532
|
},
|
|
2398
2533
|
{
|
|
2399
2534
|
k: "Status",
|
|
2400
|
-
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
2535
|
+
v: `<span class="badge ok">${esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")}</span>`
|
|
2401
2536
|
},
|
|
2402
2537
|
{ k: "Namespace", v: esc(state.namespace) },
|
|
2403
2538
|
{ k: "Collections", v: String(decoded.collections.length) },
|
|
@@ -2484,26 +2619,46 @@ function bootAdmin(config) {
|
|
|
2484
2619
|
const renderJournal = async () => {
|
|
2485
2620
|
const journal = await api("GET", "/requests?limit=100");
|
|
2486
2621
|
el("journal", HTMLElement).innerHTML = table([
|
|
2487
|
-
{ label: "When", cell: (row) => esc(row.at) },
|
|
2488
|
-
{
|
|
2622
|
+
{ label: "When", cell: (row) => esc(journalWhen(row.at)) },
|
|
2623
|
+
{
|
|
2624
|
+
label: "Method",
|
|
2625
|
+
cell: (row) => `<span class="method">${esc(row.method)}</span>`
|
|
2626
|
+
},
|
|
2489
2627
|
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2490
|
-
{
|
|
2628
|
+
{
|
|
2629
|
+
label: "Status",
|
|
2630
|
+
cell: (row) => `<span class="badge ${row.status.startsWith("2") ? "ok" : row.status.startsWith("4") || row.status.startsWith("5") ? "warn" : ""}">${esc(row.status)}</span>`
|
|
2631
|
+
},
|
|
2491
2632
|
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
2492
2633
|
], decodeJournal(journal));
|
|
2493
2634
|
};
|
|
2494
|
-
const
|
|
2495
|
-
const
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2635
|
+
const paintRoutes = () => {
|
|
2636
|
+
const query = el("route-search", HTMLInputElement).value.trim().toLocaleLowerCase();
|
|
2637
|
+
const routes = state.routes.filter((route) => route.toLocaleLowerCase().includes(query));
|
|
2638
|
+
el("route-count", HTMLElement).textContent = `${routes.length} of ${state.routes.length}`;
|
|
2639
|
+
if (state.routes.length === 0) {
|
|
2640
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No admin routes are available.</p>';
|
|
2641
|
+
return;
|
|
2642
|
+
}
|
|
2643
|
+
if (routes.length === 0) {
|
|
2644
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No routes match this filter.</p>';
|
|
2645
|
+
return;
|
|
2646
|
+
}
|
|
2499
2647
|
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2500
2648
|
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2501
2649
|
const parts = route.split(" ");
|
|
2502
2650
|
const method = parts[0] ?? "";
|
|
2503
2651
|
const path = parts.slice(1).join(" ");
|
|
2504
|
-
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>`;
|
|
2652
|
+
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>`;
|
|
2505
2653
|
}).join("");
|
|
2506
2654
|
};
|
|
2655
|
+
const renderRoutes = async () => {
|
|
2656
|
+
const data = await api("GET", "/");
|
|
2657
|
+
if (!isRecord2(data))
|
|
2658
|
+
throw new Error("Route list was not an object");
|
|
2659
|
+
state.routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
2660
|
+
paintRoutes();
|
|
2661
|
+
};
|
|
2507
2662
|
const renderers = {
|
|
2508
2663
|
overview: renderOverview,
|
|
2509
2664
|
state: renderState,
|
|
@@ -2512,19 +2667,50 @@ function bootAdmin(config) {
|
|
|
2512
2667
|
journal: renderJournal,
|
|
2513
2668
|
routes: renderRoutes
|
|
2514
2669
|
};
|
|
2670
|
+
let refreshToken = 0;
|
|
2515
2671
|
async function refresh() {
|
|
2672
|
+
const token = ++refreshToken;
|
|
2516
2673
|
clearError();
|
|
2674
|
+
const main = el("views", HTMLElement);
|
|
2675
|
+
const sync = el("sync-status", HTMLElement);
|
|
2676
|
+
main.setAttribute("aria-busy", "true");
|
|
2677
|
+
sync.classList.add("busy");
|
|
2678
|
+
sync.textContent = "Updating";
|
|
2517
2679
|
const render = renderers[state.view];
|
|
2518
|
-
|
|
2519
|
-
|
|
2680
|
+
try {
|
|
2681
|
+
if (render)
|
|
2682
|
+
await render();
|
|
2683
|
+
if (token === refreshToken)
|
|
2684
|
+
sync.textContent = "Up to date";
|
|
2685
|
+
} finally {
|
|
2686
|
+
if (token === refreshToken) {
|
|
2687
|
+
main.removeAttribute("aria-busy");
|
|
2688
|
+
sync.classList.remove("busy");
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2520
2691
|
}
|
|
2521
2692
|
const delay = (ms) => new Promise((resolve) => {
|
|
2522
2693
|
window.setTimeout(resolve, ms);
|
|
2523
2694
|
});
|
|
2524
|
-
const confirmAction = async (button, pending, done, work) => {
|
|
2695
|
+
const confirmAction = async (button, pending, done, work, confirmLabel) => {
|
|
2525
2696
|
if (button.dataset.pending === "1")
|
|
2526
2697
|
return;
|
|
2527
2698
|
const label = button.textContent ?? "";
|
|
2699
|
+
if (confirmLabel !== void 0 && button.dataset.confirming !== "1") {
|
|
2700
|
+
button.dataset.confirming = "1";
|
|
2701
|
+
button.classList.add("confirming");
|
|
2702
|
+
button.textContent = confirmLabel;
|
|
2703
|
+
window.setTimeout(() => {
|
|
2704
|
+
if (button.dataset.confirming !== "1")
|
|
2705
|
+
return;
|
|
2706
|
+
button.textContent = label;
|
|
2707
|
+
button.classList.remove("confirming");
|
|
2708
|
+
delete button.dataset.confirming;
|
|
2709
|
+
}, 3e3);
|
|
2710
|
+
return;
|
|
2711
|
+
}
|
|
2712
|
+
button.classList.remove("confirming");
|
|
2713
|
+
delete button.dataset.confirming;
|
|
2528
2714
|
const width = button.offsetWidth;
|
|
2529
2715
|
button.dataset.pending = "1";
|
|
2530
2716
|
button.classList.add("pressed");
|
|
@@ -2545,6 +2731,65 @@ function bootAdmin(config) {
|
|
|
2545
2731
|
delete button.dataset.pending;
|
|
2546
2732
|
}
|
|
2547
2733
|
};
|
|
2734
|
+
const wireJsonEditor = (textareaId, highlightId, statusId, shellId, allowEmpty) => {
|
|
2735
|
+
const textarea = el(textareaId, HTMLTextAreaElement);
|
|
2736
|
+
const highlight = el(highlightId, HTMLElement);
|
|
2737
|
+
const status = el(statusId, HTMLElement);
|
|
2738
|
+
const shell = el(shellId, HTMLElement);
|
|
2739
|
+
const sync = () => {
|
|
2740
|
+
const text = textarea.value;
|
|
2741
|
+
highlight.innerHTML = `${highlightJsonText(text)}
|
|
2742
|
+
`;
|
|
2743
|
+
let valid = true;
|
|
2744
|
+
let message = "Valid JSON";
|
|
2745
|
+
if (allowEmpty && text.trim() === "")
|
|
2746
|
+
message = "No body";
|
|
2747
|
+
else {
|
|
2748
|
+
try {
|
|
2749
|
+
readJson2(text);
|
|
2750
|
+
} catch {
|
|
2751
|
+
valid = false;
|
|
2752
|
+
message = "Invalid JSON";
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
shell.classList.toggle("invalid", !valid);
|
|
2756
|
+
status.classList.toggle("invalid", !valid);
|
|
2757
|
+
status.textContent = message;
|
|
2758
|
+
textarea.setAttribute("aria-invalid", valid ? "false" : "true");
|
|
2759
|
+
};
|
|
2760
|
+
const syncScroll = () => {
|
|
2761
|
+
const preview2 = highlight.parentElement;
|
|
2762
|
+
if (!(preview2 instanceof HTMLElement))
|
|
2763
|
+
return;
|
|
2764
|
+
preview2.scrollTop = textarea.scrollTop;
|
|
2765
|
+
preview2.scrollLeft = textarea.scrollLeft;
|
|
2766
|
+
};
|
|
2767
|
+
textarea.addEventListener("input", sync);
|
|
2768
|
+
textarea.addEventListener("scroll", syncScroll);
|
|
2769
|
+
sync();
|
|
2770
|
+
return sync;
|
|
2771
|
+
};
|
|
2772
|
+
const syncRouteEditor = wireJsonEditor("route-body", "route-highlight", "route-json-status", "route-editor", true);
|
|
2773
|
+
const syncRecordEditor = wireJsonEditor("editor-value", "editor-highlight", "editor-json-status", "record-editor", false);
|
|
2774
|
+
for (const button of document.querySelectorAll("[data-format]")) {
|
|
2775
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2776
|
+
continue;
|
|
2777
|
+
button.addEventListener("click", () => {
|
|
2778
|
+
const target = button.dataset.format;
|
|
2779
|
+
if (target === void 0)
|
|
2780
|
+
return;
|
|
2781
|
+
const textarea = document.getElementById(target);
|
|
2782
|
+
if (!(textarea instanceof HTMLTextAreaElement) || textarea.value.trim() === "")
|
|
2783
|
+
return;
|
|
2784
|
+
try {
|
|
2785
|
+
textarea.value = JSON.stringify(readJson2(textarea.value), null, 2) ?? "";
|
|
2786
|
+
textarea.dispatchEvent(new Event("input"));
|
|
2787
|
+
textarea.focus();
|
|
2788
|
+
} catch {
|
|
2789
|
+
showError(new Error("Fix the JSON before formatting it"));
|
|
2790
|
+
}
|
|
2791
|
+
});
|
|
2792
|
+
}
|
|
2548
2793
|
document.addEventListener("click", (event) => {
|
|
2549
2794
|
const target = event.target;
|
|
2550
2795
|
if (!(target instanceof Element))
|
|
@@ -2617,6 +2862,7 @@ function bootAdmin(config) {
|
|
|
2617
2862
|
} else {
|
|
2618
2863
|
editorValue.value = "{\n \n}";
|
|
2619
2864
|
}
|
|
2865
|
+
syncRecordEditor();
|
|
2620
2866
|
editor.showModal();
|
|
2621
2867
|
}
|
|
2622
2868
|
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -2646,10 +2892,10 @@ function bootAdmin(config) {
|
|
|
2646
2892
|
const id = state.editing;
|
|
2647
2893
|
if (!id)
|
|
2648
2894
|
return;
|
|
2649
|
-
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2895
|
+
confirmAction(editorDelete, "Deleting...", "Deleted", () => api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2650
2896
|
editor.close();
|
|
2651
2897
|
return renderState();
|
|
2652
|
-
}).catch(showError);
|
|
2898
|
+
}), "Confirm delete").catch(showError);
|
|
2653
2899
|
});
|
|
2654
2900
|
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2655
2901
|
if (!(button instanceof HTMLButtonElement))
|
|
@@ -2695,10 +2941,10 @@ function bootAdmin(config) {
|
|
|
2695
2941
|
}).then(() => renderFaults()).catch(showError);
|
|
2696
2942
|
});
|
|
2697
2943
|
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2698
|
-
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
|
|
2944
|
+
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults()), "Confirm clear").catch(showError);
|
|
2699
2945
|
});
|
|
2700
2946
|
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2701
|
-
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
|
|
2947
|
+
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal()), "Confirm clear").catch(showError);
|
|
2702
2948
|
});
|
|
2703
2949
|
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2704
2950
|
const target = event.target;
|
|
@@ -2712,9 +2958,15 @@ function bootAdmin(config) {
|
|
|
2712
2958
|
return;
|
|
2713
2959
|
state.route = route;
|
|
2714
2960
|
el("route-title", HTMLElement).textContent = route;
|
|
2961
|
+
el("route-send", HTMLButtonElement).disabled = false;
|
|
2715
2962
|
const method = route.split(" ")[0] ?? "";
|
|
2716
|
-
el("route-body", HTMLTextAreaElement)
|
|
2963
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
2964
|
+
routeBody.disabled = method === "GET" || method === "DELETE";
|
|
2965
|
+
el("route-editor", HTMLElement).hidden = routeBody.disabled;
|
|
2966
|
+
el("route-json-status", HTMLElement).parentElement?.toggleAttribute("hidden", routeBody.disabled);
|
|
2967
|
+
syncRouteEditor();
|
|
2717
2968
|
});
|
|
2969
|
+
el("route-search", HTMLInputElement).addEventListener("input", paintRoutes);
|
|
2718
2970
|
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2719
2971
|
event.preventDefault();
|
|
2720
2972
|
const send = async () => {
|
|
@@ -2736,7 +2988,8 @@ function bootAdmin(config) {
|
|
|
2736
2988
|
}
|
|
2737
2989
|
}
|
|
2738
2990
|
const data = await api(method, concrete, body);
|
|
2739
|
-
|
|
2991
|
+
const text = JSON.stringify(data, null, 2) ?? "null";
|
|
2992
|
+
el("route-result", HTMLElement).innerHTML = `<code>${highlightJsonText(text)}</code>`;
|
|
2740
2993
|
};
|
|
2741
2994
|
send().catch(showError);
|
|
2742
2995
|
});
|
|
@@ -2854,11 +3107,13 @@ function bootAdmin(config) {
|
|
|
2854
3107
|
views.append(section);
|
|
2855
3108
|
mountSqlExplorer(body, panelApi);
|
|
2856
3109
|
}
|
|
3110
|
+
show(document.getElementById(`view-${state.view}`) === null ? "overview" : state.view);
|
|
2857
3111
|
return refresh();
|
|
2858
3112
|
}).catch(showError);
|
|
2859
3113
|
}
|
|
2860
3114
|
var clientPrelude = [
|
|
2861
3115
|
isRecord2,
|
|
3116
|
+
highlightJsonText,
|
|
2862
3117
|
mountAdminBrand,
|
|
2863
3118
|
startAdminBrand,
|
|
2864
3119
|
faultPresetList,
|
|
@@ -2881,6 +3136,7 @@ ${entry}(${payload});
|
|
|
2881
3136
|
var defineMock = (runtime) => runtime;
|
|
2882
3137
|
var STANDARD_ADMIN_ROUTES = [
|
|
2883
3138
|
"GET /",
|
|
3139
|
+
"GET /health",
|
|
2884
3140
|
"POST /reset",
|
|
2885
3141
|
"GET /namespaces",
|
|
2886
3142
|
"GET /clock",
|
|
@@ -2960,11 +3216,11 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
2960
3216
|
}
|
|
2961
3217
|
});
|
|
2962
3218
|
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
2963
|
-
var renderAdminDocument = (service) => {
|
|
3219
|
+
var renderAdminDocument = (service, adminPrefix = ADMIN_PREFIX) => {
|
|
2964
3220
|
const name = escapeHtml(service);
|
|
2965
3221
|
const script = adminClientSource({
|
|
2966
3222
|
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
2967
|
-
adminPrefix
|
|
3223
|
+
adminPrefix,
|
|
2968
3224
|
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
2969
3225
|
brandsUrl: ADMIN_BRANDS_URL,
|
|
2970
3226
|
service
|
|
@@ -2981,75 +3237,99 @@ var renderAdminDocument = (service) => {
|
|
|
2981
3237
|
${CSS_RESET}
|
|
2982
3238
|
:root {
|
|
2983
3239
|
color-scheme: light;
|
|
2984
|
-
--bg: #
|
|
3240
|
+
--bg: #f7f8fb;
|
|
2985
3241
|
--bg-elev: #ffffff;
|
|
2986
|
-
--bg-muted: #
|
|
2987
|
-
--bg-inset: #
|
|
2988
|
-
--line: #
|
|
2989
|
-
--line-strong: #
|
|
2990
|
-
--ink: #
|
|
2991
|
-
--muted: #
|
|
2992
|
-
--faint: #
|
|
2993
|
-
--accent: #
|
|
3242
|
+
--bg-muted: #f1f3f7;
|
|
3243
|
+
--bg-inset: #f8f9fc;
|
|
3244
|
+
--line: #e4e7ec;
|
|
3245
|
+
--line-strong: #cfd4dc;
|
|
3246
|
+
--ink: #101828;
|
|
3247
|
+
--muted: #475467;
|
|
3248
|
+
--faint: #667085;
|
|
3249
|
+
--accent: #6941c6;
|
|
3250
|
+
--accent-hover: #5933b4;
|
|
2994
3251
|
--accent-ink: #ffffff;
|
|
2995
|
-
--accent-soft: #
|
|
2996
|
-
--ok: #
|
|
2997
|
-
--ok-soft: #
|
|
2998
|
-
--warn: #
|
|
2999
|
-
--warn-soft: #
|
|
3000
|
-
--err: #
|
|
3001
|
-
--err-soft: #
|
|
3002
|
-
--
|
|
3252
|
+
--accent-soft: #f4f0ff;
|
|
3253
|
+
--ok: #067647;
|
|
3254
|
+
--ok-soft: #ecfdf3;
|
|
3255
|
+
--warn: #b54708;
|
|
3256
|
+
--warn-soft: #fffaeb;
|
|
3257
|
+
--err: #b42318;
|
|
3258
|
+
--err-soft: #fef3f2;
|
|
3259
|
+
--code-key: #6941c6;
|
|
3260
|
+
--code-string: #067647;
|
|
3261
|
+
--code-number: #175cd3;
|
|
3262
|
+
--code-boolean: #c11574;
|
|
3263
|
+
--code-null: #667085;
|
|
3264
|
+
--shadow-xs: 0 1px 2px rgb(16 24 40 / 0.05);
|
|
3265
|
+
--shadow-sm: 0 1px 3px rgb(16 24 40 / 0.08), 0 1px 2px rgb(16 24 40 / 0.04);
|
|
3266
|
+
--shadow-lg: 0 18px 48px rgb(16 24 40 / 0.16), 0 4px 12px rgb(16 24 40 / 0.08);
|
|
3003
3267
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
3004
3268
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
3005
|
-
--radius:
|
|
3006
|
-
--header:
|
|
3007
|
-
--nav:
|
|
3269
|
+
--radius: 12px;
|
|
3270
|
+
--header: 72px;
|
|
3271
|
+
--nav: 224px;
|
|
3008
3272
|
}
|
|
3009
3273
|
:root[data-theme="dark"] {
|
|
3010
3274
|
color-scheme: dark;
|
|
3011
|
-
--bg: #
|
|
3012
|
-
--bg-elev: #
|
|
3013
|
-
--bg-muted: #
|
|
3014
|
-
--bg-inset: #
|
|
3015
|
-
--line: #
|
|
3016
|
-
--line-strong: #
|
|
3017
|
-
--ink: #
|
|
3018
|
-
--muted: #
|
|
3019
|
-
--faint: #
|
|
3020
|
-
--accent: #
|
|
3021
|
-
--accent-
|
|
3022
|
-
--accent-
|
|
3023
|
-
--
|
|
3024
|
-
--ok
|
|
3025
|
-
--
|
|
3026
|
-
--warn
|
|
3027
|
-
--
|
|
3028
|
-
--err
|
|
3029
|
-
--
|
|
3275
|
+
--bg: #0c0e14;
|
|
3276
|
+
--bg-elev: #151821;
|
|
3277
|
+
--bg-muted: #20232e;
|
|
3278
|
+
--bg-inset: #11141c;
|
|
3279
|
+
--line: #2b303d;
|
|
3280
|
+
--line-strong: #414858;
|
|
3281
|
+
--ink: #f5f7fa;
|
|
3282
|
+
--muted: #b6bdc9;
|
|
3283
|
+
--faint: #8d96a7;
|
|
3284
|
+
--accent: #9e77ed;
|
|
3285
|
+
--accent-hover: #b692f6;
|
|
3286
|
+
--accent-ink: #ffffff;
|
|
3287
|
+
--accent-soft: #2b2142;
|
|
3288
|
+
--ok: #75e0a7;
|
|
3289
|
+
--ok-soft: #102a20;
|
|
3290
|
+
--warn: #fec84b;
|
|
3291
|
+
--warn-soft: #302611;
|
|
3292
|
+
--err: #fda29b;
|
|
3293
|
+
--err-soft: #321817;
|
|
3294
|
+
--code-key: #c3a6ff;
|
|
3295
|
+
--code-string: #75e0a7;
|
|
3296
|
+
--code-number: #84adff;
|
|
3297
|
+
--code-boolean: #f9a8d4;
|
|
3298
|
+
--code-null: #98a2b3;
|
|
3299
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3300
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3301
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
3030
3302
|
}
|
|
3031
3303
|
@media (prefers-color-scheme: dark) {
|
|
3032
3304
|
:root:not([data-theme="light"]) {
|
|
3033
3305
|
color-scheme: dark;
|
|
3034
|
-
--bg: #
|
|
3035
|
-
--bg-elev: #
|
|
3036
|
-
--bg-muted: #
|
|
3037
|
-
--bg-inset: #
|
|
3038
|
-
--line: #
|
|
3039
|
-
--line-strong: #
|
|
3040
|
-
--ink: #
|
|
3041
|
-
--muted: #
|
|
3042
|
-
--faint: #
|
|
3043
|
-
--accent: #
|
|
3044
|
-
--accent-
|
|
3045
|
-
--accent-
|
|
3046
|
-
--
|
|
3047
|
-
--ok
|
|
3048
|
-
--
|
|
3049
|
-
--warn
|
|
3050
|
-
--
|
|
3051
|
-
--err
|
|
3052
|
-
--
|
|
3306
|
+
--bg: #0c0e14;
|
|
3307
|
+
--bg-elev: #151821;
|
|
3308
|
+
--bg-muted: #20232e;
|
|
3309
|
+
--bg-inset: #11141c;
|
|
3310
|
+
--line: #2b303d;
|
|
3311
|
+
--line-strong: #414858;
|
|
3312
|
+
--ink: #f5f7fa;
|
|
3313
|
+
--muted: #b6bdc9;
|
|
3314
|
+
--faint: #8d96a7;
|
|
3315
|
+
--accent: #9e77ed;
|
|
3316
|
+
--accent-hover: #b692f6;
|
|
3317
|
+
--accent-ink: #ffffff;
|
|
3318
|
+
--accent-soft: #2b2142;
|
|
3319
|
+
--ok: #75e0a7;
|
|
3320
|
+
--ok-soft: #102a20;
|
|
3321
|
+
--warn: #fec84b;
|
|
3322
|
+
--warn-soft: #302611;
|
|
3323
|
+
--err: #fda29b;
|
|
3324
|
+
--err-soft: #321817;
|
|
3325
|
+
--code-key: #c3a6ff;
|
|
3326
|
+
--code-string: #75e0a7;
|
|
3327
|
+
--code-number: #84adff;
|
|
3328
|
+
--code-boolean: #f9a8d4;
|
|
3329
|
+
--code-null: #98a2b3;
|
|
3330
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3331
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3332
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
3053
3333
|
}
|
|
3054
3334
|
}
|
|
3055
3335
|
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
@@ -3067,13 +3347,17 @@ var renderAdminDocument = (service) => {
|
|
|
3067
3347
|
padding-right: env(safe-area-inset-right);
|
|
3068
3348
|
-webkit-text-size-adjust: 100%;
|
|
3069
3349
|
}
|
|
3070
|
-
|
|
3350
|
+
::selection { background: color-mix(in srgb, var(--accent) 28%, transparent); }
|
|
3351
|
+
:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px; }
|
|
3352
|
+
button, input, select, textarea { transition: border-color 140ms ease, box-shadow 140ms ease, transform 100ms ease; }
|
|
3353
|
+
button:disabled, input:disabled, select:disabled, textarea:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
3071
3354
|
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
3072
3355
|
header.top {
|
|
3073
|
-
position: sticky; top: 0; z-index:
|
|
3356
|
+
position: sticky; top: 0; z-index: 20;
|
|
3074
3357
|
display: flex; align-items: center; gap: 12px;
|
|
3075
|
-
min-height: var(--header); padding:
|
|
3076
|
-
background: var(--bg-elev);
|
|
3358
|
+
min-height: var(--header); padding: 12px 20px;
|
|
3359
|
+
background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
|
|
3360
|
+
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
3077
3361
|
border-bottom: 1px solid var(--line);
|
|
3078
3362
|
min-width: 0; max-width: 100%;
|
|
3079
3363
|
}
|
|
@@ -3081,12 +3365,21 @@ var renderAdminDocument = (service) => {
|
|
|
3081
3365
|
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
3082
3366
|
min-width: 0; max-width: 100%; margin-right: auto;
|
|
3083
3367
|
}
|
|
3084
|
-
.brand-
|
|
3085
|
-
.brand-
|
|
3368
|
+
.brand-lockup { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
3369
|
+
.brand-mark {
|
|
3370
|
+
width: 38px; height: 38px; display: grid; place-items: center; flex: none;
|
|
3371
|
+
border-radius: 11px; color: #fff;
|
|
3372
|
+
background: linear-gradient(145deg, #7f56d9 0%, #53389e 100%);
|
|
3373
|
+
box-shadow: 0 6px 16px rgb(105 65 198 / 0.24), inset 0 1px 0 rgb(255 255 255 / 0.25);
|
|
3374
|
+
}
|
|
3375
|
+
.brand-mark svg { width: 22px; height: 22px; }
|
|
3376
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; line-height: 1.25; }
|
|
3377
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
3086
3378
|
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
3087
3379
|
.vendor {
|
|
3088
3380
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
3089
3381
|
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
3382
|
+
padding: 5px 8px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-inset);
|
|
3090
3383
|
}
|
|
3091
3384
|
.vendor[hidden] { display: none; }
|
|
3092
3385
|
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
@@ -3098,12 +3391,17 @@ var renderAdminDocument = (service) => {
|
|
|
3098
3391
|
.vendor-sep { color: var(--faint); }
|
|
3099
3392
|
.vendor-link { color: inherit; text-decoration: none; }
|
|
3100
3393
|
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
3101
|
-
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items:
|
|
3102
|
-
|
|
3394
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
|
|
3395
|
+
.sync-status { align-self: center; display: flex; align-items: center; gap: 6px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3396
|
+
.sync-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
|
|
3397
|
+
.sync-status.busy::before { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); animation: pulse 1.2s ease-in-out infinite; }
|
|
3398
|
+
label.field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; font-weight: 600; color: var(--faint); }
|
|
3103
3399
|
label.field input, label.field select, .controls input[type="password"] {
|
|
3104
|
-
background-color: var(--bg-
|
|
3105
|
-
padding:
|
|
3400
|
+
background-color: var(--bg-elev); border: 1px solid var(--line-strong); border-radius: 9px;
|
|
3401
|
+
padding: 8px 10px; min-height: 38px; line-height: 1.2; box-shadow: var(--shadow-xs);
|
|
3106
3402
|
}
|
|
3403
|
+
label.field input:hover, label.field select:hover, textarea:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--line-strong)); }
|
|
3404
|
+
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; }
|
|
3107
3405
|
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
3108
3406
|
the text fields in the same form. Paint it like those fields. */
|
|
3109
3407
|
label.field select {
|
|
@@ -3120,12 +3418,20 @@ var renderAdminDocument = (service) => {
|
|
|
3120
3418
|
dialog .field > textarea { width: 100%; }
|
|
3121
3419
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
3122
3420
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
.btn
|
|
3421
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
|
3422
|
+
border-radius: 9px; min-height: 38px; padding: 0 13px; border: 1px solid var(--line-strong);
|
|
3423
|
+
background: var(--bg-elev); color: var(--ink); font-size: 13px; font-weight: 600; line-height: 1;
|
|
3424
|
+
box-shadow: var(--shadow-xs);
|
|
3425
|
+
}
|
|
3426
|
+
.icon-btn { width: 38px; padding: 0; }
|
|
3427
|
+
.icon-btn svg, .btn svg, .btn-primary svg { width: 16px; height: 16px; }
|
|
3428
|
+
.icon-btn:hover, .btn:hover { background: var(--bg-muted); border-color: var(--line-strong); }
|
|
3429
|
+
.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; }
|
|
3430
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
3431
|
+
.btn-quiet { background: transparent; border-color: transparent; box-shadow: none; }
|
|
3432
|
+
.btn-quiet:hover { background: var(--bg-muted); }
|
|
3433
|
+
.btn-danger { color: var(--err); border-color: color-mix(in srgb, var(--err) 25%, var(--line)); }
|
|
3434
|
+
.btn-danger:hover, .btn-danger.confirming { background: var(--err-soft); border-color: color-mix(in srgb, var(--err) 45%, var(--line)); }
|
|
3129
3435
|
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
3130
3436
|
so the press itself has to stay visible. */
|
|
3131
3437
|
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
@@ -3141,93 +3447,159 @@ var renderAdminDocument = (service) => {
|
|
|
3141
3447
|
flex: 1 0 auto; min-width: 0;
|
|
3142
3448
|
}
|
|
3143
3449
|
nav.side {
|
|
3144
|
-
|
|
3450
|
+
position: sticky; top: var(--header); align-self: start; height: calc(100vh - var(--header));
|
|
3451
|
+
border-right: 1px solid var(--line); padding: 18px 12px;
|
|
3145
3452
|
display: flex; flex-direction: column; gap: 4px;
|
|
3146
|
-
background: var(--bg-elev);
|
|
3453
|
+
background: var(--bg-elev); overflow-y: auto;
|
|
3147
3454
|
}
|
|
3455
|
+
.nav-label { padding: 0 10px 8px; color: var(--faint); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
|
|
3148
3456
|
nav.side button {
|
|
3457
|
+
position: relative; display: flex; align-items: center; gap: 10px; width: 100%;
|
|
3149
3458
|
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
3150
|
-
padding: 9px 10px; min-height:
|
|
3151
|
-
}
|
|
3152
|
-
nav.side button
|
|
3153
|
-
|
|
3154
|
-
.
|
|
3155
|
-
.
|
|
3156
|
-
.
|
|
3459
|
+
padding: 9px 10px; min-height: 42px; color: var(--muted); font-size: 13px; font-weight: 550;
|
|
3460
|
+
}
|
|
3461
|
+
nav.side button:hover { background: var(--bg-muted); color: var(--ink); }
|
|
3462
|
+
nav.side button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 650; }
|
|
3463
|
+
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); }
|
|
3464
|
+
.nav-icon { width: 18px; height: 18px; display: grid; place-items: center; flex: none; }
|
|
3465
|
+
.nav-icon svg { width: 18px; height: 18px; }
|
|
3466
|
+
main { padding: 32px clamp(20px, 4vw, 48px) 64px; max-width: 1320px; width: 100%; min-width: 0; }
|
|
3467
|
+
main[aria-busy="true"] .view { opacity: 0.62; }
|
|
3468
|
+
.view { animation: view-in 180ms ease-out; }
|
|
3469
|
+
.view[hidden] { display: none; }
|
|
3470
|
+
.view-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
|
|
3471
|
+
.view h2 { margin: 0 0 5px; font-size: clamp(24px, 3vw, 30px); line-height: 1.2; letter-spacing: -0.035em; }
|
|
3472
|
+
.view h3 { margin: 24px 0 10px; font-size: 15px; letter-spacing: -0.01em; }
|
|
3473
|
+
.lede { margin: 0; color: var(--muted); max-width: 72ch; font-size: 14px; }
|
|
3474
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 175px), 1fr)); gap: 14px; }
|
|
3157
3475
|
.card {
|
|
3158
3476
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
3159
|
-
padding:
|
|
3477
|
+
padding: 18px; box-shadow: var(--shadow-sm);
|
|
3160
3478
|
}
|
|
3161
|
-
.card
|
|
3162
|
-
.card
|
|
3479
|
+
.stat-card { position: relative; overflow: hidden; }
|
|
3480
|
+
.stat-card::after { content: ""; position: absolute; inset: 0 0 auto; height: 2px; background: linear-gradient(90deg, var(--accent), transparent 78%); opacity: 0.7; }
|
|
3481
|
+
.card .k { color: var(--faint); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
3482
|
+
.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; }
|
|
3163
3483
|
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
3164
|
-
.
|
|
3484
|
+
.section-card { background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-xs); overflow: hidden; }
|
|
3485
|
+
.section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
|
|
3486
|
+
.section-title h3 { margin: 0; }
|
|
3487
|
+
.split { display: grid; grid-template-columns: minmax(0, 250px) minmax(0, 1fr); gap: 18px; align-items: start; }
|
|
3488
|
+
#collections { padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); box-shadow: var(--shadow-xs); }
|
|
3165
3489
|
.collection {
|
|
3166
3490
|
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
3167
|
-
border-radius:
|
|
3491
|
+
border-radius: 9px; padding: 10px 11px; min-height: 48px; color: var(--ink); cursor: pointer;
|
|
3168
3492
|
}
|
|
3169
|
-
.collection
|
|
3493
|
+
.collection:hover { background: var(--bg-muted); }
|
|
3494
|
+
.collection[aria-current="true"] { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 18%, var(--line)); color: var(--accent); }
|
|
3170
3495
|
.collection small { display: block; color: var(--faint); }
|
|
3171
3496
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
3172
|
-
.chip {
|
|
3173
|
-
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
3174
|
-
border-radius: 999px; padding: 4px 8px;
|
|
3175
|
-
}
|
|
3497
|
+
.chip { font-family: var(--mono); font-size: 11px; background: var(--bg-muted); border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; }
|
|
3176
3498
|
.chip i { color: var(--faint); font-style: normal; }
|
|
3177
3499
|
.sql-layout {
|
|
3178
3500
|
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
3179
3501
|
}
|
|
3180
|
-
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
3181
|
-
.sql-tables button { text-align: left; }
|
|
3502
|
+
.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); }
|
|
3503
|
+
.sql-tables button { text-align: left; border: 0; border-radius: 8px; padding: 9px 10px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
3504
|
+
.sql-tables button:hover, .sql-tables button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); }
|
|
3182
3505
|
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
3183
|
-
table { width: 100%; border-
|
|
3184
|
-
|
|
3185
|
-
th {
|
|
3506
|
+
.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); }
|
|
3507
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
3508
|
+
th, td { text-align: left; padding: 11px 13px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
3509
|
+
th { color: var(--faint); background: var(--bg-inset); font-weight: 650; font-size: 11px; text-transform: uppercase; letter-spacing: 0.045em; }
|
|
3510
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
3186
3511
|
tr.clickable { cursor: pointer; }
|
|
3187
|
-
tr.clickable:hover { background: var(--
|
|
3512
|
+
tr.clickable:hover { background: var(--accent-soft); }
|
|
3188
3513
|
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
3189
3514
|
pre {
|
|
3190
3515
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
3191
|
-
background: var(--bg-inset); border-radius: 10px; padding:
|
|
3192
|
-
}
|
|
3193
|
-
.
|
|
3516
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 10px; padding: 14px; font-size: 12px; line-height: 1.65;
|
|
3517
|
+
}
|
|
3518
|
+
.json-key { color: var(--code-key); }
|
|
3519
|
+
.json-string { color: var(--code-string); }
|
|
3520
|
+
.json-number { color: var(--code-number); }
|
|
3521
|
+
.json-boolean { color: var(--code-boolean); }
|
|
3522
|
+
.json-null { color: var(--code-null); font-style: italic; }
|
|
3523
|
+
.json-inline { display: block; max-width: 44rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
3524
|
+
.json-view { position: relative; max-height: 26rem; overflow: auto; }
|
|
3525
|
+
.json-editor { position: relative; min-height: 190px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--bg-inset); overflow: hidden; }
|
|
3526
|
+
.json-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); }
|
|
3527
|
+
.json-editor.invalid { border-color: var(--err); }
|
|
3528
|
+
.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; }
|
|
3529
|
+
.json-editor pre { position: absolute; inset: 0; pointer-events: none; background: transparent; }
|
|
3530
|
+
.json-editor textarea { position: relative; z-index: 1; resize: vertical; background: transparent; color: transparent; caret-color: var(--ink); -webkit-text-fill-color: transparent; }
|
|
3531
|
+
.json-editor textarea::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); }
|
|
3532
|
+
.editor-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; }
|
|
3533
|
+
.json-status { display: inline-flex; align-items: center; gap: 6px; color: var(--ok); font-size: 11px; }
|
|
3534
|
+
.json-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
3535
|
+
.json-status.invalid { color: var(--err); }
|
|
3536
|
+
.format-json { border: 0; padding: 3px 5px; background: transparent; color: var(--accent); font-size: 11px; font-weight: 650; cursor: pointer; }
|
|
3537
|
+
.row-actions { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 14px 0; }
|
|
3194
3538
|
.banner {
|
|
3195
|
-
display: none;
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3539
|
+
display: none; position: sticky; top: calc(var(--header) + 12px); z-index: 15;
|
|
3540
|
+
align-items: center; gap: 10px; margin: 0 0 16px; padding: 11px 12px 11px 14px;
|
|
3541
|
+
border: 1px solid color-mix(in srgb, var(--err) 30%, var(--line)); border-radius: 10px;
|
|
3542
|
+
background: var(--err-soft); color: var(--err); box-shadow: var(--shadow-sm); font-size: 13px;
|
|
3543
|
+
}
|
|
3544
|
+
.banner.show { display: flex; }
|
|
3545
|
+
.banner span { flex: 1; }
|
|
3546
|
+
.banner button { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 7px; background: transparent; color: currentColor; cursor: pointer; }
|
|
3547
|
+
.banner button:hover { background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
3548
|
+
.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); }
|
|
3200
3549
|
dialog {
|
|
3201
|
-
border: 1px solid var(--line); border-radius:
|
|
3202
|
-
color: var(--ink); width: min(
|
|
3203
|
-
}
|
|
3204
|
-
dialog
|
|
3205
|
-
dialog
|
|
3550
|
+
border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--bg-elev);
|
|
3551
|
+
color: var(--ink); width: min(720px, calc(100% - 24px)); max-width: calc(100vw - 24px); box-shadow: var(--shadow-lg);
|
|
3552
|
+
}
|
|
3553
|
+
dialog[open] { animation: dialog-in 160ms ease-out; }
|
|
3554
|
+
dialog::backdrop { background: rgb(12 14 20 / 0.58); backdrop-filter: blur(3px); }
|
|
3555
|
+
dialog form, .dialog-body { padding: 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
3556
|
+
.dialog-heading { display: flex; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
|
3557
|
+
.dialog-heading strong { font-size: 18px; letter-spacing: -0.02em; }
|
|
3558
|
+
.dialog-heading span { color: var(--muted); font-size: 12px; }
|
|
3206
3559
|
textarea {
|
|
3207
3560
|
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
3208
3561
|
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
3209
3562
|
font-family: var(--mono); font-size: 12px;
|
|
3210
3563
|
}
|
|
3211
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
3212
|
-
.
|
|
3564
|
+
.dialog-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding-top: 4px; }
|
|
3565
|
+
.dialog-actions .btn-danger { margin-right: auto; }
|
|
3566
|
+
.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); }
|
|
3213
3567
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
3214
3568
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
3215
3569
|
.status-2 { color: var(--ok); }
|
|
3216
3570
|
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
3217
3571
|
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
3572
|
+
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
3573
|
+
.form-grid .wide { grid-column: span 2; }
|
|
3218
3574
|
.route {
|
|
3219
|
-
display: grid; grid-template-columns:
|
|
3220
|
-
padding:
|
|
3221
|
-
}
|
|
3222
|
-
.
|
|
3575
|
+
display: grid; grid-template-columns: 84px minmax(0, 1fr) auto; gap: 10px; align-items: center;
|
|
3576
|
+
padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--bg-elev);
|
|
3577
|
+
}
|
|
3578
|
+
.route:first-child { border-radius: var(--radius) var(--radius) 0 0; }
|
|
3579
|
+
.route:last-child { border-bottom: 0; border-radius: 0 0 var(--radius) var(--radius); }
|
|
3580
|
+
.route-tools { display: flex; align-items: flex-end; gap: 12px; margin-bottom: 12px; }
|
|
3581
|
+
.route-tools .field { flex: 1; }
|
|
3582
|
+
.route-tools input { width: 100%; }
|
|
3583
|
+
.route-count { padding-bottom: 10px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3584
|
+
#route-list { max-height: 34rem; border: 1px solid var(--line); border-radius: var(--radius); overflow: auto; box-shadow: var(--shadow-xs); }
|
|
3585
|
+
.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; }
|
|
3586
|
+
@keyframes view-in { from { transform: translateY(3px); } to { transform: translateY(0); } }
|
|
3587
|
+
@keyframes dialog-in { from { transform: translateY(8px) scale(0.99); } to { transform: translateY(0) scale(1); } }
|
|
3588
|
+
@keyframes pulse { 50% { opacity: 0.45; } }
|
|
3589
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
|
|
3590
|
+
@media (forced-colors: active) { .json-editor pre { display: none; } .json-editor textarea { color: CanvasText; -webkit-text-fill-color: CanvasText; } }
|
|
3223
3591
|
@media (max-width: 800px) {
|
|
3592
|
+
:root { --header: 0px; }
|
|
3224
3593
|
header.top {
|
|
3225
|
-
align-items: stretch; flex-direction: column;
|
|
3226
|
-
padding
|
|
3594
|
+
position: relative; align-items: stretch; flex-direction: column;
|
|
3595
|
+
padding: max(12px, env(safe-area-inset-top)) 14px 12px;
|
|
3227
3596
|
}
|
|
3228
3597
|
input, select, textarea { font-size: 16px; }
|
|
3229
3598
|
.brand, .vendor, .controls { width: 100%; }
|
|
3599
|
+
.brand { gap: 10px; }
|
|
3600
|
+
.vendor { order: 2; border-radius: 9px; }
|
|
3230
3601
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
3602
|
+
.sync-status { display: none; }
|
|
3231
3603
|
/* The layout fills the screen. A single-column grid stretches every auto
|
|
3232
3604
|
row, so the tab bar grew with the leftover height and the buttons floated
|
|
3233
3605
|
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
@@ -3237,14 +3609,23 @@ var renderAdminDocument = (service) => {
|
|
|
3237
3609
|
align-content: start;
|
|
3238
3610
|
}
|
|
3239
3611
|
nav.side {
|
|
3240
|
-
align-self: start; height: auto;
|
|
3612
|
+
position: sticky; top: 0; z-index: 12; align-self: start; height: auto;
|
|
3241
3613
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
3242
3614
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
3243
|
-
padding: 8px;
|
|
3615
|
+
padding: 8px; box-shadow: var(--shadow-xs);
|
|
3244
3616
|
}
|
|
3245
|
-
nav
|
|
3617
|
+
.nav-label { display: none; }
|
|
3618
|
+
nav.side button { width: auto; white-space: nowrap; flex: none; }
|
|
3619
|
+
nav.side button[aria-current="true"]::before { inset: auto 9px -8px; width: auto; height: 3px; border-radius: 3px 3px 0 0; }
|
|
3246
3620
|
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
3247
|
-
|
|
3621
|
+
.form-grid { grid-template-columns: minmax(0, 1fr); }
|
|
3622
|
+
.form-grid .wide { grid-column: auto; }
|
|
3623
|
+
main { padding: 22px 14px 48px; }
|
|
3624
|
+
.view-head { margin-bottom: 18px; }
|
|
3625
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
3626
|
+
.card { padding: 14px; }
|
|
3627
|
+
.card .v { font-size: 20px; }
|
|
3628
|
+
.table-shell { border: 0; background: transparent; box-shadow: none; overflow: visible; }
|
|
3248
3629
|
table, thead, tbody, tr, th, td { display: block; }
|
|
3249
3630
|
thead { display: none; }
|
|
3250
3631
|
tr {
|
|
@@ -3259,50 +3640,63 @@ var renderAdminDocument = (service) => {
|
|
|
3259
3640
|
.route { grid-template-columns: 72px 1fr; }
|
|
3260
3641
|
.route button { grid-column: 1 / -1; }
|
|
3261
3642
|
}
|
|
3643
|
+
@media (max-width: 460px) {
|
|
3644
|
+
.controls label.field { min-width: calc(50% - 4px); }
|
|
3645
|
+
.controls .icon-btn, .controls .btn { flex: 1; }
|
|
3646
|
+
.grid { grid-template-columns: minmax(0, 1fr); }
|
|
3647
|
+
.dialog-actions { flex-wrap: wrap; }
|
|
3648
|
+
.dialog-actions button { flex: 1; }
|
|
3649
|
+
.dialog-actions .btn-danger { flex-basis: 100%; margin-right: 0; }
|
|
3650
|
+
}
|
|
3262
3651
|
</style>
|
|
3263
3652
|
</head>
|
|
3264
3653
|
<body>
|
|
3265
3654
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
3266
3655
|
<header class="top">
|
|
3267
3656
|
<div class="brand">
|
|
3268
|
-
<div class="brand-
|
|
3269
|
-
<
|
|
3270
|
-
|
|
3657
|
+
<div class="brand-lockup">
|
|
3658
|
+
<div class="brand-mark" aria-hidden="true">
|
|
3659
|
+
<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>
|
|
3660
|
+
</div>
|
|
3661
|
+
<div class="brand-id">
|
|
3662
|
+
<strong>${name}</strong>
|
|
3663
|
+
<span>Mock admin</span>
|
|
3664
|
+
</div>
|
|
3271
3665
|
</div>
|
|
3272
3666
|
<div class="vendor" id="vendor" hidden></div>
|
|
3273
3667
|
</div>
|
|
3274
3668
|
<div class="controls">
|
|
3669
|
+
<span class="sync-status" id="sync-status" role="status" aria-live="polite">Ready</span>
|
|
3275
3670
|
<label class="field">Namespace
|
|
3276
3671
|
<select id="namespace" aria-label="Namespace"></select>
|
|
3277
3672
|
</label>
|
|
3278
3673
|
<label class="field">Admin key
|
|
3279
3674
|
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
3280
3675
|
</label>
|
|
3281
|
-
<button class="icon-btn" id="theme" type="button" aria-label="
|
|
3282
|
-
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
3676
|
+
<button class="icon-btn" id="theme" type="button" aria-label="Use dark theme" title="Use dark theme"></button>
|
|
3677
|
+
<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>
|
|
3283
3678
|
</div>
|
|
3284
3679
|
</header>
|
|
3285
3680
|
<div class="layout">
|
|
3286
3681
|
<nav class="side" id="nav" aria-label="Admin sections">
|
|
3287
|
-
<
|
|
3288
|
-
<button type="button" data-view="
|
|
3289
|
-
<button type="button" data-view="
|
|
3290
|
-
<button type="button" data-view="
|
|
3291
|
-
<button type="button" data-view="
|
|
3292
|
-
<button type="button" data-view="
|
|
3682
|
+
<span class="nav-label">Workspace</span>
|
|
3683
|
+
<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>
|
|
3684
|
+
<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>
|
|
3685
|
+
<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>
|
|
3686
|
+
<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>
|
|
3687
|
+
<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>
|
|
3688
|
+
<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>
|
|
3293
3689
|
</nav>
|
|
3294
3690
|
<main id="views">
|
|
3295
|
-
<
|
|
3691
|
+
<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>
|
|
3296
3692
|
<section class="view" id="view-overview">
|
|
3297
|
-
<h2>Overview</h2>
|
|
3298
|
-
<p class="lede">Health, clock, and the collections this namespace is holding.</p>
|
|
3693
|
+
<div class="view-head"><div><h2>Overview</h2><p class="lede">Health, clock, and the collections this namespace is holding.</p></div></div>
|
|
3299
3694
|
<div class="grid" id="overview-cards"></div>
|
|
3300
3695
|
<h3>Recent requests</h3>
|
|
3301
3696
|
<div id="overview-requests"></div>
|
|
3302
3697
|
</section>
|
|
3303
3698
|
<section class="view" id="view-state" hidden>
|
|
3304
|
-
<h2>State</h2>
|
|
3305
|
-
<p class="lede">Every collection in this namespace. Declared fields stay visible before any row exists; stored rows add whatever else they contain.</p>
|
|
3699
|
+
<div class="view-head"><div><h2>State</h2><p class="lede">Inspect and edit every collection in this namespace.</p></div></div>
|
|
3306
3700
|
<div class="split">
|
|
3307
3701
|
<div id="collections"></div>
|
|
3308
3702
|
<div>
|
|
@@ -3315,8 +3709,7 @@ var renderAdminDocument = (service) => {
|
|
|
3315
3709
|
</div>
|
|
3316
3710
|
</section>
|
|
3317
3711
|
<section class="view" id="view-clock" hidden>
|
|
3318
|
-
<h2>Clock</h2>
|
|
3319
|
-
<p class="lede">The clock every timestamp in this mock reads.</p>
|
|
3712
|
+
<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>
|
|
3320
3713
|
<div class="card" id="clock-readout"></div>
|
|
3321
3714
|
<div class="row-actions">
|
|
3322
3715
|
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
@@ -3335,21 +3728,22 @@ var renderAdminDocument = (service) => {
|
|
|
3335
3728
|
</form>
|
|
3336
3729
|
</section>
|
|
3337
3730
|
<section class="view" id="view-faults" hidden>
|
|
3338
|
-
<h2>Faults</h2>
|
|
3339
|
-
<p class="lede">Inject a response, a delay, or a named preset. Rules apply to this namespace.</p>
|
|
3731
|
+
<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>
|
|
3340
3732
|
<form id="fault-form" class="card stack">
|
|
3341
|
-
<
|
|
3342
|
-
<
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
<
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
<
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
<
|
|
3352
|
-
|
|
3733
|
+
<div class="form-grid">
|
|
3734
|
+
<label class="field wide">Preset
|
|
3735
|
+
<select id="fault-preset"><option value="">Custom rule</option></select>
|
|
3736
|
+
</label>
|
|
3737
|
+
<label class="field">Status
|
|
3738
|
+
<input id="fault-status" inputmode="numeric" placeholder="503">
|
|
3739
|
+
</label>
|
|
3740
|
+
<label class="field wide">Path prefix
|
|
3741
|
+
<input id="fault-path" placeholder="/v1">
|
|
3742
|
+
</label>
|
|
3743
|
+
<label class="field">Method
|
|
3744
|
+
<input id="fault-method" placeholder="GET">
|
|
3745
|
+
</label>
|
|
3746
|
+
</div>
|
|
3353
3747
|
<div class="dialog-actions">
|
|
3354
3748
|
<button class="btn-primary" type="submit">Add fault</button>
|
|
3355
3749
|
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
@@ -3358,26 +3752,31 @@ var renderAdminDocument = (service) => {
|
|
|
3358
3752
|
<div id="fault-list"></div>
|
|
3359
3753
|
</section>
|
|
3360
3754
|
<section class="view" id="view-journal" hidden>
|
|
3361
|
-
<h2>Journal</h2>
|
|
3362
|
-
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
3363
|
-
<div class="row-actions">
|
|
3364
|
-
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
3365
|
-
</div>
|
|
3755
|
+
<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>
|
|
3366
3756
|
<div id="journal"></div>
|
|
3367
3757
|
</section>
|
|
3368
3758
|
<section class="view" id="view-routes" hidden>
|
|
3369
|
-
<h2>Routes</h2>
|
|
3370
|
-
<
|
|
3759
|
+
<div class="view-head"><div><h2>Routes</h2><p class="lede">Explore shared controls and service-specific admin endpoints.</p></div></div>
|
|
3760
|
+
<div class="route-tools">
|
|
3761
|
+
<label class="field">Filter routes
|
|
3762
|
+
<input id="route-search" type="search" placeholder="Method, path, or extension" autocomplete="off">
|
|
3763
|
+
</label>
|
|
3764
|
+
<span class="route-count" id="route-count"></span>
|
|
3765
|
+
</div>
|
|
3371
3766
|
<div id="route-list"></div>
|
|
3372
|
-
<form id="route-form" class="card stack">
|
|
3767
|
+
<form id="route-form" class="card stack" style="margin-top: 18px">
|
|
3373
3768
|
<strong id="route-title">Select a route</strong>
|
|
3374
3769
|
<label class="field">JSON body
|
|
3375
|
-
<
|
|
3770
|
+
<div class="json-editor" id="route-editor" hidden>
|
|
3771
|
+
<pre aria-hidden="true"><code id="route-highlight"></code></pre>
|
|
3772
|
+
<textarea id="route-body" placeholder="{ }" spellcheck="false" aria-describedby="route-json-status" disabled></textarea>
|
|
3773
|
+
</div>
|
|
3774
|
+
<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>
|
|
3376
3775
|
</label>
|
|
3377
3776
|
<div class="dialog-actions">
|
|
3378
|
-
<button class="btn-primary" type="submit">Send</button>
|
|
3777
|
+
<button class="btn-primary" id="route-send" type="submit" disabled>Send request</button>
|
|
3379
3778
|
</div>
|
|
3380
|
-
<pre id="route-result">Response will show here.</pre>
|
|
3779
|
+
<pre class="json-view" id="route-result"><code>Response will show here.</code></pre>
|
|
3381
3780
|
</form>
|
|
3382
3781
|
</section>
|
|
3383
3782
|
</main>
|
|
@@ -3385,12 +3784,16 @@ var renderAdminDocument = (service) => {
|
|
|
3385
3784
|
</div>
|
|
3386
3785
|
<dialog id="editor">
|
|
3387
3786
|
<form method="dialog" id="editor-form">
|
|
3388
|
-
<strong id="editor-title">Record</strong>
|
|
3787
|
+
<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>
|
|
3389
3788
|
<label class="field">Id
|
|
3390
3789
|
<input id="editor-id" required>
|
|
3391
3790
|
</label>
|
|
3392
3791
|
<label class="field">JSON value
|
|
3393
|
-
<
|
|
3792
|
+
<div class="json-editor" id="record-editor">
|
|
3793
|
+
<pre aria-hidden="true"><code id="editor-highlight"></code></pre>
|
|
3794
|
+
<textarea id="editor-value" spellcheck="false" aria-describedby="editor-json-status"></textarea>
|
|
3795
|
+
</div>
|
|
3796
|
+
<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>
|
|
3394
3797
|
</label>
|
|
3395
3798
|
<div class="dialog-actions">
|
|
3396
3799
|
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
@@ -3432,8 +3835,8 @@ var describeExtension = (extension) => {
|
|
|
3432
3835
|
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
3433
3836
|
};
|
|
3434
3837
|
};
|
|
3435
|
-
var adminUiRoutes = (service, ui) => {
|
|
3436
|
-
const shell = () => renderAdminDocument(service);
|
|
3838
|
+
var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
3839
|
+
const shell = () => renderAdminDocument(service, adminPrefix);
|
|
3437
3840
|
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
3438
3841
|
return {
|
|
3439
3842
|
"GET /ui": () => htmlResponse(document3()),
|
|
@@ -4124,10 +4527,13 @@ var stateAdminRoutes = (routes) => {
|
|
|
4124
4527
|
};
|
|
4125
4528
|
|
|
4126
4529
|
// ../core/dist/version.js
|
|
4127
|
-
var PACKAGE_VERSION = true ? "
|
|
4530
|
+
var PACKAGE_VERSION = true ? "2.0.0" : UNRELEASED_VERSION;
|
|
4128
4531
|
|
|
4129
4532
|
// ../core/dist/webhooks.js
|
|
4130
|
-
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4533
|
+
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4534
|
+
status,
|
|
4535
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4536
|
+
});
|
|
4131
4537
|
var adminError3 = (status, message) => json4(status, { error: { type: "mockingbird_admin", message } });
|
|
4132
4538
|
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4133
4539
|
var parseEndpoint = (value) => {
|
|
@@ -4235,7 +4641,6 @@ var AT_HEADER = "x-mockingbird-at";
|
|
|
4235
4641
|
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
4236
4642
|
var DEFAULT_NAMESPACE = "default";
|
|
4237
4643
|
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4238
|
-
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
4239
4644
|
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4240
4645
|
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
4241
4646
|
var effects = /* @__PURE__ */ new WeakMap();
|
|
@@ -4274,6 +4679,22 @@ var operationMatcher = (document3) => {
|
|
|
4274
4679
|
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
4275
4680
|
};
|
|
4276
4681
|
var createRuntime = (options) => {
|
|
4682
|
+
const adminPrefix = resolveAdminPrefix(options.adminPrefix);
|
|
4683
|
+
const internalPaths = Object.entries(options.document?.paths ?? {}).filter(([, item]) => item["x-mockingbird-internal"] === true).map(([path]) => path);
|
|
4684
|
+
for (const path of internalPaths) {
|
|
4685
|
+
if (!path.startsWith(`${ADMIN_PREFIX}/blobs/`))
|
|
4686
|
+
throw new Error(`internal transport ${path} must live under ${ADMIN_PREFIX}/blobs/`);
|
|
4687
|
+
}
|
|
4688
|
+
assertAdminPrefixAvailable(adminPrefix, Object.keys(options.document?.paths ?? {}).filter((path) => !internalPaths.includes(path)));
|
|
4689
|
+
const internalMatch = (path) => {
|
|
4690
|
+
for (const pattern of internalPaths) {
|
|
4691
|
+
const actual = path.split("/");
|
|
4692
|
+
const expected = pattern.split("/");
|
|
4693
|
+
if (actual.length === expected.length && expected.every((part, i) => part.startsWith("{") || part === actual[i]))
|
|
4694
|
+
return pattern;
|
|
4695
|
+
}
|
|
4696
|
+
return void 0;
|
|
4697
|
+
};
|
|
4277
4698
|
assertAdminUi(options.adminUi);
|
|
4278
4699
|
const sqlite = bootSqlite(options.sqlite);
|
|
4279
4700
|
const clock = options.clock ?? createClock();
|
|
@@ -4288,6 +4709,20 @@ var createRuntime = (options) => {
|
|
|
4288
4709
|
const instances = /* @__PURE__ */ new Map();
|
|
4289
4710
|
const publicNamespaces = /* @__PURE__ */ new Set();
|
|
4290
4711
|
const branchRngs = /* @__PURE__ */ new Map();
|
|
4712
|
+
const namespaceClocks = /* @__PURE__ */ new Map();
|
|
4713
|
+
const namespaceForStorage = /* @__PURE__ */ new Map();
|
|
4714
|
+
let isolatedNamespaces = false;
|
|
4715
|
+
const namespaceClock = (name = DEFAULT_NAMESPACE) => {
|
|
4716
|
+
if (!isolatedNamespaces || name === DEFAULT_NAMESPACE)
|
|
4717
|
+
return clock;
|
|
4718
|
+
let found = namespaceClocks.get(name);
|
|
4719
|
+
if (!found) {
|
|
4720
|
+
found = createClock();
|
|
4721
|
+
namespaceClocks.set(name, found);
|
|
4722
|
+
}
|
|
4723
|
+
return found;
|
|
4724
|
+
};
|
|
4725
|
+
const fleetPoints = /* @__PURE__ */ new WeakMap();
|
|
4291
4726
|
const timelines = /* @__PURE__ */ new Map();
|
|
4292
4727
|
const branchStorage = /* @__PURE__ */ new Map();
|
|
4293
4728
|
const captured = /* @__PURE__ */ new Map();
|
|
@@ -4301,17 +4736,22 @@ var createRuntime = (options) => {
|
|
|
4301
4736
|
if (!NAMESPACE_PATTERN.test(key) || !NAMESPACE_PATTERN.test(publicNamespace)) {
|
|
4302
4737
|
throw new RangeError(`namespace must match ${NAMESPACE_PATTERN}: ${JSON.stringify(publicNamespace)}`);
|
|
4303
4738
|
}
|
|
4739
|
+
const createdContextRng = isolatedRng ?? (isolatedNamespaces && key !== DEFAULT_NAMESPACE ? createRng(options.seed ?? 0) : rng);
|
|
4304
4740
|
const created = options.create({
|
|
4305
4741
|
namespace: storageNamespace(key),
|
|
4306
4742
|
publicNamespace,
|
|
4743
|
+
adminPrefix,
|
|
4307
4744
|
sqlite,
|
|
4308
|
-
clock,
|
|
4309
|
-
rng:
|
|
4745
|
+
clock: namespaceClock(publicNamespace),
|
|
4746
|
+
rng: createdContextRng
|
|
4310
4747
|
});
|
|
4311
4748
|
instances.set(key, created);
|
|
4749
|
+
namespaceForStorage.set(key, publicNamespace);
|
|
4312
4750
|
publicNamespaces.add(publicNamespace);
|
|
4313
4751
|
if (isolatedRng)
|
|
4314
4752
|
branchRngs.set(key, isolatedRng);
|
|
4753
|
+
else if (isolatedNamespaces && key !== DEFAULT_NAMESPACE)
|
|
4754
|
+
branchRngs.set(key, createdContextRng);
|
|
4315
4755
|
return created;
|
|
4316
4756
|
};
|
|
4317
4757
|
const instance = (name = DEFAULT_NAMESPACE) => instanceFor(name);
|
|
@@ -4329,7 +4769,7 @@ var createRuntime = (options) => {
|
|
|
4329
4769
|
captured.set(storage, snapshot2);
|
|
4330
4770
|
return Object.freeze({
|
|
4331
4771
|
snapshot: snapshot2,
|
|
4332
|
-
clock: Object.freeze(
|
|
4772
|
+
clock: Object.freeze(namespaceClock(namespaceForStorage.get(storage) ?? storage).state()),
|
|
4333
4773
|
rngState: (branchRngs.get(storage) ?? rng).state()
|
|
4334
4774
|
});
|
|
4335
4775
|
};
|
|
@@ -4339,7 +4779,7 @@ var createRuntime = (options) => {
|
|
|
4339
4779
|
return found;
|
|
4340
4780
|
instance(name);
|
|
4341
4781
|
found = new Timeline({
|
|
4342
|
-
now:
|
|
4782
|
+
now: namespaceClock(name).now,
|
|
4343
4783
|
...options.maxCheckpoints !== void 0 ? { maxCheckpoints: options.maxCheckpoints } : {}
|
|
4344
4784
|
});
|
|
4345
4785
|
found.commit(capture(name));
|
|
@@ -4366,12 +4806,13 @@ var createRuntime = (options) => {
|
|
|
4366
4806
|
const point = history2.checkout("main", at);
|
|
4367
4807
|
restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
|
|
4368
4808
|
captured.set(namespace, point.value.snapshot);
|
|
4369
|
-
rng.setState(point.value.rngState);
|
|
4370
|
-
|
|
4809
|
+
(branchRngs.get(namespace) ?? rng).setState(point.value.rngState);
|
|
4810
|
+
const selectedClock = namespaceClock(namespace);
|
|
4811
|
+
selectedClock.set(point.value.clock.now);
|
|
4371
4812
|
if (point.value.clock.frozen)
|
|
4372
|
-
|
|
4813
|
+
selectedClock.freeze();
|
|
4373
4814
|
else
|
|
4374
|
-
|
|
4815
|
+
selectedClock.unfreeze();
|
|
4375
4816
|
}
|
|
4376
4817
|
return namespace;
|
|
4377
4818
|
}
|
|
@@ -4429,11 +4870,12 @@ var createRuntime = (options) => {
|
|
|
4429
4870
|
const storage = ensureBranch(namespace, branchName);
|
|
4430
4871
|
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
4431
4872
|
captured.set(storage, point.value.snapshot);
|
|
4432
|
-
|
|
4873
|
+
const selectedClock = namespaceClock(namespace);
|
|
4874
|
+
selectedClock.set(point.value.clock.now);
|
|
4433
4875
|
if (point.value.clock.frozen)
|
|
4434
|
-
|
|
4876
|
+
selectedClock.freeze();
|
|
4435
4877
|
else
|
|
4436
|
-
|
|
4878
|
+
selectedClock.unfreeze();
|
|
4437
4879
|
(branchRngs.get(storage) ?? rng).setState(point.value.rngState);
|
|
4438
4880
|
};
|
|
4439
4881
|
const reset = async (name = DEFAULT_NAMESPACE) => {
|
|
@@ -4441,10 +4883,21 @@ var createRuntime = (options) => {
|
|
|
4441
4883
|
options.webhooks?.clear();
|
|
4442
4884
|
for (const each of instances.values())
|
|
4443
4885
|
await each.reset();
|
|
4886
|
+
for (const each of branchRngs.values())
|
|
4887
|
+
each.reset();
|
|
4444
4888
|
timelines.clear();
|
|
4889
|
+
for (const storage of branchStorage.values()) {
|
|
4890
|
+
instances.delete(storage);
|
|
4891
|
+
branchRngs.delete(storage);
|
|
4892
|
+
namespaceForStorage.delete(storage);
|
|
4893
|
+
}
|
|
4445
4894
|
branchStorage.clear();
|
|
4446
|
-
branchRngs.clear();
|
|
4447
4895
|
captured.clear();
|
|
4896
|
+
faults.clear();
|
|
4897
|
+
clock.reset();
|
|
4898
|
+
rng.reset();
|
|
4899
|
+
for (const each of namespaceClocks.values())
|
|
4900
|
+
each.reset();
|
|
4448
4901
|
return;
|
|
4449
4902
|
}
|
|
4450
4903
|
options.webhooks?.clear(name);
|
|
@@ -4462,11 +4915,18 @@ var createRuntime = (options) => {
|
|
|
4462
4915
|
else
|
|
4463
4916
|
clearNamespace(sqlite, storageNamespace(storage));
|
|
4464
4917
|
branchStorage.delete(mapping);
|
|
4918
|
+
instances.delete(storage);
|
|
4919
|
+
namespaceForStorage.delete(storage);
|
|
4465
4920
|
branchRngs.delete(storage);
|
|
4466
4921
|
captured.delete(storage);
|
|
4467
4922
|
}
|
|
4468
4923
|
timelines.delete(name);
|
|
4469
4924
|
captured.delete(name);
|
|
4925
|
+
faults.restore(name, { rules: [], rngState: createRng(options.seed ?? 0).state() });
|
|
4926
|
+
namespaceClock(name).reset();
|
|
4927
|
+
(branchRngs.get(name) ?? rng).reset();
|
|
4928
|
+
journal.clear(name);
|
|
4929
|
+
metrics.reset(name);
|
|
4470
4930
|
};
|
|
4471
4931
|
const snapshot = (name = DEFAULT_NAMESPACE) => {
|
|
4472
4932
|
return checkpoint(name, "main").value.snapshot;
|
|
@@ -4504,6 +4964,53 @@ var createRuntime = (options) => {
|
|
|
4504
4964
|
rng,
|
|
4505
4965
|
credentials,
|
|
4506
4966
|
webhooks: options.webhooks,
|
|
4967
|
+
isolateNamespaces() {
|
|
4968
|
+
if (publicNamespaces.size > 1)
|
|
4969
|
+
throw new Error("enable namespace isolation before creating named namespaces");
|
|
4970
|
+
isolatedNamespaces = true;
|
|
4971
|
+
faults.isolateNamespaces();
|
|
4972
|
+
},
|
|
4973
|
+
namespaceClock,
|
|
4974
|
+
namespaceOf(request) {
|
|
4975
|
+
const url = new URL(request.url);
|
|
4976
|
+
const prefixed = matchNamespacePath(url.pathname, adminPrefix);
|
|
4977
|
+
const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
|
|
4978
|
+
const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
|
|
4979
|
+
if (isAdminPath(path, adminPrefix))
|
|
4980
|
+
return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
|
|
4981
|
+
if (selected !== void 0)
|
|
4982
|
+
return selected;
|
|
4983
|
+
const credential = options.credential?.(request);
|
|
4984
|
+
return (credential !== void 0 ? credentials.get(credential) : void 0) ?? DEFAULT_NAMESPACE;
|
|
4985
|
+
},
|
|
4986
|
+
fleetSnapshot(namespace) {
|
|
4987
|
+
instance(namespace);
|
|
4988
|
+
const handle = {};
|
|
4989
|
+
fleetPoints.set(handle, {
|
|
4990
|
+
namespace,
|
|
4991
|
+
state: capture(namespace),
|
|
4992
|
+
faults: faults.snapshot(namespace),
|
|
4993
|
+
webhooks: options.webhooks?.snapshot(namespace)
|
|
4994
|
+
});
|
|
4995
|
+
return handle;
|
|
4996
|
+
},
|
|
4997
|
+
fleetRestore(handle, namespace) {
|
|
4998
|
+
const point = typeof handle === "object" && handle !== null ? fleetPoints.get(handle) : void 0;
|
|
4999
|
+
if (!point || point.namespace !== namespace)
|
|
5000
|
+
throw new Error("invalid fleet checkpoint");
|
|
5001
|
+
restoreNamespace(sqlite, storageNamespace(namespace), point.state.snapshot);
|
|
5002
|
+
captured.set(namespace, point.state.snapshot);
|
|
5003
|
+
const selectedClock = namespaceClock(namespace);
|
|
5004
|
+
selectedClock.freeze();
|
|
5005
|
+
selectedClock.set(point.state.clock.now);
|
|
5006
|
+
if (!point.state.clock.frozen)
|
|
5007
|
+
selectedClock.unfreeze();
|
|
5008
|
+
(branchRngs.get(namespace) ?? rng).setState(point.state.rngState);
|
|
5009
|
+
faults.restore(namespace, point.faults);
|
|
5010
|
+
if (point.webhooks !== void 0)
|
|
5011
|
+
options.webhooks?.restore(namespace, point.webhooks);
|
|
5012
|
+
timelines.delete(namespace);
|
|
5013
|
+
},
|
|
4507
5014
|
applyPreset: (name, namespace = DEFAULT_NAMESPACE, overrides = {}) => {
|
|
4508
5015
|
const preset = options.presets?.[name];
|
|
4509
5016
|
if (!preset)
|
|
@@ -4535,7 +5042,7 @@ var createRuntime = (options) => {
|
|
|
4535
5042
|
state: (namespace = DEFAULT_NAMESPACE) => inspectState(stateScope(namespace)),
|
|
4536
5043
|
fetch: async (incoming) => {
|
|
4537
5044
|
let request = incoming;
|
|
4538
|
-
const prefixed =
|
|
5045
|
+
const prefixed = matchNamespacePath(new URL(request.url).pathname, adminPrefix);
|
|
4539
5046
|
if (prefixed) {
|
|
4540
5047
|
const url2 = new URL(request.url);
|
|
4541
5048
|
url2.pathname = prefixed[2] ?? "/";
|
|
@@ -4551,7 +5058,15 @@ var createRuntime = (options) => {
|
|
|
4551
5058
|
signal: request.signal
|
|
4552
5059
|
});
|
|
4553
5060
|
}
|
|
4554
|
-
|
|
5061
|
+
const internalUrl = new URL(request.url);
|
|
5062
|
+
const internalPath = isAdminPath(internalUrl.pathname, adminPrefix) ? `${ADMIN_PREFIX}${internalUrl.pathname.slice(adminPrefix.length)}` : "";
|
|
5063
|
+
const internal = internalMatch(internalPath);
|
|
5064
|
+
const obsoleteInternalPath = adminPrefix !== ADMIN_PREFIX && internalMatch(internalUrl.pathname) && !internal;
|
|
5065
|
+
if (internal) {
|
|
5066
|
+
internalUrl.pathname = internalPath;
|
|
5067
|
+
request = new Request(internalUrl, request);
|
|
5068
|
+
}
|
|
5069
|
+
let namespace = internal ? internalUrl.searchParams.get("namespace") ?? control.namespaceOf(request) : control.namespaceOf(request);
|
|
4555
5070
|
if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
4556
5071
|
const credential = options.credential(request);
|
|
4557
5072
|
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
@@ -4571,7 +5086,9 @@ var createRuntime = (options) => {
|
|
|
4571
5086
|
return copy;
|
|
4572
5087
|
}
|
|
4573
5088
|
};
|
|
4574
|
-
|
|
5089
|
+
if (obsoleteInternalPath)
|
|
5090
|
+
return stamp(adminFail(404, "internal transport is only available under adminPrefix"));
|
|
5091
|
+
const handled = internal ? void 0 : await control.handle(request);
|
|
4575
5092
|
if (handled)
|
|
4576
5093
|
return stamp(handled);
|
|
4577
5094
|
const started = monotonicNow();
|
|
@@ -4612,7 +5129,7 @@ var createRuntime = (options) => {
|
|
|
4612
5129
|
type: "mockingbird_admin",
|
|
4613
5130
|
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
4614
5131
|
}
|
|
4615
|
-
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
5132
|
+
}), { status: 400, headers: { "content-type": "application/json; charset=utf-8" } }));
|
|
4616
5133
|
}
|
|
4617
5134
|
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
4618
5135
|
log(400);
|
|
@@ -4679,6 +5196,7 @@ var createRuntime = (options) => {
|
|
|
4679
5196
|
startedAt: wallNow(),
|
|
4680
5197
|
wallNow,
|
|
4681
5198
|
clock,
|
|
5199
|
+
namespaceClock,
|
|
4682
5200
|
faults,
|
|
4683
5201
|
metrics,
|
|
4684
5202
|
journal,
|
|
@@ -4728,15 +5246,16 @@ var createRuntime = (options) => {
|
|
|
4728
5246
|
...options.admin?.(runtime) ?? {},
|
|
4729
5247
|
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
4730
5248
|
// maps `presets`, and a service route must not replace that list with a record.
|
|
4731
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
5249
|
+
...options.presets ? presetRoutes(options.presets, runtime, adminPrefix) : {},
|
|
4732
5250
|
...stateAdminRoutes({
|
|
4733
5251
|
open: stateScope,
|
|
4734
5252
|
afterWrite: (namespace) => {
|
|
4735
5253
|
checkpoint(namespace, "main");
|
|
4736
5254
|
}
|
|
4737
5255
|
}),
|
|
4738
|
-
...adminUiRoutes(options.name, options.adminUi)
|
|
5256
|
+
...adminUiRoutes(options.name, options.adminUi, adminPrefix)
|
|
4739
5257
|
},
|
|
5258
|
+
adminPrefix,
|
|
4740
5259
|
adminKey: options.adminKey
|
|
4741
5260
|
});
|
|
4742
5261
|
return defineMock(runtime);
|
|
@@ -4750,7 +5269,10 @@ var mutableResponse = (response) => {
|
|
|
4750
5269
|
return new Response(response.body, response);
|
|
4751
5270
|
}
|
|
4752
5271
|
};
|
|
4753
|
-
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5272
|
+
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5273
|
+
status,
|
|
5274
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
5275
|
+
});
|
|
4754
5276
|
var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
|
|
4755
5277
|
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4756
5278
|
var credentialRoutes = (registry) => ({
|
|
@@ -4802,7 +5324,7 @@ var credentialRoutes = (registry) => ({
|
|
|
4802
5324
|
return adminJson(200, { status: "ok" });
|
|
4803
5325
|
}
|
|
4804
5326
|
});
|
|
4805
|
-
var presetRoutes = (presets, runtime) => ({
|
|
5327
|
+
var presetRoutes = (presets, runtime, adminPrefix) => ({
|
|
4806
5328
|
"GET /faults/presets": () => {
|
|
4807
5329
|
const body = {
|
|
4808
5330
|
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
@@ -4812,7 +5334,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
4812
5334
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
4813
5335
|
const name = params.name;
|
|
4814
5336
|
if (!presets[name])
|
|
4815
|
-
return adminFail(404, `no fault preset ${name}; GET /
|
|
5337
|
+
return adminFail(404, `no fault preset ${name}; GET ${adminPrefix}/faults/presets`);
|
|
4816
5338
|
const overrides = isObject(body) ? body : {};
|
|
4817
5339
|
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
4818
5340
|
}
|
|
@@ -4977,6 +5499,7 @@ var createRuntime2 = (options = {}) => createRuntime({
|
|
|
4977
5499
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
4978
5500
|
...options.clock ? { clock: options.clock } : {},
|
|
4979
5501
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
5502
|
+
...options.adminPrefix !== void 0 ? { adminPrefix: options.adminPrefix } : {},
|
|
4980
5503
|
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
4981
5504
|
...options.onLog ? { onLog: options.onLog } : {},
|
|
4982
5505
|
credential: apiKeyCredential,
|
|
@@ -5215,6 +5738,9 @@ var EasyPostAPI = class {
|
|
|
5215
5738
|
};
|
|
5216
5739
|
|
|
5217
5740
|
export {
|
|
5741
|
+
createClock,
|
|
5742
|
+
resolveAdminPrefix,
|
|
5743
|
+
parseDuration,
|
|
5218
5744
|
document2 as document,
|
|
5219
5745
|
operationIds,
|
|
5220
5746
|
supportedOperationIds,
|
|
@@ -5229,4 +5755,4 @@ export {
|
|
|
5229
5755
|
isTrackerStatus,
|
|
5230
5756
|
EasyPostAPI
|
|
5231
5757
|
};
|
|
5232
|
-
//# sourceMappingURL=chunk-
|
|
5758
|
+
//# sourceMappingURL=chunk-32BNNR7Z.js.map
|