@crvouga/mockingbird-service-slack 1.8.0 → 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 +23 -1
- package/DISCOVERY.md +55 -0
- package/README.md +1 -1
- package/SUPPORT.md +31 -0
- package/dist/{chunk-RJIEOQZA.js → chunk-56E5SPJ7.js} +629 -239
- package/dist/chunk-56E5SPJ7.js.map +7 -0
- package/dist/{chunk-BDX5J7DR.js → chunk-PVVAIER7.js} +42 -22
- package/dist/chunk-PVVAIER7.js.map +7 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +4 -3
- package/dist/index.js +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.js +2 -2
- package/openapi.yaml +1945 -0
- package/package.json +29 -4
- package/dist/chunk-BDX5J7DR.js.map +0 -7
- package/dist/chunk-RJIEOQZA.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()
|
|
@@ -339,7 +371,14 @@ var createControlPlane = (context) => {
|
|
|
339
371
|
return json(200, { status: "ok" });
|
|
340
372
|
}
|
|
341
373
|
};
|
|
342
|
-
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]) => {
|
|
343
382
|
const space = key.indexOf(" ");
|
|
344
383
|
return { method: key.slice(0, space), pattern: key.slice(space + 1), handler };
|
|
345
384
|
});
|
|
@@ -347,21 +386,13 @@ var createControlPlane = (context) => {
|
|
|
347
386
|
namespaceOf: headerNamespace,
|
|
348
387
|
async handle(request) {
|
|
349
388
|
const url = new URL(request.url);
|
|
350
|
-
if (url.pathname ===
|
|
351
|
-
return
|
|
352
|
-
status: "ok",
|
|
353
|
-
service: context.name,
|
|
354
|
-
uptimeMs: context.wallNow() - context.startedAt,
|
|
355
|
-
clock: context.clock.state(),
|
|
356
|
-
namespaces: context.namespaces().length,
|
|
357
|
-
adminUi: `${ADMIN_PREFIX}/ui`,
|
|
358
|
-
...context.describe()
|
|
359
|
-
});
|
|
389
|
+
if (url.pathname === `${adminPrefix}/health` && request.method === "GET") {
|
|
390
|
+
return health();
|
|
360
391
|
}
|
|
361
|
-
if (url.pathname
|
|
392
|
+
if (!isAdminPath(url.pathname, adminPrefix)) {
|
|
362
393
|
return void 0;
|
|
363
394
|
}
|
|
364
|
-
const path = url.pathname.slice(
|
|
395
|
+
const path = url.pathname.slice(adminPrefix.length) || "/";
|
|
365
396
|
const shell = request.method === "GET" && (path === "/ui" || path === "/ui/");
|
|
366
397
|
if (!shell && context.adminKey !== void 0 && request.headers.get(ADMIN_KEY_HEADER) !== context.adminKey) {
|
|
367
398
|
return adminError(401, `missing or wrong ${ADMIN_KEY_HEADER}`);
|
|
@@ -369,7 +400,12 @@ var createControlPlane = (context) => {
|
|
|
369
400
|
for (const route of routes) {
|
|
370
401
|
if (route.method !== request.method)
|
|
371
402
|
continue;
|
|
372
|
-
|
|
403
|
+
let params;
|
|
404
|
+
try {
|
|
405
|
+
params = matchRoute(route.pattern, path);
|
|
406
|
+
} catch {
|
|
407
|
+
return adminError(400, "invalid path encoding");
|
|
408
|
+
}
|
|
373
409
|
if (!params)
|
|
374
410
|
continue;
|
|
375
411
|
let body;
|
|
@@ -378,15 +414,18 @@ var createControlPlane = (context) => {
|
|
|
378
414
|
} catch {
|
|
379
415
|
return adminError(400, "request body is not valid JSON");
|
|
380
416
|
}
|
|
417
|
+
const namespace = adminNamespace(request, url);
|
|
418
|
+
if (!/^[A-Za-z0-9_.-]{1,64}$/.test(namespace))
|
|
419
|
+
return adminError(400, "invalid namespace");
|
|
381
420
|
return route.handler({
|
|
382
421
|
request,
|
|
383
422
|
url,
|
|
384
423
|
params,
|
|
385
|
-
namespace
|
|
424
|
+
namespace,
|
|
386
425
|
body
|
|
387
426
|
});
|
|
388
427
|
}
|
|
389
|
-
return adminError(404, `no admin route ${request.method} ${path}; GET ${
|
|
428
|
+
return adminError(404, `no admin route ${request.method} ${path}; GET ${adminPrefix} lists them`);
|
|
390
429
|
}
|
|
391
430
|
};
|
|
392
431
|
};
|
|
@@ -985,7 +1024,10 @@ var OutboxStore = class {
|
|
|
985
1024
|
return query.limit !== void 0 ? matched.slice(-Math.max(0, query.limit)) : matched;
|
|
986
1025
|
}
|
|
987
1026
|
};
|
|
988
|
-
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
|
+
});
|
|
989
1031
|
var parseSince = (value) => {
|
|
990
1032
|
if (value === null)
|
|
991
1033
|
return void 0;
|
|
@@ -1621,6 +1663,34 @@ dialog:not([open]) {
|
|
|
1621
1663
|
function isRecord2(value) {
|
|
1622
1664
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1623
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
|
+
}
|
|
1624
1694
|
function mountAdminBrand(doc, host, service, catalog) {
|
|
1625
1695
|
const clip = (value, max) => {
|
|
1626
1696
|
if (typeof value !== "string")
|
|
@@ -1982,14 +2052,16 @@ function bootAdmin(config) {
|
|
|
1982
2052
|
const state = {
|
|
1983
2053
|
namespace: "default",
|
|
1984
2054
|
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
1985
|
-
view: "overview",
|
|
2055
|
+
view: ["overview", "state", "clock", "faults", "journal", "routes"].includes(location.hash.slice(1)) ? location.hash.slice(1) : "overview",
|
|
1986
2056
|
collection: "",
|
|
1987
2057
|
shape: null,
|
|
1988
2058
|
route: null,
|
|
2059
|
+
routes: [],
|
|
1989
2060
|
editing: null
|
|
1990
2061
|
};
|
|
1991
2062
|
const standard = new Set(config.standardRoutes);
|
|
1992
2063
|
const banner = el("banner", HTMLElement);
|
|
2064
|
+
const bannerMessage = el("banner-message", HTMLElement);
|
|
1993
2065
|
const keyInput = el("admin-key", HTMLInputElement);
|
|
1994
2066
|
keyInput.value = state.key;
|
|
1995
2067
|
const params = new URLSearchParams(location.search);
|
|
@@ -2000,18 +2072,21 @@ function bootAdmin(config) {
|
|
|
2000
2072
|
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
2001
2073
|
params.delete("key");
|
|
2002
2074
|
const next = params.toString();
|
|
2003
|
-
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
2075
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : "") + location.hash);
|
|
2004
2076
|
}
|
|
2005
2077
|
const queryNamespace = params.get("namespace");
|
|
2006
2078
|
if (queryNamespace)
|
|
2007
2079
|
state.namespace = queryNamespace;
|
|
2008
2080
|
const showError = (error) => {
|
|
2009
|
-
|
|
2081
|
+
bannerMessage.textContent = error instanceof Error ? error.message : String(error);
|
|
2010
2082
|
banner.classList.add("show");
|
|
2083
|
+
banner.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
2011
2084
|
};
|
|
2012
2085
|
const clearError = () => {
|
|
2013
2086
|
banner.classList.remove("show");
|
|
2087
|
+
bannerMessage.textContent = "";
|
|
2014
2088
|
};
|
|
2089
|
+
el("banner-dismiss", HTMLButtonElement).addEventListener("click", clearError);
|
|
2015
2090
|
const api = async (method, path, body) => {
|
|
2016
2091
|
const headers = { accept: "application/json" };
|
|
2017
2092
|
if (state.key !== "")
|
|
@@ -2055,16 +2130,21 @@ function bootAdmin(config) {
|
|
|
2055
2130
|
namespace: () => state.namespace
|
|
2056
2131
|
};
|
|
2057
2132
|
const isPanelScript = (value) => typeof value === "function";
|
|
2133
|
+
const themeButton = el("theme", HTMLButtonElement);
|
|
2058
2134
|
const setTheme = (theme) => {
|
|
2059
2135
|
if (theme)
|
|
2060
2136
|
document.documentElement.dataset.theme = theme;
|
|
2061
2137
|
else
|
|
2062
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;
|
|
2063
2144
|
};
|
|
2064
2145
|
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
2146
|
+
setTheme(storedTheme === "dark" || storedTheme === "light" ? storedTheme : null);
|
|
2147
|
+
themeButton.addEventListener("click", () => {
|
|
2068
2148
|
const current = document.documentElement.dataset.theme;
|
|
2069
2149
|
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2070
2150
|
const next = dark ? "light" : "dark";
|
|
@@ -2083,6 +2163,7 @@ function bootAdmin(config) {
|
|
|
2083
2163
|
continue;
|
|
2084
2164
|
section.hidden = section.id !== `view-${view}`;
|
|
2085
2165
|
}
|
|
2166
|
+
history.replaceState(null, "", `${location.pathname}${location.search}#${encodeURIComponent(view)}`);
|
|
2086
2167
|
};
|
|
2087
2168
|
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
2088
2169
|
const target = event.target;
|
|
@@ -2097,7 +2178,7 @@ function bootAdmin(config) {
|
|
|
2097
2178
|
show(view);
|
|
2098
2179
|
refresh().catch(showError);
|
|
2099
2180
|
});
|
|
2100
|
-
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>`;
|
|
2101
2182
|
const table = (columns, rows) => {
|
|
2102
2183
|
if (rows.length === 0)
|
|
2103
2184
|
return '<p class="empty">Nothing here yet.</p>';
|
|
@@ -2107,16 +2188,22 @@ function bootAdmin(config) {
|
|
|
2107
2188
|
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
2108
2189
|
return `<tr${attrs}>${cells}</tr>`;
|
|
2109
2190
|
}).join("");
|
|
2110
|
-
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
2191
|
+
return `<div class="table-shell"><table><thead>${head}</thead><tbody>${body}</tbody></table></div>`;
|
|
2111
2192
|
};
|
|
2112
2193
|
const preview = (value) => {
|
|
2113
2194
|
const text2 = JSON.stringify(value) ?? "null";
|
|
2114
|
-
|
|
2195
|
+
const clipped = text2.length > 180 ? `${text2.slice(0, 180)}\u2026` : text2;
|
|
2196
|
+
return `<code class="json-inline">${highlightJsonText(clipped)}</code>`;
|
|
2115
2197
|
};
|
|
2116
2198
|
const when = (epoch) => {
|
|
2117
2199
|
const date = new Date(epoch);
|
|
2118
2200
|
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
2119
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
|
+
};
|
|
2120
2207
|
const clockCard = (epoch) => {
|
|
2121
2208
|
const date = new Date(epoch);
|
|
2122
2209
|
if (Number.isNaN(date.getTime()))
|
|
@@ -2236,7 +2323,7 @@ function bootAdmin(config) {
|
|
|
2236
2323
|
};
|
|
2237
2324
|
const renderOverview = async () => {
|
|
2238
2325
|
const [health, shape, clock, journal] = await Promise.all([
|
|
2239
|
-
originGet(
|
|
2326
|
+
originGet(`${config.adminPrefix}/health`),
|
|
2240
2327
|
api("GET", "/state"),
|
|
2241
2328
|
api("GET", "/clock"),
|
|
2242
2329
|
api("GET", "/requests?limit=8")
|
|
@@ -2253,7 +2340,7 @@ function bootAdmin(config) {
|
|
|
2253
2340
|
},
|
|
2254
2341
|
{
|
|
2255
2342
|
k: "Status",
|
|
2256
|
-
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>`
|
|
2257
2344
|
},
|
|
2258
2345
|
{ k: "Namespace", v: esc(state.namespace) },
|
|
2259
2346
|
{ k: "Collections", v: String(decoded.collections.length) },
|
|
@@ -2340,26 +2427,46 @@ function bootAdmin(config) {
|
|
|
2340
2427
|
const renderJournal = async () => {
|
|
2341
2428
|
const journal = await api("GET", "/requests?limit=100");
|
|
2342
2429
|
el("journal", HTMLElement).innerHTML = table([
|
|
2343
|
-
{ label: "When", cell: (row) => esc(row.at) },
|
|
2344
|
-
{
|
|
2430
|
+
{ label: "When", cell: (row) => esc(journalWhen(row.at)) },
|
|
2431
|
+
{
|
|
2432
|
+
label: "Method",
|
|
2433
|
+
cell: (row) => `<span class="method">${esc(row.method)}</span>`
|
|
2434
|
+
},
|
|
2345
2435
|
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2346
|
-
{
|
|
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
|
+
},
|
|
2347
2440
|
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
2348
2441
|
], decodeJournal(journal));
|
|
2349
2442
|
};
|
|
2350
|
-
const
|
|
2351
|
-
const
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
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
|
+
}
|
|
2355
2455
|
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2356
2456
|
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2357
2457
|
const parts = route.split(" ");
|
|
2358
2458
|
const method = parts[0] ?? "";
|
|
2359
2459
|
const path = parts.slice(1).join(" ");
|
|
2360
|
-
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>`;
|
|
2361
2461
|
}).join("");
|
|
2362
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
|
+
};
|
|
2363
2470
|
const renderers = {
|
|
2364
2471
|
overview: renderOverview,
|
|
2365
2472
|
state: renderState,
|
|
@@ -2368,19 +2475,50 @@ function bootAdmin(config) {
|
|
|
2368
2475
|
journal: renderJournal,
|
|
2369
2476
|
routes: renderRoutes
|
|
2370
2477
|
};
|
|
2478
|
+
let refreshToken = 0;
|
|
2371
2479
|
async function refresh() {
|
|
2480
|
+
const token = ++refreshToken;
|
|
2372
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";
|
|
2373
2487
|
const render = renderers[state.view];
|
|
2374
|
-
|
|
2375
|
-
|
|
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
|
+
}
|
|
2376
2499
|
}
|
|
2377
2500
|
const delay = (ms) => new Promise((resolve) => {
|
|
2378
2501
|
window.setTimeout(resolve, ms);
|
|
2379
2502
|
});
|
|
2380
|
-
const confirmAction = async (button, pending, done, work) => {
|
|
2503
|
+
const confirmAction = async (button, pending, done, work, confirmLabel) => {
|
|
2381
2504
|
if (button.dataset.pending === "1")
|
|
2382
2505
|
return;
|
|
2383
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;
|
|
2384
2522
|
const width = button.offsetWidth;
|
|
2385
2523
|
button.dataset.pending = "1";
|
|
2386
2524
|
button.classList.add("pressed");
|
|
@@ -2401,6 +2539,65 @@ function bootAdmin(config) {
|
|
|
2401
2539
|
delete button.dataset.pending;
|
|
2402
2540
|
}
|
|
2403
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
|
+
}
|
|
2404
2601
|
document.addEventListener("click", (event) => {
|
|
2405
2602
|
const target = event.target;
|
|
2406
2603
|
if (!(target instanceof Element))
|
|
@@ -2473,6 +2670,7 @@ function bootAdmin(config) {
|
|
|
2473
2670
|
} else {
|
|
2474
2671
|
editorValue.value = "{\n \n}";
|
|
2475
2672
|
}
|
|
2673
|
+
syncRecordEditor();
|
|
2476
2674
|
editor.showModal();
|
|
2477
2675
|
}
|
|
2478
2676
|
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -2502,10 +2700,10 @@ function bootAdmin(config) {
|
|
|
2502
2700
|
const id = state.editing;
|
|
2503
2701
|
if (!id)
|
|
2504
2702
|
return;
|
|
2505
|
-
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2703
|
+
confirmAction(editorDelete, "Deleting...", "Deleted", () => api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2506
2704
|
editor.close();
|
|
2507
2705
|
return renderState();
|
|
2508
|
-
}).catch(showError);
|
|
2706
|
+
}), "Confirm delete").catch(showError);
|
|
2509
2707
|
});
|
|
2510
2708
|
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2511
2709
|
if (!(button instanceof HTMLButtonElement))
|
|
@@ -2551,10 +2749,10 @@ function bootAdmin(config) {
|
|
|
2551
2749
|
}).then(() => renderFaults()).catch(showError);
|
|
2552
2750
|
});
|
|
2553
2751
|
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2554
|
-
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);
|
|
2555
2753
|
});
|
|
2556
2754
|
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2557
|
-
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);
|
|
2558
2756
|
});
|
|
2559
2757
|
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2560
2758
|
const target = event.target;
|
|
@@ -2568,9 +2766,15 @@ function bootAdmin(config) {
|
|
|
2568
2766
|
return;
|
|
2569
2767
|
state.route = route;
|
|
2570
2768
|
el("route-title", HTMLElement).textContent = route;
|
|
2769
|
+
el("route-send", HTMLButtonElement).disabled = false;
|
|
2571
2770
|
const method = route.split(" ")[0] ?? "";
|
|
2572
|
-
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();
|
|
2573
2776
|
});
|
|
2777
|
+
el("route-search", HTMLInputElement).addEventListener("input", paintRoutes);
|
|
2574
2778
|
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2575
2779
|
event.preventDefault();
|
|
2576
2780
|
const send = async () => {
|
|
@@ -2592,7 +2796,8 @@ function bootAdmin(config) {
|
|
|
2592
2796
|
}
|
|
2593
2797
|
}
|
|
2594
2798
|
const data = await api(method, concrete, body);
|
|
2595
|
-
|
|
2799
|
+
const text2 = JSON.stringify(data, null, 2) ?? "null";
|
|
2800
|
+
el("route-result", HTMLElement).innerHTML = `<code>${highlightJsonText(text2)}</code>`;
|
|
2596
2801
|
};
|
|
2597
2802
|
send().catch(showError);
|
|
2598
2803
|
});
|
|
@@ -2710,11 +2915,13 @@ function bootAdmin(config) {
|
|
|
2710
2915
|
views.append(section);
|
|
2711
2916
|
mountSqlExplorer(body, panelApi);
|
|
2712
2917
|
}
|
|
2918
|
+
show(document.getElementById(`view-${state.view}`) === null ? "overview" : state.view);
|
|
2713
2919
|
return refresh();
|
|
2714
2920
|
}).catch(showError);
|
|
2715
2921
|
}
|
|
2716
2922
|
var clientPrelude = [
|
|
2717
2923
|
isRecord2,
|
|
2924
|
+
highlightJsonText,
|
|
2718
2925
|
mountAdminBrand,
|
|
2719
2926
|
startAdminBrand,
|
|
2720
2927
|
faultPresetList,
|
|
@@ -2737,6 +2944,7 @@ ${entry}(${payload});
|
|
|
2737
2944
|
var defineMock = (runtime) => runtime;
|
|
2738
2945
|
var STANDARD_ADMIN_ROUTES = [
|
|
2739
2946
|
"GET /",
|
|
2947
|
+
"GET /health",
|
|
2740
2948
|
"POST /reset",
|
|
2741
2949
|
"GET /namespaces",
|
|
2742
2950
|
"GET /clock",
|
|
@@ -2816,11 +3024,11 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
2816
3024
|
}
|
|
2817
3025
|
});
|
|
2818
3026
|
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
2819
|
-
var renderAdminDocument = (service) => {
|
|
3027
|
+
var renderAdminDocument = (service, adminPrefix = ADMIN_PREFIX) => {
|
|
2820
3028
|
const name = escapeHtml(service);
|
|
2821
3029
|
const script = adminClientSource({
|
|
2822
3030
|
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
2823
|
-
adminPrefix
|
|
3031
|
+
adminPrefix,
|
|
2824
3032
|
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
2825
3033
|
brandsUrl: ADMIN_BRANDS_URL,
|
|
2826
3034
|
service
|
|
@@ -2837,75 +3045,99 @@ var renderAdminDocument = (service) => {
|
|
|
2837
3045
|
${CSS_RESET}
|
|
2838
3046
|
:root {
|
|
2839
3047
|
color-scheme: light;
|
|
2840
|
-
--bg: #
|
|
3048
|
+
--bg: #f7f8fb;
|
|
2841
3049
|
--bg-elev: #ffffff;
|
|
2842
|
-
--bg-muted: #
|
|
2843
|
-
--bg-inset: #
|
|
2844
|
-
--line: #
|
|
2845
|
-
--line-strong: #
|
|
2846
|
-
--ink: #
|
|
2847
|
-
--muted: #
|
|
2848
|
-
--faint: #
|
|
2849
|
-
--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;
|
|
2850
3059
|
--accent-ink: #ffffff;
|
|
2851
|
-
--accent-soft: #
|
|
2852
|
-
--ok: #
|
|
2853
|
-
--ok-soft: #
|
|
2854
|
-
--warn: #
|
|
2855
|
-
--warn-soft: #
|
|
2856
|
-
--err: #
|
|
2857
|
-
--err-soft: #
|
|
2858
|
-
--
|
|
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);
|
|
2859
3075
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2860
3076
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
2861
|
-
--radius:
|
|
2862
|
-
--header:
|
|
2863
|
-
--nav:
|
|
3077
|
+
--radius: 12px;
|
|
3078
|
+
--header: 72px;
|
|
3079
|
+
--nav: 224px;
|
|
2864
3080
|
}
|
|
2865
3081
|
:root[data-theme="dark"] {
|
|
2866
3082
|
color-scheme: dark;
|
|
2867
|
-
--bg: #
|
|
2868
|
-
--bg-elev: #
|
|
2869
|
-
--bg-muted: #
|
|
2870
|
-
--bg-inset: #
|
|
2871
|
-
--line: #
|
|
2872
|
-
--line-strong: #
|
|
2873
|
-
--ink: #
|
|
2874
|
-
--muted: #
|
|
2875
|
-
--faint: #
|
|
2876
|
-
--accent: #
|
|
2877
|
-
--accent-
|
|
2878
|
-
--accent-
|
|
2879
|
-
--
|
|
2880
|
-
--ok
|
|
2881
|
-
--
|
|
2882
|
-
--warn
|
|
2883
|
-
--
|
|
2884
|
-
--err
|
|
2885
|
-
--
|
|
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);
|
|
2886
3110
|
}
|
|
2887
3111
|
@media (prefers-color-scheme: dark) {
|
|
2888
3112
|
:root:not([data-theme="light"]) {
|
|
2889
3113
|
color-scheme: dark;
|
|
2890
|
-
--bg: #
|
|
2891
|
-
--bg-elev: #
|
|
2892
|
-
--bg-muted: #
|
|
2893
|
-
--bg-inset: #
|
|
2894
|
-
--line: #
|
|
2895
|
-
--line-strong: #
|
|
2896
|
-
--ink: #
|
|
2897
|
-
--muted: #
|
|
2898
|
-
--faint: #
|
|
2899
|
-
--accent: #
|
|
2900
|
-
--accent-
|
|
2901
|
-
--accent-
|
|
2902
|
-
--
|
|
2903
|
-
--ok
|
|
2904
|
-
--
|
|
2905
|
-
--warn
|
|
2906
|
-
--
|
|
2907
|
-
--err
|
|
2908
|
-
--
|
|
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);
|
|
2909
3141
|
}
|
|
2910
3142
|
}
|
|
2911
3143
|
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
@@ -2923,13 +3155,17 @@ var renderAdminDocument = (service) => {
|
|
|
2923
3155
|
padding-right: env(safe-area-inset-right);
|
|
2924
3156
|
-webkit-text-size-adjust: 100%;
|
|
2925
3157
|
}
|
|
2926
|
-
|
|
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; }
|
|
2927
3162
|
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
2928
3163
|
header.top {
|
|
2929
|
-
position: sticky; top: 0; z-index:
|
|
3164
|
+
position: sticky; top: 0; z-index: 20;
|
|
2930
3165
|
display: flex; align-items: center; gap: 12px;
|
|
2931
|
-
min-height: var(--header); padding:
|
|
2932
|
-
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);
|
|
2933
3169
|
border-bottom: 1px solid var(--line);
|
|
2934
3170
|
min-width: 0; max-width: 100%;
|
|
2935
3171
|
}
|
|
@@ -2937,12 +3173,21 @@ var renderAdminDocument = (service) => {
|
|
|
2937
3173
|
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
2938
3174
|
min-width: 0; max-width: 100%; margin-right: auto;
|
|
2939
3175
|
}
|
|
2940
|
-
.brand-
|
|
2941
|
-
.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; }
|
|
2942
3186
|
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
2943
3187
|
.vendor {
|
|
2944
3188
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
2945
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);
|
|
2946
3191
|
}
|
|
2947
3192
|
.vendor[hidden] { display: none; }
|
|
2948
3193
|
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
@@ -2954,12 +3199,17 @@ var renderAdminDocument = (service) => {
|
|
|
2954
3199
|
.vendor-sep { color: var(--faint); }
|
|
2955
3200
|
.vendor-link { color: inherit; text-decoration: none; }
|
|
2956
3201
|
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
2957
|
-
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items:
|
|
2958
|
-
|
|
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); }
|
|
2959
3207
|
label.field input, label.field select, .controls input[type="password"] {
|
|
2960
|
-
background-color: var(--bg-
|
|
2961
|
-
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);
|
|
2962
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; }
|
|
2963
3213
|
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
2964
3214
|
the text fields in the same form. Paint it like those fields. */
|
|
2965
3215
|
label.field select {
|
|
@@ -2976,12 +3226,20 @@ var renderAdminDocument = (service) => {
|
|
|
2976
3226
|
dialog .field > textarea { width: 100%; }
|
|
2977
3227
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
2978
3228
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
.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)); }
|
|
2985
3243
|
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
2986
3244
|
so the press itself has to stay visible. */
|
|
2987
3245
|
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
@@ -2997,93 +3255,159 @@ var renderAdminDocument = (service) => {
|
|
|
2997
3255
|
flex: 1 0 auto; min-width: 0;
|
|
2998
3256
|
}
|
|
2999
3257
|
nav.side {
|
|
3000
|
-
|
|
3258
|
+
position: sticky; top: var(--header); align-self: start; height: calc(100vh - var(--header));
|
|
3259
|
+
border-right: 1px solid var(--line); padding: 18px 12px;
|
|
3001
3260
|
display: flex; flex-direction: column; gap: 4px;
|
|
3002
|
-
background: var(--bg-elev);
|
|
3261
|
+
background: var(--bg-elev); overflow-y: auto;
|
|
3003
3262
|
}
|
|
3263
|
+
.nav-label { padding: 0 10px 8px; color: var(--faint); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
|
|
3004
3264
|
nav.side button {
|
|
3265
|
+
position: relative; display: flex; align-items: center; gap: 10px; width: 100%;
|
|
3005
3266
|
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
3006
|
-
padding: 9px 10px; min-height:
|
|
3007
|
-
}
|
|
3008
|
-
nav.side button
|
|
3009
|
-
|
|
3010
|
-
.
|
|
3011
|
-
.
|
|
3012
|
-
.
|
|
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; }
|
|
3013
3283
|
.card {
|
|
3014
3284
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
3015
|
-
padding:
|
|
3285
|
+
padding: 18px; box-shadow: var(--shadow-sm);
|
|
3016
3286
|
}
|
|
3017
|
-
.card
|
|
3018
|
-
.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; }
|
|
3019
3291
|
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
3020
|
-
.
|
|
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); }
|
|
3021
3297
|
.collection {
|
|
3022
3298
|
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
3023
|
-
border-radius:
|
|
3299
|
+
border-radius: 9px; padding: 10px 11px; min-height: 48px; color: var(--ink); cursor: pointer;
|
|
3024
3300
|
}
|
|
3025
|
-
.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); }
|
|
3026
3303
|
.collection small { display: block; color: var(--faint); }
|
|
3027
3304
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
3028
|
-
.chip {
|
|
3029
|
-
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
3030
|
-
border-radius: 999px; padding: 4px 8px;
|
|
3031
|
-
}
|
|
3305
|
+
.chip { font-family: var(--mono); font-size: 11px; background: var(--bg-muted); border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; }
|
|
3032
3306
|
.chip i { color: var(--faint); font-style: normal; }
|
|
3033
3307
|
.sql-layout {
|
|
3034
3308
|
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
3035
3309
|
}
|
|
3036
|
-
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
3037
|
-
.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); }
|
|
3038
3313
|
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
3039
|
-
table { width: 100%; border-
|
|
3040
|
-
|
|
3041
|
-
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; }
|
|
3042
3319
|
tr.clickable { cursor: pointer; }
|
|
3043
|
-
tr.clickable:hover { background: var(--
|
|
3320
|
+
tr.clickable:hover { background: var(--accent-soft); }
|
|
3044
3321
|
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
3045
3322
|
pre {
|
|
3046
3323
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
3047
|
-
background: var(--bg-inset); border-radius: 10px; padding:
|
|
3048
|
-
}
|
|
3049
|
-
.
|
|
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; }
|
|
3050
3346
|
.banner {
|
|
3051
|
-
display: none;
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
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); }
|
|
3056
3357
|
dialog {
|
|
3057
|
-
border: 1px solid var(--line); border-radius:
|
|
3058
|
-
color: var(--ink); width: min(
|
|
3059
|
-
}
|
|
3060
|
-
dialog
|
|
3061
|
-
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; }
|
|
3062
3367
|
textarea {
|
|
3063
3368
|
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
3064
3369
|
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
3065
3370
|
font-family: var(--mono); font-size: 12px;
|
|
3066
3371
|
}
|
|
3067
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
3068
|
-
.
|
|
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); }
|
|
3069
3375
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
3070
3376
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
3071
3377
|
.status-2 { color: var(--ok); }
|
|
3072
3378
|
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
3073
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; }
|
|
3074
3382
|
.route {
|
|
3075
|
-
display: grid; grid-template-columns:
|
|
3076
|
-
padding:
|
|
3077
|
-
}
|
|
3078
|
-
.
|
|
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; } }
|
|
3079
3399
|
@media (max-width: 800px) {
|
|
3400
|
+
:root { --header: 0px; }
|
|
3080
3401
|
header.top {
|
|
3081
|
-
align-items: stretch; flex-direction: column;
|
|
3082
|
-
padding
|
|
3402
|
+
position: relative; align-items: stretch; flex-direction: column;
|
|
3403
|
+
padding: max(12px, env(safe-area-inset-top)) 14px 12px;
|
|
3083
3404
|
}
|
|
3084
3405
|
input, select, textarea { font-size: 16px; }
|
|
3085
3406
|
.brand, .vendor, .controls { width: 100%; }
|
|
3407
|
+
.brand { gap: 10px; }
|
|
3408
|
+
.vendor { order: 2; border-radius: 9px; }
|
|
3086
3409
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
3410
|
+
.sync-status { display: none; }
|
|
3087
3411
|
/* The layout fills the screen. A single-column grid stretches every auto
|
|
3088
3412
|
row, so the tab bar grew with the leftover height and the buttons floated
|
|
3089
3413
|
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
@@ -3093,14 +3417,23 @@ var renderAdminDocument = (service) => {
|
|
|
3093
3417
|
align-content: start;
|
|
3094
3418
|
}
|
|
3095
3419
|
nav.side {
|
|
3096
|
-
align-self: start; height: auto;
|
|
3420
|
+
position: sticky; top: 0; z-index: 12; align-self: start; height: auto;
|
|
3097
3421
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
3098
3422
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
3099
|
-
padding: 8px;
|
|
3423
|
+
padding: 8px; box-shadow: var(--shadow-xs);
|
|
3100
3424
|
}
|
|
3101
|
-
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; }
|
|
3102
3428
|
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
3103
|
-
|
|
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; }
|
|
3104
3437
|
table, thead, tbody, tr, th, td { display: block; }
|
|
3105
3438
|
thead { display: none; }
|
|
3106
3439
|
tr {
|
|
@@ -3115,50 +3448,63 @@ var renderAdminDocument = (service) => {
|
|
|
3115
3448
|
.route { grid-template-columns: 72px 1fr; }
|
|
3116
3449
|
.route button { grid-column: 1 / -1; }
|
|
3117
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
|
+
}
|
|
3118
3459
|
</style>
|
|
3119
3460
|
</head>
|
|
3120
3461
|
<body>
|
|
3121
3462
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
3122
3463
|
<header class="top">
|
|
3123
3464
|
<div class="brand">
|
|
3124
|
-
<div class="brand-
|
|
3125
|
-
<
|
|
3126
|
-
|
|
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>
|
|
3127
3473
|
</div>
|
|
3128
3474
|
<div class="vendor" id="vendor" hidden></div>
|
|
3129
3475
|
</div>
|
|
3130
3476
|
<div class="controls">
|
|
3477
|
+
<span class="sync-status" id="sync-status" role="status" aria-live="polite">Ready</span>
|
|
3131
3478
|
<label class="field">Namespace
|
|
3132
3479
|
<select id="namespace" aria-label="Namespace"></select>
|
|
3133
3480
|
</label>
|
|
3134
3481
|
<label class="field">Admin key
|
|
3135
3482
|
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
3136
3483
|
</label>
|
|
3137
|
-
<button class="icon-btn" id="theme" type="button" aria-label="
|
|
3138
|
-
<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>
|
|
3139
3486
|
</div>
|
|
3140
3487
|
</header>
|
|
3141
3488
|
<div class="layout">
|
|
3142
3489
|
<nav class="side" id="nav" aria-label="Admin sections">
|
|
3143
|
-
<
|
|
3144
|
-
<button type="button" data-view="
|
|
3145
|
-
<button type="button" data-view="
|
|
3146
|
-
<button type="button" data-view="
|
|
3147
|
-
<button type="button" data-view="
|
|
3148
|
-
<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>
|
|
3149
3497
|
</nav>
|
|
3150
3498
|
<main id="views">
|
|
3151
|
-
<
|
|
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>
|
|
3152
3500
|
<section class="view" id="view-overview">
|
|
3153
|
-
<h2>Overview</h2>
|
|
3154
|
-
<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>
|
|
3155
3502
|
<div class="grid" id="overview-cards"></div>
|
|
3156
3503
|
<h3>Recent requests</h3>
|
|
3157
3504
|
<div id="overview-requests"></div>
|
|
3158
3505
|
</section>
|
|
3159
3506
|
<section class="view" id="view-state" hidden>
|
|
3160
|
-
<h2>State</h2>
|
|
3161
|
-
<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>
|
|
3162
3508
|
<div class="split">
|
|
3163
3509
|
<div id="collections"></div>
|
|
3164
3510
|
<div>
|
|
@@ -3171,8 +3517,7 @@ var renderAdminDocument = (service) => {
|
|
|
3171
3517
|
</div>
|
|
3172
3518
|
</section>
|
|
3173
3519
|
<section class="view" id="view-clock" hidden>
|
|
3174
|
-
<h2>Clock</h2>
|
|
3175
|
-
<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>
|
|
3176
3521
|
<div class="card" id="clock-readout"></div>
|
|
3177
3522
|
<div class="row-actions">
|
|
3178
3523
|
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
@@ -3191,21 +3536,22 @@ var renderAdminDocument = (service) => {
|
|
|
3191
3536
|
</form>
|
|
3192
3537
|
</section>
|
|
3193
3538
|
<section class="view" id="view-faults" hidden>
|
|
3194
|
-
<h2>Faults</h2>
|
|
3195
|
-
<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>
|
|
3196
3540
|
<form id="fault-form" class="card stack">
|
|
3197
|
-
<
|
|
3198
|
-
<
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
<
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
<
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
<
|
|
3208
|
-
|
|
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>
|
|
3209
3555
|
<div class="dialog-actions">
|
|
3210
3556
|
<button class="btn-primary" type="submit">Add fault</button>
|
|
3211
3557
|
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
@@ -3214,26 +3560,31 @@ var renderAdminDocument = (service) => {
|
|
|
3214
3560
|
<div id="fault-list"></div>
|
|
3215
3561
|
</section>
|
|
3216
3562
|
<section class="view" id="view-journal" hidden>
|
|
3217
|
-
<h2>Journal</h2>
|
|
3218
|
-
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
3219
|
-
<div class="row-actions">
|
|
3220
|
-
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
3221
|
-
</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>
|
|
3222
3564
|
<div id="journal"></div>
|
|
3223
3565
|
</section>
|
|
3224
3566
|
<section class="view" id="view-routes" hidden>
|
|
3225
|
-
<h2>Routes</h2>
|
|
3226
|
-
<
|
|
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>
|
|
3227
3574
|
<div id="route-list"></div>
|
|
3228
|
-
<form id="route-form" class="card stack">
|
|
3575
|
+
<form id="route-form" class="card stack" style="margin-top: 18px">
|
|
3229
3576
|
<strong id="route-title">Select a route</strong>
|
|
3230
3577
|
<label class="field">JSON body
|
|
3231
|
-
<
|
|
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>
|
|
3232
3583
|
</label>
|
|
3233
3584
|
<div class="dialog-actions">
|
|
3234
|
-
<button class="btn-primary" type="submit">Send</button>
|
|
3585
|
+
<button class="btn-primary" id="route-send" type="submit" disabled>Send request</button>
|
|
3235
3586
|
</div>
|
|
3236
|
-
<pre id="route-result">Response will show here.</pre>
|
|
3587
|
+
<pre class="json-view" id="route-result"><code>Response will show here.</code></pre>
|
|
3237
3588
|
</form>
|
|
3238
3589
|
</section>
|
|
3239
3590
|
</main>
|
|
@@ -3241,12 +3592,16 @@ var renderAdminDocument = (service) => {
|
|
|
3241
3592
|
</div>
|
|
3242
3593
|
<dialog id="editor">
|
|
3243
3594
|
<form method="dialog" id="editor-form">
|
|
3244
|
-
<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>
|
|
3245
3596
|
<label class="field">Id
|
|
3246
3597
|
<input id="editor-id" required>
|
|
3247
3598
|
</label>
|
|
3248
3599
|
<label class="field">JSON value
|
|
3249
|
-
<
|
|
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>
|
|
3250
3605
|
</label>
|
|
3251
3606
|
<div class="dialog-actions">
|
|
3252
3607
|
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
@@ -3288,8 +3643,8 @@ var describeExtension = (extension) => {
|
|
|
3288
3643
|
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
3289
3644
|
};
|
|
3290
3645
|
};
|
|
3291
|
-
var adminUiRoutes = (service, ui) => {
|
|
3292
|
-
const shell = () => renderAdminDocument(service);
|
|
3646
|
+
var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
3647
|
+
const shell = () => renderAdminDocument(service, adminPrefix);
|
|
3293
3648
|
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
3294
3649
|
return {
|
|
3295
3650
|
"GET /ui": () => htmlResponse(document3()),
|
|
@@ -3906,10 +4261,13 @@ var stateAdminRoutes = (routes) => {
|
|
|
3906
4261
|
};
|
|
3907
4262
|
|
|
3908
4263
|
// ../core/dist/version.js
|
|
3909
|
-
var PACKAGE_VERSION = true ? "
|
|
4264
|
+
var PACKAGE_VERSION = true ? "2.0.0" : UNRELEASED_VERSION;
|
|
3910
4265
|
|
|
3911
4266
|
// ../core/dist/webhooks.js
|
|
3912
|
-
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
|
+
});
|
|
3913
4271
|
var adminError3 = (status, message) => json5(status, { error: { type: "mockingbird_admin", message } });
|
|
3914
4272
|
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3915
4273
|
var parseEndpoint = (value) => {
|
|
@@ -4017,7 +4375,6 @@ var AT_HEADER = "x-mockingbird-at";
|
|
|
4017
4375
|
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
4018
4376
|
var DEFAULT_NAMESPACE = "default";
|
|
4019
4377
|
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4020
|
-
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
4021
4378
|
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4022
4379
|
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
4023
4380
|
var effects = /* @__PURE__ */ new WeakMap();
|
|
@@ -4058,6 +4415,22 @@ var operationMatcher = (document3) => {
|
|
|
4058
4415
|
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
4059
4416
|
};
|
|
4060
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
|
+
};
|
|
4061
4434
|
assertAdminUi(options.adminUi);
|
|
4062
4435
|
const sqlite = bootSqlite(options.sqlite);
|
|
4063
4436
|
const clock = options.clock ?? createClock();
|
|
@@ -4103,6 +4476,7 @@ var createRuntime = (options) => {
|
|
|
4103
4476
|
const created = options.create({
|
|
4104
4477
|
namespace: storageNamespace(key),
|
|
4105
4478
|
publicNamespace,
|
|
4479
|
+
adminPrefix,
|
|
4106
4480
|
sqlite,
|
|
4107
4481
|
clock: namespaceClock(publicNamespace),
|
|
4108
4482
|
rng: createdContextRng
|
|
@@ -4335,10 +4709,10 @@ var createRuntime = (options) => {
|
|
|
4335
4709
|
namespaceClock,
|
|
4336
4710
|
namespaceOf(request) {
|
|
4337
4711
|
const url = new URL(request.url);
|
|
4338
|
-
const prefixed =
|
|
4712
|
+
const prefixed = matchNamespacePath(url.pathname, adminPrefix);
|
|
4339
4713
|
const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
|
|
4340
4714
|
const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
|
|
4341
|
-
if (path
|
|
4715
|
+
if (isAdminPath(path, adminPrefix))
|
|
4342
4716
|
return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
|
|
4343
4717
|
if (selected !== void 0)
|
|
4344
4718
|
return selected;
|
|
@@ -4404,7 +4778,7 @@ var createRuntime = (options) => {
|
|
|
4404
4778
|
state: (namespace = DEFAULT_NAMESPACE) => inspectState(stateScope(namespace)),
|
|
4405
4779
|
fetch: async (incoming) => {
|
|
4406
4780
|
let request = incoming;
|
|
4407
|
-
const prefixed =
|
|
4781
|
+
const prefixed = matchNamespacePath(new URL(request.url).pathname, adminPrefix);
|
|
4408
4782
|
if (prefixed) {
|
|
4409
4783
|
const url2 = new URL(request.url);
|
|
4410
4784
|
url2.pathname = prefixed[2] ?? "/";
|
|
@@ -4420,7 +4794,15 @@ var createRuntime = (options) => {
|
|
|
4420
4794
|
signal: request.signal
|
|
4421
4795
|
});
|
|
4422
4796
|
}
|
|
4423
|
-
|
|
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);
|
|
4424
4806
|
if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
4425
4807
|
const credential = options.credential(request);
|
|
4426
4808
|
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
@@ -4440,7 +4822,9 @@ var createRuntime = (options) => {
|
|
|
4440
4822
|
return copy;
|
|
4441
4823
|
}
|
|
4442
4824
|
};
|
|
4443
|
-
|
|
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);
|
|
4444
4828
|
if (handled)
|
|
4445
4829
|
return stamp(handled);
|
|
4446
4830
|
const started = monotonicNow();
|
|
@@ -4481,7 +4865,7 @@ var createRuntime = (options) => {
|
|
|
4481
4865
|
type: "mockingbird_admin",
|
|
4482
4866
|
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
4483
4867
|
}
|
|
4484
|
-
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
4868
|
+
}), { status: 400, headers: { "content-type": "application/json; charset=utf-8" } }));
|
|
4485
4869
|
}
|
|
4486
4870
|
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
4487
4871
|
log(400);
|
|
@@ -4598,15 +4982,16 @@ var createRuntime = (options) => {
|
|
|
4598
4982
|
...options.admin?.(runtime) ?? {},
|
|
4599
4983
|
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
4600
4984
|
// maps `presets`, and a service route must not replace that list with a record.
|
|
4601
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
4985
|
+
...options.presets ? presetRoutes(options.presets, runtime, adminPrefix) : {},
|
|
4602
4986
|
...stateAdminRoutes({
|
|
4603
4987
|
open: stateScope,
|
|
4604
4988
|
afterWrite: (namespace) => {
|
|
4605
4989
|
checkpoint(namespace, "main");
|
|
4606
4990
|
}
|
|
4607
4991
|
}),
|
|
4608
|
-
...adminUiRoutes(options.name, options.adminUi)
|
|
4992
|
+
...adminUiRoutes(options.name, options.adminUi, adminPrefix)
|
|
4609
4993
|
},
|
|
4994
|
+
adminPrefix,
|
|
4610
4995
|
adminKey: options.adminKey
|
|
4611
4996
|
});
|
|
4612
4997
|
return defineMock(runtime);
|
|
@@ -4620,7 +5005,10 @@ var mutableResponse = (response) => {
|
|
|
4620
5005
|
return new Response(response.body, response);
|
|
4621
5006
|
}
|
|
4622
5007
|
};
|
|
4623
|
-
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
|
+
});
|
|
4624
5012
|
var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
|
|
4625
5013
|
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4626
5014
|
var credentialRoutes = (registry) => ({
|
|
@@ -4672,7 +5060,7 @@ var credentialRoutes = (registry) => ({
|
|
|
4672
5060
|
return adminJson(200, { status: "ok" });
|
|
4673
5061
|
}
|
|
4674
5062
|
});
|
|
4675
|
-
var presetRoutes = (presets, runtime) => ({
|
|
5063
|
+
var presetRoutes = (presets, runtime, adminPrefix) => ({
|
|
4676
5064
|
"GET /faults/presets": () => {
|
|
4677
5065
|
const body = {
|
|
4678
5066
|
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
@@ -4682,7 +5070,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
4682
5070
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
4683
5071
|
const name = params.name;
|
|
4684
5072
|
if (!presets[name])
|
|
4685
|
-
return adminFail(404, `no fault preset ${name}; GET /
|
|
5073
|
+
return adminFail(404, `no fault preset ${name}; GET ${adminPrefix}/faults/presets`);
|
|
4686
5074
|
const overrides = isObject(body) ? body : {};
|
|
4687
5075
|
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
4688
5076
|
}
|
|
@@ -5183,9 +5571,10 @@ var createRuntime2 = (options = {}) => createRuntime({
|
|
|
5183
5571
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
5184
5572
|
...options.clock ? { clock: options.clock } : {},
|
|
5185
5573
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
5574
|
+
...options.adminPrefix !== void 0 ? { adminPrefix: options.adminPrefix } : {},
|
|
5186
5575
|
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
5187
5576
|
...options.onLog ? { onLog: options.onLog } : {},
|
|
5188
|
-
credential: slackCredential,
|
|
5577
|
+
credential: (request) => slackCredential(request, options.adminPrefix),
|
|
5189
5578
|
presets: SLACK_PRESETS,
|
|
5190
5579
|
create: ({ sqlite, namespace, publicNamespace, clock }) => new SlackAPI({
|
|
5191
5580
|
sqlite,
|
|
@@ -5200,11 +5589,11 @@ var createRuntime2 = (options = {}) => createRuntime({
|
|
|
5200
5589
|
// src/index.ts
|
|
5201
5590
|
var SLACK_NAMESPACE = "slack";
|
|
5202
5591
|
var WEBHOOK_PATH = /^\/services\/([^/]+\/[^/]+\/[^/]+)\/?$/;
|
|
5203
|
-
var slackCredential = (request) => {
|
|
5592
|
+
var slackCredential = (request, adminPrefix = ADMIN_PREFIX) => {
|
|
5204
5593
|
const token = bearerToken(request);
|
|
5205
5594
|
if (token) return token;
|
|
5206
5595
|
const path = new URL(request.url).pathname;
|
|
5207
|
-
return WEBHOOK_PATH.exec(path
|
|
5596
|
+
return WEBHOOK_PATH.exec(matchNamespacePath(path, adminPrefix)?.[2] ?? path)?.[1];
|
|
5208
5597
|
};
|
|
5209
5598
|
var isRecord7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5210
5599
|
var text = (status, body, headers = {}) => new Response(body, {
|
|
@@ -5805,6 +6194,7 @@ var SlackAPI = class {
|
|
|
5805
6194
|
|
|
5806
6195
|
export {
|
|
5807
6196
|
createClock,
|
|
6197
|
+
resolveAdminPrefix,
|
|
5808
6198
|
parseDuration,
|
|
5809
6199
|
document2 as document,
|
|
5810
6200
|
operationIds,
|
|
@@ -5824,4 +6214,4 @@ export {
|
|
|
5824
6214
|
slackCredential,
|
|
5825
6215
|
SlackAPI
|
|
5826
6216
|
};
|
|
5827
|
-
//# sourceMappingURL=chunk-
|
|
6217
|
+
//# sourceMappingURL=chunk-56E5SPJ7.js.map
|