@crvouga/mockingbird-service-odx 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 +25 -0
- package/dist/{chunk-BSGAYJGM.js → chunk-K6M4YJNF.js} +622 -235
- package/dist/chunk-K6M4YJNF.js.map +7 -0
- package/dist/{chunk-WAJT2FWY.js → chunk-YKFRTJP3.js} +44 -24
- package/dist/chunk-YKFRTJP3.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 +953 -0
- package/package.json +29 -4
- package/dist/chunk-BSGAYJGM.js.map +0 -7
- package/dist/chunk-WAJT2FWY.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, message2) => json(status, { error: { type: "mockingbird_admin", message: message2 } });
|
|
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
|
};
|
|
@@ -1525,6 +1564,34 @@ dialog:not([open]) {
|
|
|
1525
1564
|
function isRecord2(value) {
|
|
1526
1565
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1527
1566
|
}
|
|
1567
|
+
function highlightJsonText(text2) {
|
|
1568
|
+
const escapeToken = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
1569
|
+
switch (char) {
|
|
1570
|
+
case "&":
|
|
1571
|
+
return "&";
|
|
1572
|
+
case "<":
|
|
1573
|
+
return "<";
|
|
1574
|
+
case ">":
|
|
1575
|
+
return ">";
|
|
1576
|
+
case '"':
|
|
1577
|
+
return """;
|
|
1578
|
+
default:
|
|
1579
|
+
return "'";
|
|
1580
|
+
}
|
|
1581
|
+
});
|
|
1582
|
+
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;
|
|
1583
|
+
let cursor = 0;
|
|
1584
|
+
let html = "";
|
|
1585
|
+
for (const match of text2.matchAll(token)) {
|
|
1586
|
+
const index = match.index ?? 0;
|
|
1587
|
+
const value = match[0];
|
|
1588
|
+
html += escapeToken(text2.slice(cursor, index));
|
|
1589
|
+
const kind = value.startsWith('"') ? value.trimEnd().endsWith(":") ? "key" : "string" : value === "true" || value === "false" ? "boolean" : value === "null" ? "null" : "number";
|
|
1590
|
+
html += `<span class="json-${kind}">${escapeToken(value)}</span>`;
|
|
1591
|
+
cursor = index + value.length;
|
|
1592
|
+
}
|
|
1593
|
+
return html + escapeToken(text2.slice(cursor));
|
|
1594
|
+
}
|
|
1528
1595
|
function mountAdminBrand(doc, host, service, catalog) {
|
|
1529
1596
|
const clip = (value, max) => {
|
|
1530
1597
|
if (typeof value !== "string")
|
|
@@ -1886,14 +1953,16 @@ function bootAdmin(config) {
|
|
|
1886
1953
|
const state = {
|
|
1887
1954
|
namespace: "default",
|
|
1888
1955
|
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
1889
|
-
view: "overview",
|
|
1956
|
+
view: ["overview", "state", "clock", "faults", "journal", "routes"].includes(location.hash.slice(1)) ? location.hash.slice(1) : "overview",
|
|
1890
1957
|
collection: "",
|
|
1891
1958
|
shape: null,
|
|
1892
1959
|
route: null,
|
|
1960
|
+
routes: [],
|
|
1893
1961
|
editing: null
|
|
1894
1962
|
};
|
|
1895
1963
|
const standard = new Set(config.standardRoutes);
|
|
1896
1964
|
const banner = el("banner", HTMLElement);
|
|
1965
|
+
const bannerMessage = el("banner-message", HTMLElement);
|
|
1897
1966
|
const keyInput = el("admin-key", HTMLInputElement);
|
|
1898
1967
|
keyInput.value = state.key;
|
|
1899
1968
|
const params = new URLSearchParams(location.search);
|
|
@@ -1904,18 +1973,21 @@ function bootAdmin(config) {
|
|
|
1904
1973
|
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
1905
1974
|
params.delete("key");
|
|
1906
1975
|
const next = params.toString();
|
|
1907
|
-
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
1976
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : "") + location.hash);
|
|
1908
1977
|
}
|
|
1909
1978
|
const queryNamespace = params.get("namespace");
|
|
1910
1979
|
if (queryNamespace)
|
|
1911
1980
|
state.namespace = queryNamespace;
|
|
1912
1981
|
const showError = (error) => {
|
|
1913
|
-
|
|
1982
|
+
bannerMessage.textContent = error instanceof Error ? error.message : String(error);
|
|
1914
1983
|
banner.classList.add("show");
|
|
1984
|
+
banner.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
1915
1985
|
};
|
|
1916
1986
|
const clearError = () => {
|
|
1917
1987
|
banner.classList.remove("show");
|
|
1988
|
+
bannerMessage.textContent = "";
|
|
1918
1989
|
};
|
|
1990
|
+
el("banner-dismiss", HTMLButtonElement).addEventListener("click", clearError);
|
|
1919
1991
|
const api = async (method, path, body) => {
|
|
1920
1992
|
const headers = { accept: "application/json" };
|
|
1921
1993
|
if (state.key !== "")
|
|
@@ -1959,16 +2031,21 @@ function bootAdmin(config) {
|
|
|
1959
2031
|
namespace: () => state.namespace
|
|
1960
2032
|
};
|
|
1961
2033
|
const isPanelScript = (value) => typeof value === "function";
|
|
2034
|
+
const themeButton = el("theme", HTMLButtonElement);
|
|
1962
2035
|
const setTheme = (theme) => {
|
|
1963
2036
|
if (theme)
|
|
1964
2037
|
document.documentElement.dataset.theme = theme;
|
|
1965
2038
|
else
|
|
1966
2039
|
delete document.documentElement.dataset.theme;
|
|
2040
|
+
const dark = theme ? theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2041
|
+
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>';
|
|
2042
|
+
const label = dark ? "Use light theme" : "Use dark theme";
|
|
2043
|
+
themeButton.setAttribute("aria-label", label);
|
|
2044
|
+
themeButton.title = label;
|
|
1967
2045
|
};
|
|
1968
2046
|
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
2047
|
+
setTheme(storedTheme === "dark" || storedTheme === "light" ? storedTheme : null);
|
|
2048
|
+
themeButton.addEventListener("click", () => {
|
|
1972
2049
|
const current = document.documentElement.dataset.theme;
|
|
1973
2050
|
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
1974
2051
|
const next = dark ? "light" : "dark";
|
|
@@ -1987,6 +2064,7 @@ function bootAdmin(config) {
|
|
|
1987
2064
|
continue;
|
|
1988
2065
|
section.hidden = section.id !== `view-${view}`;
|
|
1989
2066
|
}
|
|
2067
|
+
history.replaceState(null, "", `${location.pathname}${location.search}#${encodeURIComponent(view)}`);
|
|
1990
2068
|
};
|
|
1991
2069
|
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
1992
2070
|
const target = event.target;
|
|
@@ -2001,7 +2079,7 @@ function bootAdmin(config) {
|
|
|
2001
2079
|
show(view);
|
|
2002
2080
|
refresh().catch(showError);
|
|
2003
2081
|
});
|
|
2004
|
-
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>`;
|
|
2082
|
+
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>`;
|
|
2005
2083
|
const table = (columns, rows) => {
|
|
2006
2084
|
if (rows.length === 0)
|
|
2007
2085
|
return '<p class="empty">Nothing here yet.</p>';
|
|
@@ -2011,16 +2089,22 @@ function bootAdmin(config) {
|
|
|
2011
2089
|
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
2012
2090
|
return `<tr${attrs}>${cells}</tr>`;
|
|
2013
2091
|
}).join("");
|
|
2014
|
-
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
2092
|
+
return `<div class="table-shell"><table><thead>${head}</thead><tbody>${body}</tbody></table></div>`;
|
|
2015
2093
|
};
|
|
2016
2094
|
const preview = (value) => {
|
|
2017
2095
|
const text2 = JSON.stringify(value) ?? "null";
|
|
2018
|
-
|
|
2096
|
+
const clipped = text2.length > 180 ? `${text2.slice(0, 180)}\u2026` : text2;
|
|
2097
|
+
return `<code class="json-inline">${highlightJsonText(clipped)}</code>`;
|
|
2019
2098
|
};
|
|
2020
2099
|
const when = (epoch) => {
|
|
2021
2100
|
const date = new Date(epoch);
|
|
2022
2101
|
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
2023
2102
|
};
|
|
2103
|
+
const journalWhen = (value) => {
|
|
2104
|
+
const numeric = value.trim() === "" ? Number.NaN : Number(value);
|
|
2105
|
+
const epoch = Number.isFinite(numeric) ? numeric : Date.parse(value);
|
|
2106
|
+
return Number.isFinite(epoch) ? when(epoch) : value;
|
|
2107
|
+
};
|
|
2024
2108
|
const clockCard = (epoch) => {
|
|
2025
2109
|
const date = new Date(epoch);
|
|
2026
2110
|
if (Number.isNaN(date.getTime()))
|
|
@@ -2140,7 +2224,7 @@ function bootAdmin(config) {
|
|
|
2140
2224
|
};
|
|
2141
2225
|
const renderOverview = async () => {
|
|
2142
2226
|
const [health, shape, clock, journal] = await Promise.all([
|
|
2143
|
-
originGet(
|
|
2227
|
+
originGet(`${config.adminPrefix}/health`),
|
|
2144
2228
|
api("GET", "/state"),
|
|
2145
2229
|
api("GET", "/clock"),
|
|
2146
2230
|
api("GET", "/requests?limit=8")
|
|
@@ -2157,7 +2241,7 @@ function bootAdmin(config) {
|
|
|
2157
2241
|
},
|
|
2158
2242
|
{
|
|
2159
2243
|
k: "Status",
|
|
2160
|
-
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
2244
|
+
v: `<span class="badge ok">${esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")}</span>`
|
|
2161
2245
|
},
|
|
2162
2246
|
{ k: "Namespace", v: esc(state.namespace) },
|
|
2163
2247
|
{ k: "Collections", v: String(decoded.collections.length) },
|
|
@@ -2244,26 +2328,46 @@ function bootAdmin(config) {
|
|
|
2244
2328
|
const renderJournal = async () => {
|
|
2245
2329
|
const journal = await api("GET", "/requests?limit=100");
|
|
2246
2330
|
el("journal", HTMLElement).innerHTML = table([
|
|
2247
|
-
{ label: "When", cell: (row) => esc(row.at) },
|
|
2248
|
-
{
|
|
2331
|
+
{ label: "When", cell: (row) => esc(journalWhen(row.at)) },
|
|
2332
|
+
{
|
|
2333
|
+
label: "Method",
|
|
2334
|
+
cell: (row) => `<span class="method">${esc(row.method)}</span>`
|
|
2335
|
+
},
|
|
2249
2336
|
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2250
|
-
{
|
|
2337
|
+
{
|
|
2338
|
+
label: "Status",
|
|
2339
|
+
cell: (row) => `<span class="badge ${row.status.startsWith("2") ? "ok" : row.status.startsWith("4") || row.status.startsWith("5") ? "warn" : ""}">${esc(row.status)}</span>`
|
|
2340
|
+
},
|
|
2251
2341
|
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
2252
2342
|
], decodeJournal(journal));
|
|
2253
2343
|
};
|
|
2254
|
-
const
|
|
2255
|
-
const
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2344
|
+
const paintRoutes = () => {
|
|
2345
|
+
const query = el("route-search", HTMLInputElement).value.trim().toLocaleLowerCase();
|
|
2346
|
+
const routes = state.routes.filter((route) => route.toLocaleLowerCase().includes(query));
|
|
2347
|
+
el("route-count", HTMLElement).textContent = `${routes.length} of ${state.routes.length}`;
|
|
2348
|
+
if (state.routes.length === 0) {
|
|
2349
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No admin routes are available.</p>';
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2352
|
+
if (routes.length === 0) {
|
|
2353
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No routes match this filter.</p>';
|
|
2354
|
+
return;
|
|
2355
|
+
}
|
|
2259
2356
|
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2260
2357
|
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2261
2358
|
const parts = route.split(" ");
|
|
2262
2359
|
const method = parts[0] ?? "";
|
|
2263
2360
|
const path = parts.slice(1).join(" ");
|
|
2264
|
-
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>`;
|
|
2361
|
+
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>`;
|
|
2265
2362
|
}).join("");
|
|
2266
2363
|
};
|
|
2364
|
+
const renderRoutes = async () => {
|
|
2365
|
+
const data = await api("GET", "/");
|
|
2366
|
+
if (!isRecord2(data))
|
|
2367
|
+
throw new Error("Route list was not an object");
|
|
2368
|
+
state.routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
2369
|
+
paintRoutes();
|
|
2370
|
+
};
|
|
2267
2371
|
const renderers = {
|
|
2268
2372
|
overview: renderOverview,
|
|
2269
2373
|
state: renderState,
|
|
@@ -2272,19 +2376,50 @@ function bootAdmin(config) {
|
|
|
2272
2376
|
journal: renderJournal,
|
|
2273
2377
|
routes: renderRoutes
|
|
2274
2378
|
};
|
|
2379
|
+
let refreshToken = 0;
|
|
2275
2380
|
async function refresh() {
|
|
2381
|
+
const token = ++refreshToken;
|
|
2276
2382
|
clearError();
|
|
2383
|
+
const main = el("views", HTMLElement);
|
|
2384
|
+
const sync = el("sync-status", HTMLElement);
|
|
2385
|
+
main.setAttribute("aria-busy", "true");
|
|
2386
|
+
sync.classList.add("busy");
|
|
2387
|
+
sync.textContent = "Updating";
|
|
2277
2388
|
const render = renderers[state.view];
|
|
2278
|
-
|
|
2279
|
-
|
|
2389
|
+
try {
|
|
2390
|
+
if (render)
|
|
2391
|
+
await render();
|
|
2392
|
+
if (token === refreshToken)
|
|
2393
|
+
sync.textContent = "Up to date";
|
|
2394
|
+
} finally {
|
|
2395
|
+
if (token === refreshToken) {
|
|
2396
|
+
main.removeAttribute("aria-busy");
|
|
2397
|
+
sync.classList.remove("busy");
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2280
2400
|
}
|
|
2281
2401
|
const delay = (ms) => new Promise((resolve) => {
|
|
2282
2402
|
window.setTimeout(resolve, ms);
|
|
2283
2403
|
});
|
|
2284
|
-
const confirmAction = async (button, pending, done, work) => {
|
|
2404
|
+
const confirmAction = async (button, pending, done, work, confirmLabel) => {
|
|
2285
2405
|
if (button.dataset.pending === "1")
|
|
2286
2406
|
return;
|
|
2287
2407
|
const label = button.textContent ?? "";
|
|
2408
|
+
if (confirmLabel !== void 0 && button.dataset.confirming !== "1") {
|
|
2409
|
+
button.dataset.confirming = "1";
|
|
2410
|
+
button.classList.add("confirming");
|
|
2411
|
+
button.textContent = confirmLabel;
|
|
2412
|
+
window.setTimeout(() => {
|
|
2413
|
+
if (button.dataset.confirming !== "1")
|
|
2414
|
+
return;
|
|
2415
|
+
button.textContent = label;
|
|
2416
|
+
button.classList.remove("confirming");
|
|
2417
|
+
delete button.dataset.confirming;
|
|
2418
|
+
}, 3e3);
|
|
2419
|
+
return;
|
|
2420
|
+
}
|
|
2421
|
+
button.classList.remove("confirming");
|
|
2422
|
+
delete button.dataset.confirming;
|
|
2288
2423
|
const width = button.offsetWidth;
|
|
2289
2424
|
button.dataset.pending = "1";
|
|
2290
2425
|
button.classList.add("pressed");
|
|
@@ -2305,6 +2440,65 @@ function bootAdmin(config) {
|
|
|
2305
2440
|
delete button.dataset.pending;
|
|
2306
2441
|
}
|
|
2307
2442
|
};
|
|
2443
|
+
const wireJsonEditor = (textareaId, highlightId, statusId, shellId, allowEmpty) => {
|
|
2444
|
+
const textarea = el(textareaId, HTMLTextAreaElement);
|
|
2445
|
+
const highlight = el(highlightId, HTMLElement);
|
|
2446
|
+
const status = el(statusId, HTMLElement);
|
|
2447
|
+
const shell = el(shellId, HTMLElement);
|
|
2448
|
+
const sync = () => {
|
|
2449
|
+
const text2 = textarea.value;
|
|
2450
|
+
highlight.innerHTML = `${highlightJsonText(text2)}
|
|
2451
|
+
`;
|
|
2452
|
+
let valid = true;
|
|
2453
|
+
let message2 = "Valid JSON";
|
|
2454
|
+
if (allowEmpty && text2.trim() === "")
|
|
2455
|
+
message2 = "No body";
|
|
2456
|
+
else {
|
|
2457
|
+
try {
|
|
2458
|
+
readJson2(text2);
|
|
2459
|
+
} catch {
|
|
2460
|
+
valid = false;
|
|
2461
|
+
message2 = "Invalid JSON";
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
shell.classList.toggle("invalid", !valid);
|
|
2465
|
+
status.classList.toggle("invalid", !valid);
|
|
2466
|
+
status.textContent = message2;
|
|
2467
|
+
textarea.setAttribute("aria-invalid", valid ? "false" : "true");
|
|
2468
|
+
};
|
|
2469
|
+
const syncScroll = () => {
|
|
2470
|
+
const preview2 = highlight.parentElement;
|
|
2471
|
+
if (!(preview2 instanceof HTMLElement))
|
|
2472
|
+
return;
|
|
2473
|
+
preview2.scrollTop = textarea.scrollTop;
|
|
2474
|
+
preview2.scrollLeft = textarea.scrollLeft;
|
|
2475
|
+
};
|
|
2476
|
+
textarea.addEventListener("input", sync);
|
|
2477
|
+
textarea.addEventListener("scroll", syncScroll);
|
|
2478
|
+
sync();
|
|
2479
|
+
return sync;
|
|
2480
|
+
};
|
|
2481
|
+
const syncRouteEditor = wireJsonEditor("route-body", "route-highlight", "route-json-status", "route-editor", true);
|
|
2482
|
+
const syncRecordEditor = wireJsonEditor("editor-value", "editor-highlight", "editor-json-status", "record-editor", false);
|
|
2483
|
+
for (const button of document.querySelectorAll("[data-format]")) {
|
|
2484
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2485
|
+
continue;
|
|
2486
|
+
button.addEventListener("click", () => {
|
|
2487
|
+
const target = button.dataset.format;
|
|
2488
|
+
if (target === void 0)
|
|
2489
|
+
return;
|
|
2490
|
+
const textarea = document.getElementById(target);
|
|
2491
|
+
if (!(textarea instanceof HTMLTextAreaElement) || textarea.value.trim() === "")
|
|
2492
|
+
return;
|
|
2493
|
+
try {
|
|
2494
|
+
textarea.value = JSON.stringify(readJson2(textarea.value), null, 2) ?? "";
|
|
2495
|
+
textarea.dispatchEvent(new Event("input"));
|
|
2496
|
+
textarea.focus();
|
|
2497
|
+
} catch {
|
|
2498
|
+
showError(new Error("Fix the JSON before formatting it"));
|
|
2499
|
+
}
|
|
2500
|
+
});
|
|
2501
|
+
}
|
|
2308
2502
|
document.addEventListener("click", (event) => {
|
|
2309
2503
|
const target = event.target;
|
|
2310
2504
|
if (!(target instanceof Element))
|
|
@@ -2377,6 +2571,7 @@ function bootAdmin(config) {
|
|
|
2377
2571
|
} else {
|
|
2378
2572
|
editorValue.value = "{\n \n}";
|
|
2379
2573
|
}
|
|
2574
|
+
syncRecordEditor();
|
|
2380
2575
|
editor.showModal();
|
|
2381
2576
|
}
|
|
2382
2577
|
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -2406,10 +2601,10 @@ function bootAdmin(config) {
|
|
|
2406
2601
|
const id = state.editing;
|
|
2407
2602
|
if (!id)
|
|
2408
2603
|
return;
|
|
2409
|
-
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2604
|
+
confirmAction(editorDelete, "Deleting...", "Deleted", () => api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2410
2605
|
editor.close();
|
|
2411
2606
|
return renderState();
|
|
2412
|
-
}).catch(showError);
|
|
2607
|
+
}), "Confirm delete").catch(showError);
|
|
2413
2608
|
});
|
|
2414
2609
|
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2415
2610
|
if (!(button instanceof HTMLButtonElement))
|
|
@@ -2455,10 +2650,10 @@ function bootAdmin(config) {
|
|
|
2455
2650
|
}).then(() => renderFaults()).catch(showError);
|
|
2456
2651
|
});
|
|
2457
2652
|
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2458
|
-
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
|
|
2653
|
+
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults()), "Confirm clear").catch(showError);
|
|
2459
2654
|
});
|
|
2460
2655
|
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2461
|
-
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
|
|
2656
|
+
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal()), "Confirm clear").catch(showError);
|
|
2462
2657
|
});
|
|
2463
2658
|
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2464
2659
|
const target = event.target;
|
|
@@ -2472,9 +2667,15 @@ function bootAdmin(config) {
|
|
|
2472
2667
|
return;
|
|
2473
2668
|
state.route = route;
|
|
2474
2669
|
el("route-title", HTMLElement).textContent = route;
|
|
2670
|
+
el("route-send", HTMLButtonElement).disabled = false;
|
|
2475
2671
|
const method = route.split(" ")[0] ?? "";
|
|
2476
|
-
el("route-body", HTMLTextAreaElement)
|
|
2672
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
2673
|
+
routeBody.disabled = method === "GET" || method === "DELETE";
|
|
2674
|
+
el("route-editor", HTMLElement).hidden = routeBody.disabled;
|
|
2675
|
+
el("route-json-status", HTMLElement).parentElement?.toggleAttribute("hidden", routeBody.disabled);
|
|
2676
|
+
syncRouteEditor();
|
|
2477
2677
|
});
|
|
2678
|
+
el("route-search", HTMLInputElement).addEventListener("input", paintRoutes);
|
|
2478
2679
|
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2479
2680
|
event.preventDefault();
|
|
2480
2681
|
const send = async () => {
|
|
@@ -2496,7 +2697,8 @@ function bootAdmin(config) {
|
|
|
2496
2697
|
}
|
|
2497
2698
|
}
|
|
2498
2699
|
const data = await api(method, concrete, body);
|
|
2499
|
-
|
|
2700
|
+
const text2 = JSON.stringify(data, null, 2) ?? "null";
|
|
2701
|
+
el("route-result", HTMLElement).innerHTML = `<code>${highlightJsonText(text2)}</code>`;
|
|
2500
2702
|
};
|
|
2501
2703
|
send().catch(showError);
|
|
2502
2704
|
});
|
|
@@ -2614,11 +2816,13 @@ function bootAdmin(config) {
|
|
|
2614
2816
|
views.append(section);
|
|
2615
2817
|
mountSqlExplorer(body, panelApi);
|
|
2616
2818
|
}
|
|
2819
|
+
show(document.getElementById(`view-${state.view}`) === null ? "overview" : state.view);
|
|
2617
2820
|
return refresh();
|
|
2618
2821
|
}).catch(showError);
|
|
2619
2822
|
}
|
|
2620
2823
|
var clientPrelude = [
|
|
2621
2824
|
isRecord2,
|
|
2825
|
+
highlightJsonText,
|
|
2622
2826
|
mountAdminBrand,
|
|
2623
2827
|
startAdminBrand,
|
|
2624
2828
|
faultPresetList,
|
|
@@ -2641,6 +2845,7 @@ ${entry}(${payload});
|
|
|
2641
2845
|
var defineMock = (runtime) => runtime;
|
|
2642
2846
|
var STANDARD_ADMIN_ROUTES = [
|
|
2643
2847
|
"GET /",
|
|
2848
|
+
"GET /health",
|
|
2644
2849
|
"POST /reset",
|
|
2645
2850
|
"GET /namespaces",
|
|
2646
2851
|
"GET /clock",
|
|
@@ -2720,11 +2925,11 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
2720
2925
|
}
|
|
2721
2926
|
});
|
|
2722
2927
|
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
2723
|
-
var renderAdminDocument = (service) => {
|
|
2928
|
+
var renderAdminDocument = (service, adminPrefix = ADMIN_PREFIX) => {
|
|
2724
2929
|
const name = escapeHtml(service);
|
|
2725
2930
|
const script = adminClientSource({
|
|
2726
2931
|
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
2727
|
-
adminPrefix
|
|
2932
|
+
adminPrefix,
|
|
2728
2933
|
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
2729
2934
|
brandsUrl: ADMIN_BRANDS_URL,
|
|
2730
2935
|
service
|
|
@@ -2741,75 +2946,99 @@ var renderAdminDocument = (service) => {
|
|
|
2741
2946
|
${CSS_RESET}
|
|
2742
2947
|
:root {
|
|
2743
2948
|
color-scheme: light;
|
|
2744
|
-
--bg: #
|
|
2949
|
+
--bg: #f7f8fb;
|
|
2745
2950
|
--bg-elev: #ffffff;
|
|
2746
|
-
--bg-muted: #
|
|
2747
|
-
--bg-inset: #
|
|
2748
|
-
--line: #
|
|
2749
|
-
--line-strong: #
|
|
2750
|
-
--ink: #
|
|
2751
|
-
--muted: #
|
|
2752
|
-
--faint: #
|
|
2753
|
-
--accent: #
|
|
2951
|
+
--bg-muted: #f1f3f7;
|
|
2952
|
+
--bg-inset: #f8f9fc;
|
|
2953
|
+
--line: #e4e7ec;
|
|
2954
|
+
--line-strong: #cfd4dc;
|
|
2955
|
+
--ink: #101828;
|
|
2956
|
+
--muted: #475467;
|
|
2957
|
+
--faint: #667085;
|
|
2958
|
+
--accent: #6941c6;
|
|
2959
|
+
--accent-hover: #5933b4;
|
|
2754
2960
|
--accent-ink: #ffffff;
|
|
2755
|
-
--accent-soft: #
|
|
2756
|
-
--ok: #
|
|
2757
|
-
--ok-soft: #
|
|
2758
|
-
--warn: #
|
|
2759
|
-
--warn-soft: #
|
|
2760
|
-
--err: #
|
|
2761
|
-
--err-soft: #
|
|
2762
|
-
--
|
|
2961
|
+
--accent-soft: #f4f0ff;
|
|
2962
|
+
--ok: #067647;
|
|
2963
|
+
--ok-soft: #ecfdf3;
|
|
2964
|
+
--warn: #b54708;
|
|
2965
|
+
--warn-soft: #fffaeb;
|
|
2966
|
+
--err: #b42318;
|
|
2967
|
+
--err-soft: #fef3f2;
|
|
2968
|
+
--code-key: #6941c6;
|
|
2969
|
+
--code-string: #067647;
|
|
2970
|
+
--code-number: #175cd3;
|
|
2971
|
+
--code-boolean: #c11574;
|
|
2972
|
+
--code-null: #667085;
|
|
2973
|
+
--shadow-xs: 0 1px 2px rgb(16 24 40 / 0.05);
|
|
2974
|
+
--shadow-sm: 0 1px 3px rgb(16 24 40 / 0.08), 0 1px 2px rgb(16 24 40 / 0.04);
|
|
2975
|
+
--shadow-lg: 0 18px 48px rgb(16 24 40 / 0.16), 0 4px 12px rgb(16 24 40 / 0.08);
|
|
2763
2976
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2764
2977
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
2765
|
-
--radius:
|
|
2766
|
-
--header:
|
|
2767
|
-
--nav:
|
|
2978
|
+
--radius: 12px;
|
|
2979
|
+
--header: 72px;
|
|
2980
|
+
--nav: 224px;
|
|
2768
2981
|
}
|
|
2769
2982
|
:root[data-theme="dark"] {
|
|
2770
2983
|
color-scheme: dark;
|
|
2771
|
-
--bg: #
|
|
2772
|
-
--bg-elev: #
|
|
2773
|
-
--bg-muted: #
|
|
2774
|
-
--bg-inset: #
|
|
2775
|
-
--line: #
|
|
2776
|
-
--line-strong: #
|
|
2777
|
-
--ink: #
|
|
2778
|
-
--muted: #
|
|
2779
|
-
--faint: #
|
|
2780
|
-
--accent: #
|
|
2781
|
-
--accent-
|
|
2782
|
-
--accent-
|
|
2783
|
-
--
|
|
2784
|
-
--ok
|
|
2785
|
-
--
|
|
2786
|
-
--warn
|
|
2787
|
-
--
|
|
2788
|
-
--err
|
|
2789
|
-
--
|
|
2984
|
+
--bg: #0c0e14;
|
|
2985
|
+
--bg-elev: #151821;
|
|
2986
|
+
--bg-muted: #20232e;
|
|
2987
|
+
--bg-inset: #11141c;
|
|
2988
|
+
--line: #2b303d;
|
|
2989
|
+
--line-strong: #414858;
|
|
2990
|
+
--ink: #f5f7fa;
|
|
2991
|
+
--muted: #b6bdc9;
|
|
2992
|
+
--faint: #8d96a7;
|
|
2993
|
+
--accent: #9e77ed;
|
|
2994
|
+
--accent-hover: #b692f6;
|
|
2995
|
+
--accent-ink: #ffffff;
|
|
2996
|
+
--accent-soft: #2b2142;
|
|
2997
|
+
--ok: #75e0a7;
|
|
2998
|
+
--ok-soft: #102a20;
|
|
2999
|
+
--warn: #fec84b;
|
|
3000
|
+
--warn-soft: #302611;
|
|
3001
|
+
--err: #fda29b;
|
|
3002
|
+
--err-soft: #321817;
|
|
3003
|
+
--code-key: #c3a6ff;
|
|
3004
|
+
--code-string: #75e0a7;
|
|
3005
|
+
--code-number: #84adff;
|
|
3006
|
+
--code-boolean: #f9a8d4;
|
|
3007
|
+
--code-null: #98a2b3;
|
|
3008
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3009
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3010
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2790
3011
|
}
|
|
2791
3012
|
@media (prefers-color-scheme: dark) {
|
|
2792
3013
|
:root:not([data-theme="light"]) {
|
|
2793
3014
|
color-scheme: dark;
|
|
2794
|
-
--bg: #
|
|
2795
|
-
--bg-elev: #
|
|
2796
|
-
--bg-muted: #
|
|
2797
|
-
--bg-inset: #
|
|
2798
|
-
--line: #
|
|
2799
|
-
--line-strong: #
|
|
2800
|
-
--ink: #
|
|
2801
|
-
--muted: #
|
|
2802
|
-
--faint: #
|
|
2803
|
-
--accent: #
|
|
2804
|
-
--accent-
|
|
2805
|
-
--accent-
|
|
2806
|
-
--
|
|
2807
|
-
--ok
|
|
2808
|
-
--
|
|
2809
|
-
--warn
|
|
2810
|
-
--
|
|
2811
|
-
--err
|
|
2812
|
-
--
|
|
3015
|
+
--bg: #0c0e14;
|
|
3016
|
+
--bg-elev: #151821;
|
|
3017
|
+
--bg-muted: #20232e;
|
|
3018
|
+
--bg-inset: #11141c;
|
|
3019
|
+
--line: #2b303d;
|
|
3020
|
+
--line-strong: #414858;
|
|
3021
|
+
--ink: #f5f7fa;
|
|
3022
|
+
--muted: #b6bdc9;
|
|
3023
|
+
--faint: #8d96a7;
|
|
3024
|
+
--accent: #9e77ed;
|
|
3025
|
+
--accent-hover: #b692f6;
|
|
3026
|
+
--accent-ink: #ffffff;
|
|
3027
|
+
--accent-soft: #2b2142;
|
|
3028
|
+
--ok: #75e0a7;
|
|
3029
|
+
--ok-soft: #102a20;
|
|
3030
|
+
--warn: #fec84b;
|
|
3031
|
+
--warn-soft: #302611;
|
|
3032
|
+
--err: #fda29b;
|
|
3033
|
+
--err-soft: #321817;
|
|
3034
|
+
--code-key: #c3a6ff;
|
|
3035
|
+
--code-string: #75e0a7;
|
|
3036
|
+
--code-number: #84adff;
|
|
3037
|
+
--code-boolean: #f9a8d4;
|
|
3038
|
+
--code-null: #98a2b3;
|
|
3039
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3040
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3041
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2813
3042
|
}
|
|
2814
3043
|
}
|
|
2815
3044
|
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
@@ -2827,13 +3056,17 @@ var renderAdminDocument = (service) => {
|
|
|
2827
3056
|
padding-right: env(safe-area-inset-right);
|
|
2828
3057
|
-webkit-text-size-adjust: 100%;
|
|
2829
3058
|
}
|
|
2830
|
-
|
|
3059
|
+
::selection { background: color-mix(in srgb, var(--accent) 28%, transparent); }
|
|
3060
|
+
:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px; }
|
|
3061
|
+
button, input, select, textarea { transition: border-color 140ms ease, box-shadow 140ms ease, transform 100ms ease; }
|
|
3062
|
+
button:disabled, input:disabled, select:disabled, textarea:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
2831
3063
|
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
2832
3064
|
header.top {
|
|
2833
|
-
position: sticky; top: 0; z-index:
|
|
3065
|
+
position: sticky; top: 0; z-index: 20;
|
|
2834
3066
|
display: flex; align-items: center; gap: 12px;
|
|
2835
|
-
min-height: var(--header); padding:
|
|
2836
|
-
background: var(--bg-elev);
|
|
3067
|
+
min-height: var(--header); padding: 12px 20px;
|
|
3068
|
+
background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
|
|
3069
|
+
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
2837
3070
|
border-bottom: 1px solid var(--line);
|
|
2838
3071
|
min-width: 0; max-width: 100%;
|
|
2839
3072
|
}
|
|
@@ -2841,12 +3074,21 @@ var renderAdminDocument = (service) => {
|
|
|
2841
3074
|
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
2842
3075
|
min-width: 0; max-width: 100%; margin-right: auto;
|
|
2843
3076
|
}
|
|
2844
|
-
.brand-
|
|
2845
|
-
.brand-
|
|
3077
|
+
.brand-lockup { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
3078
|
+
.brand-mark {
|
|
3079
|
+
width: 38px; height: 38px; display: grid; place-items: center; flex: none;
|
|
3080
|
+
border-radius: 11px; color: #fff;
|
|
3081
|
+
background: linear-gradient(145deg, #7f56d9 0%, #53389e 100%);
|
|
3082
|
+
box-shadow: 0 6px 16px rgb(105 65 198 / 0.24), inset 0 1px 0 rgb(255 255 255 / 0.25);
|
|
3083
|
+
}
|
|
3084
|
+
.brand-mark svg { width: 22px; height: 22px; }
|
|
3085
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; line-height: 1.25; }
|
|
3086
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
2846
3087
|
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
2847
3088
|
.vendor {
|
|
2848
3089
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
2849
3090
|
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
3091
|
+
padding: 5px 8px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-inset);
|
|
2850
3092
|
}
|
|
2851
3093
|
.vendor[hidden] { display: none; }
|
|
2852
3094
|
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
@@ -2858,12 +3100,17 @@ var renderAdminDocument = (service) => {
|
|
|
2858
3100
|
.vendor-sep { color: var(--faint); }
|
|
2859
3101
|
.vendor-link { color: inherit; text-decoration: none; }
|
|
2860
3102
|
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
2861
|
-
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items:
|
|
2862
|
-
|
|
3103
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
|
|
3104
|
+
.sync-status { align-self: center; display: flex; align-items: center; gap: 6px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3105
|
+
.sync-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
|
|
3106
|
+
.sync-status.busy::before { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); animation: pulse 1.2s ease-in-out infinite; }
|
|
3107
|
+
label.field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; font-weight: 600; color: var(--faint); }
|
|
2863
3108
|
label.field input, label.field select, .controls input[type="password"] {
|
|
2864
|
-
background-color: var(--bg-
|
|
2865
|
-
padding:
|
|
3109
|
+
background-color: var(--bg-elev); border: 1px solid var(--line-strong); border-radius: 9px;
|
|
3110
|
+
padding: 8px 10px; min-height: 38px; line-height: 1.2; box-shadow: var(--shadow-xs);
|
|
2866
3111
|
}
|
|
3112
|
+
label.field input:hover, label.field select:hover, textarea:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--line-strong)); }
|
|
3113
|
+
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; }
|
|
2867
3114
|
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
2868
3115
|
the text fields in the same form. Paint it like those fields. */
|
|
2869
3116
|
label.field select {
|
|
@@ -2880,12 +3127,20 @@ var renderAdminDocument = (service) => {
|
|
|
2880
3127
|
dialog .field > textarea { width: 100%; }
|
|
2881
3128
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
2882
3129
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
.btn
|
|
3130
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
|
3131
|
+
border-radius: 9px; min-height: 38px; padding: 0 13px; border: 1px solid var(--line-strong);
|
|
3132
|
+
background: var(--bg-elev); color: var(--ink); font-size: 13px; font-weight: 600; line-height: 1;
|
|
3133
|
+
box-shadow: var(--shadow-xs);
|
|
3134
|
+
}
|
|
3135
|
+
.icon-btn { width: 38px; padding: 0; }
|
|
3136
|
+
.icon-btn svg, .btn svg, .btn-primary svg { width: 16px; height: 16px; }
|
|
3137
|
+
.icon-btn:hover, .btn:hover { background: var(--bg-muted); border-color: var(--line-strong); }
|
|
3138
|
+
.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; }
|
|
3139
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
3140
|
+
.btn-quiet { background: transparent; border-color: transparent; box-shadow: none; }
|
|
3141
|
+
.btn-quiet:hover { background: var(--bg-muted); }
|
|
3142
|
+
.btn-danger { color: var(--err); border-color: color-mix(in srgb, var(--err) 25%, var(--line)); }
|
|
3143
|
+
.btn-danger:hover, .btn-danger.confirming { background: var(--err-soft); border-color: color-mix(in srgb, var(--err) 45%, var(--line)); }
|
|
2889
3144
|
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
2890
3145
|
so the press itself has to stay visible. */
|
|
2891
3146
|
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
@@ -2901,93 +3156,159 @@ var renderAdminDocument = (service) => {
|
|
|
2901
3156
|
flex: 1 0 auto; min-width: 0;
|
|
2902
3157
|
}
|
|
2903
3158
|
nav.side {
|
|
2904
|
-
|
|
3159
|
+
position: sticky; top: var(--header); align-self: start; height: calc(100vh - var(--header));
|
|
3160
|
+
border-right: 1px solid var(--line); padding: 18px 12px;
|
|
2905
3161
|
display: flex; flex-direction: column; gap: 4px;
|
|
2906
|
-
background: var(--bg-elev);
|
|
3162
|
+
background: var(--bg-elev); overflow-y: auto;
|
|
2907
3163
|
}
|
|
3164
|
+
.nav-label { padding: 0 10px 8px; color: var(--faint); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
|
|
2908
3165
|
nav.side button {
|
|
3166
|
+
position: relative; display: flex; align-items: center; gap: 10px; width: 100%;
|
|
2909
3167
|
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
2910
|
-
padding: 9px 10px; min-height:
|
|
2911
|
-
}
|
|
2912
|
-
nav.side button
|
|
2913
|
-
|
|
2914
|
-
.
|
|
2915
|
-
.
|
|
2916
|
-
.
|
|
3168
|
+
padding: 9px 10px; min-height: 42px; color: var(--muted); font-size: 13px; font-weight: 550;
|
|
3169
|
+
}
|
|
3170
|
+
nav.side button:hover { background: var(--bg-muted); color: var(--ink); }
|
|
3171
|
+
nav.side button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 650; }
|
|
3172
|
+
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); }
|
|
3173
|
+
.nav-icon { width: 18px; height: 18px; display: grid; place-items: center; flex: none; }
|
|
3174
|
+
.nav-icon svg { width: 18px; height: 18px; }
|
|
3175
|
+
main { padding: 32px clamp(20px, 4vw, 48px) 64px; max-width: 1320px; width: 100%; min-width: 0; }
|
|
3176
|
+
main[aria-busy="true"] .view { opacity: 0.62; }
|
|
3177
|
+
.view { animation: view-in 180ms ease-out; }
|
|
3178
|
+
.view[hidden] { display: none; }
|
|
3179
|
+
.view-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
|
|
3180
|
+
.view h2 { margin: 0 0 5px; font-size: clamp(24px, 3vw, 30px); line-height: 1.2; letter-spacing: -0.035em; }
|
|
3181
|
+
.view h3 { margin: 24px 0 10px; font-size: 15px; letter-spacing: -0.01em; }
|
|
3182
|
+
.lede { margin: 0; color: var(--muted); max-width: 72ch; font-size: 14px; }
|
|
3183
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 175px), 1fr)); gap: 14px; }
|
|
2917
3184
|
.card {
|
|
2918
3185
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
2919
|
-
padding:
|
|
3186
|
+
padding: 18px; box-shadow: var(--shadow-sm);
|
|
2920
3187
|
}
|
|
2921
|
-
.card
|
|
2922
|
-
.card
|
|
3188
|
+
.stat-card { position: relative; overflow: hidden; }
|
|
3189
|
+
.stat-card::after { content: ""; position: absolute; inset: 0 0 auto; height: 2px; background: linear-gradient(90deg, var(--accent), transparent 78%); opacity: 0.7; }
|
|
3190
|
+
.card .k { color: var(--faint); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
3191
|
+
.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; }
|
|
2923
3192
|
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
2924
|
-
.
|
|
3193
|
+
.section-card { background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-xs); overflow: hidden; }
|
|
3194
|
+
.section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
|
|
3195
|
+
.section-title h3 { margin: 0; }
|
|
3196
|
+
.split { display: grid; grid-template-columns: minmax(0, 250px) minmax(0, 1fr); gap: 18px; align-items: start; }
|
|
3197
|
+
#collections { padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); box-shadow: var(--shadow-xs); }
|
|
2925
3198
|
.collection {
|
|
2926
3199
|
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
2927
|
-
border-radius:
|
|
3200
|
+
border-radius: 9px; padding: 10px 11px; min-height: 48px; color: var(--ink); cursor: pointer;
|
|
2928
3201
|
}
|
|
2929
|
-
.collection
|
|
3202
|
+
.collection:hover { background: var(--bg-muted); }
|
|
3203
|
+
.collection[aria-current="true"] { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 18%, var(--line)); color: var(--accent); }
|
|
2930
3204
|
.collection small { display: block; color: var(--faint); }
|
|
2931
3205
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
2932
|
-
.chip {
|
|
2933
|
-
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
2934
|
-
border-radius: 999px; padding: 4px 8px;
|
|
2935
|
-
}
|
|
3206
|
+
.chip { font-family: var(--mono); font-size: 11px; background: var(--bg-muted); border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; }
|
|
2936
3207
|
.chip i { color: var(--faint); font-style: normal; }
|
|
2937
3208
|
.sql-layout {
|
|
2938
3209
|
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
2939
3210
|
}
|
|
2940
|
-
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
2941
|
-
.sql-tables button { text-align: left; }
|
|
3211
|
+
.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); }
|
|
3212
|
+
.sql-tables button { text-align: left; border: 0; border-radius: 8px; padding: 9px 10px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
3213
|
+
.sql-tables button:hover, .sql-tables button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); }
|
|
2942
3214
|
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
2943
|
-
table { width: 100%; border-
|
|
2944
|
-
|
|
2945
|
-
th {
|
|
3215
|
+
.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); }
|
|
3216
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
3217
|
+
th, td { text-align: left; padding: 11px 13px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
3218
|
+
th { color: var(--faint); background: var(--bg-inset); font-weight: 650; font-size: 11px; text-transform: uppercase; letter-spacing: 0.045em; }
|
|
3219
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
2946
3220
|
tr.clickable { cursor: pointer; }
|
|
2947
|
-
tr.clickable:hover { background: var(--
|
|
3221
|
+
tr.clickable:hover { background: var(--accent-soft); }
|
|
2948
3222
|
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
2949
3223
|
pre {
|
|
2950
3224
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
2951
|
-
background: var(--bg-inset); border-radius: 10px; padding:
|
|
2952
|
-
}
|
|
2953
|
-
.
|
|
3225
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 10px; padding: 14px; font-size: 12px; line-height: 1.65;
|
|
3226
|
+
}
|
|
3227
|
+
.json-key { color: var(--code-key); }
|
|
3228
|
+
.json-string { color: var(--code-string); }
|
|
3229
|
+
.json-number { color: var(--code-number); }
|
|
3230
|
+
.json-boolean { color: var(--code-boolean); }
|
|
3231
|
+
.json-null { color: var(--code-null); font-style: italic; }
|
|
3232
|
+
.json-inline { display: block; max-width: 44rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
3233
|
+
.json-view { position: relative; max-height: 26rem; overflow: auto; }
|
|
3234
|
+
.json-editor { position: relative; min-height: 190px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--bg-inset); overflow: hidden; }
|
|
3235
|
+
.json-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); }
|
|
3236
|
+
.json-editor.invalid { border-color: var(--err); }
|
|
3237
|
+
.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; }
|
|
3238
|
+
.json-editor pre { position: absolute; inset: 0; pointer-events: none; background: transparent; }
|
|
3239
|
+
.json-editor textarea { position: relative; z-index: 1; resize: vertical; background: transparent; color: transparent; caret-color: var(--ink); -webkit-text-fill-color: transparent; }
|
|
3240
|
+
.json-editor textarea::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); }
|
|
3241
|
+
.editor-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; }
|
|
3242
|
+
.json-status { display: inline-flex; align-items: center; gap: 6px; color: var(--ok); font-size: 11px; }
|
|
3243
|
+
.json-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
3244
|
+
.json-status.invalid { color: var(--err); }
|
|
3245
|
+
.format-json { border: 0; padding: 3px 5px; background: transparent; color: var(--accent); font-size: 11px; font-weight: 650; cursor: pointer; }
|
|
3246
|
+
.row-actions { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 14px 0; }
|
|
2954
3247
|
.banner {
|
|
2955
|
-
display: none;
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
3248
|
+
display: none; position: sticky; top: calc(var(--header) + 12px); z-index: 15;
|
|
3249
|
+
align-items: center; gap: 10px; margin: 0 0 16px; padding: 11px 12px 11px 14px;
|
|
3250
|
+
border: 1px solid color-mix(in srgb, var(--err) 30%, var(--line)); border-radius: 10px;
|
|
3251
|
+
background: var(--err-soft); color: var(--err); box-shadow: var(--shadow-sm); font-size: 13px;
|
|
3252
|
+
}
|
|
3253
|
+
.banner.show { display: flex; }
|
|
3254
|
+
.banner span { flex: 1; }
|
|
3255
|
+
.banner button { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 7px; background: transparent; color: currentColor; cursor: pointer; }
|
|
3256
|
+
.banner button:hover { background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
3257
|
+
.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); }
|
|
2960
3258
|
dialog {
|
|
2961
|
-
border: 1px solid var(--line); border-radius:
|
|
2962
|
-
color: var(--ink); width: min(
|
|
2963
|
-
}
|
|
2964
|
-
dialog
|
|
2965
|
-
dialog
|
|
3259
|
+
border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--bg-elev);
|
|
3260
|
+
color: var(--ink); width: min(720px, calc(100% - 24px)); max-width: calc(100vw - 24px); box-shadow: var(--shadow-lg);
|
|
3261
|
+
}
|
|
3262
|
+
dialog[open] { animation: dialog-in 160ms ease-out; }
|
|
3263
|
+
dialog::backdrop { background: rgb(12 14 20 / 0.58); backdrop-filter: blur(3px); }
|
|
3264
|
+
dialog form, .dialog-body { padding: 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
3265
|
+
.dialog-heading { display: flex; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
|
3266
|
+
.dialog-heading strong { font-size: 18px; letter-spacing: -0.02em; }
|
|
3267
|
+
.dialog-heading span { color: var(--muted); font-size: 12px; }
|
|
2966
3268
|
textarea {
|
|
2967
3269
|
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
2968
3270
|
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
2969
3271
|
font-family: var(--mono); font-size: 12px;
|
|
2970
3272
|
}
|
|
2971
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
2972
|
-
.
|
|
3273
|
+
.dialog-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding-top: 4px; }
|
|
3274
|
+
.dialog-actions .btn-danger { margin-right: auto; }
|
|
3275
|
+
.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); }
|
|
2973
3276
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
2974
3277
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
2975
3278
|
.status-2 { color: var(--ok); }
|
|
2976
3279
|
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
2977
3280
|
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
3281
|
+
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
3282
|
+
.form-grid .wide { grid-column: span 2; }
|
|
2978
3283
|
.route {
|
|
2979
|
-
display: grid; grid-template-columns:
|
|
2980
|
-
padding:
|
|
2981
|
-
}
|
|
2982
|
-
.
|
|
3284
|
+
display: grid; grid-template-columns: 84px minmax(0, 1fr) auto; gap: 10px; align-items: center;
|
|
3285
|
+
padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--bg-elev);
|
|
3286
|
+
}
|
|
3287
|
+
.route:first-child { border-radius: var(--radius) var(--radius) 0 0; }
|
|
3288
|
+
.route:last-child { border-bottom: 0; border-radius: 0 0 var(--radius) var(--radius); }
|
|
3289
|
+
.route-tools { display: flex; align-items: flex-end; gap: 12px; margin-bottom: 12px; }
|
|
3290
|
+
.route-tools .field { flex: 1; }
|
|
3291
|
+
.route-tools input { width: 100%; }
|
|
3292
|
+
.route-count { padding-bottom: 10px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3293
|
+
#route-list { max-height: 34rem; border: 1px solid var(--line); border-radius: var(--radius); overflow: auto; box-shadow: var(--shadow-xs); }
|
|
3294
|
+
.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; }
|
|
3295
|
+
@keyframes view-in { from { transform: translateY(3px); } to { transform: translateY(0); } }
|
|
3296
|
+
@keyframes dialog-in { from { transform: translateY(8px) scale(0.99); } to { transform: translateY(0) scale(1); } }
|
|
3297
|
+
@keyframes pulse { 50% { opacity: 0.45; } }
|
|
3298
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
|
|
3299
|
+
@media (forced-colors: active) { .json-editor pre { display: none; } .json-editor textarea { color: CanvasText; -webkit-text-fill-color: CanvasText; } }
|
|
2983
3300
|
@media (max-width: 800px) {
|
|
3301
|
+
:root { --header: 0px; }
|
|
2984
3302
|
header.top {
|
|
2985
|
-
align-items: stretch; flex-direction: column;
|
|
2986
|
-
padding
|
|
3303
|
+
position: relative; align-items: stretch; flex-direction: column;
|
|
3304
|
+
padding: max(12px, env(safe-area-inset-top)) 14px 12px;
|
|
2987
3305
|
}
|
|
2988
3306
|
input, select, textarea { font-size: 16px; }
|
|
2989
3307
|
.brand, .vendor, .controls { width: 100%; }
|
|
3308
|
+
.brand { gap: 10px; }
|
|
3309
|
+
.vendor { order: 2; border-radius: 9px; }
|
|
2990
3310
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
3311
|
+
.sync-status { display: none; }
|
|
2991
3312
|
/* The layout fills the screen. A single-column grid stretches every auto
|
|
2992
3313
|
row, so the tab bar grew with the leftover height and the buttons floated
|
|
2993
3314
|
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
@@ -2997,14 +3318,23 @@ var renderAdminDocument = (service) => {
|
|
|
2997
3318
|
align-content: start;
|
|
2998
3319
|
}
|
|
2999
3320
|
nav.side {
|
|
3000
|
-
align-self: start; height: auto;
|
|
3321
|
+
position: sticky; top: 0; z-index: 12; align-self: start; height: auto;
|
|
3001
3322
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
3002
3323
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
3003
|
-
padding: 8px;
|
|
3324
|
+
padding: 8px; box-shadow: var(--shadow-xs);
|
|
3004
3325
|
}
|
|
3005
|
-
nav
|
|
3326
|
+
.nav-label { display: none; }
|
|
3327
|
+
nav.side button { width: auto; white-space: nowrap; flex: none; }
|
|
3328
|
+
nav.side button[aria-current="true"]::before { inset: auto 9px -8px; width: auto; height: 3px; border-radius: 3px 3px 0 0; }
|
|
3006
3329
|
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
3007
|
-
|
|
3330
|
+
.form-grid { grid-template-columns: minmax(0, 1fr); }
|
|
3331
|
+
.form-grid .wide { grid-column: auto; }
|
|
3332
|
+
main { padding: 22px 14px 48px; }
|
|
3333
|
+
.view-head { margin-bottom: 18px; }
|
|
3334
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
3335
|
+
.card { padding: 14px; }
|
|
3336
|
+
.card .v { font-size: 20px; }
|
|
3337
|
+
.table-shell { border: 0; background: transparent; box-shadow: none; overflow: visible; }
|
|
3008
3338
|
table, thead, tbody, tr, th, td { display: block; }
|
|
3009
3339
|
thead { display: none; }
|
|
3010
3340
|
tr {
|
|
@@ -3019,50 +3349,63 @@ var renderAdminDocument = (service) => {
|
|
|
3019
3349
|
.route { grid-template-columns: 72px 1fr; }
|
|
3020
3350
|
.route button { grid-column: 1 / -1; }
|
|
3021
3351
|
}
|
|
3352
|
+
@media (max-width: 460px) {
|
|
3353
|
+
.controls label.field { min-width: calc(50% - 4px); }
|
|
3354
|
+
.controls .icon-btn, .controls .btn { flex: 1; }
|
|
3355
|
+
.grid { grid-template-columns: minmax(0, 1fr); }
|
|
3356
|
+
.dialog-actions { flex-wrap: wrap; }
|
|
3357
|
+
.dialog-actions button { flex: 1; }
|
|
3358
|
+
.dialog-actions .btn-danger { flex-basis: 100%; margin-right: 0; }
|
|
3359
|
+
}
|
|
3022
3360
|
</style>
|
|
3023
3361
|
</head>
|
|
3024
3362
|
<body>
|
|
3025
3363
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
3026
3364
|
<header class="top">
|
|
3027
3365
|
<div class="brand">
|
|
3028
|
-
<div class="brand-
|
|
3029
|
-
<
|
|
3030
|
-
|
|
3366
|
+
<div class="brand-lockup">
|
|
3367
|
+
<div class="brand-mark" aria-hidden="true">
|
|
3368
|
+
<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>
|
|
3369
|
+
</div>
|
|
3370
|
+
<div class="brand-id">
|
|
3371
|
+
<strong>${name}</strong>
|
|
3372
|
+
<span>Mock admin</span>
|
|
3373
|
+
</div>
|
|
3031
3374
|
</div>
|
|
3032
3375
|
<div class="vendor" id="vendor" hidden></div>
|
|
3033
3376
|
</div>
|
|
3034
3377
|
<div class="controls">
|
|
3378
|
+
<span class="sync-status" id="sync-status" role="status" aria-live="polite">Ready</span>
|
|
3035
3379
|
<label class="field">Namespace
|
|
3036
3380
|
<select id="namespace" aria-label="Namespace"></select>
|
|
3037
3381
|
</label>
|
|
3038
3382
|
<label class="field">Admin key
|
|
3039
3383
|
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
3040
3384
|
</label>
|
|
3041
|
-
<button class="icon-btn" id="theme" type="button" aria-label="
|
|
3042
|
-
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
3385
|
+
<button class="icon-btn" id="theme" type="button" aria-label="Use dark theme" title="Use dark theme"></button>
|
|
3386
|
+
<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>
|
|
3043
3387
|
</div>
|
|
3044
3388
|
</header>
|
|
3045
3389
|
<div class="layout">
|
|
3046
3390
|
<nav class="side" id="nav" aria-label="Admin sections">
|
|
3047
|
-
<
|
|
3048
|
-
<button type="button" data-view="
|
|
3049
|
-
<button type="button" data-view="
|
|
3050
|
-
<button type="button" data-view="
|
|
3051
|
-
<button type="button" data-view="
|
|
3052
|
-
<button type="button" data-view="
|
|
3391
|
+
<span class="nav-label">Workspace</span>
|
|
3392
|
+
<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>
|
|
3393
|
+
<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>
|
|
3394
|
+
<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>
|
|
3395
|
+
<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>
|
|
3396
|
+
<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>
|
|
3397
|
+
<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>
|
|
3053
3398
|
</nav>
|
|
3054
3399
|
<main id="views">
|
|
3055
|
-
<
|
|
3400
|
+
<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>
|
|
3056
3401
|
<section class="view" id="view-overview">
|
|
3057
|
-
<h2>Overview</h2>
|
|
3058
|
-
<p class="lede">Health, clock, and the collections this namespace is holding.</p>
|
|
3402
|
+
<div class="view-head"><div><h2>Overview</h2><p class="lede">Health, clock, and the collections this namespace is holding.</p></div></div>
|
|
3059
3403
|
<div class="grid" id="overview-cards"></div>
|
|
3060
3404
|
<h3>Recent requests</h3>
|
|
3061
3405
|
<div id="overview-requests"></div>
|
|
3062
3406
|
</section>
|
|
3063
3407
|
<section class="view" id="view-state" hidden>
|
|
3064
|
-
<h2>State</h2>
|
|
3065
|
-
<p class="lede">Every collection in this namespace. Declared fields stay visible before any row exists; stored rows add whatever else they contain.</p>
|
|
3408
|
+
<div class="view-head"><div><h2>State</h2><p class="lede">Inspect and edit every collection in this namespace.</p></div></div>
|
|
3066
3409
|
<div class="split">
|
|
3067
3410
|
<div id="collections"></div>
|
|
3068
3411
|
<div>
|
|
@@ -3075,8 +3418,7 @@ var renderAdminDocument = (service) => {
|
|
|
3075
3418
|
</div>
|
|
3076
3419
|
</section>
|
|
3077
3420
|
<section class="view" id="view-clock" hidden>
|
|
3078
|
-
<h2>Clock</h2>
|
|
3079
|
-
<p class="lede">The clock every timestamp in this mock reads.</p>
|
|
3421
|
+
<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>
|
|
3080
3422
|
<div class="card" id="clock-readout"></div>
|
|
3081
3423
|
<div class="row-actions">
|
|
3082
3424
|
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
@@ -3095,21 +3437,22 @@ var renderAdminDocument = (service) => {
|
|
|
3095
3437
|
</form>
|
|
3096
3438
|
</section>
|
|
3097
3439
|
<section class="view" id="view-faults" hidden>
|
|
3098
|
-
<h2>Faults</h2>
|
|
3099
|
-
<p class="lede">Inject a response, a delay, or a named preset. Rules apply to this namespace.</p>
|
|
3440
|
+
<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>
|
|
3100
3441
|
<form id="fault-form" class="card stack">
|
|
3101
|
-
<
|
|
3102
|
-
<
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
<
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
<
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
<
|
|
3112
|
-
|
|
3442
|
+
<div class="form-grid">
|
|
3443
|
+
<label class="field wide">Preset
|
|
3444
|
+
<select id="fault-preset"><option value="">Custom rule</option></select>
|
|
3445
|
+
</label>
|
|
3446
|
+
<label class="field">Status
|
|
3447
|
+
<input id="fault-status" inputmode="numeric" placeholder="503">
|
|
3448
|
+
</label>
|
|
3449
|
+
<label class="field wide">Path prefix
|
|
3450
|
+
<input id="fault-path" placeholder="/v1">
|
|
3451
|
+
</label>
|
|
3452
|
+
<label class="field">Method
|
|
3453
|
+
<input id="fault-method" placeholder="GET">
|
|
3454
|
+
</label>
|
|
3455
|
+
</div>
|
|
3113
3456
|
<div class="dialog-actions">
|
|
3114
3457
|
<button class="btn-primary" type="submit">Add fault</button>
|
|
3115
3458
|
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
@@ -3118,26 +3461,31 @@ var renderAdminDocument = (service) => {
|
|
|
3118
3461
|
<div id="fault-list"></div>
|
|
3119
3462
|
</section>
|
|
3120
3463
|
<section class="view" id="view-journal" hidden>
|
|
3121
|
-
<h2>Journal</h2>
|
|
3122
|
-
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
3123
|
-
<div class="row-actions">
|
|
3124
|
-
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
3125
|
-
</div>
|
|
3464
|
+
<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>
|
|
3126
3465
|
<div id="journal"></div>
|
|
3127
3466
|
</section>
|
|
3128
3467
|
<section class="view" id="view-routes" hidden>
|
|
3129
|
-
<h2>Routes</h2>
|
|
3130
|
-
<
|
|
3468
|
+
<div class="view-head"><div><h2>Routes</h2><p class="lede">Explore shared controls and service-specific admin endpoints.</p></div></div>
|
|
3469
|
+
<div class="route-tools">
|
|
3470
|
+
<label class="field">Filter routes
|
|
3471
|
+
<input id="route-search" type="search" placeholder="Method, path, or extension" autocomplete="off">
|
|
3472
|
+
</label>
|
|
3473
|
+
<span class="route-count" id="route-count"></span>
|
|
3474
|
+
</div>
|
|
3131
3475
|
<div id="route-list"></div>
|
|
3132
|
-
<form id="route-form" class="card stack">
|
|
3476
|
+
<form id="route-form" class="card stack" style="margin-top: 18px">
|
|
3133
3477
|
<strong id="route-title">Select a route</strong>
|
|
3134
3478
|
<label class="field">JSON body
|
|
3135
|
-
<
|
|
3479
|
+
<div class="json-editor" id="route-editor" hidden>
|
|
3480
|
+
<pre aria-hidden="true"><code id="route-highlight"></code></pre>
|
|
3481
|
+
<textarea id="route-body" placeholder="{ }" spellcheck="false" aria-describedby="route-json-status" disabled></textarea>
|
|
3482
|
+
</div>
|
|
3483
|
+
<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>
|
|
3136
3484
|
</label>
|
|
3137
3485
|
<div class="dialog-actions">
|
|
3138
|
-
<button class="btn-primary" type="submit">Send</button>
|
|
3486
|
+
<button class="btn-primary" id="route-send" type="submit" disabled>Send request</button>
|
|
3139
3487
|
</div>
|
|
3140
|
-
<pre id="route-result">Response will show here.</pre>
|
|
3488
|
+
<pre class="json-view" id="route-result"><code>Response will show here.</code></pre>
|
|
3141
3489
|
</form>
|
|
3142
3490
|
</section>
|
|
3143
3491
|
</main>
|
|
@@ -3145,12 +3493,16 @@ var renderAdminDocument = (service) => {
|
|
|
3145
3493
|
</div>
|
|
3146
3494
|
<dialog id="editor">
|
|
3147
3495
|
<form method="dialog" id="editor-form">
|
|
3148
|
-
<strong id="editor-title">Record</strong>
|
|
3496
|
+
<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>
|
|
3149
3497
|
<label class="field">Id
|
|
3150
3498
|
<input id="editor-id" required>
|
|
3151
3499
|
</label>
|
|
3152
3500
|
<label class="field">JSON value
|
|
3153
|
-
<
|
|
3501
|
+
<div class="json-editor" id="record-editor">
|
|
3502
|
+
<pre aria-hidden="true"><code id="editor-highlight"></code></pre>
|
|
3503
|
+
<textarea id="editor-value" spellcheck="false" aria-describedby="editor-json-status"></textarea>
|
|
3504
|
+
</div>
|
|
3505
|
+
<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>
|
|
3154
3506
|
</label>
|
|
3155
3507
|
<div class="dialog-actions">
|
|
3156
3508
|
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
@@ -3192,8 +3544,8 @@ var describeExtension = (extension) => {
|
|
|
3192
3544
|
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
3193
3545
|
};
|
|
3194
3546
|
};
|
|
3195
|
-
var adminUiRoutes = (service, ui) => {
|
|
3196
|
-
const shell = () => renderAdminDocument(service);
|
|
3547
|
+
var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
3548
|
+
const shell = () => renderAdminDocument(service, adminPrefix);
|
|
3197
3549
|
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
3198
3550
|
return {
|
|
3199
3551
|
"GET /ui": () => htmlResponse(document3()),
|
|
@@ -3810,7 +4162,7 @@ var stateAdminRoutes = (routes) => {
|
|
|
3810
4162
|
};
|
|
3811
4163
|
|
|
3812
4164
|
// ../core/dist/version.js
|
|
3813
|
-
var PACKAGE_VERSION = true ? "
|
|
4165
|
+
var PACKAGE_VERSION = true ? "2.0.0" : UNRELEASED_VERSION;
|
|
3814
4166
|
|
|
3815
4167
|
// ../core/dist/signing.js
|
|
3816
4168
|
var encoder = new TextEncoder();
|
|
@@ -4183,7 +4535,10 @@ var createWebhookHub = (options) => {
|
|
|
4183
4535
|
};
|
|
4184
4536
|
return hub;
|
|
4185
4537
|
};
|
|
4186
|
-
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4538
|
+
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4539
|
+
status,
|
|
4540
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4541
|
+
});
|
|
4187
4542
|
var adminError3 = (status, message2) => json4(status, { error: { type: "mockingbird_admin", message: message2 } });
|
|
4188
4543
|
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4189
4544
|
var parseEndpoint = (value) => {
|
|
@@ -4291,7 +4646,6 @@ var AT_HEADER = "x-mockingbird-at";
|
|
|
4291
4646
|
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
4292
4647
|
var DEFAULT_NAMESPACE = "default";
|
|
4293
4648
|
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4294
|
-
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
4295
4649
|
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4296
4650
|
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
4297
4651
|
var effects = /* @__PURE__ */ new WeakMap();
|
|
@@ -4332,6 +4686,22 @@ var operationMatcher = (document3) => {
|
|
|
4332
4686
|
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
4333
4687
|
};
|
|
4334
4688
|
var createRuntime = (options) => {
|
|
4689
|
+
const adminPrefix = resolveAdminPrefix(options.adminPrefix);
|
|
4690
|
+
const internalPaths = Object.entries(options.document?.paths ?? {}).filter(([, item]) => item["x-mockingbird-internal"] === true).map(([path]) => path);
|
|
4691
|
+
for (const path of internalPaths) {
|
|
4692
|
+
if (!path.startsWith(`${ADMIN_PREFIX}/blobs/`))
|
|
4693
|
+
throw new Error(`internal transport ${path} must live under ${ADMIN_PREFIX}/blobs/`);
|
|
4694
|
+
}
|
|
4695
|
+
assertAdminPrefixAvailable(adminPrefix, Object.keys(options.document?.paths ?? {}).filter((path) => !internalPaths.includes(path)));
|
|
4696
|
+
const internalMatch = (path) => {
|
|
4697
|
+
for (const pattern of internalPaths) {
|
|
4698
|
+
const actual = path.split("/");
|
|
4699
|
+
const expected = pattern.split("/");
|
|
4700
|
+
if (actual.length === expected.length && expected.every((part, i) => part.startsWith("{") || part === actual[i]))
|
|
4701
|
+
return pattern;
|
|
4702
|
+
}
|
|
4703
|
+
return void 0;
|
|
4704
|
+
};
|
|
4335
4705
|
assertAdminUi(options.adminUi);
|
|
4336
4706
|
const sqlite = bootSqlite(options.sqlite);
|
|
4337
4707
|
const clock = options.clock ?? createClock();
|
|
@@ -4377,6 +4747,7 @@ var createRuntime = (options) => {
|
|
|
4377
4747
|
const created = options.create({
|
|
4378
4748
|
namespace: storageNamespace(key),
|
|
4379
4749
|
publicNamespace,
|
|
4750
|
+
adminPrefix,
|
|
4380
4751
|
sqlite,
|
|
4381
4752
|
clock: namespaceClock(publicNamespace),
|
|
4382
4753
|
rng: createdContextRng
|
|
@@ -4609,10 +4980,10 @@ var createRuntime = (options) => {
|
|
|
4609
4980
|
namespaceClock,
|
|
4610
4981
|
namespaceOf(request) {
|
|
4611
4982
|
const url = new URL(request.url);
|
|
4612
|
-
const prefixed =
|
|
4983
|
+
const prefixed = matchNamespacePath(url.pathname, adminPrefix);
|
|
4613
4984
|
const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
|
|
4614
4985
|
const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
|
|
4615
|
-
if (path
|
|
4986
|
+
if (isAdminPath(path, adminPrefix))
|
|
4616
4987
|
return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
|
|
4617
4988
|
if (selected !== void 0)
|
|
4618
4989
|
return selected;
|
|
@@ -4678,7 +5049,7 @@ var createRuntime = (options) => {
|
|
|
4678
5049
|
state: (namespace = DEFAULT_NAMESPACE) => inspectState(stateScope(namespace)),
|
|
4679
5050
|
fetch: async (incoming) => {
|
|
4680
5051
|
let request = incoming;
|
|
4681
|
-
const prefixed =
|
|
5052
|
+
const prefixed = matchNamespacePath(new URL(request.url).pathname, adminPrefix);
|
|
4682
5053
|
if (prefixed) {
|
|
4683
5054
|
const url2 = new URL(request.url);
|
|
4684
5055
|
url2.pathname = prefixed[2] ?? "/";
|
|
@@ -4694,7 +5065,15 @@ var createRuntime = (options) => {
|
|
|
4694
5065
|
signal: request.signal
|
|
4695
5066
|
});
|
|
4696
5067
|
}
|
|
4697
|
-
|
|
5068
|
+
const internalUrl = new URL(request.url);
|
|
5069
|
+
const internalPath = isAdminPath(internalUrl.pathname, adminPrefix) ? `${ADMIN_PREFIX}${internalUrl.pathname.slice(adminPrefix.length)}` : "";
|
|
5070
|
+
const internal = internalMatch(internalPath);
|
|
5071
|
+
const obsoleteInternalPath = adminPrefix !== ADMIN_PREFIX && internalMatch(internalUrl.pathname) && !internal;
|
|
5072
|
+
if (internal) {
|
|
5073
|
+
internalUrl.pathname = internalPath;
|
|
5074
|
+
request = new Request(internalUrl, request);
|
|
5075
|
+
}
|
|
5076
|
+
let namespace = internal ? internalUrl.searchParams.get("namespace") ?? control.namespaceOf(request) : control.namespaceOf(request);
|
|
4698
5077
|
if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
4699
5078
|
const credential = options.credential(request);
|
|
4700
5079
|
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
@@ -4714,7 +5093,9 @@ var createRuntime = (options) => {
|
|
|
4714
5093
|
return copy;
|
|
4715
5094
|
}
|
|
4716
5095
|
};
|
|
4717
|
-
|
|
5096
|
+
if (obsoleteInternalPath)
|
|
5097
|
+
return stamp(adminFail(404, "internal transport is only available under adminPrefix"));
|
|
5098
|
+
const handled = internal ? void 0 : await control.handle(request);
|
|
4718
5099
|
if (handled)
|
|
4719
5100
|
return stamp(handled);
|
|
4720
5101
|
const started = monotonicNow();
|
|
@@ -4755,7 +5136,7 @@ var createRuntime = (options) => {
|
|
|
4755
5136
|
type: "mockingbird_admin",
|
|
4756
5137
|
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
4757
5138
|
}
|
|
4758
|
-
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
5139
|
+
}), { status: 400, headers: { "content-type": "application/json; charset=utf-8" } }));
|
|
4759
5140
|
}
|
|
4760
5141
|
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
4761
5142
|
log(400);
|
|
@@ -4872,15 +5253,16 @@ var createRuntime = (options) => {
|
|
|
4872
5253
|
...options.admin?.(runtime) ?? {},
|
|
4873
5254
|
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
4874
5255
|
// maps `presets`, and a service route must not replace that list with a record.
|
|
4875
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
5256
|
+
...options.presets ? presetRoutes(options.presets, runtime, adminPrefix) : {},
|
|
4876
5257
|
...stateAdminRoutes({
|
|
4877
5258
|
open: stateScope,
|
|
4878
5259
|
afterWrite: (namespace) => {
|
|
4879
5260
|
checkpoint(namespace, "main");
|
|
4880
5261
|
}
|
|
4881
5262
|
}),
|
|
4882
|
-
...adminUiRoutes(options.name, options.adminUi)
|
|
5263
|
+
...adminUiRoutes(options.name, options.adminUi, adminPrefix)
|
|
4883
5264
|
},
|
|
5265
|
+
adminPrefix,
|
|
4884
5266
|
adminKey: options.adminKey
|
|
4885
5267
|
});
|
|
4886
5268
|
return defineMock(runtime);
|
|
@@ -4894,7 +5276,10 @@ var mutableResponse = (response) => {
|
|
|
4894
5276
|
return new Response(response.body, response);
|
|
4895
5277
|
}
|
|
4896
5278
|
};
|
|
4897
|
-
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5279
|
+
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5280
|
+
status,
|
|
5281
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
5282
|
+
});
|
|
4898
5283
|
var adminFail = (status, message2) => adminJson(status, { error: { type: "mockingbird_admin", message: message2 } });
|
|
4899
5284
|
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4900
5285
|
var credentialRoutes = (registry) => ({
|
|
@@ -4946,7 +5331,7 @@ var credentialRoutes = (registry) => ({
|
|
|
4946
5331
|
return adminJson(200, { status: "ok" });
|
|
4947
5332
|
}
|
|
4948
5333
|
});
|
|
4949
|
-
var presetRoutes = (presets, runtime) => ({
|
|
5334
|
+
var presetRoutes = (presets, runtime, adminPrefix) => ({
|
|
4950
5335
|
"GET /faults/presets": () => {
|
|
4951
5336
|
const body = {
|
|
4952
5337
|
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
@@ -4956,7 +5341,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
4956
5341
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
4957
5342
|
const name = params.name;
|
|
4958
5343
|
if (!presets[name])
|
|
4959
|
-
return adminFail(404, `no fault preset ${name}; GET /
|
|
5344
|
+
return adminFail(404, `no fault preset ${name}; GET ${adminPrefix}/faults/presets`);
|
|
4960
5345
|
const overrides = isObject(body) ? body : {};
|
|
4961
5346
|
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
4962
5347
|
}
|
|
@@ -5527,6 +5912,7 @@ var createRuntime2 = (options = {}) => {
|
|
|
5527
5912
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
5528
5913
|
...options.clock ? { clock: options.clock } : {},
|
|
5529
5914
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
5915
|
+
...options.adminPrefix !== void 0 ? { adminPrefix: options.adminPrefix } : {},
|
|
5530
5916
|
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
5531
5917
|
...options.onLog ? { onLog: options.onLog } : {},
|
|
5532
5918
|
credential: apiKeyCredential,
|
|
@@ -5955,6 +6341,7 @@ var publicPatient = ({ partnerUserId: _partner, ...patient }) => patient;
|
|
|
5955
6341
|
|
|
5956
6342
|
export {
|
|
5957
6343
|
createClock,
|
|
6344
|
+
resolveAdminPrefix,
|
|
5958
6345
|
parseDuration,
|
|
5959
6346
|
LABS,
|
|
5960
6347
|
ELEMENTS,
|
|
@@ -5972,4 +6359,4 @@ export {
|
|
|
5972
6359
|
apiKeyCredential,
|
|
5973
6360
|
OdxAPI
|
|
5974
6361
|
};
|
|
5975
|
-
//# sourceMappingURL=chunk-
|
|
6362
|
+
//# sourceMappingURL=chunk-K6M4YJNF.js.map
|