@crvouga/mockingbird-service-formbricks 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 +18 -0
- package/dist/{chunk-JEV2BAL7.js → chunk-2YLOIK2J.js} +622 -235
- package/dist/chunk-2YLOIK2J.js.map +7 -0
- package/dist/{chunk-CEGOD5GL.js → chunk-QQG3NBRB.js} +44 -24
- package/dist/chunk-QQG3NBRB.js.map +7 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.js +2 -2
- package/openapi.yaml +555 -0
- package/package.json +29 -4
- package/dist/chunk-CEGOD5GL.js.map +0 -7
- package/dist/chunk-JEV2BAL7.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
|
};
|
|
@@ -1566,6 +1605,34 @@ dialog:not([open]) {
|
|
|
1566
1605
|
function isRecord2(value) {
|
|
1567
1606
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1568
1607
|
}
|
|
1608
|
+
function highlightJsonText(text) {
|
|
1609
|
+
const escapeToken = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
1610
|
+
switch (char) {
|
|
1611
|
+
case "&":
|
|
1612
|
+
return "&";
|
|
1613
|
+
case "<":
|
|
1614
|
+
return "<";
|
|
1615
|
+
case ">":
|
|
1616
|
+
return ">";
|
|
1617
|
+
case '"':
|
|
1618
|
+
return """;
|
|
1619
|
+
default:
|
|
1620
|
+
return "'";
|
|
1621
|
+
}
|
|
1622
|
+
});
|
|
1623
|
+
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;
|
|
1624
|
+
let cursor = 0;
|
|
1625
|
+
let html = "";
|
|
1626
|
+
for (const match of text.matchAll(token)) {
|
|
1627
|
+
const index = match.index ?? 0;
|
|
1628
|
+
const value = match[0];
|
|
1629
|
+
html += escapeToken(text.slice(cursor, index));
|
|
1630
|
+
const kind = value.startsWith('"') ? value.trimEnd().endsWith(":") ? "key" : "string" : value === "true" || value === "false" ? "boolean" : value === "null" ? "null" : "number";
|
|
1631
|
+
html += `<span class="json-${kind}">${escapeToken(value)}</span>`;
|
|
1632
|
+
cursor = index + value.length;
|
|
1633
|
+
}
|
|
1634
|
+
return html + escapeToken(text.slice(cursor));
|
|
1635
|
+
}
|
|
1569
1636
|
function mountAdminBrand(doc, host, service, catalog) {
|
|
1570
1637
|
const clip = (value, max) => {
|
|
1571
1638
|
if (typeof value !== "string")
|
|
@@ -1927,14 +1994,16 @@ function bootAdmin(config) {
|
|
|
1927
1994
|
const state = {
|
|
1928
1995
|
namespace: "default",
|
|
1929
1996
|
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
1930
|
-
view: "overview",
|
|
1997
|
+
view: ["overview", "state", "clock", "faults", "journal", "routes"].includes(location.hash.slice(1)) ? location.hash.slice(1) : "overview",
|
|
1931
1998
|
collection: "",
|
|
1932
1999
|
shape: null,
|
|
1933
2000
|
route: null,
|
|
2001
|
+
routes: [],
|
|
1934
2002
|
editing: null
|
|
1935
2003
|
};
|
|
1936
2004
|
const standard = new Set(config.standardRoutes);
|
|
1937
2005
|
const banner = el("banner", HTMLElement);
|
|
2006
|
+
const bannerMessage = el("banner-message", HTMLElement);
|
|
1938
2007
|
const keyInput = el("admin-key", HTMLInputElement);
|
|
1939
2008
|
keyInput.value = state.key;
|
|
1940
2009
|
const params = new URLSearchParams(location.search);
|
|
@@ -1945,18 +2014,21 @@ function bootAdmin(config) {
|
|
|
1945
2014
|
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
1946
2015
|
params.delete("key");
|
|
1947
2016
|
const next = params.toString();
|
|
1948
|
-
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
2017
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : "") + location.hash);
|
|
1949
2018
|
}
|
|
1950
2019
|
const queryNamespace = params.get("namespace");
|
|
1951
2020
|
if (queryNamespace)
|
|
1952
2021
|
state.namespace = queryNamespace;
|
|
1953
2022
|
const showError = (error2) => {
|
|
1954
|
-
|
|
2023
|
+
bannerMessage.textContent = error2 instanceof Error ? error2.message : String(error2);
|
|
1955
2024
|
banner.classList.add("show");
|
|
2025
|
+
banner.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
1956
2026
|
};
|
|
1957
2027
|
const clearError = () => {
|
|
1958
2028
|
banner.classList.remove("show");
|
|
2029
|
+
bannerMessage.textContent = "";
|
|
1959
2030
|
};
|
|
2031
|
+
el("banner-dismiss", HTMLButtonElement).addEventListener("click", clearError);
|
|
1960
2032
|
const api = async (method, path, body) => {
|
|
1961
2033
|
const headers = { accept: "application/json" };
|
|
1962
2034
|
if (state.key !== "")
|
|
@@ -2000,16 +2072,21 @@ function bootAdmin(config) {
|
|
|
2000
2072
|
namespace: () => state.namespace
|
|
2001
2073
|
};
|
|
2002
2074
|
const isPanelScript = (value) => typeof value === "function";
|
|
2075
|
+
const themeButton = el("theme", HTMLButtonElement);
|
|
2003
2076
|
const setTheme = (theme) => {
|
|
2004
2077
|
if (theme)
|
|
2005
2078
|
document.documentElement.dataset.theme = theme;
|
|
2006
2079
|
else
|
|
2007
2080
|
delete document.documentElement.dataset.theme;
|
|
2081
|
+
const dark = theme ? theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2082
|
+
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>';
|
|
2083
|
+
const label = dark ? "Use light theme" : "Use dark theme";
|
|
2084
|
+
themeButton.setAttribute("aria-label", label);
|
|
2085
|
+
themeButton.title = label;
|
|
2008
2086
|
};
|
|
2009
2087
|
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
2088
|
+
setTheme(storedTheme === "dark" || storedTheme === "light" ? storedTheme : null);
|
|
2089
|
+
themeButton.addEventListener("click", () => {
|
|
2013
2090
|
const current = document.documentElement.dataset.theme;
|
|
2014
2091
|
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2015
2092
|
const next = dark ? "light" : "dark";
|
|
@@ -2028,6 +2105,7 @@ function bootAdmin(config) {
|
|
|
2028
2105
|
continue;
|
|
2029
2106
|
section.hidden = section.id !== `view-${view}`;
|
|
2030
2107
|
}
|
|
2108
|
+
history.replaceState(null, "", `${location.pathname}${location.search}#${encodeURIComponent(view)}`);
|
|
2031
2109
|
};
|
|
2032
2110
|
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
2033
2111
|
const target = event.target;
|
|
@@ -2042,7 +2120,7 @@ function bootAdmin(config) {
|
|
|
2042
2120
|
show(view);
|
|
2043
2121
|
refresh().catch(showError);
|
|
2044
2122
|
});
|
|
2045
|
-
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>`;
|
|
2123
|
+
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>`;
|
|
2046
2124
|
const table = (columns, rows) => {
|
|
2047
2125
|
if (rows.length === 0)
|
|
2048
2126
|
return '<p class="empty">Nothing here yet.</p>';
|
|
@@ -2052,16 +2130,22 @@ function bootAdmin(config) {
|
|
|
2052
2130
|
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
2053
2131
|
return `<tr${attrs}>${cells}</tr>`;
|
|
2054
2132
|
}).join("");
|
|
2055
|
-
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
2133
|
+
return `<div class="table-shell"><table><thead>${head}</thead><tbody>${body}</tbody></table></div>`;
|
|
2056
2134
|
};
|
|
2057
2135
|
const preview = (value) => {
|
|
2058
2136
|
const text = JSON.stringify(value) ?? "null";
|
|
2059
|
-
|
|
2137
|
+
const clipped = text.length > 180 ? `${text.slice(0, 180)}\u2026` : text;
|
|
2138
|
+
return `<code class="json-inline">${highlightJsonText(clipped)}</code>`;
|
|
2060
2139
|
};
|
|
2061
2140
|
const when = (epoch) => {
|
|
2062
2141
|
const date = new Date(epoch);
|
|
2063
2142
|
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
2064
2143
|
};
|
|
2144
|
+
const journalWhen = (value) => {
|
|
2145
|
+
const numeric = value.trim() === "" ? Number.NaN : Number(value);
|
|
2146
|
+
const epoch = Number.isFinite(numeric) ? numeric : Date.parse(value);
|
|
2147
|
+
return Number.isFinite(epoch) ? when(epoch) : value;
|
|
2148
|
+
};
|
|
2065
2149
|
const clockCard = (epoch) => {
|
|
2066
2150
|
const date = new Date(epoch);
|
|
2067
2151
|
if (Number.isNaN(date.getTime()))
|
|
@@ -2181,7 +2265,7 @@ function bootAdmin(config) {
|
|
|
2181
2265
|
};
|
|
2182
2266
|
const renderOverview = async () => {
|
|
2183
2267
|
const [health, shape, clock, journal] = await Promise.all([
|
|
2184
|
-
originGet(
|
|
2268
|
+
originGet(`${config.adminPrefix}/health`),
|
|
2185
2269
|
api("GET", "/state"),
|
|
2186
2270
|
api("GET", "/clock"),
|
|
2187
2271
|
api("GET", "/requests?limit=8")
|
|
@@ -2198,7 +2282,7 @@ function bootAdmin(config) {
|
|
|
2198
2282
|
},
|
|
2199
2283
|
{
|
|
2200
2284
|
k: "Status",
|
|
2201
|
-
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
2285
|
+
v: `<span class="badge ok">${esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")}</span>`
|
|
2202
2286
|
},
|
|
2203
2287
|
{ k: "Namespace", v: esc(state.namespace) },
|
|
2204
2288
|
{ k: "Collections", v: String(decoded.collections.length) },
|
|
@@ -2285,26 +2369,46 @@ function bootAdmin(config) {
|
|
|
2285
2369
|
const renderJournal = async () => {
|
|
2286
2370
|
const journal = await api("GET", "/requests?limit=100");
|
|
2287
2371
|
el("journal", HTMLElement).innerHTML = table([
|
|
2288
|
-
{ label: "When", cell: (row) => esc(row.at) },
|
|
2289
|
-
{
|
|
2372
|
+
{ label: "When", cell: (row) => esc(journalWhen(row.at)) },
|
|
2373
|
+
{
|
|
2374
|
+
label: "Method",
|
|
2375
|
+
cell: (row) => `<span class="method">${esc(row.method)}</span>`
|
|
2376
|
+
},
|
|
2290
2377
|
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2291
|
-
{
|
|
2378
|
+
{
|
|
2379
|
+
label: "Status",
|
|
2380
|
+
cell: (row) => `<span class="badge ${row.status.startsWith("2") ? "ok" : row.status.startsWith("4") || row.status.startsWith("5") ? "warn" : ""}">${esc(row.status)}</span>`
|
|
2381
|
+
},
|
|
2292
2382
|
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
2293
2383
|
], decodeJournal(journal));
|
|
2294
2384
|
};
|
|
2295
|
-
const
|
|
2296
|
-
const
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2385
|
+
const paintRoutes = () => {
|
|
2386
|
+
const query = el("route-search", HTMLInputElement).value.trim().toLocaleLowerCase();
|
|
2387
|
+
const routes = state.routes.filter((route) => route.toLocaleLowerCase().includes(query));
|
|
2388
|
+
el("route-count", HTMLElement).textContent = `${routes.length} of ${state.routes.length}`;
|
|
2389
|
+
if (state.routes.length === 0) {
|
|
2390
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No admin routes are available.</p>';
|
|
2391
|
+
return;
|
|
2392
|
+
}
|
|
2393
|
+
if (routes.length === 0) {
|
|
2394
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No routes match this filter.</p>';
|
|
2395
|
+
return;
|
|
2396
|
+
}
|
|
2300
2397
|
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2301
2398
|
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2302
2399
|
const parts = route.split(" ");
|
|
2303
2400
|
const method = parts[0] ?? "";
|
|
2304
2401
|
const path = parts.slice(1).join(" ");
|
|
2305
|
-
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>`;
|
|
2402
|
+
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>`;
|
|
2306
2403
|
}).join("");
|
|
2307
2404
|
};
|
|
2405
|
+
const renderRoutes = async () => {
|
|
2406
|
+
const data = await api("GET", "/");
|
|
2407
|
+
if (!isRecord2(data))
|
|
2408
|
+
throw new Error("Route list was not an object");
|
|
2409
|
+
state.routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
2410
|
+
paintRoutes();
|
|
2411
|
+
};
|
|
2308
2412
|
const renderers = {
|
|
2309
2413
|
overview: renderOverview,
|
|
2310
2414
|
state: renderState,
|
|
@@ -2313,19 +2417,50 @@ function bootAdmin(config) {
|
|
|
2313
2417
|
journal: renderJournal,
|
|
2314
2418
|
routes: renderRoutes
|
|
2315
2419
|
};
|
|
2420
|
+
let refreshToken = 0;
|
|
2316
2421
|
async function refresh() {
|
|
2422
|
+
const token = ++refreshToken;
|
|
2317
2423
|
clearError();
|
|
2424
|
+
const main = el("views", HTMLElement);
|
|
2425
|
+
const sync = el("sync-status", HTMLElement);
|
|
2426
|
+
main.setAttribute("aria-busy", "true");
|
|
2427
|
+
sync.classList.add("busy");
|
|
2428
|
+
sync.textContent = "Updating";
|
|
2318
2429
|
const render = renderers[state.view];
|
|
2319
|
-
|
|
2320
|
-
|
|
2430
|
+
try {
|
|
2431
|
+
if (render)
|
|
2432
|
+
await render();
|
|
2433
|
+
if (token === refreshToken)
|
|
2434
|
+
sync.textContent = "Up to date";
|
|
2435
|
+
} finally {
|
|
2436
|
+
if (token === refreshToken) {
|
|
2437
|
+
main.removeAttribute("aria-busy");
|
|
2438
|
+
sync.classList.remove("busy");
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2321
2441
|
}
|
|
2322
2442
|
const delay = (ms) => new Promise((resolve) => {
|
|
2323
2443
|
window.setTimeout(resolve, ms);
|
|
2324
2444
|
});
|
|
2325
|
-
const confirmAction = async (button, pending, done, work) => {
|
|
2445
|
+
const confirmAction = async (button, pending, done, work, confirmLabel) => {
|
|
2326
2446
|
if (button.dataset.pending === "1")
|
|
2327
2447
|
return;
|
|
2328
2448
|
const label = button.textContent ?? "";
|
|
2449
|
+
if (confirmLabel !== void 0 && button.dataset.confirming !== "1") {
|
|
2450
|
+
button.dataset.confirming = "1";
|
|
2451
|
+
button.classList.add("confirming");
|
|
2452
|
+
button.textContent = confirmLabel;
|
|
2453
|
+
window.setTimeout(() => {
|
|
2454
|
+
if (button.dataset.confirming !== "1")
|
|
2455
|
+
return;
|
|
2456
|
+
button.textContent = label;
|
|
2457
|
+
button.classList.remove("confirming");
|
|
2458
|
+
delete button.dataset.confirming;
|
|
2459
|
+
}, 3e3);
|
|
2460
|
+
return;
|
|
2461
|
+
}
|
|
2462
|
+
button.classList.remove("confirming");
|
|
2463
|
+
delete button.dataset.confirming;
|
|
2329
2464
|
const width = button.offsetWidth;
|
|
2330
2465
|
button.dataset.pending = "1";
|
|
2331
2466
|
button.classList.add("pressed");
|
|
@@ -2346,6 +2481,65 @@ function bootAdmin(config) {
|
|
|
2346
2481
|
delete button.dataset.pending;
|
|
2347
2482
|
}
|
|
2348
2483
|
};
|
|
2484
|
+
const wireJsonEditor = (textareaId, highlightId, statusId, shellId, allowEmpty) => {
|
|
2485
|
+
const textarea = el(textareaId, HTMLTextAreaElement);
|
|
2486
|
+
const highlight = el(highlightId, HTMLElement);
|
|
2487
|
+
const status = el(statusId, HTMLElement);
|
|
2488
|
+
const shell = el(shellId, HTMLElement);
|
|
2489
|
+
const sync = () => {
|
|
2490
|
+
const text = textarea.value;
|
|
2491
|
+
highlight.innerHTML = `${highlightJsonText(text)}
|
|
2492
|
+
`;
|
|
2493
|
+
let valid = true;
|
|
2494
|
+
let message = "Valid JSON";
|
|
2495
|
+
if (allowEmpty && text.trim() === "")
|
|
2496
|
+
message = "No body";
|
|
2497
|
+
else {
|
|
2498
|
+
try {
|
|
2499
|
+
readJson2(text);
|
|
2500
|
+
} catch {
|
|
2501
|
+
valid = false;
|
|
2502
|
+
message = "Invalid JSON";
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
shell.classList.toggle("invalid", !valid);
|
|
2506
|
+
status.classList.toggle("invalid", !valid);
|
|
2507
|
+
status.textContent = message;
|
|
2508
|
+
textarea.setAttribute("aria-invalid", valid ? "false" : "true");
|
|
2509
|
+
};
|
|
2510
|
+
const syncScroll = () => {
|
|
2511
|
+
const preview2 = highlight.parentElement;
|
|
2512
|
+
if (!(preview2 instanceof HTMLElement))
|
|
2513
|
+
return;
|
|
2514
|
+
preview2.scrollTop = textarea.scrollTop;
|
|
2515
|
+
preview2.scrollLeft = textarea.scrollLeft;
|
|
2516
|
+
};
|
|
2517
|
+
textarea.addEventListener("input", sync);
|
|
2518
|
+
textarea.addEventListener("scroll", syncScroll);
|
|
2519
|
+
sync();
|
|
2520
|
+
return sync;
|
|
2521
|
+
};
|
|
2522
|
+
const syncRouteEditor = wireJsonEditor("route-body", "route-highlight", "route-json-status", "route-editor", true);
|
|
2523
|
+
const syncRecordEditor = wireJsonEditor("editor-value", "editor-highlight", "editor-json-status", "record-editor", false);
|
|
2524
|
+
for (const button of document.querySelectorAll("[data-format]")) {
|
|
2525
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2526
|
+
continue;
|
|
2527
|
+
button.addEventListener("click", () => {
|
|
2528
|
+
const target = button.dataset.format;
|
|
2529
|
+
if (target === void 0)
|
|
2530
|
+
return;
|
|
2531
|
+
const textarea = document.getElementById(target);
|
|
2532
|
+
if (!(textarea instanceof HTMLTextAreaElement) || textarea.value.trim() === "")
|
|
2533
|
+
return;
|
|
2534
|
+
try {
|
|
2535
|
+
textarea.value = JSON.stringify(readJson2(textarea.value), null, 2) ?? "";
|
|
2536
|
+
textarea.dispatchEvent(new Event("input"));
|
|
2537
|
+
textarea.focus();
|
|
2538
|
+
} catch {
|
|
2539
|
+
showError(new Error("Fix the JSON before formatting it"));
|
|
2540
|
+
}
|
|
2541
|
+
});
|
|
2542
|
+
}
|
|
2349
2543
|
document.addEventListener("click", (event) => {
|
|
2350
2544
|
const target = event.target;
|
|
2351
2545
|
if (!(target instanceof Element))
|
|
@@ -2418,6 +2612,7 @@ function bootAdmin(config) {
|
|
|
2418
2612
|
} else {
|
|
2419
2613
|
editorValue.value = "{\n \n}";
|
|
2420
2614
|
}
|
|
2615
|
+
syncRecordEditor();
|
|
2421
2616
|
editor.showModal();
|
|
2422
2617
|
}
|
|
2423
2618
|
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -2447,10 +2642,10 @@ function bootAdmin(config) {
|
|
|
2447
2642
|
const id = state.editing;
|
|
2448
2643
|
if (!id)
|
|
2449
2644
|
return;
|
|
2450
|
-
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2645
|
+
confirmAction(editorDelete, "Deleting...", "Deleted", () => api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2451
2646
|
editor.close();
|
|
2452
2647
|
return renderState();
|
|
2453
|
-
}).catch(showError);
|
|
2648
|
+
}), "Confirm delete").catch(showError);
|
|
2454
2649
|
});
|
|
2455
2650
|
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2456
2651
|
if (!(button instanceof HTMLButtonElement))
|
|
@@ -2496,10 +2691,10 @@ function bootAdmin(config) {
|
|
|
2496
2691
|
}).then(() => renderFaults()).catch(showError);
|
|
2497
2692
|
});
|
|
2498
2693
|
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2499
|
-
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
|
|
2694
|
+
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults()), "Confirm clear").catch(showError);
|
|
2500
2695
|
});
|
|
2501
2696
|
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2502
|
-
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
|
|
2697
|
+
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal()), "Confirm clear").catch(showError);
|
|
2503
2698
|
});
|
|
2504
2699
|
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2505
2700
|
const target = event.target;
|
|
@@ -2513,9 +2708,15 @@ function bootAdmin(config) {
|
|
|
2513
2708
|
return;
|
|
2514
2709
|
state.route = route;
|
|
2515
2710
|
el("route-title", HTMLElement).textContent = route;
|
|
2711
|
+
el("route-send", HTMLButtonElement).disabled = false;
|
|
2516
2712
|
const method = route.split(" ")[0] ?? "";
|
|
2517
|
-
el("route-body", HTMLTextAreaElement)
|
|
2713
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
2714
|
+
routeBody.disabled = method === "GET" || method === "DELETE";
|
|
2715
|
+
el("route-editor", HTMLElement).hidden = routeBody.disabled;
|
|
2716
|
+
el("route-json-status", HTMLElement).parentElement?.toggleAttribute("hidden", routeBody.disabled);
|
|
2717
|
+
syncRouteEditor();
|
|
2518
2718
|
});
|
|
2719
|
+
el("route-search", HTMLInputElement).addEventListener("input", paintRoutes);
|
|
2519
2720
|
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2520
2721
|
event.preventDefault();
|
|
2521
2722
|
const send = async () => {
|
|
@@ -2537,7 +2738,8 @@ function bootAdmin(config) {
|
|
|
2537
2738
|
}
|
|
2538
2739
|
}
|
|
2539
2740
|
const data = await api(method, concrete, body);
|
|
2540
|
-
|
|
2741
|
+
const text = JSON.stringify(data, null, 2) ?? "null";
|
|
2742
|
+
el("route-result", HTMLElement).innerHTML = `<code>${highlightJsonText(text)}</code>`;
|
|
2541
2743
|
};
|
|
2542
2744
|
send().catch(showError);
|
|
2543
2745
|
});
|
|
@@ -2655,11 +2857,13 @@ function bootAdmin(config) {
|
|
|
2655
2857
|
views.append(section);
|
|
2656
2858
|
mountSqlExplorer(body, panelApi);
|
|
2657
2859
|
}
|
|
2860
|
+
show(document.getElementById(`view-${state.view}`) === null ? "overview" : state.view);
|
|
2658
2861
|
return refresh();
|
|
2659
2862
|
}).catch(showError);
|
|
2660
2863
|
}
|
|
2661
2864
|
var clientPrelude = [
|
|
2662
2865
|
isRecord2,
|
|
2866
|
+
highlightJsonText,
|
|
2663
2867
|
mountAdminBrand,
|
|
2664
2868
|
startAdminBrand,
|
|
2665
2869
|
faultPresetList,
|
|
@@ -2682,6 +2886,7 @@ ${entry}(${payload});
|
|
|
2682
2886
|
var defineMock = (runtime) => runtime;
|
|
2683
2887
|
var STANDARD_ADMIN_ROUTES = [
|
|
2684
2888
|
"GET /",
|
|
2889
|
+
"GET /health",
|
|
2685
2890
|
"POST /reset",
|
|
2686
2891
|
"GET /namespaces",
|
|
2687
2892
|
"GET /clock",
|
|
@@ -2761,11 +2966,11 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
2761
2966
|
}
|
|
2762
2967
|
});
|
|
2763
2968
|
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
2764
|
-
var renderAdminDocument = (service) => {
|
|
2969
|
+
var renderAdminDocument = (service, adminPrefix = ADMIN_PREFIX) => {
|
|
2765
2970
|
const name = escapeHtml(service);
|
|
2766
2971
|
const script = adminClientSource({
|
|
2767
2972
|
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
2768
|
-
adminPrefix
|
|
2973
|
+
adminPrefix,
|
|
2769
2974
|
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
2770
2975
|
brandsUrl: ADMIN_BRANDS_URL,
|
|
2771
2976
|
service
|
|
@@ -2782,75 +2987,99 @@ var renderAdminDocument = (service) => {
|
|
|
2782
2987
|
${CSS_RESET}
|
|
2783
2988
|
:root {
|
|
2784
2989
|
color-scheme: light;
|
|
2785
|
-
--bg: #
|
|
2990
|
+
--bg: #f7f8fb;
|
|
2786
2991
|
--bg-elev: #ffffff;
|
|
2787
|
-
--bg-muted: #
|
|
2788
|
-
--bg-inset: #
|
|
2789
|
-
--line: #
|
|
2790
|
-
--line-strong: #
|
|
2791
|
-
--ink: #
|
|
2792
|
-
--muted: #
|
|
2793
|
-
--faint: #
|
|
2794
|
-
--accent: #
|
|
2992
|
+
--bg-muted: #f1f3f7;
|
|
2993
|
+
--bg-inset: #f8f9fc;
|
|
2994
|
+
--line: #e4e7ec;
|
|
2995
|
+
--line-strong: #cfd4dc;
|
|
2996
|
+
--ink: #101828;
|
|
2997
|
+
--muted: #475467;
|
|
2998
|
+
--faint: #667085;
|
|
2999
|
+
--accent: #6941c6;
|
|
3000
|
+
--accent-hover: #5933b4;
|
|
2795
3001
|
--accent-ink: #ffffff;
|
|
2796
|
-
--accent-soft: #
|
|
2797
|
-
--ok: #
|
|
2798
|
-
--ok-soft: #
|
|
2799
|
-
--warn: #
|
|
2800
|
-
--warn-soft: #
|
|
2801
|
-
--err: #
|
|
2802
|
-
--err-soft: #
|
|
2803
|
-
--
|
|
3002
|
+
--accent-soft: #f4f0ff;
|
|
3003
|
+
--ok: #067647;
|
|
3004
|
+
--ok-soft: #ecfdf3;
|
|
3005
|
+
--warn: #b54708;
|
|
3006
|
+
--warn-soft: #fffaeb;
|
|
3007
|
+
--err: #b42318;
|
|
3008
|
+
--err-soft: #fef3f2;
|
|
3009
|
+
--code-key: #6941c6;
|
|
3010
|
+
--code-string: #067647;
|
|
3011
|
+
--code-number: #175cd3;
|
|
3012
|
+
--code-boolean: #c11574;
|
|
3013
|
+
--code-null: #667085;
|
|
3014
|
+
--shadow-xs: 0 1px 2px rgb(16 24 40 / 0.05);
|
|
3015
|
+
--shadow-sm: 0 1px 3px rgb(16 24 40 / 0.08), 0 1px 2px rgb(16 24 40 / 0.04);
|
|
3016
|
+
--shadow-lg: 0 18px 48px rgb(16 24 40 / 0.16), 0 4px 12px rgb(16 24 40 / 0.08);
|
|
2804
3017
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2805
3018
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
2806
|
-
--radius:
|
|
2807
|
-
--header:
|
|
2808
|
-
--nav:
|
|
3019
|
+
--radius: 12px;
|
|
3020
|
+
--header: 72px;
|
|
3021
|
+
--nav: 224px;
|
|
2809
3022
|
}
|
|
2810
3023
|
:root[data-theme="dark"] {
|
|
2811
3024
|
color-scheme: dark;
|
|
2812
|
-
--bg: #
|
|
2813
|
-
--bg-elev: #
|
|
2814
|
-
--bg-muted: #
|
|
2815
|
-
--bg-inset: #
|
|
2816
|
-
--line: #
|
|
2817
|
-
--line-strong: #
|
|
2818
|
-
--ink: #
|
|
2819
|
-
--muted: #
|
|
2820
|
-
--faint: #
|
|
2821
|
-
--accent: #
|
|
2822
|
-
--accent-
|
|
2823
|
-
--accent-
|
|
2824
|
-
--
|
|
2825
|
-
--ok
|
|
2826
|
-
--
|
|
2827
|
-
--warn
|
|
2828
|
-
--
|
|
2829
|
-
--err
|
|
2830
|
-
--
|
|
3025
|
+
--bg: #0c0e14;
|
|
3026
|
+
--bg-elev: #151821;
|
|
3027
|
+
--bg-muted: #20232e;
|
|
3028
|
+
--bg-inset: #11141c;
|
|
3029
|
+
--line: #2b303d;
|
|
3030
|
+
--line-strong: #414858;
|
|
3031
|
+
--ink: #f5f7fa;
|
|
3032
|
+
--muted: #b6bdc9;
|
|
3033
|
+
--faint: #8d96a7;
|
|
3034
|
+
--accent: #9e77ed;
|
|
3035
|
+
--accent-hover: #b692f6;
|
|
3036
|
+
--accent-ink: #ffffff;
|
|
3037
|
+
--accent-soft: #2b2142;
|
|
3038
|
+
--ok: #75e0a7;
|
|
3039
|
+
--ok-soft: #102a20;
|
|
3040
|
+
--warn: #fec84b;
|
|
3041
|
+
--warn-soft: #302611;
|
|
3042
|
+
--err: #fda29b;
|
|
3043
|
+
--err-soft: #321817;
|
|
3044
|
+
--code-key: #c3a6ff;
|
|
3045
|
+
--code-string: #75e0a7;
|
|
3046
|
+
--code-number: #84adff;
|
|
3047
|
+
--code-boolean: #f9a8d4;
|
|
3048
|
+
--code-null: #98a2b3;
|
|
3049
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3050
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3051
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2831
3052
|
}
|
|
2832
3053
|
@media (prefers-color-scheme: dark) {
|
|
2833
3054
|
:root:not([data-theme="light"]) {
|
|
2834
3055
|
color-scheme: dark;
|
|
2835
|
-
--bg: #
|
|
2836
|
-
--bg-elev: #
|
|
2837
|
-
--bg-muted: #
|
|
2838
|
-
--bg-inset: #
|
|
2839
|
-
--line: #
|
|
2840
|
-
--line-strong: #
|
|
2841
|
-
--ink: #
|
|
2842
|
-
--muted: #
|
|
2843
|
-
--faint: #
|
|
2844
|
-
--accent: #
|
|
2845
|
-
--accent-
|
|
2846
|
-
--accent-
|
|
2847
|
-
--
|
|
2848
|
-
--ok
|
|
2849
|
-
--
|
|
2850
|
-
--warn
|
|
2851
|
-
--
|
|
2852
|
-
--err
|
|
2853
|
-
--
|
|
3056
|
+
--bg: #0c0e14;
|
|
3057
|
+
--bg-elev: #151821;
|
|
3058
|
+
--bg-muted: #20232e;
|
|
3059
|
+
--bg-inset: #11141c;
|
|
3060
|
+
--line: #2b303d;
|
|
3061
|
+
--line-strong: #414858;
|
|
3062
|
+
--ink: #f5f7fa;
|
|
3063
|
+
--muted: #b6bdc9;
|
|
3064
|
+
--faint: #8d96a7;
|
|
3065
|
+
--accent: #9e77ed;
|
|
3066
|
+
--accent-hover: #b692f6;
|
|
3067
|
+
--accent-ink: #ffffff;
|
|
3068
|
+
--accent-soft: #2b2142;
|
|
3069
|
+
--ok: #75e0a7;
|
|
3070
|
+
--ok-soft: #102a20;
|
|
3071
|
+
--warn: #fec84b;
|
|
3072
|
+
--warn-soft: #302611;
|
|
3073
|
+
--err: #fda29b;
|
|
3074
|
+
--err-soft: #321817;
|
|
3075
|
+
--code-key: #c3a6ff;
|
|
3076
|
+
--code-string: #75e0a7;
|
|
3077
|
+
--code-number: #84adff;
|
|
3078
|
+
--code-boolean: #f9a8d4;
|
|
3079
|
+
--code-null: #98a2b3;
|
|
3080
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3081
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3082
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2854
3083
|
}
|
|
2855
3084
|
}
|
|
2856
3085
|
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
@@ -2868,13 +3097,17 @@ var renderAdminDocument = (service) => {
|
|
|
2868
3097
|
padding-right: env(safe-area-inset-right);
|
|
2869
3098
|
-webkit-text-size-adjust: 100%;
|
|
2870
3099
|
}
|
|
2871
|
-
|
|
3100
|
+
::selection { background: color-mix(in srgb, var(--accent) 28%, transparent); }
|
|
3101
|
+
:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px; }
|
|
3102
|
+
button, input, select, textarea { transition: border-color 140ms ease, box-shadow 140ms ease, transform 100ms ease; }
|
|
3103
|
+
button:disabled, input:disabled, select:disabled, textarea:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
2872
3104
|
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
2873
3105
|
header.top {
|
|
2874
|
-
position: sticky; top: 0; z-index:
|
|
3106
|
+
position: sticky; top: 0; z-index: 20;
|
|
2875
3107
|
display: flex; align-items: center; gap: 12px;
|
|
2876
|
-
min-height: var(--header); padding:
|
|
2877
|
-
background: var(--bg-elev);
|
|
3108
|
+
min-height: var(--header); padding: 12px 20px;
|
|
3109
|
+
background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
|
|
3110
|
+
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
2878
3111
|
border-bottom: 1px solid var(--line);
|
|
2879
3112
|
min-width: 0; max-width: 100%;
|
|
2880
3113
|
}
|
|
@@ -2882,12 +3115,21 @@ var renderAdminDocument = (service) => {
|
|
|
2882
3115
|
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
2883
3116
|
min-width: 0; max-width: 100%; margin-right: auto;
|
|
2884
3117
|
}
|
|
2885
|
-
.brand-
|
|
2886
|
-
.brand-
|
|
3118
|
+
.brand-lockup { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
3119
|
+
.brand-mark {
|
|
3120
|
+
width: 38px; height: 38px; display: grid; place-items: center; flex: none;
|
|
3121
|
+
border-radius: 11px; color: #fff;
|
|
3122
|
+
background: linear-gradient(145deg, #7f56d9 0%, #53389e 100%);
|
|
3123
|
+
box-shadow: 0 6px 16px rgb(105 65 198 / 0.24), inset 0 1px 0 rgb(255 255 255 / 0.25);
|
|
3124
|
+
}
|
|
3125
|
+
.brand-mark svg { width: 22px; height: 22px; }
|
|
3126
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; line-height: 1.25; }
|
|
3127
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
2887
3128
|
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
2888
3129
|
.vendor {
|
|
2889
3130
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
2890
3131
|
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
3132
|
+
padding: 5px 8px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-inset);
|
|
2891
3133
|
}
|
|
2892
3134
|
.vendor[hidden] { display: none; }
|
|
2893
3135
|
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
@@ -2899,12 +3141,17 @@ var renderAdminDocument = (service) => {
|
|
|
2899
3141
|
.vendor-sep { color: var(--faint); }
|
|
2900
3142
|
.vendor-link { color: inherit; text-decoration: none; }
|
|
2901
3143
|
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
2902
|
-
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items:
|
|
2903
|
-
|
|
3144
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
|
|
3145
|
+
.sync-status { align-self: center; display: flex; align-items: center; gap: 6px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3146
|
+
.sync-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
|
|
3147
|
+
.sync-status.busy::before { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); animation: pulse 1.2s ease-in-out infinite; }
|
|
3148
|
+
label.field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; font-weight: 600; color: var(--faint); }
|
|
2904
3149
|
label.field input, label.field select, .controls input[type="password"] {
|
|
2905
|
-
background-color: var(--bg-
|
|
2906
|
-
padding:
|
|
3150
|
+
background-color: var(--bg-elev); border: 1px solid var(--line-strong); border-radius: 9px;
|
|
3151
|
+
padding: 8px 10px; min-height: 38px; line-height: 1.2; box-shadow: var(--shadow-xs);
|
|
2907
3152
|
}
|
|
3153
|
+
label.field input:hover, label.field select:hover, textarea:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--line-strong)); }
|
|
3154
|
+
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; }
|
|
2908
3155
|
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
2909
3156
|
the text fields in the same form. Paint it like those fields. */
|
|
2910
3157
|
label.field select {
|
|
@@ -2921,12 +3168,20 @@ var renderAdminDocument = (service) => {
|
|
|
2921
3168
|
dialog .field > textarea { width: 100%; }
|
|
2922
3169
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
2923
3170
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
.btn
|
|
3171
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
|
3172
|
+
border-radius: 9px; min-height: 38px; padding: 0 13px; border: 1px solid var(--line-strong);
|
|
3173
|
+
background: var(--bg-elev); color: var(--ink); font-size: 13px; font-weight: 600; line-height: 1;
|
|
3174
|
+
box-shadow: var(--shadow-xs);
|
|
3175
|
+
}
|
|
3176
|
+
.icon-btn { width: 38px; padding: 0; }
|
|
3177
|
+
.icon-btn svg, .btn svg, .btn-primary svg { width: 16px; height: 16px; }
|
|
3178
|
+
.icon-btn:hover, .btn:hover { background: var(--bg-muted); border-color: var(--line-strong); }
|
|
3179
|
+
.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; }
|
|
3180
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
3181
|
+
.btn-quiet { background: transparent; border-color: transparent; box-shadow: none; }
|
|
3182
|
+
.btn-quiet:hover { background: var(--bg-muted); }
|
|
3183
|
+
.btn-danger { color: var(--err); border-color: color-mix(in srgb, var(--err) 25%, var(--line)); }
|
|
3184
|
+
.btn-danger:hover, .btn-danger.confirming { background: var(--err-soft); border-color: color-mix(in srgb, var(--err) 45%, var(--line)); }
|
|
2930
3185
|
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
2931
3186
|
so the press itself has to stay visible. */
|
|
2932
3187
|
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
@@ -2942,93 +3197,159 @@ var renderAdminDocument = (service) => {
|
|
|
2942
3197
|
flex: 1 0 auto; min-width: 0;
|
|
2943
3198
|
}
|
|
2944
3199
|
nav.side {
|
|
2945
|
-
|
|
3200
|
+
position: sticky; top: var(--header); align-self: start; height: calc(100vh - var(--header));
|
|
3201
|
+
border-right: 1px solid var(--line); padding: 18px 12px;
|
|
2946
3202
|
display: flex; flex-direction: column; gap: 4px;
|
|
2947
|
-
background: var(--bg-elev);
|
|
3203
|
+
background: var(--bg-elev); overflow-y: auto;
|
|
2948
3204
|
}
|
|
3205
|
+
.nav-label { padding: 0 10px 8px; color: var(--faint); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
|
|
2949
3206
|
nav.side button {
|
|
3207
|
+
position: relative; display: flex; align-items: center; gap: 10px; width: 100%;
|
|
2950
3208
|
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
2951
|
-
padding: 9px 10px; min-height:
|
|
2952
|
-
}
|
|
2953
|
-
nav.side button
|
|
2954
|
-
|
|
2955
|
-
.
|
|
2956
|
-
.
|
|
2957
|
-
.
|
|
3209
|
+
padding: 9px 10px; min-height: 42px; color: var(--muted); font-size: 13px; font-weight: 550;
|
|
3210
|
+
}
|
|
3211
|
+
nav.side button:hover { background: var(--bg-muted); color: var(--ink); }
|
|
3212
|
+
nav.side button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 650; }
|
|
3213
|
+
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); }
|
|
3214
|
+
.nav-icon { width: 18px; height: 18px; display: grid; place-items: center; flex: none; }
|
|
3215
|
+
.nav-icon svg { width: 18px; height: 18px; }
|
|
3216
|
+
main { padding: 32px clamp(20px, 4vw, 48px) 64px; max-width: 1320px; width: 100%; min-width: 0; }
|
|
3217
|
+
main[aria-busy="true"] .view { opacity: 0.62; }
|
|
3218
|
+
.view { animation: view-in 180ms ease-out; }
|
|
3219
|
+
.view[hidden] { display: none; }
|
|
3220
|
+
.view-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
|
|
3221
|
+
.view h2 { margin: 0 0 5px; font-size: clamp(24px, 3vw, 30px); line-height: 1.2; letter-spacing: -0.035em; }
|
|
3222
|
+
.view h3 { margin: 24px 0 10px; font-size: 15px; letter-spacing: -0.01em; }
|
|
3223
|
+
.lede { margin: 0; color: var(--muted); max-width: 72ch; font-size: 14px; }
|
|
3224
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 175px), 1fr)); gap: 14px; }
|
|
2958
3225
|
.card {
|
|
2959
3226
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
2960
|
-
padding:
|
|
3227
|
+
padding: 18px; box-shadow: var(--shadow-sm);
|
|
2961
3228
|
}
|
|
2962
|
-
.card
|
|
2963
|
-
.card
|
|
3229
|
+
.stat-card { position: relative; overflow: hidden; }
|
|
3230
|
+
.stat-card::after { content: ""; position: absolute; inset: 0 0 auto; height: 2px; background: linear-gradient(90deg, var(--accent), transparent 78%); opacity: 0.7; }
|
|
3231
|
+
.card .k { color: var(--faint); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
3232
|
+
.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; }
|
|
2964
3233
|
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
2965
|
-
.
|
|
3234
|
+
.section-card { background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-xs); overflow: hidden; }
|
|
3235
|
+
.section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
|
|
3236
|
+
.section-title h3 { margin: 0; }
|
|
3237
|
+
.split { display: grid; grid-template-columns: minmax(0, 250px) minmax(0, 1fr); gap: 18px; align-items: start; }
|
|
3238
|
+
#collections { padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); box-shadow: var(--shadow-xs); }
|
|
2966
3239
|
.collection {
|
|
2967
3240
|
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
2968
|
-
border-radius:
|
|
3241
|
+
border-radius: 9px; padding: 10px 11px; min-height: 48px; color: var(--ink); cursor: pointer;
|
|
2969
3242
|
}
|
|
2970
|
-
.collection
|
|
3243
|
+
.collection:hover { background: var(--bg-muted); }
|
|
3244
|
+
.collection[aria-current="true"] { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 18%, var(--line)); color: var(--accent); }
|
|
2971
3245
|
.collection small { display: block; color: var(--faint); }
|
|
2972
3246
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
2973
|
-
.chip {
|
|
2974
|
-
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
2975
|
-
border-radius: 999px; padding: 4px 8px;
|
|
2976
|
-
}
|
|
3247
|
+
.chip { font-family: var(--mono); font-size: 11px; background: var(--bg-muted); border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; }
|
|
2977
3248
|
.chip i { color: var(--faint); font-style: normal; }
|
|
2978
3249
|
.sql-layout {
|
|
2979
3250
|
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
2980
3251
|
}
|
|
2981
|
-
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
2982
|
-
.sql-tables button { text-align: left; }
|
|
3252
|
+
.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); }
|
|
3253
|
+
.sql-tables button { text-align: left; border: 0; border-radius: 8px; padding: 9px 10px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
3254
|
+
.sql-tables button:hover, .sql-tables button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); }
|
|
2983
3255
|
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
2984
|
-
table { width: 100%; border-
|
|
2985
|
-
|
|
2986
|
-
th {
|
|
3256
|
+
.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); }
|
|
3257
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
3258
|
+
th, td { text-align: left; padding: 11px 13px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
3259
|
+
th { color: var(--faint); background: var(--bg-inset); font-weight: 650; font-size: 11px; text-transform: uppercase; letter-spacing: 0.045em; }
|
|
3260
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
2987
3261
|
tr.clickable { cursor: pointer; }
|
|
2988
|
-
tr.clickable:hover { background: var(--
|
|
3262
|
+
tr.clickable:hover { background: var(--accent-soft); }
|
|
2989
3263
|
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
2990
3264
|
pre {
|
|
2991
3265
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
2992
|
-
background: var(--bg-inset); border-radius: 10px; padding:
|
|
2993
|
-
}
|
|
2994
|
-
.
|
|
3266
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 10px; padding: 14px; font-size: 12px; line-height: 1.65;
|
|
3267
|
+
}
|
|
3268
|
+
.json-key { color: var(--code-key); }
|
|
3269
|
+
.json-string { color: var(--code-string); }
|
|
3270
|
+
.json-number { color: var(--code-number); }
|
|
3271
|
+
.json-boolean { color: var(--code-boolean); }
|
|
3272
|
+
.json-null { color: var(--code-null); font-style: italic; }
|
|
3273
|
+
.json-inline { display: block; max-width: 44rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
3274
|
+
.json-view { position: relative; max-height: 26rem; overflow: auto; }
|
|
3275
|
+
.json-editor { position: relative; min-height: 190px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--bg-inset); overflow: hidden; }
|
|
3276
|
+
.json-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); }
|
|
3277
|
+
.json-editor.invalid { border-color: var(--err); }
|
|
3278
|
+
.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; }
|
|
3279
|
+
.json-editor pre { position: absolute; inset: 0; pointer-events: none; background: transparent; }
|
|
3280
|
+
.json-editor textarea { position: relative; z-index: 1; resize: vertical; background: transparent; color: transparent; caret-color: var(--ink); -webkit-text-fill-color: transparent; }
|
|
3281
|
+
.json-editor textarea::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); }
|
|
3282
|
+
.editor-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; }
|
|
3283
|
+
.json-status { display: inline-flex; align-items: center; gap: 6px; color: var(--ok); font-size: 11px; }
|
|
3284
|
+
.json-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
3285
|
+
.json-status.invalid { color: var(--err); }
|
|
3286
|
+
.format-json { border: 0; padding: 3px 5px; background: transparent; color: var(--accent); font-size: 11px; font-weight: 650; cursor: pointer; }
|
|
3287
|
+
.row-actions { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 14px 0; }
|
|
2995
3288
|
.banner {
|
|
2996
|
-
display: none;
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3289
|
+
display: none; position: sticky; top: calc(var(--header) + 12px); z-index: 15;
|
|
3290
|
+
align-items: center; gap: 10px; margin: 0 0 16px; padding: 11px 12px 11px 14px;
|
|
3291
|
+
border: 1px solid color-mix(in srgb, var(--err) 30%, var(--line)); border-radius: 10px;
|
|
3292
|
+
background: var(--err-soft); color: var(--err); box-shadow: var(--shadow-sm); font-size: 13px;
|
|
3293
|
+
}
|
|
3294
|
+
.banner.show { display: flex; }
|
|
3295
|
+
.banner span { flex: 1; }
|
|
3296
|
+
.banner button { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 7px; background: transparent; color: currentColor; cursor: pointer; }
|
|
3297
|
+
.banner button:hover { background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
3298
|
+
.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); }
|
|
3001
3299
|
dialog {
|
|
3002
|
-
border: 1px solid var(--line); border-radius:
|
|
3003
|
-
color: var(--ink); width: min(
|
|
3004
|
-
}
|
|
3005
|
-
dialog
|
|
3006
|
-
dialog
|
|
3300
|
+
border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--bg-elev);
|
|
3301
|
+
color: var(--ink); width: min(720px, calc(100% - 24px)); max-width: calc(100vw - 24px); box-shadow: var(--shadow-lg);
|
|
3302
|
+
}
|
|
3303
|
+
dialog[open] { animation: dialog-in 160ms ease-out; }
|
|
3304
|
+
dialog::backdrop { background: rgb(12 14 20 / 0.58); backdrop-filter: blur(3px); }
|
|
3305
|
+
dialog form, .dialog-body { padding: 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
3306
|
+
.dialog-heading { display: flex; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
|
3307
|
+
.dialog-heading strong { font-size: 18px; letter-spacing: -0.02em; }
|
|
3308
|
+
.dialog-heading span { color: var(--muted); font-size: 12px; }
|
|
3007
3309
|
textarea {
|
|
3008
3310
|
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
3009
3311
|
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
3010
3312
|
font-family: var(--mono); font-size: 12px;
|
|
3011
3313
|
}
|
|
3012
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
3013
|
-
.
|
|
3314
|
+
.dialog-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding-top: 4px; }
|
|
3315
|
+
.dialog-actions .btn-danger { margin-right: auto; }
|
|
3316
|
+
.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); }
|
|
3014
3317
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
3015
3318
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
3016
3319
|
.status-2 { color: var(--ok); }
|
|
3017
3320
|
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
3018
3321
|
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
3322
|
+
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
3323
|
+
.form-grid .wide { grid-column: span 2; }
|
|
3019
3324
|
.route {
|
|
3020
|
-
display: grid; grid-template-columns:
|
|
3021
|
-
padding:
|
|
3022
|
-
}
|
|
3023
|
-
.
|
|
3325
|
+
display: grid; grid-template-columns: 84px minmax(0, 1fr) auto; gap: 10px; align-items: center;
|
|
3326
|
+
padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--bg-elev);
|
|
3327
|
+
}
|
|
3328
|
+
.route:first-child { border-radius: var(--radius) var(--radius) 0 0; }
|
|
3329
|
+
.route:last-child { border-bottom: 0; border-radius: 0 0 var(--radius) var(--radius); }
|
|
3330
|
+
.route-tools { display: flex; align-items: flex-end; gap: 12px; margin-bottom: 12px; }
|
|
3331
|
+
.route-tools .field { flex: 1; }
|
|
3332
|
+
.route-tools input { width: 100%; }
|
|
3333
|
+
.route-count { padding-bottom: 10px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3334
|
+
#route-list { max-height: 34rem; border: 1px solid var(--line); border-radius: var(--radius); overflow: auto; box-shadow: var(--shadow-xs); }
|
|
3335
|
+
.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; }
|
|
3336
|
+
@keyframes view-in { from { transform: translateY(3px); } to { transform: translateY(0); } }
|
|
3337
|
+
@keyframes dialog-in { from { transform: translateY(8px) scale(0.99); } to { transform: translateY(0) scale(1); } }
|
|
3338
|
+
@keyframes pulse { 50% { opacity: 0.45; } }
|
|
3339
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
|
|
3340
|
+
@media (forced-colors: active) { .json-editor pre { display: none; } .json-editor textarea { color: CanvasText; -webkit-text-fill-color: CanvasText; } }
|
|
3024
3341
|
@media (max-width: 800px) {
|
|
3342
|
+
:root { --header: 0px; }
|
|
3025
3343
|
header.top {
|
|
3026
|
-
align-items: stretch; flex-direction: column;
|
|
3027
|
-
padding
|
|
3344
|
+
position: relative; align-items: stretch; flex-direction: column;
|
|
3345
|
+
padding: max(12px, env(safe-area-inset-top)) 14px 12px;
|
|
3028
3346
|
}
|
|
3029
3347
|
input, select, textarea { font-size: 16px; }
|
|
3030
3348
|
.brand, .vendor, .controls { width: 100%; }
|
|
3349
|
+
.brand { gap: 10px; }
|
|
3350
|
+
.vendor { order: 2; border-radius: 9px; }
|
|
3031
3351
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
3352
|
+
.sync-status { display: none; }
|
|
3032
3353
|
/* The layout fills the screen. A single-column grid stretches every auto
|
|
3033
3354
|
row, so the tab bar grew with the leftover height and the buttons floated
|
|
3034
3355
|
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
@@ -3038,14 +3359,23 @@ var renderAdminDocument = (service) => {
|
|
|
3038
3359
|
align-content: start;
|
|
3039
3360
|
}
|
|
3040
3361
|
nav.side {
|
|
3041
|
-
align-self: start; height: auto;
|
|
3362
|
+
position: sticky; top: 0; z-index: 12; align-self: start; height: auto;
|
|
3042
3363
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
3043
3364
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
3044
|
-
padding: 8px;
|
|
3365
|
+
padding: 8px; box-shadow: var(--shadow-xs);
|
|
3045
3366
|
}
|
|
3046
|
-
nav
|
|
3367
|
+
.nav-label { display: none; }
|
|
3368
|
+
nav.side button { width: auto; white-space: nowrap; flex: none; }
|
|
3369
|
+
nav.side button[aria-current="true"]::before { inset: auto 9px -8px; width: auto; height: 3px; border-radius: 3px 3px 0 0; }
|
|
3047
3370
|
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
3048
|
-
|
|
3371
|
+
.form-grid { grid-template-columns: minmax(0, 1fr); }
|
|
3372
|
+
.form-grid .wide { grid-column: auto; }
|
|
3373
|
+
main { padding: 22px 14px 48px; }
|
|
3374
|
+
.view-head { margin-bottom: 18px; }
|
|
3375
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
3376
|
+
.card { padding: 14px; }
|
|
3377
|
+
.card .v { font-size: 20px; }
|
|
3378
|
+
.table-shell { border: 0; background: transparent; box-shadow: none; overflow: visible; }
|
|
3049
3379
|
table, thead, tbody, tr, th, td { display: block; }
|
|
3050
3380
|
thead { display: none; }
|
|
3051
3381
|
tr {
|
|
@@ -3060,50 +3390,63 @@ var renderAdminDocument = (service) => {
|
|
|
3060
3390
|
.route { grid-template-columns: 72px 1fr; }
|
|
3061
3391
|
.route button { grid-column: 1 / -1; }
|
|
3062
3392
|
}
|
|
3393
|
+
@media (max-width: 460px) {
|
|
3394
|
+
.controls label.field { min-width: calc(50% - 4px); }
|
|
3395
|
+
.controls .icon-btn, .controls .btn { flex: 1; }
|
|
3396
|
+
.grid { grid-template-columns: minmax(0, 1fr); }
|
|
3397
|
+
.dialog-actions { flex-wrap: wrap; }
|
|
3398
|
+
.dialog-actions button { flex: 1; }
|
|
3399
|
+
.dialog-actions .btn-danger { flex-basis: 100%; margin-right: 0; }
|
|
3400
|
+
}
|
|
3063
3401
|
</style>
|
|
3064
3402
|
</head>
|
|
3065
3403
|
<body>
|
|
3066
3404
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
3067
3405
|
<header class="top">
|
|
3068
3406
|
<div class="brand">
|
|
3069
|
-
<div class="brand-
|
|
3070
|
-
<
|
|
3071
|
-
|
|
3407
|
+
<div class="brand-lockup">
|
|
3408
|
+
<div class="brand-mark" aria-hidden="true">
|
|
3409
|
+
<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>
|
|
3410
|
+
</div>
|
|
3411
|
+
<div class="brand-id">
|
|
3412
|
+
<strong>${name}</strong>
|
|
3413
|
+
<span>Mock admin</span>
|
|
3414
|
+
</div>
|
|
3072
3415
|
</div>
|
|
3073
3416
|
<div class="vendor" id="vendor" hidden></div>
|
|
3074
3417
|
</div>
|
|
3075
3418
|
<div class="controls">
|
|
3419
|
+
<span class="sync-status" id="sync-status" role="status" aria-live="polite">Ready</span>
|
|
3076
3420
|
<label class="field">Namespace
|
|
3077
3421
|
<select id="namespace" aria-label="Namespace"></select>
|
|
3078
3422
|
</label>
|
|
3079
3423
|
<label class="field">Admin key
|
|
3080
3424
|
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
3081
3425
|
</label>
|
|
3082
|
-
<button class="icon-btn" id="theme" type="button" aria-label="
|
|
3083
|
-
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
3426
|
+
<button class="icon-btn" id="theme" type="button" aria-label="Use dark theme" title="Use dark theme"></button>
|
|
3427
|
+
<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>
|
|
3084
3428
|
</div>
|
|
3085
3429
|
</header>
|
|
3086
3430
|
<div class="layout">
|
|
3087
3431
|
<nav class="side" id="nav" aria-label="Admin sections">
|
|
3088
|
-
<
|
|
3089
|
-
<button type="button" data-view="
|
|
3090
|
-
<button type="button" data-view="
|
|
3091
|
-
<button type="button" data-view="
|
|
3092
|
-
<button type="button" data-view="
|
|
3093
|
-
<button type="button" data-view="
|
|
3432
|
+
<span class="nav-label">Workspace</span>
|
|
3433
|
+
<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>
|
|
3434
|
+
<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>
|
|
3435
|
+
<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>
|
|
3436
|
+
<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>
|
|
3437
|
+
<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>
|
|
3438
|
+
<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>
|
|
3094
3439
|
</nav>
|
|
3095
3440
|
<main id="views">
|
|
3096
|
-
<
|
|
3441
|
+
<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>
|
|
3097
3442
|
<section class="view" id="view-overview">
|
|
3098
|
-
<h2>Overview</h2>
|
|
3099
|
-
<p class="lede">Health, clock, and the collections this namespace is holding.</p>
|
|
3443
|
+
<div class="view-head"><div><h2>Overview</h2><p class="lede">Health, clock, and the collections this namespace is holding.</p></div></div>
|
|
3100
3444
|
<div class="grid" id="overview-cards"></div>
|
|
3101
3445
|
<h3>Recent requests</h3>
|
|
3102
3446
|
<div id="overview-requests"></div>
|
|
3103
3447
|
</section>
|
|
3104
3448
|
<section class="view" id="view-state" hidden>
|
|
3105
|
-
<h2>State</h2>
|
|
3106
|
-
<p class="lede">Every collection in this namespace. Declared fields stay visible before any row exists; stored rows add whatever else they contain.</p>
|
|
3449
|
+
<div class="view-head"><div><h2>State</h2><p class="lede">Inspect and edit every collection in this namespace.</p></div></div>
|
|
3107
3450
|
<div class="split">
|
|
3108
3451
|
<div id="collections"></div>
|
|
3109
3452
|
<div>
|
|
@@ -3116,8 +3459,7 @@ var renderAdminDocument = (service) => {
|
|
|
3116
3459
|
</div>
|
|
3117
3460
|
</section>
|
|
3118
3461
|
<section class="view" id="view-clock" hidden>
|
|
3119
|
-
<h2>Clock</h2>
|
|
3120
|
-
<p class="lede">The clock every timestamp in this mock reads.</p>
|
|
3462
|
+
<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>
|
|
3121
3463
|
<div class="card" id="clock-readout"></div>
|
|
3122
3464
|
<div class="row-actions">
|
|
3123
3465
|
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
@@ -3136,21 +3478,22 @@ var renderAdminDocument = (service) => {
|
|
|
3136
3478
|
</form>
|
|
3137
3479
|
</section>
|
|
3138
3480
|
<section class="view" id="view-faults" hidden>
|
|
3139
|
-
<h2>Faults</h2>
|
|
3140
|
-
<p class="lede">Inject a response, a delay, or a named preset. Rules apply to this namespace.</p>
|
|
3481
|
+
<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>
|
|
3141
3482
|
<form id="fault-form" class="card stack">
|
|
3142
|
-
<
|
|
3143
|
-
<
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
<
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
<
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
<
|
|
3153
|
-
|
|
3483
|
+
<div class="form-grid">
|
|
3484
|
+
<label class="field wide">Preset
|
|
3485
|
+
<select id="fault-preset"><option value="">Custom rule</option></select>
|
|
3486
|
+
</label>
|
|
3487
|
+
<label class="field">Status
|
|
3488
|
+
<input id="fault-status" inputmode="numeric" placeholder="503">
|
|
3489
|
+
</label>
|
|
3490
|
+
<label class="field wide">Path prefix
|
|
3491
|
+
<input id="fault-path" placeholder="/v1">
|
|
3492
|
+
</label>
|
|
3493
|
+
<label class="field">Method
|
|
3494
|
+
<input id="fault-method" placeholder="GET">
|
|
3495
|
+
</label>
|
|
3496
|
+
</div>
|
|
3154
3497
|
<div class="dialog-actions">
|
|
3155
3498
|
<button class="btn-primary" type="submit">Add fault</button>
|
|
3156
3499
|
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
@@ -3159,26 +3502,31 @@ var renderAdminDocument = (service) => {
|
|
|
3159
3502
|
<div id="fault-list"></div>
|
|
3160
3503
|
</section>
|
|
3161
3504
|
<section class="view" id="view-journal" hidden>
|
|
3162
|
-
<h2>Journal</h2>
|
|
3163
|
-
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
3164
|
-
<div class="row-actions">
|
|
3165
|
-
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
3166
|
-
</div>
|
|
3505
|
+
<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>
|
|
3167
3506
|
<div id="journal"></div>
|
|
3168
3507
|
</section>
|
|
3169
3508
|
<section class="view" id="view-routes" hidden>
|
|
3170
|
-
<h2>Routes</h2>
|
|
3171
|
-
<
|
|
3509
|
+
<div class="view-head"><div><h2>Routes</h2><p class="lede">Explore shared controls and service-specific admin endpoints.</p></div></div>
|
|
3510
|
+
<div class="route-tools">
|
|
3511
|
+
<label class="field">Filter routes
|
|
3512
|
+
<input id="route-search" type="search" placeholder="Method, path, or extension" autocomplete="off">
|
|
3513
|
+
</label>
|
|
3514
|
+
<span class="route-count" id="route-count"></span>
|
|
3515
|
+
</div>
|
|
3172
3516
|
<div id="route-list"></div>
|
|
3173
|
-
<form id="route-form" class="card stack">
|
|
3517
|
+
<form id="route-form" class="card stack" style="margin-top: 18px">
|
|
3174
3518
|
<strong id="route-title">Select a route</strong>
|
|
3175
3519
|
<label class="field">JSON body
|
|
3176
|
-
<
|
|
3520
|
+
<div class="json-editor" id="route-editor" hidden>
|
|
3521
|
+
<pre aria-hidden="true"><code id="route-highlight"></code></pre>
|
|
3522
|
+
<textarea id="route-body" placeholder="{ }" spellcheck="false" aria-describedby="route-json-status" disabled></textarea>
|
|
3523
|
+
</div>
|
|
3524
|
+
<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>
|
|
3177
3525
|
</label>
|
|
3178
3526
|
<div class="dialog-actions">
|
|
3179
|
-
<button class="btn-primary" type="submit">Send</button>
|
|
3527
|
+
<button class="btn-primary" id="route-send" type="submit" disabled>Send request</button>
|
|
3180
3528
|
</div>
|
|
3181
|
-
<pre id="route-result">Response will show here.</pre>
|
|
3529
|
+
<pre class="json-view" id="route-result"><code>Response will show here.</code></pre>
|
|
3182
3530
|
</form>
|
|
3183
3531
|
</section>
|
|
3184
3532
|
</main>
|
|
@@ -3186,12 +3534,16 @@ var renderAdminDocument = (service) => {
|
|
|
3186
3534
|
</div>
|
|
3187
3535
|
<dialog id="editor">
|
|
3188
3536
|
<form method="dialog" id="editor-form">
|
|
3189
|
-
<strong id="editor-title">Record</strong>
|
|
3537
|
+
<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>
|
|
3190
3538
|
<label class="field">Id
|
|
3191
3539
|
<input id="editor-id" required>
|
|
3192
3540
|
</label>
|
|
3193
3541
|
<label class="field">JSON value
|
|
3194
|
-
<
|
|
3542
|
+
<div class="json-editor" id="record-editor">
|
|
3543
|
+
<pre aria-hidden="true"><code id="editor-highlight"></code></pre>
|
|
3544
|
+
<textarea id="editor-value" spellcheck="false" aria-describedby="editor-json-status"></textarea>
|
|
3545
|
+
</div>
|
|
3546
|
+
<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>
|
|
3195
3547
|
</label>
|
|
3196
3548
|
<div class="dialog-actions">
|
|
3197
3549
|
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
@@ -3233,8 +3585,8 @@ var describeExtension = (extension) => {
|
|
|
3233
3585
|
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
3234
3586
|
};
|
|
3235
3587
|
};
|
|
3236
|
-
var adminUiRoutes = (service, ui) => {
|
|
3237
|
-
const shell = () => renderAdminDocument(service);
|
|
3588
|
+
var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
3589
|
+
const shell = () => renderAdminDocument(service, adminPrefix);
|
|
3238
3590
|
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
3239
3591
|
return {
|
|
3240
3592
|
"GET /ui": () => htmlResponse(document3()),
|
|
@@ -3851,7 +4203,7 @@ var stateAdminRoutes = (routes) => {
|
|
|
3851
4203
|
};
|
|
3852
4204
|
|
|
3853
4205
|
// ../core/dist/version.js
|
|
3854
|
-
var PACKAGE_VERSION = true ? "
|
|
4206
|
+
var PACKAGE_VERSION = true ? "2.0.0" : UNRELEASED_VERSION;
|
|
3855
4207
|
|
|
3856
4208
|
// ../core/dist/signing.js
|
|
3857
4209
|
var encoder = new TextEncoder();
|
|
@@ -4224,7 +4576,10 @@ var createWebhookHub = (options) => {
|
|
|
4224
4576
|
};
|
|
4225
4577
|
return hub;
|
|
4226
4578
|
};
|
|
4227
|
-
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4579
|
+
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4580
|
+
status,
|
|
4581
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4582
|
+
});
|
|
4228
4583
|
var adminError3 = (status, message) => json4(status, { error: { type: "mockingbird_admin", message } });
|
|
4229
4584
|
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4230
4585
|
var parseEndpoint = (value) => {
|
|
@@ -4332,7 +4687,6 @@ var AT_HEADER = "x-mockingbird-at";
|
|
|
4332
4687
|
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
4333
4688
|
var DEFAULT_NAMESPACE = "default";
|
|
4334
4689
|
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4335
|
-
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
4336
4690
|
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4337
4691
|
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
4338
4692
|
var effects = /* @__PURE__ */ new WeakMap();
|
|
@@ -4373,6 +4727,22 @@ var operationMatcher = (document3) => {
|
|
|
4373
4727
|
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
4374
4728
|
};
|
|
4375
4729
|
var createRuntime = (options) => {
|
|
4730
|
+
const adminPrefix = resolveAdminPrefix(options.adminPrefix);
|
|
4731
|
+
const internalPaths = Object.entries(options.document?.paths ?? {}).filter(([, item]) => item["x-mockingbird-internal"] === true).map(([path]) => path);
|
|
4732
|
+
for (const path of internalPaths) {
|
|
4733
|
+
if (!path.startsWith(`${ADMIN_PREFIX}/blobs/`))
|
|
4734
|
+
throw new Error(`internal transport ${path} must live under ${ADMIN_PREFIX}/blobs/`);
|
|
4735
|
+
}
|
|
4736
|
+
assertAdminPrefixAvailable(adminPrefix, Object.keys(options.document?.paths ?? {}).filter((path) => !internalPaths.includes(path)));
|
|
4737
|
+
const internalMatch = (path) => {
|
|
4738
|
+
for (const pattern of internalPaths) {
|
|
4739
|
+
const actual = path.split("/");
|
|
4740
|
+
const expected = pattern.split("/");
|
|
4741
|
+
if (actual.length === expected.length && expected.every((part, i) => part.startsWith("{") || part === actual[i]))
|
|
4742
|
+
return pattern;
|
|
4743
|
+
}
|
|
4744
|
+
return void 0;
|
|
4745
|
+
};
|
|
4376
4746
|
assertAdminUi(options.adminUi);
|
|
4377
4747
|
const sqlite = bootSqlite(options.sqlite);
|
|
4378
4748
|
const clock = options.clock ?? createClock();
|
|
@@ -4418,6 +4788,7 @@ var createRuntime = (options) => {
|
|
|
4418
4788
|
const created = options.create({
|
|
4419
4789
|
namespace: storageNamespace(key),
|
|
4420
4790
|
publicNamespace,
|
|
4791
|
+
adminPrefix,
|
|
4421
4792
|
sqlite,
|
|
4422
4793
|
clock: namespaceClock(publicNamespace),
|
|
4423
4794
|
rng: createdContextRng
|
|
@@ -4650,10 +5021,10 @@ var createRuntime = (options) => {
|
|
|
4650
5021
|
namespaceClock,
|
|
4651
5022
|
namespaceOf(request) {
|
|
4652
5023
|
const url = new URL(request.url);
|
|
4653
|
-
const prefixed =
|
|
5024
|
+
const prefixed = matchNamespacePath(url.pathname, adminPrefix);
|
|
4654
5025
|
const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
|
|
4655
5026
|
const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
|
|
4656
|
-
if (path
|
|
5027
|
+
if (isAdminPath(path, adminPrefix))
|
|
4657
5028
|
return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
|
|
4658
5029
|
if (selected !== void 0)
|
|
4659
5030
|
return selected;
|
|
@@ -4719,7 +5090,7 @@ var createRuntime = (options) => {
|
|
|
4719
5090
|
state: (namespace = DEFAULT_NAMESPACE) => inspectState(stateScope(namespace)),
|
|
4720
5091
|
fetch: async (incoming) => {
|
|
4721
5092
|
let request = incoming;
|
|
4722
|
-
const prefixed =
|
|
5093
|
+
const prefixed = matchNamespacePath(new URL(request.url).pathname, adminPrefix);
|
|
4723
5094
|
if (prefixed) {
|
|
4724
5095
|
const url2 = new URL(request.url);
|
|
4725
5096
|
url2.pathname = prefixed[2] ?? "/";
|
|
@@ -4735,7 +5106,15 @@ var createRuntime = (options) => {
|
|
|
4735
5106
|
signal: request.signal
|
|
4736
5107
|
});
|
|
4737
5108
|
}
|
|
4738
|
-
|
|
5109
|
+
const internalUrl = new URL(request.url);
|
|
5110
|
+
const internalPath = isAdminPath(internalUrl.pathname, adminPrefix) ? `${ADMIN_PREFIX}${internalUrl.pathname.slice(adminPrefix.length)}` : "";
|
|
5111
|
+
const internal = internalMatch(internalPath);
|
|
5112
|
+
const obsoleteInternalPath = adminPrefix !== ADMIN_PREFIX && internalMatch(internalUrl.pathname) && !internal;
|
|
5113
|
+
if (internal) {
|
|
5114
|
+
internalUrl.pathname = internalPath;
|
|
5115
|
+
request = new Request(internalUrl, request);
|
|
5116
|
+
}
|
|
5117
|
+
let namespace = internal ? internalUrl.searchParams.get("namespace") ?? control.namespaceOf(request) : control.namespaceOf(request);
|
|
4739
5118
|
if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
4740
5119
|
const credential = options.credential(request);
|
|
4741
5120
|
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
@@ -4755,7 +5134,9 @@ var createRuntime = (options) => {
|
|
|
4755
5134
|
return copy;
|
|
4756
5135
|
}
|
|
4757
5136
|
};
|
|
4758
|
-
|
|
5137
|
+
if (obsoleteInternalPath)
|
|
5138
|
+
return stamp(adminFail(404, "internal transport is only available under adminPrefix"));
|
|
5139
|
+
const handled = internal ? void 0 : await control.handle(request);
|
|
4759
5140
|
if (handled)
|
|
4760
5141
|
return stamp(handled);
|
|
4761
5142
|
const started = monotonicNow();
|
|
@@ -4796,7 +5177,7 @@ var createRuntime = (options) => {
|
|
|
4796
5177
|
type: "mockingbird_admin",
|
|
4797
5178
|
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
4798
5179
|
}
|
|
4799
|
-
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
5180
|
+
}), { status: 400, headers: { "content-type": "application/json; charset=utf-8" } }));
|
|
4800
5181
|
}
|
|
4801
5182
|
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
4802
5183
|
log(400);
|
|
@@ -4913,15 +5294,16 @@ var createRuntime = (options) => {
|
|
|
4913
5294
|
...options.admin?.(runtime) ?? {},
|
|
4914
5295
|
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
4915
5296
|
// maps `presets`, and a service route must not replace that list with a record.
|
|
4916
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
5297
|
+
...options.presets ? presetRoutes(options.presets, runtime, adminPrefix) : {},
|
|
4917
5298
|
...stateAdminRoutes({
|
|
4918
5299
|
open: stateScope,
|
|
4919
5300
|
afterWrite: (namespace) => {
|
|
4920
5301
|
checkpoint(namespace, "main");
|
|
4921
5302
|
}
|
|
4922
5303
|
}),
|
|
4923
|
-
...adminUiRoutes(options.name, options.adminUi)
|
|
5304
|
+
...adminUiRoutes(options.name, options.adminUi, adminPrefix)
|
|
4924
5305
|
},
|
|
5306
|
+
adminPrefix,
|
|
4925
5307
|
adminKey: options.adminKey
|
|
4926
5308
|
});
|
|
4927
5309
|
return defineMock(runtime);
|
|
@@ -4935,7 +5317,10 @@ var mutableResponse = (response) => {
|
|
|
4935
5317
|
return new Response(response.body, response);
|
|
4936
5318
|
}
|
|
4937
5319
|
};
|
|
4938
|
-
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5320
|
+
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5321
|
+
status,
|
|
5322
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
5323
|
+
});
|
|
4939
5324
|
var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
|
|
4940
5325
|
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4941
5326
|
var credentialRoutes = (registry) => ({
|
|
@@ -4987,7 +5372,7 @@ var credentialRoutes = (registry) => ({
|
|
|
4987
5372
|
return adminJson(200, { status: "ok" });
|
|
4988
5373
|
}
|
|
4989
5374
|
});
|
|
4990
|
-
var presetRoutes = (presets, runtime) => ({
|
|
5375
|
+
var presetRoutes = (presets, runtime, adminPrefix) => ({
|
|
4991
5376
|
"GET /faults/presets": () => {
|
|
4992
5377
|
const body = {
|
|
4993
5378
|
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
@@ -4997,7 +5382,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
4997
5382
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
4998
5383
|
const name = params.name;
|
|
4999
5384
|
if (!presets[name])
|
|
5000
|
-
return adminFail(404, `no fault preset ${name}; GET /
|
|
5385
|
+
return adminFail(404, `no fault preset ${name}; GET ${adminPrefix}/faults/presets`);
|
|
5001
5386
|
const overrides = isObject(body) ? body : {};
|
|
5002
5387
|
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
5003
5388
|
}
|
|
@@ -6106,6 +6491,7 @@ var createRuntime2 = (options = {}) => {
|
|
|
6106
6491
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
6107
6492
|
...options.clock ? { clock: options.clock } : {},
|
|
6108
6493
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
6494
|
+
...options.adminPrefix !== void 0 ? { adminPrefix: options.adminPrefix } : {},
|
|
6109
6495
|
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
6110
6496
|
...options.onLog ? { onLog: options.onLog } : {},
|
|
6111
6497
|
credential: formbricksCredential,
|
|
@@ -6736,6 +7122,7 @@ var FormbricksAPI = class {
|
|
|
6736
7122
|
|
|
6737
7123
|
export {
|
|
6738
7124
|
createClock,
|
|
7125
|
+
resolveAdminPrefix,
|
|
6739
7126
|
parseDuration,
|
|
6740
7127
|
document2 as document,
|
|
6741
7128
|
operationIds,
|
|
@@ -6753,4 +7140,4 @@ export {
|
|
|
6753
7140
|
formbricksError,
|
|
6754
7141
|
FormbricksAPI
|
|
6755
7142
|
};
|
|
6756
|
-
//# sourceMappingURL=chunk-
|
|
7143
|
+
//# sourceMappingURL=chunk-2YLOIK2J.js.map
|