@crvouga/mockingbird-service-postgres 1.6.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 +35 -0
- package/COMPATIBILITY.md +4 -0
- package/DISCOVERY.md +48 -0
- package/README.md +7 -0
- package/dist/admin.js +629 -235
- package/dist/admin.js.map +3 -3
- package/dist/server.d.ts +9 -0
- package/dist/server.js +25808 -0
- package/dist/server.js.map +7 -0
- package/dist/wire/cli.js +1215 -107
- package/dist/wire/cli.js.map +4 -4
- package/dist/wire/cluster.d.ts +3 -0
- package/dist/wire/fleet.d.ts +81 -0
- package/dist/wire/index.d.ts +6 -0
- package/dist/wire/index.js +23 -3
- package/dist/wire/index.js.map +2 -2
- package/dist/wire/probe.d.ts +2 -0
- package/package.json +31 -3
package/dist/admin.js
CHANGED
|
@@ -375,6 +375,34 @@ dialog:not([open]) {
|
|
|
375
375
|
function isRecord(value) {
|
|
376
376
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
377
377
|
}
|
|
378
|
+
function highlightJsonText(text) {
|
|
379
|
+
const escapeToken = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
380
|
+
switch (char) {
|
|
381
|
+
case "&":
|
|
382
|
+
return "&";
|
|
383
|
+
case "<":
|
|
384
|
+
return "<";
|
|
385
|
+
case ">":
|
|
386
|
+
return ">";
|
|
387
|
+
case '"':
|
|
388
|
+
return """;
|
|
389
|
+
default:
|
|
390
|
+
return "'";
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
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;
|
|
394
|
+
let cursor = 0;
|
|
395
|
+
let html = "";
|
|
396
|
+
for (const match of text.matchAll(token)) {
|
|
397
|
+
const index = match.index ?? 0;
|
|
398
|
+
const value = match[0];
|
|
399
|
+
html += escapeToken(text.slice(cursor, index));
|
|
400
|
+
const kind = value.startsWith('"') ? value.trimEnd().endsWith(":") ? "key" : "string" : value === "true" || value === "false" ? "boolean" : value === "null" ? "null" : "number";
|
|
401
|
+
html += `<span class="json-${kind}">${escapeToken(value)}</span>`;
|
|
402
|
+
cursor = index + value.length;
|
|
403
|
+
}
|
|
404
|
+
return html + escapeToken(text.slice(cursor));
|
|
405
|
+
}
|
|
378
406
|
function mountAdminBrand(doc, host, service, catalog) {
|
|
379
407
|
const clip = (value, max) => {
|
|
380
408
|
if (typeof value !== "string")
|
|
@@ -736,14 +764,16 @@ function bootAdmin(config) {
|
|
|
736
764
|
const state = {
|
|
737
765
|
namespace: "default",
|
|
738
766
|
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
739
|
-
view: "overview",
|
|
767
|
+
view: ["overview", "state", "clock", "faults", "journal", "routes"].includes(location.hash.slice(1)) ? location.hash.slice(1) : "overview",
|
|
740
768
|
collection: "",
|
|
741
769
|
shape: null,
|
|
742
770
|
route: null,
|
|
771
|
+
routes: [],
|
|
743
772
|
editing: null
|
|
744
773
|
};
|
|
745
774
|
const standard = new Set(config.standardRoutes);
|
|
746
775
|
const banner = el("banner", HTMLElement);
|
|
776
|
+
const bannerMessage = el("banner-message", HTMLElement);
|
|
747
777
|
const keyInput = el("admin-key", HTMLInputElement);
|
|
748
778
|
keyInput.value = state.key;
|
|
749
779
|
const params = new URLSearchParams(location.search);
|
|
@@ -754,18 +784,21 @@ function bootAdmin(config) {
|
|
|
754
784
|
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
755
785
|
params.delete("key");
|
|
756
786
|
const next = params.toString();
|
|
757
|
-
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
787
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : "") + location.hash);
|
|
758
788
|
}
|
|
759
789
|
const queryNamespace = params.get("namespace");
|
|
760
790
|
if (queryNamespace)
|
|
761
791
|
state.namespace = queryNamespace;
|
|
762
792
|
const showError = (error) => {
|
|
763
|
-
|
|
793
|
+
bannerMessage.textContent = error instanceof Error ? error.message : String(error);
|
|
764
794
|
banner.classList.add("show");
|
|
795
|
+
banner.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
765
796
|
};
|
|
766
797
|
const clearError = () => {
|
|
767
798
|
banner.classList.remove("show");
|
|
799
|
+
bannerMessage.textContent = "";
|
|
768
800
|
};
|
|
801
|
+
el("banner-dismiss", HTMLButtonElement).addEventListener("click", clearError);
|
|
769
802
|
const api = async (method, path, body) => {
|
|
770
803
|
const headers = { accept: "application/json" };
|
|
771
804
|
if (state.key !== "")
|
|
@@ -809,16 +842,21 @@ function bootAdmin(config) {
|
|
|
809
842
|
namespace: () => state.namespace
|
|
810
843
|
};
|
|
811
844
|
const isPanelScript = (value) => typeof value === "function";
|
|
845
|
+
const themeButton = el("theme", HTMLButtonElement);
|
|
812
846
|
const setTheme = (theme) => {
|
|
813
847
|
if (theme)
|
|
814
848
|
document.documentElement.dataset.theme = theme;
|
|
815
849
|
else
|
|
816
850
|
delete document.documentElement.dataset.theme;
|
|
851
|
+
const dark = theme ? theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
852
|
+
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>';
|
|
853
|
+
const label = dark ? "Use light theme" : "Use dark theme";
|
|
854
|
+
themeButton.setAttribute("aria-label", label);
|
|
855
|
+
themeButton.title = label;
|
|
817
856
|
};
|
|
818
857
|
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
858
|
+
setTheme(storedTheme === "dark" || storedTheme === "light" ? storedTheme : null);
|
|
859
|
+
themeButton.addEventListener("click", () => {
|
|
822
860
|
const current = document.documentElement.dataset.theme;
|
|
823
861
|
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
824
862
|
const next = dark ? "light" : "dark";
|
|
@@ -837,6 +875,7 @@ function bootAdmin(config) {
|
|
|
837
875
|
continue;
|
|
838
876
|
section.hidden = section.id !== `view-${view}`;
|
|
839
877
|
}
|
|
878
|
+
history.replaceState(null, "", `${location.pathname}${location.search}#${encodeURIComponent(view)}`);
|
|
840
879
|
};
|
|
841
880
|
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
842
881
|
const target = event.target;
|
|
@@ -851,7 +890,7 @@ function bootAdmin(config) {
|
|
|
851
890
|
show(view);
|
|
852
891
|
refresh().catch(showError);
|
|
853
892
|
});
|
|
854
|
-
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>`;
|
|
893
|
+
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>`;
|
|
855
894
|
const table = (columns, rows) => {
|
|
856
895
|
if (rows.length === 0)
|
|
857
896
|
return '<p class="empty">Nothing here yet.</p>';
|
|
@@ -861,16 +900,22 @@ function bootAdmin(config) {
|
|
|
861
900
|
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
862
901
|
return `<tr${attrs}>${cells}</tr>`;
|
|
863
902
|
}).join("");
|
|
864
|
-
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
903
|
+
return `<div class="table-shell"><table><thead>${head}</thead><tbody>${body}</tbody></table></div>`;
|
|
865
904
|
};
|
|
866
905
|
const preview = (value) => {
|
|
867
906
|
const text = JSON.stringify(value) ?? "null";
|
|
868
|
-
|
|
907
|
+
const clipped = text.length > 180 ? `${text.slice(0, 180)}\u2026` : text;
|
|
908
|
+
return `<code class="json-inline">${highlightJsonText(clipped)}</code>`;
|
|
869
909
|
};
|
|
870
910
|
const when = (epoch) => {
|
|
871
911
|
const date = new Date(epoch);
|
|
872
912
|
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
873
913
|
};
|
|
914
|
+
const journalWhen = (value) => {
|
|
915
|
+
const numeric = value.trim() === "" ? Number.NaN : Number(value);
|
|
916
|
+
const epoch = Number.isFinite(numeric) ? numeric : Date.parse(value);
|
|
917
|
+
return Number.isFinite(epoch) ? when(epoch) : value;
|
|
918
|
+
};
|
|
874
919
|
const clockCard = (epoch) => {
|
|
875
920
|
const date = new Date(epoch);
|
|
876
921
|
if (Number.isNaN(date.getTime()))
|
|
@@ -990,7 +1035,7 @@ function bootAdmin(config) {
|
|
|
990
1035
|
};
|
|
991
1036
|
const renderOverview = async () => {
|
|
992
1037
|
const [health, shape, clock, journal] = await Promise.all([
|
|
993
|
-
originGet(
|
|
1038
|
+
originGet(`${config.adminPrefix}/health`),
|
|
994
1039
|
api("GET", "/state"),
|
|
995
1040
|
api("GET", "/clock"),
|
|
996
1041
|
api("GET", "/requests?limit=8")
|
|
@@ -1007,7 +1052,7 @@ function bootAdmin(config) {
|
|
|
1007
1052
|
},
|
|
1008
1053
|
{
|
|
1009
1054
|
k: "Status",
|
|
1010
|
-
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
1055
|
+
v: `<span class="badge ok">${esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")}</span>`
|
|
1011
1056
|
},
|
|
1012
1057
|
{ k: "Namespace", v: esc(state.namespace) },
|
|
1013
1058
|
{ k: "Collections", v: String(decoded.collections.length) },
|
|
@@ -1094,26 +1139,46 @@ function bootAdmin(config) {
|
|
|
1094
1139
|
const renderJournal = async () => {
|
|
1095
1140
|
const journal = await api("GET", "/requests?limit=100");
|
|
1096
1141
|
el("journal", HTMLElement).innerHTML = table([
|
|
1097
|
-
{ label: "When", cell: (row) => esc(row.at) },
|
|
1098
|
-
{
|
|
1142
|
+
{ label: "When", cell: (row) => esc(journalWhen(row.at)) },
|
|
1143
|
+
{
|
|
1144
|
+
label: "Method",
|
|
1145
|
+
cell: (row) => `<span class="method">${esc(row.method)}</span>`
|
|
1146
|
+
},
|
|
1099
1147
|
{ label: "Path", cell: (row) => esc(row.path) },
|
|
1100
|
-
{
|
|
1148
|
+
{
|
|
1149
|
+
label: "Status",
|
|
1150
|
+
cell: (row) => `<span class="badge ${row.status.startsWith("2") ? "ok" : row.status.startsWith("4") || row.status.startsWith("5") ? "warn" : ""}">${esc(row.status)}</span>`
|
|
1151
|
+
},
|
|
1101
1152
|
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
1102
1153
|
], decodeJournal(journal));
|
|
1103
1154
|
};
|
|
1104
|
-
const
|
|
1105
|
-
const
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1155
|
+
const paintRoutes = () => {
|
|
1156
|
+
const query = el("route-search", HTMLInputElement).value.trim().toLocaleLowerCase();
|
|
1157
|
+
const routes = state.routes.filter((route) => route.toLocaleLowerCase().includes(query));
|
|
1158
|
+
el("route-count", HTMLElement).textContent = `${routes.length} of ${state.routes.length}`;
|
|
1159
|
+
if (state.routes.length === 0) {
|
|
1160
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No admin routes are available.</p>';
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
if (routes.length === 0) {
|
|
1164
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No routes match this filter.</p>';
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1109
1167
|
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
1110
1168
|
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
1111
1169
|
const parts = route.split(" ");
|
|
1112
1170
|
const method = parts[0] ?? "";
|
|
1113
1171
|
const path = parts.slice(1).join(" ");
|
|
1114
|
-
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>`;
|
|
1172
|
+
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>`;
|
|
1115
1173
|
}).join("");
|
|
1116
1174
|
};
|
|
1175
|
+
const renderRoutes = async () => {
|
|
1176
|
+
const data = await api("GET", "/");
|
|
1177
|
+
if (!isRecord(data))
|
|
1178
|
+
throw new Error("Route list was not an object");
|
|
1179
|
+
state.routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
1180
|
+
paintRoutes();
|
|
1181
|
+
};
|
|
1117
1182
|
const renderers = {
|
|
1118
1183
|
overview: renderOverview,
|
|
1119
1184
|
state: renderState,
|
|
@@ -1122,19 +1187,50 @@ function bootAdmin(config) {
|
|
|
1122
1187
|
journal: renderJournal,
|
|
1123
1188
|
routes: renderRoutes
|
|
1124
1189
|
};
|
|
1190
|
+
let refreshToken = 0;
|
|
1125
1191
|
async function refresh() {
|
|
1192
|
+
const token = ++refreshToken;
|
|
1126
1193
|
clearError();
|
|
1194
|
+
const main = el("views", HTMLElement);
|
|
1195
|
+
const sync = el("sync-status", HTMLElement);
|
|
1196
|
+
main.setAttribute("aria-busy", "true");
|
|
1197
|
+
sync.classList.add("busy");
|
|
1198
|
+
sync.textContent = "Updating";
|
|
1127
1199
|
const render = renderers[state.view];
|
|
1128
|
-
|
|
1129
|
-
|
|
1200
|
+
try {
|
|
1201
|
+
if (render)
|
|
1202
|
+
await render();
|
|
1203
|
+
if (token === refreshToken)
|
|
1204
|
+
sync.textContent = "Up to date";
|
|
1205
|
+
} finally {
|
|
1206
|
+
if (token === refreshToken) {
|
|
1207
|
+
main.removeAttribute("aria-busy");
|
|
1208
|
+
sync.classList.remove("busy");
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1130
1211
|
}
|
|
1131
1212
|
const delay = (ms) => new Promise((resolve) => {
|
|
1132
1213
|
window.setTimeout(resolve, ms);
|
|
1133
1214
|
});
|
|
1134
|
-
const confirmAction = async (button, pending, done, work) => {
|
|
1215
|
+
const confirmAction = async (button, pending, done, work, confirmLabel) => {
|
|
1135
1216
|
if (button.dataset.pending === "1")
|
|
1136
1217
|
return;
|
|
1137
1218
|
const label = button.textContent ?? "";
|
|
1219
|
+
if (confirmLabel !== void 0 && button.dataset.confirming !== "1") {
|
|
1220
|
+
button.dataset.confirming = "1";
|
|
1221
|
+
button.classList.add("confirming");
|
|
1222
|
+
button.textContent = confirmLabel;
|
|
1223
|
+
window.setTimeout(() => {
|
|
1224
|
+
if (button.dataset.confirming !== "1")
|
|
1225
|
+
return;
|
|
1226
|
+
button.textContent = label;
|
|
1227
|
+
button.classList.remove("confirming");
|
|
1228
|
+
delete button.dataset.confirming;
|
|
1229
|
+
}, 3e3);
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
button.classList.remove("confirming");
|
|
1233
|
+
delete button.dataset.confirming;
|
|
1138
1234
|
const width = button.offsetWidth;
|
|
1139
1235
|
button.dataset.pending = "1";
|
|
1140
1236
|
button.classList.add("pressed");
|
|
@@ -1155,6 +1251,65 @@ function bootAdmin(config) {
|
|
|
1155
1251
|
delete button.dataset.pending;
|
|
1156
1252
|
}
|
|
1157
1253
|
};
|
|
1254
|
+
const wireJsonEditor = (textareaId, highlightId, statusId, shellId, allowEmpty) => {
|
|
1255
|
+
const textarea = el(textareaId, HTMLTextAreaElement);
|
|
1256
|
+
const highlight = el(highlightId, HTMLElement);
|
|
1257
|
+
const status = el(statusId, HTMLElement);
|
|
1258
|
+
const shell = el(shellId, HTMLElement);
|
|
1259
|
+
const sync = () => {
|
|
1260
|
+
const text = textarea.value;
|
|
1261
|
+
highlight.innerHTML = `${highlightJsonText(text)}
|
|
1262
|
+
`;
|
|
1263
|
+
let valid = true;
|
|
1264
|
+
let message = "Valid JSON";
|
|
1265
|
+
if (allowEmpty && text.trim() === "")
|
|
1266
|
+
message = "No body";
|
|
1267
|
+
else {
|
|
1268
|
+
try {
|
|
1269
|
+
readJson2(text);
|
|
1270
|
+
} catch {
|
|
1271
|
+
valid = false;
|
|
1272
|
+
message = "Invalid JSON";
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
shell.classList.toggle("invalid", !valid);
|
|
1276
|
+
status.classList.toggle("invalid", !valid);
|
|
1277
|
+
status.textContent = message;
|
|
1278
|
+
textarea.setAttribute("aria-invalid", valid ? "false" : "true");
|
|
1279
|
+
};
|
|
1280
|
+
const syncScroll = () => {
|
|
1281
|
+
const preview2 = highlight.parentElement;
|
|
1282
|
+
if (!(preview2 instanceof HTMLElement))
|
|
1283
|
+
return;
|
|
1284
|
+
preview2.scrollTop = textarea.scrollTop;
|
|
1285
|
+
preview2.scrollLeft = textarea.scrollLeft;
|
|
1286
|
+
};
|
|
1287
|
+
textarea.addEventListener("input", sync);
|
|
1288
|
+
textarea.addEventListener("scroll", syncScroll);
|
|
1289
|
+
sync();
|
|
1290
|
+
return sync;
|
|
1291
|
+
};
|
|
1292
|
+
const syncRouteEditor = wireJsonEditor("route-body", "route-highlight", "route-json-status", "route-editor", true);
|
|
1293
|
+
const syncRecordEditor = wireJsonEditor("editor-value", "editor-highlight", "editor-json-status", "record-editor", false);
|
|
1294
|
+
for (const button of document.querySelectorAll("[data-format]")) {
|
|
1295
|
+
if (!(button instanceof HTMLButtonElement))
|
|
1296
|
+
continue;
|
|
1297
|
+
button.addEventListener("click", () => {
|
|
1298
|
+
const target = button.dataset.format;
|
|
1299
|
+
if (target === void 0)
|
|
1300
|
+
return;
|
|
1301
|
+
const textarea = document.getElementById(target);
|
|
1302
|
+
if (!(textarea instanceof HTMLTextAreaElement) || textarea.value.trim() === "")
|
|
1303
|
+
return;
|
|
1304
|
+
try {
|
|
1305
|
+
textarea.value = JSON.stringify(readJson2(textarea.value), null, 2) ?? "";
|
|
1306
|
+
textarea.dispatchEvent(new Event("input"));
|
|
1307
|
+
textarea.focus();
|
|
1308
|
+
} catch {
|
|
1309
|
+
showError(new Error("Fix the JSON before formatting it"));
|
|
1310
|
+
}
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1158
1313
|
document.addEventListener("click", (event) => {
|
|
1159
1314
|
const target = event.target;
|
|
1160
1315
|
if (!(target instanceof Element))
|
|
@@ -1227,6 +1382,7 @@ function bootAdmin(config) {
|
|
|
1227
1382
|
} else {
|
|
1228
1383
|
editorValue.value = "{\n \n}";
|
|
1229
1384
|
}
|
|
1385
|
+
syncRecordEditor();
|
|
1230
1386
|
editor.showModal();
|
|
1231
1387
|
}
|
|
1232
1388
|
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -1256,10 +1412,10 @@ function bootAdmin(config) {
|
|
|
1256
1412
|
const id = state.editing;
|
|
1257
1413
|
if (!id)
|
|
1258
1414
|
return;
|
|
1259
|
-
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
1415
|
+
confirmAction(editorDelete, "Deleting...", "Deleted", () => api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
1260
1416
|
editor.close();
|
|
1261
1417
|
return renderState();
|
|
1262
|
-
}).catch(showError);
|
|
1418
|
+
}), "Confirm delete").catch(showError);
|
|
1263
1419
|
});
|
|
1264
1420
|
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
1265
1421
|
if (!(button instanceof HTMLButtonElement))
|
|
@@ -1305,10 +1461,10 @@ function bootAdmin(config) {
|
|
|
1305
1461
|
}).then(() => renderFaults()).catch(showError);
|
|
1306
1462
|
});
|
|
1307
1463
|
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
1308
|
-
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
|
|
1464
|
+
confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults()), "Confirm clear").catch(showError);
|
|
1309
1465
|
});
|
|
1310
1466
|
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
1311
|
-
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
|
|
1467
|
+
confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal()), "Confirm clear").catch(showError);
|
|
1312
1468
|
});
|
|
1313
1469
|
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
1314
1470
|
const target = event.target;
|
|
@@ -1322,9 +1478,15 @@ function bootAdmin(config) {
|
|
|
1322
1478
|
return;
|
|
1323
1479
|
state.route = route;
|
|
1324
1480
|
el("route-title", HTMLElement).textContent = route;
|
|
1481
|
+
el("route-send", HTMLButtonElement).disabled = false;
|
|
1325
1482
|
const method = route.split(" ")[0] ?? "";
|
|
1326
|
-
el("route-body", HTMLTextAreaElement)
|
|
1483
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
1484
|
+
routeBody.disabled = method === "GET" || method === "DELETE";
|
|
1485
|
+
el("route-editor", HTMLElement).hidden = routeBody.disabled;
|
|
1486
|
+
el("route-json-status", HTMLElement).parentElement?.toggleAttribute("hidden", routeBody.disabled);
|
|
1487
|
+
syncRouteEditor();
|
|
1327
1488
|
});
|
|
1489
|
+
el("route-search", HTMLInputElement).addEventListener("input", paintRoutes);
|
|
1328
1490
|
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
1329
1491
|
event.preventDefault();
|
|
1330
1492
|
const send = async () => {
|
|
@@ -1346,7 +1508,8 @@ function bootAdmin(config) {
|
|
|
1346
1508
|
}
|
|
1347
1509
|
}
|
|
1348
1510
|
const data = await api(method, concrete, body);
|
|
1349
|
-
|
|
1511
|
+
const text = JSON.stringify(data, null, 2) ?? "null";
|
|
1512
|
+
el("route-result", HTMLElement).innerHTML = `<code>${highlightJsonText(text)}</code>`;
|
|
1350
1513
|
};
|
|
1351
1514
|
send().catch(showError);
|
|
1352
1515
|
});
|
|
@@ -1464,11 +1627,13 @@ function bootAdmin(config) {
|
|
|
1464
1627
|
views.append(section);
|
|
1465
1628
|
mountSqlExplorer(body, panelApi);
|
|
1466
1629
|
}
|
|
1630
|
+
show(document.getElementById(`view-${state.view}`) === null ? "overview" : state.view);
|
|
1467
1631
|
return refresh();
|
|
1468
1632
|
}).catch(showError);
|
|
1469
1633
|
}
|
|
1470
1634
|
var clientPrelude = [
|
|
1471
1635
|
isRecord,
|
|
1636
|
+
highlightJsonText,
|
|
1472
1637
|
mountAdminBrand,
|
|
1473
1638
|
startAdminBrand,
|
|
1474
1639
|
faultPresetList,
|
|
@@ -1488,13 +1653,19 @@ ${entry}(${payload});
|
|
|
1488
1653
|
};
|
|
1489
1654
|
|
|
1490
1655
|
// ../core/dist/control.js
|
|
1491
|
-
var HEALTH_PATH = "/health";
|
|
1492
1656
|
var ADMIN_PREFIX = "/__admin";
|
|
1657
|
+
var resolveAdminPrefix = (prefix = ADMIN_PREFIX) => {
|
|
1658
|
+
if (!/^\/[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*$/.test(prefix)) {
|
|
1659
|
+
throw new Error("adminPrefix must be an absolute non-root path of literal segments, without a trailing slash");
|
|
1660
|
+
}
|
|
1661
|
+
return prefix;
|
|
1662
|
+
};
|
|
1663
|
+
var isAdminPath = (path, prefix) => path === prefix || path.startsWith(`${prefix}/`);
|
|
1493
1664
|
var ADMIN_KEY_HEADER = "x-mockingbird-admin-key";
|
|
1494
1665
|
var NAMESPACE_HEADER = "x-mockingbird-namespace";
|
|
1495
1666
|
var json = (status, body) => new Response(JSON.stringify(body), {
|
|
1496
1667
|
status,
|
|
1497
|
-
headers: { "content-type": "application/json" }
|
|
1668
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
1498
1669
|
});
|
|
1499
1670
|
var adminError = (status, message) => json(status, { error: { type: "mockingbird_admin", message } });
|
|
1500
1671
|
var UNITS = {
|
|
@@ -1546,11 +1717,25 @@ var readJson = async (request) => {
|
|
|
1546
1717
|
};
|
|
1547
1718
|
var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1548
1719
|
var createControlPlane = (context) => {
|
|
1720
|
+
const adminPrefix = resolveAdminPrefix(context.adminPrefix);
|
|
1549
1721
|
const snapshots = /* @__PURE__ */ new Map();
|
|
1550
1722
|
let snapshotCounter = 0;
|
|
1551
1723
|
const headerNamespace = (request) => request.headers.get(NAMESPACE_HEADER) ?? context.defaultNamespace;
|
|
1552
1724
|
const adminNamespace = (request, url) => url.searchParams.get("namespace") ?? headerNamespace(request);
|
|
1725
|
+
const health = () => {
|
|
1726
|
+
return json(200, {
|
|
1727
|
+
...context.describe(),
|
|
1728
|
+
status: "ok",
|
|
1729
|
+
service: context.name,
|
|
1730
|
+
uptimeMs: context.wallNow() - context.startedAt,
|
|
1731
|
+
clock: context.clock.state(),
|
|
1732
|
+
namespaces: context.namespaces().length,
|
|
1733
|
+
adminPrefix,
|
|
1734
|
+
adminUi: `${adminPrefix}/ui`
|
|
1735
|
+
});
|
|
1736
|
+
};
|
|
1553
1737
|
const builtin = {
|
|
1738
|
+
"GET /health": health,
|
|
1554
1739
|
"GET /": () => json(200, {
|
|
1555
1740
|
service: context.name,
|
|
1556
1741
|
routes: [...Object.keys(builtin), ...Object.keys(context.routes)].sort()
|
|
@@ -1561,29 +1746,30 @@ var createControlPlane = (context) => {
|
|
|
1561
1746
|
return json(200, { status: "ok", reset: target === "*" ? context.namespaces() : [target] });
|
|
1562
1747
|
},
|
|
1563
1748
|
"GET /namespaces": () => json(200, { default: context.defaultNamespace, namespaces: context.namespaces() }),
|
|
1564
|
-
"GET /clock": () => json(200, context.clock.state()),
|
|
1565
|
-
"POST /clock": ({ body }) => {
|
|
1749
|
+
"GET /clock": ({ namespace }) => json(200, (context.namespaceClock?.(namespace) ?? context.clock).state()),
|
|
1750
|
+
"POST /clock": ({ body, namespace }) => {
|
|
1751
|
+
const clock = context.namespaceClock?.(namespace) ?? context.clock;
|
|
1566
1752
|
if (!isRecord2(body))
|
|
1567
1753
|
return adminError(400, "expected a JSON object");
|
|
1568
1754
|
if (body.reset === true)
|
|
1569
|
-
|
|
1755
|
+
clock.reset();
|
|
1570
1756
|
if (body.freeze === true)
|
|
1571
|
-
|
|
1757
|
+
clock.freeze();
|
|
1572
1758
|
if (body.set !== void 0) {
|
|
1573
1759
|
const instant = parseInstant(body.set);
|
|
1574
1760
|
if (instant === void 0)
|
|
1575
1761
|
return adminError(400, "set: expected epoch ms or ISO-8601");
|
|
1576
|
-
|
|
1762
|
+
clock.set(instant);
|
|
1577
1763
|
}
|
|
1578
1764
|
if (body.advance !== void 0) {
|
|
1579
1765
|
const delta = parseDuration(body.advance);
|
|
1580
1766
|
if (delta === void 0)
|
|
1581
1767
|
return adminError(400, 'advance: expected ms or "15m"-style');
|
|
1582
|
-
|
|
1768
|
+
clock.advance(delta);
|
|
1583
1769
|
}
|
|
1584
1770
|
if (body.freeze === false)
|
|
1585
|
-
|
|
1586
|
-
return json(200,
|
|
1771
|
+
clock.unfreeze();
|
|
1772
|
+
return json(200, clock.state());
|
|
1587
1773
|
},
|
|
1588
1774
|
"GET /faults": () => json(200, { faults: context.faults.list() }),
|
|
1589
1775
|
"POST /faults": ({ body, namespace }) => {
|
|
@@ -1716,7 +1902,14 @@ var createControlPlane = (context) => {
|
|
|
1716
1902
|
return json(200, { status: "ok" });
|
|
1717
1903
|
}
|
|
1718
1904
|
};
|
|
1719
|
-
const
|
|
1905
|
+
for (const key of Object.keys(context.routes)) {
|
|
1906
|
+
if (Object.hasOwn(builtin, key))
|
|
1907
|
+
throw new Error(`admin route ${key} overrides a shared route`);
|
|
1908
|
+
const path = key.slice(key.indexOf(" ") + 1);
|
|
1909
|
+
if (isAdminPath(path, "/ns") || isAdminPath(path, "/blobs"))
|
|
1910
|
+
throw new Error(`admin route ${key} overlaps an internal transport`);
|
|
1911
|
+
}
|
|
1912
|
+
const routes = [...Object.entries(builtin), ...Object.entries(context.routes)].map(([key, handler]) => {
|
|
1720
1913
|
const space = key.indexOf(" ");
|
|
1721
1914
|
return { method: key.slice(0, space), pattern: key.slice(space + 1), handler };
|
|
1722
1915
|
});
|
|
@@ -1724,21 +1917,13 @@ var createControlPlane = (context) => {
|
|
|
1724
1917
|
namespaceOf: headerNamespace,
|
|
1725
1918
|
async handle(request) {
|
|
1726
1919
|
const url = new URL(request.url);
|
|
1727
|
-
if (url.pathname ===
|
|
1728
|
-
return
|
|
1729
|
-
status: "ok",
|
|
1730
|
-
service: context.name,
|
|
1731
|
-
uptimeMs: context.wallNow() - context.startedAt,
|
|
1732
|
-
clock: context.clock.state(),
|
|
1733
|
-
namespaces: context.namespaces().length,
|
|
1734
|
-
adminUi: `${ADMIN_PREFIX}/ui`,
|
|
1735
|
-
...context.describe()
|
|
1736
|
-
});
|
|
1920
|
+
if (url.pathname === `${adminPrefix}/health` && request.method === "GET") {
|
|
1921
|
+
return health();
|
|
1737
1922
|
}
|
|
1738
|
-
if (url.pathname
|
|
1923
|
+
if (!isAdminPath(url.pathname, adminPrefix)) {
|
|
1739
1924
|
return void 0;
|
|
1740
1925
|
}
|
|
1741
|
-
const path = url.pathname.slice(
|
|
1926
|
+
const path = url.pathname.slice(adminPrefix.length) || "/";
|
|
1742
1927
|
const shell = request.method === "GET" && (path === "/ui" || path === "/ui/");
|
|
1743
1928
|
if (!shell && context.adminKey !== void 0 && request.headers.get(ADMIN_KEY_HEADER) !== context.adminKey) {
|
|
1744
1929
|
return adminError(401, `missing or wrong ${ADMIN_KEY_HEADER}`);
|
|
@@ -1746,7 +1931,12 @@ var createControlPlane = (context) => {
|
|
|
1746
1931
|
for (const route of routes) {
|
|
1747
1932
|
if (route.method !== request.method)
|
|
1748
1933
|
continue;
|
|
1749
|
-
|
|
1934
|
+
let params;
|
|
1935
|
+
try {
|
|
1936
|
+
params = matchRoute(route.pattern, path);
|
|
1937
|
+
} catch {
|
|
1938
|
+
return adminError(400, "invalid path encoding");
|
|
1939
|
+
}
|
|
1750
1940
|
if (!params)
|
|
1751
1941
|
continue;
|
|
1752
1942
|
let body;
|
|
@@ -1755,15 +1945,18 @@ var createControlPlane = (context) => {
|
|
|
1755
1945
|
} catch {
|
|
1756
1946
|
return adminError(400, "request body is not valid JSON");
|
|
1757
1947
|
}
|
|
1948
|
+
const namespace = adminNamespace(request, url);
|
|
1949
|
+
if (!/^[A-Za-z0-9_.-]{1,64}$/.test(namespace))
|
|
1950
|
+
return adminError(400, "invalid namespace");
|
|
1758
1951
|
return route.handler({
|
|
1759
1952
|
request,
|
|
1760
1953
|
url,
|
|
1761
1954
|
params,
|
|
1762
|
-
namespace
|
|
1955
|
+
namespace,
|
|
1763
1956
|
body
|
|
1764
1957
|
});
|
|
1765
1958
|
}
|
|
1766
|
-
return adminError(404, `no admin route ${request.method} ${path}; GET ${
|
|
1959
|
+
return adminError(404, `no admin route ${request.method} ${path}; GET ${adminPrefix} lists them`);
|
|
1767
1960
|
}
|
|
1768
1961
|
};
|
|
1769
1962
|
};
|
|
@@ -1771,6 +1964,7 @@ var createControlPlane = (context) => {
|
|
|
1771
1964
|
// ../core/dist/surface.js
|
|
1772
1965
|
var STANDARD_ADMIN_ROUTES = [
|
|
1773
1966
|
"GET /",
|
|
1967
|
+
"GET /health",
|
|
1774
1968
|
"POST /reset",
|
|
1775
1969
|
"GET /namespaces",
|
|
1776
1970
|
"GET /clock",
|
|
@@ -1820,11 +2014,11 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
1820
2014
|
}
|
|
1821
2015
|
});
|
|
1822
2016
|
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
1823
|
-
var renderAdminDocument = (service) => {
|
|
2017
|
+
var renderAdminDocument = (service, adminPrefix = ADMIN_PREFIX) => {
|
|
1824
2018
|
const name = escapeHtml(service);
|
|
1825
2019
|
const script = adminClientSource({
|
|
1826
2020
|
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
1827
|
-
adminPrefix
|
|
2021
|
+
adminPrefix,
|
|
1828
2022
|
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
1829
2023
|
brandsUrl: ADMIN_BRANDS_URL,
|
|
1830
2024
|
service
|
|
@@ -1841,75 +2035,99 @@ var renderAdminDocument = (service) => {
|
|
|
1841
2035
|
${CSS_RESET}
|
|
1842
2036
|
:root {
|
|
1843
2037
|
color-scheme: light;
|
|
1844
|
-
--bg: #
|
|
2038
|
+
--bg: #f7f8fb;
|
|
1845
2039
|
--bg-elev: #ffffff;
|
|
1846
|
-
--bg-muted: #
|
|
1847
|
-
--bg-inset: #
|
|
1848
|
-
--line: #
|
|
1849
|
-
--line-strong: #
|
|
1850
|
-
--ink: #
|
|
1851
|
-
--muted: #
|
|
1852
|
-
--faint: #
|
|
1853
|
-
--accent: #
|
|
2040
|
+
--bg-muted: #f1f3f7;
|
|
2041
|
+
--bg-inset: #f8f9fc;
|
|
2042
|
+
--line: #e4e7ec;
|
|
2043
|
+
--line-strong: #cfd4dc;
|
|
2044
|
+
--ink: #101828;
|
|
2045
|
+
--muted: #475467;
|
|
2046
|
+
--faint: #667085;
|
|
2047
|
+
--accent: #6941c6;
|
|
2048
|
+
--accent-hover: #5933b4;
|
|
1854
2049
|
--accent-ink: #ffffff;
|
|
1855
|
-
--accent-soft: #
|
|
1856
|
-
--ok: #
|
|
1857
|
-
--ok-soft: #
|
|
1858
|
-
--warn: #
|
|
1859
|
-
--warn-soft: #
|
|
1860
|
-
--err: #
|
|
1861
|
-
--err-soft: #
|
|
1862
|
-
--
|
|
2050
|
+
--accent-soft: #f4f0ff;
|
|
2051
|
+
--ok: #067647;
|
|
2052
|
+
--ok-soft: #ecfdf3;
|
|
2053
|
+
--warn: #b54708;
|
|
2054
|
+
--warn-soft: #fffaeb;
|
|
2055
|
+
--err: #b42318;
|
|
2056
|
+
--err-soft: #fef3f2;
|
|
2057
|
+
--code-key: #6941c6;
|
|
2058
|
+
--code-string: #067647;
|
|
2059
|
+
--code-number: #175cd3;
|
|
2060
|
+
--code-boolean: #c11574;
|
|
2061
|
+
--code-null: #667085;
|
|
2062
|
+
--shadow-xs: 0 1px 2px rgb(16 24 40 / 0.05);
|
|
2063
|
+
--shadow-sm: 0 1px 3px rgb(16 24 40 / 0.08), 0 1px 2px rgb(16 24 40 / 0.04);
|
|
2064
|
+
--shadow-lg: 0 18px 48px rgb(16 24 40 / 0.16), 0 4px 12px rgb(16 24 40 / 0.08);
|
|
1863
2065
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
1864
2066
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
1865
|
-
--radius:
|
|
1866
|
-
--header:
|
|
1867
|
-
--nav:
|
|
2067
|
+
--radius: 12px;
|
|
2068
|
+
--header: 72px;
|
|
2069
|
+
--nav: 224px;
|
|
1868
2070
|
}
|
|
1869
2071
|
:root[data-theme="dark"] {
|
|
1870
2072
|
color-scheme: dark;
|
|
1871
|
-
--bg: #
|
|
1872
|
-
--bg-elev: #
|
|
1873
|
-
--bg-muted: #
|
|
1874
|
-
--bg-inset: #
|
|
1875
|
-
--line: #
|
|
1876
|
-
--line-strong: #
|
|
1877
|
-
--ink: #
|
|
1878
|
-
--muted: #
|
|
1879
|
-
--faint: #
|
|
1880
|
-
--accent: #
|
|
1881
|
-
--accent-
|
|
1882
|
-
--accent-
|
|
1883
|
-
--
|
|
1884
|
-
--ok
|
|
1885
|
-
--
|
|
1886
|
-
--warn
|
|
1887
|
-
--
|
|
1888
|
-
--err
|
|
1889
|
-
--
|
|
2073
|
+
--bg: #0c0e14;
|
|
2074
|
+
--bg-elev: #151821;
|
|
2075
|
+
--bg-muted: #20232e;
|
|
2076
|
+
--bg-inset: #11141c;
|
|
2077
|
+
--line: #2b303d;
|
|
2078
|
+
--line-strong: #414858;
|
|
2079
|
+
--ink: #f5f7fa;
|
|
2080
|
+
--muted: #b6bdc9;
|
|
2081
|
+
--faint: #8d96a7;
|
|
2082
|
+
--accent: #9e77ed;
|
|
2083
|
+
--accent-hover: #b692f6;
|
|
2084
|
+
--accent-ink: #ffffff;
|
|
2085
|
+
--accent-soft: #2b2142;
|
|
2086
|
+
--ok: #75e0a7;
|
|
2087
|
+
--ok-soft: #102a20;
|
|
2088
|
+
--warn: #fec84b;
|
|
2089
|
+
--warn-soft: #302611;
|
|
2090
|
+
--err: #fda29b;
|
|
2091
|
+
--err-soft: #321817;
|
|
2092
|
+
--code-key: #c3a6ff;
|
|
2093
|
+
--code-string: #75e0a7;
|
|
2094
|
+
--code-number: #84adff;
|
|
2095
|
+
--code-boolean: #f9a8d4;
|
|
2096
|
+
--code-null: #98a2b3;
|
|
2097
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
2098
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
2099
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
1890
2100
|
}
|
|
1891
2101
|
@media (prefers-color-scheme: dark) {
|
|
1892
2102
|
:root:not([data-theme="light"]) {
|
|
1893
2103
|
color-scheme: dark;
|
|
1894
|
-
--bg: #
|
|
1895
|
-
--bg-elev: #
|
|
1896
|
-
--bg-muted: #
|
|
1897
|
-
--bg-inset: #
|
|
1898
|
-
--line: #
|
|
1899
|
-
--line-strong: #
|
|
1900
|
-
--ink: #
|
|
1901
|
-
--muted: #
|
|
1902
|
-
--faint: #
|
|
1903
|
-
--accent: #
|
|
1904
|
-
--accent-
|
|
1905
|
-
--accent-
|
|
1906
|
-
--
|
|
1907
|
-
--ok
|
|
1908
|
-
--
|
|
1909
|
-
--warn
|
|
1910
|
-
--
|
|
1911
|
-
--err
|
|
1912
|
-
--
|
|
2104
|
+
--bg: #0c0e14;
|
|
2105
|
+
--bg-elev: #151821;
|
|
2106
|
+
--bg-muted: #20232e;
|
|
2107
|
+
--bg-inset: #11141c;
|
|
2108
|
+
--line: #2b303d;
|
|
2109
|
+
--line-strong: #414858;
|
|
2110
|
+
--ink: #f5f7fa;
|
|
2111
|
+
--muted: #b6bdc9;
|
|
2112
|
+
--faint: #8d96a7;
|
|
2113
|
+
--accent: #9e77ed;
|
|
2114
|
+
--accent-hover: #b692f6;
|
|
2115
|
+
--accent-ink: #ffffff;
|
|
2116
|
+
--accent-soft: #2b2142;
|
|
2117
|
+
--ok: #75e0a7;
|
|
2118
|
+
--ok-soft: #102a20;
|
|
2119
|
+
--warn: #fec84b;
|
|
2120
|
+
--warn-soft: #302611;
|
|
2121
|
+
--err: #fda29b;
|
|
2122
|
+
--err-soft: #321817;
|
|
2123
|
+
--code-key: #c3a6ff;
|
|
2124
|
+
--code-string: #75e0a7;
|
|
2125
|
+
--code-number: #84adff;
|
|
2126
|
+
--code-boolean: #f9a8d4;
|
|
2127
|
+
--code-null: #98a2b3;
|
|
2128
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
2129
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
2130
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
1913
2131
|
}
|
|
1914
2132
|
}
|
|
1915
2133
|
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
@@ -1927,13 +2145,17 @@ var renderAdminDocument = (service) => {
|
|
|
1927
2145
|
padding-right: env(safe-area-inset-right);
|
|
1928
2146
|
-webkit-text-size-adjust: 100%;
|
|
1929
2147
|
}
|
|
1930
|
-
|
|
2148
|
+
::selection { background: color-mix(in srgb, var(--accent) 28%, transparent); }
|
|
2149
|
+
:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px; }
|
|
2150
|
+
button, input, select, textarea { transition: border-color 140ms ease, box-shadow 140ms ease, transform 100ms ease; }
|
|
2151
|
+
button:disabled, input:disabled, select:disabled, textarea:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
1931
2152
|
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
1932
2153
|
header.top {
|
|
1933
|
-
position: sticky; top: 0; z-index:
|
|
2154
|
+
position: sticky; top: 0; z-index: 20;
|
|
1934
2155
|
display: flex; align-items: center; gap: 12px;
|
|
1935
|
-
min-height: var(--header); padding:
|
|
1936
|
-
background: var(--bg-elev);
|
|
2156
|
+
min-height: var(--header); padding: 12px 20px;
|
|
2157
|
+
background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
|
|
2158
|
+
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
1937
2159
|
border-bottom: 1px solid var(--line);
|
|
1938
2160
|
min-width: 0; max-width: 100%;
|
|
1939
2161
|
}
|
|
@@ -1941,12 +2163,21 @@ var renderAdminDocument = (service) => {
|
|
|
1941
2163
|
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
1942
2164
|
min-width: 0; max-width: 100%; margin-right: auto;
|
|
1943
2165
|
}
|
|
1944
|
-
.brand-
|
|
1945
|
-
.brand-
|
|
2166
|
+
.brand-lockup { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
2167
|
+
.brand-mark {
|
|
2168
|
+
width: 38px; height: 38px; display: grid; place-items: center; flex: none;
|
|
2169
|
+
border-radius: 11px; color: #fff;
|
|
2170
|
+
background: linear-gradient(145deg, #7f56d9 0%, #53389e 100%);
|
|
2171
|
+
box-shadow: 0 6px 16px rgb(105 65 198 / 0.24), inset 0 1px 0 rgb(255 255 255 / 0.25);
|
|
2172
|
+
}
|
|
2173
|
+
.brand-mark svg { width: 22px; height: 22px; }
|
|
2174
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; line-height: 1.25; }
|
|
2175
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1946
2176
|
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
1947
2177
|
.vendor {
|
|
1948
2178
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
1949
2179
|
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
2180
|
+
padding: 5px 8px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-inset);
|
|
1950
2181
|
}
|
|
1951
2182
|
.vendor[hidden] { display: none; }
|
|
1952
2183
|
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
@@ -1958,12 +2189,17 @@ var renderAdminDocument = (service) => {
|
|
|
1958
2189
|
.vendor-sep { color: var(--faint); }
|
|
1959
2190
|
.vendor-link { color: inherit; text-decoration: none; }
|
|
1960
2191
|
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
1961
|
-
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items:
|
|
1962
|
-
|
|
2192
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
|
|
2193
|
+
.sync-status { align-self: center; display: flex; align-items: center; gap: 6px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
2194
|
+
.sync-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
|
|
2195
|
+
.sync-status.busy::before { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); animation: pulse 1.2s ease-in-out infinite; }
|
|
2196
|
+
label.field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; font-weight: 600; color: var(--faint); }
|
|
1963
2197
|
label.field input, label.field select, .controls input[type="password"] {
|
|
1964
|
-
background-color: var(--bg-
|
|
1965
|
-
padding:
|
|
2198
|
+
background-color: var(--bg-elev); border: 1px solid var(--line-strong); border-radius: 9px;
|
|
2199
|
+
padding: 8px 10px; min-height: 38px; line-height: 1.2; box-shadow: var(--shadow-xs);
|
|
1966
2200
|
}
|
|
2201
|
+
label.field input:hover, label.field select:hover, textarea:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--line-strong)); }
|
|
2202
|
+
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; }
|
|
1967
2203
|
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
1968
2204
|
the text fields in the same form. Paint it like those fields. */
|
|
1969
2205
|
label.field select {
|
|
@@ -1980,12 +2216,20 @@ var renderAdminDocument = (service) => {
|
|
|
1980
2216
|
dialog .field > textarea { width: 100%; }
|
|
1981
2217
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
1982
2218
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
.btn
|
|
2219
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
|
2220
|
+
border-radius: 9px; min-height: 38px; padding: 0 13px; border: 1px solid var(--line-strong);
|
|
2221
|
+
background: var(--bg-elev); color: var(--ink); font-size: 13px; font-weight: 600; line-height: 1;
|
|
2222
|
+
box-shadow: var(--shadow-xs);
|
|
2223
|
+
}
|
|
2224
|
+
.icon-btn { width: 38px; padding: 0; }
|
|
2225
|
+
.icon-btn svg, .btn svg, .btn-primary svg { width: 16px; height: 16px; }
|
|
2226
|
+
.icon-btn:hover, .btn:hover { background: var(--bg-muted); border-color: var(--line-strong); }
|
|
2227
|
+
.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; }
|
|
2228
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
2229
|
+
.btn-quiet { background: transparent; border-color: transparent; box-shadow: none; }
|
|
2230
|
+
.btn-quiet:hover { background: var(--bg-muted); }
|
|
2231
|
+
.btn-danger { color: var(--err); border-color: color-mix(in srgb, var(--err) 25%, var(--line)); }
|
|
2232
|
+
.btn-danger:hover, .btn-danger.confirming { background: var(--err-soft); border-color: color-mix(in srgb, var(--err) 45%, var(--line)); }
|
|
1989
2233
|
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
1990
2234
|
so the press itself has to stay visible. */
|
|
1991
2235
|
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
@@ -2001,93 +2245,159 @@ var renderAdminDocument = (service) => {
|
|
|
2001
2245
|
flex: 1 0 auto; min-width: 0;
|
|
2002
2246
|
}
|
|
2003
2247
|
nav.side {
|
|
2004
|
-
|
|
2248
|
+
position: sticky; top: var(--header); align-self: start; height: calc(100vh - var(--header));
|
|
2249
|
+
border-right: 1px solid var(--line); padding: 18px 12px;
|
|
2005
2250
|
display: flex; flex-direction: column; gap: 4px;
|
|
2006
|
-
background: var(--bg-elev);
|
|
2251
|
+
background: var(--bg-elev); overflow-y: auto;
|
|
2007
2252
|
}
|
|
2253
|
+
.nav-label { padding: 0 10px 8px; color: var(--faint); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
|
|
2008
2254
|
nav.side button {
|
|
2255
|
+
position: relative; display: flex; align-items: center; gap: 10px; width: 100%;
|
|
2009
2256
|
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
2010
|
-
padding: 9px 10px; min-height:
|
|
2011
|
-
}
|
|
2012
|
-
nav.side button
|
|
2013
|
-
|
|
2014
|
-
.
|
|
2015
|
-
.
|
|
2016
|
-
.
|
|
2257
|
+
padding: 9px 10px; min-height: 42px; color: var(--muted); font-size: 13px; font-weight: 550;
|
|
2258
|
+
}
|
|
2259
|
+
nav.side button:hover { background: var(--bg-muted); color: var(--ink); }
|
|
2260
|
+
nav.side button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 650; }
|
|
2261
|
+
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); }
|
|
2262
|
+
.nav-icon { width: 18px; height: 18px; display: grid; place-items: center; flex: none; }
|
|
2263
|
+
.nav-icon svg { width: 18px; height: 18px; }
|
|
2264
|
+
main { padding: 32px clamp(20px, 4vw, 48px) 64px; max-width: 1320px; width: 100%; min-width: 0; }
|
|
2265
|
+
main[aria-busy="true"] .view { opacity: 0.62; }
|
|
2266
|
+
.view { animation: view-in 180ms ease-out; }
|
|
2267
|
+
.view[hidden] { display: none; }
|
|
2268
|
+
.view-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
|
|
2269
|
+
.view h2 { margin: 0 0 5px; font-size: clamp(24px, 3vw, 30px); line-height: 1.2; letter-spacing: -0.035em; }
|
|
2270
|
+
.view h3 { margin: 24px 0 10px; font-size: 15px; letter-spacing: -0.01em; }
|
|
2271
|
+
.lede { margin: 0; color: var(--muted); max-width: 72ch; font-size: 14px; }
|
|
2272
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 175px), 1fr)); gap: 14px; }
|
|
2017
2273
|
.card {
|
|
2018
2274
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
2019
|
-
padding:
|
|
2275
|
+
padding: 18px; box-shadow: var(--shadow-sm);
|
|
2020
2276
|
}
|
|
2021
|
-
.card
|
|
2022
|
-
.card
|
|
2277
|
+
.stat-card { position: relative; overflow: hidden; }
|
|
2278
|
+
.stat-card::after { content: ""; position: absolute; inset: 0 0 auto; height: 2px; background: linear-gradient(90deg, var(--accent), transparent 78%); opacity: 0.7; }
|
|
2279
|
+
.card .k { color: var(--faint); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
2280
|
+
.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; }
|
|
2023
2281
|
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
2024
|
-
.
|
|
2282
|
+
.section-card { background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-xs); overflow: hidden; }
|
|
2283
|
+
.section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
|
|
2284
|
+
.section-title h3 { margin: 0; }
|
|
2285
|
+
.split { display: grid; grid-template-columns: minmax(0, 250px) minmax(0, 1fr); gap: 18px; align-items: start; }
|
|
2286
|
+
#collections { padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); box-shadow: var(--shadow-xs); }
|
|
2025
2287
|
.collection {
|
|
2026
2288
|
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
2027
|
-
border-radius:
|
|
2289
|
+
border-radius: 9px; padding: 10px 11px; min-height: 48px; color: var(--ink); cursor: pointer;
|
|
2028
2290
|
}
|
|
2029
|
-
.collection
|
|
2291
|
+
.collection:hover { background: var(--bg-muted); }
|
|
2292
|
+
.collection[aria-current="true"] { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 18%, var(--line)); color: var(--accent); }
|
|
2030
2293
|
.collection small { display: block; color: var(--faint); }
|
|
2031
2294
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
2032
|
-
.chip {
|
|
2033
|
-
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
2034
|
-
border-radius: 999px; padding: 4px 8px;
|
|
2035
|
-
}
|
|
2295
|
+
.chip { font-family: var(--mono); font-size: 11px; background: var(--bg-muted); border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; }
|
|
2036
2296
|
.chip i { color: var(--faint); font-style: normal; }
|
|
2037
2297
|
.sql-layout {
|
|
2038
2298
|
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
2039
2299
|
}
|
|
2040
|
-
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
2041
|
-
.sql-tables button { text-align: left; }
|
|
2300
|
+
.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); }
|
|
2301
|
+
.sql-tables button { text-align: left; border: 0; border-radius: 8px; padding: 9px 10px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
2302
|
+
.sql-tables button:hover, .sql-tables button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); }
|
|
2042
2303
|
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
2043
|
-
table { width: 100%; border-
|
|
2044
|
-
|
|
2045
|
-
th {
|
|
2304
|
+
.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); }
|
|
2305
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
2306
|
+
th, td { text-align: left; padding: 11px 13px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
2307
|
+
th { color: var(--faint); background: var(--bg-inset); font-weight: 650; font-size: 11px; text-transform: uppercase; letter-spacing: 0.045em; }
|
|
2308
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
2046
2309
|
tr.clickable { cursor: pointer; }
|
|
2047
|
-
tr.clickable:hover { background: var(--
|
|
2310
|
+
tr.clickable:hover { background: var(--accent-soft); }
|
|
2048
2311
|
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
2049
2312
|
pre {
|
|
2050
2313
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
2051
|
-
background: var(--bg-inset); border-radius: 10px; padding:
|
|
2052
|
-
}
|
|
2053
|
-
.
|
|
2314
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 10px; padding: 14px; font-size: 12px; line-height: 1.65;
|
|
2315
|
+
}
|
|
2316
|
+
.json-key { color: var(--code-key); }
|
|
2317
|
+
.json-string { color: var(--code-string); }
|
|
2318
|
+
.json-number { color: var(--code-number); }
|
|
2319
|
+
.json-boolean { color: var(--code-boolean); }
|
|
2320
|
+
.json-null { color: var(--code-null); font-style: italic; }
|
|
2321
|
+
.json-inline { display: block; max-width: 44rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
2322
|
+
.json-view { position: relative; max-height: 26rem; overflow: auto; }
|
|
2323
|
+
.json-editor { position: relative; min-height: 190px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--bg-inset); overflow: hidden; }
|
|
2324
|
+
.json-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); }
|
|
2325
|
+
.json-editor.invalid { border-color: var(--err); }
|
|
2326
|
+
.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; }
|
|
2327
|
+
.json-editor pre { position: absolute; inset: 0; pointer-events: none; background: transparent; }
|
|
2328
|
+
.json-editor textarea { position: relative; z-index: 1; resize: vertical; background: transparent; color: transparent; caret-color: var(--ink); -webkit-text-fill-color: transparent; }
|
|
2329
|
+
.json-editor textarea::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); }
|
|
2330
|
+
.editor-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; }
|
|
2331
|
+
.json-status { display: inline-flex; align-items: center; gap: 6px; color: var(--ok); font-size: 11px; }
|
|
2332
|
+
.json-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
2333
|
+
.json-status.invalid { color: var(--err); }
|
|
2334
|
+
.format-json { border: 0; padding: 3px 5px; background: transparent; color: var(--accent); font-size: 11px; font-weight: 650; cursor: pointer; }
|
|
2335
|
+
.row-actions { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 14px 0; }
|
|
2054
2336
|
.banner {
|
|
2055
|
-
display: none;
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2337
|
+
display: none; position: sticky; top: calc(var(--header) + 12px); z-index: 15;
|
|
2338
|
+
align-items: center; gap: 10px; margin: 0 0 16px; padding: 11px 12px 11px 14px;
|
|
2339
|
+
border: 1px solid color-mix(in srgb, var(--err) 30%, var(--line)); border-radius: 10px;
|
|
2340
|
+
background: var(--err-soft); color: var(--err); box-shadow: var(--shadow-sm); font-size: 13px;
|
|
2341
|
+
}
|
|
2342
|
+
.banner.show { display: flex; }
|
|
2343
|
+
.banner span { flex: 1; }
|
|
2344
|
+
.banner button { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 7px; background: transparent; color: currentColor; cursor: pointer; }
|
|
2345
|
+
.banner button:hover { background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
2346
|
+
.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); }
|
|
2060
2347
|
dialog {
|
|
2061
|
-
border: 1px solid var(--line); border-radius:
|
|
2062
|
-
color: var(--ink); width: min(
|
|
2063
|
-
}
|
|
2064
|
-
dialog
|
|
2065
|
-
dialog
|
|
2348
|
+
border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--bg-elev);
|
|
2349
|
+
color: var(--ink); width: min(720px, calc(100% - 24px)); max-width: calc(100vw - 24px); box-shadow: var(--shadow-lg);
|
|
2350
|
+
}
|
|
2351
|
+
dialog[open] { animation: dialog-in 160ms ease-out; }
|
|
2352
|
+
dialog::backdrop { background: rgb(12 14 20 / 0.58); backdrop-filter: blur(3px); }
|
|
2353
|
+
dialog form, .dialog-body { padding: 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
2354
|
+
.dialog-heading { display: flex; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
|
2355
|
+
.dialog-heading strong { font-size: 18px; letter-spacing: -0.02em; }
|
|
2356
|
+
.dialog-heading span { color: var(--muted); font-size: 12px; }
|
|
2066
2357
|
textarea {
|
|
2067
2358
|
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
2068
2359
|
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
2069
2360
|
font-family: var(--mono); font-size: 12px;
|
|
2070
2361
|
}
|
|
2071
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
2072
|
-
.
|
|
2362
|
+
.dialog-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding-top: 4px; }
|
|
2363
|
+
.dialog-actions .btn-danger { margin-right: auto; }
|
|
2364
|
+
.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); }
|
|
2073
2365
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
2074
2366
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
2075
2367
|
.status-2 { color: var(--ok); }
|
|
2076
2368
|
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
2077
2369
|
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
2370
|
+
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
2371
|
+
.form-grid .wide { grid-column: span 2; }
|
|
2078
2372
|
.route {
|
|
2079
|
-
display: grid; grid-template-columns:
|
|
2080
|
-
padding:
|
|
2081
|
-
}
|
|
2082
|
-
.
|
|
2373
|
+
display: grid; grid-template-columns: 84px minmax(0, 1fr) auto; gap: 10px; align-items: center;
|
|
2374
|
+
padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--bg-elev);
|
|
2375
|
+
}
|
|
2376
|
+
.route:first-child { border-radius: var(--radius) var(--radius) 0 0; }
|
|
2377
|
+
.route:last-child { border-bottom: 0; border-radius: 0 0 var(--radius) var(--radius); }
|
|
2378
|
+
.route-tools { display: flex; align-items: flex-end; gap: 12px; margin-bottom: 12px; }
|
|
2379
|
+
.route-tools .field { flex: 1; }
|
|
2380
|
+
.route-tools input { width: 100%; }
|
|
2381
|
+
.route-count { padding-bottom: 10px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
2382
|
+
#route-list { max-height: 34rem; border: 1px solid var(--line); border-radius: var(--radius); overflow: auto; box-shadow: var(--shadow-xs); }
|
|
2383
|
+
.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; }
|
|
2384
|
+
@keyframes view-in { from { transform: translateY(3px); } to { transform: translateY(0); } }
|
|
2385
|
+
@keyframes dialog-in { from { transform: translateY(8px) scale(0.99); } to { transform: translateY(0) scale(1); } }
|
|
2386
|
+
@keyframes pulse { 50% { opacity: 0.45; } }
|
|
2387
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
|
|
2388
|
+
@media (forced-colors: active) { .json-editor pre { display: none; } .json-editor textarea { color: CanvasText; -webkit-text-fill-color: CanvasText; } }
|
|
2083
2389
|
@media (max-width: 800px) {
|
|
2390
|
+
:root { --header: 0px; }
|
|
2084
2391
|
header.top {
|
|
2085
|
-
align-items: stretch; flex-direction: column;
|
|
2086
|
-
padding
|
|
2392
|
+
position: relative; align-items: stretch; flex-direction: column;
|
|
2393
|
+
padding: max(12px, env(safe-area-inset-top)) 14px 12px;
|
|
2087
2394
|
}
|
|
2088
2395
|
input, select, textarea { font-size: 16px; }
|
|
2089
2396
|
.brand, .vendor, .controls { width: 100%; }
|
|
2397
|
+
.brand { gap: 10px; }
|
|
2398
|
+
.vendor { order: 2; border-radius: 9px; }
|
|
2090
2399
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
2400
|
+
.sync-status { display: none; }
|
|
2091
2401
|
/* The layout fills the screen. A single-column grid stretches every auto
|
|
2092
2402
|
row, so the tab bar grew with the leftover height and the buttons floated
|
|
2093
2403
|
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
@@ -2097,14 +2407,23 @@ var renderAdminDocument = (service) => {
|
|
|
2097
2407
|
align-content: start;
|
|
2098
2408
|
}
|
|
2099
2409
|
nav.side {
|
|
2100
|
-
align-self: start; height: auto;
|
|
2410
|
+
position: sticky; top: 0; z-index: 12; align-self: start; height: auto;
|
|
2101
2411
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
2102
2412
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
2103
|
-
padding: 8px;
|
|
2413
|
+
padding: 8px; box-shadow: var(--shadow-xs);
|
|
2104
2414
|
}
|
|
2105
|
-
nav
|
|
2415
|
+
.nav-label { display: none; }
|
|
2416
|
+
nav.side button { width: auto; white-space: nowrap; flex: none; }
|
|
2417
|
+
nav.side button[aria-current="true"]::before { inset: auto 9px -8px; width: auto; height: 3px; border-radius: 3px 3px 0 0; }
|
|
2106
2418
|
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
2107
|
-
|
|
2419
|
+
.form-grid { grid-template-columns: minmax(0, 1fr); }
|
|
2420
|
+
.form-grid .wide { grid-column: auto; }
|
|
2421
|
+
main { padding: 22px 14px 48px; }
|
|
2422
|
+
.view-head { margin-bottom: 18px; }
|
|
2423
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
2424
|
+
.card { padding: 14px; }
|
|
2425
|
+
.card .v { font-size: 20px; }
|
|
2426
|
+
.table-shell { border: 0; background: transparent; box-shadow: none; overflow: visible; }
|
|
2108
2427
|
table, thead, tbody, tr, th, td { display: block; }
|
|
2109
2428
|
thead { display: none; }
|
|
2110
2429
|
tr {
|
|
@@ -2119,50 +2438,63 @@ var renderAdminDocument = (service) => {
|
|
|
2119
2438
|
.route { grid-template-columns: 72px 1fr; }
|
|
2120
2439
|
.route button { grid-column: 1 / -1; }
|
|
2121
2440
|
}
|
|
2441
|
+
@media (max-width: 460px) {
|
|
2442
|
+
.controls label.field { min-width: calc(50% - 4px); }
|
|
2443
|
+
.controls .icon-btn, .controls .btn { flex: 1; }
|
|
2444
|
+
.grid { grid-template-columns: minmax(0, 1fr); }
|
|
2445
|
+
.dialog-actions { flex-wrap: wrap; }
|
|
2446
|
+
.dialog-actions button { flex: 1; }
|
|
2447
|
+
.dialog-actions .btn-danger { flex-basis: 100%; margin-right: 0; }
|
|
2448
|
+
}
|
|
2122
2449
|
</style>
|
|
2123
2450
|
</head>
|
|
2124
2451
|
<body>
|
|
2125
2452
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
2126
2453
|
<header class="top">
|
|
2127
2454
|
<div class="brand">
|
|
2128
|
-
<div class="brand-
|
|
2129
|
-
<
|
|
2130
|
-
|
|
2455
|
+
<div class="brand-lockup">
|
|
2456
|
+
<div class="brand-mark" aria-hidden="true">
|
|
2457
|
+
<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>
|
|
2458
|
+
</div>
|
|
2459
|
+
<div class="brand-id">
|
|
2460
|
+
<strong>${name}</strong>
|
|
2461
|
+
<span>Mock admin</span>
|
|
2462
|
+
</div>
|
|
2131
2463
|
</div>
|
|
2132
2464
|
<div class="vendor" id="vendor" hidden></div>
|
|
2133
2465
|
</div>
|
|
2134
2466
|
<div class="controls">
|
|
2467
|
+
<span class="sync-status" id="sync-status" role="status" aria-live="polite">Ready</span>
|
|
2135
2468
|
<label class="field">Namespace
|
|
2136
2469
|
<select id="namespace" aria-label="Namespace"></select>
|
|
2137
2470
|
</label>
|
|
2138
2471
|
<label class="field">Admin key
|
|
2139
2472
|
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
2140
2473
|
</label>
|
|
2141
|
-
<button class="icon-btn" id="theme" type="button" aria-label="
|
|
2142
|
-
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
2474
|
+
<button class="icon-btn" id="theme" type="button" aria-label="Use dark theme" title="Use dark theme"></button>
|
|
2475
|
+
<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>
|
|
2143
2476
|
</div>
|
|
2144
2477
|
</header>
|
|
2145
2478
|
<div class="layout">
|
|
2146
2479
|
<nav class="side" id="nav" aria-label="Admin sections">
|
|
2147
|
-
<
|
|
2148
|
-
<button type="button" data-view="
|
|
2149
|
-
<button type="button" data-view="
|
|
2150
|
-
<button type="button" data-view="
|
|
2151
|
-
<button type="button" data-view="
|
|
2152
|
-
<button type="button" data-view="
|
|
2480
|
+
<span class="nav-label">Workspace</span>
|
|
2481
|
+
<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>
|
|
2482
|
+
<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>
|
|
2483
|
+
<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>
|
|
2484
|
+
<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>
|
|
2485
|
+
<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>
|
|
2486
|
+
<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>
|
|
2153
2487
|
</nav>
|
|
2154
2488
|
<main id="views">
|
|
2155
|
-
<
|
|
2489
|
+
<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>
|
|
2156
2490
|
<section class="view" id="view-overview">
|
|
2157
|
-
<h2>Overview</h2>
|
|
2158
|
-
<p class="lede">Health, clock, and the collections this namespace is holding.</p>
|
|
2491
|
+
<div class="view-head"><div><h2>Overview</h2><p class="lede">Health, clock, and the collections this namespace is holding.</p></div></div>
|
|
2159
2492
|
<div class="grid" id="overview-cards"></div>
|
|
2160
2493
|
<h3>Recent requests</h3>
|
|
2161
2494
|
<div id="overview-requests"></div>
|
|
2162
2495
|
</section>
|
|
2163
2496
|
<section class="view" id="view-state" hidden>
|
|
2164
|
-
<h2>State</h2>
|
|
2165
|
-
<p class="lede">Every collection in this namespace. Declared fields stay visible before any row exists; stored rows add whatever else they contain.</p>
|
|
2497
|
+
<div class="view-head"><div><h2>State</h2><p class="lede">Inspect and edit every collection in this namespace.</p></div></div>
|
|
2166
2498
|
<div class="split">
|
|
2167
2499
|
<div id="collections"></div>
|
|
2168
2500
|
<div>
|
|
@@ -2175,8 +2507,7 @@ var renderAdminDocument = (service) => {
|
|
|
2175
2507
|
</div>
|
|
2176
2508
|
</section>
|
|
2177
2509
|
<section class="view" id="view-clock" hidden>
|
|
2178
|
-
<h2>Clock</h2>
|
|
2179
|
-
<p class="lede">The clock every timestamp in this mock reads.</p>
|
|
2510
|
+
<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>
|
|
2180
2511
|
<div class="card" id="clock-readout"></div>
|
|
2181
2512
|
<div class="row-actions">
|
|
2182
2513
|
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
@@ -2195,21 +2526,22 @@ var renderAdminDocument = (service) => {
|
|
|
2195
2526
|
</form>
|
|
2196
2527
|
</section>
|
|
2197
2528
|
<section class="view" id="view-faults" hidden>
|
|
2198
|
-
<h2>Faults</h2>
|
|
2199
|
-
<p class="lede">Inject a response, a delay, or a named preset. Rules apply to this namespace.</p>
|
|
2529
|
+
<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>
|
|
2200
2530
|
<form id="fault-form" class="card stack">
|
|
2201
|
-
<
|
|
2202
|
-
<
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
<
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
<
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
<
|
|
2212
|
-
|
|
2531
|
+
<div class="form-grid">
|
|
2532
|
+
<label class="field wide">Preset
|
|
2533
|
+
<select id="fault-preset"><option value="">Custom rule</option></select>
|
|
2534
|
+
</label>
|
|
2535
|
+
<label class="field">Status
|
|
2536
|
+
<input id="fault-status" inputmode="numeric" placeholder="503">
|
|
2537
|
+
</label>
|
|
2538
|
+
<label class="field wide">Path prefix
|
|
2539
|
+
<input id="fault-path" placeholder="/v1">
|
|
2540
|
+
</label>
|
|
2541
|
+
<label class="field">Method
|
|
2542
|
+
<input id="fault-method" placeholder="GET">
|
|
2543
|
+
</label>
|
|
2544
|
+
</div>
|
|
2213
2545
|
<div class="dialog-actions">
|
|
2214
2546
|
<button class="btn-primary" type="submit">Add fault</button>
|
|
2215
2547
|
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
@@ -2218,26 +2550,31 @@ var renderAdminDocument = (service) => {
|
|
|
2218
2550
|
<div id="fault-list"></div>
|
|
2219
2551
|
</section>
|
|
2220
2552
|
<section class="view" id="view-journal" hidden>
|
|
2221
|
-
<h2>Journal</h2>
|
|
2222
|
-
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
2223
|
-
<div class="row-actions">
|
|
2224
|
-
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
2225
|
-
</div>
|
|
2553
|
+
<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>
|
|
2226
2554
|
<div id="journal"></div>
|
|
2227
2555
|
</section>
|
|
2228
2556
|
<section class="view" id="view-routes" hidden>
|
|
2229
|
-
<h2>Routes</h2>
|
|
2230
|
-
<
|
|
2557
|
+
<div class="view-head"><div><h2>Routes</h2><p class="lede">Explore shared controls and service-specific admin endpoints.</p></div></div>
|
|
2558
|
+
<div class="route-tools">
|
|
2559
|
+
<label class="field">Filter routes
|
|
2560
|
+
<input id="route-search" type="search" placeholder="Method, path, or extension" autocomplete="off">
|
|
2561
|
+
</label>
|
|
2562
|
+
<span class="route-count" id="route-count"></span>
|
|
2563
|
+
</div>
|
|
2231
2564
|
<div id="route-list"></div>
|
|
2232
|
-
<form id="route-form" class="card stack">
|
|
2565
|
+
<form id="route-form" class="card stack" style="margin-top: 18px">
|
|
2233
2566
|
<strong id="route-title">Select a route</strong>
|
|
2234
2567
|
<label class="field">JSON body
|
|
2235
|
-
<
|
|
2568
|
+
<div class="json-editor" id="route-editor" hidden>
|
|
2569
|
+
<pre aria-hidden="true"><code id="route-highlight"></code></pre>
|
|
2570
|
+
<textarea id="route-body" placeholder="{ }" spellcheck="false" aria-describedby="route-json-status" disabled></textarea>
|
|
2571
|
+
</div>
|
|
2572
|
+
<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>
|
|
2236
2573
|
</label>
|
|
2237
2574
|
<div class="dialog-actions">
|
|
2238
|
-
<button class="btn-primary" type="submit">Send</button>
|
|
2575
|
+
<button class="btn-primary" id="route-send" type="submit" disabled>Send request</button>
|
|
2239
2576
|
</div>
|
|
2240
|
-
<pre id="route-result">Response will show here.</pre>
|
|
2577
|
+
<pre class="json-view" id="route-result"><code>Response will show here.</code></pre>
|
|
2241
2578
|
</form>
|
|
2242
2579
|
</section>
|
|
2243
2580
|
</main>
|
|
@@ -2245,12 +2582,16 @@ var renderAdminDocument = (service) => {
|
|
|
2245
2582
|
</div>
|
|
2246
2583
|
<dialog id="editor">
|
|
2247
2584
|
<form method="dialog" id="editor-form">
|
|
2248
|
-
<strong id="editor-title">Record</strong>
|
|
2585
|
+
<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>
|
|
2249
2586
|
<label class="field">Id
|
|
2250
2587
|
<input id="editor-id" required>
|
|
2251
2588
|
</label>
|
|
2252
2589
|
<label class="field">JSON value
|
|
2253
|
-
<
|
|
2590
|
+
<div class="json-editor" id="record-editor">
|
|
2591
|
+
<pre aria-hidden="true"><code id="editor-highlight"></code></pre>
|
|
2592
|
+
<textarea id="editor-value" spellcheck="false" aria-describedby="editor-json-status"></textarea>
|
|
2593
|
+
</div>
|
|
2594
|
+
<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>
|
|
2254
2595
|
</label>
|
|
2255
2596
|
<div class="dialog-actions">
|
|
2256
2597
|
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
@@ -2292,8 +2633,8 @@ var describeExtension = (extension) => {
|
|
|
2292
2633
|
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
2293
2634
|
};
|
|
2294
2635
|
};
|
|
2295
|
-
var adminUiRoutes = (service, ui) => {
|
|
2296
|
-
const shell = () => renderAdminDocument(service);
|
|
2636
|
+
var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
2637
|
+
const shell = () => renderAdminDocument(service, adminPrefix);
|
|
2297
2638
|
const document2 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
2298
2639
|
return {
|
|
2299
2640
|
"GET /ui": () => htmlResponse(document2()),
|
|
@@ -2425,7 +2766,33 @@ var faultResponse = (rule) => {
|
|
|
2425
2766
|
};
|
|
2426
2767
|
var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) => {
|
|
2427
2768
|
const entries = [];
|
|
2769
|
+
let isolated = false;
|
|
2770
|
+
const scopedRngs = /* @__PURE__ */ new Map();
|
|
2771
|
+
const random = (namespace) => {
|
|
2772
|
+
if (!isolated || namespace === "default")
|
|
2773
|
+
return rng;
|
|
2774
|
+
let scoped = scopedRngs.get(namespace);
|
|
2775
|
+
if (!scoped) {
|
|
2776
|
+
scoped = createRng(rng.seed);
|
|
2777
|
+
scopedRngs.set(namespace, scoped);
|
|
2778
|
+
}
|
|
2779
|
+
return scoped;
|
|
2780
|
+
};
|
|
2428
2781
|
return {
|
|
2782
|
+
isolateNamespaces() {
|
|
2783
|
+
isolated = true;
|
|
2784
|
+
},
|
|
2785
|
+
snapshot(namespace) {
|
|
2786
|
+
return {
|
|
2787
|
+
rules: structuredClone(entries.map((entry, position) => ({
|
|
2788
|
+
...entry.rule,
|
|
2789
|
+
remaining: entry.remaining,
|
|
2790
|
+
hits: entry.hits,
|
|
2791
|
+
position
|
|
2792
|
+
})).filter((entry) => entry.namespace === namespace)),
|
|
2793
|
+
rngState: random(namespace).state()
|
|
2794
|
+
};
|
|
2795
|
+
},
|
|
2429
2796
|
add(rule) {
|
|
2430
2797
|
const existing = entries.findIndex((e) => e.rule.id === rule.id);
|
|
2431
2798
|
const entry = { rule, remaining: rule.count ?? null, hits: 0 };
|
|
@@ -2445,6 +2812,16 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
|
|
|
2445
2812
|
},
|
|
2446
2813
|
clear() {
|
|
2447
2814
|
entries.length = 0;
|
|
2815
|
+
rng.reset();
|
|
2816
|
+
scopedRngs.clear();
|
|
2817
|
+
},
|
|
2818
|
+
restore(namespace, state) {
|
|
2819
|
+
for (let i = entries.length - 1; i >= 0; i--)
|
|
2820
|
+
if (entries[i]?.rule.namespace === namespace)
|
|
2821
|
+
entries.splice(i, 1);
|
|
2822
|
+
for (const { remaining, hits, position, ...rule } of state.rules)
|
|
2823
|
+
entries.splice(Math.min(position, entries.length), 0, { rule, remaining, hits });
|
|
2824
|
+
random(namespace).setState(state.rngState);
|
|
2448
2825
|
},
|
|
2449
2826
|
async take(candidate) {
|
|
2450
2827
|
const hits = [];
|
|
@@ -2454,7 +2831,7 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
|
|
|
2454
2831
|
if (!matches(entry.rule, candidate))
|
|
2455
2832
|
continue;
|
|
2456
2833
|
const rate = entry.rule.rate ?? 1;
|
|
2457
|
-
if (
|
|
2834
|
+
if (random(candidate.namespace).next() >= rate)
|
|
2458
2835
|
continue;
|
|
2459
2836
|
entry.hits++;
|
|
2460
2837
|
if (entry.remaining !== null)
|
|
@@ -2521,7 +2898,8 @@ var createJournal = (size = DEFAULT_JOURNAL_SIZE) => {
|
|
|
2521
2898
|
};
|
|
2522
2899
|
|
|
2523
2900
|
// ../core/dist/metrics.js
|
|
2524
|
-
var createMetrics = () => {
|
|
2901
|
+
var createMetrics = (partition = true) => {
|
|
2902
|
+
const namespaces = /* @__PURE__ */ new Map();
|
|
2525
2903
|
let requests = 0;
|
|
2526
2904
|
let faults = 0;
|
|
2527
2905
|
let totalDurationMs = 0;
|
|
@@ -2529,6 +2907,14 @@ var createMetrics = () => {
|
|
|
2529
2907
|
const unmatched = /* @__PURE__ */ new Map();
|
|
2530
2908
|
return {
|
|
2531
2909
|
record(entry) {
|
|
2910
|
+
if (partition) {
|
|
2911
|
+
let scoped = namespaces.get(entry.namespace);
|
|
2912
|
+
if (!scoped) {
|
|
2913
|
+
scoped = createMetrics(false);
|
|
2914
|
+
namespaces.set(entry.namespace, scoped);
|
|
2915
|
+
}
|
|
2916
|
+
scoped.record(entry);
|
|
2917
|
+
}
|
|
2532
2918
|
requests++;
|
|
2533
2919
|
totalDurationMs += entry.durationMs;
|
|
2534
2920
|
if (entry.faultId !== void 0)
|
|
@@ -2540,7 +2926,7 @@ var createMetrics = () => {
|
|
|
2540
2926
|
unmatched.set(route, (unmatched.get(route) ?? 0) + 1);
|
|
2541
2927
|
}
|
|
2542
2928
|
},
|
|
2543
|
-
report: () => ({
|
|
2929
|
+
report: (namespace) => namespace !== void 0 ? (namespaces.get(namespace) ?? createMetrics(false)).report() : {
|
|
2544
2930
|
requests,
|
|
2545
2931
|
byOperation: Object.fromEntries([...byOperation].sort(([a], [b]) => a.localeCompare(b))),
|
|
2546
2932
|
unmatched: [...unmatched].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([route, count]) => {
|
|
@@ -2549,8 +2935,13 @@ var createMetrics = () => {
|
|
|
2549
2935
|
}),
|
|
2550
2936
|
faults,
|
|
2551
2937
|
totalDurationMs
|
|
2552
|
-
}
|
|
2553
|
-
reset() {
|
|
2938
|
+
},
|
|
2939
|
+
reset(namespace) {
|
|
2940
|
+
if (namespace !== void 0) {
|
|
2941
|
+
namespaces.delete(namespace);
|
|
2942
|
+
return;
|
|
2943
|
+
}
|
|
2944
|
+
namespaces.clear();
|
|
2554
2945
|
requests = 0;
|
|
2555
2946
|
faults = 0;
|
|
2556
2947
|
totalDurationMs = 0;
|
|
@@ -2574,7 +2965,7 @@ var PAGE_DEFAULT = 50;
|
|
|
2574
2965
|
var PAGE_MAX = 200;
|
|
2575
2966
|
var json3 = (status, body) => new Response(JSON.stringify(body), {
|
|
2576
2967
|
status,
|
|
2577
|
-
headers: { "content-type": "application/json" }
|
|
2968
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
2578
2969
|
});
|
|
2579
2970
|
var adminError2 = (status, message) => json3(status, { error: { type: "mockingbird_admin", message } });
|
|
2580
2971
|
var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -2827,6 +3218,7 @@ var sqlRoutes = (engine) => ({
|
|
|
2827
3218
|
}
|
|
2828
3219
|
});
|
|
2829
3220
|
var createEngineAdmin = (options) => {
|
|
3221
|
+
const adminPrefix = resolveAdminPrefix(options.adminPrefix);
|
|
2830
3222
|
const engines = /* @__PURE__ */ new Map();
|
|
2831
3223
|
const engine = (namespace) => {
|
|
2832
3224
|
const existing = engines.get(namespace);
|
|
@@ -2841,7 +3233,7 @@ var createEngineAdmin = (options) => {
|
|
|
2841
3233
|
...credentialRoutes(createCredentialRegistry()),
|
|
2842
3234
|
...stateRoutes(engine),
|
|
2843
3235
|
...sqlRoutes(engine),
|
|
2844
|
-
...adminUiRoutes(options.name, { extensions: [SQL_ADMIN_EXTENSION] })
|
|
3236
|
+
...adminUiRoutes(options.name, { extensions: [SQL_ADMIN_EXTENSION] }, adminPrefix)
|
|
2845
3237
|
};
|
|
2846
3238
|
const startedAt = Date.now();
|
|
2847
3239
|
const plane = createControlPlane({
|
|
@@ -2872,6 +3264,7 @@ var createEngineAdmin = (options) => {
|
|
|
2872
3264
|
},
|
|
2873
3265
|
describe: () => options.dialect === void 0 ? {} : { dialect: options.dialect },
|
|
2874
3266
|
routes,
|
|
3267
|
+
adminPrefix,
|
|
2875
3268
|
adminKey: options.adminKey
|
|
2876
3269
|
});
|
|
2877
3270
|
return {
|
|
@@ -26711,6 +27104,7 @@ function createAdmin(options = {}) {
|
|
|
26711
27104
|
return createEngineAdmin({
|
|
26712
27105
|
name: "postgres",
|
|
26713
27106
|
dialect: "postgres",
|
|
27107
|
+
...options.adminPrefix !== void 0 ? { adminPrefix: options.adminPrefix } : {},
|
|
26714
27108
|
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
26715
27109
|
open(namespace) {
|
|
26716
27110
|
if (namespace === "default" && seeded) return adapt(seeded);
|