@crvouga/mockingbird-service-google-maps 1.9.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 +16 -0
- package/dist/{chunk-6O3ECBQ4.js → chunk-DY4P32X4.js} +44 -24
- package/dist/chunk-DY4P32X4.js.map +7 -0
- package/dist/{chunk-CODFR63R.js → chunk-O2FDQBMY.js} +627 -237
- package/dist/chunk-O2FDQBMY.js.map +7 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +6 -3
- package/dist/index.js +1 -1
- package/dist/server.d.ts +4 -0
- package/dist/server.js +2 -2
- package/openapi.yaml +747 -0
- package/package.json +29 -4
- package/dist/chunk-6O3ECBQ4.js.map +0 -7
- package/dist/chunk-CODFR63R.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
|
};
|
|
@@ -1547,6 +1586,34 @@ dialog:not([open]) {
|
|
|
1547
1586
|
function isRecord2(value) {
|
|
1548
1587
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1549
1588
|
}
|
|
1589
|
+
function highlightJsonText(text2) {
|
|
1590
|
+
const escapeToken = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
1591
|
+
switch (char) {
|
|
1592
|
+
case "&":
|
|
1593
|
+
return "&";
|
|
1594
|
+
case "<":
|
|
1595
|
+
return "<";
|
|
1596
|
+
case ">":
|
|
1597
|
+
return ">";
|
|
1598
|
+
case '"':
|
|
1599
|
+
return """;
|
|
1600
|
+
default:
|
|
1601
|
+
return "'";
|
|
1602
|
+
}
|
|
1603
|
+
});
|
|
1604
|
+
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;
|
|
1605
|
+
let cursor = 0;
|
|
1606
|
+
let html = "";
|
|
1607
|
+
for (const match of text2.matchAll(token)) {
|
|
1608
|
+
const index = match.index ?? 0;
|
|
1609
|
+
const value = match[0];
|
|
1610
|
+
html += escapeToken(text2.slice(cursor, index));
|
|
1611
|
+
const kind = value.startsWith('"') ? value.trimEnd().endsWith(":") ? "key" : "string" : value === "true" || value === "false" ? "boolean" : value === "null" ? "null" : "number";
|
|
1612
|
+
html += `<span class="json-${kind}">${escapeToken(value)}</span>`;
|
|
1613
|
+
cursor = index + value.length;
|
|
1614
|
+
}
|
|
1615
|
+
return html + escapeToken(text2.slice(cursor));
|
|
1616
|
+
}
|
|
1550
1617
|
function mountAdminBrand(doc, host, service, catalog) {
|
|
1551
1618
|
const clip = (value, max) => {
|
|
1552
1619
|
if (typeof value !== "string")
|
|
@@ -1908,14 +1975,16 @@ function bootAdmin(config) {
|
|
|
1908
1975
|
const state = {
|
|
1909
1976
|
namespace: "default",
|
|
1910
1977
|
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
1911
|
-
view: "overview",
|
|
1978
|
+
view: ["overview", "state", "clock", "faults", "journal", "routes"].includes(location.hash.slice(1)) ? location.hash.slice(1) : "overview",
|
|
1912
1979
|
collection: "",
|
|
1913
1980
|
shape: null,
|
|
1914
1981
|
route: null,
|
|
1982
|
+
routes: [],
|
|
1915
1983
|
editing: null
|
|
1916
1984
|
};
|
|
1917
1985
|
const standard = new Set(config.standardRoutes);
|
|
1918
1986
|
const banner = el("banner", HTMLElement);
|
|
1987
|
+
const bannerMessage = el("banner-message", HTMLElement);
|
|
1919
1988
|
const keyInput = el("admin-key", HTMLInputElement);
|
|
1920
1989
|
keyInput.value = state.key;
|
|
1921
1990
|
const params = new URLSearchParams(location.search);
|
|
@@ -1926,18 +1995,21 @@ function bootAdmin(config) {
|
|
|
1926
1995
|
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
1927
1996
|
params.delete("key");
|
|
1928
1997
|
const next = params.toString();
|
|
1929
|
-
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
1998
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : "") + location.hash);
|
|
1930
1999
|
}
|
|
1931
2000
|
const queryNamespace = params.get("namespace");
|
|
1932
2001
|
if (queryNamespace)
|
|
1933
2002
|
state.namespace = queryNamespace;
|
|
1934
2003
|
const showError = (error) => {
|
|
1935
|
-
|
|
2004
|
+
bannerMessage.textContent = error instanceof Error ? error.message : String(error);
|
|
1936
2005
|
banner.classList.add("show");
|
|
2006
|
+
banner.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
1937
2007
|
};
|
|
1938
2008
|
const clearError = () => {
|
|
1939
2009
|
banner.classList.remove("show");
|
|
2010
|
+
bannerMessage.textContent = "";
|
|
1940
2011
|
};
|
|
2012
|
+
el("banner-dismiss", HTMLButtonElement).addEventListener("click", clearError);
|
|
1941
2013
|
const api = async (method, path, body) => {
|
|
1942
2014
|
const headers = { accept: "application/json" };
|
|
1943
2015
|
if (state.key !== "")
|
|
@@ -1981,16 +2053,21 @@ function bootAdmin(config) {
|
|
|
1981
2053
|
namespace: () => state.namespace
|
|
1982
2054
|
};
|
|
1983
2055
|
const isPanelScript = (value) => typeof value === "function";
|
|
2056
|
+
const themeButton = el("theme", HTMLButtonElement);
|
|
1984
2057
|
const setTheme = (theme) => {
|
|
1985
2058
|
if (theme)
|
|
1986
2059
|
document.documentElement.dataset.theme = theme;
|
|
1987
2060
|
else
|
|
1988
2061
|
delete document.documentElement.dataset.theme;
|
|
2062
|
+
const dark = theme ? theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2063
|
+
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>';
|
|
2064
|
+
const label = dark ? "Use light theme" : "Use dark theme";
|
|
2065
|
+
themeButton.setAttribute("aria-label", label);
|
|
2066
|
+
themeButton.title = label;
|
|
1989
2067
|
};
|
|
1990
2068
|
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
2069
|
+
setTheme(storedTheme === "dark" || storedTheme === "light" ? storedTheme : null);
|
|
2070
|
+
themeButton.addEventListener("click", () => {
|
|
1994
2071
|
const current = document.documentElement.dataset.theme;
|
|
1995
2072
|
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
1996
2073
|
const next = dark ? "light" : "dark";
|
|
@@ -2009,6 +2086,7 @@ function bootAdmin(config) {
|
|
|
2009
2086
|
continue;
|
|
2010
2087
|
section.hidden = section.id !== `view-${view}`;
|
|
2011
2088
|
}
|
|
2089
|
+
history.replaceState(null, "", `${location.pathname}${location.search}#${encodeURIComponent(view)}`);
|
|
2012
2090
|
};
|
|
2013
2091
|
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
2014
2092
|
const target = event.target;
|
|
@@ -2023,7 +2101,7 @@ function bootAdmin(config) {
|
|
|
2023
2101
|
show(view);
|
|
2024
2102
|
refresh().catch(showError);
|
|
2025
2103
|
});
|
|
2026
|
-
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>`;
|
|
2104
|
+
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>`;
|
|
2027
2105
|
const table = (columns, rows) => {
|
|
2028
2106
|
if (rows.length === 0)
|
|
2029
2107
|
return '<p class="empty">Nothing here yet.</p>';
|
|
@@ -2033,16 +2111,22 @@ function bootAdmin(config) {
|
|
|
2033
2111
|
const attrs = "attrs" in row2 && typeof row2.attrs === "string" ? row2.attrs : "";
|
|
2034
2112
|
return `<tr${attrs}>${cells}</tr>`;
|
|
2035
2113
|
}).join("");
|
|
2036
|
-
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
2114
|
+
return `<div class="table-shell"><table><thead>${head}</thead><tbody>${body}</tbody></table></div>`;
|
|
2037
2115
|
};
|
|
2038
2116
|
const preview = (value) => {
|
|
2039
2117
|
const text2 = JSON.stringify(value) ?? "null";
|
|
2040
|
-
|
|
2118
|
+
const clipped = text2.length > 180 ? `${text2.slice(0, 180)}\u2026` : text2;
|
|
2119
|
+
return `<code class="json-inline">${highlightJsonText(clipped)}</code>`;
|
|
2041
2120
|
};
|
|
2042
2121
|
const when = (epoch) => {
|
|
2043
2122
|
const date = new Date(epoch);
|
|
2044
2123
|
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
2045
2124
|
};
|
|
2125
|
+
const journalWhen = (value) => {
|
|
2126
|
+
const numeric = value.trim() === "" ? Number.NaN : Number(value);
|
|
2127
|
+
const epoch = Number.isFinite(numeric) ? numeric : Date.parse(value);
|
|
2128
|
+
return Number.isFinite(epoch) ? when(epoch) : value;
|
|
2129
|
+
};
|
|
2046
2130
|
const clockCard = (epoch) => {
|
|
2047
2131
|
const date = new Date(epoch);
|
|
2048
2132
|
if (Number.isNaN(date.getTime()))
|
|
@@ -2162,7 +2246,7 @@ function bootAdmin(config) {
|
|
|
2162
2246
|
};
|
|
2163
2247
|
const renderOverview = async () => {
|
|
2164
2248
|
const [health, shape, clock, journal] = await Promise.all([
|
|
2165
|
-
originGet(
|
|
2249
|
+
originGet(`${config.adminPrefix}/health`),
|
|
2166
2250
|
api("GET", "/state"),
|
|
2167
2251
|
api("GET", "/clock"),
|
|
2168
2252
|
api("GET", "/requests?limit=8")
|
|
@@ -2179,7 +2263,7 @@ function bootAdmin(config) {
|
|
|
2179
2263
|
},
|
|
2180
2264
|
{
|
|
2181
2265
|
k: "Status",
|
|
2182
|
-
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
2266
|
+
v: `<span class="badge ok">${esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")}</span>`
|
|
2183
2267
|
},
|
|
2184
2268
|
{ k: "Namespace", v: esc(state.namespace) },
|
|
2185
2269
|
{ k: "Collections", v: String(decoded.collections.length) },
|
|
@@ -2266,26 +2350,46 @@ function bootAdmin(config) {
|
|
|
2266
2350
|
const renderJournal = async () => {
|
|
2267
2351
|
const journal = await api("GET", "/requests?limit=100");
|
|
2268
2352
|
el("journal", HTMLElement).innerHTML = table([
|
|
2269
|
-
{ label: "When", cell: (row2) => esc(row2.at) },
|
|
2270
|
-
{
|
|
2353
|
+
{ label: "When", cell: (row2) => esc(journalWhen(row2.at)) },
|
|
2354
|
+
{
|
|
2355
|
+
label: "Method",
|
|
2356
|
+
cell: (row2) => `<span class="method">${esc(row2.method)}</span>`
|
|
2357
|
+
},
|
|
2271
2358
|
{ label: "Path", cell: (row2) => esc(row2.path) },
|
|
2272
|
-
{
|
|
2359
|
+
{
|
|
2360
|
+
label: "Status",
|
|
2361
|
+
cell: (row2) => `<span class="badge ${row2.status.startsWith("2") ? "ok" : row2.status.startsWith("4") || row2.status.startsWith("5") ? "warn" : ""}">${esc(row2.status)}</span>`
|
|
2362
|
+
},
|
|
2273
2363
|
{ label: "ms", cell: (row2) => esc(row2.durationMs) }
|
|
2274
2364
|
], decodeJournal(journal));
|
|
2275
2365
|
};
|
|
2276
|
-
const
|
|
2277
|
-
const
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2366
|
+
const paintRoutes = () => {
|
|
2367
|
+
const query = el("route-search", HTMLInputElement).value.trim().toLocaleLowerCase();
|
|
2368
|
+
const routes = state.routes.filter((route) => route.toLocaleLowerCase().includes(query));
|
|
2369
|
+
el("route-count", HTMLElement).textContent = `${routes.length} of ${state.routes.length}`;
|
|
2370
|
+
if (state.routes.length === 0) {
|
|
2371
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No admin routes are available.</p>';
|
|
2372
|
+
return;
|
|
2373
|
+
}
|
|
2374
|
+
if (routes.length === 0) {
|
|
2375
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No routes match this filter.</p>';
|
|
2376
|
+
return;
|
|
2377
|
+
}
|
|
2281
2378
|
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2282
2379
|
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2283
2380
|
const parts = route.split(" ");
|
|
2284
2381
|
const method = parts[0] ?? "";
|
|
2285
2382
|
const path = parts.slice(1).join(" ");
|
|
2286
|
-
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>`;
|
|
2383
|
+
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>`;
|
|
2287
2384
|
}).join("");
|
|
2288
2385
|
};
|
|
2386
|
+
const renderRoutes = async () => {
|
|
2387
|
+
const data = await api("GET", "/");
|
|
2388
|
+
if (!isRecord2(data))
|
|
2389
|
+
throw new Error("Route list was not an object");
|
|
2390
|
+
state.routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
2391
|
+
paintRoutes();
|
|
2392
|
+
};
|
|
2289
2393
|
const renderers = {
|
|
2290
2394
|
overview: renderOverview,
|
|
2291
2395
|
state: renderState,
|
|
@@ -2294,19 +2398,50 @@ function bootAdmin(config) {
|
|
|
2294
2398
|
journal: renderJournal,
|
|
2295
2399
|
routes: renderRoutes
|
|
2296
2400
|
};
|
|
2401
|
+
let refreshToken = 0;
|
|
2297
2402
|
async function refresh() {
|
|
2403
|
+
const token = ++refreshToken;
|
|
2298
2404
|
clearError();
|
|
2405
|
+
const main = el("views", HTMLElement);
|
|
2406
|
+
const sync = el("sync-status", HTMLElement);
|
|
2407
|
+
main.setAttribute("aria-busy", "true");
|
|
2408
|
+
sync.classList.add("busy");
|
|
2409
|
+
sync.textContent = "Updating";
|
|
2299
2410
|
const render = renderers[state.view];
|
|
2300
|
-
|
|
2301
|
-
|
|
2411
|
+
try {
|
|
2412
|
+
if (render)
|
|
2413
|
+
await render();
|
|
2414
|
+
if (token === refreshToken)
|
|
2415
|
+
sync.textContent = "Up to date";
|
|
2416
|
+
} finally {
|
|
2417
|
+
if (token === refreshToken) {
|
|
2418
|
+
main.removeAttribute("aria-busy");
|
|
2419
|
+
sync.classList.remove("busy");
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2302
2422
|
}
|
|
2303
2423
|
const delay = (ms) => new Promise((resolve2) => {
|
|
2304
2424
|
window.setTimeout(resolve2, ms);
|
|
2305
2425
|
});
|
|
2306
|
-
const confirmAction = async (button, pending, done, work) => {
|
|
2426
|
+
const confirmAction = async (button, pending, done, work, confirmLabel) => {
|
|
2307
2427
|
if (button.dataset.pending === "1")
|
|
2308
2428
|
return;
|
|
2309
2429
|
const label = button.textContent ?? "";
|
|
2430
|
+
if (confirmLabel !== void 0 && button.dataset.confirming !== "1") {
|
|
2431
|
+
button.dataset.confirming = "1";
|
|
2432
|
+
button.classList.add("confirming");
|
|
2433
|
+
button.textContent = confirmLabel;
|
|
2434
|
+
window.setTimeout(() => {
|
|
2435
|
+
if (button.dataset.confirming !== "1")
|
|
2436
|
+
return;
|
|
2437
|
+
button.textContent = label;
|
|
2438
|
+
button.classList.remove("confirming");
|
|
2439
|
+
delete button.dataset.confirming;
|
|
2440
|
+
}, 3e3);
|
|
2441
|
+
return;
|
|
2442
|
+
}
|
|
2443
|
+
button.classList.remove("confirming");
|
|
2444
|
+
delete button.dataset.confirming;
|
|
2310
2445
|
const width = button.offsetWidth;
|
|
2311
2446
|
button.dataset.pending = "1";
|
|
2312
2447
|
button.classList.add("pressed");
|
|
@@ -2327,6 +2462,65 @@ function bootAdmin(config) {
|
|
|
2327
2462
|
delete button.dataset.pending;
|
|
2328
2463
|
}
|
|
2329
2464
|
};
|
|
2465
|
+
const wireJsonEditor = (textareaId, highlightId, statusId, shellId, allowEmpty) => {
|
|
2466
|
+
const textarea = el(textareaId, HTMLTextAreaElement);
|
|
2467
|
+
const highlight = el(highlightId, HTMLElement);
|
|
2468
|
+
const status = el(statusId, HTMLElement);
|
|
2469
|
+
const shell = el(shellId, HTMLElement);
|
|
2470
|
+
const sync = () => {
|
|
2471
|
+
const text2 = textarea.value;
|
|
2472
|
+
highlight.innerHTML = `${highlightJsonText(text2)}
|
|
2473
|
+
`;
|
|
2474
|
+
let valid = true;
|
|
2475
|
+
let message = "Valid JSON";
|
|
2476
|
+
if (allowEmpty && text2.trim() === "")
|
|
2477
|
+
message = "No body";
|
|
2478
|
+
else {
|
|
2479
|
+
try {
|
|
2480
|
+
readJson2(text2);
|
|
2481
|
+
} catch {
|
|
2482
|
+
valid = false;
|
|
2483
|
+
message = "Invalid JSON";
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
shell.classList.toggle("invalid", !valid);
|
|
2487
|
+
status.classList.toggle("invalid", !valid);
|
|
2488
|
+
status.textContent = message;
|
|
2489
|
+
textarea.setAttribute("aria-invalid", valid ? "false" : "true");
|
|
2490
|
+
};
|
|
2491
|
+
const syncScroll = () => {
|
|
2492
|
+
const preview2 = highlight.parentElement;
|
|
2493
|
+
if (!(preview2 instanceof HTMLElement))
|
|
2494
|
+
return;
|
|
2495
|
+
preview2.scrollTop = textarea.scrollTop;
|
|
2496
|
+
preview2.scrollLeft = textarea.scrollLeft;
|
|
2497
|
+
};
|
|
2498
|
+
textarea.addEventListener("input", sync);
|
|
2499
|
+
textarea.addEventListener("scroll", syncScroll);
|
|
2500
|
+
sync();
|
|
2501
|
+
return sync;
|
|
2502
|
+
};
|
|
2503
|
+
const syncRouteEditor = wireJsonEditor("route-body", "route-highlight", "route-json-status", "route-editor", true);
|
|
2504
|
+
const syncRecordEditor = wireJsonEditor("editor-value", "editor-highlight", "editor-json-status", "record-editor", false);
|
|
2505
|
+
for (const button of document.querySelectorAll("[data-format]")) {
|
|
2506
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2507
|
+
continue;
|
|
2508
|
+
button.addEventListener("click", () => {
|
|
2509
|
+
const target = button.dataset.format;
|
|
2510
|
+
if (target === void 0)
|
|
2511
|
+
return;
|
|
2512
|
+
const textarea = document.getElementById(target);
|
|
2513
|
+
if (!(textarea instanceof HTMLTextAreaElement) || textarea.value.trim() === "")
|
|
2514
|
+
return;
|
|
2515
|
+
try {
|
|
2516
|
+
textarea.value = JSON.stringify(readJson2(textarea.value), null, 2) ?? "";
|
|
2517
|
+
textarea.dispatchEvent(new Event("input"));
|
|
2518
|
+
textarea.focus();
|
|
2519
|
+
} catch {
|
|
2520
|
+
showError(new Error("Fix the JSON before formatting it"));
|
|
2521
|
+
}
|
|
2522
|
+
});
|
|
2523
|
+
}
|
|
2330
2524
|
document.addEventListener("click", (event) => {
|
|
2331
2525
|
const target = event.target;
|
|
2332
2526
|
if (!(target instanceof Element))
|
|
@@ -2399,6 +2593,7 @@ function bootAdmin(config) {
|
|
|
2399
2593
|
} else {
|
|
2400
2594
|
editorValue.value = "{\n \n}";
|
|
2401
2595
|
}
|
|
2596
|
+
syncRecordEditor();
|
|
2402
2597
|
editor.showModal();
|
|
2403
2598
|
}
|
|
2404
2599
|
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -2428,10 +2623,10 @@ function bootAdmin(config) {
|
|
|
2428
2623
|
const id = state.editing;
|
|
2429
2624
|
if (!id)
|
|
2430
2625
|
return;
|
|
2431
|
-
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2626
|
+
confirmAction(editorDelete, "Deleting...", "Deleted", () => api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2432
2627
|
editor.close();
|
|
2433
2628
|
return renderState();
|
|
2434
|
-
}).catch(showError);
|
|
2629
|
+
}), "Confirm delete").catch(showError);
|
|
2435
2630
|
});
|
|
2436
2631
|
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2437
2632
|
if (!(button instanceof HTMLButtonElement))
|
|
@@ -2477,10 +2672,10 @@ function bootAdmin(config) {
|
|
|
2477
2672
|
}).then(() => renderFaults()).catch(showError);
|
|
2478
2673
|
});
|
|
2479
2674
|
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2480
|
-
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
|
|
2675
|
+
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults()), "Confirm clear").catch(showError);
|
|
2481
2676
|
});
|
|
2482
2677
|
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2483
|
-
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
|
|
2678
|
+
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal()), "Confirm clear").catch(showError);
|
|
2484
2679
|
});
|
|
2485
2680
|
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2486
2681
|
const target = event.target;
|
|
@@ -2494,9 +2689,15 @@ function bootAdmin(config) {
|
|
|
2494
2689
|
return;
|
|
2495
2690
|
state.route = route;
|
|
2496
2691
|
el("route-title", HTMLElement).textContent = route;
|
|
2692
|
+
el("route-send", HTMLButtonElement).disabled = false;
|
|
2497
2693
|
const method = route.split(" ")[0] ?? "";
|
|
2498
|
-
el("route-body", HTMLTextAreaElement)
|
|
2694
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
2695
|
+
routeBody.disabled = method === "GET" || method === "DELETE";
|
|
2696
|
+
el("route-editor", HTMLElement).hidden = routeBody.disabled;
|
|
2697
|
+
el("route-json-status", HTMLElement).parentElement?.toggleAttribute("hidden", routeBody.disabled);
|
|
2698
|
+
syncRouteEditor();
|
|
2499
2699
|
});
|
|
2700
|
+
el("route-search", HTMLInputElement).addEventListener("input", paintRoutes);
|
|
2500
2701
|
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2501
2702
|
event.preventDefault();
|
|
2502
2703
|
const send = async () => {
|
|
@@ -2518,7 +2719,8 @@ function bootAdmin(config) {
|
|
|
2518
2719
|
}
|
|
2519
2720
|
}
|
|
2520
2721
|
const data = await api(method, concrete, body);
|
|
2521
|
-
|
|
2722
|
+
const text2 = JSON.stringify(data, null, 2) ?? "null";
|
|
2723
|
+
el("route-result", HTMLElement).innerHTML = `<code>${highlightJsonText(text2)}</code>`;
|
|
2522
2724
|
};
|
|
2523
2725
|
send().catch(showError);
|
|
2524
2726
|
});
|
|
@@ -2636,11 +2838,13 @@ function bootAdmin(config) {
|
|
|
2636
2838
|
views.append(section);
|
|
2637
2839
|
mountSqlExplorer(body, panelApi);
|
|
2638
2840
|
}
|
|
2841
|
+
show(document.getElementById(`view-${state.view}`) === null ? "overview" : state.view);
|
|
2639
2842
|
return refresh();
|
|
2640
2843
|
}).catch(showError);
|
|
2641
2844
|
}
|
|
2642
2845
|
var clientPrelude = [
|
|
2643
2846
|
isRecord2,
|
|
2847
|
+
highlightJsonText,
|
|
2644
2848
|
mountAdminBrand,
|
|
2645
2849
|
startAdminBrand,
|
|
2646
2850
|
faultPresetList,
|
|
@@ -2663,6 +2867,7 @@ ${entry}(${payload});
|
|
|
2663
2867
|
var defineMock = (runtime) => runtime;
|
|
2664
2868
|
var STANDARD_ADMIN_ROUTES = [
|
|
2665
2869
|
"GET /",
|
|
2870
|
+
"GET /health",
|
|
2666
2871
|
"POST /reset",
|
|
2667
2872
|
"GET /namespaces",
|
|
2668
2873
|
"GET /clock",
|
|
@@ -2742,11 +2947,11 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
2742
2947
|
}
|
|
2743
2948
|
});
|
|
2744
2949
|
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
2745
|
-
var renderAdminDocument = (service) => {
|
|
2950
|
+
var renderAdminDocument = (service, adminPrefix = ADMIN_PREFIX) => {
|
|
2746
2951
|
const name = escapeHtml(service);
|
|
2747
2952
|
const script = adminClientSource({
|
|
2748
2953
|
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
2749
|
-
adminPrefix
|
|
2954
|
+
adminPrefix,
|
|
2750
2955
|
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
2751
2956
|
brandsUrl: ADMIN_BRANDS_URL,
|
|
2752
2957
|
service
|
|
@@ -2763,75 +2968,99 @@ var renderAdminDocument = (service) => {
|
|
|
2763
2968
|
${CSS_RESET}
|
|
2764
2969
|
:root {
|
|
2765
2970
|
color-scheme: light;
|
|
2766
|
-
--bg: #
|
|
2971
|
+
--bg: #f7f8fb;
|
|
2767
2972
|
--bg-elev: #ffffff;
|
|
2768
|
-
--bg-muted: #
|
|
2769
|
-
--bg-inset: #
|
|
2770
|
-
--line: #
|
|
2771
|
-
--line-strong: #
|
|
2772
|
-
--ink: #
|
|
2773
|
-
--muted: #
|
|
2774
|
-
--faint: #
|
|
2775
|
-
--accent: #
|
|
2973
|
+
--bg-muted: #f1f3f7;
|
|
2974
|
+
--bg-inset: #f8f9fc;
|
|
2975
|
+
--line: #e4e7ec;
|
|
2976
|
+
--line-strong: #cfd4dc;
|
|
2977
|
+
--ink: #101828;
|
|
2978
|
+
--muted: #475467;
|
|
2979
|
+
--faint: #667085;
|
|
2980
|
+
--accent: #6941c6;
|
|
2981
|
+
--accent-hover: #5933b4;
|
|
2776
2982
|
--accent-ink: #ffffff;
|
|
2777
|
-
--accent-soft: #
|
|
2778
|
-
--ok: #
|
|
2779
|
-
--ok-soft: #
|
|
2780
|
-
--warn: #
|
|
2781
|
-
--warn-soft: #
|
|
2782
|
-
--err: #
|
|
2783
|
-
--err-soft: #
|
|
2784
|
-
--
|
|
2983
|
+
--accent-soft: #f4f0ff;
|
|
2984
|
+
--ok: #067647;
|
|
2985
|
+
--ok-soft: #ecfdf3;
|
|
2986
|
+
--warn: #b54708;
|
|
2987
|
+
--warn-soft: #fffaeb;
|
|
2988
|
+
--err: #b42318;
|
|
2989
|
+
--err-soft: #fef3f2;
|
|
2990
|
+
--code-key: #6941c6;
|
|
2991
|
+
--code-string: #067647;
|
|
2992
|
+
--code-number: #175cd3;
|
|
2993
|
+
--code-boolean: #c11574;
|
|
2994
|
+
--code-null: #667085;
|
|
2995
|
+
--shadow-xs: 0 1px 2px rgb(16 24 40 / 0.05);
|
|
2996
|
+
--shadow-sm: 0 1px 3px rgb(16 24 40 / 0.08), 0 1px 2px rgb(16 24 40 / 0.04);
|
|
2997
|
+
--shadow-lg: 0 18px 48px rgb(16 24 40 / 0.16), 0 4px 12px rgb(16 24 40 / 0.08);
|
|
2785
2998
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2786
2999
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
2787
|
-
--radius:
|
|
2788
|
-
--header:
|
|
2789
|
-
--nav:
|
|
3000
|
+
--radius: 12px;
|
|
3001
|
+
--header: 72px;
|
|
3002
|
+
--nav: 224px;
|
|
2790
3003
|
}
|
|
2791
3004
|
:root[data-theme="dark"] {
|
|
2792
3005
|
color-scheme: dark;
|
|
2793
|
-
--bg: #
|
|
2794
|
-
--bg-elev: #
|
|
2795
|
-
--bg-muted: #
|
|
2796
|
-
--bg-inset: #
|
|
2797
|
-
--line: #
|
|
2798
|
-
--line-strong: #
|
|
2799
|
-
--ink: #
|
|
2800
|
-
--muted: #
|
|
2801
|
-
--faint: #
|
|
2802
|
-
--accent: #
|
|
2803
|
-
--accent-
|
|
2804
|
-
--accent-
|
|
2805
|
-
--
|
|
2806
|
-
--ok
|
|
2807
|
-
--
|
|
2808
|
-
--warn
|
|
2809
|
-
--
|
|
2810
|
-
--err
|
|
2811
|
-
--
|
|
3006
|
+
--bg: #0c0e14;
|
|
3007
|
+
--bg-elev: #151821;
|
|
3008
|
+
--bg-muted: #20232e;
|
|
3009
|
+
--bg-inset: #11141c;
|
|
3010
|
+
--line: #2b303d;
|
|
3011
|
+
--line-strong: #414858;
|
|
3012
|
+
--ink: #f5f7fa;
|
|
3013
|
+
--muted: #b6bdc9;
|
|
3014
|
+
--faint: #8d96a7;
|
|
3015
|
+
--accent: #9e77ed;
|
|
3016
|
+
--accent-hover: #b692f6;
|
|
3017
|
+
--accent-ink: #ffffff;
|
|
3018
|
+
--accent-soft: #2b2142;
|
|
3019
|
+
--ok: #75e0a7;
|
|
3020
|
+
--ok-soft: #102a20;
|
|
3021
|
+
--warn: #fec84b;
|
|
3022
|
+
--warn-soft: #302611;
|
|
3023
|
+
--err: #fda29b;
|
|
3024
|
+
--err-soft: #321817;
|
|
3025
|
+
--code-key: #c3a6ff;
|
|
3026
|
+
--code-string: #75e0a7;
|
|
3027
|
+
--code-number: #84adff;
|
|
3028
|
+
--code-boolean: #f9a8d4;
|
|
3029
|
+
--code-null: #98a2b3;
|
|
3030
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3031
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3032
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2812
3033
|
}
|
|
2813
3034
|
@media (prefers-color-scheme: dark) {
|
|
2814
3035
|
:root:not([data-theme="light"]) {
|
|
2815
3036
|
color-scheme: dark;
|
|
2816
|
-
--bg: #
|
|
2817
|
-
--bg-elev: #
|
|
2818
|
-
--bg-muted: #
|
|
2819
|
-
--bg-inset: #
|
|
2820
|
-
--line: #
|
|
2821
|
-
--line-strong: #
|
|
2822
|
-
--ink: #
|
|
2823
|
-
--muted: #
|
|
2824
|
-
--faint: #
|
|
2825
|
-
--accent: #
|
|
2826
|
-
--accent-
|
|
2827
|
-
--accent-
|
|
2828
|
-
--
|
|
2829
|
-
--ok
|
|
2830
|
-
--
|
|
2831
|
-
--warn
|
|
2832
|
-
--
|
|
2833
|
-
--err
|
|
2834
|
-
--
|
|
3037
|
+
--bg: #0c0e14;
|
|
3038
|
+
--bg-elev: #151821;
|
|
3039
|
+
--bg-muted: #20232e;
|
|
3040
|
+
--bg-inset: #11141c;
|
|
3041
|
+
--line: #2b303d;
|
|
3042
|
+
--line-strong: #414858;
|
|
3043
|
+
--ink: #f5f7fa;
|
|
3044
|
+
--muted: #b6bdc9;
|
|
3045
|
+
--faint: #8d96a7;
|
|
3046
|
+
--accent: #9e77ed;
|
|
3047
|
+
--accent-hover: #b692f6;
|
|
3048
|
+
--accent-ink: #ffffff;
|
|
3049
|
+
--accent-soft: #2b2142;
|
|
3050
|
+
--ok: #75e0a7;
|
|
3051
|
+
--ok-soft: #102a20;
|
|
3052
|
+
--warn: #fec84b;
|
|
3053
|
+
--warn-soft: #302611;
|
|
3054
|
+
--err: #fda29b;
|
|
3055
|
+
--err-soft: #321817;
|
|
3056
|
+
--code-key: #c3a6ff;
|
|
3057
|
+
--code-string: #75e0a7;
|
|
3058
|
+
--code-number: #84adff;
|
|
3059
|
+
--code-boolean: #f9a8d4;
|
|
3060
|
+
--code-null: #98a2b3;
|
|
3061
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
3062
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
3063
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2835
3064
|
}
|
|
2836
3065
|
}
|
|
2837
3066
|
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
@@ -2849,13 +3078,17 @@ var renderAdminDocument = (service) => {
|
|
|
2849
3078
|
padding-right: env(safe-area-inset-right);
|
|
2850
3079
|
-webkit-text-size-adjust: 100%;
|
|
2851
3080
|
}
|
|
2852
|
-
|
|
3081
|
+
::selection { background: color-mix(in srgb, var(--accent) 28%, transparent); }
|
|
3082
|
+
:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px; }
|
|
3083
|
+
button, input, select, textarea { transition: border-color 140ms ease, box-shadow 140ms ease, transform 100ms ease; }
|
|
3084
|
+
button:disabled, input:disabled, select:disabled, textarea:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
2853
3085
|
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
2854
3086
|
header.top {
|
|
2855
|
-
position: sticky; top: 0; z-index:
|
|
3087
|
+
position: sticky; top: 0; z-index: 20;
|
|
2856
3088
|
display: flex; align-items: center; gap: 12px;
|
|
2857
|
-
min-height: var(--header); padding:
|
|
2858
|
-
background: var(--bg-elev);
|
|
3089
|
+
min-height: var(--header); padding: 12px 20px;
|
|
3090
|
+
background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
|
|
3091
|
+
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
2859
3092
|
border-bottom: 1px solid var(--line);
|
|
2860
3093
|
min-width: 0; max-width: 100%;
|
|
2861
3094
|
}
|
|
@@ -2863,12 +3096,21 @@ var renderAdminDocument = (service) => {
|
|
|
2863
3096
|
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
2864
3097
|
min-width: 0; max-width: 100%; margin-right: auto;
|
|
2865
3098
|
}
|
|
2866
|
-
.brand-
|
|
2867
|
-
.brand-
|
|
3099
|
+
.brand-lockup { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
3100
|
+
.brand-mark {
|
|
3101
|
+
width: 38px; height: 38px; display: grid; place-items: center; flex: none;
|
|
3102
|
+
border-radius: 11px; color: #fff;
|
|
3103
|
+
background: linear-gradient(145deg, #7f56d9 0%, #53389e 100%);
|
|
3104
|
+
box-shadow: 0 6px 16px rgb(105 65 198 / 0.24), inset 0 1px 0 rgb(255 255 255 / 0.25);
|
|
3105
|
+
}
|
|
3106
|
+
.brand-mark svg { width: 22px; height: 22px; }
|
|
3107
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; line-height: 1.25; }
|
|
3108
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
2868
3109
|
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
2869
3110
|
.vendor {
|
|
2870
3111
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
2871
3112
|
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
3113
|
+
padding: 5px 8px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-inset);
|
|
2872
3114
|
}
|
|
2873
3115
|
.vendor[hidden] { display: none; }
|
|
2874
3116
|
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
@@ -2880,12 +3122,17 @@ var renderAdminDocument = (service) => {
|
|
|
2880
3122
|
.vendor-sep { color: var(--faint); }
|
|
2881
3123
|
.vendor-link { color: inherit; text-decoration: none; }
|
|
2882
3124
|
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
2883
|
-
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items:
|
|
2884
|
-
|
|
3125
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
|
|
3126
|
+
.sync-status { align-self: center; display: flex; align-items: center; gap: 6px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3127
|
+
.sync-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
|
|
3128
|
+
.sync-status.busy::before { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); animation: pulse 1.2s ease-in-out infinite; }
|
|
3129
|
+
label.field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; font-weight: 600; color: var(--faint); }
|
|
2885
3130
|
label.field input, label.field select, .controls input[type="password"] {
|
|
2886
|
-
background-color: var(--bg-
|
|
2887
|
-
padding:
|
|
3131
|
+
background-color: var(--bg-elev); border: 1px solid var(--line-strong); border-radius: 9px;
|
|
3132
|
+
padding: 8px 10px; min-height: 38px; line-height: 1.2; box-shadow: var(--shadow-xs);
|
|
2888
3133
|
}
|
|
3134
|
+
label.field input:hover, label.field select:hover, textarea:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--line-strong)); }
|
|
3135
|
+
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; }
|
|
2889
3136
|
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
2890
3137
|
the text fields in the same form. Paint it like those fields. */
|
|
2891
3138
|
label.field select {
|
|
@@ -2902,12 +3149,20 @@ var renderAdminDocument = (service) => {
|
|
|
2902
3149
|
dialog .field > textarea { width: 100%; }
|
|
2903
3150
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
2904
3151
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
.btn
|
|
3152
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
|
3153
|
+
border-radius: 9px; min-height: 38px; padding: 0 13px; border: 1px solid var(--line-strong);
|
|
3154
|
+
background: var(--bg-elev); color: var(--ink); font-size: 13px; font-weight: 600; line-height: 1;
|
|
3155
|
+
box-shadow: var(--shadow-xs);
|
|
3156
|
+
}
|
|
3157
|
+
.icon-btn { width: 38px; padding: 0; }
|
|
3158
|
+
.icon-btn svg, .btn svg, .btn-primary svg { width: 16px; height: 16px; }
|
|
3159
|
+
.icon-btn:hover, .btn:hover { background: var(--bg-muted); border-color: var(--line-strong); }
|
|
3160
|
+
.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; }
|
|
3161
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
3162
|
+
.btn-quiet { background: transparent; border-color: transparent; box-shadow: none; }
|
|
3163
|
+
.btn-quiet:hover { background: var(--bg-muted); }
|
|
3164
|
+
.btn-danger { color: var(--err); border-color: color-mix(in srgb, var(--err) 25%, var(--line)); }
|
|
3165
|
+
.btn-danger:hover, .btn-danger.confirming { background: var(--err-soft); border-color: color-mix(in srgb, var(--err) 45%, var(--line)); }
|
|
2911
3166
|
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
2912
3167
|
so the press itself has to stay visible. */
|
|
2913
3168
|
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
@@ -2923,93 +3178,159 @@ var renderAdminDocument = (service) => {
|
|
|
2923
3178
|
flex: 1 0 auto; min-width: 0;
|
|
2924
3179
|
}
|
|
2925
3180
|
nav.side {
|
|
2926
|
-
|
|
3181
|
+
position: sticky; top: var(--header); align-self: start; height: calc(100vh - var(--header));
|
|
3182
|
+
border-right: 1px solid var(--line); padding: 18px 12px;
|
|
2927
3183
|
display: flex; flex-direction: column; gap: 4px;
|
|
2928
|
-
background: var(--bg-elev);
|
|
3184
|
+
background: var(--bg-elev); overflow-y: auto;
|
|
2929
3185
|
}
|
|
3186
|
+
.nav-label { padding: 0 10px 8px; color: var(--faint); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
|
|
2930
3187
|
nav.side button {
|
|
3188
|
+
position: relative; display: flex; align-items: center; gap: 10px; width: 100%;
|
|
2931
3189
|
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
2932
|
-
padding: 9px 10px; min-height:
|
|
2933
|
-
}
|
|
2934
|
-
nav.side button
|
|
2935
|
-
|
|
2936
|
-
.
|
|
2937
|
-
.
|
|
2938
|
-
.
|
|
3190
|
+
padding: 9px 10px; min-height: 42px; color: var(--muted); font-size: 13px; font-weight: 550;
|
|
3191
|
+
}
|
|
3192
|
+
nav.side button:hover { background: var(--bg-muted); color: var(--ink); }
|
|
3193
|
+
nav.side button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 650; }
|
|
3194
|
+
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); }
|
|
3195
|
+
.nav-icon { width: 18px; height: 18px; display: grid; place-items: center; flex: none; }
|
|
3196
|
+
.nav-icon svg { width: 18px; height: 18px; }
|
|
3197
|
+
main { padding: 32px clamp(20px, 4vw, 48px) 64px; max-width: 1320px; width: 100%; min-width: 0; }
|
|
3198
|
+
main[aria-busy="true"] .view { opacity: 0.62; }
|
|
3199
|
+
.view { animation: view-in 180ms ease-out; }
|
|
3200
|
+
.view[hidden] { display: none; }
|
|
3201
|
+
.view-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
|
|
3202
|
+
.view h2 { margin: 0 0 5px; font-size: clamp(24px, 3vw, 30px); line-height: 1.2; letter-spacing: -0.035em; }
|
|
3203
|
+
.view h3 { margin: 24px 0 10px; font-size: 15px; letter-spacing: -0.01em; }
|
|
3204
|
+
.lede { margin: 0; color: var(--muted); max-width: 72ch; font-size: 14px; }
|
|
3205
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 175px), 1fr)); gap: 14px; }
|
|
2939
3206
|
.card {
|
|
2940
3207
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
2941
|
-
padding:
|
|
3208
|
+
padding: 18px; box-shadow: var(--shadow-sm);
|
|
2942
3209
|
}
|
|
2943
|
-
.card
|
|
2944
|
-
.card
|
|
3210
|
+
.stat-card { position: relative; overflow: hidden; }
|
|
3211
|
+
.stat-card::after { content: ""; position: absolute; inset: 0 0 auto; height: 2px; background: linear-gradient(90deg, var(--accent), transparent 78%); opacity: 0.7; }
|
|
3212
|
+
.card .k { color: var(--faint); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
3213
|
+
.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; }
|
|
2945
3214
|
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
2946
|
-
.
|
|
3215
|
+
.section-card { background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-xs); overflow: hidden; }
|
|
3216
|
+
.section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
|
|
3217
|
+
.section-title h3 { margin: 0; }
|
|
3218
|
+
.split { display: grid; grid-template-columns: minmax(0, 250px) minmax(0, 1fr); gap: 18px; align-items: start; }
|
|
3219
|
+
#collections { padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); box-shadow: var(--shadow-xs); }
|
|
2947
3220
|
.collection {
|
|
2948
3221
|
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
2949
|
-
border-radius:
|
|
3222
|
+
border-radius: 9px; padding: 10px 11px; min-height: 48px; color: var(--ink); cursor: pointer;
|
|
2950
3223
|
}
|
|
2951
|
-
.collection
|
|
3224
|
+
.collection:hover { background: var(--bg-muted); }
|
|
3225
|
+
.collection[aria-current="true"] { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 18%, var(--line)); color: var(--accent); }
|
|
2952
3226
|
.collection small { display: block; color: var(--faint); }
|
|
2953
3227
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
2954
|
-
.chip {
|
|
2955
|
-
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
2956
|
-
border-radius: 999px; padding: 4px 8px;
|
|
2957
|
-
}
|
|
3228
|
+
.chip { font-family: var(--mono); font-size: 11px; background: var(--bg-muted); border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; }
|
|
2958
3229
|
.chip i { color: var(--faint); font-style: normal; }
|
|
2959
3230
|
.sql-layout {
|
|
2960
3231
|
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
2961
3232
|
}
|
|
2962
|
-
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
2963
|
-
.sql-tables button { text-align: left; }
|
|
3233
|
+
.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); }
|
|
3234
|
+
.sql-tables button { text-align: left; border: 0; border-radius: 8px; padding: 9px 10px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
3235
|
+
.sql-tables button:hover, .sql-tables button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); }
|
|
2964
3236
|
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
2965
|
-
table { width: 100%; border-
|
|
2966
|
-
|
|
2967
|
-
th {
|
|
3237
|
+
.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); }
|
|
3238
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
3239
|
+
th, td { text-align: left; padding: 11px 13px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
3240
|
+
th { color: var(--faint); background: var(--bg-inset); font-weight: 650; font-size: 11px; text-transform: uppercase; letter-spacing: 0.045em; }
|
|
3241
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
2968
3242
|
tr.clickable { cursor: pointer; }
|
|
2969
|
-
tr.clickable:hover { background: var(--
|
|
3243
|
+
tr.clickable:hover { background: var(--accent-soft); }
|
|
2970
3244
|
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
2971
3245
|
pre {
|
|
2972
3246
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
2973
|
-
background: var(--bg-inset); border-radius: 10px; padding:
|
|
2974
|
-
}
|
|
2975
|
-
.
|
|
3247
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 10px; padding: 14px; font-size: 12px; line-height: 1.65;
|
|
3248
|
+
}
|
|
3249
|
+
.json-key { color: var(--code-key); }
|
|
3250
|
+
.json-string { color: var(--code-string); }
|
|
3251
|
+
.json-number { color: var(--code-number); }
|
|
3252
|
+
.json-boolean { color: var(--code-boolean); }
|
|
3253
|
+
.json-null { color: var(--code-null); font-style: italic; }
|
|
3254
|
+
.json-inline { display: block; max-width: 44rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
3255
|
+
.json-view { position: relative; max-height: 26rem; overflow: auto; }
|
|
3256
|
+
.json-editor { position: relative; min-height: 190px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--bg-inset); overflow: hidden; }
|
|
3257
|
+
.json-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); }
|
|
3258
|
+
.json-editor.invalid { border-color: var(--err); }
|
|
3259
|
+
.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; }
|
|
3260
|
+
.json-editor pre { position: absolute; inset: 0; pointer-events: none; background: transparent; }
|
|
3261
|
+
.json-editor textarea { position: relative; z-index: 1; resize: vertical; background: transparent; color: transparent; caret-color: var(--ink); -webkit-text-fill-color: transparent; }
|
|
3262
|
+
.json-editor textarea::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); }
|
|
3263
|
+
.editor-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; }
|
|
3264
|
+
.json-status { display: inline-flex; align-items: center; gap: 6px; color: var(--ok); font-size: 11px; }
|
|
3265
|
+
.json-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
3266
|
+
.json-status.invalid { color: var(--err); }
|
|
3267
|
+
.format-json { border: 0; padding: 3px 5px; background: transparent; color: var(--accent); font-size: 11px; font-weight: 650; cursor: pointer; }
|
|
3268
|
+
.row-actions { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 14px 0; }
|
|
2976
3269
|
.banner {
|
|
2977
|
-
display: none;
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
3270
|
+
display: none; position: sticky; top: calc(var(--header) + 12px); z-index: 15;
|
|
3271
|
+
align-items: center; gap: 10px; margin: 0 0 16px; padding: 11px 12px 11px 14px;
|
|
3272
|
+
border: 1px solid color-mix(in srgb, var(--err) 30%, var(--line)); border-radius: 10px;
|
|
3273
|
+
background: var(--err-soft); color: var(--err); box-shadow: var(--shadow-sm); font-size: 13px;
|
|
3274
|
+
}
|
|
3275
|
+
.banner.show { display: flex; }
|
|
3276
|
+
.banner span { flex: 1; }
|
|
3277
|
+
.banner button { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 7px; background: transparent; color: currentColor; cursor: pointer; }
|
|
3278
|
+
.banner button:hover { background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
3279
|
+
.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); }
|
|
2982
3280
|
dialog {
|
|
2983
|
-
border: 1px solid var(--line); border-radius:
|
|
2984
|
-
color: var(--ink); width: min(
|
|
2985
|
-
}
|
|
2986
|
-
dialog
|
|
2987
|
-
dialog
|
|
3281
|
+
border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--bg-elev);
|
|
3282
|
+
color: var(--ink); width: min(720px, calc(100% - 24px)); max-width: calc(100vw - 24px); box-shadow: var(--shadow-lg);
|
|
3283
|
+
}
|
|
3284
|
+
dialog[open] { animation: dialog-in 160ms ease-out; }
|
|
3285
|
+
dialog::backdrop { background: rgb(12 14 20 / 0.58); backdrop-filter: blur(3px); }
|
|
3286
|
+
dialog form, .dialog-body { padding: 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
3287
|
+
.dialog-heading { display: flex; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
|
3288
|
+
.dialog-heading strong { font-size: 18px; letter-spacing: -0.02em; }
|
|
3289
|
+
.dialog-heading span { color: var(--muted); font-size: 12px; }
|
|
2988
3290
|
textarea {
|
|
2989
3291
|
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
2990
3292
|
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
2991
3293
|
font-family: var(--mono); font-size: 12px;
|
|
2992
3294
|
}
|
|
2993
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
2994
|
-
.
|
|
3295
|
+
.dialog-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding-top: 4px; }
|
|
3296
|
+
.dialog-actions .btn-danger { margin-right: auto; }
|
|
3297
|
+
.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); }
|
|
2995
3298
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
2996
3299
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
2997
3300
|
.status-2 { color: var(--ok); }
|
|
2998
3301
|
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
2999
3302
|
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
3303
|
+
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
3304
|
+
.form-grid .wide { grid-column: span 2; }
|
|
3000
3305
|
.route {
|
|
3001
|
-
display: grid; grid-template-columns:
|
|
3002
|
-
padding:
|
|
3003
|
-
}
|
|
3004
|
-
.
|
|
3306
|
+
display: grid; grid-template-columns: 84px minmax(0, 1fr) auto; gap: 10px; align-items: center;
|
|
3307
|
+
padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--bg-elev);
|
|
3308
|
+
}
|
|
3309
|
+
.route:first-child { border-radius: var(--radius) var(--radius) 0 0; }
|
|
3310
|
+
.route:last-child { border-bottom: 0; border-radius: 0 0 var(--radius) var(--radius); }
|
|
3311
|
+
.route-tools { display: flex; align-items: flex-end; gap: 12px; margin-bottom: 12px; }
|
|
3312
|
+
.route-tools .field { flex: 1; }
|
|
3313
|
+
.route-tools input { width: 100%; }
|
|
3314
|
+
.route-count { padding-bottom: 10px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3315
|
+
#route-list { max-height: 34rem; border: 1px solid var(--line); border-radius: var(--radius); overflow: auto; box-shadow: var(--shadow-xs); }
|
|
3316
|
+
.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; }
|
|
3317
|
+
@keyframes view-in { from { transform: translateY(3px); } to { transform: translateY(0); } }
|
|
3318
|
+
@keyframes dialog-in { from { transform: translateY(8px) scale(0.99); } to { transform: translateY(0) scale(1); } }
|
|
3319
|
+
@keyframes pulse { 50% { opacity: 0.45; } }
|
|
3320
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
|
|
3321
|
+
@media (forced-colors: active) { .json-editor pre { display: none; } .json-editor textarea { color: CanvasText; -webkit-text-fill-color: CanvasText; } }
|
|
3005
3322
|
@media (max-width: 800px) {
|
|
3323
|
+
:root { --header: 0px; }
|
|
3006
3324
|
header.top {
|
|
3007
|
-
align-items: stretch; flex-direction: column;
|
|
3008
|
-
padding
|
|
3325
|
+
position: relative; align-items: stretch; flex-direction: column;
|
|
3326
|
+
padding: max(12px, env(safe-area-inset-top)) 14px 12px;
|
|
3009
3327
|
}
|
|
3010
3328
|
input, select, textarea { font-size: 16px; }
|
|
3011
3329
|
.brand, .vendor, .controls { width: 100%; }
|
|
3330
|
+
.brand { gap: 10px; }
|
|
3331
|
+
.vendor { order: 2; border-radius: 9px; }
|
|
3012
3332
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
3333
|
+
.sync-status { display: none; }
|
|
3013
3334
|
/* The layout fills the screen. A single-column grid stretches every auto
|
|
3014
3335
|
row, so the tab bar grew with the leftover height and the buttons floated
|
|
3015
3336
|
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
@@ -3019,14 +3340,23 @@ var renderAdminDocument = (service) => {
|
|
|
3019
3340
|
align-content: start;
|
|
3020
3341
|
}
|
|
3021
3342
|
nav.side {
|
|
3022
|
-
align-self: start; height: auto;
|
|
3343
|
+
position: sticky; top: 0; z-index: 12; align-self: start; height: auto;
|
|
3023
3344
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
3024
3345
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
3025
|
-
padding: 8px;
|
|
3346
|
+
padding: 8px; box-shadow: var(--shadow-xs);
|
|
3026
3347
|
}
|
|
3027
|
-
nav
|
|
3348
|
+
.nav-label { display: none; }
|
|
3349
|
+
nav.side button { width: auto; white-space: nowrap; flex: none; }
|
|
3350
|
+
nav.side button[aria-current="true"]::before { inset: auto 9px -8px; width: auto; height: 3px; border-radius: 3px 3px 0 0; }
|
|
3028
3351
|
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
3029
|
-
|
|
3352
|
+
.form-grid { grid-template-columns: minmax(0, 1fr); }
|
|
3353
|
+
.form-grid .wide { grid-column: auto; }
|
|
3354
|
+
main { padding: 22px 14px 48px; }
|
|
3355
|
+
.view-head { margin-bottom: 18px; }
|
|
3356
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
3357
|
+
.card { padding: 14px; }
|
|
3358
|
+
.card .v { font-size: 20px; }
|
|
3359
|
+
.table-shell { border: 0; background: transparent; box-shadow: none; overflow: visible; }
|
|
3030
3360
|
table, thead, tbody, tr, th, td { display: block; }
|
|
3031
3361
|
thead { display: none; }
|
|
3032
3362
|
tr {
|
|
@@ -3041,50 +3371,63 @@ var renderAdminDocument = (service) => {
|
|
|
3041
3371
|
.route { grid-template-columns: 72px 1fr; }
|
|
3042
3372
|
.route button { grid-column: 1 / -1; }
|
|
3043
3373
|
}
|
|
3374
|
+
@media (max-width: 460px) {
|
|
3375
|
+
.controls label.field { min-width: calc(50% - 4px); }
|
|
3376
|
+
.controls .icon-btn, .controls .btn { flex: 1; }
|
|
3377
|
+
.grid { grid-template-columns: minmax(0, 1fr); }
|
|
3378
|
+
.dialog-actions { flex-wrap: wrap; }
|
|
3379
|
+
.dialog-actions button { flex: 1; }
|
|
3380
|
+
.dialog-actions .btn-danger { flex-basis: 100%; margin-right: 0; }
|
|
3381
|
+
}
|
|
3044
3382
|
</style>
|
|
3045
3383
|
</head>
|
|
3046
3384
|
<body>
|
|
3047
3385
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
3048
3386
|
<header class="top">
|
|
3049
3387
|
<div class="brand">
|
|
3050
|
-
<div class="brand-
|
|
3051
|
-
<
|
|
3052
|
-
|
|
3388
|
+
<div class="brand-lockup">
|
|
3389
|
+
<div class="brand-mark" aria-hidden="true">
|
|
3390
|
+
<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>
|
|
3391
|
+
</div>
|
|
3392
|
+
<div class="brand-id">
|
|
3393
|
+
<strong>${name}</strong>
|
|
3394
|
+
<span>Mock admin</span>
|
|
3395
|
+
</div>
|
|
3053
3396
|
</div>
|
|
3054
3397
|
<div class="vendor" id="vendor" hidden></div>
|
|
3055
3398
|
</div>
|
|
3056
3399
|
<div class="controls">
|
|
3400
|
+
<span class="sync-status" id="sync-status" role="status" aria-live="polite">Ready</span>
|
|
3057
3401
|
<label class="field">Namespace
|
|
3058
3402
|
<select id="namespace" aria-label="Namespace"></select>
|
|
3059
3403
|
</label>
|
|
3060
3404
|
<label class="field">Admin key
|
|
3061
3405
|
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
3062
3406
|
</label>
|
|
3063
|
-
<button class="icon-btn" id="theme" type="button" aria-label="
|
|
3064
|
-
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
3407
|
+
<button class="icon-btn" id="theme" type="button" aria-label="Use dark theme" title="Use dark theme"></button>
|
|
3408
|
+
<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>
|
|
3065
3409
|
</div>
|
|
3066
3410
|
</header>
|
|
3067
3411
|
<div class="layout">
|
|
3068
3412
|
<nav class="side" id="nav" aria-label="Admin sections">
|
|
3069
|
-
<
|
|
3070
|
-
<button type="button" data-view="
|
|
3071
|
-
<button type="button" data-view="
|
|
3072
|
-
<button type="button" data-view="
|
|
3073
|
-
<button type="button" data-view="
|
|
3074
|
-
<button type="button" data-view="
|
|
3413
|
+
<span class="nav-label">Workspace</span>
|
|
3414
|
+
<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>
|
|
3415
|
+
<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>
|
|
3416
|
+
<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>
|
|
3417
|
+
<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>
|
|
3418
|
+
<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>
|
|
3419
|
+
<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>
|
|
3075
3420
|
</nav>
|
|
3076
3421
|
<main id="views">
|
|
3077
|
-
<
|
|
3422
|
+
<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>
|
|
3078
3423
|
<section class="view" id="view-overview">
|
|
3079
|
-
<h2>Overview</h2>
|
|
3080
|
-
<p class="lede">Health, clock, and the collections this namespace is holding.</p>
|
|
3424
|
+
<div class="view-head"><div><h2>Overview</h2><p class="lede">Health, clock, and the collections this namespace is holding.</p></div></div>
|
|
3081
3425
|
<div class="grid" id="overview-cards"></div>
|
|
3082
3426
|
<h3>Recent requests</h3>
|
|
3083
3427
|
<div id="overview-requests"></div>
|
|
3084
3428
|
</section>
|
|
3085
3429
|
<section class="view" id="view-state" hidden>
|
|
3086
|
-
<h2>State</h2>
|
|
3087
|
-
<p class="lede">Every collection in this namespace. Declared fields stay visible before any row exists; stored rows add whatever else they contain.</p>
|
|
3430
|
+
<div class="view-head"><div><h2>State</h2><p class="lede">Inspect and edit every collection in this namespace.</p></div></div>
|
|
3088
3431
|
<div class="split">
|
|
3089
3432
|
<div id="collections"></div>
|
|
3090
3433
|
<div>
|
|
@@ -3097,8 +3440,7 @@ var renderAdminDocument = (service) => {
|
|
|
3097
3440
|
</div>
|
|
3098
3441
|
</section>
|
|
3099
3442
|
<section class="view" id="view-clock" hidden>
|
|
3100
|
-
<h2>Clock</h2>
|
|
3101
|
-
<p class="lede">The clock every timestamp in this mock reads.</p>
|
|
3443
|
+
<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>
|
|
3102
3444
|
<div class="card" id="clock-readout"></div>
|
|
3103
3445
|
<div class="row-actions">
|
|
3104
3446
|
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
@@ -3117,21 +3459,22 @@ var renderAdminDocument = (service) => {
|
|
|
3117
3459
|
</form>
|
|
3118
3460
|
</section>
|
|
3119
3461
|
<section class="view" id="view-faults" hidden>
|
|
3120
|
-
<h2>Faults</h2>
|
|
3121
|
-
<p class="lede">Inject a response, a delay, or a named preset. Rules apply to this namespace.</p>
|
|
3462
|
+
<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>
|
|
3122
3463
|
<form id="fault-form" class="card stack">
|
|
3123
|
-
<
|
|
3124
|
-
<
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
<
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
<
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
<
|
|
3134
|
-
|
|
3464
|
+
<div class="form-grid">
|
|
3465
|
+
<label class="field wide">Preset
|
|
3466
|
+
<select id="fault-preset"><option value="">Custom rule</option></select>
|
|
3467
|
+
</label>
|
|
3468
|
+
<label class="field">Status
|
|
3469
|
+
<input id="fault-status" inputmode="numeric" placeholder="503">
|
|
3470
|
+
</label>
|
|
3471
|
+
<label class="field wide">Path prefix
|
|
3472
|
+
<input id="fault-path" placeholder="/v1">
|
|
3473
|
+
</label>
|
|
3474
|
+
<label class="field">Method
|
|
3475
|
+
<input id="fault-method" placeholder="GET">
|
|
3476
|
+
</label>
|
|
3477
|
+
</div>
|
|
3135
3478
|
<div class="dialog-actions">
|
|
3136
3479
|
<button class="btn-primary" type="submit">Add fault</button>
|
|
3137
3480
|
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
@@ -3140,26 +3483,31 @@ var renderAdminDocument = (service) => {
|
|
|
3140
3483
|
<div id="fault-list"></div>
|
|
3141
3484
|
</section>
|
|
3142
3485
|
<section class="view" id="view-journal" hidden>
|
|
3143
|
-
<h2>Journal</h2>
|
|
3144
|
-
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
3145
|
-
<div class="row-actions">
|
|
3146
|
-
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
3147
|
-
</div>
|
|
3486
|
+
<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>
|
|
3148
3487
|
<div id="journal"></div>
|
|
3149
3488
|
</section>
|
|
3150
3489
|
<section class="view" id="view-routes" hidden>
|
|
3151
|
-
<h2>Routes</h2>
|
|
3152
|
-
<
|
|
3490
|
+
<div class="view-head"><div><h2>Routes</h2><p class="lede">Explore shared controls and service-specific admin endpoints.</p></div></div>
|
|
3491
|
+
<div class="route-tools">
|
|
3492
|
+
<label class="field">Filter routes
|
|
3493
|
+
<input id="route-search" type="search" placeholder="Method, path, or extension" autocomplete="off">
|
|
3494
|
+
</label>
|
|
3495
|
+
<span class="route-count" id="route-count"></span>
|
|
3496
|
+
</div>
|
|
3153
3497
|
<div id="route-list"></div>
|
|
3154
|
-
<form id="route-form" class="card stack">
|
|
3498
|
+
<form id="route-form" class="card stack" style="margin-top: 18px">
|
|
3155
3499
|
<strong id="route-title">Select a route</strong>
|
|
3156
3500
|
<label class="field">JSON body
|
|
3157
|
-
<
|
|
3501
|
+
<div class="json-editor" id="route-editor" hidden>
|
|
3502
|
+
<pre aria-hidden="true"><code id="route-highlight"></code></pre>
|
|
3503
|
+
<textarea id="route-body" placeholder="{ }" spellcheck="false" aria-describedby="route-json-status" disabled></textarea>
|
|
3504
|
+
</div>
|
|
3505
|
+
<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>
|
|
3158
3506
|
</label>
|
|
3159
3507
|
<div class="dialog-actions">
|
|
3160
|
-
<button class="btn-primary" type="submit">Send</button>
|
|
3508
|
+
<button class="btn-primary" id="route-send" type="submit" disabled>Send request</button>
|
|
3161
3509
|
</div>
|
|
3162
|
-
<pre id="route-result">Response will show here.</pre>
|
|
3510
|
+
<pre class="json-view" id="route-result"><code>Response will show here.</code></pre>
|
|
3163
3511
|
</form>
|
|
3164
3512
|
</section>
|
|
3165
3513
|
</main>
|
|
@@ -3167,12 +3515,16 @@ var renderAdminDocument = (service) => {
|
|
|
3167
3515
|
</div>
|
|
3168
3516
|
<dialog id="editor">
|
|
3169
3517
|
<form method="dialog" id="editor-form">
|
|
3170
|
-
<strong id="editor-title">Record</strong>
|
|
3518
|
+
<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>
|
|
3171
3519
|
<label class="field">Id
|
|
3172
3520
|
<input id="editor-id" required>
|
|
3173
3521
|
</label>
|
|
3174
3522
|
<label class="field">JSON value
|
|
3175
|
-
<
|
|
3523
|
+
<div class="json-editor" id="record-editor">
|
|
3524
|
+
<pre aria-hidden="true"><code id="editor-highlight"></code></pre>
|
|
3525
|
+
<textarea id="editor-value" spellcheck="false" aria-describedby="editor-json-status"></textarea>
|
|
3526
|
+
</div>
|
|
3527
|
+
<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>
|
|
3176
3528
|
</label>
|
|
3177
3529
|
<div class="dialog-actions">
|
|
3178
3530
|
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
@@ -3214,8 +3566,8 @@ var describeExtension = (extension) => {
|
|
|
3214
3566
|
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
3215
3567
|
};
|
|
3216
3568
|
};
|
|
3217
|
-
var adminUiRoutes = (service, ui) => {
|
|
3218
|
-
const shell = () => renderAdminDocument(service);
|
|
3569
|
+
var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
3570
|
+
const shell = () => renderAdminDocument(service, adminPrefix);
|
|
3219
3571
|
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
3220
3572
|
return {
|
|
3221
3573
|
"GET /ui": () => htmlResponse(document3()),
|
|
@@ -3832,7 +4184,7 @@ var stateAdminRoutes = (routes) => {
|
|
|
3832
4184
|
};
|
|
3833
4185
|
|
|
3834
4186
|
// ../core/dist/version.js
|
|
3835
|
-
var PACKAGE_VERSION = true ? "
|
|
4187
|
+
var PACKAGE_VERSION = true ? "2.0.0" : UNRELEASED_VERSION;
|
|
3836
4188
|
|
|
3837
4189
|
// ../core/dist/signing.js
|
|
3838
4190
|
var encoder = new TextEncoder();
|
|
@@ -3846,7 +4198,10 @@ var toBase64 = (bytes) => {
|
|
|
3846
4198
|
var fromBase64 = (value) => Uint8Array.from(atob(value), (char) => char.charCodeAt(0));
|
|
3847
4199
|
|
|
3848
4200
|
// ../core/dist/webhooks.js
|
|
3849
|
-
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4201
|
+
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4202
|
+
status,
|
|
4203
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4204
|
+
});
|
|
3850
4205
|
var adminError3 = (status, message) => json4(status, { error: { type: "mockingbird_admin", message } });
|
|
3851
4206
|
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3852
4207
|
var parseEndpoint = (value) => {
|
|
@@ -3954,7 +4309,6 @@ var AT_HEADER = "x-mockingbird-at";
|
|
|
3954
4309
|
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
3955
4310
|
var DEFAULT_NAMESPACE = "default";
|
|
3956
4311
|
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
3957
|
-
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
3958
4312
|
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
3959
4313
|
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
3960
4314
|
var effects = /* @__PURE__ */ new WeakMap();
|
|
@@ -3995,6 +4349,22 @@ var operationMatcher = (document3) => {
|
|
|
3995
4349
|
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
3996
4350
|
};
|
|
3997
4351
|
var createRuntime = (options) => {
|
|
4352
|
+
const adminPrefix = resolveAdminPrefix(options.adminPrefix);
|
|
4353
|
+
const internalPaths = Object.entries(options.document?.paths ?? {}).filter(([, item]) => item["x-mockingbird-internal"] === true).map(([path]) => path);
|
|
4354
|
+
for (const path of internalPaths) {
|
|
4355
|
+
if (!path.startsWith(`${ADMIN_PREFIX}/blobs/`))
|
|
4356
|
+
throw new Error(`internal transport ${path} must live under ${ADMIN_PREFIX}/blobs/`);
|
|
4357
|
+
}
|
|
4358
|
+
assertAdminPrefixAvailable(adminPrefix, Object.keys(options.document?.paths ?? {}).filter((path) => !internalPaths.includes(path)));
|
|
4359
|
+
const internalMatch = (path) => {
|
|
4360
|
+
for (const pattern of internalPaths) {
|
|
4361
|
+
const actual = path.split("/");
|
|
4362
|
+
const expected = pattern.split("/");
|
|
4363
|
+
if (actual.length === expected.length && expected.every((part, i) => part.startsWith("{") || part === actual[i]))
|
|
4364
|
+
return pattern;
|
|
4365
|
+
}
|
|
4366
|
+
return void 0;
|
|
4367
|
+
};
|
|
3998
4368
|
assertAdminUi(options.adminUi);
|
|
3999
4369
|
const sqlite = bootSqlite(options.sqlite);
|
|
4000
4370
|
const clock = options.clock ?? createClock();
|
|
@@ -4040,6 +4410,7 @@ var createRuntime = (options) => {
|
|
|
4040
4410
|
const created = options.create({
|
|
4041
4411
|
namespace: storageNamespace(key),
|
|
4042
4412
|
publicNamespace,
|
|
4413
|
+
adminPrefix,
|
|
4043
4414
|
sqlite,
|
|
4044
4415
|
clock: namespaceClock(publicNamespace),
|
|
4045
4416
|
rng: createdContextRng
|
|
@@ -4272,10 +4643,10 @@ var createRuntime = (options) => {
|
|
|
4272
4643
|
namespaceClock,
|
|
4273
4644
|
namespaceOf(request) {
|
|
4274
4645
|
const url = new URL(request.url);
|
|
4275
|
-
const prefixed =
|
|
4646
|
+
const prefixed = matchNamespacePath(url.pathname, adminPrefix);
|
|
4276
4647
|
const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
|
|
4277
4648
|
const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
|
|
4278
|
-
if (path
|
|
4649
|
+
if (isAdminPath(path, adminPrefix))
|
|
4279
4650
|
return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
|
|
4280
4651
|
if (selected !== void 0)
|
|
4281
4652
|
return selected;
|
|
@@ -4341,7 +4712,7 @@ var createRuntime = (options) => {
|
|
|
4341
4712
|
state: (namespace = DEFAULT_NAMESPACE) => inspectState(stateScope(namespace)),
|
|
4342
4713
|
fetch: async (incoming) => {
|
|
4343
4714
|
let request = incoming;
|
|
4344
|
-
const prefixed =
|
|
4715
|
+
const prefixed = matchNamespacePath(new URL(request.url).pathname, adminPrefix);
|
|
4345
4716
|
if (prefixed) {
|
|
4346
4717
|
const url2 = new URL(request.url);
|
|
4347
4718
|
url2.pathname = prefixed[2] ?? "/";
|
|
@@ -4357,7 +4728,15 @@ var createRuntime = (options) => {
|
|
|
4357
4728
|
signal: request.signal
|
|
4358
4729
|
});
|
|
4359
4730
|
}
|
|
4360
|
-
|
|
4731
|
+
const internalUrl = new URL(request.url);
|
|
4732
|
+
const internalPath = isAdminPath(internalUrl.pathname, adminPrefix) ? `${ADMIN_PREFIX}${internalUrl.pathname.slice(adminPrefix.length)}` : "";
|
|
4733
|
+
const internal = internalMatch(internalPath);
|
|
4734
|
+
const obsoleteInternalPath = adminPrefix !== ADMIN_PREFIX && internalMatch(internalUrl.pathname) && !internal;
|
|
4735
|
+
if (internal) {
|
|
4736
|
+
internalUrl.pathname = internalPath;
|
|
4737
|
+
request = new Request(internalUrl, request);
|
|
4738
|
+
}
|
|
4739
|
+
let namespace = internal ? internalUrl.searchParams.get("namespace") ?? control.namespaceOf(request) : control.namespaceOf(request);
|
|
4361
4740
|
if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
4362
4741
|
const credential = options.credential(request);
|
|
4363
4742
|
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
@@ -4377,7 +4756,9 @@ var createRuntime = (options) => {
|
|
|
4377
4756
|
return copy;
|
|
4378
4757
|
}
|
|
4379
4758
|
};
|
|
4380
|
-
|
|
4759
|
+
if (obsoleteInternalPath)
|
|
4760
|
+
return stamp(adminFail(404, "internal transport is only available under adminPrefix"));
|
|
4761
|
+
const handled = internal ? void 0 : await control.handle(request);
|
|
4381
4762
|
if (handled)
|
|
4382
4763
|
return stamp(handled);
|
|
4383
4764
|
const started = monotonicNow();
|
|
@@ -4418,7 +4799,7 @@ var createRuntime = (options) => {
|
|
|
4418
4799
|
type: "mockingbird_admin",
|
|
4419
4800
|
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
4420
4801
|
}
|
|
4421
|
-
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
4802
|
+
}), { status: 400, headers: { "content-type": "application/json; charset=utf-8" } }));
|
|
4422
4803
|
}
|
|
4423
4804
|
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
4424
4805
|
log(400);
|
|
@@ -4535,15 +4916,16 @@ var createRuntime = (options) => {
|
|
|
4535
4916
|
...options.admin?.(runtime) ?? {},
|
|
4536
4917
|
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
4537
4918
|
// maps `presets`, and a service route must not replace that list with a record.
|
|
4538
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
4919
|
+
...options.presets ? presetRoutes(options.presets, runtime, adminPrefix) : {},
|
|
4539
4920
|
...stateAdminRoutes({
|
|
4540
4921
|
open: stateScope,
|
|
4541
4922
|
afterWrite: (namespace) => {
|
|
4542
4923
|
checkpoint(namespace, "main");
|
|
4543
4924
|
}
|
|
4544
4925
|
}),
|
|
4545
|
-
...adminUiRoutes(options.name, options.adminUi)
|
|
4926
|
+
...adminUiRoutes(options.name, options.adminUi, adminPrefix)
|
|
4546
4927
|
},
|
|
4928
|
+
adminPrefix,
|
|
4547
4929
|
adminKey: options.adminKey
|
|
4548
4930
|
});
|
|
4549
4931
|
return defineMock(runtime);
|
|
@@ -4557,7 +4939,10 @@ var mutableResponse = (response) => {
|
|
|
4557
4939
|
return new Response(response.body, response);
|
|
4558
4940
|
}
|
|
4559
4941
|
};
|
|
4560
|
-
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
4942
|
+
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
4943
|
+
status,
|
|
4944
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4945
|
+
});
|
|
4561
4946
|
var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
|
|
4562
4947
|
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4563
4948
|
var credentialRoutes = (registry) => ({
|
|
@@ -4609,7 +4994,7 @@ var credentialRoutes = (registry) => ({
|
|
|
4609
4994
|
return adminJson(200, { status: "ok" });
|
|
4610
4995
|
}
|
|
4611
4996
|
});
|
|
4612
|
-
var presetRoutes = (presets, runtime) => ({
|
|
4997
|
+
var presetRoutes = (presets, runtime, adminPrefix) => ({
|
|
4613
4998
|
"GET /faults/presets": () => {
|
|
4614
4999
|
const body = {
|
|
4615
5000
|
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
@@ -4619,7 +5004,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
4619
5004
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
4620
5005
|
const name = params.name;
|
|
4621
5006
|
if (!presets[name])
|
|
4622
|
-
return adminFail(404, `no fault preset ${name}; GET /
|
|
5007
|
+
return adminFail(404, `no fault preset ${name}; GET ${adminPrefix}/faults/presets`);
|
|
4623
5008
|
const overrides = isObject(body) ? body : {};
|
|
4624
5009
|
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
4625
5010
|
}
|
|
@@ -6462,14 +6847,16 @@ var createRuntime2 = (options = {}) => createRuntime({
|
|
|
6462
6847
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
6463
6848
|
...options.clock ? { clock: options.clock } : {},
|
|
6464
6849
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
6850
|
+
...options.adminPrefix !== void 0 ? { adminPrefix: options.adminPrefix } : {},
|
|
6465
6851
|
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
6466
6852
|
...options.onLog ? { onLog: options.onLog } : {},
|
|
6467
6853
|
credential: keyCredential,
|
|
6468
6854
|
presets: GOOGLE_MAPS_PRESETS,
|
|
6469
|
-
create: ({ sqlite, namespace, publicNamespace, clock }) => new GoogleMapsAPI({
|
|
6855
|
+
create: ({ sqlite, namespace, publicNamespace, adminPrefix, clock }) => new GoogleMapsAPI({
|
|
6470
6856
|
sqlite,
|
|
6471
6857
|
namespace,
|
|
6472
6858
|
publicNamespace,
|
|
6859
|
+
adminPrefix,
|
|
6473
6860
|
now: clock.now,
|
|
6474
6861
|
...options.corpus ? { corpus: options.corpus } : {},
|
|
6475
6862
|
...options.settings ? { settings: options.settings } : {}
|
|
@@ -6520,10 +6907,12 @@ var GoogleMapsAPI = class {
|
|
|
6520
6907
|
sqlite;
|
|
6521
6908
|
state;
|
|
6522
6909
|
service;
|
|
6910
|
+
adminPrefix;
|
|
6523
6911
|
publicNamespace;
|
|
6524
6912
|
constructor(options = {}) {
|
|
6525
6913
|
const sqlite = bootSqlite(options.sqlite);
|
|
6526
6914
|
const namespace = options.namespace ?? GOOGLE_MAPS_NAMESPACE;
|
|
6915
|
+
this.adminPrefix = options.adminPrefix ?? "/__admin";
|
|
6527
6916
|
this.publicNamespace = options.publicNamespace ?? "default";
|
|
6528
6917
|
this.state = new GoogleMapsState(sqlite, namespace, {
|
|
6529
6918
|
corpus: options.corpus ?? [],
|
|
@@ -6742,7 +7131,7 @@ var GoogleMapsAPI = class {
|
|
|
6742
7131
|
const key = text(context.query.key) ?? "";
|
|
6743
7132
|
const settings = this.state.current();
|
|
6744
7133
|
const origin = (settings.publicUrl ?? context.url.origin).replace(/\/$/, "");
|
|
6745
|
-
const prefix = this.publicNamespace === "default" ? "" :
|
|
7134
|
+
const prefix = this.publicNamespace === "default" ? "" : `${this.adminPrefix}/ns/${encodeURIComponent(this.publicNamespace)}`;
|
|
6746
7135
|
const callback = text(context.query.callback) ?? null;
|
|
6747
7136
|
const safeCallback = callback && /^[A-Za-z_$][\w$.]*$/.test(callback) ? callback : null;
|
|
6748
7137
|
const authFailed = !key || settings.keys.length > 0 && !settings.keys.includes(key);
|
|
@@ -6758,6 +7147,7 @@ var GoogleMapsAPI = class {
|
|
|
6758
7147
|
|
|
6759
7148
|
export {
|
|
6760
7149
|
createClock,
|
|
7150
|
+
resolveAdminPrefix,
|
|
6761
7151
|
parseDuration,
|
|
6762
7152
|
document2 as document,
|
|
6763
7153
|
operationIds,
|
|
@@ -6778,4 +7168,4 @@ export {
|
|
|
6778
7168
|
keyCredential,
|
|
6779
7169
|
GoogleMapsAPI
|
|
6780
7170
|
};
|
|
6781
|
-
//# sourceMappingURL=chunk-
|
|
7171
|
+
//# sourceMappingURL=chunk-O2FDQBMY.js.map
|