@crvouga/mockingbird-service-slack 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 +31 -0
- package/dist/{chunk-73BP3FZK.js → chunk-56E5SPJ7.js} +791 -262
- package/dist/chunk-56E5SPJ7.js.map +7 -0
- package/dist/{chunk-LOAZPGDT.js → chunk-PVVAIER7.js} +518 -31
- package/dist/chunk-PVVAIER7.js.map +7 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +35 -5
- package/dist/index.js +1 -1
- package/dist/server.d.ts +64 -4
- package/dist/server.js +2 -2
- package/openapi.yaml +1945 -0
- package/package.json +29 -4
- package/dist/chunk-73BP3FZK.js.map +0 -7
- package/dist/chunk-LOAZPGDT.js.map +0 -7
|
@@ -110,13 +110,31 @@ var Collection = class {
|
|
|
110
110
|
};
|
|
111
111
|
|
|
112
112
|
// ../core/dist/control.js
|
|
113
|
-
var HEALTH_PATH = "/health";
|
|
114
113
|
var ADMIN_PREFIX = "/__admin";
|
|
114
|
+
var resolveAdminPrefix = (prefix = ADMIN_PREFIX) => {
|
|
115
|
+
if (!/^\/[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*$/.test(prefix)) {
|
|
116
|
+
throw new Error("adminPrefix must be an absolute non-root path of literal segments, without a trailing slash");
|
|
117
|
+
}
|
|
118
|
+
return prefix;
|
|
119
|
+
};
|
|
120
|
+
var isAdminPath = (path, prefix) => path === prefix || path.startsWith(`${prefix}/`);
|
|
121
|
+
var matchNamespacePath = (path, prefix) => /^\/ns\/([^/]+)(\/.*)?$/.exec(isAdminPath(path, prefix) ? path.slice(prefix.length) : "");
|
|
122
|
+
var assertAdminPrefixAvailable = (prefix, paths) => {
|
|
123
|
+
const reserved = prefix.split("/").slice(1);
|
|
124
|
+
for (const path of paths) {
|
|
125
|
+
const segments = path.split("/").slice(1);
|
|
126
|
+
if (segments.length < reserved.length || /^\{[^}]+\}$/.test(segments[0] ?? ""))
|
|
127
|
+
continue;
|
|
128
|
+
if (reserved.every((part, i) => segments[i] === part || /^\{[^}]+\}$/.test(segments[i] ?? ""))) {
|
|
129
|
+
throw new Error(`vendor route ${path} collides with adminPrefix ${prefix}; choose a different adminPrefix`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
115
133
|
var ADMIN_KEY_HEADER = "x-mockingbird-admin-key";
|
|
116
134
|
var NAMESPACE_HEADER = "x-mockingbird-namespace";
|
|
117
135
|
var json = (status, body) => new Response(JSON.stringify(body), {
|
|
118
136
|
status,
|
|
119
|
-
headers: { "content-type": "application/json" }
|
|
137
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
120
138
|
});
|
|
121
139
|
var adminError = (status, message) => json(status, { error: { type: "mockingbird_admin", message } });
|
|
122
140
|
var UNITS = {
|
|
@@ -168,11 +186,25 @@ var readJson = async (request) => {
|
|
|
168
186
|
};
|
|
169
187
|
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
170
188
|
var createControlPlane = (context) => {
|
|
189
|
+
const adminPrefix = resolveAdminPrefix(context.adminPrefix);
|
|
171
190
|
const snapshots = /* @__PURE__ */ new Map();
|
|
172
191
|
let snapshotCounter = 0;
|
|
173
192
|
const headerNamespace = (request) => request.headers.get(NAMESPACE_HEADER) ?? context.defaultNamespace;
|
|
174
193
|
const adminNamespace = (request, url) => url.searchParams.get("namespace") ?? headerNamespace(request);
|
|
194
|
+
const health = () => {
|
|
195
|
+
return json(200, {
|
|
196
|
+
...context.describe(),
|
|
197
|
+
status: "ok",
|
|
198
|
+
service: context.name,
|
|
199
|
+
uptimeMs: context.wallNow() - context.startedAt,
|
|
200
|
+
clock: context.clock.state(),
|
|
201
|
+
namespaces: context.namespaces().length,
|
|
202
|
+
adminPrefix,
|
|
203
|
+
adminUi: `${adminPrefix}/ui`
|
|
204
|
+
});
|
|
205
|
+
};
|
|
175
206
|
const builtin = {
|
|
207
|
+
"GET /health": health,
|
|
176
208
|
"GET /": () => json(200, {
|
|
177
209
|
service: context.name,
|
|
178
210
|
routes: [...Object.keys(builtin), ...Object.keys(context.routes)].sort()
|
|
@@ -183,29 +215,30 @@ var createControlPlane = (context) => {
|
|
|
183
215
|
return json(200, { status: "ok", reset: target === "*" ? context.namespaces() : [target] });
|
|
184
216
|
},
|
|
185
217
|
"GET /namespaces": () => json(200, { default: context.defaultNamespace, namespaces: context.namespaces() }),
|
|
186
|
-
"GET /clock": () => json(200, context.clock.state()),
|
|
187
|
-
"POST /clock": ({ body }) => {
|
|
218
|
+
"GET /clock": ({ namespace }) => json(200, (context.namespaceClock?.(namespace) ?? context.clock).state()),
|
|
219
|
+
"POST /clock": ({ body, namespace }) => {
|
|
220
|
+
const clock = context.namespaceClock?.(namespace) ?? context.clock;
|
|
188
221
|
if (!isRecord(body))
|
|
189
222
|
return adminError(400, "expected a JSON object");
|
|
190
223
|
if (body.reset === true)
|
|
191
|
-
|
|
224
|
+
clock.reset();
|
|
192
225
|
if (body.freeze === true)
|
|
193
|
-
|
|
226
|
+
clock.freeze();
|
|
194
227
|
if (body.set !== void 0) {
|
|
195
228
|
const instant = parseInstant(body.set);
|
|
196
229
|
if (instant === void 0)
|
|
197
230
|
return adminError(400, "set: expected epoch ms or ISO-8601");
|
|
198
|
-
|
|
231
|
+
clock.set(instant);
|
|
199
232
|
}
|
|
200
233
|
if (body.advance !== void 0) {
|
|
201
234
|
const delta = parseDuration(body.advance);
|
|
202
235
|
if (delta === void 0)
|
|
203
236
|
return adminError(400, 'advance: expected ms or "15m"-style');
|
|
204
|
-
|
|
237
|
+
clock.advance(delta);
|
|
205
238
|
}
|
|
206
239
|
if (body.freeze === false)
|
|
207
|
-
|
|
208
|
-
return json(200,
|
|
240
|
+
clock.unfreeze();
|
|
241
|
+
return json(200, clock.state());
|
|
209
242
|
},
|
|
210
243
|
"GET /faults": () => json(200, { faults: context.faults.list() }),
|
|
211
244
|
"POST /faults": ({ body, namespace }) => {
|
|
@@ -338,7 +371,14 @@ var createControlPlane = (context) => {
|
|
|
338
371
|
return json(200, { status: "ok" });
|
|
339
372
|
}
|
|
340
373
|
};
|
|
341
|
-
const
|
|
374
|
+
for (const key of Object.keys(context.routes)) {
|
|
375
|
+
if (Object.hasOwn(builtin, key))
|
|
376
|
+
throw new Error(`admin route ${key} overrides a shared route`);
|
|
377
|
+
const path = key.slice(key.indexOf(" ") + 1);
|
|
378
|
+
if (isAdminPath(path, "/ns") || isAdminPath(path, "/blobs"))
|
|
379
|
+
throw new Error(`admin route ${key} overlaps an internal transport`);
|
|
380
|
+
}
|
|
381
|
+
const routes = [...Object.entries(builtin), ...Object.entries(context.routes)].map(([key, handler]) => {
|
|
342
382
|
const space = key.indexOf(" ");
|
|
343
383
|
return { method: key.slice(0, space), pattern: key.slice(space + 1), handler };
|
|
344
384
|
});
|
|
@@ -346,21 +386,13 @@ var createControlPlane = (context) => {
|
|
|
346
386
|
namespaceOf: headerNamespace,
|
|
347
387
|
async handle(request) {
|
|
348
388
|
const url = new URL(request.url);
|
|
349
|
-
if (url.pathname ===
|
|
350
|
-
return
|
|
351
|
-
status: "ok",
|
|
352
|
-
service: context.name,
|
|
353
|
-
uptimeMs: context.wallNow() - context.startedAt,
|
|
354
|
-
clock: context.clock.state(),
|
|
355
|
-
namespaces: context.namespaces().length,
|
|
356
|
-
adminUi: `${ADMIN_PREFIX}/ui`,
|
|
357
|
-
...context.describe()
|
|
358
|
-
});
|
|
389
|
+
if (url.pathname === `${adminPrefix}/health` && request.method === "GET") {
|
|
390
|
+
return health();
|
|
359
391
|
}
|
|
360
|
-
if (url.pathname
|
|
392
|
+
if (!isAdminPath(url.pathname, adminPrefix)) {
|
|
361
393
|
return void 0;
|
|
362
394
|
}
|
|
363
|
-
const path = url.pathname.slice(
|
|
395
|
+
const path = url.pathname.slice(adminPrefix.length) || "/";
|
|
364
396
|
const shell = request.method === "GET" && (path === "/ui" || path === "/ui/");
|
|
365
397
|
if (!shell && context.adminKey !== void 0 && request.headers.get(ADMIN_KEY_HEADER) !== context.adminKey) {
|
|
366
398
|
return adminError(401, `missing or wrong ${ADMIN_KEY_HEADER}`);
|
|
@@ -368,7 +400,12 @@ var createControlPlane = (context) => {
|
|
|
368
400
|
for (const route of routes) {
|
|
369
401
|
if (route.method !== request.method)
|
|
370
402
|
continue;
|
|
371
|
-
|
|
403
|
+
let params;
|
|
404
|
+
try {
|
|
405
|
+
params = matchRoute(route.pattern, path);
|
|
406
|
+
} catch {
|
|
407
|
+
return adminError(400, "invalid path encoding");
|
|
408
|
+
}
|
|
372
409
|
if (!params)
|
|
373
410
|
continue;
|
|
374
411
|
let body;
|
|
@@ -377,15 +414,18 @@ var createControlPlane = (context) => {
|
|
|
377
414
|
} catch {
|
|
378
415
|
return adminError(400, "request body is not valid JSON");
|
|
379
416
|
}
|
|
417
|
+
const namespace = adminNamespace(request, url);
|
|
418
|
+
if (!/^[A-Za-z0-9_.-]{1,64}$/.test(namespace))
|
|
419
|
+
return adminError(400, "invalid namespace");
|
|
380
420
|
return route.handler({
|
|
381
421
|
request,
|
|
382
422
|
url,
|
|
383
423
|
params,
|
|
384
|
-
namespace
|
|
424
|
+
namespace,
|
|
385
425
|
body
|
|
386
426
|
});
|
|
387
427
|
}
|
|
388
|
-
return adminError(404, `no admin route ${request.method} ${path}; GET ${
|
|
428
|
+
return adminError(404, `no admin route ${request.method} ${path}; GET ${adminPrefix} lists them`);
|
|
389
429
|
}
|
|
390
430
|
};
|
|
391
431
|
};
|
|
@@ -474,7 +514,33 @@ var faultResponse = (rule) => {
|
|
|
474
514
|
};
|
|
475
515
|
var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) => {
|
|
476
516
|
const entries = [];
|
|
517
|
+
let isolated = false;
|
|
518
|
+
const scopedRngs = /* @__PURE__ */ new Map();
|
|
519
|
+
const random = (namespace) => {
|
|
520
|
+
if (!isolated || namespace === "default")
|
|
521
|
+
return rng;
|
|
522
|
+
let scoped = scopedRngs.get(namespace);
|
|
523
|
+
if (!scoped) {
|
|
524
|
+
scoped = createRng(rng.seed);
|
|
525
|
+
scopedRngs.set(namespace, scoped);
|
|
526
|
+
}
|
|
527
|
+
return scoped;
|
|
528
|
+
};
|
|
477
529
|
return {
|
|
530
|
+
isolateNamespaces() {
|
|
531
|
+
isolated = true;
|
|
532
|
+
},
|
|
533
|
+
snapshot(namespace) {
|
|
534
|
+
return {
|
|
535
|
+
rules: structuredClone(entries.map((entry, position) => ({
|
|
536
|
+
...entry.rule,
|
|
537
|
+
remaining: entry.remaining,
|
|
538
|
+
hits: entry.hits,
|
|
539
|
+
position
|
|
540
|
+
})).filter((entry) => entry.namespace === namespace)),
|
|
541
|
+
rngState: random(namespace).state()
|
|
542
|
+
};
|
|
543
|
+
},
|
|
478
544
|
add(rule) {
|
|
479
545
|
const existing = entries.findIndex((e) => e.rule.id === rule.id);
|
|
480
546
|
const entry = { rule, remaining: rule.count ?? null, hits: 0 };
|
|
@@ -494,6 +560,16 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
|
|
|
494
560
|
},
|
|
495
561
|
clear() {
|
|
496
562
|
entries.length = 0;
|
|
563
|
+
rng.reset();
|
|
564
|
+
scopedRngs.clear();
|
|
565
|
+
},
|
|
566
|
+
restore(namespace, state) {
|
|
567
|
+
for (let i = entries.length - 1; i >= 0; i--)
|
|
568
|
+
if (entries[i]?.rule.namespace === namespace)
|
|
569
|
+
entries.splice(i, 1);
|
|
570
|
+
for (const { remaining, hits, position, ...rule } of state.rules)
|
|
571
|
+
entries.splice(Math.min(position, entries.length), 0, { rule, remaining, hits });
|
|
572
|
+
random(namespace).setState(state.rngState);
|
|
497
573
|
},
|
|
498
574
|
async take(candidate) {
|
|
499
575
|
const hits = [];
|
|
@@ -503,7 +579,7 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
|
|
|
503
579
|
if (!matches(entry.rule, candidate))
|
|
504
580
|
continue;
|
|
505
581
|
const rate = entry.rule.rate ?? 1;
|
|
506
|
-
if (
|
|
582
|
+
if (random(candidate.namespace).next() >= rate)
|
|
507
583
|
continue;
|
|
508
584
|
entry.hits++;
|
|
509
585
|
if (entry.remaining !== null)
|
|
@@ -861,7 +937,8 @@ var annotateResponse = (response, extra) => {
|
|
|
861
937
|
var responseNotes = (response) => notes.get(response);
|
|
862
938
|
|
|
863
939
|
// ../core/dist/metrics.js
|
|
864
|
-
var createMetrics = () => {
|
|
940
|
+
var createMetrics = (partition = true) => {
|
|
941
|
+
const namespaces = /* @__PURE__ */ new Map();
|
|
865
942
|
let requests = 0;
|
|
866
943
|
let faults = 0;
|
|
867
944
|
let totalDurationMs = 0;
|
|
@@ -869,6 +946,14 @@ var createMetrics = () => {
|
|
|
869
946
|
const unmatched = /* @__PURE__ */ new Map();
|
|
870
947
|
return {
|
|
871
948
|
record(entry) {
|
|
949
|
+
if (partition) {
|
|
950
|
+
let scoped = namespaces.get(entry.namespace);
|
|
951
|
+
if (!scoped) {
|
|
952
|
+
scoped = createMetrics(false);
|
|
953
|
+
namespaces.set(entry.namespace, scoped);
|
|
954
|
+
}
|
|
955
|
+
scoped.record(entry);
|
|
956
|
+
}
|
|
872
957
|
requests++;
|
|
873
958
|
totalDurationMs += entry.durationMs;
|
|
874
959
|
if (entry.faultId !== void 0)
|
|
@@ -880,7 +965,7 @@ var createMetrics = () => {
|
|
|
880
965
|
unmatched.set(route, (unmatched.get(route) ?? 0) + 1);
|
|
881
966
|
}
|
|
882
967
|
},
|
|
883
|
-
report: () => ({
|
|
968
|
+
report: (namespace) => namespace !== void 0 ? (namespaces.get(namespace) ?? createMetrics(false)).report() : {
|
|
884
969
|
requests,
|
|
885
970
|
byOperation: Object.fromEntries([...byOperation].sort(([a], [b]) => a.localeCompare(b))),
|
|
886
971
|
unmatched: [...unmatched].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([route, count]) => {
|
|
@@ -889,8 +974,13 @@ var createMetrics = () => {
|
|
|
889
974
|
}),
|
|
890
975
|
faults,
|
|
891
976
|
totalDurationMs
|
|
892
|
-
}
|
|
893
|
-
reset() {
|
|
977
|
+
},
|
|
978
|
+
reset(namespace) {
|
|
979
|
+
if (namespace !== void 0) {
|
|
980
|
+
namespaces.delete(namespace);
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
namespaces.clear();
|
|
894
984
|
requests = 0;
|
|
895
985
|
faults = 0;
|
|
896
986
|
totalDurationMs = 0;
|
|
@@ -934,7 +1024,10 @@ var OutboxStore = class {
|
|
|
934
1024
|
return query.limit !== void 0 ? matched.slice(-Math.max(0, query.limit)) : matched;
|
|
935
1025
|
}
|
|
936
1026
|
};
|
|
937
|
-
var json2 = (status, body) => new Response(JSON.stringify(body), {
|
|
1027
|
+
var json2 = (status, body) => new Response(JSON.stringify(body), {
|
|
1028
|
+
status,
|
|
1029
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
1030
|
+
});
|
|
938
1031
|
var parseSince = (value) => {
|
|
939
1032
|
if (value === null)
|
|
940
1033
|
return void 0;
|
|
@@ -1570,6 +1663,34 @@ dialog:not([open]) {
|
|
|
1570
1663
|
function isRecord2(value) {
|
|
1571
1664
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1572
1665
|
}
|
|
1666
|
+
function highlightJsonText(text2) {
|
|
1667
|
+
const escapeToken = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
1668
|
+
switch (char) {
|
|
1669
|
+
case "&":
|
|
1670
|
+
return "&";
|
|
1671
|
+
case "<":
|
|
1672
|
+
return "<";
|
|
1673
|
+
case ">":
|
|
1674
|
+
return ">";
|
|
1675
|
+
case '"':
|
|
1676
|
+
return """;
|
|
1677
|
+
default:
|
|
1678
|
+
return "'";
|
|
1679
|
+
}
|
|
1680
|
+
});
|
|
1681
|
+
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;
|
|
1682
|
+
let cursor = 0;
|
|
1683
|
+
let html = "";
|
|
1684
|
+
for (const match of text2.matchAll(token)) {
|
|
1685
|
+
const index = match.index ?? 0;
|
|
1686
|
+
const value = match[0];
|
|
1687
|
+
html += escapeToken(text2.slice(cursor, index));
|
|
1688
|
+
const kind = value.startsWith('"') ? value.trimEnd().endsWith(":") ? "key" : "string" : value === "true" || value === "false" ? "boolean" : value === "null" ? "null" : "number";
|
|
1689
|
+
html += `<span class="json-${kind}">${escapeToken(value)}</span>`;
|
|
1690
|
+
cursor = index + value.length;
|
|
1691
|
+
}
|
|
1692
|
+
return html + escapeToken(text2.slice(cursor));
|
|
1693
|
+
}
|
|
1573
1694
|
function mountAdminBrand(doc, host, service, catalog) {
|
|
1574
1695
|
const clip = (value, max) => {
|
|
1575
1696
|
if (typeof value !== "string")
|
|
@@ -1931,14 +2052,16 @@ function bootAdmin(config) {
|
|
|
1931
2052
|
const state = {
|
|
1932
2053
|
namespace: "default",
|
|
1933
2054
|
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
1934
|
-
view: "overview",
|
|
2055
|
+
view: ["overview", "state", "clock", "faults", "journal", "routes"].includes(location.hash.slice(1)) ? location.hash.slice(1) : "overview",
|
|
1935
2056
|
collection: "",
|
|
1936
2057
|
shape: null,
|
|
1937
2058
|
route: null,
|
|
2059
|
+
routes: [],
|
|
1938
2060
|
editing: null
|
|
1939
2061
|
};
|
|
1940
2062
|
const standard = new Set(config.standardRoutes);
|
|
1941
2063
|
const banner = el("banner", HTMLElement);
|
|
2064
|
+
const bannerMessage = el("banner-message", HTMLElement);
|
|
1942
2065
|
const keyInput = el("admin-key", HTMLInputElement);
|
|
1943
2066
|
keyInput.value = state.key;
|
|
1944
2067
|
const params = new URLSearchParams(location.search);
|
|
@@ -1949,18 +2072,21 @@ function bootAdmin(config) {
|
|
|
1949
2072
|
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
1950
2073
|
params.delete("key");
|
|
1951
2074
|
const next = params.toString();
|
|
1952
|
-
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
2075
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : "") + location.hash);
|
|
1953
2076
|
}
|
|
1954
2077
|
const queryNamespace = params.get("namespace");
|
|
1955
2078
|
if (queryNamespace)
|
|
1956
2079
|
state.namespace = queryNamespace;
|
|
1957
2080
|
const showError = (error) => {
|
|
1958
|
-
|
|
2081
|
+
bannerMessage.textContent = error instanceof Error ? error.message : String(error);
|
|
1959
2082
|
banner.classList.add("show");
|
|
2083
|
+
banner.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
1960
2084
|
};
|
|
1961
2085
|
const clearError = () => {
|
|
1962
2086
|
banner.classList.remove("show");
|
|
2087
|
+
bannerMessage.textContent = "";
|
|
1963
2088
|
};
|
|
2089
|
+
el("banner-dismiss", HTMLButtonElement).addEventListener("click", clearError);
|
|
1964
2090
|
const api = async (method, path, body) => {
|
|
1965
2091
|
const headers = { accept: "application/json" };
|
|
1966
2092
|
if (state.key !== "")
|
|
@@ -2004,16 +2130,21 @@ function bootAdmin(config) {
|
|
|
2004
2130
|
namespace: () => state.namespace
|
|
2005
2131
|
};
|
|
2006
2132
|
const isPanelScript = (value) => typeof value === "function";
|
|
2133
|
+
const themeButton = el("theme", HTMLButtonElement);
|
|
2007
2134
|
const setTheme = (theme) => {
|
|
2008
2135
|
if (theme)
|
|
2009
2136
|
document.documentElement.dataset.theme = theme;
|
|
2010
2137
|
else
|
|
2011
2138
|
delete document.documentElement.dataset.theme;
|
|
2139
|
+
const dark = theme ? theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2140
|
+
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>';
|
|
2141
|
+
const label = dark ? "Use light theme" : "Use dark theme";
|
|
2142
|
+
themeButton.setAttribute("aria-label", label);
|
|
2143
|
+
themeButton.title = label;
|
|
2012
2144
|
};
|
|
2013
2145
|
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
2146
|
+
setTheme(storedTheme === "dark" || storedTheme === "light" ? storedTheme : null);
|
|
2147
|
+
themeButton.addEventListener("click", () => {
|
|
2017
2148
|
const current = document.documentElement.dataset.theme;
|
|
2018
2149
|
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2019
2150
|
const next = dark ? "light" : "dark";
|
|
@@ -2032,6 +2163,7 @@ function bootAdmin(config) {
|
|
|
2032
2163
|
continue;
|
|
2033
2164
|
section.hidden = section.id !== `view-${view}`;
|
|
2034
2165
|
}
|
|
2166
|
+
history.replaceState(null, "", `${location.pathname}${location.search}#${encodeURIComponent(view)}`);
|
|
2035
2167
|
};
|
|
2036
2168
|
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
2037
2169
|
const target = event.target;
|
|
@@ -2046,7 +2178,7 @@ function bootAdmin(config) {
|
|
|
2046
2178
|
show(view);
|
|
2047
2179
|
refresh().catch(showError);
|
|
2048
2180
|
});
|
|
2049
|
-
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>`;
|
|
2181
|
+
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>`;
|
|
2050
2182
|
const table = (columns, rows) => {
|
|
2051
2183
|
if (rows.length === 0)
|
|
2052
2184
|
return '<p class="empty">Nothing here yet.</p>';
|
|
@@ -2056,16 +2188,22 @@ function bootAdmin(config) {
|
|
|
2056
2188
|
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
2057
2189
|
return `<tr${attrs}>${cells}</tr>`;
|
|
2058
2190
|
}).join("");
|
|
2059
|
-
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
2191
|
+
return `<div class="table-shell"><table><thead>${head}</thead><tbody>${body}</tbody></table></div>`;
|
|
2060
2192
|
};
|
|
2061
2193
|
const preview = (value) => {
|
|
2062
2194
|
const text2 = JSON.stringify(value) ?? "null";
|
|
2063
|
-
|
|
2195
|
+
const clipped = text2.length > 180 ? `${text2.slice(0, 180)}\u2026` : text2;
|
|
2196
|
+
return `<code class="json-inline">${highlightJsonText(clipped)}</code>`;
|
|
2064
2197
|
};
|
|
2065
2198
|
const when = (epoch) => {
|
|
2066
2199
|
const date = new Date(epoch);
|
|
2067
2200
|
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
2068
2201
|
};
|
|
2202
|
+
const journalWhen = (value) => {
|
|
2203
|
+
const numeric = value.trim() === "" ? Number.NaN : Number(value);
|
|
2204
|
+
const epoch = Number.isFinite(numeric) ? numeric : Date.parse(value);
|
|
2205
|
+
return Number.isFinite(epoch) ? when(epoch) : value;
|
|
2206
|
+
};
|
|
2069
2207
|
const clockCard = (epoch) => {
|
|
2070
2208
|
const date = new Date(epoch);
|
|
2071
2209
|
if (Number.isNaN(date.getTime()))
|
|
@@ -2185,7 +2323,7 @@ function bootAdmin(config) {
|
|
|
2185
2323
|
};
|
|
2186
2324
|
const renderOverview = async () => {
|
|
2187
2325
|
const [health, shape, clock, journal] = await Promise.all([
|
|
2188
|
-
originGet(
|
|
2326
|
+
originGet(`${config.adminPrefix}/health`),
|
|
2189
2327
|
api("GET", "/state"),
|
|
2190
2328
|
api("GET", "/clock"),
|
|
2191
2329
|
api("GET", "/requests?limit=8")
|
|
@@ -2202,7 +2340,7 @@ function bootAdmin(config) {
|
|
|
2202
2340
|
},
|
|
2203
2341
|
{
|
|
2204
2342
|
k: "Status",
|
|
2205
|
-
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
2343
|
+
v: `<span class="badge ok">${esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")}</span>`
|
|
2206
2344
|
},
|
|
2207
2345
|
{ k: "Namespace", v: esc(state.namespace) },
|
|
2208
2346
|
{ k: "Collections", v: String(decoded.collections.length) },
|
|
@@ -2289,26 +2427,46 @@ function bootAdmin(config) {
|
|
|
2289
2427
|
const renderJournal = async () => {
|
|
2290
2428
|
const journal = await api("GET", "/requests?limit=100");
|
|
2291
2429
|
el("journal", HTMLElement).innerHTML = table([
|
|
2292
|
-
{ label: "When", cell: (row) => esc(row.at) },
|
|
2293
|
-
{
|
|
2430
|
+
{ label: "When", cell: (row) => esc(journalWhen(row.at)) },
|
|
2431
|
+
{
|
|
2432
|
+
label: "Method",
|
|
2433
|
+
cell: (row) => `<span class="method">${esc(row.method)}</span>`
|
|
2434
|
+
},
|
|
2294
2435
|
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2295
|
-
{
|
|
2436
|
+
{
|
|
2437
|
+
label: "Status",
|
|
2438
|
+
cell: (row) => `<span class="badge ${row.status.startsWith("2") ? "ok" : row.status.startsWith("4") || row.status.startsWith("5") ? "warn" : ""}">${esc(row.status)}</span>`
|
|
2439
|
+
},
|
|
2296
2440
|
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
2297
2441
|
], decodeJournal(journal));
|
|
2298
2442
|
};
|
|
2299
|
-
const
|
|
2300
|
-
const
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2443
|
+
const paintRoutes = () => {
|
|
2444
|
+
const query = el("route-search", HTMLInputElement).value.trim().toLocaleLowerCase();
|
|
2445
|
+
const routes = state.routes.filter((route) => route.toLocaleLowerCase().includes(query));
|
|
2446
|
+
el("route-count", HTMLElement).textContent = `${routes.length} of ${state.routes.length}`;
|
|
2447
|
+
if (state.routes.length === 0) {
|
|
2448
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No admin routes are available.</p>';
|
|
2449
|
+
return;
|
|
2450
|
+
}
|
|
2451
|
+
if (routes.length === 0) {
|
|
2452
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No routes match this filter.</p>';
|
|
2453
|
+
return;
|
|
2454
|
+
}
|
|
2304
2455
|
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2305
2456
|
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2306
2457
|
const parts = route.split(" ");
|
|
2307
2458
|
const method = parts[0] ?? "";
|
|
2308
2459
|
const path = parts.slice(1).join(" ");
|
|
2309
|
-
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>`;
|
|
2460
|
+
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>`;
|
|
2310
2461
|
}).join("");
|
|
2311
2462
|
};
|
|
2463
|
+
const renderRoutes = async () => {
|
|
2464
|
+
const data = await api("GET", "/");
|
|
2465
|
+
if (!isRecord2(data))
|
|
2466
|
+
throw new Error("Route list was not an object");
|
|
2467
|
+
state.routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
2468
|
+
paintRoutes();
|
|
2469
|
+
};
|
|
2312
2470
|
const renderers = {
|
|
2313
2471
|
overview: renderOverview,
|
|
2314
2472
|
state: renderState,
|
|
@@ -2317,19 +2475,50 @@ function bootAdmin(config) {
|
|
|
2317
2475
|
journal: renderJournal,
|
|
2318
2476
|
routes: renderRoutes
|
|
2319
2477
|
};
|
|
2478
|
+
let refreshToken = 0;
|
|
2320
2479
|
async function refresh() {
|
|
2480
|
+
const token = ++refreshToken;
|
|
2321
2481
|
clearError();
|
|
2482
|
+
const main = el("views", HTMLElement);
|
|
2483
|
+
const sync = el("sync-status", HTMLElement);
|
|
2484
|
+
main.setAttribute("aria-busy", "true");
|
|
2485
|
+
sync.classList.add("busy");
|
|
2486
|
+
sync.textContent = "Updating";
|
|
2322
2487
|
const render = renderers[state.view];
|
|
2323
|
-
|
|
2324
|
-
|
|
2488
|
+
try {
|
|
2489
|
+
if (render)
|
|
2490
|
+
await render();
|
|
2491
|
+
if (token === refreshToken)
|
|
2492
|
+
sync.textContent = "Up to date";
|
|
2493
|
+
} finally {
|
|
2494
|
+
if (token === refreshToken) {
|
|
2495
|
+
main.removeAttribute("aria-busy");
|
|
2496
|
+
sync.classList.remove("busy");
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2325
2499
|
}
|
|
2326
2500
|
const delay = (ms) => new Promise((resolve) => {
|
|
2327
2501
|
window.setTimeout(resolve, ms);
|
|
2328
2502
|
});
|
|
2329
|
-
const confirmAction = async (button, pending, done, work) => {
|
|
2503
|
+
const confirmAction = async (button, pending, done, work, confirmLabel) => {
|
|
2330
2504
|
if (button.dataset.pending === "1")
|
|
2331
2505
|
return;
|
|
2332
2506
|
const label = button.textContent ?? "";
|
|
2507
|
+
if (confirmLabel !== void 0 && button.dataset.confirming !== "1") {
|
|
2508
|
+
button.dataset.confirming = "1";
|
|
2509
|
+
button.classList.add("confirming");
|
|
2510
|
+
button.textContent = confirmLabel;
|
|
2511
|
+
window.setTimeout(() => {
|
|
2512
|
+
if (button.dataset.confirming !== "1")
|
|
2513
|
+
return;
|
|
2514
|
+
button.textContent = label;
|
|
2515
|
+
button.classList.remove("confirming");
|
|
2516
|
+
delete button.dataset.confirming;
|
|
2517
|
+
}, 3e3);
|
|
2518
|
+
return;
|
|
2519
|
+
}
|
|
2520
|
+
button.classList.remove("confirming");
|
|
2521
|
+
delete button.dataset.confirming;
|
|
2333
2522
|
const width = button.offsetWidth;
|
|
2334
2523
|
button.dataset.pending = "1";
|
|
2335
2524
|
button.classList.add("pressed");
|
|
@@ -2350,6 +2539,65 @@ function bootAdmin(config) {
|
|
|
2350
2539
|
delete button.dataset.pending;
|
|
2351
2540
|
}
|
|
2352
2541
|
};
|
|
2542
|
+
const wireJsonEditor = (textareaId, highlightId, statusId, shellId, allowEmpty) => {
|
|
2543
|
+
const textarea = el(textareaId, HTMLTextAreaElement);
|
|
2544
|
+
const highlight = el(highlightId, HTMLElement);
|
|
2545
|
+
const status = el(statusId, HTMLElement);
|
|
2546
|
+
const shell = el(shellId, HTMLElement);
|
|
2547
|
+
const sync = () => {
|
|
2548
|
+
const text2 = textarea.value;
|
|
2549
|
+
highlight.innerHTML = `${highlightJsonText(text2)}
|
|
2550
|
+
`;
|
|
2551
|
+
let valid = true;
|
|
2552
|
+
let message = "Valid JSON";
|
|
2553
|
+
if (allowEmpty && text2.trim() === "")
|
|
2554
|
+
message = "No body";
|
|
2555
|
+
else {
|
|
2556
|
+
try {
|
|
2557
|
+
readJson2(text2);
|
|
2558
|
+
} catch {
|
|
2559
|
+
valid = false;
|
|
2560
|
+
message = "Invalid JSON";
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
shell.classList.toggle("invalid", !valid);
|
|
2564
|
+
status.classList.toggle("invalid", !valid);
|
|
2565
|
+
status.textContent = message;
|
|
2566
|
+
textarea.setAttribute("aria-invalid", valid ? "false" : "true");
|
|
2567
|
+
};
|
|
2568
|
+
const syncScroll = () => {
|
|
2569
|
+
const preview2 = highlight.parentElement;
|
|
2570
|
+
if (!(preview2 instanceof HTMLElement))
|
|
2571
|
+
return;
|
|
2572
|
+
preview2.scrollTop = textarea.scrollTop;
|
|
2573
|
+
preview2.scrollLeft = textarea.scrollLeft;
|
|
2574
|
+
};
|
|
2575
|
+
textarea.addEventListener("input", sync);
|
|
2576
|
+
textarea.addEventListener("scroll", syncScroll);
|
|
2577
|
+
sync();
|
|
2578
|
+
return sync;
|
|
2579
|
+
};
|
|
2580
|
+
const syncRouteEditor = wireJsonEditor("route-body", "route-highlight", "route-json-status", "route-editor", true);
|
|
2581
|
+
const syncRecordEditor = wireJsonEditor("editor-value", "editor-highlight", "editor-json-status", "record-editor", false);
|
|
2582
|
+
for (const button of document.querySelectorAll("[data-format]")) {
|
|
2583
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2584
|
+
continue;
|
|
2585
|
+
button.addEventListener("click", () => {
|
|
2586
|
+
const target = button.dataset.format;
|
|
2587
|
+
if (target === void 0)
|
|
2588
|
+
return;
|
|
2589
|
+
const textarea = document.getElementById(target);
|
|
2590
|
+
if (!(textarea instanceof HTMLTextAreaElement) || textarea.value.trim() === "")
|
|
2591
|
+
return;
|
|
2592
|
+
try {
|
|
2593
|
+
textarea.value = JSON.stringify(readJson2(textarea.value), null, 2) ?? "";
|
|
2594
|
+
textarea.dispatchEvent(new Event("input"));
|
|
2595
|
+
textarea.focus();
|
|
2596
|
+
} catch {
|
|
2597
|
+
showError(new Error("Fix the JSON before formatting it"));
|
|
2598
|
+
}
|
|
2599
|
+
});
|
|
2600
|
+
}
|
|
2353
2601
|
document.addEventListener("click", (event) => {
|
|
2354
2602
|
const target = event.target;
|
|
2355
2603
|
if (!(target instanceof Element))
|
|
@@ -2422,6 +2670,7 @@ function bootAdmin(config) {
|
|
|
2422
2670
|
} else {
|
|
2423
2671
|
editorValue.value = "{\n \n}";
|
|
2424
2672
|
}
|
|
2673
|
+
syncRecordEditor();
|
|
2425
2674
|
editor.showModal();
|
|
2426
2675
|
}
|
|
2427
2676
|
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -2451,10 +2700,10 @@ function bootAdmin(config) {
|
|
|
2451
2700
|
const id = state.editing;
|
|
2452
2701
|
if (!id)
|
|
2453
2702
|
return;
|
|
2454
|
-
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2703
|
+
confirmAction(editorDelete, "Deleting...", "Deleted", () => api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2455
2704
|
editor.close();
|
|
2456
2705
|
return renderState();
|
|
2457
|
-
}).catch(showError);
|
|
2706
|
+
}), "Confirm delete").catch(showError);
|
|
2458
2707
|
});
|
|
2459
2708
|
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2460
2709
|
if (!(button instanceof HTMLButtonElement))
|
|
@@ -2500,10 +2749,10 @@ function bootAdmin(config) {
|
|
|
2500
2749
|
}).then(() => renderFaults()).catch(showError);
|
|
2501
2750
|
});
|
|
2502
2751
|
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2503
|
-
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
|
|
2752
|
+
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults()), "Confirm clear").catch(showError);
|
|
2504
2753
|
});
|
|
2505
2754
|
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2506
|
-
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
|
|
2755
|
+
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal()), "Confirm clear").catch(showError);
|
|
2507
2756
|
});
|
|
2508
2757
|
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2509
2758
|
const target = event.target;
|
|
@@ -2517,9 +2766,15 @@ function bootAdmin(config) {
|
|
|
2517
2766
|
return;
|
|
2518
2767
|
state.route = route;
|
|
2519
2768
|
el("route-title", HTMLElement).textContent = route;
|
|
2769
|
+
el("route-send", HTMLButtonElement).disabled = false;
|
|
2520
2770
|
const method = route.split(" ")[0] ?? "";
|
|
2521
|
-
el("route-body", HTMLTextAreaElement)
|
|
2771
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
2772
|
+
routeBody.disabled = method === "GET" || method === "DELETE";
|
|
2773
|
+
el("route-editor", HTMLElement).hidden = routeBody.disabled;
|
|
2774
|
+
el("route-json-status", HTMLElement).parentElement?.toggleAttribute("hidden", routeBody.disabled);
|
|
2775
|
+
syncRouteEditor();
|
|
2522
2776
|
});
|
|
2777
|
+
el("route-search", HTMLInputElement).addEventListener("input", paintRoutes);
|
|
2523
2778
|
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2524
2779
|
event.preventDefault();
|
|
2525
2780
|
const send = async () => {
|
|
@@ -2541,7 +2796,8 @@ function bootAdmin(config) {
|
|
|
2541
2796
|
}
|
|
2542
2797
|
}
|
|
2543
2798
|
const data = await api(method, concrete, body);
|
|
2544
|
-
|
|
2799
|
+
const text2 = JSON.stringify(data, null, 2) ?? "null";
|
|
2800
|
+
el("route-result", HTMLElement).innerHTML = `<code>${highlightJsonText(text2)}</code>`;
|
|
2545
2801
|
};
|
|
2546
2802
|
send().catch(showError);
|
|
2547
2803
|
});
|
|
@@ -2659,11 +2915,13 @@ function bootAdmin(config) {
|
|
|
2659
2915
|
views.append(section);
|
|
2660
2916
|
mountSqlExplorer(body, panelApi);
|
|
2661
2917
|
}
|
|
2918
|
+
show(document.getElementById(`view-${state.view}`) === null ? "overview" : state.view);
|
|
2662
2919
|
return refresh();
|
|
2663
2920
|
}).catch(showError);
|
|
2664
2921
|
}
|
|
2665
2922
|
var clientPrelude = [
|
|
2666
2923
|
isRecord2,
|
|
2924
|
+
highlightJsonText,
|
|
2667
2925
|
mountAdminBrand,
|
|
2668
2926
|
startAdminBrand,
|
|
2669
2927
|
faultPresetList,
|
|
@@ -2686,6 +2944,7 @@ ${entry}(${payload});
|
|
|
2686
2944
|
var defineMock = (runtime) => runtime;
|
|
2687
2945
|
var STANDARD_ADMIN_ROUTES = [
|
|
2688
2946
|
"GET /",
|
|
2947
|
+
"GET /health",
|
|
2689
2948
|
"POST /reset",
|
|
2690
2949
|
"GET /namespaces",
|
|
2691
2950
|
"GET /clock",
|
|
@@ -2765,11 +3024,11 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
2765
3024
|
}
|
|
2766
3025
|
});
|
|
2767
3026
|
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
2768
|
-
var renderAdminDocument = (service) => {
|
|
3027
|
+
var renderAdminDocument = (service, adminPrefix = ADMIN_PREFIX) => {
|
|
2769
3028
|
const name = escapeHtml(service);
|
|
2770
3029
|
const script = adminClientSource({
|
|
2771
3030
|
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
2772
|
-
adminPrefix
|
|
3031
|
+
adminPrefix,
|
|
2773
3032
|
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
2774
3033
|
brandsUrl: ADMIN_BRANDS_URL,
|
|
2775
3034
|
service
|
|
@@ -2786,75 +3045,99 @@ var renderAdminDocument = (service) => {
|
|
|
2786
3045
|
${CSS_RESET}
|
|
2787
3046
|
:root {
|
|
2788
3047
|
color-scheme: light;
|
|
2789
|
-
--bg: #
|
|
3048
|
+
--bg: #f7f8fb;
|
|
2790
3049
|
--bg-elev: #ffffff;
|
|
2791
|
-
--bg-muted: #
|
|
2792
|
-
--bg-inset: #
|
|
2793
|
-
--line: #
|
|
2794
|
-
--line-strong: #
|
|
2795
|
-
--ink: #
|
|
2796
|
-
--muted: #
|
|
2797
|
-
--faint: #
|
|
2798
|
-
--accent: #
|
|
3050
|
+
--bg-muted: #f1f3f7;
|
|
3051
|
+
--bg-inset: #f8f9fc;
|
|
3052
|
+
--line: #e4e7ec;
|
|
3053
|
+
--line-strong: #cfd4dc;
|
|
3054
|
+
--ink: #101828;
|
|
3055
|
+
--muted: #475467;
|
|
3056
|
+
--faint: #667085;
|
|
3057
|
+
--accent: #6941c6;
|
|
3058
|
+
--accent-hover: #5933b4;
|
|
2799
3059
|
--accent-ink: #ffffff;
|
|
2800
|
-
--accent-soft: #
|
|
2801
|
-
--ok: #
|
|
2802
|
-
--ok-soft: #
|
|
2803
|
-
--warn: #
|
|
2804
|
-
--warn-soft: #
|
|
2805
|
-
--err: #
|
|
2806
|
-
--err-soft: #
|
|
2807
|
-
--
|
|
3060
|
+
--accent-soft: #f4f0ff;
|
|
3061
|
+
--ok: #067647;
|
|
3062
|
+
--ok-soft: #ecfdf3;
|
|
3063
|
+
--warn: #b54708;
|
|
3064
|
+
--warn-soft: #fffaeb;
|
|
3065
|
+
--err: #b42318;
|
|
3066
|
+
--err-soft: #fef3f2;
|
|
3067
|
+
--code-key: #6941c6;
|
|
3068
|
+
--code-string: #067647;
|
|
3069
|
+
--code-number: #175cd3;
|
|
3070
|
+
--code-boolean: #c11574;
|
|
3071
|
+
--code-null: #667085;
|
|
3072
|
+
--shadow-xs: 0 1px 2px rgb(16 24 40 / 0.05);
|
|
3073
|
+
--shadow-sm: 0 1px 3px rgb(16 24 40 / 0.08), 0 1px 2px rgb(16 24 40 / 0.04);
|
|
3074
|
+
--shadow-lg: 0 18px 48px rgb(16 24 40 / 0.16), 0 4px 12px rgb(16 24 40 / 0.08);
|
|
2808
3075
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2809
3076
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
2810
|
-
--radius:
|
|
2811
|
-
--header:
|
|
2812
|
-
--nav:
|
|
3077
|
+
--radius: 12px;
|
|
3078
|
+
--header: 72px;
|
|
3079
|
+
--nav: 224px;
|
|
2813
3080
|
}
|
|
2814
3081
|
:root[data-theme="dark"] {
|
|
2815
3082
|
color-scheme: dark;
|
|
2816
|
-
--bg: #
|
|
2817
|
-
--bg-elev: #
|
|
2818
|
-
--bg-muted: #
|
|
2819
|
-
--bg-inset: #
|
|
2820
|
-
--line: #
|
|
2821
|
-
--line-strong: #
|
|
2822
|
-
--ink: #
|
|
2823
|
-
--muted: #
|
|
2824
|
-
--faint: #
|
|
2825
|
-
--accent: #
|
|
2826
|
-
--accent-
|
|
2827
|
-
--accent-
|
|
2828
|
-
--
|
|
2829
|
-
--ok
|
|
2830
|
-
--
|
|
2831
|
-
--warn
|
|
2832
|
-
--
|
|
2833
|
-
--err
|
|
2834
|
-
--
|
|
3083
|
+
--bg: #0c0e14;
|
|
3084
|
+
--bg-elev: #151821;
|
|
3085
|
+
--bg-muted: #20232e;
|
|
3086
|
+
--bg-inset: #11141c;
|
|
3087
|
+
--line: #2b303d;
|
|
3088
|
+
--line-strong: #414858;
|
|
3089
|
+
--ink: #f5f7fa;
|
|
3090
|
+
--muted: #b6bdc9;
|
|
3091
|
+
--faint: #8d96a7;
|
|
3092
|
+
--accent: #9e77ed;
|
|
3093
|
+
--accent-hover: #b692f6;
|
|
3094
|
+
--accent-ink: #ffffff;
|
|
3095
|
+
--accent-soft: #2b2142;
|
|
3096
|
+
--ok: #75e0a7;
|
|
3097
|
+
--ok-soft: #102a20;
|
|
3098
|
+
--warn: #fec84b;
|
|
3099
|
+
--warn-soft: #302611;
|
|
3100
|
+
--err: #fda29b;
|
|
3101
|
+
--err-soft: #321817;
|
|
3102
|
+
--code-key: #c3a6ff;
|
|
3103
|
+
--code-string: #75e0a7;
|
|
3104
|
+
--code-number: #84adff;
|
|
3105
|
+
--code-boolean: #f9a8d4;
|
|
3106
|
+
--code-null: #98a2b3;
|
|
3107
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3108
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3109
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2835
3110
|
}
|
|
2836
3111
|
@media (prefers-color-scheme: dark) {
|
|
2837
3112
|
:root:not([data-theme="light"]) {
|
|
2838
3113
|
color-scheme: dark;
|
|
2839
|
-
--bg: #
|
|
2840
|
-
--bg-elev: #
|
|
2841
|
-
--bg-muted: #
|
|
2842
|
-
--bg-inset: #
|
|
2843
|
-
--line: #
|
|
2844
|
-
--line-strong: #
|
|
2845
|
-
--ink: #
|
|
2846
|
-
--muted: #
|
|
2847
|
-
--faint: #
|
|
2848
|
-
--accent: #
|
|
2849
|
-
--accent-
|
|
2850
|
-
--accent-
|
|
2851
|
-
--
|
|
2852
|
-
--ok
|
|
2853
|
-
--
|
|
2854
|
-
--warn
|
|
2855
|
-
--
|
|
2856
|
-
--err
|
|
2857
|
-
--
|
|
3114
|
+
--bg: #0c0e14;
|
|
3115
|
+
--bg-elev: #151821;
|
|
3116
|
+
--bg-muted: #20232e;
|
|
3117
|
+
--bg-inset: #11141c;
|
|
3118
|
+
--line: #2b303d;
|
|
3119
|
+
--line-strong: #414858;
|
|
3120
|
+
--ink: #f5f7fa;
|
|
3121
|
+
--muted: #b6bdc9;
|
|
3122
|
+
--faint: #8d96a7;
|
|
3123
|
+
--accent: #9e77ed;
|
|
3124
|
+
--accent-hover: #b692f6;
|
|
3125
|
+
--accent-ink: #ffffff;
|
|
3126
|
+
--accent-soft: #2b2142;
|
|
3127
|
+
--ok: #75e0a7;
|
|
3128
|
+
--ok-soft: #102a20;
|
|
3129
|
+
--warn: #fec84b;
|
|
3130
|
+
--warn-soft: #302611;
|
|
3131
|
+
--err: #fda29b;
|
|
3132
|
+
--err-soft: #321817;
|
|
3133
|
+
--code-key: #c3a6ff;
|
|
3134
|
+
--code-string: #75e0a7;
|
|
3135
|
+
--code-number: #84adff;
|
|
3136
|
+
--code-boolean: #f9a8d4;
|
|
3137
|
+
--code-null: #98a2b3;
|
|
3138
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3139
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3140
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2858
3141
|
}
|
|
2859
3142
|
}
|
|
2860
3143
|
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
@@ -2872,13 +3155,17 @@ var renderAdminDocument = (service) => {
|
|
|
2872
3155
|
padding-right: env(safe-area-inset-right);
|
|
2873
3156
|
-webkit-text-size-adjust: 100%;
|
|
2874
3157
|
}
|
|
2875
|
-
|
|
3158
|
+
::selection { background: color-mix(in srgb, var(--accent) 28%, transparent); }
|
|
3159
|
+
:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px; }
|
|
3160
|
+
button, input, select, textarea { transition: border-color 140ms ease, box-shadow 140ms ease, transform 100ms ease; }
|
|
3161
|
+
button:disabled, input:disabled, select:disabled, textarea:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
2876
3162
|
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
2877
3163
|
header.top {
|
|
2878
|
-
position: sticky; top: 0; z-index:
|
|
3164
|
+
position: sticky; top: 0; z-index: 20;
|
|
2879
3165
|
display: flex; align-items: center; gap: 12px;
|
|
2880
|
-
min-height: var(--header); padding:
|
|
2881
|
-
background: var(--bg-elev);
|
|
3166
|
+
min-height: var(--header); padding: 12px 20px;
|
|
3167
|
+
background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
|
|
3168
|
+
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
2882
3169
|
border-bottom: 1px solid var(--line);
|
|
2883
3170
|
min-width: 0; max-width: 100%;
|
|
2884
3171
|
}
|
|
@@ -2886,12 +3173,21 @@ var renderAdminDocument = (service) => {
|
|
|
2886
3173
|
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
2887
3174
|
min-width: 0; max-width: 100%; margin-right: auto;
|
|
2888
3175
|
}
|
|
2889
|
-
.brand-
|
|
2890
|
-
.brand-
|
|
3176
|
+
.brand-lockup { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
3177
|
+
.brand-mark {
|
|
3178
|
+
width: 38px; height: 38px; display: grid; place-items: center; flex: none;
|
|
3179
|
+
border-radius: 11px; color: #fff;
|
|
3180
|
+
background: linear-gradient(145deg, #7f56d9 0%, #53389e 100%);
|
|
3181
|
+
box-shadow: 0 6px 16px rgb(105 65 198 / 0.24), inset 0 1px 0 rgb(255 255 255 / 0.25);
|
|
3182
|
+
}
|
|
3183
|
+
.brand-mark svg { width: 22px; height: 22px; }
|
|
3184
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; line-height: 1.25; }
|
|
3185
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
2891
3186
|
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
2892
3187
|
.vendor {
|
|
2893
3188
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
2894
3189
|
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
3190
|
+
padding: 5px 8px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-inset);
|
|
2895
3191
|
}
|
|
2896
3192
|
.vendor[hidden] { display: none; }
|
|
2897
3193
|
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
@@ -2903,12 +3199,17 @@ var renderAdminDocument = (service) => {
|
|
|
2903
3199
|
.vendor-sep { color: var(--faint); }
|
|
2904
3200
|
.vendor-link { color: inherit; text-decoration: none; }
|
|
2905
3201
|
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
2906
|
-
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items:
|
|
2907
|
-
|
|
3202
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
|
|
3203
|
+
.sync-status { align-self: center; display: flex; align-items: center; gap: 6px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3204
|
+
.sync-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
|
|
3205
|
+
.sync-status.busy::before { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); animation: pulse 1.2s ease-in-out infinite; }
|
|
3206
|
+
label.field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; font-weight: 600; color: var(--faint); }
|
|
2908
3207
|
label.field input, label.field select, .controls input[type="password"] {
|
|
2909
|
-
background-color: var(--bg-
|
|
2910
|
-
padding:
|
|
3208
|
+
background-color: var(--bg-elev); border: 1px solid var(--line-strong); border-radius: 9px;
|
|
3209
|
+
padding: 8px 10px; min-height: 38px; line-height: 1.2; box-shadow: var(--shadow-xs);
|
|
2911
3210
|
}
|
|
3211
|
+
label.field input:hover, label.field select:hover, textarea:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--line-strong)); }
|
|
3212
|
+
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; }
|
|
2912
3213
|
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
2913
3214
|
the text fields in the same form. Paint it like those fields. */
|
|
2914
3215
|
label.field select {
|
|
@@ -2925,12 +3226,20 @@ var renderAdminDocument = (service) => {
|
|
|
2925
3226
|
dialog .field > textarea { width: 100%; }
|
|
2926
3227
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
2927
3228
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
.btn
|
|
3229
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
|
3230
|
+
border-radius: 9px; min-height: 38px; padding: 0 13px; border: 1px solid var(--line-strong);
|
|
3231
|
+
background: var(--bg-elev); color: var(--ink); font-size: 13px; font-weight: 600; line-height: 1;
|
|
3232
|
+
box-shadow: var(--shadow-xs);
|
|
3233
|
+
}
|
|
3234
|
+
.icon-btn { width: 38px; padding: 0; }
|
|
3235
|
+
.icon-btn svg, .btn svg, .btn-primary svg { width: 16px; height: 16px; }
|
|
3236
|
+
.icon-btn:hover, .btn:hover { background: var(--bg-muted); border-color: var(--line-strong); }
|
|
3237
|
+
.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; }
|
|
3238
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
3239
|
+
.btn-quiet { background: transparent; border-color: transparent; box-shadow: none; }
|
|
3240
|
+
.btn-quiet:hover { background: var(--bg-muted); }
|
|
3241
|
+
.btn-danger { color: var(--err); border-color: color-mix(in srgb, var(--err) 25%, var(--line)); }
|
|
3242
|
+
.btn-danger:hover, .btn-danger.confirming { background: var(--err-soft); border-color: color-mix(in srgb, var(--err) 45%, var(--line)); }
|
|
2934
3243
|
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
2935
3244
|
so the press itself has to stay visible. */
|
|
2936
3245
|
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
@@ -2946,93 +3255,159 @@ var renderAdminDocument = (service) => {
|
|
|
2946
3255
|
flex: 1 0 auto; min-width: 0;
|
|
2947
3256
|
}
|
|
2948
3257
|
nav.side {
|
|
2949
|
-
|
|
3258
|
+
position: sticky; top: var(--header); align-self: start; height: calc(100vh - var(--header));
|
|
3259
|
+
border-right: 1px solid var(--line); padding: 18px 12px;
|
|
2950
3260
|
display: flex; flex-direction: column; gap: 4px;
|
|
2951
|
-
background: var(--bg-elev);
|
|
3261
|
+
background: var(--bg-elev); overflow-y: auto;
|
|
2952
3262
|
}
|
|
3263
|
+
.nav-label { padding: 0 10px 8px; color: var(--faint); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
|
|
2953
3264
|
nav.side button {
|
|
3265
|
+
position: relative; display: flex; align-items: center; gap: 10px; width: 100%;
|
|
2954
3266
|
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
2955
|
-
padding: 9px 10px; min-height:
|
|
2956
|
-
}
|
|
2957
|
-
nav.side button
|
|
2958
|
-
|
|
2959
|
-
.
|
|
2960
|
-
.
|
|
2961
|
-
.
|
|
3267
|
+
padding: 9px 10px; min-height: 42px; color: var(--muted); font-size: 13px; font-weight: 550;
|
|
3268
|
+
}
|
|
3269
|
+
nav.side button:hover { background: var(--bg-muted); color: var(--ink); }
|
|
3270
|
+
nav.side button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 650; }
|
|
3271
|
+
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); }
|
|
3272
|
+
.nav-icon { width: 18px; height: 18px; display: grid; place-items: center; flex: none; }
|
|
3273
|
+
.nav-icon svg { width: 18px; height: 18px; }
|
|
3274
|
+
main { padding: 32px clamp(20px, 4vw, 48px) 64px; max-width: 1320px; width: 100%; min-width: 0; }
|
|
3275
|
+
main[aria-busy="true"] .view { opacity: 0.62; }
|
|
3276
|
+
.view { animation: view-in 180ms ease-out; }
|
|
3277
|
+
.view[hidden] { display: none; }
|
|
3278
|
+
.view-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
|
|
3279
|
+
.view h2 { margin: 0 0 5px; font-size: clamp(24px, 3vw, 30px); line-height: 1.2; letter-spacing: -0.035em; }
|
|
3280
|
+
.view h3 { margin: 24px 0 10px; font-size: 15px; letter-spacing: -0.01em; }
|
|
3281
|
+
.lede { margin: 0; color: var(--muted); max-width: 72ch; font-size: 14px; }
|
|
3282
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 175px), 1fr)); gap: 14px; }
|
|
2962
3283
|
.card {
|
|
2963
3284
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
2964
|
-
padding:
|
|
3285
|
+
padding: 18px; box-shadow: var(--shadow-sm);
|
|
2965
3286
|
}
|
|
2966
|
-
.card
|
|
2967
|
-
.card
|
|
3287
|
+
.stat-card { position: relative; overflow: hidden; }
|
|
3288
|
+
.stat-card::after { content: ""; position: absolute; inset: 0 0 auto; height: 2px; background: linear-gradient(90deg, var(--accent), transparent 78%); opacity: 0.7; }
|
|
3289
|
+
.card .k { color: var(--faint); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
3290
|
+
.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; }
|
|
2968
3291
|
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
2969
|
-
.
|
|
3292
|
+
.section-card { background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-xs); overflow: hidden; }
|
|
3293
|
+
.section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
|
|
3294
|
+
.section-title h3 { margin: 0; }
|
|
3295
|
+
.split { display: grid; grid-template-columns: minmax(0, 250px) minmax(0, 1fr); gap: 18px; align-items: start; }
|
|
3296
|
+
#collections { padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); box-shadow: var(--shadow-xs); }
|
|
2970
3297
|
.collection {
|
|
2971
3298
|
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
2972
|
-
border-radius:
|
|
3299
|
+
border-radius: 9px; padding: 10px 11px; min-height: 48px; color: var(--ink); cursor: pointer;
|
|
2973
3300
|
}
|
|
2974
|
-
.collection
|
|
3301
|
+
.collection:hover { background: var(--bg-muted); }
|
|
3302
|
+
.collection[aria-current="true"] { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 18%, var(--line)); color: var(--accent); }
|
|
2975
3303
|
.collection small { display: block; color: var(--faint); }
|
|
2976
3304
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
2977
|
-
.chip {
|
|
2978
|
-
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
2979
|
-
border-radius: 999px; padding: 4px 8px;
|
|
2980
|
-
}
|
|
3305
|
+
.chip { font-family: var(--mono); font-size: 11px; background: var(--bg-muted); border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; }
|
|
2981
3306
|
.chip i { color: var(--faint); font-style: normal; }
|
|
2982
3307
|
.sql-layout {
|
|
2983
3308
|
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
2984
3309
|
}
|
|
2985
|
-
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
2986
|
-
.sql-tables button { text-align: left; }
|
|
3310
|
+
.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); }
|
|
3311
|
+
.sql-tables button { text-align: left; border: 0; border-radius: 8px; padding: 9px 10px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
3312
|
+
.sql-tables button:hover, .sql-tables button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); }
|
|
2987
3313
|
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
2988
|
-
table { width: 100%; border-
|
|
2989
|
-
|
|
2990
|
-
th {
|
|
3314
|
+
.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); }
|
|
3315
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
3316
|
+
th, td { text-align: left; padding: 11px 13px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
3317
|
+
th { color: var(--faint); background: var(--bg-inset); font-weight: 650; font-size: 11px; text-transform: uppercase; letter-spacing: 0.045em; }
|
|
3318
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
2991
3319
|
tr.clickable { cursor: pointer; }
|
|
2992
|
-
tr.clickable:hover { background: var(--
|
|
3320
|
+
tr.clickable:hover { background: var(--accent-soft); }
|
|
2993
3321
|
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
2994
3322
|
pre {
|
|
2995
3323
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
2996
|
-
background: var(--bg-inset); border-radius: 10px; padding:
|
|
2997
|
-
}
|
|
2998
|
-
.
|
|
3324
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 10px; padding: 14px; font-size: 12px; line-height: 1.65;
|
|
3325
|
+
}
|
|
3326
|
+
.json-key { color: var(--code-key); }
|
|
3327
|
+
.json-string { color: var(--code-string); }
|
|
3328
|
+
.json-number { color: var(--code-number); }
|
|
3329
|
+
.json-boolean { color: var(--code-boolean); }
|
|
3330
|
+
.json-null { color: var(--code-null); font-style: italic; }
|
|
3331
|
+
.json-inline { display: block; max-width: 44rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
3332
|
+
.json-view { position: relative; max-height: 26rem; overflow: auto; }
|
|
3333
|
+
.json-editor { position: relative; min-height: 190px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--bg-inset); overflow: hidden; }
|
|
3334
|
+
.json-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); }
|
|
3335
|
+
.json-editor.invalid { border-color: var(--err); }
|
|
3336
|
+
.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; }
|
|
3337
|
+
.json-editor pre { position: absolute; inset: 0; pointer-events: none; background: transparent; }
|
|
3338
|
+
.json-editor textarea { position: relative; z-index: 1; resize: vertical; background: transparent; color: transparent; caret-color: var(--ink); -webkit-text-fill-color: transparent; }
|
|
3339
|
+
.json-editor textarea::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); }
|
|
3340
|
+
.editor-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; }
|
|
3341
|
+
.json-status { display: inline-flex; align-items: center; gap: 6px; color: var(--ok); font-size: 11px; }
|
|
3342
|
+
.json-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
3343
|
+
.json-status.invalid { color: var(--err); }
|
|
3344
|
+
.format-json { border: 0; padding: 3px 5px; background: transparent; color: var(--accent); font-size: 11px; font-weight: 650; cursor: pointer; }
|
|
3345
|
+
.row-actions { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 14px 0; }
|
|
2999
3346
|
.banner {
|
|
3000
|
-
display: none;
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3347
|
+
display: none; position: sticky; top: calc(var(--header) + 12px); z-index: 15;
|
|
3348
|
+
align-items: center; gap: 10px; margin: 0 0 16px; padding: 11px 12px 11px 14px;
|
|
3349
|
+
border: 1px solid color-mix(in srgb, var(--err) 30%, var(--line)); border-radius: 10px;
|
|
3350
|
+
background: var(--err-soft); color: var(--err); box-shadow: var(--shadow-sm); font-size: 13px;
|
|
3351
|
+
}
|
|
3352
|
+
.banner.show { display: flex; }
|
|
3353
|
+
.banner span { flex: 1; }
|
|
3354
|
+
.banner button { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 7px; background: transparent; color: currentColor; cursor: pointer; }
|
|
3355
|
+
.banner button:hover { background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
3356
|
+
.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); }
|
|
3005
3357
|
dialog {
|
|
3006
|
-
border: 1px solid var(--line); border-radius:
|
|
3007
|
-
color: var(--ink); width: min(
|
|
3008
|
-
}
|
|
3009
|
-
dialog
|
|
3010
|
-
dialog
|
|
3358
|
+
border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--bg-elev);
|
|
3359
|
+
color: var(--ink); width: min(720px, calc(100% - 24px)); max-width: calc(100vw - 24px); box-shadow: var(--shadow-lg);
|
|
3360
|
+
}
|
|
3361
|
+
dialog[open] { animation: dialog-in 160ms ease-out; }
|
|
3362
|
+
dialog::backdrop { background: rgb(12 14 20 / 0.58); backdrop-filter: blur(3px); }
|
|
3363
|
+
dialog form, .dialog-body { padding: 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
3364
|
+
.dialog-heading { display: flex; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
|
3365
|
+
.dialog-heading strong { font-size: 18px; letter-spacing: -0.02em; }
|
|
3366
|
+
.dialog-heading span { color: var(--muted); font-size: 12px; }
|
|
3011
3367
|
textarea {
|
|
3012
3368
|
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
3013
3369
|
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
3014
3370
|
font-family: var(--mono); font-size: 12px;
|
|
3015
3371
|
}
|
|
3016
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
3017
|
-
.
|
|
3372
|
+
.dialog-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding-top: 4px; }
|
|
3373
|
+
.dialog-actions .btn-danger { margin-right: auto; }
|
|
3374
|
+
.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); }
|
|
3018
3375
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
3019
3376
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
3020
3377
|
.status-2 { color: var(--ok); }
|
|
3021
3378
|
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
3022
3379
|
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
3380
|
+
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
3381
|
+
.form-grid .wide { grid-column: span 2; }
|
|
3023
3382
|
.route {
|
|
3024
|
-
display: grid; grid-template-columns:
|
|
3025
|
-
padding:
|
|
3026
|
-
}
|
|
3027
|
-
.
|
|
3383
|
+
display: grid; grid-template-columns: 84px minmax(0, 1fr) auto; gap: 10px; align-items: center;
|
|
3384
|
+
padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--bg-elev);
|
|
3385
|
+
}
|
|
3386
|
+
.route:first-child { border-radius: var(--radius) var(--radius) 0 0; }
|
|
3387
|
+
.route:last-child { border-bottom: 0; border-radius: 0 0 var(--radius) var(--radius); }
|
|
3388
|
+
.route-tools { display: flex; align-items: flex-end; gap: 12px; margin-bottom: 12px; }
|
|
3389
|
+
.route-tools .field { flex: 1; }
|
|
3390
|
+
.route-tools input { width: 100%; }
|
|
3391
|
+
.route-count { padding-bottom: 10px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3392
|
+
#route-list { max-height: 34rem; border: 1px solid var(--line); border-radius: var(--radius); overflow: auto; box-shadow: var(--shadow-xs); }
|
|
3393
|
+
.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; }
|
|
3394
|
+
@keyframes view-in { from { transform: translateY(3px); } to { transform: translateY(0); } }
|
|
3395
|
+
@keyframes dialog-in { from { transform: translateY(8px) scale(0.99); } to { transform: translateY(0) scale(1); } }
|
|
3396
|
+
@keyframes pulse { 50% { opacity: 0.45; } }
|
|
3397
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
|
|
3398
|
+
@media (forced-colors: active) { .json-editor pre { display: none; } .json-editor textarea { color: CanvasText; -webkit-text-fill-color: CanvasText; } }
|
|
3028
3399
|
@media (max-width: 800px) {
|
|
3400
|
+
:root { --header: 0px; }
|
|
3029
3401
|
header.top {
|
|
3030
|
-
align-items: stretch; flex-direction: column;
|
|
3031
|
-
padding
|
|
3402
|
+
position: relative; align-items: stretch; flex-direction: column;
|
|
3403
|
+
padding: max(12px, env(safe-area-inset-top)) 14px 12px;
|
|
3032
3404
|
}
|
|
3033
3405
|
input, select, textarea { font-size: 16px; }
|
|
3034
3406
|
.brand, .vendor, .controls { width: 100%; }
|
|
3407
|
+
.brand { gap: 10px; }
|
|
3408
|
+
.vendor { order: 2; border-radius: 9px; }
|
|
3035
3409
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
3410
|
+
.sync-status { display: none; }
|
|
3036
3411
|
/* The layout fills the screen. A single-column grid stretches every auto
|
|
3037
3412
|
row, so the tab bar grew with the leftover height and the buttons floated
|
|
3038
3413
|
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
@@ -3042,14 +3417,23 @@ var renderAdminDocument = (service) => {
|
|
|
3042
3417
|
align-content: start;
|
|
3043
3418
|
}
|
|
3044
3419
|
nav.side {
|
|
3045
|
-
align-self: start; height: auto;
|
|
3420
|
+
position: sticky; top: 0; z-index: 12; align-self: start; height: auto;
|
|
3046
3421
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
3047
3422
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
3048
|
-
padding: 8px;
|
|
3423
|
+
padding: 8px; box-shadow: var(--shadow-xs);
|
|
3049
3424
|
}
|
|
3050
|
-
nav
|
|
3425
|
+
.nav-label { display: none; }
|
|
3426
|
+
nav.side button { width: auto; white-space: nowrap; flex: none; }
|
|
3427
|
+
nav.side button[aria-current="true"]::before { inset: auto 9px -8px; width: auto; height: 3px; border-radius: 3px 3px 0 0; }
|
|
3051
3428
|
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
3052
|
-
|
|
3429
|
+
.form-grid { grid-template-columns: minmax(0, 1fr); }
|
|
3430
|
+
.form-grid .wide { grid-column: auto; }
|
|
3431
|
+
main { padding: 22px 14px 48px; }
|
|
3432
|
+
.view-head { margin-bottom: 18px; }
|
|
3433
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
3434
|
+
.card { padding: 14px; }
|
|
3435
|
+
.card .v { font-size: 20px; }
|
|
3436
|
+
.table-shell { border: 0; background: transparent; box-shadow: none; overflow: visible; }
|
|
3053
3437
|
table, thead, tbody, tr, th, td { display: block; }
|
|
3054
3438
|
thead { display: none; }
|
|
3055
3439
|
tr {
|
|
@@ -3064,50 +3448,63 @@ var renderAdminDocument = (service) => {
|
|
|
3064
3448
|
.route { grid-template-columns: 72px 1fr; }
|
|
3065
3449
|
.route button { grid-column: 1 / -1; }
|
|
3066
3450
|
}
|
|
3451
|
+
@media (max-width: 460px) {
|
|
3452
|
+
.controls label.field { min-width: calc(50% - 4px); }
|
|
3453
|
+
.controls .icon-btn, .controls .btn { flex: 1; }
|
|
3454
|
+
.grid { grid-template-columns: minmax(0, 1fr); }
|
|
3455
|
+
.dialog-actions { flex-wrap: wrap; }
|
|
3456
|
+
.dialog-actions button { flex: 1; }
|
|
3457
|
+
.dialog-actions .btn-danger { flex-basis: 100%; margin-right: 0; }
|
|
3458
|
+
}
|
|
3067
3459
|
</style>
|
|
3068
3460
|
</head>
|
|
3069
3461
|
<body>
|
|
3070
3462
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
3071
3463
|
<header class="top">
|
|
3072
3464
|
<div class="brand">
|
|
3073
|
-
<div class="brand-
|
|
3074
|
-
<
|
|
3075
|
-
|
|
3465
|
+
<div class="brand-lockup">
|
|
3466
|
+
<div class="brand-mark" aria-hidden="true">
|
|
3467
|
+
<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>
|
|
3468
|
+
</div>
|
|
3469
|
+
<div class="brand-id">
|
|
3470
|
+
<strong>${name}</strong>
|
|
3471
|
+
<span>Mock admin</span>
|
|
3472
|
+
</div>
|
|
3076
3473
|
</div>
|
|
3077
3474
|
<div class="vendor" id="vendor" hidden></div>
|
|
3078
3475
|
</div>
|
|
3079
3476
|
<div class="controls">
|
|
3477
|
+
<span class="sync-status" id="sync-status" role="status" aria-live="polite">Ready</span>
|
|
3080
3478
|
<label class="field">Namespace
|
|
3081
3479
|
<select id="namespace" aria-label="Namespace"></select>
|
|
3082
3480
|
</label>
|
|
3083
3481
|
<label class="field">Admin key
|
|
3084
3482
|
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
3085
3483
|
</label>
|
|
3086
|
-
<button class="icon-btn" id="theme" type="button" aria-label="
|
|
3087
|
-
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
3484
|
+
<button class="icon-btn" id="theme" type="button" aria-label="Use dark theme" title="Use dark theme"></button>
|
|
3485
|
+
<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>
|
|
3088
3486
|
</div>
|
|
3089
3487
|
</header>
|
|
3090
3488
|
<div class="layout">
|
|
3091
3489
|
<nav class="side" id="nav" aria-label="Admin sections">
|
|
3092
|
-
<
|
|
3093
|
-
<button type="button" data-view="
|
|
3094
|
-
<button type="button" data-view="
|
|
3095
|
-
<button type="button" data-view="
|
|
3096
|
-
<button type="button" data-view="
|
|
3097
|
-
<button type="button" data-view="
|
|
3490
|
+
<span class="nav-label">Workspace</span>
|
|
3491
|
+
<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>
|
|
3492
|
+
<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>
|
|
3493
|
+
<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>
|
|
3494
|
+
<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>
|
|
3495
|
+
<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>
|
|
3496
|
+
<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>
|
|
3098
3497
|
</nav>
|
|
3099
3498
|
<main id="views">
|
|
3100
|
-
<
|
|
3499
|
+
<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>
|
|
3101
3500
|
<section class="view" id="view-overview">
|
|
3102
|
-
<h2>Overview</h2>
|
|
3103
|
-
<p class="lede">Health, clock, and the collections this namespace is holding.</p>
|
|
3501
|
+
<div class="view-head"><div><h2>Overview</h2><p class="lede">Health, clock, and the collections this namespace is holding.</p></div></div>
|
|
3104
3502
|
<div class="grid" id="overview-cards"></div>
|
|
3105
3503
|
<h3>Recent requests</h3>
|
|
3106
3504
|
<div id="overview-requests"></div>
|
|
3107
3505
|
</section>
|
|
3108
3506
|
<section class="view" id="view-state" hidden>
|
|
3109
|
-
<h2>State</h2>
|
|
3110
|
-
<p class="lede">Every collection in this namespace. Declared fields stay visible before any row exists; stored rows add whatever else they contain.</p>
|
|
3507
|
+
<div class="view-head"><div><h2>State</h2><p class="lede">Inspect and edit every collection in this namespace.</p></div></div>
|
|
3111
3508
|
<div class="split">
|
|
3112
3509
|
<div id="collections"></div>
|
|
3113
3510
|
<div>
|
|
@@ -3120,8 +3517,7 @@ var renderAdminDocument = (service) => {
|
|
|
3120
3517
|
</div>
|
|
3121
3518
|
</section>
|
|
3122
3519
|
<section class="view" id="view-clock" hidden>
|
|
3123
|
-
<h2>Clock</h2>
|
|
3124
|
-
<p class="lede">The clock every timestamp in this mock reads.</p>
|
|
3520
|
+
<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>
|
|
3125
3521
|
<div class="card" id="clock-readout"></div>
|
|
3126
3522
|
<div class="row-actions">
|
|
3127
3523
|
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
@@ -3140,21 +3536,22 @@ var renderAdminDocument = (service) => {
|
|
|
3140
3536
|
</form>
|
|
3141
3537
|
</section>
|
|
3142
3538
|
<section class="view" id="view-faults" hidden>
|
|
3143
|
-
<h2>Faults</h2>
|
|
3144
|
-
<p class="lede">Inject a response, a delay, or a named preset. Rules apply to this namespace.</p>
|
|
3539
|
+
<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>
|
|
3145
3540
|
<form id="fault-form" class="card stack">
|
|
3146
|
-
<
|
|
3147
|
-
<
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
<
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
<
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
<
|
|
3157
|
-
|
|
3541
|
+
<div class="form-grid">
|
|
3542
|
+
<label class="field wide">Preset
|
|
3543
|
+
<select id="fault-preset"><option value="">Custom rule</option></select>
|
|
3544
|
+
</label>
|
|
3545
|
+
<label class="field">Status
|
|
3546
|
+
<input id="fault-status" inputmode="numeric" placeholder="503">
|
|
3547
|
+
</label>
|
|
3548
|
+
<label class="field wide">Path prefix
|
|
3549
|
+
<input id="fault-path" placeholder="/v1">
|
|
3550
|
+
</label>
|
|
3551
|
+
<label class="field">Method
|
|
3552
|
+
<input id="fault-method" placeholder="GET">
|
|
3553
|
+
</label>
|
|
3554
|
+
</div>
|
|
3158
3555
|
<div class="dialog-actions">
|
|
3159
3556
|
<button class="btn-primary" type="submit">Add fault</button>
|
|
3160
3557
|
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
@@ -3163,26 +3560,31 @@ var renderAdminDocument = (service) => {
|
|
|
3163
3560
|
<div id="fault-list"></div>
|
|
3164
3561
|
</section>
|
|
3165
3562
|
<section class="view" id="view-journal" hidden>
|
|
3166
|
-
<h2>Journal</h2>
|
|
3167
|
-
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
3168
|
-
<div class="row-actions">
|
|
3169
|
-
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
3170
|
-
</div>
|
|
3563
|
+
<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>
|
|
3171
3564
|
<div id="journal"></div>
|
|
3172
3565
|
</section>
|
|
3173
3566
|
<section class="view" id="view-routes" hidden>
|
|
3174
|
-
<h2>Routes</h2>
|
|
3175
|
-
<
|
|
3567
|
+
<div class="view-head"><div><h2>Routes</h2><p class="lede">Explore shared controls and service-specific admin endpoints.</p></div></div>
|
|
3568
|
+
<div class="route-tools">
|
|
3569
|
+
<label class="field">Filter routes
|
|
3570
|
+
<input id="route-search" type="search" placeholder="Method, path, or extension" autocomplete="off">
|
|
3571
|
+
</label>
|
|
3572
|
+
<span class="route-count" id="route-count"></span>
|
|
3573
|
+
</div>
|
|
3176
3574
|
<div id="route-list"></div>
|
|
3177
|
-
<form id="route-form" class="card stack">
|
|
3575
|
+
<form id="route-form" class="card stack" style="margin-top: 18px">
|
|
3178
3576
|
<strong id="route-title">Select a route</strong>
|
|
3179
3577
|
<label class="field">JSON body
|
|
3180
|
-
<
|
|
3578
|
+
<div class="json-editor" id="route-editor" hidden>
|
|
3579
|
+
<pre aria-hidden="true"><code id="route-highlight"></code></pre>
|
|
3580
|
+
<textarea id="route-body" placeholder="{ }" spellcheck="false" aria-describedby="route-json-status" disabled></textarea>
|
|
3581
|
+
</div>
|
|
3582
|
+
<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>
|
|
3181
3583
|
</label>
|
|
3182
3584
|
<div class="dialog-actions">
|
|
3183
|
-
<button class="btn-primary" type="submit">Send</button>
|
|
3585
|
+
<button class="btn-primary" id="route-send" type="submit" disabled>Send request</button>
|
|
3184
3586
|
</div>
|
|
3185
|
-
<pre id="route-result">Response will show here.</pre>
|
|
3587
|
+
<pre class="json-view" id="route-result"><code>Response will show here.</code></pre>
|
|
3186
3588
|
</form>
|
|
3187
3589
|
</section>
|
|
3188
3590
|
</main>
|
|
@@ -3190,12 +3592,16 @@ var renderAdminDocument = (service) => {
|
|
|
3190
3592
|
</div>
|
|
3191
3593
|
<dialog id="editor">
|
|
3192
3594
|
<form method="dialog" id="editor-form">
|
|
3193
|
-
<strong id="editor-title">Record</strong>
|
|
3595
|
+
<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>
|
|
3194
3596
|
<label class="field">Id
|
|
3195
3597
|
<input id="editor-id" required>
|
|
3196
3598
|
</label>
|
|
3197
3599
|
<label class="field">JSON value
|
|
3198
|
-
<
|
|
3600
|
+
<div class="json-editor" id="record-editor">
|
|
3601
|
+
<pre aria-hidden="true"><code id="editor-highlight"></code></pre>
|
|
3602
|
+
<textarea id="editor-value" spellcheck="false" aria-describedby="editor-json-status"></textarea>
|
|
3603
|
+
</div>
|
|
3604
|
+
<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>
|
|
3199
3605
|
</label>
|
|
3200
3606
|
<div class="dialog-actions">
|
|
3201
3607
|
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
@@ -3237,8 +3643,8 @@ var describeExtension = (extension) => {
|
|
|
3237
3643
|
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
3238
3644
|
};
|
|
3239
3645
|
};
|
|
3240
|
-
var adminUiRoutes = (service, ui) => {
|
|
3241
|
-
const shell = () => renderAdminDocument(service);
|
|
3646
|
+
var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
3647
|
+
const shell = () => renderAdminDocument(service, adminPrefix);
|
|
3242
3648
|
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
3243
3649
|
return {
|
|
3244
3650
|
"GET /ui": () => htmlResponse(document3()),
|
|
@@ -3855,10 +4261,13 @@ var stateAdminRoutes = (routes) => {
|
|
|
3855
4261
|
};
|
|
3856
4262
|
|
|
3857
4263
|
// ../core/dist/version.js
|
|
3858
|
-
var PACKAGE_VERSION = true ? "
|
|
4264
|
+
var PACKAGE_VERSION = true ? "2.0.0" : UNRELEASED_VERSION;
|
|
3859
4265
|
|
|
3860
4266
|
// ../core/dist/webhooks.js
|
|
3861
|
-
var json5 = (status, body) => new Response(JSON.stringify(body), {
|
|
4267
|
+
var json5 = (status, body) => new Response(JSON.stringify(body), {
|
|
4268
|
+
status,
|
|
4269
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4270
|
+
});
|
|
3862
4271
|
var adminError3 = (status, message) => json5(status, { error: { type: "mockingbird_admin", message } });
|
|
3863
4272
|
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3864
4273
|
var parseEndpoint = (value) => {
|
|
@@ -3966,7 +4375,6 @@ var AT_HEADER = "x-mockingbird-at";
|
|
|
3966
4375
|
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
3967
4376
|
var DEFAULT_NAMESPACE = "default";
|
|
3968
4377
|
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
3969
|
-
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
3970
4378
|
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
3971
4379
|
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
3972
4380
|
var effects = /* @__PURE__ */ new WeakMap();
|
|
@@ -4007,6 +4415,22 @@ var operationMatcher = (document3) => {
|
|
|
4007
4415
|
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
4008
4416
|
};
|
|
4009
4417
|
var createRuntime = (options) => {
|
|
4418
|
+
const adminPrefix = resolveAdminPrefix(options.adminPrefix);
|
|
4419
|
+
const internalPaths = Object.entries(options.document?.paths ?? {}).filter(([, item]) => item["x-mockingbird-internal"] === true).map(([path]) => path);
|
|
4420
|
+
for (const path of internalPaths) {
|
|
4421
|
+
if (!path.startsWith(`${ADMIN_PREFIX}/blobs/`))
|
|
4422
|
+
throw new Error(`internal transport ${path} must live under ${ADMIN_PREFIX}/blobs/`);
|
|
4423
|
+
}
|
|
4424
|
+
assertAdminPrefixAvailable(adminPrefix, Object.keys(options.document?.paths ?? {}).filter((path) => !internalPaths.includes(path)));
|
|
4425
|
+
const internalMatch = (path) => {
|
|
4426
|
+
for (const pattern of internalPaths) {
|
|
4427
|
+
const actual = path.split("/");
|
|
4428
|
+
const expected = pattern.split("/");
|
|
4429
|
+
if (actual.length === expected.length && expected.every((part, i) => part.startsWith("{") || part === actual[i]))
|
|
4430
|
+
return pattern;
|
|
4431
|
+
}
|
|
4432
|
+
return void 0;
|
|
4433
|
+
};
|
|
4010
4434
|
assertAdminUi(options.adminUi);
|
|
4011
4435
|
const sqlite = bootSqlite(options.sqlite);
|
|
4012
4436
|
const clock = options.clock ?? createClock();
|
|
@@ -4021,6 +4445,20 @@ var createRuntime = (options) => {
|
|
|
4021
4445
|
const instances = /* @__PURE__ */ new Map();
|
|
4022
4446
|
const publicNamespaces = /* @__PURE__ */ new Set();
|
|
4023
4447
|
const branchRngs = /* @__PURE__ */ new Map();
|
|
4448
|
+
const namespaceClocks = /* @__PURE__ */ new Map();
|
|
4449
|
+
const namespaceForStorage = /* @__PURE__ */ new Map();
|
|
4450
|
+
let isolatedNamespaces = false;
|
|
4451
|
+
const namespaceClock = (name = DEFAULT_NAMESPACE) => {
|
|
4452
|
+
if (!isolatedNamespaces || name === DEFAULT_NAMESPACE)
|
|
4453
|
+
return clock;
|
|
4454
|
+
let found = namespaceClocks.get(name);
|
|
4455
|
+
if (!found) {
|
|
4456
|
+
found = createClock();
|
|
4457
|
+
namespaceClocks.set(name, found);
|
|
4458
|
+
}
|
|
4459
|
+
return found;
|
|
4460
|
+
};
|
|
4461
|
+
const fleetPoints = /* @__PURE__ */ new WeakMap();
|
|
4024
4462
|
const timelines = /* @__PURE__ */ new Map();
|
|
4025
4463
|
const branchStorage = /* @__PURE__ */ new Map();
|
|
4026
4464
|
const captured = /* @__PURE__ */ new Map();
|
|
@@ -4034,17 +4472,22 @@ var createRuntime = (options) => {
|
|
|
4034
4472
|
if (!NAMESPACE_PATTERN.test(key) || !NAMESPACE_PATTERN.test(publicNamespace)) {
|
|
4035
4473
|
throw new RangeError(`namespace must match ${NAMESPACE_PATTERN}: ${JSON.stringify(publicNamespace)}`);
|
|
4036
4474
|
}
|
|
4475
|
+
const createdContextRng = isolatedRng ?? (isolatedNamespaces && key !== DEFAULT_NAMESPACE ? createRng(options.seed ?? 0) : rng);
|
|
4037
4476
|
const created = options.create({
|
|
4038
4477
|
namespace: storageNamespace(key),
|
|
4039
4478
|
publicNamespace,
|
|
4479
|
+
adminPrefix,
|
|
4040
4480
|
sqlite,
|
|
4041
|
-
clock,
|
|
4042
|
-
rng:
|
|
4481
|
+
clock: namespaceClock(publicNamespace),
|
|
4482
|
+
rng: createdContextRng
|
|
4043
4483
|
});
|
|
4044
4484
|
instances.set(key, created);
|
|
4485
|
+
namespaceForStorage.set(key, publicNamespace);
|
|
4045
4486
|
publicNamespaces.add(publicNamespace);
|
|
4046
4487
|
if (isolatedRng)
|
|
4047
4488
|
branchRngs.set(key, isolatedRng);
|
|
4489
|
+
else if (isolatedNamespaces && key !== DEFAULT_NAMESPACE)
|
|
4490
|
+
branchRngs.set(key, createdContextRng);
|
|
4048
4491
|
return created;
|
|
4049
4492
|
};
|
|
4050
4493
|
const instance = (name = DEFAULT_NAMESPACE) => instanceFor(name);
|
|
@@ -4062,7 +4505,7 @@ var createRuntime = (options) => {
|
|
|
4062
4505
|
captured.set(storage, snapshot2);
|
|
4063
4506
|
return Object.freeze({
|
|
4064
4507
|
snapshot: snapshot2,
|
|
4065
|
-
clock: Object.freeze(
|
|
4508
|
+
clock: Object.freeze(namespaceClock(namespaceForStorage.get(storage) ?? storage).state()),
|
|
4066
4509
|
rngState: (branchRngs.get(storage) ?? rng).state()
|
|
4067
4510
|
});
|
|
4068
4511
|
};
|
|
@@ -4072,7 +4515,7 @@ var createRuntime = (options) => {
|
|
|
4072
4515
|
return found;
|
|
4073
4516
|
instance(name);
|
|
4074
4517
|
found = new Timeline({
|
|
4075
|
-
now:
|
|
4518
|
+
now: namespaceClock(name).now,
|
|
4076
4519
|
...options.maxCheckpoints !== void 0 ? { maxCheckpoints: options.maxCheckpoints } : {}
|
|
4077
4520
|
});
|
|
4078
4521
|
found.commit(capture(name));
|
|
@@ -4099,12 +4542,13 @@ var createRuntime = (options) => {
|
|
|
4099
4542
|
const point = history2.checkout("main", at);
|
|
4100
4543
|
restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
|
|
4101
4544
|
captured.set(namespace, point.value.snapshot);
|
|
4102
|
-
rng.setState(point.value.rngState);
|
|
4103
|
-
|
|
4545
|
+
(branchRngs.get(namespace) ?? rng).setState(point.value.rngState);
|
|
4546
|
+
const selectedClock = namespaceClock(namespace);
|
|
4547
|
+
selectedClock.set(point.value.clock.now);
|
|
4104
4548
|
if (point.value.clock.frozen)
|
|
4105
|
-
|
|
4549
|
+
selectedClock.freeze();
|
|
4106
4550
|
else
|
|
4107
|
-
|
|
4551
|
+
selectedClock.unfreeze();
|
|
4108
4552
|
}
|
|
4109
4553
|
return namespace;
|
|
4110
4554
|
}
|
|
@@ -4162,11 +4606,12 @@ var createRuntime = (options) => {
|
|
|
4162
4606
|
const storage = ensureBranch(namespace, branchName);
|
|
4163
4607
|
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
4164
4608
|
captured.set(storage, point.value.snapshot);
|
|
4165
|
-
|
|
4609
|
+
const selectedClock = namespaceClock(namespace);
|
|
4610
|
+
selectedClock.set(point.value.clock.now);
|
|
4166
4611
|
if (point.value.clock.frozen)
|
|
4167
|
-
|
|
4612
|
+
selectedClock.freeze();
|
|
4168
4613
|
else
|
|
4169
|
-
|
|
4614
|
+
selectedClock.unfreeze();
|
|
4170
4615
|
(branchRngs.get(storage) ?? rng).setState(point.value.rngState);
|
|
4171
4616
|
};
|
|
4172
4617
|
const reset = async (name = DEFAULT_NAMESPACE) => {
|
|
@@ -4174,10 +4619,21 @@ var createRuntime = (options) => {
|
|
|
4174
4619
|
options.webhooks?.clear();
|
|
4175
4620
|
for (const each of instances.values())
|
|
4176
4621
|
await each.reset();
|
|
4622
|
+
for (const each of branchRngs.values())
|
|
4623
|
+
each.reset();
|
|
4177
4624
|
timelines.clear();
|
|
4625
|
+
for (const storage of branchStorage.values()) {
|
|
4626
|
+
instances.delete(storage);
|
|
4627
|
+
branchRngs.delete(storage);
|
|
4628
|
+
namespaceForStorage.delete(storage);
|
|
4629
|
+
}
|
|
4178
4630
|
branchStorage.clear();
|
|
4179
|
-
branchRngs.clear();
|
|
4180
4631
|
captured.clear();
|
|
4632
|
+
faults.clear();
|
|
4633
|
+
clock.reset();
|
|
4634
|
+
rng.reset();
|
|
4635
|
+
for (const each of namespaceClocks.values())
|
|
4636
|
+
each.reset();
|
|
4181
4637
|
return;
|
|
4182
4638
|
}
|
|
4183
4639
|
options.webhooks?.clear(name);
|
|
@@ -4195,11 +4651,18 @@ var createRuntime = (options) => {
|
|
|
4195
4651
|
else
|
|
4196
4652
|
clearNamespace(sqlite, storageNamespace(storage));
|
|
4197
4653
|
branchStorage.delete(mapping);
|
|
4654
|
+
instances.delete(storage);
|
|
4655
|
+
namespaceForStorage.delete(storage);
|
|
4198
4656
|
branchRngs.delete(storage);
|
|
4199
4657
|
captured.delete(storage);
|
|
4200
4658
|
}
|
|
4201
4659
|
timelines.delete(name);
|
|
4202
4660
|
captured.delete(name);
|
|
4661
|
+
faults.restore(name, { rules: [], rngState: createRng(options.seed ?? 0).state() });
|
|
4662
|
+
namespaceClock(name).reset();
|
|
4663
|
+
(branchRngs.get(name) ?? rng).reset();
|
|
4664
|
+
journal.clear(name);
|
|
4665
|
+
metrics.reset(name);
|
|
4203
4666
|
};
|
|
4204
4667
|
const snapshot = (name = DEFAULT_NAMESPACE) => {
|
|
4205
4668
|
return checkpoint(name, "main").value.snapshot;
|
|
@@ -4237,6 +4700,53 @@ var createRuntime = (options) => {
|
|
|
4237
4700
|
rng,
|
|
4238
4701
|
credentials,
|
|
4239
4702
|
webhooks: options.webhooks,
|
|
4703
|
+
isolateNamespaces() {
|
|
4704
|
+
if (publicNamespaces.size > 1)
|
|
4705
|
+
throw new Error("enable namespace isolation before creating named namespaces");
|
|
4706
|
+
isolatedNamespaces = true;
|
|
4707
|
+
faults.isolateNamespaces();
|
|
4708
|
+
},
|
|
4709
|
+
namespaceClock,
|
|
4710
|
+
namespaceOf(request) {
|
|
4711
|
+
const url = new URL(request.url);
|
|
4712
|
+
const prefixed = matchNamespacePath(url.pathname, adminPrefix);
|
|
4713
|
+
const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
|
|
4714
|
+
const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
|
|
4715
|
+
if (isAdminPath(path, adminPrefix))
|
|
4716
|
+
return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
|
|
4717
|
+
if (selected !== void 0)
|
|
4718
|
+
return selected;
|
|
4719
|
+
const credential = options.credential?.(request);
|
|
4720
|
+
return (credential !== void 0 ? credentials.get(credential) : void 0) ?? DEFAULT_NAMESPACE;
|
|
4721
|
+
},
|
|
4722
|
+
fleetSnapshot(namespace) {
|
|
4723
|
+
instance(namespace);
|
|
4724
|
+
const handle = {};
|
|
4725
|
+
fleetPoints.set(handle, {
|
|
4726
|
+
namespace,
|
|
4727
|
+
state: capture(namespace),
|
|
4728
|
+
faults: faults.snapshot(namespace),
|
|
4729
|
+
webhooks: options.webhooks?.snapshot(namespace)
|
|
4730
|
+
});
|
|
4731
|
+
return handle;
|
|
4732
|
+
},
|
|
4733
|
+
fleetRestore(handle, namespace) {
|
|
4734
|
+
const point = typeof handle === "object" && handle !== null ? fleetPoints.get(handle) : void 0;
|
|
4735
|
+
if (!point || point.namespace !== namespace)
|
|
4736
|
+
throw new Error("invalid fleet checkpoint");
|
|
4737
|
+
restoreNamespace(sqlite, storageNamespace(namespace), point.state.snapshot);
|
|
4738
|
+
captured.set(namespace, point.state.snapshot);
|
|
4739
|
+
const selectedClock = namespaceClock(namespace);
|
|
4740
|
+
selectedClock.freeze();
|
|
4741
|
+
selectedClock.set(point.state.clock.now);
|
|
4742
|
+
if (!point.state.clock.frozen)
|
|
4743
|
+
selectedClock.unfreeze();
|
|
4744
|
+
(branchRngs.get(namespace) ?? rng).setState(point.state.rngState);
|
|
4745
|
+
faults.restore(namespace, point.faults);
|
|
4746
|
+
if (point.webhooks !== void 0)
|
|
4747
|
+
options.webhooks?.restore(namespace, point.webhooks);
|
|
4748
|
+
timelines.delete(namespace);
|
|
4749
|
+
},
|
|
4240
4750
|
applyPreset: (name, namespace = DEFAULT_NAMESPACE, overrides = {}) => {
|
|
4241
4751
|
const preset = options.presets?.[name];
|
|
4242
4752
|
if (!preset)
|
|
@@ -4268,7 +4778,7 @@ var createRuntime = (options) => {
|
|
|
4268
4778
|
state: (namespace = DEFAULT_NAMESPACE) => inspectState(stateScope(namespace)),
|
|
4269
4779
|
fetch: async (incoming) => {
|
|
4270
4780
|
let request = incoming;
|
|
4271
|
-
const prefixed =
|
|
4781
|
+
const prefixed = matchNamespacePath(new URL(request.url).pathname, adminPrefix);
|
|
4272
4782
|
if (prefixed) {
|
|
4273
4783
|
const url2 = new URL(request.url);
|
|
4274
4784
|
url2.pathname = prefixed[2] ?? "/";
|
|
@@ -4284,7 +4794,15 @@ var createRuntime = (options) => {
|
|
|
4284
4794
|
signal: request.signal
|
|
4285
4795
|
});
|
|
4286
4796
|
}
|
|
4287
|
-
|
|
4797
|
+
const internalUrl = new URL(request.url);
|
|
4798
|
+
const internalPath = isAdminPath(internalUrl.pathname, adminPrefix) ? `${ADMIN_PREFIX}${internalUrl.pathname.slice(adminPrefix.length)}` : "";
|
|
4799
|
+
const internal = internalMatch(internalPath);
|
|
4800
|
+
const obsoleteInternalPath = adminPrefix !== ADMIN_PREFIX && internalMatch(internalUrl.pathname) && !internal;
|
|
4801
|
+
if (internal) {
|
|
4802
|
+
internalUrl.pathname = internalPath;
|
|
4803
|
+
request = new Request(internalUrl, request);
|
|
4804
|
+
}
|
|
4805
|
+
let namespace = internal ? internalUrl.searchParams.get("namespace") ?? control.namespaceOf(request) : control.namespaceOf(request);
|
|
4288
4806
|
if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
4289
4807
|
const credential = options.credential(request);
|
|
4290
4808
|
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
@@ -4304,7 +4822,9 @@ var createRuntime = (options) => {
|
|
|
4304
4822
|
return copy;
|
|
4305
4823
|
}
|
|
4306
4824
|
};
|
|
4307
|
-
|
|
4825
|
+
if (obsoleteInternalPath)
|
|
4826
|
+
return stamp(adminFail(404, "internal transport is only available under adminPrefix"));
|
|
4827
|
+
const handled = internal ? void 0 : await control.handle(request);
|
|
4308
4828
|
if (handled)
|
|
4309
4829
|
return stamp(handled);
|
|
4310
4830
|
const started = monotonicNow();
|
|
@@ -4345,7 +4865,7 @@ var createRuntime = (options) => {
|
|
|
4345
4865
|
type: "mockingbird_admin",
|
|
4346
4866
|
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
4347
4867
|
}
|
|
4348
|
-
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
4868
|
+
}), { status: 400, headers: { "content-type": "application/json; charset=utf-8" } }));
|
|
4349
4869
|
}
|
|
4350
4870
|
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
4351
4871
|
log(400);
|
|
@@ -4412,6 +4932,7 @@ var createRuntime = (options) => {
|
|
|
4412
4932
|
startedAt: wallNow(),
|
|
4413
4933
|
wallNow,
|
|
4414
4934
|
clock,
|
|
4935
|
+
namespaceClock,
|
|
4415
4936
|
faults,
|
|
4416
4937
|
metrics,
|
|
4417
4938
|
journal,
|
|
@@ -4461,15 +4982,16 @@ var createRuntime = (options) => {
|
|
|
4461
4982
|
...options.admin?.(runtime) ?? {},
|
|
4462
4983
|
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
4463
4984
|
// maps `presets`, and a service route must not replace that list with a record.
|
|
4464
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
4985
|
+
...options.presets ? presetRoutes(options.presets, runtime, adminPrefix) : {},
|
|
4465
4986
|
...stateAdminRoutes({
|
|
4466
4987
|
open: stateScope,
|
|
4467
4988
|
afterWrite: (namespace) => {
|
|
4468
4989
|
checkpoint(namespace, "main");
|
|
4469
4990
|
}
|
|
4470
4991
|
}),
|
|
4471
|
-
...adminUiRoutes(options.name, options.adminUi)
|
|
4992
|
+
...adminUiRoutes(options.name, options.adminUi, adminPrefix)
|
|
4472
4993
|
},
|
|
4994
|
+
adminPrefix,
|
|
4473
4995
|
adminKey: options.adminKey
|
|
4474
4996
|
});
|
|
4475
4997
|
return defineMock(runtime);
|
|
@@ -4483,7 +5005,10 @@ var mutableResponse = (response) => {
|
|
|
4483
5005
|
return new Response(response.body, response);
|
|
4484
5006
|
}
|
|
4485
5007
|
};
|
|
4486
|
-
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5008
|
+
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5009
|
+
status,
|
|
5010
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
5011
|
+
});
|
|
4487
5012
|
var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
|
|
4488
5013
|
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4489
5014
|
var credentialRoutes = (registry) => ({
|
|
@@ -4535,7 +5060,7 @@ var credentialRoutes = (registry) => ({
|
|
|
4535
5060
|
return adminJson(200, { status: "ok" });
|
|
4536
5061
|
}
|
|
4537
5062
|
});
|
|
4538
|
-
var presetRoutes = (presets, runtime) => ({
|
|
5063
|
+
var presetRoutes = (presets, runtime, adminPrefix) => ({
|
|
4539
5064
|
"GET /faults/presets": () => {
|
|
4540
5065
|
const body = {
|
|
4541
5066
|
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
@@ -4545,7 +5070,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
4545
5070
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
4546
5071
|
const name = params.name;
|
|
4547
5072
|
if (!presets[name])
|
|
4548
|
-
return adminFail(404, `no fault preset ${name}; GET /
|
|
5073
|
+
return adminFail(404, `no fault preset ${name}; GET ${adminPrefix}/faults/presets`);
|
|
4549
5074
|
const overrides = isObject(body) ? body : {};
|
|
4550
5075
|
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
4551
5076
|
}
|
|
@@ -5046,9 +5571,10 @@ var createRuntime2 = (options = {}) => createRuntime({
|
|
|
5046
5571
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
5047
5572
|
...options.clock ? { clock: options.clock } : {},
|
|
5048
5573
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
5574
|
+
...options.adminPrefix !== void 0 ? { adminPrefix: options.adminPrefix } : {},
|
|
5049
5575
|
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
5050
5576
|
...options.onLog ? { onLog: options.onLog } : {},
|
|
5051
|
-
credential: slackCredential,
|
|
5577
|
+
credential: (request) => slackCredential(request, options.adminPrefix),
|
|
5052
5578
|
presets: SLACK_PRESETS,
|
|
5053
5579
|
create: ({ sqlite, namespace, publicNamespace, clock }) => new SlackAPI({
|
|
5054
5580
|
sqlite,
|
|
@@ -5063,11 +5589,11 @@ var createRuntime2 = (options = {}) => createRuntime({
|
|
|
5063
5589
|
// src/index.ts
|
|
5064
5590
|
var SLACK_NAMESPACE = "slack";
|
|
5065
5591
|
var WEBHOOK_PATH = /^\/services\/([^/]+\/[^/]+\/[^/]+)\/?$/;
|
|
5066
|
-
var slackCredential = (request) => {
|
|
5592
|
+
var slackCredential = (request, adminPrefix = ADMIN_PREFIX) => {
|
|
5067
5593
|
const token = bearerToken(request);
|
|
5068
5594
|
if (token) return token;
|
|
5069
5595
|
const path = new URL(request.url).pathname;
|
|
5070
|
-
return WEBHOOK_PATH.exec(path
|
|
5596
|
+
return WEBHOOK_PATH.exec(matchNamespacePath(path, adminPrefix)?.[2] ?? path)?.[1];
|
|
5071
5597
|
};
|
|
5072
5598
|
var isRecord7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5073
5599
|
var text = (status, body, headers = {}) => new Response(body, {
|
|
@@ -5667,6 +6193,9 @@ var SlackAPI = class {
|
|
|
5667
6193
|
};
|
|
5668
6194
|
|
|
5669
6195
|
export {
|
|
6196
|
+
createClock,
|
|
6197
|
+
resolveAdminPrefix,
|
|
6198
|
+
parseDuration,
|
|
5670
6199
|
document2 as document,
|
|
5671
6200
|
operationIds,
|
|
5672
6201
|
supportedOperationIds,
|
|
@@ -5685,4 +6214,4 @@ export {
|
|
|
5685
6214
|
slackCredential,
|
|
5686
6215
|
SlackAPI
|
|
5687
6216
|
};
|
|
5688
|
-
//# sourceMappingURL=chunk-
|
|
6217
|
+
//# sourceMappingURL=chunk-56E5SPJ7.js.map
|