@crvouga/mockingbird-service-step-functions 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 +2 -2
- package/SUPPORT.md +11 -0
- package/dist/{chunk-5ZU42EQK.js → chunk-EBPLA3FJ.js} +622 -235
- package/dist/chunk-EBPLA3FJ.js.map +7 -0
- package/dist/{chunk-YGTX62QV.js → chunk-PIVL5IYK.js} +43 -23
- package/dist/chunk-PIVL5IYK.js.map +7 -0
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.js +2 -2
- package/openapi.yaml +22 -0
- package/package.json +29 -4
- package/dist/chunk-5ZU42EQK.js.map +0 -7
- package/dist/chunk-YGTX62QV.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
|
};
|
|
@@ -1399,6 +1438,34 @@ dialog:not([open]) {
|
|
|
1399
1438
|
function isRecord2(value) {
|
|
1400
1439
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1401
1440
|
}
|
|
1441
|
+
function highlightJsonText(text) {
|
|
1442
|
+
const escapeToken = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
1443
|
+
switch (char) {
|
|
1444
|
+
case "&":
|
|
1445
|
+
return "&";
|
|
1446
|
+
case "<":
|
|
1447
|
+
return "<";
|
|
1448
|
+
case ">":
|
|
1449
|
+
return ">";
|
|
1450
|
+
case '"':
|
|
1451
|
+
return """;
|
|
1452
|
+
default:
|
|
1453
|
+
return "'";
|
|
1454
|
+
}
|
|
1455
|
+
});
|
|
1456
|
+
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;
|
|
1457
|
+
let cursor = 0;
|
|
1458
|
+
let html = "";
|
|
1459
|
+
for (const match2 of text.matchAll(token)) {
|
|
1460
|
+
const index = match2.index ?? 0;
|
|
1461
|
+
const value = match2[0];
|
|
1462
|
+
html += escapeToken(text.slice(cursor, index));
|
|
1463
|
+
const kind = value.startsWith('"') ? value.trimEnd().endsWith(":") ? "key" : "string" : value === "true" || value === "false" ? "boolean" : value === "null" ? "null" : "number";
|
|
1464
|
+
html += `<span class="json-${kind}">${escapeToken(value)}</span>`;
|
|
1465
|
+
cursor = index + value.length;
|
|
1466
|
+
}
|
|
1467
|
+
return html + escapeToken(text.slice(cursor));
|
|
1468
|
+
}
|
|
1402
1469
|
function mountAdminBrand(doc, host, service, catalog) {
|
|
1403
1470
|
const clip = (value, max) => {
|
|
1404
1471
|
if (typeof value !== "string")
|
|
@@ -1760,14 +1827,16 @@ function bootAdmin(config) {
|
|
|
1760
1827
|
const state = {
|
|
1761
1828
|
namespace: "default",
|
|
1762
1829
|
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
1763
|
-
view: "overview",
|
|
1830
|
+
view: ["overview", "state", "clock", "faults", "journal", "routes"].includes(location.hash.slice(1)) ? location.hash.slice(1) : "overview",
|
|
1764
1831
|
collection: "",
|
|
1765
1832
|
shape: null,
|
|
1766
1833
|
route: null,
|
|
1834
|
+
routes: [],
|
|
1767
1835
|
editing: null
|
|
1768
1836
|
};
|
|
1769
1837
|
const standard = new Set(config.standardRoutes);
|
|
1770
1838
|
const banner = el("banner", HTMLElement);
|
|
1839
|
+
const bannerMessage = el("banner-message", HTMLElement);
|
|
1771
1840
|
const keyInput = el("admin-key", HTMLInputElement);
|
|
1772
1841
|
keyInput.value = state.key;
|
|
1773
1842
|
const params = new URLSearchParams(location.search);
|
|
@@ -1778,18 +1847,21 @@ function bootAdmin(config) {
|
|
|
1778
1847
|
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
1779
1848
|
params.delete("key");
|
|
1780
1849
|
const next = params.toString();
|
|
1781
|
-
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
1850
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : "") + location.hash);
|
|
1782
1851
|
}
|
|
1783
1852
|
const queryNamespace = params.get("namespace");
|
|
1784
1853
|
if (queryNamespace)
|
|
1785
1854
|
state.namespace = queryNamespace;
|
|
1786
1855
|
const showError = (error) => {
|
|
1787
|
-
|
|
1856
|
+
bannerMessage.textContent = error instanceof Error ? error.message : String(error);
|
|
1788
1857
|
banner.classList.add("show");
|
|
1858
|
+
banner.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
1789
1859
|
};
|
|
1790
1860
|
const clearError = () => {
|
|
1791
1861
|
banner.classList.remove("show");
|
|
1862
|
+
bannerMessage.textContent = "";
|
|
1792
1863
|
};
|
|
1864
|
+
el("banner-dismiss", HTMLButtonElement).addEventListener("click", clearError);
|
|
1793
1865
|
const api = async (method, path, body) => {
|
|
1794
1866
|
const headers = { accept: "application/json" };
|
|
1795
1867
|
if (state.key !== "")
|
|
@@ -1833,16 +1905,21 @@ function bootAdmin(config) {
|
|
|
1833
1905
|
namespace: () => state.namespace
|
|
1834
1906
|
};
|
|
1835
1907
|
const isPanelScript = (value) => typeof value === "function";
|
|
1908
|
+
const themeButton = el("theme", HTMLButtonElement);
|
|
1836
1909
|
const setTheme = (theme) => {
|
|
1837
1910
|
if (theme)
|
|
1838
1911
|
document.documentElement.dataset.theme = theme;
|
|
1839
1912
|
else
|
|
1840
1913
|
delete document.documentElement.dataset.theme;
|
|
1914
|
+
const dark = theme ? theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
1915
|
+
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>';
|
|
1916
|
+
const label = dark ? "Use light theme" : "Use dark theme";
|
|
1917
|
+
themeButton.setAttribute("aria-label", label);
|
|
1918
|
+
themeButton.title = label;
|
|
1841
1919
|
};
|
|
1842
1920
|
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
1921
|
+
setTheme(storedTheme === "dark" || storedTheme === "light" ? storedTheme : null);
|
|
1922
|
+
themeButton.addEventListener("click", () => {
|
|
1846
1923
|
const current = document.documentElement.dataset.theme;
|
|
1847
1924
|
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
1848
1925
|
const next = dark ? "light" : "dark";
|
|
@@ -1861,6 +1938,7 @@ function bootAdmin(config) {
|
|
|
1861
1938
|
continue;
|
|
1862
1939
|
section.hidden = section.id !== `view-${view}`;
|
|
1863
1940
|
}
|
|
1941
|
+
history.replaceState(null, "", `${location.pathname}${location.search}#${encodeURIComponent(view)}`);
|
|
1864
1942
|
};
|
|
1865
1943
|
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
1866
1944
|
const target = event.target;
|
|
@@ -1875,7 +1953,7 @@ function bootAdmin(config) {
|
|
|
1875
1953
|
show(view);
|
|
1876
1954
|
refresh().catch(showError);
|
|
1877
1955
|
});
|
|
1878
|
-
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>`;
|
|
1956
|
+
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>`;
|
|
1879
1957
|
const table = (columns, rows) => {
|
|
1880
1958
|
if (rows.length === 0)
|
|
1881
1959
|
return '<p class="empty">Nothing here yet.</p>';
|
|
@@ -1885,16 +1963,22 @@ function bootAdmin(config) {
|
|
|
1885
1963
|
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
1886
1964
|
return `<tr${attrs}>${cells}</tr>`;
|
|
1887
1965
|
}).join("");
|
|
1888
|
-
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
1966
|
+
return `<div class="table-shell"><table><thead>${head}</thead><tbody>${body}</tbody></table></div>`;
|
|
1889
1967
|
};
|
|
1890
1968
|
const preview = (value) => {
|
|
1891
1969
|
const text = JSON.stringify(value) ?? "null";
|
|
1892
|
-
|
|
1970
|
+
const clipped = text.length > 180 ? `${text.slice(0, 180)}\u2026` : text;
|
|
1971
|
+
return `<code class="json-inline">${highlightJsonText(clipped)}</code>`;
|
|
1893
1972
|
};
|
|
1894
1973
|
const when = (epoch) => {
|
|
1895
1974
|
const date = new Date(epoch);
|
|
1896
1975
|
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
1897
1976
|
};
|
|
1977
|
+
const journalWhen = (value) => {
|
|
1978
|
+
const numeric = value.trim() === "" ? Number.NaN : Number(value);
|
|
1979
|
+
const epoch = Number.isFinite(numeric) ? numeric : Date.parse(value);
|
|
1980
|
+
return Number.isFinite(epoch) ? when(epoch) : value;
|
|
1981
|
+
};
|
|
1898
1982
|
const clockCard = (epoch) => {
|
|
1899
1983
|
const date = new Date(epoch);
|
|
1900
1984
|
if (Number.isNaN(date.getTime()))
|
|
@@ -2014,7 +2098,7 @@ function bootAdmin(config) {
|
|
|
2014
2098
|
};
|
|
2015
2099
|
const renderOverview = async () => {
|
|
2016
2100
|
const [health, shape, clock, journal] = await Promise.all([
|
|
2017
|
-
originGet(
|
|
2101
|
+
originGet(`${config.adminPrefix}/health`),
|
|
2018
2102
|
api("GET", "/state"),
|
|
2019
2103
|
api("GET", "/clock"),
|
|
2020
2104
|
api("GET", "/requests?limit=8")
|
|
@@ -2031,7 +2115,7 @@ function bootAdmin(config) {
|
|
|
2031
2115
|
},
|
|
2032
2116
|
{
|
|
2033
2117
|
k: "Status",
|
|
2034
|
-
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
2118
|
+
v: `<span class="badge ok">${esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")}</span>`
|
|
2035
2119
|
},
|
|
2036
2120
|
{ k: "Namespace", v: esc(state.namespace) },
|
|
2037
2121
|
{ k: "Collections", v: String(decoded.collections.length) },
|
|
@@ -2118,26 +2202,46 @@ function bootAdmin(config) {
|
|
|
2118
2202
|
const renderJournal = async () => {
|
|
2119
2203
|
const journal = await api("GET", "/requests?limit=100");
|
|
2120
2204
|
el("journal", HTMLElement).innerHTML = table([
|
|
2121
|
-
{ label: "When", cell: (row) => esc(row.at) },
|
|
2122
|
-
{
|
|
2205
|
+
{ label: "When", cell: (row) => esc(journalWhen(row.at)) },
|
|
2206
|
+
{
|
|
2207
|
+
label: "Method",
|
|
2208
|
+
cell: (row) => `<span class="method">${esc(row.method)}</span>`
|
|
2209
|
+
},
|
|
2123
2210
|
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2124
|
-
{
|
|
2211
|
+
{
|
|
2212
|
+
label: "Status",
|
|
2213
|
+
cell: (row) => `<span class="badge ${row.status.startsWith("2") ? "ok" : row.status.startsWith("4") || row.status.startsWith("5") ? "warn" : ""}">${esc(row.status)}</span>`
|
|
2214
|
+
},
|
|
2125
2215
|
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
2126
2216
|
], decodeJournal(journal));
|
|
2127
2217
|
};
|
|
2128
|
-
const
|
|
2129
|
-
const
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2218
|
+
const paintRoutes = () => {
|
|
2219
|
+
const query = el("route-search", HTMLInputElement).value.trim().toLocaleLowerCase();
|
|
2220
|
+
const routes = state.routes.filter((route) => route.toLocaleLowerCase().includes(query));
|
|
2221
|
+
el("route-count", HTMLElement).textContent = `${routes.length} of ${state.routes.length}`;
|
|
2222
|
+
if (state.routes.length === 0) {
|
|
2223
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No admin routes are available.</p>';
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
if (routes.length === 0) {
|
|
2227
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No routes match this filter.</p>';
|
|
2228
|
+
return;
|
|
2229
|
+
}
|
|
2133
2230
|
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2134
2231
|
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2135
2232
|
const parts = route.split(" ");
|
|
2136
2233
|
const method = parts[0] ?? "";
|
|
2137
2234
|
const path = parts.slice(1).join(" ");
|
|
2138
|
-
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>`;
|
|
2235
|
+
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>`;
|
|
2139
2236
|
}).join("");
|
|
2140
2237
|
};
|
|
2238
|
+
const renderRoutes = async () => {
|
|
2239
|
+
const data = await api("GET", "/");
|
|
2240
|
+
if (!isRecord2(data))
|
|
2241
|
+
throw new Error("Route list was not an object");
|
|
2242
|
+
state.routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
2243
|
+
paintRoutes();
|
|
2244
|
+
};
|
|
2141
2245
|
const renderers = {
|
|
2142
2246
|
overview: renderOverview,
|
|
2143
2247
|
state: renderState,
|
|
@@ -2146,19 +2250,50 @@ function bootAdmin(config) {
|
|
|
2146
2250
|
journal: renderJournal,
|
|
2147
2251
|
routes: renderRoutes
|
|
2148
2252
|
};
|
|
2253
|
+
let refreshToken = 0;
|
|
2149
2254
|
async function refresh() {
|
|
2255
|
+
const token = ++refreshToken;
|
|
2150
2256
|
clearError();
|
|
2257
|
+
const main = el("views", HTMLElement);
|
|
2258
|
+
const sync = el("sync-status", HTMLElement);
|
|
2259
|
+
main.setAttribute("aria-busy", "true");
|
|
2260
|
+
sync.classList.add("busy");
|
|
2261
|
+
sync.textContent = "Updating";
|
|
2151
2262
|
const render = renderers[state.view];
|
|
2152
|
-
|
|
2153
|
-
|
|
2263
|
+
try {
|
|
2264
|
+
if (render)
|
|
2265
|
+
await render();
|
|
2266
|
+
if (token === refreshToken)
|
|
2267
|
+
sync.textContent = "Up to date";
|
|
2268
|
+
} finally {
|
|
2269
|
+
if (token === refreshToken) {
|
|
2270
|
+
main.removeAttribute("aria-busy");
|
|
2271
|
+
sync.classList.remove("busy");
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2154
2274
|
}
|
|
2155
2275
|
const delay = (ms) => new Promise((resolve) => {
|
|
2156
2276
|
window.setTimeout(resolve, ms);
|
|
2157
2277
|
});
|
|
2158
|
-
const confirmAction = async (button, pending, done, work) => {
|
|
2278
|
+
const confirmAction = async (button, pending, done, work, confirmLabel) => {
|
|
2159
2279
|
if (button.dataset.pending === "1")
|
|
2160
2280
|
return;
|
|
2161
2281
|
const label = button.textContent ?? "";
|
|
2282
|
+
if (confirmLabel !== void 0 && button.dataset.confirming !== "1") {
|
|
2283
|
+
button.dataset.confirming = "1";
|
|
2284
|
+
button.classList.add("confirming");
|
|
2285
|
+
button.textContent = confirmLabel;
|
|
2286
|
+
window.setTimeout(() => {
|
|
2287
|
+
if (button.dataset.confirming !== "1")
|
|
2288
|
+
return;
|
|
2289
|
+
button.textContent = label;
|
|
2290
|
+
button.classList.remove("confirming");
|
|
2291
|
+
delete button.dataset.confirming;
|
|
2292
|
+
}, 3e3);
|
|
2293
|
+
return;
|
|
2294
|
+
}
|
|
2295
|
+
button.classList.remove("confirming");
|
|
2296
|
+
delete button.dataset.confirming;
|
|
2162
2297
|
const width = button.offsetWidth;
|
|
2163
2298
|
button.dataset.pending = "1";
|
|
2164
2299
|
button.classList.add("pressed");
|
|
@@ -2179,6 +2314,65 @@ function bootAdmin(config) {
|
|
|
2179
2314
|
delete button.dataset.pending;
|
|
2180
2315
|
}
|
|
2181
2316
|
};
|
|
2317
|
+
const wireJsonEditor = (textareaId, highlightId, statusId, shellId, allowEmpty) => {
|
|
2318
|
+
const textarea = el(textareaId, HTMLTextAreaElement);
|
|
2319
|
+
const highlight = el(highlightId, HTMLElement);
|
|
2320
|
+
const status = el(statusId, HTMLElement);
|
|
2321
|
+
const shell = el(shellId, HTMLElement);
|
|
2322
|
+
const sync = () => {
|
|
2323
|
+
const text = textarea.value;
|
|
2324
|
+
highlight.innerHTML = `${highlightJsonText(text)}
|
|
2325
|
+
`;
|
|
2326
|
+
let valid = true;
|
|
2327
|
+
let message = "Valid JSON";
|
|
2328
|
+
if (allowEmpty && text.trim() === "")
|
|
2329
|
+
message = "No body";
|
|
2330
|
+
else {
|
|
2331
|
+
try {
|
|
2332
|
+
readJson2(text);
|
|
2333
|
+
} catch {
|
|
2334
|
+
valid = false;
|
|
2335
|
+
message = "Invalid JSON";
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
shell.classList.toggle("invalid", !valid);
|
|
2339
|
+
status.classList.toggle("invalid", !valid);
|
|
2340
|
+
status.textContent = message;
|
|
2341
|
+
textarea.setAttribute("aria-invalid", valid ? "false" : "true");
|
|
2342
|
+
};
|
|
2343
|
+
const syncScroll = () => {
|
|
2344
|
+
const preview2 = highlight.parentElement;
|
|
2345
|
+
if (!(preview2 instanceof HTMLElement))
|
|
2346
|
+
return;
|
|
2347
|
+
preview2.scrollTop = textarea.scrollTop;
|
|
2348
|
+
preview2.scrollLeft = textarea.scrollLeft;
|
|
2349
|
+
};
|
|
2350
|
+
textarea.addEventListener("input", sync);
|
|
2351
|
+
textarea.addEventListener("scroll", syncScroll);
|
|
2352
|
+
sync();
|
|
2353
|
+
return sync;
|
|
2354
|
+
};
|
|
2355
|
+
const syncRouteEditor = wireJsonEditor("route-body", "route-highlight", "route-json-status", "route-editor", true);
|
|
2356
|
+
const syncRecordEditor = wireJsonEditor("editor-value", "editor-highlight", "editor-json-status", "record-editor", false);
|
|
2357
|
+
for (const button of document.querySelectorAll("[data-format]")) {
|
|
2358
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2359
|
+
continue;
|
|
2360
|
+
button.addEventListener("click", () => {
|
|
2361
|
+
const target = button.dataset.format;
|
|
2362
|
+
if (target === void 0)
|
|
2363
|
+
return;
|
|
2364
|
+
const textarea = document.getElementById(target);
|
|
2365
|
+
if (!(textarea instanceof HTMLTextAreaElement) || textarea.value.trim() === "")
|
|
2366
|
+
return;
|
|
2367
|
+
try {
|
|
2368
|
+
textarea.value = JSON.stringify(readJson2(textarea.value), null, 2) ?? "";
|
|
2369
|
+
textarea.dispatchEvent(new Event("input"));
|
|
2370
|
+
textarea.focus();
|
|
2371
|
+
} catch {
|
|
2372
|
+
showError(new Error("Fix the JSON before formatting it"));
|
|
2373
|
+
}
|
|
2374
|
+
});
|
|
2375
|
+
}
|
|
2182
2376
|
document.addEventListener("click", (event) => {
|
|
2183
2377
|
const target = event.target;
|
|
2184
2378
|
if (!(target instanceof Element))
|
|
@@ -2251,6 +2445,7 @@ function bootAdmin(config) {
|
|
|
2251
2445
|
} else {
|
|
2252
2446
|
editorValue.value = "{\n \n}";
|
|
2253
2447
|
}
|
|
2448
|
+
syncRecordEditor();
|
|
2254
2449
|
editor.showModal();
|
|
2255
2450
|
}
|
|
2256
2451
|
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -2280,10 +2475,10 @@ function bootAdmin(config) {
|
|
|
2280
2475
|
const id = state.editing;
|
|
2281
2476
|
if (!id)
|
|
2282
2477
|
return;
|
|
2283
|
-
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2478
|
+
confirmAction(editorDelete, "Deleting...", "Deleted", () => api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2284
2479
|
editor.close();
|
|
2285
2480
|
return renderState();
|
|
2286
|
-
}).catch(showError);
|
|
2481
|
+
}), "Confirm delete").catch(showError);
|
|
2287
2482
|
});
|
|
2288
2483
|
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2289
2484
|
if (!(button instanceof HTMLButtonElement))
|
|
@@ -2329,10 +2524,10 @@ function bootAdmin(config) {
|
|
|
2329
2524
|
}).then(() => renderFaults()).catch(showError);
|
|
2330
2525
|
});
|
|
2331
2526
|
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2332
|
-
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
|
|
2527
|
+
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults()), "Confirm clear").catch(showError);
|
|
2333
2528
|
});
|
|
2334
2529
|
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2335
|
-
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
|
|
2530
|
+
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal()), "Confirm clear").catch(showError);
|
|
2336
2531
|
});
|
|
2337
2532
|
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2338
2533
|
const target = event.target;
|
|
@@ -2346,9 +2541,15 @@ function bootAdmin(config) {
|
|
|
2346
2541
|
return;
|
|
2347
2542
|
state.route = route;
|
|
2348
2543
|
el("route-title", HTMLElement).textContent = route;
|
|
2544
|
+
el("route-send", HTMLButtonElement).disabled = false;
|
|
2349
2545
|
const method = route.split(" ")[0] ?? "";
|
|
2350
|
-
el("route-body", HTMLTextAreaElement)
|
|
2546
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
2547
|
+
routeBody.disabled = method === "GET" || method === "DELETE";
|
|
2548
|
+
el("route-editor", HTMLElement).hidden = routeBody.disabled;
|
|
2549
|
+
el("route-json-status", HTMLElement).parentElement?.toggleAttribute("hidden", routeBody.disabled);
|
|
2550
|
+
syncRouteEditor();
|
|
2351
2551
|
});
|
|
2552
|
+
el("route-search", HTMLInputElement).addEventListener("input", paintRoutes);
|
|
2352
2553
|
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2353
2554
|
event.preventDefault();
|
|
2354
2555
|
const send = async () => {
|
|
@@ -2370,7 +2571,8 @@ function bootAdmin(config) {
|
|
|
2370
2571
|
}
|
|
2371
2572
|
}
|
|
2372
2573
|
const data = await api(method, concrete, body);
|
|
2373
|
-
|
|
2574
|
+
const text = JSON.stringify(data, null, 2) ?? "null";
|
|
2575
|
+
el("route-result", HTMLElement).innerHTML = `<code>${highlightJsonText(text)}</code>`;
|
|
2374
2576
|
};
|
|
2375
2577
|
send().catch(showError);
|
|
2376
2578
|
});
|
|
@@ -2488,11 +2690,13 @@ function bootAdmin(config) {
|
|
|
2488
2690
|
views.append(section);
|
|
2489
2691
|
mountSqlExplorer(body, panelApi);
|
|
2490
2692
|
}
|
|
2693
|
+
show(document.getElementById(`view-${state.view}`) === null ? "overview" : state.view);
|
|
2491
2694
|
return refresh();
|
|
2492
2695
|
}).catch(showError);
|
|
2493
2696
|
}
|
|
2494
2697
|
var clientPrelude = [
|
|
2495
2698
|
isRecord2,
|
|
2699
|
+
highlightJsonText,
|
|
2496
2700
|
mountAdminBrand,
|
|
2497
2701
|
startAdminBrand,
|
|
2498
2702
|
faultPresetList,
|
|
@@ -2515,6 +2719,7 @@ ${entry}(${payload});
|
|
|
2515
2719
|
var defineMock = (runtime) => runtime;
|
|
2516
2720
|
var STANDARD_ADMIN_ROUTES = [
|
|
2517
2721
|
"GET /",
|
|
2722
|
+
"GET /health",
|
|
2518
2723
|
"POST /reset",
|
|
2519
2724
|
"GET /namespaces",
|
|
2520
2725
|
"GET /clock",
|
|
@@ -2594,11 +2799,11 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
2594
2799
|
}
|
|
2595
2800
|
});
|
|
2596
2801
|
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
2597
|
-
var renderAdminDocument = (service) => {
|
|
2802
|
+
var renderAdminDocument = (service, adminPrefix = ADMIN_PREFIX) => {
|
|
2598
2803
|
const name = escapeHtml(service);
|
|
2599
2804
|
const script = adminClientSource({
|
|
2600
2805
|
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
2601
|
-
adminPrefix
|
|
2806
|
+
adminPrefix,
|
|
2602
2807
|
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
2603
2808
|
brandsUrl: ADMIN_BRANDS_URL,
|
|
2604
2809
|
service
|
|
@@ -2615,75 +2820,99 @@ var renderAdminDocument = (service) => {
|
|
|
2615
2820
|
${CSS_RESET}
|
|
2616
2821
|
:root {
|
|
2617
2822
|
color-scheme: light;
|
|
2618
|
-
--bg: #
|
|
2823
|
+
--bg: #f7f8fb;
|
|
2619
2824
|
--bg-elev: #ffffff;
|
|
2620
|
-
--bg-muted: #
|
|
2621
|
-
--bg-inset: #
|
|
2622
|
-
--line: #
|
|
2623
|
-
--line-strong: #
|
|
2624
|
-
--ink: #
|
|
2625
|
-
--muted: #
|
|
2626
|
-
--faint: #
|
|
2627
|
-
--accent: #
|
|
2825
|
+
--bg-muted: #f1f3f7;
|
|
2826
|
+
--bg-inset: #f8f9fc;
|
|
2827
|
+
--line: #e4e7ec;
|
|
2828
|
+
--line-strong: #cfd4dc;
|
|
2829
|
+
--ink: #101828;
|
|
2830
|
+
--muted: #475467;
|
|
2831
|
+
--faint: #667085;
|
|
2832
|
+
--accent: #6941c6;
|
|
2833
|
+
--accent-hover: #5933b4;
|
|
2628
2834
|
--accent-ink: #ffffff;
|
|
2629
|
-
--accent-soft: #
|
|
2630
|
-
--ok: #
|
|
2631
|
-
--ok-soft: #
|
|
2632
|
-
--warn: #
|
|
2633
|
-
--warn-soft: #
|
|
2634
|
-
--err: #
|
|
2635
|
-
--err-soft: #
|
|
2636
|
-
--
|
|
2835
|
+
--accent-soft: #f4f0ff;
|
|
2836
|
+
--ok: #067647;
|
|
2837
|
+
--ok-soft: #ecfdf3;
|
|
2838
|
+
--warn: #b54708;
|
|
2839
|
+
--warn-soft: #fffaeb;
|
|
2840
|
+
--err: #b42318;
|
|
2841
|
+
--err-soft: #fef3f2;
|
|
2842
|
+
--code-key: #6941c6;
|
|
2843
|
+
--code-string: #067647;
|
|
2844
|
+
--code-number: #175cd3;
|
|
2845
|
+
--code-boolean: #c11574;
|
|
2846
|
+
--code-null: #667085;
|
|
2847
|
+
--shadow-xs: 0 1px 2px rgb(16 24 40 / 0.05);
|
|
2848
|
+
--shadow-sm: 0 1px 3px rgb(16 24 40 / 0.08), 0 1px 2px rgb(16 24 40 / 0.04);
|
|
2849
|
+
--shadow-lg: 0 18px 48px rgb(16 24 40 / 0.16), 0 4px 12px rgb(16 24 40 / 0.08);
|
|
2637
2850
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2638
2851
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
2639
|
-
--radius:
|
|
2640
|
-
--header:
|
|
2641
|
-
--nav:
|
|
2852
|
+
--radius: 12px;
|
|
2853
|
+
--header: 72px;
|
|
2854
|
+
--nav: 224px;
|
|
2642
2855
|
}
|
|
2643
2856
|
:root[data-theme="dark"] {
|
|
2644
2857
|
color-scheme: dark;
|
|
2645
|
-
--bg: #
|
|
2646
|
-
--bg-elev: #
|
|
2647
|
-
--bg-muted: #
|
|
2648
|
-
--bg-inset: #
|
|
2649
|
-
--line: #
|
|
2650
|
-
--line-strong: #
|
|
2651
|
-
--ink: #
|
|
2652
|
-
--muted: #
|
|
2653
|
-
--faint: #
|
|
2654
|
-
--accent: #
|
|
2655
|
-
--accent-
|
|
2656
|
-
--accent-
|
|
2657
|
-
--
|
|
2658
|
-
--ok
|
|
2659
|
-
--
|
|
2660
|
-
--warn
|
|
2661
|
-
--
|
|
2662
|
-
--err
|
|
2663
|
-
--
|
|
2858
|
+
--bg: #0c0e14;
|
|
2859
|
+
--bg-elev: #151821;
|
|
2860
|
+
--bg-muted: #20232e;
|
|
2861
|
+
--bg-inset: #11141c;
|
|
2862
|
+
--line: #2b303d;
|
|
2863
|
+
--line-strong: #414858;
|
|
2864
|
+
--ink: #f5f7fa;
|
|
2865
|
+
--muted: #b6bdc9;
|
|
2866
|
+
--faint: #8d96a7;
|
|
2867
|
+
--accent: #9e77ed;
|
|
2868
|
+
--accent-hover: #b692f6;
|
|
2869
|
+
--accent-ink: #ffffff;
|
|
2870
|
+
--accent-soft: #2b2142;
|
|
2871
|
+
--ok: #75e0a7;
|
|
2872
|
+
--ok-soft: #102a20;
|
|
2873
|
+
--warn: #fec84b;
|
|
2874
|
+
--warn-soft: #302611;
|
|
2875
|
+
--err: #fda29b;
|
|
2876
|
+
--err-soft: #321817;
|
|
2877
|
+
--code-key: #c3a6ff;
|
|
2878
|
+
--code-string: #75e0a7;
|
|
2879
|
+
--code-number: #84adff;
|
|
2880
|
+
--code-boolean: #f9a8d4;
|
|
2881
|
+
--code-null: #98a2b3;
|
|
2882
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
2883
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
2884
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2664
2885
|
}
|
|
2665
2886
|
@media (prefers-color-scheme: dark) {
|
|
2666
2887
|
:root:not([data-theme="light"]) {
|
|
2667
2888
|
color-scheme: dark;
|
|
2668
|
-
--bg: #
|
|
2669
|
-
--bg-elev: #
|
|
2670
|
-
--bg-muted: #
|
|
2671
|
-
--bg-inset: #
|
|
2672
|
-
--line: #
|
|
2673
|
-
--line-strong: #
|
|
2674
|
-
--ink: #
|
|
2675
|
-
--muted: #
|
|
2676
|
-
--faint: #
|
|
2677
|
-
--accent: #
|
|
2678
|
-
--accent-
|
|
2679
|
-
--accent-
|
|
2680
|
-
--
|
|
2681
|
-
--ok
|
|
2682
|
-
--
|
|
2683
|
-
--warn
|
|
2684
|
-
--
|
|
2685
|
-
--err
|
|
2686
|
-
--
|
|
2889
|
+
--bg: #0c0e14;
|
|
2890
|
+
--bg-elev: #151821;
|
|
2891
|
+
--bg-muted: #20232e;
|
|
2892
|
+
--bg-inset: #11141c;
|
|
2893
|
+
--line: #2b303d;
|
|
2894
|
+
--line-strong: #414858;
|
|
2895
|
+
--ink: #f5f7fa;
|
|
2896
|
+
--muted: #b6bdc9;
|
|
2897
|
+
--faint: #8d96a7;
|
|
2898
|
+
--accent: #9e77ed;
|
|
2899
|
+
--accent-hover: #b692f6;
|
|
2900
|
+
--accent-ink: #ffffff;
|
|
2901
|
+
--accent-soft: #2b2142;
|
|
2902
|
+
--ok: #75e0a7;
|
|
2903
|
+
--ok-soft: #102a20;
|
|
2904
|
+
--warn: #fec84b;
|
|
2905
|
+
--warn-soft: #302611;
|
|
2906
|
+
--err: #fda29b;
|
|
2907
|
+
--err-soft: #321817;
|
|
2908
|
+
--code-key: #c3a6ff;
|
|
2909
|
+
--code-string: #75e0a7;
|
|
2910
|
+
--code-number: #84adff;
|
|
2911
|
+
--code-boolean: #f9a8d4;
|
|
2912
|
+
--code-null: #98a2b3;
|
|
2913
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
2914
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
2915
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
2687
2916
|
}
|
|
2688
2917
|
}
|
|
2689
2918
|
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
@@ -2701,13 +2930,17 @@ var renderAdminDocument = (service) => {
|
|
|
2701
2930
|
padding-right: env(safe-area-inset-right);
|
|
2702
2931
|
-webkit-text-size-adjust: 100%;
|
|
2703
2932
|
}
|
|
2704
|
-
|
|
2933
|
+
::selection { background: color-mix(in srgb, var(--accent) 28%, transparent); }
|
|
2934
|
+
:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px; }
|
|
2935
|
+
button, input, select, textarea { transition: border-color 140ms ease, box-shadow 140ms ease, transform 100ms ease; }
|
|
2936
|
+
button:disabled, input:disabled, select:disabled, textarea:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
2705
2937
|
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
2706
2938
|
header.top {
|
|
2707
|
-
position: sticky; top: 0; z-index:
|
|
2939
|
+
position: sticky; top: 0; z-index: 20;
|
|
2708
2940
|
display: flex; align-items: center; gap: 12px;
|
|
2709
|
-
min-height: var(--header); padding:
|
|
2710
|
-
background: var(--bg-elev);
|
|
2941
|
+
min-height: var(--header); padding: 12px 20px;
|
|
2942
|
+
background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
|
|
2943
|
+
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
2711
2944
|
border-bottom: 1px solid var(--line);
|
|
2712
2945
|
min-width: 0; max-width: 100%;
|
|
2713
2946
|
}
|
|
@@ -2715,12 +2948,21 @@ var renderAdminDocument = (service) => {
|
|
|
2715
2948
|
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
2716
2949
|
min-width: 0; max-width: 100%; margin-right: auto;
|
|
2717
2950
|
}
|
|
2718
|
-
.brand-
|
|
2719
|
-
.brand-
|
|
2951
|
+
.brand-lockup { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
2952
|
+
.brand-mark {
|
|
2953
|
+
width: 38px; height: 38px; display: grid; place-items: center; flex: none;
|
|
2954
|
+
border-radius: 11px; color: #fff;
|
|
2955
|
+
background: linear-gradient(145deg, #7f56d9 0%, #53389e 100%);
|
|
2956
|
+
box-shadow: 0 6px 16px rgb(105 65 198 / 0.24), inset 0 1px 0 rgb(255 255 255 / 0.25);
|
|
2957
|
+
}
|
|
2958
|
+
.brand-mark svg { width: 22px; height: 22px; }
|
|
2959
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; line-height: 1.25; }
|
|
2960
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
2720
2961
|
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
2721
2962
|
.vendor {
|
|
2722
2963
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
2723
2964
|
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
2965
|
+
padding: 5px 8px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-inset);
|
|
2724
2966
|
}
|
|
2725
2967
|
.vendor[hidden] { display: none; }
|
|
2726
2968
|
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
@@ -2732,12 +2974,17 @@ var renderAdminDocument = (service) => {
|
|
|
2732
2974
|
.vendor-sep { color: var(--faint); }
|
|
2733
2975
|
.vendor-link { color: inherit; text-decoration: none; }
|
|
2734
2976
|
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
2735
|
-
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items:
|
|
2736
|
-
|
|
2977
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
|
|
2978
|
+
.sync-status { align-self: center; display: flex; align-items: center; gap: 6px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
2979
|
+
.sync-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
|
|
2980
|
+
.sync-status.busy::before { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); animation: pulse 1.2s ease-in-out infinite; }
|
|
2981
|
+
label.field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; font-weight: 600; color: var(--faint); }
|
|
2737
2982
|
label.field input, label.field select, .controls input[type="password"] {
|
|
2738
|
-
background-color: var(--bg-
|
|
2739
|
-
padding:
|
|
2983
|
+
background-color: var(--bg-elev); border: 1px solid var(--line-strong); border-radius: 9px;
|
|
2984
|
+
padding: 8px 10px; min-height: 38px; line-height: 1.2; box-shadow: var(--shadow-xs);
|
|
2740
2985
|
}
|
|
2986
|
+
label.field input:hover, label.field select:hover, textarea:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--line-strong)); }
|
|
2987
|
+
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; }
|
|
2741
2988
|
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
2742
2989
|
the text fields in the same form. Paint it like those fields. */
|
|
2743
2990
|
label.field select {
|
|
@@ -2754,12 +3001,20 @@ var renderAdminDocument = (service) => {
|
|
|
2754
3001
|
dialog .field > textarea { width: 100%; }
|
|
2755
3002
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
2756
3003
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
.btn
|
|
3004
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
|
3005
|
+
border-radius: 9px; min-height: 38px; padding: 0 13px; border: 1px solid var(--line-strong);
|
|
3006
|
+
background: var(--bg-elev); color: var(--ink); font-size: 13px; font-weight: 600; line-height: 1;
|
|
3007
|
+
box-shadow: var(--shadow-xs);
|
|
3008
|
+
}
|
|
3009
|
+
.icon-btn { width: 38px; padding: 0; }
|
|
3010
|
+
.icon-btn svg, .btn svg, .btn-primary svg { width: 16px; height: 16px; }
|
|
3011
|
+
.icon-btn:hover, .btn:hover { background: var(--bg-muted); border-color: var(--line-strong); }
|
|
3012
|
+
.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; }
|
|
3013
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
3014
|
+
.btn-quiet { background: transparent; border-color: transparent; box-shadow: none; }
|
|
3015
|
+
.btn-quiet:hover { background: var(--bg-muted); }
|
|
3016
|
+
.btn-danger { color: var(--err); border-color: color-mix(in srgb, var(--err) 25%, var(--line)); }
|
|
3017
|
+
.btn-danger:hover, .btn-danger.confirming { background: var(--err-soft); border-color: color-mix(in srgb, var(--err) 45%, var(--line)); }
|
|
2763
3018
|
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
2764
3019
|
so the press itself has to stay visible. */
|
|
2765
3020
|
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
@@ -2775,93 +3030,159 @@ var renderAdminDocument = (service) => {
|
|
|
2775
3030
|
flex: 1 0 auto; min-width: 0;
|
|
2776
3031
|
}
|
|
2777
3032
|
nav.side {
|
|
2778
|
-
|
|
3033
|
+
position: sticky; top: var(--header); align-self: start; height: calc(100vh - var(--header));
|
|
3034
|
+
border-right: 1px solid var(--line); padding: 18px 12px;
|
|
2779
3035
|
display: flex; flex-direction: column; gap: 4px;
|
|
2780
|
-
background: var(--bg-elev);
|
|
3036
|
+
background: var(--bg-elev); overflow-y: auto;
|
|
2781
3037
|
}
|
|
3038
|
+
.nav-label { padding: 0 10px 8px; color: var(--faint); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
|
|
2782
3039
|
nav.side button {
|
|
3040
|
+
position: relative; display: flex; align-items: center; gap: 10px; width: 100%;
|
|
2783
3041
|
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
2784
|
-
padding: 9px 10px; min-height:
|
|
2785
|
-
}
|
|
2786
|
-
nav.side button
|
|
2787
|
-
|
|
2788
|
-
.
|
|
2789
|
-
.
|
|
2790
|
-
.
|
|
3042
|
+
padding: 9px 10px; min-height: 42px; color: var(--muted); font-size: 13px; font-weight: 550;
|
|
3043
|
+
}
|
|
3044
|
+
nav.side button:hover { background: var(--bg-muted); color: var(--ink); }
|
|
3045
|
+
nav.side button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 650; }
|
|
3046
|
+
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); }
|
|
3047
|
+
.nav-icon { width: 18px; height: 18px; display: grid; place-items: center; flex: none; }
|
|
3048
|
+
.nav-icon svg { width: 18px; height: 18px; }
|
|
3049
|
+
main { padding: 32px clamp(20px, 4vw, 48px) 64px; max-width: 1320px; width: 100%; min-width: 0; }
|
|
3050
|
+
main[aria-busy="true"] .view { opacity: 0.62; }
|
|
3051
|
+
.view { animation: view-in 180ms ease-out; }
|
|
3052
|
+
.view[hidden] { display: none; }
|
|
3053
|
+
.view-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
|
|
3054
|
+
.view h2 { margin: 0 0 5px; font-size: clamp(24px, 3vw, 30px); line-height: 1.2; letter-spacing: -0.035em; }
|
|
3055
|
+
.view h3 { margin: 24px 0 10px; font-size: 15px; letter-spacing: -0.01em; }
|
|
3056
|
+
.lede { margin: 0; color: var(--muted); max-width: 72ch; font-size: 14px; }
|
|
3057
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 175px), 1fr)); gap: 14px; }
|
|
2791
3058
|
.card {
|
|
2792
3059
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
2793
|
-
padding:
|
|
3060
|
+
padding: 18px; box-shadow: var(--shadow-sm);
|
|
2794
3061
|
}
|
|
2795
|
-
.card
|
|
2796
|
-
.card
|
|
3062
|
+
.stat-card { position: relative; overflow: hidden; }
|
|
3063
|
+
.stat-card::after { content: ""; position: absolute; inset: 0 0 auto; height: 2px; background: linear-gradient(90deg, var(--accent), transparent 78%); opacity: 0.7; }
|
|
3064
|
+
.card .k { color: var(--faint); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
3065
|
+
.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; }
|
|
2797
3066
|
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
2798
|
-
.
|
|
3067
|
+
.section-card { background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-xs); overflow: hidden; }
|
|
3068
|
+
.section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
|
|
3069
|
+
.section-title h3 { margin: 0; }
|
|
3070
|
+
.split { display: grid; grid-template-columns: minmax(0, 250px) minmax(0, 1fr); gap: 18px; align-items: start; }
|
|
3071
|
+
#collections { padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); box-shadow: var(--shadow-xs); }
|
|
2799
3072
|
.collection {
|
|
2800
3073
|
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
2801
|
-
border-radius:
|
|
3074
|
+
border-radius: 9px; padding: 10px 11px; min-height: 48px; color: var(--ink); cursor: pointer;
|
|
2802
3075
|
}
|
|
2803
|
-
.collection
|
|
3076
|
+
.collection:hover { background: var(--bg-muted); }
|
|
3077
|
+
.collection[aria-current="true"] { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 18%, var(--line)); color: var(--accent); }
|
|
2804
3078
|
.collection small { display: block; color: var(--faint); }
|
|
2805
3079
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
2806
|
-
.chip {
|
|
2807
|
-
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
2808
|
-
border-radius: 999px; padding: 4px 8px;
|
|
2809
|
-
}
|
|
3080
|
+
.chip { font-family: var(--mono); font-size: 11px; background: var(--bg-muted); border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; }
|
|
2810
3081
|
.chip i { color: var(--faint); font-style: normal; }
|
|
2811
3082
|
.sql-layout {
|
|
2812
3083
|
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
2813
3084
|
}
|
|
2814
|
-
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
2815
|
-
.sql-tables button { text-align: left; }
|
|
3085
|
+
.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); }
|
|
3086
|
+
.sql-tables button { text-align: left; border: 0; border-radius: 8px; padding: 9px 10px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
3087
|
+
.sql-tables button:hover, .sql-tables button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); }
|
|
2816
3088
|
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
2817
|
-
table { width: 100%; border-
|
|
2818
|
-
|
|
2819
|
-
th {
|
|
3089
|
+
.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); }
|
|
3090
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
3091
|
+
th, td { text-align: left; padding: 11px 13px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
3092
|
+
th { color: var(--faint); background: var(--bg-inset); font-weight: 650; font-size: 11px; text-transform: uppercase; letter-spacing: 0.045em; }
|
|
3093
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
2820
3094
|
tr.clickable { cursor: pointer; }
|
|
2821
|
-
tr.clickable:hover { background: var(--
|
|
3095
|
+
tr.clickable:hover { background: var(--accent-soft); }
|
|
2822
3096
|
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
2823
3097
|
pre {
|
|
2824
3098
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
2825
|
-
background: var(--bg-inset); border-radius: 10px; padding:
|
|
2826
|
-
}
|
|
2827
|
-
.
|
|
3099
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 10px; padding: 14px; font-size: 12px; line-height: 1.65;
|
|
3100
|
+
}
|
|
3101
|
+
.json-key { color: var(--code-key); }
|
|
3102
|
+
.json-string { color: var(--code-string); }
|
|
3103
|
+
.json-number { color: var(--code-number); }
|
|
3104
|
+
.json-boolean { color: var(--code-boolean); }
|
|
3105
|
+
.json-null { color: var(--code-null); font-style: italic; }
|
|
3106
|
+
.json-inline { display: block; max-width: 44rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
3107
|
+
.json-view { position: relative; max-height: 26rem; overflow: auto; }
|
|
3108
|
+
.json-editor { position: relative; min-height: 190px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--bg-inset); overflow: hidden; }
|
|
3109
|
+
.json-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); }
|
|
3110
|
+
.json-editor.invalid { border-color: var(--err); }
|
|
3111
|
+
.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; }
|
|
3112
|
+
.json-editor pre { position: absolute; inset: 0; pointer-events: none; background: transparent; }
|
|
3113
|
+
.json-editor textarea { position: relative; z-index: 1; resize: vertical; background: transparent; color: transparent; caret-color: var(--ink); -webkit-text-fill-color: transparent; }
|
|
3114
|
+
.json-editor textarea::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); }
|
|
3115
|
+
.editor-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; }
|
|
3116
|
+
.json-status { display: inline-flex; align-items: center; gap: 6px; color: var(--ok); font-size: 11px; }
|
|
3117
|
+
.json-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
3118
|
+
.json-status.invalid { color: var(--err); }
|
|
3119
|
+
.format-json { border: 0; padding: 3px 5px; background: transparent; color: var(--accent); font-size: 11px; font-weight: 650; cursor: pointer; }
|
|
3120
|
+
.row-actions { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 14px 0; }
|
|
2828
3121
|
.banner {
|
|
2829
|
-
display: none;
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
3122
|
+
display: none; position: sticky; top: calc(var(--header) + 12px); z-index: 15;
|
|
3123
|
+
align-items: center; gap: 10px; margin: 0 0 16px; padding: 11px 12px 11px 14px;
|
|
3124
|
+
border: 1px solid color-mix(in srgb, var(--err) 30%, var(--line)); border-radius: 10px;
|
|
3125
|
+
background: var(--err-soft); color: var(--err); box-shadow: var(--shadow-sm); font-size: 13px;
|
|
3126
|
+
}
|
|
3127
|
+
.banner.show { display: flex; }
|
|
3128
|
+
.banner span { flex: 1; }
|
|
3129
|
+
.banner button { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 7px; background: transparent; color: currentColor; cursor: pointer; }
|
|
3130
|
+
.banner button:hover { background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
3131
|
+
.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); }
|
|
2834
3132
|
dialog {
|
|
2835
|
-
border: 1px solid var(--line); border-radius:
|
|
2836
|
-
color: var(--ink); width: min(
|
|
2837
|
-
}
|
|
2838
|
-
dialog
|
|
2839
|
-
dialog
|
|
3133
|
+
border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--bg-elev);
|
|
3134
|
+
color: var(--ink); width: min(720px, calc(100% - 24px)); max-width: calc(100vw - 24px); box-shadow: var(--shadow-lg);
|
|
3135
|
+
}
|
|
3136
|
+
dialog[open] { animation: dialog-in 160ms ease-out; }
|
|
3137
|
+
dialog::backdrop { background: rgb(12 14 20 / 0.58); backdrop-filter: blur(3px); }
|
|
3138
|
+
dialog form, .dialog-body { padding: 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
3139
|
+
.dialog-heading { display: flex; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
|
3140
|
+
.dialog-heading strong { font-size: 18px; letter-spacing: -0.02em; }
|
|
3141
|
+
.dialog-heading span { color: var(--muted); font-size: 12px; }
|
|
2840
3142
|
textarea {
|
|
2841
3143
|
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
2842
3144
|
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
2843
3145
|
font-family: var(--mono); font-size: 12px;
|
|
2844
3146
|
}
|
|
2845
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
2846
|
-
.
|
|
3147
|
+
.dialog-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding-top: 4px; }
|
|
3148
|
+
.dialog-actions .btn-danger { margin-right: auto; }
|
|
3149
|
+
.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); }
|
|
2847
3150
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
2848
3151
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
2849
3152
|
.status-2 { color: var(--ok); }
|
|
2850
3153
|
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
2851
3154
|
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
3155
|
+
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
3156
|
+
.form-grid .wide { grid-column: span 2; }
|
|
2852
3157
|
.route {
|
|
2853
|
-
display: grid; grid-template-columns:
|
|
2854
|
-
padding:
|
|
2855
|
-
}
|
|
2856
|
-
.
|
|
3158
|
+
display: grid; grid-template-columns: 84px minmax(0, 1fr) auto; gap: 10px; align-items: center;
|
|
3159
|
+
padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--bg-elev);
|
|
3160
|
+
}
|
|
3161
|
+
.route:first-child { border-radius: var(--radius) var(--radius) 0 0; }
|
|
3162
|
+
.route:last-child { border-bottom: 0; border-radius: 0 0 var(--radius) var(--radius); }
|
|
3163
|
+
.route-tools { display: flex; align-items: flex-end; gap: 12px; margin-bottom: 12px; }
|
|
3164
|
+
.route-tools .field { flex: 1; }
|
|
3165
|
+
.route-tools input { width: 100%; }
|
|
3166
|
+
.route-count { padding-bottom: 10px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
3167
|
+
#route-list { max-height: 34rem; border: 1px solid var(--line); border-radius: var(--radius); overflow: auto; box-shadow: var(--shadow-xs); }
|
|
3168
|
+
.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; }
|
|
3169
|
+
@keyframes view-in { from { transform: translateY(3px); } to { transform: translateY(0); } }
|
|
3170
|
+
@keyframes dialog-in { from { transform: translateY(8px) scale(0.99); } to { transform: translateY(0) scale(1); } }
|
|
3171
|
+
@keyframes pulse { 50% { opacity: 0.45; } }
|
|
3172
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
|
|
3173
|
+
@media (forced-colors: active) { .json-editor pre { display: none; } .json-editor textarea { color: CanvasText; -webkit-text-fill-color: CanvasText; } }
|
|
2857
3174
|
@media (max-width: 800px) {
|
|
3175
|
+
:root { --header: 0px; }
|
|
2858
3176
|
header.top {
|
|
2859
|
-
align-items: stretch; flex-direction: column;
|
|
2860
|
-
padding
|
|
3177
|
+
position: relative; align-items: stretch; flex-direction: column;
|
|
3178
|
+
padding: max(12px, env(safe-area-inset-top)) 14px 12px;
|
|
2861
3179
|
}
|
|
2862
3180
|
input, select, textarea { font-size: 16px; }
|
|
2863
3181
|
.brand, .vendor, .controls { width: 100%; }
|
|
3182
|
+
.brand { gap: 10px; }
|
|
3183
|
+
.vendor { order: 2; border-radius: 9px; }
|
|
2864
3184
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
3185
|
+
.sync-status { display: none; }
|
|
2865
3186
|
/* The layout fills the screen. A single-column grid stretches every auto
|
|
2866
3187
|
row, so the tab bar grew with the leftover height and the buttons floated
|
|
2867
3188
|
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
@@ -2871,14 +3192,23 @@ var renderAdminDocument = (service) => {
|
|
|
2871
3192
|
align-content: start;
|
|
2872
3193
|
}
|
|
2873
3194
|
nav.side {
|
|
2874
|
-
align-self: start; height: auto;
|
|
3195
|
+
position: sticky; top: 0; z-index: 12; align-self: start; height: auto;
|
|
2875
3196
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
2876
3197
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
2877
|
-
padding: 8px;
|
|
3198
|
+
padding: 8px; box-shadow: var(--shadow-xs);
|
|
2878
3199
|
}
|
|
2879
|
-
nav
|
|
3200
|
+
.nav-label { display: none; }
|
|
3201
|
+
nav.side button { width: auto; white-space: nowrap; flex: none; }
|
|
3202
|
+
nav.side button[aria-current="true"]::before { inset: auto 9px -8px; width: auto; height: 3px; border-radius: 3px 3px 0 0; }
|
|
2880
3203
|
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
2881
|
-
|
|
3204
|
+
.form-grid { grid-template-columns: minmax(0, 1fr); }
|
|
3205
|
+
.form-grid .wide { grid-column: auto; }
|
|
3206
|
+
main { padding: 22px 14px 48px; }
|
|
3207
|
+
.view-head { margin-bottom: 18px; }
|
|
3208
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
3209
|
+
.card { padding: 14px; }
|
|
3210
|
+
.card .v { font-size: 20px; }
|
|
3211
|
+
.table-shell { border: 0; background: transparent; box-shadow: none; overflow: visible; }
|
|
2882
3212
|
table, thead, tbody, tr, th, td { display: block; }
|
|
2883
3213
|
thead { display: none; }
|
|
2884
3214
|
tr {
|
|
@@ -2893,50 +3223,63 @@ var renderAdminDocument = (service) => {
|
|
|
2893
3223
|
.route { grid-template-columns: 72px 1fr; }
|
|
2894
3224
|
.route button { grid-column: 1 / -1; }
|
|
2895
3225
|
}
|
|
3226
|
+
@media (max-width: 460px) {
|
|
3227
|
+
.controls label.field { min-width: calc(50% - 4px); }
|
|
3228
|
+
.controls .icon-btn, .controls .btn { flex: 1; }
|
|
3229
|
+
.grid { grid-template-columns: minmax(0, 1fr); }
|
|
3230
|
+
.dialog-actions { flex-wrap: wrap; }
|
|
3231
|
+
.dialog-actions button { flex: 1; }
|
|
3232
|
+
.dialog-actions .btn-danger { flex-basis: 100%; margin-right: 0; }
|
|
3233
|
+
}
|
|
2896
3234
|
</style>
|
|
2897
3235
|
</head>
|
|
2898
3236
|
<body>
|
|
2899
3237
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
2900
3238
|
<header class="top">
|
|
2901
3239
|
<div class="brand">
|
|
2902
|
-
<div class="brand-
|
|
2903
|
-
<
|
|
2904
|
-
|
|
3240
|
+
<div class="brand-lockup">
|
|
3241
|
+
<div class="brand-mark" aria-hidden="true">
|
|
3242
|
+
<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>
|
|
3243
|
+
</div>
|
|
3244
|
+
<div class="brand-id">
|
|
3245
|
+
<strong>${name}</strong>
|
|
3246
|
+
<span>Mock admin</span>
|
|
3247
|
+
</div>
|
|
2905
3248
|
</div>
|
|
2906
3249
|
<div class="vendor" id="vendor" hidden></div>
|
|
2907
3250
|
</div>
|
|
2908
3251
|
<div class="controls">
|
|
3252
|
+
<span class="sync-status" id="sync-status" role="status" aria-live="polite">Ready</span>
|
|
2909
3253
|
<label class="field">Namespace
|
|
2910
3254
|
<select id="namespace" aria-label="Namespace"></select>
|
|
2911
3255
|
</label>
|
|
2912
3256
|
<label class="field">Admin key
|
|
2913
3257
|
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
2914
3258
|
</label>
|
|
2915
|
-
<button class="icon-btn" id="theme" type="button" aria-label="
|
|
2916
|
-
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
3259
|
+
<button class="icon-btn" id="theme" type="button" aria-label="Use dark theme" title="Use dark theme"></button>
|
|
3260
|
+
<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>
|
|
2917
3261
|
</div>
|
|
2918
3262
|
</header>
|
|
2919
3263
|
<div class="layout">
|
|
2920
3264
|
<nav class="side" id="nav" aria-label="Admin sections">
|
|
2921
|
-
<
|
|
2922
|
-
<button type="button" data-view="
|
|
2923
|
-
<button type="button" data-view="
|
|
2924
|
-
<button type="button" data-view="
|
|
2925
|
-
<button type="button" data-view="
|
|
2926
|
-
<button type="button" data-view="
|
|
3265
|
+
<span class="nav-label">Workspace</span>
|
|
3266
|
+
<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>
|
|
3267
|
+
<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>
|
|
3268
|
+
<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>
|
|
3269
|
+
<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>
|
|
3270
|
+
<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>
|
|
3271
|
+
<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>
|
|
2927
3272
|
</nav>
|
|
2928
3273
|
<main id="views">
|
|
2929
|
-
<
|
|
3274
|
+
<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>
|
|
2930
3275
|
<section class="view" id="view-overview">
|
|
2931
|
-
<h2>Overview</h2>
|
|
2932
|
-
<p class="lede">Health, clock, and the collections this namespace is holding.</p>
|
|
3276
|
+
<div class="view-head"><div><h2>Overview</h2><p class="lede">Health, clock, and the collections this namespace is holding.</p></div></div>
|
|
2933
3277
|
<div class="grid" id="overview-cards"></div>
|
|
2934
3278
|
<h3>Recent requests</h3>
|
|
2935
3279
|
<div id="overview-requests"></div>
|
|
2936
3280
|
</section>
|
|
2937
3281
|
<section class="view" id="view-state" hidden>
|
|
2938
|
-
<h2>State</h2>
|
|
2939
|
-
<p class="lede">Every collection in this namespace. Declared fields stay visible before any row exists; stored rows add whatever else they contain.</p>
|
|
3282
|
+
<div class="view-head"><div><h2>State</h2><p class="lede">Inspect and edit every collection in this namespace.</p></div></div>
|
|
2940
3283
|
<div class="split">
|
|
2941
3284
|
<div id="collections"></div>
|
|
2942
3285
|
<div>
|
|
@@ -2949,8 +3292,7 @@ var renderAdminDocument = (service) => {
|
|
|
2949
3292
|
</div>
|
|
2950
3293
|
</section>
|
|
2951
3294
|
<section class="view" id="view-clock" hidden>
|
|
2952
|
-
<h2>Clock</h2>
|
|
2953
|
-
<p class="lede">The clock every timestamp in this mock reads.</p>
|
|
3295
|
+
<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>
|
|
2954
3296
|
<div class="card" id="clock-readout"></div>
|
|
2955
3297
|
<div class="row-actions">
|
|
2956
3298
|
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
@@ -2969,21 +3311,22 @@ var renderAdminDocument = (service) => {
|
|
|
2969
3311
|
</form>
|
|
2970
3312
|
</section>
|
|
2971
3313
|
<section class="view" id="view-faults" hidden>
|
|
2972
|
-
<h2>Faults</h2>
|
|
2973
|
-
<p class="lede">Inject a response, a delay, or a named preset. Rules apply to this namespace.</p>
|
|
3314
|
+
<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>
|
|
2974
3315
|
<form id="fault-form" class="card stack">
|
|
2975
|
-
<
|
|
2976
|
-
<
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
<
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
<
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
<
|
|
2986
|
-
|
|
3316
|
+
<div class="form-grid">
|
|
3317
|
+
<label class="field wide">Preset
|
|
3318
|
+
<select id="fault-preset"><option value="">Custom rule</option></select>
|
|
3319
|
+
</label>
|
|
3320
|
+
<label class="field">Status
|
|
3321
|
+
<input id="fault-status" inputmode="numeric" placeholder="503">
|
|
3322
|
+
</label>
|
|
3323
|
+
<label class="field wide">Path prefix
|
|
3324
|
+
<input id="fault-path" placeholder="/v1">
|
|
3325
|
+
</label>
|
|
3326
|
+
<label class="field">Method
|
|
3327
|
+
<input id="fault-method" placeholder="GET">
|
|
3328
|
+
</label>
|
|
3329
|
+
</div>
|
|
2987
3330
|
<div class="dialog-actions">
|
|
2988
3331
|
<button class="btn-primary" type="submit">Add fault</button>
|
|
2989
3332
|
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
@@ -2992,26 +3335,31 @@ var renderAdminDocument = (service) => {
|
|
|
2992
3335
|
<div id="fault-list"></div>
|
|
2993
3336
|
</section>
|
|
2994
3337
|
<section class="view" id="view-journal" hidden>
|
|
2995
|
-
<h2>Journal</h2>
|
|
2996
|
-
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
2997
|
-
<div class="row-actions">
|
|
2998
|
-
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
2999
|
-
</div>
|
|
3338
|
+
<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>
|
|
3000
3339
|
<div id="journal"></div>
|
|
3001
3340
|
</section>
|
|
3002
3341
|
<section class="view" id="view-routes" hidden>
|
|
3003
|
-
<h2>Routes</h2>
|
|
3004
|
-
<
|
|
3342
|
+
<div class="view-head"><div><h2>Routes</h2><p class="lede">Explore shared controls and service-specific admin endpoints.</p></div></div>
|
|
3343
|
+
<div class="route-tools">
|
|
3344
|
+
<label class="field">Filter routes
|
|
3345
|
+
<input id="route-search" type="search" placeholder="Method, path, or extension" autocomplete="off">
|
|
3346
|
+
</label>
|
|
3347
|
+
<span class="route-count" id="route-count"></span>
|
|
3348
|
+
</div>
|
|
3005
3349
|
<div id="route-list"></div>
|
|
3006
|
-
<form id="route-form" class="card stack">
|
|
3350
|
+
<form id="route-form" class="card stack" style="margin-top: 18px">
|
|
3007
3351
|
<strong id="route-title">Select a route</strong>
|
|
3008
3352
|
<label class="field">JSON body
|
|
3009
|
-
<
|
|
3353
|
+
<div class="json-editor" id="route-editor" hidden>
|
|
3354
|
+
<pre aria-hidden="true"><code id="route-highlight"></code></pre>
|
|
3355
|
+
<textarea id="route-body" placeholder="{ }" spellcheck="false" aria-describedby="route-json-status" disabled></textarea>
|
|
3356
|
+
</div>
|
|
3357
|
+
<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>
|
|
3010
3358
|
</label>
|
|
3011
3359
|
<div class="dialog-actions">
|
|
3012
|
-
<button class="btn-primary" type="submit">Send</button>
|
|
3360
|
+
<button class="btn-primary" id="route-send" type="submit" disabled>Send request</button>
|
|
3013
3361
|
</div>
|
|
3014
|
-
<pre id="route-result">Response will show here.</pre>
|
|
3362
|
+
<pre class="json-view" id="route-result"><code>Response will show here.</code></pre>
|
|
3015
3363
|
</form>
|
|
3016
3364
|
</section>
|
|
3017
3365
|
</main>
|
|
@@ -3019,12 +3367,16 @@ var renderAdminDocument = (service) => {
|
|
|
3019
3367
|
</div>
|
|
3020
3368
|
<dialog id="editor">
|
|
3021
3369
|
<form method="dialog" id="editor-form">
|
|
3022
|
-
<strong id="editor-title">Record</strong>
|
|
3370
|
+
<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>
|
|
3023
3371
|
<label class="field">Id
|
|
3024
3372
|
<input id="editor-id" required>
|
|
3025
3373
|
</label>
|
|
3026
3374
|
<label class="field">JSON value
|
|
3027
|
-
<
|
|
3375
|
+
<div class="json-editor" id="record-editor">
|
|
3376
|
+
<pre aria-hidden="true"><code id="editor-highlight"></code></pre>
|
|
3377
|
+
<textarea id="editor-value" spellcheck="false" aria-describedby="editor-json-status"></textarea>
|
|
3378
|
+
</div>
|
|
3379
|
+
<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>
|
|
3028
3380
|
</label>
|
|
3029
3381
|
<div class="dialog-actions">
|
|
3030
3382
|
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
@@ -3066,8 +3418,8 @@ var describeExtension = (extension) => {
|
|
|
3066
3418
|
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
3067
3419
|
};
|
|
3068
3420
|
};
|
|
3069
|
-
var adminUiRoutes = (service, ui) => {
|
|
3070
|
-
const shell = () => renderAdminDocument(service);
|
|
3421
|
+
var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
3422
|
+
const shell = () => renderAdminDocument(service, adminPrefix);
|
|
3071
3423
|
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
3072
3424
|
return {
|
|
3073
3425
|
"GET /ui": () => htmlResponse(document3()),
|
|
@@ -3988,10 +4340,13 @@ var stateAdminRoutes = (routes) => {
|
|
|
3988
4340
|
};
|
|
3989
4341
|
|
|
3990
4342
|
// ../core/dist/version.js
|
|
3991
|
-
var PACKAGE_VERSION = true ? "
|
|
4343
|
+
var PACKAGE_VERSION = true ? "2.0.0" : UNRELEASED_VERSION;
|
|
3992
4344
|
|
|
3993
4345
|
// ../core/dist/webhooks.js
|
|
3994
|
-
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4346
|
+
var json4 = (status, body) => new Response(JSON.stringify(body), {
|
|
4347
|
+
status,
|
|
4348
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
4349
|
+
});
|
|
3995
4350
|
var adminError3 = (status, message) => json4(status, { error: { type: "mockingbird_admin", message } });
|
|
3996
4351
|
var isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3997
4352
|
var parseEndpoint = (value) => {
|
|
@@ -4099,7 +4454,6 @@ var AT_HEADER = "x-mockingbird-at";
|
|
|
4099
4454
|
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
4100
4455
|
var DEFAULT_NAMESPACE = "default";
|
|
4101
4456
|
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4102
|
-
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
4103
4457
|
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4104
4458
|
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
4105
4459
|
var effects = /* @__PURE__ */ new WeakMap();
|
|
@@ -4138,6 +4492,22 @@ var operationMatcher = (document3) => {
|
|
|
4138
4492
|
return (request, path) => matchers.find((m) => m.method === request.method && m.pattern.test(path))?.operationId;
|
|
4139
4493
|
};
|
|
4140
4494
|
var createRuntime = (options) => {
|
|
4495
|
+
const adminPrefix = resolveAdminPrefix(options.adminPrefix);
|
|
4496
|
+
const internalPaths = Object.entries(options.document?.paths ?? {}).filter(([, item]) => item["x-mockingbird-internal"] === true).map(([path]) => path);
|
|
4497
|
+
for (const path of internalPaths) {
|
|
4498
|
+
if (!path.startsWith(`${ADMIN_PREFIX}/blobs/`))
|
|
4499
|
+
throw new Error(`internal transport ${path} must live under ${ADMIN_PREFIX}/blobs/`);
|
|
4500
|
+
}
|
|
4501
|
+
assertAdminPrefixAvailable(adminPrefix, Object.keys(options.document?.paths ?? {}).filter((path) => !internalPaths.includes(path)));
|
|
4502
|
+
const internalMatch = (path) => {
|
|
4503
|
+
for (const pattern of internalPaths) {
|
|
4504
|
+
const actual = path.split("/");
|
|
4505
|
+
const expected = pattern.split("/");
|
|
4506
|
+
if (actual.length === expected.length && expected.every((part, i) => part.startsWith("{") || part === actual[i]))
|
|
4507
|
+
return pattern;
|
|
4508
|
+
}
|
|
4509
|
+
return void 0;
|
|
4510
|
+
};
|
|
4141
4511
|
assertAdminUi(options.adminUi);
|
|
4142
4512
|
const sqlite = bootSqlite(options.sqlite);
|
|
4143
4513
|
const clock = options.clock ?? createClock();
|
|
@@ -4183,6 +4553,7 @@ var createRuntime = (options) => {
|
|
|
4183
4553
|
const created = options.create({
|
|
4184
4554
|
namespace: storageNamespace(key),
|
|
4185
4555
|
publicNamespace,
|
|
4556
|
+
adminPrefix,
|
|
4186
4557
|
sqlite,
|
|
4187
4558
|
clock: namespaceClock(publicNamespace),
|
|
4188
4559
|
rng: createdContextRng
|
|
@@ -4415,10 +4786,10 @@ var createRuntime = (options) => {
|
|
|
4415
4786
|
namespaceClock,
|
|
4416
4787
|
namespaceOf(request) {
|
|
4417
4788
|
const url = new URL(request.url);
|
|
4418
|
-
const prefixed =
|
|
4789
|
+
const prefixed = matchNamespacePath(url.pathname, adminPrefix);
|
|
4419
4790
|
const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
|
|
4420
4791
|
const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
|
|
4421
|
-
if (path
|
|
4792
|
+
if (isAdminPath(path, adminPrefix))
|
|
4422
4793
|
return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
|
|
4423
4794
|
if (selected !== void 0)
|
|
4424
4795
|
return selected;
|
|
@@ -4484,7 +4855,7 @@ var createRuntime = (options) => {
|
|
|
4484
4855
|
state: (namespace = DEFAULT_NAMESPACE) => inspectState(stateScope(namespace)),
|
|
4485
4856
|
fetch: async (incoming) => {
|
|
4486
4857
|
let request = incoming;
|
|
4487
|
-
const prefixed =
|
|
4858
|
+
const prefixed = matchNamespacePath(new URL(request.url).pathname, adminPrefix);
|
|
4488
4859
|
if (prefixed) {
|
|
4489
4860
|
const url2 = new URL(request.url);
|
|
4490
4861
|
url2.pathname = prefixed[2] ?? "/";
|
|
@@ -4500,7 +4871,15 @@ var createRuntime = (options) => {
|
|
|
4500
4871
|
signal: request.signal
|
|
4501
4872
|
});
|
|
4502
4873
|
}
|
|
4503
|
-
|
|
4874
|
+
const internalUrl = new URL(request.url);
|
|
4875
|
+
const internalPath = isAdminPath(internalUrl.pathname, adminPrefix) ? `${ADMIN_PREFIX}${internalUrl.pathname.slice(adminPrefix.length)}` : "";
|
|
4876
|
+
const internal = internalMatch(internalPath);
|
|
4877
|
+
const obsoleteInternalPath = adminPrefix !== ADMIN_PREFIX && internalMatch(internalUrl.pathname) && !internal;
|
|
4878
|
+
if (internal) {
|
|
4879
|
+
internalUrl.pathname = internalPath;
|
|
4880
|
+
request = new Request(internalUrl, request);
|
|
4881
|
+
}
|
|
4882
|
+
let namespace = internal ? internalUrl.searchParams.get("namespace") ?? control.namespaceOf(request) : control.namespaceOf(request);
|
|
4504
4883
|
if (!request.headers.has(NAMESPACE_HEADER) && options.credential) {
|
|
4505
4884
|
const credential = options.credential(request);
|
|
4506
4885
|
const mapped = credential !== void 0 ? credentials.get(credential) : void 0;
|
|
@@ -4520,7 +4899,9 @@ var createRuntime = (options) => {
|
|
|
4520
4899
|
return copy;
|
|
4521
4900
|
}
|
|
4522
4901
|
};
|
|
4523
|
-
|
|
4902
|
+
if (obsoleteInternalPath)
|
|
4903
|
+
return stamp(adminFail(404, "internal transport is only available under adminPrefix"));
|
|
4904
|
+
const handled = internal ? void 0 : await control.handle(request);
|
|
4524
4905
|
if (handled)
|
|
4525
4906
|
return stamp(handled);
|
|
4526
4907
|
const started = monotonicNow();
|
|
@@ -4561,7 +4942,7 @@ var createRuntime = (options) => {
|
|
|
4561
4942
|
type: "mockingbird_admin",
|
|
4562
4943
|
message: `${NAMESPACE_HEADER} must match ${NAMESPACE_PATTERN}`
|
|
4563
4944
|
}
|
|
4564
|
-
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
4945
|
+
}), { status: 400, headers: { "content-type": "application/json; charset=utf-8" } }));
|
|
4565
4946
|
}
|
|
4566
4947
|
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
4567
4948
|
log(400);
|
|
@@ -4678,15 +5059,16 @@ var createRuntime = (options) => {
|
|
|
4678
5059
|
...options.admin?.(runtime) ?? {},
|
|
4679
5060
|
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
4680
5061
|
// maps `presets`, and a service route must not replace that list with a record.
|
|
4681
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
5062
|
+
...options.presets ? presetRoutes(options.presets, runtime, adminPrefix) : {},
|
|
4682
5063
|
...stateAdminRoutes({
|
|
4683
5064
|
open: stateScope,
|
|
4684
5065
|
afterWrite: (namespace) => {
|
|
4685
5066
|
checkpoint(namespace, "main");
|
|
4686
5067
|
}
|
|
4687
5068
|
}),
|
|
4688
|
-
...adminUiRoutes(options.name, options.adminUi)
|
|
5069
|
+
...adminUiRoutes(options.name, options.adminUi, adminPrefix)
|
|
4689
5070
|
},
|
|
5071
|
+
adminPrefix,
|
|
4690
5072
|
adminKey: options.adminKey
|
|
4691
5073
|
});
|
|
4692
5074
|
return defineMock(runtime);
|
|
@@ -4700,7 +5082,10 @@ var mutableResponse = (response) => {
|
|
|
4700
5082
|
return new Response(response.body, response);
|
|
4701
5083
|
}
|
|
4702
5084
|
};
|
|
4703
|
-
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5085
|
+
var adminJson = (status, body) => new Response(JSON.stringify(body), {
|
|
5086
|
+
status,
|
|
5087
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
5088
|
+
});
|
|
4704
5089
|
var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
|
|
4705
5090
|
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4706
5091
|
var credentialRoutes = (registry) => ({
|
|
@@ -4752,7 +5137,7 @@ var credentialRoutes = (registry) => ({
|
|
|
4752
5137
|
return adminJson(200, { status: "ok" });
|
|
4753
5138
|
}
|
|
4754
5139
|
});
|
|
4755
|
-
var presetRoutes = (presets, runtime) => ({
|
|
5140
|
+
var presetRoutes = (presets, runtime, adminPrefix) => ({
|
|
4756
5141
|
"GET /faults/presets": () => {
|
|
4757
5142
|
const body = {
|
|
4758
5143
|
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
@@ -4762,7 +5147,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
4762
5147
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
4763
5148
|
const name = params.name;
|
|
4764
5149
|
if (!presets[name])
|
|
4765
|
-
return adminFail(404, `no fault preset ${name}; GET /
|
|
5150
|
+
return adminFail(404, `no fault preset ${name}; GET ${adminPrefix}/faults/presets`);
|
|
4766
5151
|
const overrides = isObject(body) ? body : {};
|
|
4767
5152
|
return adminJson(201, { preset: name, rules: runtime.applyPreset(name, namespace, overrides) });
|
|
4768
5153
|
}
|
|
@@ -4838,6 +5223,7 @@ var createRuntime2 = (options = {}) => createRuntime({
|
|
|
4838
5223
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
4839
5224
|
...options.clock ? { clock: options.clock } : {},
|
|
4840
5225
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
5226
|
+
...options.adminPrefix !== void 0 ? { adminPrefix: options.adminPrefix } : {},
|
|
4841
5227
|
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
4842
5228
|
...options.onLog ? { onLog: options.onLog } : {},
|
|
4843
5229
|
credential: accessKeyCredential,
|
|
@@ -5098,6 +5484,7 @@ var StepFunctionsAPI = class {
|
|
|
5098
5484
|
|
|
5099
5485
|
export {
|
|
5100
5486
|
createClock,
|
|
5487
|
+
resolveAdminPrefix,
|
|
5101
5488
|
parseDuration,
|
|
5102
5489
|
document2 as document,
|
|
5103
5490
|
operationIds,
|
|
@@ -5108,4 +5495,4 @@ export {
|
|
|
5108
5495
|
accessKeyCredential,
|
|
5109
5496
|
StepFunctionsAPI
|
|
5110
5497
|
};
|
|
5111
|
-
//# sourceMappingURL=chunk-
|
|
5498
|
+
//# sourceMappingURL=chunk-EBPLA3FJ.js.map
|