@crvouga/mockingbird-service-posthog 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 +3 -3
- package/SUPPORT.md +26 -0
- package/dist/{chunk-LKPLI7YI.js → chunk-FM7IAA7L.js} +43 -23
- package/dist/chunk-FM7IAA7L.js.map +7 -0
- package/dist/{chunk-Y3KAEWE2.js → chunk-NRFKITRM.js} +623 -236
- package/dist/chunk-NRFKITRM.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 +1017 -0
- package/package.json +29 -4
- package/dist/chunk-LKPLI7YI.js.map +0 -7
- package/dist/chunk-Y3KAEWE2.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
|
};
|
|
@@ -1581,6 +1620,34 @@ dialog:not([open]) {
|
|
|
1581
1620
|
function isRecord2(value) {
|
|
1582
1621
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1583
1622
|
}
|
|
1623
|
+
function highlightJsonText(text) {
|
|
1624
|
+
const escapeToken = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
1625
|
+
switch (char) {
|
|
1626
|
+
case "&":
|
|
1627
|
+
return "&";
|
|
1628
|
+
case "<":
|
|
1629
|
+
return "<";
|
|
1630
|
+
case ">":
|
|
1631
|
+
return ">";
|
|
1632
|
+
case '"':
|
|
1633
|
+
return """;
|
|
1634
|
+
default:
|
|
1635
|
+
return "'";
|
|
1636
|
+
}
|
|
1637
|
+
});
|
|
1638
|
+
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;
|
|
1639
|
+
let cursor = 0;
|
|
1640
|
+
let html = "";
|
|
1641
|
+
for (const match of text.matchAll(token)) {
|
|
1642
|
+
const index = match.index ?? 0;
|
|
1643
|
+
const value = match[0];
|
|
1644
|
+
html += escapeToken(text.slice(cursor, index));
|
|
1645
|
+
const kind = value.startsWith('"') ? value.trimEnd().endsWith(":") ? "key" : "string" : value === "true" || value === "false" ? "boolean" : value === "null" ? "null" : "number";
|
|
1646
|
+
html += `<span class="json-${kind}">${escapeToken(value)}</span>`;
|
|
1647
|
+
cursor = index + value.length;
|
|
1648
|
+
}
|
|
1649
|
+
return html + escapeToken(text.slice(cursor));
|
|
1650
|
+
}
|
|
1584
1651
|
function mountAdminBrand(doc, host, service, catalog) {
|
|
1585
1652
|
const clip = (value, max) => {
|
|
1586
1653
|
if (typeof value !== "string")
|
|
@@ -1942,14 +2009,16 @@ function bootAdmin(config) {
|
|
|
1942
2009
|
const state = {
|
|
1943
2010
|
namespace: "default",
|
|
1944
2011
|
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
1945
|
-
view: "overview",
|
|
2012
|
+
view: ["overview", "state", "clock", "faults", "journal", "routes"].includes(location.hash.slice(1)) ? location.hash.slice(1) : "overview",
|
|
1946
2013
|
collection: "",
|
|
1947
2014
|
shape: null,
|
|
1948
2015
|
route: null,
|
|
2016
|
+
routes: [],
|
|
1949
2017
|
editing: null
|
|
1950
2018
|
};
|
|
1951
2019
|
const standard = new Set(config.standardRoutes);
|
|
1952
2020
|
const banner = el("banner", HTMLElement);
|
|
2021
|
+
const bannerMessage = el("banner-message", HTMLElement);
|
|
1953
2022
|
const keyInput = el("admin-key", HTMLInputElement);
|
|
1954
2023
|
keyInput.value = state.key;
|
|
1955
2024
|
const params = new URLSearchParams(location.search);
|
|
@@ -1960,18 +2029,21 @@ function bootAdmin(config) {
|
|
|
1960
2029
|
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
1961
2030
|
params.delete("key");
|
|
1962
2031
|
const next = params.toString();
|
|
1963
|
-
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
2032
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : "") + location.hash);
|
|
1964
2033
|
}
|
|
1965
2034
|
const queryNamespace = params.get("namespace");
|
|
1966
2035
|
if (queryNamespace)
|
|
1967
2036
|
state.namespace = queryNamespace;
|
|
1968
2037
|
const showError = (error2) => {
|
|
1969
|
-
|
|
2038
|
+
bannerMessage.textContent = error2 instanceof Error ? error2.message : String(error2);
|
|
1970
2039
|
banner.classList.add("show");
|
|
2040
|
+
banner.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
1971
2041
|
};
|
|
1972
2042
|
const clearError = () => {
|
|
1973
2043
|
banner.classList.remove("show");
|
|
2044
|
+
bannerMessage.textContent = "";
|
|
1974
2045
|
};
|
|
2046
|
+
el("banner-dismiss", HTMLButtonElement).addEventListener("click", clearError);
|
|
1975
2047
|
const api = async (method, path, body) => {
|
|
1976
2048
|
const headers = { accept: "application/json" };
|
|
1977
2049
|
if (state.key !== "")
|
|
@@ -2015,16 +2087,21 @@ function bootAdmin(config) {
|
|
|
2015
2087
|
namespace: () => state.namespace
|
|
2016
2088
|
};
|
|
2017
2089
|
const isPanelScript = (value) => typeof value === "function";
|
|
2090
|
+
const themeButton = el("theme", HTMLButtonElement);
|
|
2018
2091
|
const setTheme = (theme) => {
|
|
2019
2092
|
if (theme)
|
|
2020
2093
|
document.documentElement.dataset.theme = theme;
|
|
2021
2094
|
else
|
|
2022
2095
|
delete document.documentElement.dataset.theme;
|
|
2096
|
+
const dark = theme ? theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2097
|
+
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>';
|
|
2098
|
+
const label = dark ? "Use light theme" : "Use dark theme";
|
|
2099
|
+
themeButton.setAttribute("aria-label", label);
|
|
2100
|
+
themeButton.title = label;
|
|
2023
2101
|
};
|
|
2024
2102
|
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
2103
|
+
setTheme(storedTheme === "dark" || storedTheme === "light" ? storedTheme : null);
|
|
2104
|
+
themeButton.addEventListener("click", () => {
|
|
2028
2105
|
const current = document.documentElement.dataset.theme;
|
|
2029
2106
|
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2030
2107
|
const next = dark ? "light" : "dark";
|
|
@@ -2043,6 +2120,7 @@ function bootAdmin(config) {
|
|
|
2043
2120
|
continue;
|
|
2044
2121
|
section.hidden = section.id !== `view-${view}`;
|
|
2045
2122
|
}
|
|
2123
|
+
history.replaceState(null, "", `${location.pathname}${location.search}#${encodeURIComponent(view)}`);
|
|
2046
2124
|
};
|
|
2047
2125
|
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
2048
2126
|
const target = event.target;
|
|
@@ -2057,7 +2135,7 @@ function bootAdmin(config) {
|
|
|
2057
2135
|
show(view);
|
|
2058
2136
|
refresh().catch(showError);
|
|
2059
2137
|
});
|
|
2060
|
-
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>`;
|
|
2138
|
+
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>`;
|
|
2061
2139
|
const table = (columns, rows) => {
|
|
2062
2140
|
if (rows.length === 0)
|
|
2063
2141
|
return '<p class="empty">Nothing here yet.</p>';
|
|
@@ -2067,16 +2145,22 @@ function bootAdmin(config) {
|
|
|
2067
2145
|
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
2068
2146
|
return `<tr${attrs}>${cells}</tr>`;
|
|
2069
2147
|
}).join("");
|
|
2070
|
-
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
2148
|
+
return `<div class="table-shell"><table><thead>${head}</thead><tbody>${body}</tbody></table></div>`;
|
|
2071
2149
|
};
|
|
2072
2150
|
const preview = (value) => {
|
|
2073
2151
|
const text = JSON.stringify(value) ?? "null";
|
|
2074
|
-
|
|
2152
|
+
const clipped = text.length > 180 ? `${text.slice(0, 180)}\u2026` : text;
|
|
2153
|
+
return `<code class="json-inline">${highlightJsonText(clipped)}</code>`;
|
|
2075
2154
|
};
|
|
2076
2155
|
const when = (epoch) => {
|
|
2077
2156
|
const date = new Date(epoch);
|
|
2078
2157
|
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
2079
2158
|
};
|
|
2159
|
+
const journalWhen = (value) => {
|
|
2160
|
+
const numeric = value.trim() === "" ? Number.NaN : Number(value);
|
|
2161
|
+
const epoch = Number.isFinite(numeric) ? numeric : Date.parse(value);
|
|
2162
|
+
return Number.isFinite(epoch) ? when(epoch) : value;
|
|
2163
|
+
};
|
|
2080
2164
|
const clockCard = (epoch) => {
|
|
2081
2165
|
const date = new Date(epoch);
|
|
2082
2166
|
if (Number.isNaN(date.getTime()))
|
|
@@ -2196,7 +2280,7 @@ function bootAdmin(config) {
|
|
|
2196
2280
|
};
|
|
2197
2281
|
const renderOverview = async () => {
|
|
2198
2282
|
const [health, shape, clock, journal] = await Promise.all([
|
|
2199
|
-
originGet(
|
|
2283
|
+
originGet(`${config.adminPrefix}/health`),
|
|
2200
2284
|
api("GET", "/state"),
|
|
2201
2285
|
api("GET", "/clock"),
|
|
2202
2286
|
api("GET", "/requests?limit=8")
|
|
@@ -2213,7 +2297,7 @@ function bootAdmin(config) {
|
|
|
2213
2297
|
},
|
|
2214
2298
|
{
|
|
2215
2299
|
k: "Status",
|
|
2216
|
-
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
2300
|
+
v: `<span class="badge ok">${esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")}</span>`
|
|
2217
2301
|
},
|
|
2218
2302
|
{ k: "Namespace", v: esc(state.namespace) },
|
|
2219
2303
|
{ k: "Collections", v: String(decoded2.collections.length) },
|
|
@@ -2300,26 +2384,46 @@ function bootAdmin(config) {
|
|
|
2300
2384
|
const renderJournal = async () => {
|
|
2301
2385
|
const journal = await api("GET", "/requests?limit=100");
|
|
2302
2386
|
el("journal", HTMLElement).innerHTML = table([
|
|
2303
|
-
{ label: "When", cell: (row) => esc(row.at) },
|
|
2304
|
-
{
|
|
2387
|
+
{ label: "When", cell: (row) => esc(journalWhen(row.at)) },
|
|
2388
|
+
{
|
|
2389
|
+
label: "Method",
|
|
2390
|
+
cell: (row) => `<span class="method">${esc(row.method)}</span>`
|
|
2391
|
+
},
|
|
2305
2392
|
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2306
|
-
{
|
|
2393
|
+
{
|
|
2394
|
+
label: "Status",
|
|
2395
|
+
cell: (row) => `<span class="badge ${row.status.startsWith("2") ? "ok" : row.status.startsWith("4") || row.status.startsWith("5") ? "warn" : ""}">${esc(row.status)}</span>`
|
|
2396
|
+
},
|
|
2307
2397
|
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
2308
2398
|
], decodeJournal(journal));
|
|
2309
2399
|
};
|
|
2310
|
-
const
|
|
2311
|
-
const
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2400
|
+
const paintRoutes = () => {
|
|
2401
|
+
const query = el("route-search", HTMLInputElement).value.trim().toLocaleLowerCase();
|
|
2402
|
+
const routes = state.routes.filter((route) => route.toLocaleLowerCase().includes(query));
|
|
2403
|
+
el("route-count", HTMLElement).textContent = `${routes.length} of ${state.routes.length}`;
|
|
2404
|
+
if (state.routes.length === 0) {
|
|
2405
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No admin routes are available.</p>';
|
|
2406
|
+
return;
|
|
2407
|
+
}
|
|
2408
|
+
if (routes.length === 0) {
|
|
2409
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No routes match this filter.</p>';
|
|
2410
|
+
return;
|
|
2411
|
+
}
|
|
2315
2412
|
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2316
2413
|
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2317
2414
|
const parts = route.split(" ");
|
|
2318
2415
|
const method = parts[0] ?? "";
|
|
2319
2416
|
const path = parts.slice(1).join(" ");
|
|
2320
|
-
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>`;
|
|
2417
|
+
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>`;
|
|
2321
2418
|
}).join("");
|
|
2322
2419
|
};
|
|
2420
|
+
const renderRoutes = async () => {
|
|
2421
|
+
const data = await api("GET", "/");
|
|
2422
|
+
if (!isRecord2(data))
|
|
2423
|
+
throw new Error("Route list was not an object");
|
|
2424
|
+
state.routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
2425
|
+
paintRoutes();
|
|
2426
|
+
};
|
|
2323
2427
|
const renderers = {
|
|
2324
2428
|
overview: renderOverview,
|
|
2325
2429
|
state: renderState,
|
|
@@ -2328,19 +2432,50 @@ function bootAdmin(config) {
|
|
|
2328
2432
|
journal: renderJournal,
|
|
2329
2433
|
routes: renderRoutes
|
|
2330
2434
|
};
|
|
2435
|
+
let refreshToken = 0;
|
|
2331
2436
|
async function refresh() {
|
|
2437
|
+
const token = ++refreshToken;
|
|
2332
2438
|
clearError();
|
|
2439
|
+
const main = el("views", HTMLElement);
|
|
2440
|
+
const sync = el("sync-status", HTMLElement);
|
|
2441
|
+
main.setAttribute("aria-busy", "true");
|
|
2442
|
+
sync.classList.add("busy");
|
|
2443
|
+
sync.textContent = "Updating";
|
|
2333
2444
|
const render = renderers[state.view];
|
|
2334
|
-
|
|
2335
|
-
|
|
2445
|
+
try {
|
|
2446
|
+
if (render)
|
|
2447
|
+
await render();
|
|
2448
|
+
if (token === refreshToken)
|
|
2449
|
+
sync.textContent = "Up to date";
|
|
2450
|
+
} finally {
|
|
2451
|
+
if (token === refreshToken) {
|
|
2452
|
+
main.removeAttribute("aria-busy");
|
|
2453
|
+
sync.classList.remove("busy");
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2336
2456
|
}
|
|
2337
2457
|
const delay = (ms) => new Promise((resolve) => {
|
|
2338
2458
|
window.setTimeout(resolve, ms);
|
|
2339
2459
|
});
|
|
2340
|
-
const confirmAction = async (button, pending, done, work) => {
|
|
2460
|
+
const confirmAction = async (button, pending, done, work, confirmLabel) => {
|
|
2341
2461
|
if (button.dataset.pending === "1")
|
|
2342
2462
|
return;
|
|
2343
2463
|
const label = button.textContent ?? "";
|
|
2464
|
+
if (confirmLabel !== void 0 && button.dataset.confirming !== "1") {
|
|
2465
|
+
button.dataset.confirming = "1";
|
|
2466
|
+
button.classList.add("confirming");
|
|
2467
|
+
button.textContent = confirmLabel;
|
|
2468
|
+
window.setTimeout(() => {
|
|
2469
|
+
if (button.dataset.confirming !== "1")
|
|
2470
|
+
return;
|
|
2471
|
+
button.textContent = label;
|
|
2472
|
+
button.classList.remove("confirming");
|
|
2473
|
+
delete button.dataset.confirming;
|
|
2474
|
+
}, 3e3);
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
button.classList.remove("confirming");
|
|
2478
|
+
delete button.dataset.confirming;
|
|
2344
2479
|
const width = button.offsetWidth;
|
|
2345
2480
|
button.dataset.pending = "1";
|
|
2346
2481
|
button.classList.add("pressed");
|
|
@@ -2361,6 +2496,65 @@ function bootAdmin(config) {
|
|
|
2361
2496
|
delete button.dataset.pending;
|
|
2362
2497
|
}
|
|
2363
2498
|
};
|
|
2499
|
+
const wireJsonEditor = (textareaId, highlightId, statusId, shellId, allowEmpty) => {
|
|
2500
|
+
const textarea = el(textareaId, HTMLTextAreaElement);
|
|
2501
|
+
const highlight = el(highlightId, HTMLElement);
|
|
2502
|
+
const status = el(statusId, HTMLElement);
|
|
2503
|
+
const shell = el(shellId, HTMLElement);
|
|
2504
|
+
const sync = () => {
|
|
2505
|
+
const text = textarea.value;
|
|
2506
|
+
highlight.innerHTML = `${highlightJsonText(text)}
|
|
2507
|
+
`;
|
|
2508
|
+
let valid = true;
|
|
2509
|
+
let message = "Valid JSON";
|
|
2510
|
+
if (allowEmpty && text.trim() === "")
|
|
2511
|
+
message = "No body";
|
|
2512
|
+
else {
|
|
2513
|
+
try {
|
|
2514
|
+
readJson2(text);
|
|
2515
|
+
} catch {
|
|
2516
|
+
valid = false;
|
|
2517
|
+
message = "Invalid JSON";
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
shell.classList.toggle("invalid", !valid);
|
|
2521
|
+
status.classList.toggle("invalid", !valid);
|
|
2522
|
+
status.textContent = message;
|
|
2523
|
+
textarea.setAttribute("aria-invalid", valid ? "false" : "true");
|
|
2524
|
+
};
|
|
2525
|
+
const syncScroll = () => {
|
|
2526
|
+
const preview2 = highlight.parentElement;
|
|
2527
|
+
if (!(preview2 instanceof HTMLElement))
|
|
2528
|
+
return;
|
|
2529
|
+
preview2.scrollTop = textarea.scrollTop;
|
|
2530
|
+
preview2.scrollLeft = textarea.scrollLeft;
|
|
2531
|
+
};
|
|
2532
|
+
textarea.addEventListener("input", sync);
|
|
2533
|
+
textarea.addEventListener("scroll", syncScroll);
|
|
2534
|
+
sync();
|
|
2535
|
+
return sync;
|
|
2536
|
+
};
|
|
2537
|
+
const syncRouteEditor = wireJsonEditor("route-body", "route-highlight", "route-json-status", "route-editor", true);
|
|
2538
|
+
const syncRecordEditor = wireJsonEditor("editor-value", "editor-highlight", "editor-json-status", "record-editor", false);
|
|
2539
|
+
for (const button of document.querySelectorAll("[data-format]")) {
|
|
2540
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2541
|
+
continue;
|
|
2542
|
+
button.addEventListener("click", () => {
|
|
2543
|
+
const target = button.dataset.format;
|
|
2544
|
+
if (target === void 0)
|
|
2545
|
+
return;
|
|
2546
|
+
const textarea = document.getElementById(target);
|
|
2547
|
+
if (!(textarea instanceof HTMLTextAreaElement) || textarea.value.trim() === "")
|
|
2548
|
+
return;
|
|
2549
|
+
try {
|
|
2550
|
+
textarea.value = JSON.stringify(readJson2(textarea.value), null, 2) ?? "";
|
|
2551
|
+
textarea.dispatchEvent(new Event("input"));
|
|
2552
|
+
textarea.focus();
|
|
2553
|
+
} catch {
|
|
2554
|
+
showError(new Error("Fix the JSON before formatting it"));
|
|
2555
|
+
}
|
|
2556
|
+
});
|
|
2557
|
+
}
|
|
2364
2558
|
document.addEventListener("click", (event) => {
|
|
2365
2559
|
const target = event.target;
|
|
2366
2560
|
if (!(target instanceof Element))
|
|
@@ -2433,6 +2627,7 @@ function bootAdmin(config) {
|
|
|
2433
2627
|
} else {
|
|
2434
2628
|
editorValue.value = "{\n \n}";
|
|
2435
2629
|
}
|
|
2630
|
+
syncRecordEditor();
|
|
2436
2631
|
editor.showModal();
|
|
2437
2632
|
}
|
|
2438
2633
|
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -2462,10 +2657,10 @@ function bootAdmin(config) {
|
|
|
2462
2657
|
const id = state.editing;
|
|
2463
2658
|
if (!id)
|
|
2464
2659
|
return;
|
|
2465
|
-
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2660
|
+
confirmAction(editorDelete, "Deleting...", "Deleted", () => api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2466
2661
|
editor.close();
|
|
2467
2662
|
return renderState();
|
|
2468
|
-
}).catch(showError);
|
|
2663
|
+
}), "Confirm delete").catch(showError);
|
|
2469
2664
|
});
|
|
2470
2665
|
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2471
2666
|
if (!(button instanceof HTMLButtonElement))
|
|
@@ -2511,10 +2706,10 @@ function bootAdmin(config) {
|
|
|
2511
2706
|
}).then(() => renderFaults()).catch(showError);
|
|
2512
2707
|
});
|
|
2513
2708
|
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2514
|
-
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
|
|
2709
|
+
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults()), "Confirm clear").catch(showError);
|
|
2515
2710
|
});
|
|
2516
2711
|
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2517
|
-
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
|
|
2712
|
+
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal()), "Confirm clear").catch(showError);
|
|
2518
2713
|
});
|
|
2519
2714
|
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2520
2715
|
const target = event.target;
|
|
@@ -2528,9 +2723,15 @@ function bootAdmin(config) {
|
|
|
2528
2723
|
return;
|
|
2529
2724
|
state.route = route;
|
|
2530
2725
|
el("route-title", HTMLElement).textContent = route;
|
|
2726
|
+
el("route-send", HTMLButtonElement).disabled = false;
|
|
2531
2727
|
const method = route.split(" ")[0] ?? "";
|
|
2532
|
-
el("route-body", HTMLTextAreaElement)
|
|
2728
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
2729
|
+
routeBody.disabled = method === "GET" || method === "DELETE";
|
|
2730
|
+
el("route-editor", HTMLElement).hidden = routeBody.disabled;
|
|
2731
|
+
el("route-json-status", HTMLElement).parentElement?.toggleAttribute("hidden", routeBody.disabled);
|
|
2732
|
+
syncRouteEditor();
|
|
2533
2733
|
});
|
|
2734
|
+
el("route-search", HTMLInputElement).addEventListener("input", paintRoutes);
|
|
2534
2735
|
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2535
2736
|
event.preventDefault();
|
|
2536
2737
|
const send = async () => {
|
|
@@ -2552,7 +2753,8 @@ function bootAdmin(config) {
|
|
|
2552
2753
|
}
|
|
2553
2754
|
}
|
|
2554
2755
|
const data = await api(method, concrete, body);
|
|
2555
|
-
|
|
2756
|
+
const text = JSON.stringify(data, null, 2) ?? "null";
|
|
2757
|
+
el("route-result", HTMLElement).innerHTML = `<code>${highlightJsonText(text)}</code>`;
|
|
2556
2758
|
};
|
|
2557
2759
|
send().catch(showError);
|
|
2558
2760
|
});
|
|
@@ -2670,11 +2872,13 @@ function bootAdmin(config) {
|
|
|
2670
2872
|
views.append(section);
|
|
2671
2873
|
mountSqlExplorer(body, panelApi);
|
|
2672
2874
|
}
|
|
2875
|
+
show(document.getElementById(`view-${state.view}`) === null ? "overview" : state.view);
|
|
2673
2876
|
return refresh();
|
|
2674
2877
|
}).catch(showError);
|
|
2675
2878
|
}
|
|
2676
2879
|
var clientPrelude = [
|
|
2677
2880
|
isRecord2,
|
|
2881
|
+
highlightJsonText,
|
|
2678
2882
|
mountAdminBrand,
|
|
2679
2883
|
startAdminBrand,
|
|
2680
2884
|
faultPresetList,
|
|
@@ -2697,6 +2901,7 @@ ${entry}(${payload});
|
|
|
2697
2901
|
var defineMock = (runtime) => runtime;
|
|
2698
2902
|
var STANDARD_ADMIN_ROUTES = [
|
|
2699
2903
|
"GET /",
|
|
2904
|
+
"GET /health",
|
|
2700
2905
|
"POST /reset",
|
|
2701
2906
|
"GET /namespaces",
|
|
2702
2907
|
"GET /clock",
|
|
@@ -2776,11 +2981,11 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
2776
2981
|
}
|
|
2777
2982
|
});
|
|
2778
2983
|
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
2779
|
-
var renderAdminDocument = (service) => {
|
|
2984
|
+
var renderAdminDocument = (service, adminPrefix = ADMIN_PREFIX) => {
|
|
2780
2985
|
const name = escapeHtml(service);
|
|
2781
2986
|
const script = adminClientSource({
|
|
2782
2987
|
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
2783
|
-
adminPrefix
|
|
2988
|
+
adminPrefix,
|
|
2784
2989
|
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
2785
2990
|
brandsUrl: ADMIN_BRANDS_URL,
|
|
2786
2991
|
service
|
|
@@ -2797,75 +3002,99 @@ var renderAdminDocument = (service) => {
|
|
|
2797
3002
|
${CSS_RESET}
|
|
2798
3003
|
:root {
|
|
2799
3004
|
color-scheme: light;
|
|
2800
|
-
--bg: #
|
|
3005
|
+
--bg: #f7f8fb;
|
|
2801
3006
|
--bg-elev: #ffffff;
|
|
2802
|
-
--bg-muted: #
|
|
2803
|
-
--bg-inset: #
|
|
2804
|
-
--line: #
|
|
2805
|
-
--line-strong: #
|
|
2806
|
-
--ink: #
|
|
2807
|
-
--muted: #
|
|
2808
|
-
--faint: #
|
|
2809
|
-
--accent: #
|
|
3007
|
+
--bg-muted: #f1f3f7;
|
|
3008
|
+
--bg-inset: #f8f9fc;
|
|
3009
|
+
--line: #e4e7ec;
|
|
3010
|
+
--line-strong: #cfd4dc;
|
|
3011
|
+
--ink: #101828;
|
|
3012
|
+
--muted: #475467;
|
|
3013
|
+
--faint: #667085;
|
|
3014
|
+
--accent: #6941c6;
|
|
3015
|
+
--accent-hover: #5933b4;
|
|
2810
3016
|
--accent-ink: #ffffff;
|
|
2811
|
-
--accent-soft: #
|
|
2812
|
-
--ok: #
|
|
2813
|
-
--ok-soft: #
|
|
2814
|
-
--warn: #
|
|
2815
|
-
--warn-soft: #
|
|
2816
|
-
--err: #
|
|
2817
|
-
--err-soft: #
|
|
2818
|
-
--
|
|
3017
|
+
--accent-soft: #f4f0ff;
|
|
3018
|
+
--ok: #067647;
|
|
3019
|
+
--ok-soft: #ecfdf3;
|
|
3020
|
+
--warn: #b54708;
|
|
3021
|
+
--warn-soft: #fffaeb;
|
|
3022
|
+
--err: #b42318;
|
|
3023
|
+
--err-soft: #fef3f2;
|
|
3024
|
+
--code-key: #6941c6;
|
|
3025
|
+
--code-string: #067647;
|
|
3026
|
+
--code-number: #175cd3;
|
|
3027
|
+
--code-boolean: #c11574;
|
|
3028
|
+
--code-null: #667085;
|
|
3029
|
+
--shadow-xs: 0 1px 2px rgb(16 24 40 / 0.05);
|
|
3030
|
+
--shadow-sm: 0 1px 3px rgb(16 24 40 / 0.08), 0 1px 2px rgb(16 24 40 / 0.04);
|
|
3031
|
+
--shadow-lg: 0 18px 48px rgb(16 24 40 / 0.16), 0 4px 12px rgb(16 24 40 / 0.08);
|
|
2819
3032
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2820
3033
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
2821
|
-
--radius:
|
|
2822
|
-
--header:
|
|
2823
|
-
--nav:
|
|
3034
|
+
--radius: 12px;
|
|
3035
|
+
--header: 72px;
|
|
3036
|
+
--nav: 224px;
|
|
2824
3037
|
}
|
|
2825
3038
|
:root[data-theme="dark"] {
|
|
2826
3039
|
color-scheme: dark;
|
|
2827
|
-
--bg: #
|
|
2828
|
-
--bg-elev: #
|
|
2829
|
-
--bg-muted: #
|
|
2830
|
-
--bg-inset: #
|
|
2831
|
-
--line: #
|
|
2832
|
-
--line-strong: #
|
|
2833
|
-
--ink: #
|
|
2834
|
-
--muted: #
|
|
2835
|
-
--faint: #
|
|
2836
|
-
--accent: #
|
|
2837
|
-
--accent-
|
|
2838
|
-
--accent-
|
|
2839
|
-
--
|
|
2840
|
-
--ok
|
|
2841
|
-
--
|
|
2842
|
-
--warn
|
|
2843
|
-
--
|
|
2844
|
-
--err
|
|
2845
|
-
--
|
|
3040
|
+
--bg: #0c0e14;
|
|
3041
|
+
--bg-elev: #151821;
|
|
3042
|
+
--bg-muted: #20232e;
|
|
3043
|
+
--bg-inset: #11141c;
|
|
3044
|
+
--line: #2b303d;
|
|
3045
|
+
--line-strong: #414858;
|
|
3046
|
+
--ink: #f5f7fa;
|
|
3047
|
+
--muted: #b6bdc9;
|
|
3048
|
+
--faint: #8d96a7;
|
|
3049
|
+
--accent: #9e77ed;
|
|
3050
|
+
--accent-hover: #b692f6;
|
|
3051
|
+
--accent-ink: #ffffff;
|
|
3052
|
+
--accent-soft: #2b2142;
|
|
3053
|
+
--ok: #75e0a7;
|
|
3054
|
+
--ok-soft: #102a20;
|
|
3055
|
+
--warn: #fec84b;
|
|
3056
|
+
--warn-soft: #302611;
|
|
3057
|
+
--err: #fda29b;
|
|
3058
|
+
--err-soft: #321817;
|
|
3059
|
+
--code-key: #c3a6ff;
|
|
3060
|
+
--code-string: #75e0a7;
|
|
3061
|
+
--code-number: #84adff;
|
|
3062
|
+
--code-boolean: #f9a8d4;
|
|
3063
|
+
--code-null: #98a2b3;
|
|
3064
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3065
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3066
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2846
3067
|
}
|
|
2847
3068
|
@media (prefers-color-scheme: dark) {
|
|
2848
3069
|
:root:not([data-theme="light"]) {
|
|
2849
3070
|
color-scheme: dark;
|
|
2850
|
-
--bg: #
|
|
2851
|
-
--bg-elev: #
|
|
2852
|
-
--bg-muted: #
|
|
2853
|
-
--bg-inset: #
|
|
2854
|
-
--line: #
|
|
2855
|
-
--line-strong: #
|
|
2856
|
-
--ink: #
|
|
2857
|
-
--muted: #
|
|
2858
|
-
--faint: #
|
|
2859
|
-
--accent: #
|
|
2860
|
-
--accent-
|
|
2861
|
-
--accent-
|
|
2862
|
-
--
|
|
2863
|
-
--ok
|
|
2864
|
-
--
|
|
2865
|
-
--warn
|
|
2866
|
-
--
|
|
2867
|
-
--err
|
|
2868
|
-
--
|
|
3071
|
+
--bg: #0c0e14;
|
|
3072
|
+
--bg-elev: #151821;
|
|
3073
|
+
--bg-muted: #20232e;
|
|
3074
|
+
--bg-inset: #11141c;
|
|
3075
|
+
--line: #2b303d;
|
|
3076
|
+
--line-strong: #414858;
|
|
3077
|
+
--ink: #f5f7fa;
|
|
3078
|
+
--muted: #b6bdc9;
|
|
3079
|
+
--faint: #8d96a7;
|
|
3080
|
+
--accent: #9e77ed;
|
|
3081
|
+
--accent-hover: #b692f6;
|
|
3082
|
+
--accent-ink: #ffffff;
|
|
3083
|
+
--accent-soft: #2b2142;
|
|
3084
|
+
--ok: #75e0a7;
|
|
3085
|
+
--ok-soft: #102a20;
|
|
3086
|
+
--warn: #fec84b;
|
|
3087
|
+
--warn-soft: #302611;
|
|
3088
|
+
--err: #fda29b;
|
|
3089
|
+
--err-soft: #321817;
|
|
3090
|
+
--code-key: #c3a6ff;
|
|
3091
|
+
--code-string: #75e0a7;
|
|
3092
|
+
--code-number: #84adff;
|
|
3093
|
+
--code-boolean: #f9a8d4;
|
|
3094
|
+
--code-null: #98a2b3;
|
|
3095
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3096
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3097
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2869
3098
|
}
|
|
2870
3099
|
}
|
|
2871
3100
|
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
@@ -2883,13 +3112,17 @@ var renderAdminDocument = (service) => {
|
|
|
2883
3112
|
padding-right: env(safe-area-inset-right);
|
|
2884
3113
|
-webkit-text-size-adjust: 100%;
|
|
2885
3114
|
}
|
|
2886
|
-
|
|
3115
|
+
::selection { background: color-mix(in srgb, var(--accent) 28%, transparent); }
|
|
3116
|
+
:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px; }
|
|
3117
|
+
button, input, select, textarea { transition: border-color 140ms ease, box-shadow 140ms ease, transform 100ms ease; }
|
|
3118
|
+
button:disabled, input:disabled, select:disabled, textarea:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
2887
3119
|
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
2888
3120
|
header.top {
|
|
2889
|
-
position: sticky; top: 0; z-index:
|
|
3121
|
+
position: sticky; top: 0; z-index: 20;
|
|
2890
3122
|
display: flex; align-items: center; gap: 12px;
|
|
2891
|
-
min-height: var(--header); padding:
|
|
2892
|
-
background: var(--bg-elev);
|
|
3123
|
+
min-height: var(--header); padding: 12px 20px;
|
|
3124
|
+
background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
|
|
3125
|
+
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
2893
3126
|
border-bottom: 1px solid var(--line);
|
|
2894
3127
|
min-width: 0; max-width: 100%;
|
|
2895
3128
|
}
|
|
@@ -2897,12 +3130,21 @@ var renderAdminDocument = (service) => {
|
|
|
2897
3130
|
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
2898
3131
|
min-width: 0; max-width: 100%; margin-right: auto;
|
|
2899
3132
|
}
|
|
2900
|
-
.brand-
|
|
2901
|
-
.brand-
|
|
3133
|
+
.brand-lockup { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
3134
|
+
.brand-mark {
|
|
3135
|
+
width: 38px; height: 38px; display: grid; place-items: center; flex: none;
|
|
3136
|
+
border-radius: 11px; color: #fff;
|
|
3137
|
+
background: linear-gradient(145deg, #7f56d9 0%, #53389e 100%);
|
|
3138
|
+
box-shadow: 0 6px 16px rgb(105 65 198 / 0.24), inset 0 1px 0 rgb(255 255 255 / 0.25);
|
|
3139
|
+
}
|
|
3140
|
+
.brand-mark svg { width: 22px; height: 22px; }
|
|
3141
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; line-height: 1.25; }
|
|
3142
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
2902
3143
|
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
2903
3144
|
.vendor {
|
|
2904
3145
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
2905
3146
|
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
3147
|
+
padding: 5px 8px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-inset);
|
|
2906
3148
|
}
|
|
2907
3149
|
.vendor[hidden] { display: none; }
|
|
2908
3150
|
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
@@ -2914,12 +3156,17 @@ var renderAdminDocument = (service) => {
|
|
|
2914
3156
|
.vendor-sep { color: var(--faint); }
|
|
2915
3157
|
.vendor-link { color: inherit; text-decoration: none; }
|
|
2916
3158
|
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
2917
|
-
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items:
|
|
2918
|
-
|
|
3159
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
|
|
3160
|
+
.sync-status { align-self: center; display: flex; align-items: center; gap: 6px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3161
|
+
.sync-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
|
|
3162
|
+
.sync-status.busy::before { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); animation: pulse 1.2s ease-in-out infinite; }
|
|
3163
|
+
label.field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; font-weight: 600; color: var(--faint); }
|
|
2919
3164
|
label.field input, label.field select, .controls input[type="password"] {
|
|
2920
|
-
background-color: var(--bg-
|
|
2921
|
-
padding:
|
|
3165
|
+
background-color: var(--bg-elev); border: 1px solid var(--line-strong); border-radius: 9px;
|
|
3166
|
+
padding: 8px 10px; min-height: 38px; line-height: 1.2; box-shadow: var(--shadow-xs);
|
|
2922
3167
|
}
|
|
3168
|
+
label.field input:hover, label.field select:hover, textarea:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--line-strong)); }
|
|
3169
|
+
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; }
|
|
2923
3170
|
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
2924
3171
|
the text fields in the same form. Paint it like those fields. */
|
|
2925
3172
|
label.field select {
|
|
@@ -2936,12 +3183,20 @@ var renderAdminDocument = (service) => {
|
|
|
2936
3183
|
dialog .field > textarea { width: 100%; }
|
|
2937
3184
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
2938
3185
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
.btn
|
|
3186
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
|
3187
|
+
border-radius: 9px; min-height: 38px; padding: 0 13px; border: 1px solid var(--line-strong);
|
|
3188
|
+
background: var(--bg-elev); color: var(--ink); font-size: 13px; font-weight: 600; line-height: 1;
|
|
3189
|
+
box-shadow: var(--shadow-xs);
|
|
3190
|
+
}
|
|
3191
|
+
.icon-btn { width: 38px; padding: 0; }
|
|
3192
|
+
.icon-btn svg, .btn svg, .btn-primary svg { width: 16px; height: 16px; }
|
|
3193
|
+
.icon-btn:hover, .btn:hover { background: var(--bg-muted); border-color: var(--line-strong); }
|
|
3194
|
+
.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; }
|
|
3195
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
3196
|
+
.btn-quiet { background: transparent; border-color: transparent; box-shadow: none; }
|
|
3197
|
+
.btn-quiet:hover { background: var(--bg-muted); }
|
|
3198
|
+
.btn-danger { color: var(--err); border-color: color-mix(in srgb, var(--err) 25%, var(--line)); }
|
|
3199
|
+
.btn-danger:hover, .btn-danger.confirming { background: var(--err-soft); border-color: color-mix(in srgb, var(--err) 45%, var(--line)); }
|
|
2945
3200
|
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
2946
3201
|
so the press itself has to stay visible. */
|
|
2947
3202
|
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
@@ -2957,93 +3212,159 @@ var renderAdminDocument = (service) => {
|
|
|
2957
3212
|
flex: 1 0 auto; min-width: 0;
|
|
2958
3213
|
}
|
|
2959
3214
|
nav.side {
|
|
2960
|
-
|
|
3215
|
+
position: sticky; top: var(--header); align-self: start; height: calc(100vh - var(--header));
|
|
3216
|
+
border-right: 1px solid var(--line); padding: 18px 12px;
|
|
2961
3217
|
display: flex; flex-direction: column; gap: 4px;
|
|
2962
|
-
background: var(--bg-elev);
|
|
3218
|
+
background: var(--bg-elev); overflow-y: auto;
|
|
2963
3219
|
}
|
|
3220
|
+
.nav-label { padding: 0 10px 8px; color: var(--faint); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
|
|
2964
3221
|
nav.side button {
|
|
3222
|
+
position: relative; display: flex; align-items: center; gap: 10px; width: 100%;
|
|
2965
3223
|
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
2966
|
-
padding: 9px 10px; min-height:
|
|
2967
|
-
}
|
|
2968
|
-
nav.side button
|
|
2969
|
-
|
|
2970
|
-
.
|
|
2971
|
-
.
|
|
2972
|
-
.
|
|
3224
|
+
padding: 9px 10px; min-height: 42px; color: var(--muted); font-size: 13px; font-weight: 550;
|
|
3225
|
+
}
|
|
3226
|
+
nav.side button:hover { background: var(--bg-muted); color: var(--ink); }
|
|
3227
|
+
nav.side button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 650; }
|
|
3228
|
+
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); }
|
|
3229
|
+
.nav-icon { width: 18px; height: 18px; display: grid; place-items: center; flex: none; }
|
|
3230
|
+
.nav-icon svg { width: 18px; height: 18px; }
|
|
3231
|
+
main { padding: 32px clamp(20px, 4vw, 48px) 64px; max-width: 1320px; width: 100%; min-width: 0; }
|
|
3232
|
+
main[aria-busy="true"] .view { opacity: 0.62; }
|
|
3233
|
+
.view { animation: view-in 180ms ease-out; }
|
|
3234
|
+
.view[hidden] { display: none; }
|
|
3235
|
+
.view-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
|
|
3236
|
+
.view h2 { margin: 0 0 5px; font-size: clamp(24px, 3vw, 30px); line-height: 1.2; letter-spacing: -0.035em; }
|
|
3237
|
+
.view h3 { margin: 24px 0 10px; font-size: 15px; letter-spacing: -0.01em; }
|
|
3238
|
+
.lede { margin: 0; color: var(--muted); max-width: 72ch; font-size: 14px; }
|
|
3239
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 175px), 1fr)); gap: 14px; }
|
|
2973
3240
|
.card {
|
|
2974
3241
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
2975
|
-
padding:
|
|
3242
|
+
padding: 18px; box-shadow: var(--shadow-sm);
|
|
2976
3243
|
}
|
|
2977
|
-
.card
|
|
2978
|
-
.card
|
|
3244
|
+
.stat-card { position: relative; overflow: hidden; }
|
|
3245
|
+
.stat-card::after { content: ""; position: absolute; inset: 0 0 auto; height: 2px; background: linear-gradient(90deg, var(--accent), transparent 78%); opacity: 0.7; }
|
|
3246
|
+
.card .k { color: var(--faint); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
3247
|
+
.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; }
|
|
2979
3248
|
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
2980
|
-
.
|
|
3249
|
+
.section-card { background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-xs); overflow: hidden; }
|
|
3250
|
+
.section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
|
|
3251
|
+
.section-title h3 { margin: 0; }
|
|
3252
|
+
.split { display: grid; grid-template-columns: minmax(0, 250px) minmax(0, 1fr); gap: 18px; align-items: start; }
|
|
3253
|
+
#collections { padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); box-shadow: var(--shadow-xs); }
|
|
2981
3254
|
.collection {
|
|
2982
3255
|
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
2983
|
-
border-radius:
|
|
3256
|
+
border-radius: 9px; padding: 10px 11px; min-height: 48px; color: var(--ink); cursor: pointer;
|
|
2984
3257
|
}
|
|
2985
|
-
.collection
|
|
3258
|
+
.collection:hover { background: var(--bg-muted); }
|
|
3259
|
+
.collection[aria-current="true"] { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 18%, var(--line)); color: var(--accent); }
|
|
2986
3260
|
.collection small { display: block; color: var(--faint); }
|
|
2987
3261
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
2988
|
-
.chip {
|
|
2989
|
-
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
2990
|
-
border-radius: 999px; padding: 4px 8px;
|
|
2991
|
-
}
|
|
3262
|
+
.chip { font-family: var(--mono); font-size: 11px; background: var(--bg-muted); border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; }
|
|
2992
3263
|
.chip i { color: var(--faint); font-style: normal; }
|
|
2993
3264
|
.sql-layout {
|
|
2994
3265
|
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
2995
3266
|
}
|
|
2996
|
-
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
2997
|
-
.sql-tables button { text-align: left; }
|
|
3267
|
+
.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); }
|
|
3268
|
+
.sql-tables button { text-align: left; border: 0; border-radius: 8px; padding: 9px 10px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
3269
|
+
.sql-tables button:hover, .sql-tables button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); }
|
|
2998
3270
|
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
2999
|
-
table { width: 100%; border-
|
|
3000
|
-
|
|
3001
|
-
th {
|
|
3271
|
+
.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); }
|
|
3272
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
3273
|
+
th, td { text-align: left; padding: 11px 13px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
3274
|
+
th { color: var(--faint); background: var(--bg-inset); font-weight: 650; font-size: 11px; text-transform: uppercase; letter-spacing: 0.045em; }
|
|
3275
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
3002
3276
|
tr.clickable { cursor: pointer; }
|
|
3003
|
-
tr.clickable:hover { background: var(--
|
|
3277
|
+
tr.clickable:hover { background: var(--accent-soft); }
|
|
3004
3278
|
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
3005
3279
|
pre {
|
|
3006
3280
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
3007
|
-
background: var(--bg-inset); border-radius: 10px; padding:
|
|
3008
|
-
}
|
|
3009
|
-
.
|
|
3281
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 10px; padding: 14px; font-size: 12px; line-height: 1.65;
|
|
3282
|
+
}
|
|
3283
|
+
.json-key { color: var(--code-key); }
|
|
3284
|
+
.json-string { color: var(--code-string); }
|
|
3285
|
+
.json-number { color: var(--code-number); }
|
|
3286
|
+
.json-boolean { color: var(--code-boolean); }
|
|
3287
|
+
.json-null { color: var(--code-null); font-style: italic; }
|
|
3288
|
+
.json-inline { display: block; max-width: 44rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
3289
|
+
.json-view { position: relative; max-height: 26rem; overflow: auto; }
|
|
3290
|
+
.json-editor { position: relative; min-height: 190px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--bg-inset); overflow: hidden; }
|
|
3291
|
+
.json-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); }
|
|
3292
|
+
.json-editor.invalid { border-color: var(--err); }
|
|
3293
|
+
.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; }
|
|
3294
|
+
.json-editor pre { position: absolute; inset: 0; pointer-events: none; background: transparent; }
|
|
3295
|
+
.json-editor textarea { position: relative; z-index: 1; resize: vertical; background: transparent; color: transparent; caret-color: var(--ink); -webkit-text-fill-color: transparent; }
|
|
3296
|
+
.json-editor textarea::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); }
|
|
3297
|
+
.editor-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; }
|
|
3298
|
+
.json-status { display: inline-flex; align-items: center; gap: 6px; color: var(--ok); font-size: 11px; }
|
|
3299
|
+
.json-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
3300
|
+
.json-status.invalid { color: var(--err); }
|
|
3301
|
+
.format-json { border: 0; padding: 3px 5px; background: transparent; color: var(--accent); font-size: 11px; font-weight: 650; cursor: pointer; }
|
|
3302
|
+
.row-actions { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 14px 0; }
|
|
3010
3303
|
.banner {
|
|
3011
|
-
display: none;
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3304
|
+
display: none; position: sticky; top: calc(var(--header) + 12px); z-index: 15;
|
|
3305
|
+
align-items: center; gap: 10px; margin: 0 0 16px; padding: 11px 12px 11px 14px;
|
|
3306
|
+
border: 1px solid color-mix(in srgb, var(--err) 30%, var(--line)); border-radius: 10px;
|
|
3307
|
+
background: var(--err-soft); color: var(--err); box-shadow: var(--shadow-sm); font-size: 13px;
|
|
3308
|
+
}
|
|
3309
|
+
.banner.show { display: flex; }
|
|
3310
|
+
.banner span { flex: 1; }
|
|
3311
|
+
.banner button { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 7px; background: transparent; color: currentColor; cursor: pointer; }
|
|
3312
|
+
.banner button:hover { background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
3313
|
+
.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); }
|
|
3016
3314
|
dialog {
|
|
3017
|
-
border: 1px solid var(--line); border-radius:
|
|
3018
|
-
color: var(--ink); width: min(
|
|
3019
|
-
}
|
|
3020
|
-
dialog
|
|
3021
|
-
dialog
|
|
3315
|
+
border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--bg-elev);
|
|
3316
|
+
color: var(--ink); width: min(720px, calc(100% - 24px)); max-width: calc(100vw - 24px); box-shadow: var(--shadow-lg);
|
|
3317
|
+
}
|
|
3318
|
+
dialog[open] { animation: dialog-in 160ms ease-out; }
|
|
3319
|
+
dialog::backdrop { background: rgb(12 14 20 / 0.58); backdrop-filter: blur(3px); }
|
|
3320
|
+
dialog form, .dialog-body { padding: 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
3321
|
+
.dialog-heading { display: flex; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
|
3322
|
+
.dialog-heading strong { font-size: 18px; letter-spacing: -0.02em; }
|
|
3323
|
+
.dialog-heading span { color: var(--muted); font-size: 12px; }
|
|
3022
3324
|
textarea {
|
|
3023
3325
|
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
3024
3326
|
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
3025
3327
|
font-family: var(--mono); font-size: 12px;
|
|
3026
3328
|
}
|
|
3027
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
3028
|
-
.
|
|
3329
|
+
.dialog-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding-top: 4px; }
|
|
3330
|
+
.dialog-actions .btn-danger { margin-right: auto; }
|
|
3331
|
+
.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); }
|
|
3029
3332
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
3030
3333
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
3031
3334
|
.status-2 { color: var(--ok); }
|
|
3032
3335
|
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
3033
3336
|
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
3337
|
+
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
3338
|
+
.form-grid .wide { grid-column: span 2; }
|
|
3034
3339
|
.route {
|
|
3035
|
-
display: grid; grid-template-columns:
|
|
3036
|
-
padding:
|
|
3037
|
-
}
|
|
3038
|
-
.
|
|
3340
|
+
display: grid; grid-template-columns: 84px minmax(0, 1fr) auto; gap: 10px; align-items: center;
|
|
3341
|
+
padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--bg-elev);
|
|
3342
|
+
}
|
|
3343
|
+
.route:first-child { border-radius: var(--radius) var(--radius) 0 0; }
|
|
3344
|
+
.route:last-child { border-bottom: 0; border-radius: 0 0 var(--radius) var(--radius); }
|
|
3345
|
+
.route-tools { display: flex; align-items: flex-end; gap: 12px; margin-bottom: 12px; }
|
|
3346
|
+
.route-tools .field { flex: 1; }
|
|
3347
|
+
.route-tools input { width: 100%; }
|
|
3348
|
+
.route-count { padding-bottom: 10px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3349
|
+
#route-list { max-height: 34rem; border: 1px solid var(--line); border-radius: var(--radius); overflow: auto; box-shadow: var(--shadow-xs); }
|
|
3350
|
+
.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; }
|
|
3351
|
+
@keyframes view-in { from { transform: translateY(3px); } to { transform: translateY(0); } }
|
|
3352
|
+
@keyframes dialog-in { from { transform: translateY(8px) scale(0.99); } to { transform: translateY(0) scale(1); } }
|
|
3353
|
+
@keyframes pulse { 50% { opacity: 0.45; } }
|
|
3354
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
|
|
3355
|
+
@media (forced-colors: active) { .json-editor pre { display: none; } .json-editor textarea { color: CanvasText; -webkit-text-fill-color: CanvasText; } }
|
|
3039
3356
|
@media (max-width: 800px) {
|
|
3357
|
+
:root { --header: 0px; }
|
|
3040
3358
|
header.top {
|
|
3041
|
-
align-items: stretch; flex-direction: column;
|
|
3042
|
-
padding
|
|
3359
|
+
position: relative; align-items: stretch; flex-direction: column;
|
|
3360
|
+
padding: max(12px, env(safe-area-inset-top)) 14px 12px;
|
|
3043
3361
|
}
|
|
3044
3362
|
input, select, textarea { font-size: 16px; }
|
|
3045
3363
|
.brand, .vendor, .controls { width: 100%; }
|
|
3364
|
+
.brand { gap: 10px; }
|
|
3365
|
+
.vendor { order: 2; border-radius: 9px; }
|
|
3046
3366
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
3367
|
+
.sync-status { display: none; }
|
|
3047
3368
|
/* The layout fills the screen. A single-column grid stretches every auto
|
|
3048
3369
|
row, so the tab bar grew with the leftover height and the buttons floated
|
|
3049
3370
|
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
@@ -3053,14 +3374,23 @@ var renderAdminDocument = (service) => {
|
|
|
3053
3374
|
align-content: start;
|
|
3054
3375
|
}
|
|
3055
3376
|
nav.side {
|
|
3056
|
-
align-self: start; height: auto;
|
|
3377
|
+
position: sticky; top: 0; z-index: 12; align-self: start; height: auto;
|
|
3057
3378
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
3058
3379
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
3059
|
-
padding: 8px;
|
|
3380
|
+
padding: 8px; box-shadow: var(--shadow-xs);
|
|
3060
3381
|
}
|
|
3061
|
-
nav
|
|
3382
|
+
.nav-label { display: none; }
|
|
3383
|
+
nav.side button { width: auto; white-space: nowrap; flex: none; }
|
|
3384
|
+
nav.side button[aria-current="true"]::before { inset: auto 9px -8px; width: auto; height: 3px; border-radius: 3px 3px 0 0; }
|
|
3062
3385
|
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
3063
|
-
|
|
3386
|
+
.form-grid { grid-template-columns: minmax(0, 1fr); }
|
|
3387
|
+
.form-grid .wide { grid-column: auto; }
|
|
3388
|
+
main { padding: 22px 14px 48px; }
|
|
3389
|
+
.view-head { margin-bottom: 18px; }
|
|
3390
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
3391
|
+
.card { padding: 14px; }
|
|
3392
|
+
.card .v { font-size: 20px; }
|
|
3393
|
+
.table-shell { border: 0; background: transparent; box-shadow: none; overflow: visible; }
|
|
3064
3394
|
table, thead, tbody, tr, th, td { display: block; }
|
|
3065
3395
|
thead { display: none; }
|
|
3066
3396
|
tr {
|
|
@@ -3075,50 +3405,63 @@ var renderAdminDocument = (service) => {
|
|
|
3075
3405
|
.route { grid-template-columns: 72px 1fr; }
|
|
3076
3406
|
.route button { grid-column: 1 / -1; }
|
|
3077
3407
|
}
|
|
3408
|
+
@media (max-width: 460px) {
|
|
3409
|
+
.controls label.field { min-width: calc(50% - 4px); }
|
|
3410
|
+
.controls .icon-btn, .controls .btn { flex: 1; }
|
|
3411
|
+
.grid { grid-template-columns: minmax(0, 1fr); }
|
|
3412
|
+
.dialog-actions { flex-wrap: wrap; }
|
|
3413
|
+
.dialog-actions button { flex: 1; }
|
|
3414
|
+
.dialog-actions .btn-danger { flex-basis: 100%; margin-right: 0; }
|
|
3415
|
+
}
|
|
3078
3416
|
</style>
|
|
3079
3417
|
</head>
|
|
3080
3418
|
<body>
|
|
3081
3419
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
3082
3420
|
<header class="top">
|
|
3083
3421
|
<div class="brand">
|
|
3084
|
-
<div class="brand-
|
|
3085
|
-
<
|
|
3086
|
-
|
|
3422
|
+
<div class="brand-lockup">
|
|
3423
|
+
<div class="brand-mark" aria-hidden="true">
|
|
3424
|
+
<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>
|
|
3425
|
+
</div>
|
|
3426
|
+
<div class="brand-id">
|
|
3427
|
+
<strong>${name}</strong>
|
|
3428
|
+
<span>Mock admin</span>
|
|
3429
|
+
</div>
|
|
3087
3430
|
</div>
|
|
3088
3431
|
<div class="vendor" id="vendor" hidden></div>
|
|
3089
3432
|
</div>
|
|
3090
3433
|
<div class="controls">
|
|
3434
|
+
<span class="sync-status" id="sync-status" role="status" aria-live="polite">Ready</span>
|
|
3091
3435
|
<label class="field">Namespace
|
|
3092
3436
|
<select id="namespace" aria-label="Namespace"></select>
|
|
3093
3437
|
</label>
|
|
3094
3438
|
<label class="field">Admin key
|
|
3095
3439
|
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
3096
3440
|
</label>
|
|
3097
|
-
<button class="icon-btn" id="theme" type="button" aria-label="
|
|
3098
|
-
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
3441
|
+
<button class="icon-btn" id="theme" type="button" aria-label="Use dark theme" title="Use dark theme"></button>
|
|
3442
|
+
<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>
|
|
3099
3443
|
</div>
|
|
3100
3444
|
</header>
|
|
3101
3445
|
<div class="layout">
|
|
3102
3446
|
<nav class="side" id="nav" aria-label="Admin sections">
|
|
3103
|
-
<
|
|
3104
|
-
<button type="button" data-view="
|
|
3105
|
-
<button type="button" data-view="
|
|
3106
|
-
<button type="button" data-view="
|
|
3107
|
-
<button type="button" data-view="
|
|
3108
|
-
<button type="button" data-view="
|
|
3447
|
+
<span class="nav-label">Workspace</span>
|
|
3448
|
+
<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>
|
|
3449
|
+
<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>
|
|
3450
|
+
<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>
|
|
3451
|
+
<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>
|
|
3452
|
+
<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>
|
|
3453
|
+
<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>
|
|
3109
3454
|
</nav>
|
|
3110
3455
|
<main id="views">
|
|
3111
|
-
<
|
|
3456
|
+
<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>
|
|
3112
3457
|
<section class="view" id="view-overview">
|
|
3113
|
-
<h2>Overview</h2>
|
|
3114
|
-
<p class="lede">Health, clock, and the collections this namespace is holding.</p>
|
|
3458
|
+
<div class="view-head"><div><h2>Overview</h2><p class="lede">Health, clock, and the collections this namespace is holding.</p></div></div>
|
|
3115
3459
|
<div class="grid" id="overview-cards"></div>
|
|
3116
3460
|
<h3>Recent requests</h3>
|
|
3117
3461
|
<div id="overview-requests"></div>
|
|
3118
3462
|
</section>
|
|
3119
3463
|
<section class="view" id="view-state" hidden>
|
|
3120
|
-
<h2>State</h2>
|
|
3121
|
-
<p class="lede">Every collection in this namespace. Declared fields stay visible before any row exists; stored rows add whatever else they contain.</p>
|
|
3464
|
+
<div class="view-head"><div><h2>State</h2><p class="lede">Inspect and edit every collection in this namespace.</p></div></div>
|
|
3122
3465
|
<div class="split">
|
|
3123
3466
|
<div id="collections"></div>
|
|
3124
3467
|
<div>
|
|
@@ -3131,8 +3474,7 @@ var renderAdminDocument = (service) => {
|
|
|
3131
3474
|
</div>
|
|
3132
3475
|
</section>
|
|
3133
3476
|
<section class="view" id="view-clock" hidden>
|
|
3134
|
-
<h2>Clock</h2>
|
|
3135
|
-
<p class="lede">The clock every timestamp in this mock reads.</p>
|
|
3477
|
+
<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>
|
|
3136
3478
|
<div class="card" id="clock-readout"></div>
|
|
3137
3479
|
<div class="row-actions">
|
|
3138
3480
|
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
@@ -3151,21 +3493,22 @@ var renderAdminDocument = (service) => {
|
|
|
3151
3493
|
</form>
|
|
3152
3494
|
</section>
|
|
3153
3495
|
<section class="view" id="view-faults" hidden>
|
|
3154
|
-
<h2>Faults</h2>
|
|
3155
|
-
<p class="lede">Inject a response, a delay, or a named preset. Rules apply to this namespace.</p>
|
|
3496
|
+
<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>
|
|
3156
3497
|
<form id="fault-form" class="card stack">
|
|
3157
|
-
<
|
|
3158
|
-
<
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
<
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
<
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
<
|
|
3168
|
-
|
|
3498
|
+
<div class="form-grid">
|
|
3499
|
+
<label class="field wide">Preset
|
|
3500
|
+
<select id="fault-preset"><option value="">Custom rule</option></select>
|
|
3501
|
+
</label>
|
|
3502
|
+
<label class="field">Status
|
|
3503
|
+
<input id="fault-status" inputmode="numeric" placeholder="503">
|
|
3504
|
+
</label>
|
|
3505
|
+
<label class="field wide">Path prefix
|
|
3506
|
+
<input id="fault-path" placeholder="/v1">
|
|
3507
|
+
</label>
|
|
3508
|
+
<label class="field">Method
|
|
3509
|
+
<input id="fault-method" placeholder="GET">
|
|
3510
|
+
</label>
|
|
3511
|
+
</div>
|
|
3169
3512
|
<div class="dialog-actions">
|
|
3170
3513
|
<button class="btn-primary" type="submit">Add fault</button>
|
|
3171
3514
|
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
@@ -3174,26 +3517,31 @@ var renderAdminDocument = (service) => {
|
|
|
3174
3517
|
<div id="fault-list"></div>
|
|
3175
3518
|
</section>
|
|
3176
3519
|
<section class="view" id="view-journal" hidden>
|
|
3177
|
-
<h2>Journal</h2>
|
|
3178
|
-
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
3179
|
-
<div class="row-actions">
|
|
3180
|
-
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
3181
|
-
</div>
|
|
3520
|
+
<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>
|
|
3182
3521
|
<div id="journal"></div>
|
|
3183
3522
|
</section>
|
|
3184
3523
|
<section class="view" id="view-routes" hidden>
|
|
3185
|
-
<h2>Routes</h2>
|
|
3186
|
-
<
|
|
3524
|
+
<div class="view-head"><div><h2>Routes</h2><p class="lede">Explore shared controls and service-specific admin endpoints.</p></div></div>
|
|
3525
|
+
<div class="route-tools">
|
|
3526
|
+
<label class="field">Filter routes
|
|
3527
|
+
<input id="route-search" type="search" placeholder="Method, path, or extension" autocomplete="off">
|
|
3528
|
+
</label>
|
|
3529
|
+
<span class="route-count" id="route-count"></span>
|
|
3530
|
+
</div>
|
|
3187
3531
|
<div id="route-list"></div>
|
|
3188
|
-
<form id="route-form" class="card stack">
|
|
3532
|
+
<form id="route-form" class="card stack" style="margin-top: 18px">
|
|
3189
3533
|
<strong id="route-title">Select a route</strong>
|
|
3190
3534
|
<label class="field">JSON body
|
|
3191
|
-
<
|
|
3535
|
+
<div class="json-editor" id="route-editor" hidden>
|
|
3536
|
+
<pre aria-hidden="true"><code id="route-highlight"></code></pre>
|
|
3537
|
+
<textarea id="route-body" placeholder="{ }" spellcheck="false" aria-describedby="route-json-status" disabled></textarea>
|
|
3538
|
+
</div>
|
|
3539
|
+
<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>
|
|
3192
3540
|
</label>
|
|
3193
3541
|
<div class="dialog-actions">
|
|
3194
|
-
<button class="btn-primary" type="submit">Send</button>
|
|
3542
|
+
<button class="btn-primary" id="route-send" type="submit" disabled>Send request</button>
|
|
3195
3543
|
</div>
|
|
3196
|
-
<pre id="route-result">Response will show here.</pre>
|
|
3544
|
+
<pre class="json-view" id="route-result"><code>Response will show here.</code></pre>
|
|
3197
3545
|
</form>
|
|
3198
3546
|
</section>
|
|
3199
3547
|
</main>
|
|
@@ -3201,12 +3549,16 @@ var renderAdminDocument = (service) => {
|
|
|
3201
3549
|
</div>
|
|
3202
3550
|
<dialog id="editor">
|
|
3203
3551
|
<form method="dialog" id="editor-form">
|
|
3204
|
-
<strong id="editor-title">Record</strong>
|
|
3552
|
+
<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>
|
|
3205
3553
|
<label class="field">Id
|
|
3206
3554
|
<input id="editor-id" required>
|
|
3207
3555
|
</label>
|
|
3208
3556
|
<label class="field">JSON value
|
|
3209
|
-
<
|
|
3557
|
+
<div class="json-editor" id="record-editor">
|
|
3558
|
+
<pre aria-hidden="true"><code id="editor-highlight"></code></pre>
|
|
3559
|
+
<textarea id="editor-value" spellcheck="false" aria-describedby="editor-json-status"></textarea>
|
|
3560
|
+
</div>
|
|
3561
|
+
<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>
|
|
3210
3562
|
</label>
|
|
3211
3563
|
<div class="dialog-actions">
|
|
3212
3564
|
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
@@ -3248,8 +3600,8 @@ var describeExtension = (extension) => {
|
|
|
3248
3600
|
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
3249
3601
|
};
|
|
3250
3602
|
};
|
|
3251
|
-
var adminUiRoutes = (service, ui) => {
|
|
3252
|
-
const shell = () => renderAdminDocument(service);
|
|
3603
|
+
var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
3604
|
+
const shell = () => renderAdminDocument(service, adminPrefix);
|
|
3253
3605
|
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
3254
3606
|
return {
|
|
3255
3607
|
"GET /ui": () => htmlResponse(document3()),
|
|
@@ -3866,14 +4218,17 @@ var stateAdminRoutes = (routes) => {
|
|
|
3866
4218
|
};
|
|
3867
4219
|
|
|
3868
4220
|
// ../core/dist/version.js
|
|
3869
|
-
var PACKAGE_VERSION = true ? "
|
|
4221
|
+
var PACKAGE_VERSION = true ? "2.0.0" : UNRELEASED_VERSION;
|
|
3870
4222
|
|
|
3871
4223
|
// ../core/dist/signing.js
|
|
3872
4224
|
var encoder = new TextEncoder();
|
|
3873
4225
|
var fromBase64 = (value) => Uint8Array.from(atob(value), (char) => char.charCodeAt(0));
|
|
3874
4226
|
|
|
3875
4227
|
// ../core/dist/webhooks.js
|
|
3876
|
-
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4228
|
+
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4229
|
+
status,
|
|
4230
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4231
|
+
});
|
|
3877
4232
|
var adminError3 = (status, message) => json4(status, { error: { type: "mockingbird_admin", message } });
|
|
3878
4233
|
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3879
4234
|
var parseEndpoint = (value) => {
|
|
@@ -3981,7 +4336,6 @@ var AT_HEADER = "x-mockingbird-at";
|
|
|
3981
4336
|
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
3982
4337
|
var DEFAULT_NAMESPACE = "default";
|
|
3983
4338
|
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
3984
|
-
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
3985
4339
|
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
3986
4340
|
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
3987
4341
|
var effects = /* @__PURE__ */ new WeakMap();
|
|
@@ -4021,6 +4375,22 @@ var operationMatcher = (document3) => {
|
|
|
4021
4375
|
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
4022
4376
|
};
|
|
4023
4377
|
var createRuntime = (options) => {
|
|
4378
|
+
const adminPrefix = resolveAdminPrefix(options.adminPrefix);
|
|
4379
|
+
const internalPaths = Object.entries(options.document?.paths ?? {}).filter(([, item]) => item["x-mockingbird-internal"] === true).map(([path]) => path);
|
|
4380
|
+
for (const path of internalPaths) {
|
|
4381
|
+
if (!path.startsWith(`${ADMIN_PREFIX}/blobs/`))
|
|
4382
|
+
throw new Error(`internal transport ${path} must live under ${ADMIN_PREFIX}/blobs/`);
|
|
4383
|
+
}
|
|
4384
|
+
assertAdminPrefixAvailable(adminPrefix, Object.keys(options.document?.paths ?? {}).filter((path) => !internalPaths.includes(path)));
|
|
4385
|
+
const internalMatch = (path) => {
|
|
4386
|
+
for (const pattern of internalPaths) {
|
|
4387
|
+
const actual = path.split("/");
|
|
4388
|
+
const expected = pattern.split("/");
|
|
4389
|
+
if (actual.length === expected.length && expected.every((part, i) => part.startsWith("{") || part === actual[i]))
|
|
4390
|
+
return pattern;
|
|
4391
|
+
}
|
|
4392
|
+
return void 0;
|
|
4393
|
+
};
|
|
4024
4394
|
assertAdminUi(options.adminUi);
|
|
4025
4395
|
const sqlite = bootSqlite(options.sqlite);
|
|
4026
4396
|
const clock = options.clock ?? createClock();
|
|
@@ -4066,6 +4436,7 @@ var createRuntime = (options) => {
|
|
|
4066
4436
|
const created = options.create({
|
|
4067
4437
|
namespace: storageNamespace(key),
|
|
4068
4438
|
publicNamespace,
|
|
4439
|
+
adminPrefix,
|
|
4069
4440
|
sqlite,
|
|
4070
4441
|
clock: namespaceClock(publicNamespace),
|
|
4071
4442
|
rng: createdContextRng
|
|
@@ -4298,10 +4669,10 @@ var createRuntime = (options) => {
|
|
|
4298
4669
|
namespaceClock,
|
|
4299
4670
|
namespaceOf(request) {
|
|
4300
4671
|
const url = new URL(request.url);
|
|
4301
|
-
const prefixed =
|
|
4672
|
+
const prefixed = matchNamespacePath(url.pathname, adminPrefix);
|
|
4302
4673
|
const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
|
|
4303
4674
|
const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
|
|
4304
|
-
if (path
|
|
4675
|
+
if (isAdminPath(path, adminPrefix))
|
|
4305
4676
|
return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
|
|
4306
4677
|
if (selected !== void 0)
|
|
4307
4678
|
return selected;
|
|
@@ -4367,7 +4738,7 @@ var createRuntime = (options) => {
|
|
|
4367
4738
|
state: (namespace = DEFAULT_NAMESPACE) => inspectState(stateScope(namespace)),
|
|
4368
4739
|
fetch: async (incoming) => {
|
|
4369
4740
|
let request = incoming;
|
|
4370
|
-
const prefixed =
|
|
4741
|
+
const prefixed = matchNamespacePath(new URL(request.url).pathname, adminPrefix);
|
|
4371
4742
|
if (prefixed) {
|
|
4372
4743
|
const url2 = new URL(request.url);
|
|
4373
4744
|
url2.pathname = prefixed[2] ?? "/";
|
|
@@ -4383,7 +4754,15 @@ var createRuntime = (options) => {
|
|
|
4383
4754
|
signal: request.signal
|
|
4384
4755
|
});
|
|
4385
4756
|
}
|
|
4386
|
-
|
|
4757
|
+
const internalUrl = new URL(request.url);
|
|
4758
|
+
const internalPath = isAdminPath(internalUrl.pathname, adminPrefix) ? `${ADMIN_PREFIX}${internalUrl.pathname.slice(adminPrefix.length)}` : "";
|
|
4759
|
+
const internal = internalMatch(internalPath);
|
|
4760
|
+
const obsoleteInternalPath = adminPrefix !== ADMIN_PREFIX && internalMatch(internalUrl.pathname) && !internal;
|
|
4761
|
+
if (internal) {
|
|
4762
|
+
internalUrl.pathname = internalPath;
|
|
4763
|
+
request = new Request(internalUrl, request);
|
|
4764
|
+
}
|
|
4765
|
+
let namespace = internal ? internalUrl.searchParams.get("namespace") ?? control.namespaceOf(request) : control.namespaceOf(request);
|
|
4387
4766
|
if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
4388
4767
|
const credential = options.credential(request);
|
|
4389
4768
|
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
@@ -4403,7 +4782,9 @@ var createRuntime = (options) => {
|
|
|
4403
4782
|
return copy;
|
|
4404
4783
|
}
|
|
4405
4784
|
};
|
|
4406
|
-
|
|
4785
|
+
if (obsoleteInternalPath)
|
|
4786
|
+
return stamp(adminFail(404, "internal transport is only available under adminPrefix"));
|
|
4787
|
+
const handled = internal ? void 0 : await control.handle(request);
|
|
4407
4788
|
if (handled)
|
|
4408
4789
|
return stamp(handled);
|
|
4409
4790
|
const started = monotonicNow();
|
|
@@ -4444,7 +4825,7 @@ var createRuntime = (options) => {
|
|
|
4444
4825
|
type: "mockingbird_admin",
|
|
4445
4826
|
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
4446
4827
|
}
|
|
4447
|
-
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
4828
|
+
}), { status: 400, headers: { "content-type": "application/json; charset=utf-8" } }));
|
|
4448
4829
|
}
|
|
4449
4830
|
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
4450
4831
|
log(400);
|
|
@@ -4561,15 +4942,16 @@ var createRuntime = (options) => {
|
|
|
4561
4942
|
...options.admin?.(runtime) ?? {},
|
|
4562
4943
|
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
4563
4944
|
// maps `presets`, and a service route must not replace that list with a record.
|
|
4564
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
4945
|
+
...options.presets ? presetRoutes(options.presets, runtime, adminPrefix) : {},
|
|
4565
4946
|
...stateAdminRoutes({
|
|
4566
4947
|
open: stateScope,
|
|
4567
4948
|
afterWrite: (namespace) => {
|
|
4568
4949
|
checkpoint(namespace, "main");
|
|
4569
4950
|
}
|
|
4570
4951
|
}),
|
|
4571
|
-
...adminUiRoutes(options.name, options.adminUi)
|
|
4952
|
+
...adminUiRoutes(options.name, options.adminUi, adminPrefix)
|
|
4572
4953
|
},
|
|
4954
|
+
adminPrefix,
|
|
4573
4955
|
adminKey: options.adminKey
|
|
4574
4956
|
});
|
|
4575
4957
|
return defineMock(runtime);
|
|
@@ -4583,7 +4965,10 @@ var mutableResponse = (response) => {
|
|
|
4583
4965
|
return new Response(response.body, response);
|
|
4584
4966
|
}
|
|
4585
4967
|
};
|
|
4586
|
-
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
4968
|
+
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
4969
|
+
status,
|
|
4970
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4971
|
+
});
|
|
4587
4972
|
var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
|
|
4588
4973
|
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4589
4974
|
var credentialRoutes = (registry) => ({
|
|
@@ -4635,7 +5020,7 @@ var credentialRoutes = (registry) => ({
|
|
|
4635
5020
|
return adminJson(200, { status: "ok" });
|
|
4636
5021
|
}
|
|
4637
5022
|
});
|
|
4638
|
-
var presetRoutes = (presets, runtime) => ({
|
|
5023
|
+
var presetRoutes = (presets, runtime, adminPrefix) => ({
|
|
4639
5024
|
"GET /faults/presets": () => {
|
|
4640
5025
|
const body = {
|
|
4641
5026
|
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
@@ -4645,7 +5030,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
4645
5030
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
4646
5031
|
const name = params.name;
|
|
4647
5032
|
if (!presets[name])
|
|
4648
|
-
return adminFail(404, `no fault preset ${name}; GET /
|
|
5033
|
+
return adminFail(404, `no fault preset ${name}; GET ${adminPrefix}/faults/presets`);
|
|
4649
5034
|
const overrides = isObject(body) ? body : {};
|
|
4650
5035
|
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
4651
5036
|
}
|
|
@@ -5258,6 +5643,7 @@ var createRuntime2 = (options = {}) => {
|
|
|
5258
5643
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
5259
5644
|
...options.clock ? { clock: options.clock } : {},
|
|
5260
5645
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
5646
|
+
...options.adminPrefix !== void 0 ? { adminPrefix: options.adminPrefix } : {},
|
|
5261
5647
|
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
5262
5648
|
...options.onLog ? { onLog: options.onLog } : {},
|
|
5263
5649
|
credential: (request) => tokens.get(request),
|
|
@@ -5275,7 +5661,7 @@ var createRuntime2 = (options = {}) => {
|
|
|
5275
5661
|
const inner = runtime.fetch;
|
|
5276
5662
|
const fetch2 = async (request) => {
|
|
5277
5663
|
const path = new URL(request.url).pathname;
|
|
5278
|
-
if (!request.headers.has(NAMESPACE_HEADER) && !path.
|
|
5664
|
+
if (!request.headers.has(NAMESPACE_HEADER) && !(path === (options.adminPrefix ?? "/__admin") || path.startsWith(`${options.adminPrefix ?? "/__admin"}/`))) {
|
|
5279
5665
|
const token = await requestToken(request, path) ?? bearerToken(request);
|
|
5280
5666
|
if (token !== void 0) tokens.set(request, token);
|
|
5281
5667
|
}
|
|
@@ -5607,6 +5993,7 @@ var malformedBody = () => ({
|
|
|
5607
5993
|
|
|
5608
5994
|
export {
|
|
5609
5995
|
createClock,
|
|
5996
|
+
resolveAdminPrefix,
|
|
5610
5997
|
parseDuration,
|
|
5611
5998
|
decodePostHogBody,
|
|
5612
5999
|
tokenFromBody,
|
|
@@ -5625,4 +6012,4 @@ export {
|
|
|
5625
6012
|
scrubProperties,
|
|
5626
6013
|
PostHogAPI
|
|
5627
6014
|
};
|
|
5628
|
-
//# sourceMappingURL=chunk-
|
|
6015
|
+
//# sourceMappingURL=chunk-NRFKITRM.js.map
|