@crvouga/mockingbird-service-sqlite 1.3.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 +15 -1
- package/DISCOVERY.md +48 -0
- package/dist/admin.js +659 -235
- package/dist/admin.js.map +3 -3
- package/package.json +19 -2
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(text2) {
|
|
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 text2.matchAll(token)) {
|
|
397
|
+
const index = match.index ?? 0;
|
|
398
|
+
const value = match[0];
|
|
399
|
+
html += escapeToken(text2.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(text2.slice(cursor));
|
|
405
|
+
}
|
|
378
406
|
function mountAdminBrand(doc, host, service, catalog) {
|
|
379
407
|
const clip = (value, max) => {
|
|
380
408
|
if (typeof value !== "string") return null;
|
|
@@ -714,14 +742,18 @@ function bootAdmin(config) {
|
|
|
714
742
|
const state = {
|
|
715
743
|
namespace: "default",
|
|
716
744
|
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
717
|
-
view: "overview",
|
|
745
|
+
view: ["overview", "state", "clock", "faults", "journal", "routes"].includes(
|
|
746
|
+
location.hash.slice(1)
|
|
747
|
+
) ? location.hash.slice(1) : "overview",
|
|
718
748
|
collection: "",
|
|
719
749
|
shape: null,
|
|
720
750
|
route: null,
|
|
751
|
+
routes: [],
|
|
721
752
|
editing: null
|
|
722
753
|
};
|
|
723
754
|
const standard = new Set(config.standardRoutes);
|
|
724
755
|
const banner = el("banner", HTMLElement);
|
|
756
|
+
const bannerMessage = el("banner-message", HTMLElement);
|
|
725
757
|
const keyInput = el("admin-key", HTMLInputElement);
|
|
726
758
|
keyInput.value = state.key;
|
|
727
759
|
const params = new URLSearchParams(location.search);
|
|
@@ -732,17 +764,20 @@ function bootAdmin(config) {
|
|
|
732
764
|
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
733
765
|
params.delete("key");
|
|
734
766
|
const next = params.toString();
|
|
735
|
-
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
767
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : "") + location.hash);
|
|
736
768
|
}
|
|
737
769
|
const queryNamespace = params.get("namespace");
|
|
738
770
|
if (queryNamespace) state.namespace = queryNamespace;
|
|
739
771
|
const showError = (error) => {
|
|
740
|
-
|
|
772
|
+
bannerMessage.textContent = error instanceof Error ? error.message : String(error);
|
|
741
773
|
banner.classList.add("show");
|
|
774
|
+
banner.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
742
775
|
};
|
|
743
776
|
const clearError = () => {
|
|
744
777
|
banner.classList.remove("show");
|
|
778
|
+
bannerMessage.textContent = "";
|
|
745
779
|
};
|
|
780
|
+
el("banner-dismiss", HTMLButtonElement).addEventListener("click", clearError);
|
|
746
781
|
const api = async (method, path, body) => {
|
|
747
782
|
const headers = { accept: "application/json" };
|
|
748
783
|
if (state.key !== "") headers[config.adminKeyHeader] = state.key;
|
|
@@ -781,13 +816,19 @@ function bootAdmin(config) {
|
|
|
781
816
|
namespace: () => state.namespace
|
|
782
817
|
};
|
|
783
818
|
const isPanelScript = (value) => typeof value === "function";
|
|
819
|
+
const themeButton = el("theme", HTMLButtonElement);
|
|
784
820
|
const setTheme = (theme) => {
|
|
785
821
|
if (theme) document.documentElement.dataset.theme = theme;
|
|
786
822
|
else delete document.documentElement.dataset.theme;
|
|
823
|
+
const dark = theme ? theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
824
|
+
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>';
|
|
825
|
+
const label = dark ? "Use light theme" : "Use dark theme";
|
|
826
|
+
themeButton.setAttribute("aria-label", label);
|
|
827
|
+
themeButton.title = label;
|
|
787
828
|
};
|
|
788
829
|
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
789
|
-
|
|
790
|
-
|
|
830
|
+
setTheme(storedTheme === "dark" || storedTheme === "light" ? storedTheme : null);
|
|
831
|
+
themeButton.addEventListener("click", () => {
|
|
791
832
|
const current = document.documentElement.dataset.theme;
|
|
792
833
|
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
793
834
|
const next = dark ? "light" : "dark";
|
|
@@ -804,6 +845,11 @@ function bootAdmin(config) {
|
|
|
804
845
|
if (!(section instanceof HTMLElement)) continue;
|
|
805
846
|
section.hidden = section.id !== `view-${view}`;
|
|
806
847
|
}
|
|
848
|
+
history.replaceState(
|
|
849
|
+
null,
|
|
850
|
+
"",
|
|
851
|
+
`${location.pathname}${location.search}#${encodeURIComponent(view)}`
|
|
852
|
+
);
|
|
807
853
|
};
|
|
808
854
|
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
809
855
|
const target = event.target;
|
|
@@ -816,7 +862,7 @@ function bootAdmin(config) {
|
|
|
816
862
|
refresh().catch(showError);
|
|
817
863
|
});
|
|
818
864
|
const cards = (items) => `<div class="grid">${items.map(
|
|
819
|
-
(item) => `<article class="card"><div class="k">${item.k}</div><div class="v">${item.v}</div></article>`
|
|
865
|
+
(item) => `<article class="card stat-card"><div class="k">${item.k}</div><div class="v">${item.v}</div></article>`
|
|
820
866
|
).join("")}</div>`;
|
|
821
867
|
const table = (columns, rows) => {
|
|
822
868
|
if (rows.length === 0) return '<p class="empty">Nothing here yet.</p>';
|
|
@@ -826,16 +872,22 @@ function bootAdmin(config) {
|
|
|
826
872
|
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
827
873
|
return `<tr${attrs}>${cells}</tr>`;
|
|
828
874
|
}).join("");
|
|
829
|
-
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
875
|
+
return `<div class="table-shell"><table><thead>${head}</thead><tbody>${body}</tbody></table></div>`;
|
|
830
876
|
};
|
|
831
877
|
const preview = (value) => {
|
|
832
878
|
const text2 = JSON.stringify(value) ?? "null";
|
|
833
|
-
|
|
879
|
+
const clipped = text2.length > 180 ? `${text2.slice(0, 180)}\u2026` : text2;
|
|
880
|
+
return `<code class="json-inline">${highlightJsonText(clipped)}</code>`;
|
|
834
881
|
};
|
|
835
882
|
const when = (epoch) => {
|
|
836
883
|
const date = new Date(epoch);
|
|
837
884
|
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
838
885
|
};
|
|
886
|
+
const journalWhen = (value) => {
|
|
887
|
+
const numeric2 = value.trim() === "" ? Number.NaN : Number(value);
|
|
888
|
+
const epoch = Number.isFinite(numeric2) ? numeric2 : Date.parse(value);
|
|
889
|
+
return Number.isFinite(epoch) ? when(epoch) : value;
|
|
890
|
+
};
|
|
839
891
|
const clockCard = (epoch) => {
|
|
840
892
|
const date = new Date(epoch);
|
|
841
893
|
if (Number.isNaN(date.getTime())) return esc(String(epoch));
|
|
@@ -947,7 +999,7 @@ function bootAdmin(config) {
|
|
|
947
999
|
};
|
|
948
1000
|
const renderOverview = async () => {
|
|
949
1001
|
const [health, shape, clock, journal] = await Promise.all([
|
|
950
|
-
originGet(
|
|
1002
|
+
originGet(`${config.adminPrefix}/health`),
|
|
951
1003
|
api("GET", "/state"),
|
|
952
1004
|
api("GET", "/clock"),
|
|
953
1005
|
api("GET", "/requests?limit=8")
|
|
@@ -965,7 +1017,9 @@ function bootAdmin(config) {
|
|
|
965
1017
|
},
|
|
966
1018
|
{
|
|
967
1019
|
k: "Status",
|
|
968
|
-
v:
|
|
1020
|
+
v: `<span class="badge ok">${esc(
|
|
1021
|
+
typeof health.status === "string" && health.status !== "" ? health.status : "ok"
|
|
1022
|
+
)}</span>`
|
|
969
1023
|
},
|
|
970
1024
|
{ k: "Namespace", v: esc(state.namespace) },
|
|
971
1025
|
{ k: "Collections", v: String(decoded.collections.length) },
|
|
@@ -1068,27 +1122,47 @@ function bootAdmin(config) {
|
|
|
1068
1122
|
const journal = await api("GET", "/requests?limit=100");
|
|
1069
1123
|
el("journal", HTMLElement).innerHTML = table(
|
|
1070
1124
|
[
|
|
1071
|
-
{ label: "When", cell: (row) => esc(row.at) },
|
|
1072
|
-
{
|
|
1125
|
+
{ label: "When", cell: (row) => esc(journalWhen(row.at)) },
|
|
1126
|
+
{
|
|
1127
|
+
label: "Method",
|
|
1128
|
+
cell: (row) => `<span class="method">${esc(row.method)}</span>`
|
|
1129
|
+
},
|
|
1073
1130
|
{ label: "Path", cell: (row) => esc(row.path) },
|
|
1074
|
-
{
|
|
1131
|
+
{
|
|
1132
|
+
label: "Status",
|
|
1133
|
+
cell: (row) => `<span class="badge ${row.status.startsWith("2") ? "ok" : row.status.startsWith("4") || row.status.startsWith("5") ? "warn" : ""}">${esc(row.status)}</span>`
|
|
1134
|
+
},
|
|
1075
1135
|
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
1076
1136
|
],
|
|
1077
1137
|
decodeJournal(journal)
|
|
1078
1138
|
);
|
|
1079
1139
|
};
|
|
1080
|
-
const
|
|
1081
|
-
const
|
|
1082
|
-
|
|
1083
|
-
|
|
1140
|
+
const paintRoutes = () => {
|
|
1141
|
+
const query = el("route-search", HTMLInputElement).value.trim().toLocaleLowerCase();
|
|
1142
|
+
const routes = state.routes.filter((route) => route.toLocaleLowerCase().includes(query));
|
|
1143
|
+
el("route-count", HTMLElement).textContent = `${routes.length} of ${state.routes.length}`;
|
|
1144
|
+
if (state.routes.length === 0) {
|
|
1145
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No admin routes are available.</p>';
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
if (routes.length === 0) {
|
|
1149
|
+
el("route-list", HTMLElement).innerHTML = '<p class="empty">No routes match this filter.</p>';
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1084
1152
|
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
1085
1153
|
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
1086
1154
|
const parts = route.split(" ");
|
|
1087
1155
|
const method = parts[0] ?? "";
|
|
1088
1156
|
const path = parts.slice(1).join(" ");
|
|
1089
|
-
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>`;
|
|
1157
|
+
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>`;
|
|
1090
1158
|
}).join("");
|
|
1091
1159
|
};
|
|
1160
|
+
const renderRoutes = async () => {
|
|
1161
|
+
const data = await api("GET", "/");
|
|
1162
|
+
if (!isRecord(data)) throw new Error("Route list was not an object");
|
|
1163
|
+
state.routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
1164
|
+
paintRoutes();
|
|
1165
|
+
};
|
|
1092
1166
|
const renderers = {
|
|
1093
1167
|
overview: renderOverview,
|
|
1094
1168
|
state: renderState,
|
|
@@ -1097,17 +1171,46 @@ function bootAdmin(config) {
|
|
|
1097
1171
|
journal: renderJournal,
|
|
1098
1172
|
routes: renderRoutes
|
|
1099
1173
|
};
|
|
1174
|
+
let refreshToken = 0;
|
|
1100
1175
|
async function refresh() {
|
|
1176
|
+
const token = ++refreshToken;
|
|
1101
1177
|
clearError();
|
|
1178
|
+
const main = el("views", HTMLElement);
|
|
1179
|
+
const sync = el("sync-status", HTMLElement);
|
|
1180
|
+
main.setAttribute("aria-busy", "true");
|
|
1181
|
+
sync.classList.add("busy");
|
|
1182
|
+
sync.textContent = "Updating";
|
|
1102
1183
|
const render = renderers[state.view];
|
|
1103
|
-
|
|
1184
|
+
try {
|
|
1185
|
+
if (render) await render();
|
|
1186
|
+
if (token === refreshToken) sync.textContent = "Up to date";
|
|
1187
|
+
} finally {
|
|
1188
|
+
if (token === refreshToken) {
|
|
1189
|
+
main.removeAttribute("aria-busy");
|
|
1190
|
+
sync.classList.remove("busy");
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1104
1193
|
}
|
|
1105
1194
|
const delay = (ms) => new Promise((resolve) => {
|
|
1106
1195
|
window.setTimeout(resolve, ms);
|
|
1107
1196
|
});
|
|
1108
|
-
const confirmAction = async (button, pending, done, work) => {
|
|
1197
|
+
const confirmAction = async (button, pending, done, work, confirmLabel) => {
|
|
1109
1198
|
if (button.dataset.pending === "1") return;
|
|
1110
1199
|
const label = button.textContent ?? "";
|
|
1200
|
+
if (confirmLabel !== void 0 && button.dataset.confirming !== "1") {
|
|
1201
|
+
button.dataset.confirming = "1";
|
|
1202
|
+
button.classList.add("confirming");
|
|
1203
|
+
button.textContent = confirmLabel;
|
|
1204
|
+
window.setTimeout(() => {
|
|
1205
|
+
if (button.dataset.confirming !== "1") return;
|
|
1206
|
+
button.textContent = label;
|
|
1207
|
+
button.classList.remove("confirming");
|
|
1208
|
+
delete button.dataset.confirming;
|
|
1209
|
+
}, 3e3);
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
button.classList.remove("confirming");
|
|
1213
|
+
delete button.dataset.confirming;
|
|
1111
1214
|
const width = button.offsetWidth;
|
|
1112
1215
|
button.dataset.pending = "1";
|
|
1113
1216
|
button.classList.add("pressed");
|
|
@@ -1128,6 +1231,72 @@ function bootAdmin(config) {
|
|
|
1128
1231
|
delete button.dataset.pending;
|
|
1129
1232
|
}
|
|
1130
1233
|
};
|
|
1234
|
+
const wireJsonEditor = (textareaId, highlightId, statusId, shellId, allowEmpty) => {
|
|
1235
|
+
const textarea = el(textareaId, HTMLTextAreaElement);
|
|
1236
|
+
const highlight = el(highlightId, HTMLElement);
|
|
1237
|
+
const status = el(statusId, HTMLElement);
|
|
1238
|
+
const shell = el(shellId, HTMLElement);
|
|
1239
|
+
const sync = () => {
|
|
1240
|
+
const text2 = textarea.value;
|
|
1241
|
+
highlight.innerHTML = `${highlightJsonText(text2)}
|
|
1242
|
+
`;
|
|
1243
|
+
let valid = true;
|
|
1244
|
+
let message = "Valid JSON";
|
|
1245
|
+
if (allowEmpty && text2.trim() === "") message = "No body";
|
|
1246
|
+
else {
|
|
1247
|
+
try {
|
|
1248
|
+
readJson2(text2);
|
|
1249
|
+
} catch {
|
|
1250
|
+
valid = false;
|
|
1251
|
+
message = "Invalid JSON";
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
shell.classList.toggle("invalid", !valid);
|
|
1255
|
+
status.classList.toggle("invalid", !valid);
|
|
1256
|
+
status.textContent = message;
|
|
1257
|
+
textarea.setAttribute("aria-invalid", valid ? "false" : "true");
|
|
1258
|
+
};
|
|
1259
|
+
const syncScroll = () => {
|
|
1260
|
+
const preview2 = highlight.parentElement;
|
|
1261
|
+
if (!(preview2 instanceof HTMLElement)) return;
|
|
1262
|
+
preview2.scrollTop = textarea.scrollTop;
|
|
1263
|
+
preview2.scrollLeft = textarea.scrollLeft;
|
|
1264
|
+
};
|
|
1265
|
+
textarea.addEventListener("input", sync);
|
|
1266
|
+
textarea.addEventListener("scroll", syncScroll);
|
|
1267
|
+
sync();
|
|
1268
|
+
return sync;
|
|
1269
|
+
};
|
|
1270
|
+
const syncRouteEditor = wireJsonEditor(
|
|
1271
|
+
"route-body",
|
|
1272
|
+
"route-highlight",
|
|
1273
|
+
"route-json-status",
|
|
1274
|
+
"route-editor",
|
|
1275
|
+
true
|
|
1276
|
+
);
|
|
1277
|
+
const syncRecordEditor = wireJsonEditor(
|
|
1278
|
+
"editor-value",
|
|
1279
|
+
"editor-highlight",
|
|
1280
|
+
"editor-json-status",
|
|
1281
|
+
"record-editor",
|
|
1282
|
+
false
|
|
1283
|
+
);
|
|
1284
|
+
for (const button of document.querySelectorAll("[data-format]")) {
|
|
1285
|
+
if (!(button instanceof HTMLButtonElement)) continue;
|
|
1286
|
+
button.addEventListener("click", () => {
|
|
1287
|
+
const target = button.dataset.format;
|
|
1288
|
+
if (target === void 0) return;
|
|
1289
|
+
const textarea = document.getElementById(target);
|
|
1290
|
+
if (!(textarea instanceof HTMLTextAreaElement) || textarea.value.trim() === "") return;
|
|
1291
|
+
try {
|
|
1292
|
+
textarea.value = JSON.stringify(readJson2(textarea.value), null, 2) ?? "";
|
|
1293
|
+
textarea.dispatchEvent(new Event("input"));
|
|
1294
|
+
textarea.focus();
|
|
1295
|
+
} catch {
|
|
1296
|
+
showError(new Error("Fix the JSON before formatting it"));
|
|
1297
|
+
}
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1131
1300
|
document.addEventListener("click", (event) => {
|
|
1132
1301
|
const target = event.target;
|
|
1133
1302
|
if (!(target instanceof Element)) return;
|
|
@@ -1198,6 +1367,7 @@ function bootAdmin(config) {
|
|
|
1198
1367
|
} else {
|
|
1199
1368
|
editorValue.value = "{\n \n}";
|
|
1200
1369
|
}
|
|
1370
|
+
syncRecordEditor();
|
|
1201
1371
|
editor.showModal();
|
|
1202
1372
|
}
|
|
1203
1373
|
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -1225,10 +1395,19 @@ function bootAdmin(config) {
|
|
|
1225
1395
|
editorDelete.addEventListener("click", () => {
|
|
1226
1396
|
const id = state.editing;
|
|
1227
1397
|
if (!id) return;
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1398
|
+
confirmAction(
|
|
1399
|
+
editorDelete,
|
|
1400
|
+
"Deleting...",
|
|
1401
|
+
"Deleted",
|
|
1402
|
+
() => api(
|
|
1403
|
+
"DELETE",
|
|
1404
|
+
`/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`
|
|
1405
|
+
).then(() => {
|
|
1406
|
+
editor.close();
|
|
1407
|
+
return renderState();
|
|
1408
|
+
}),
|
|
1409
|
+
"Confirm delete"
|
|
1410
|
+
).catch(showError);
|
|
1232
1411
|
});
|
|
1233
1412
|
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
1234
1413
|
if (!(button instanceof HTMLButtonElement)) continue;
|
|
@@ -1276,7 +1455,8 @@ function bootAdmin(config) {
|
|
|
1276
1455
|
el("fault-clear", HTMLButtonElement),
|
|
1277
1456
|
"Clearing...",
|
|
1278
1457
|
"Cleared",
|
|
1279
|
-
() => api("DELETE", "/faults").then(() => renderFaults())
|
|
1458
|
+
() => api("DELETE", "/faults").then(() => renderFaults()),
|
|
1459
|
+
"Confirm clear"
|
|
1280
1460
|
).catch(showError);
|
|
1281
1461
|
});
|
|
1282
1462
|
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
@@ -1284,7 +1464,8 @@ function bootAdmin(config) {
|
|
|
1284
1464
|
el("journal-clear", HTMLButtonElement),
|
|
1285
1465
|
"Clearing...",
|
|
1286
1466
|
"Cleared",
|
|
1287
|
-
() => api("DELETE", "/requests").then(() => renderJournal())
|
|
1467
|
+
() => api("DELETE", "/requests").then(() => renderJournal()),
|
|
1468
|
+
"Confirm clear"
|
|
1288
1469
|
).catch(showError);
|
|
1289
1470
|
});
|
|
1290
1471
|
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
@@ -1296,9 +1477,18 @@ function bootAdmin(config) {
|
|
|
1296
1477
|
if (route === void 0) return;
|
|
1297
1478
|
state.route = route;
|
|
1298
1479
|
el("route-title", HTMLElement).textContent = route;
|
|
1480
|
+
el("route-send", HTMLButtonElement).disabled = false;
|
|
1299
1481
|
const method = route.split(" ")[0] ?? "";
|
|
1300
|
-
el("route-body", HTMLTextAreaElement)
|
|
1482
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
1483
|
+
routeBody.disabled = method === "GET" || method === "DELETE";
|
|
1484
|
+
el("route-editor", HTMLElement).hidden = routeBody.disabled;
|
|
1485
|
+
el("route-json-status", HTMLElement).parentElement?.toggleAttribute(
|
|
1486
|
+
"hidden",
|
|
1487
|
+
routeBody.disabled
|
|
1488
|
+
);
|
|
1489
|
+
syncRouteEditor();
|
|
1301
1490
|
});
|
|
1491
|
+
el("route-search", HTMLInputElement).addEventListener("input", paintRoutes);
|
|
1302
1492
|
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
1303
1493
|
event.preventDefault();
|
|
1304
1494
|
const send = async () => {
|
|
@@ -1321,7 +1511,8 @@ function bootAdmin(config) {
|
|
|
1321
1511
|
}
|
|
1322
1512
|
}
|
|
1323
1513
|
const data = await api(method, concrete, body);
|
|
1324
|
-
|
|
1514
|
+
const text2 = JSON.stringify(data, null, 2) ?? "null";
|
|
1515
|
+
el("route-result", HTMLElement).innerHTML = `<code>${highlightJsonText(text2)}</code>`;
|
|
1325
1516
|
};
|
|
1326
1517
|
send().catch(showError);
|
|
1327
1518
|
});
|
|
@@ -1441,11 +1632,13 @@ function bootAdmin(config) {
|
|
|
1441
1632
|
views.append(section);
|
|
1442
1633
|
mountSqlExplorer(body, panelApi);
|
|
1443
1634
|
}
|
|
1635
|
+
show(document.getElementById(`view-${state.view}`) === null ? "overview" : state.view);
|
|
1444
1636
|
return refresh();
|
|
1445
1637
|
}).catch(showError);
|
|
1446
1638
|
}
|
|
1447
1639
|
var clientPrelude = [
|
|
1448
1640
|
isRecord,
|
|
1641
|
+
highlightJsonText,
|
|
1449
1642
|
mountAdminBrand,
|
|
1450
1643
|
startAdminBrand,
|
|
1451
1644
|
faultPresetList,
|
|
@@ -1465,13 +1658,21 @@ ${entry}(${payload});
|
|
|
1465
1658
|
};
|
|
1466
1659
|
|
|
1467
1660
|
// ../core/src/control.ts
|
|
1468
|
-
var HEALTH_PATH = "/health";
|
|
1469
1661
|
var ADMIN_PREFIX = "/__admin";
|
|
1662
|
+
var resolveAdminPrefix = (prefix = ADMIN_PREFIX) => {
|
|
1663
|
+
if (!/^\/[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*$/.test(prefix)) {
|
|
1664
|
+
throw new Error(
|
|
1665
|
+
"adminPrefix must be an absolute non-root path of literal segments, without a trailing slash"
|
|
1666
|
+
);
|
|
1667
|
+
}
|
|
1668
|
+
return prefix;
|
|
1669
|
+
};
|
|
1670
|
+
var isAdminPath = (path, prefix) => path === prefix || path.startsWith(`${prefix}/`);
|
|
1470
1671
|
var ADMIN_KEY_HEADER = "x-mockingbird-admin-key";
|
|
1471
1672
|
var NAMESPACE_HEADER = "x-mockingbird-namespace";
|
|
1472
1673
|
var json = (status, body) => new Response(JSON.stringify(body), {
|
|
1473
1674
|
status,
|
|
1474
|
-
headers: { "content-type": "application/json" }
|
|
1675
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
1475
1676
|
});
|
|
1476
1677
|
var adminError = (status, message) => json(status, { error: { type: "mockingbird_admin", message } });
|
|
1477
1678
|
var UNITS = {
|
|
@@ -1514,11 +1715,25 @@ var readJson = async (request) => {
|
|
|
1514
1715
|
};
|
|
1515
1716
|
var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1516
1717
|
var createControlPlane = (context) => {
|
|
1718
|
+
const adminPrefix = resolveAdminPrefix(context.adminPrefix);
|
|
1517
1719
|
const snapshots = /* @__PURE__ */ new Map();
|
|
1518
1720
|
let snapshotCounter = 0;
|
|
1519
1721
|
const headerNamespace = (request) => request.headers.get(NAMESPACE_HEADER) ?? context.defaultNamespace;
|
|
1520
1722
|
const adminNamespace = (request, url) => url.searchParams.get("namespace") ?? headerNamespace(request);
|
|
1723
|
+
const health = () => {
|
|
1724
|
+
return json(200, {
|
|
1725
|
+
...context.describe(),
|
|
1726
|
+
status: "ok",
|
|
1727
|
+
service: context.name,
|
|
1728
|
+
uptimeMs: context.wallNow() - context.startedAt,
|
|
1729
|
+
clock: context.clock.state(),
|
|
1730
|
+
namespaces: context.namespaces().length,
|
|
1731
|
+
adminPrefix,
|
|
1732
|
+
adminUi: `${adminPrefix}/ui`
|
|
1733
|
+
});
|
|
1734
|
+
};
|
|
1521
1735
|
const builtin = {
|
|
1736
|
+
"GET /health": health,
|
|
1522
1737
|
"GET /": () => json(200, {
|
|
1523
1738
|
service: context.name,
|
|
1524
1739
|
routes: [...Object.keys(builtin), ...Object.keys(context.routes)].sort()
|
|
@@ -1529,23 +1744,24 @@ var createControlPlane = (context) => {
|
|
|
1529
1744
|
return json(200, { status: "ok", reset: target === "*" ? context.namespaces() : [target] });
|
|
1530
1745
|
},
|
|
1531
1746
|
"GET /namespaces": () => json(200, { default: context.defaultNamespace, namespaces: context.namespaces() }),
|
|
1532
|
-
"GET /clock": () => json(200, context.clock.state()),
|
|
1533
|
-
"POST /clock": ({ body }) => {
|
|
1747
|
+
"GET /clock": ({ namespace }) => json(200, (context.namespaceClock?.(namespace) ?? context.clock).state()),
|
|
1748
|
+
"POST /clock": ({ body, namespace }) => {
|
|
1749
|
+
const clock = context.namespaceClock?.(namespace) ?? context.clock;
|
|
1534
1750
|
if (!isRecord2(body)) return adminError(400, "expected a JSON object");
|
|
1535
|
-
if (body.reset === true)
|
|
1751
|
+
if (body.reset === true) clock.reset();
|
|
1752
|
+
if (body.freeze === true) clock.freeze();
|
|
1536
1753
|
if (body.set !== void 0) {
|
|
1537
1754
|
const instant = parseInstant(body.set);
|
|
1538
1755
|
if (instant === void 0) return adminError(400, "set: expected epoch ms or ISO-8601");
|
|
1539
|
-
|
|
1756
|
+
clock.set(instant);
|
|
1540
1757
|
}
|
|
1541
1758
|
if (body.advance !== void 0) {
|
|
1542
1759
|
const delta = parseDuration(body.advance);
|
|
1543
1760
|
if (delta === void 0) return adminError(400, 'advance: expected ms or "15m"-style');
|
|
1544
|
-
|
|
1761
|
+
clock.advance(delta);
|
|
1545
1762
|
}
|
|
1546
|
-
if (body.freeze ===
|
|
1547
|
-
|
|
1548
|
-
return json(200, context.clock.state());
|
|
1763
|
+
if (body.freeze === false) clock.unfreeze();
|
|
1764
|
+
return json(200, clock.state());
|
|
1549
1765
|
},
|
|
1550
1766
|
"GET /faults": () => json(200, { faults: context.faults.list() }),
|
|
1551
1767
|
"POST /faults": ({ body, namespace }) => {
|
|
@@ -1680,7 +1896,13 @@ var createControlPlane = (context) => {
|
|
|
1680
1896
|
return json(200, { status: "ok" });
|
|
1681
1897
|
}
|
|
1682
1898
|
};
|
|
1683
|
-
const
|
|
1899
|
+
for (const key of Object.keys(context.routes)) {
|
|
1900
|
+
if (Object.hasOwn(builtin, key)) throw new Error(`admin route ${key} overrides a shared route`);
|
|
1901
|
+
const path = key.slice(key.indexOf(" ") + 1);
|
|
1902
|
+
if (isAdminPath(path, "/ns") || isAdminPath(path, "/blobs"))
|
|
1903
|
+
throw new Error(`admin route ${key} overlaps an internal transport`);
|
|
1904
|
+
}
|
|
1905
|
+
const routes = [...Object.entries(builtin), ...Object.entries(context.routes)].map(
|
|
1684
1906
|
([key, handler]) => {
|
|
1685
1907
|
const space = key.indexOf(" ");
|
|
1686
1908
|
return { method: key.slice(0, space), pattern: key.slice(space + 1), handler };
|
|
@@ -1690,28 +1912,25 @@ var createControlPlane = (context) => {
|
|
|
1690
1912
|
namespaceOf: headerNamespace,
|
|
1691
1913
|
async handle(request) {
|
|
1692
1914
|
const url = new URL(request.url);
|
|
1693
|
-
if (url.pathname ===
|
|
1694
|
-
return
|
|
1695
|
-
status: "ok",
|
|
1696
|
-
service: context.name,
|
|
1697
|
-
uptimeMs: context.wallNow() - context.startedAt,
|
|
1698
|
-
clock: context.clock.state(),
|
|
1699
|
-
namespaces: context.namespaces().length,
|
|
1700
|
-
adminUi: `${ADMIN_PREFIX}/ui`,
|
|
1701
|
-
...context.describe()
|
|
1702
|
-
});
|
|
1915
|
+
if (url.pathname === `${adminPrefix}/health` && request.method === "GET") {
|
|
1916
|
+
return health();
|
|
1703
1917
|
}
|
|
1704
|
-
if (url.pathname
|
|
1918
|
+
if (!isAdminPath(url.pathname, adminPrefix)) {
|
|
1705
1919
|
return void 0;
|
|
1706
1920
|
}
|
|
1707
|
-
const path = url.pathname.slice(
|
|
1921
|
+
const path = url.pathname.slice(adminPrefix.length) || "/";
|
|
1708
1922
|
const shell = request.method === "GET" && (path === "/ui" || path === "/ui/");
|
|
1709
1923
|
if (!shell && context.adminKey !== void 0 && request.headers.get(ADMIN_KEY_HEADER) !== context.adminKey) {
|
|
1710
1924
|
return adminError(401, `missing or wrong ${ADMIN_KEY_HEADER}`);
|
|
1711
1925
|
}
|
|
1712
1926
|
for (const route of routes) {
|
|
1713
1927
|
if (route.method !== request.method) continue;
|
|
1714
|
-
|
|
1928
|
+
let params;
|
|
1929
|
+
try {
|
|
1930
|
+
params = matchRoute(route.pattern, path);
|
|
1931
|
+
} catch {
|
|
1932
|
+
return adminError(400, "invalid path encoding");
|
|
1933
|
+
}
|
|
1715
1934
|
if (!params) continue;
|
|
1716
1935
|
let body;
|
|
1717
1936
|
try {
|
|
@@ -1719,17 +1938,19 @@ var createControlPlane = (context) => {
|
|
|
1719
1938
|
} catch {
|
|
1720
1939
|
return adminError(400, "request body is not valid JSON");
|
|
1721
1940
|
}
|
|
1941
|
+
const namespace = adminNamespace(request, url);
|
|
1942
|
+
if (!/^[A-Za-z0-9_.-]{1,64}$/.test(namespace)) return adminError(400, "invalid namespace");
|
|
1722
1943
|
return route.handler({
|
|
1723
1944
|
request,
|
|
1724
1945
|
url,
|
|
1725
1946
|
params,
|
|
1726
|
-
namespace
|
|
1947
|
+
namespace,
|
|
1727
1948
|
body
|
|
1728
1949
|
});
|
|
1729
1950
|
}
|
|
1730
1951
|
return adminError(
|
|
1731
1952
|
404,
|
|
1732
|
-
`no admin route ${request.method} ${path}; GET ${
|
|
1953
|
+
`no admin route ${request.method} ${path}; GET ${adminPrefix} lists them`
|
|
1733
1954
|
);
|
|
1734
1955
|
}
|
|
1735
1956
|
};
|
|
@@ -1738,6 +1959,7 @@ var createControlPlane = (context) => {
|
|
|
1738
1959
|
// ../core/src/surface.ts
|
|
1739
1960
|
var STANDARD_ADMIN_ROUTES = [
|
|
1740
1961
|
"GET /",
|
|
1962
|
+
"GET /health",
|
|
1741
1963
|
"POST /reset",
|
|
1742
1964
|
"GET /namespaces",
|
|
1743
1965
|
"GET /clock",
|
|
@@ -1787,11 +2009,11 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
1787
2009
|
}
|
|
1788
2010
|
});
|
|
1789
2011
|
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
1790
|
-
var renderAdminDocument = (service) => {
|
|
2012
|
+
var renderAdminDocument = (service, adminPrefix = ADMIN_PREFIX) => {
|
|
1791
2013
|
const name = escapeHtml(service);
|
|
1792
2014
|
const script = adminClientSource({
|
|
1793
2015
|
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
1794
|
-
adminPrefix
|
|
2016
|
+
adminPrefix,
|
|
1795
2017
|
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
1796
2018
|
brandsUrl: ADMIN_BRANDS_URL,
|
|
1797
2019
|
service
|
|
@@ -1808,75 +2030,99 @@ var renderAdminDocument = (service) => {
|
|
|
1808
2030
|
${CSS_RESET}
|
|
1809
2031
|
:root {
|
|
1810
2032
|
color-scheme: light;
|
|
1811
|
-
--bg: #
|
|
2033
|
+
--bg: #f7f8fb;
|
|
1812
2034
|
--bg-elev: #ffffff;
|
|
1813
|
-
--bg-muted: #
|
|
1814
|
-
--bg-inset: #
|
|
1815
|
-
--line: #
|
|
1816
|
-
--line-strong: #
|
|
1817
|
-
--ink: #
|
|
1818
|
-
--muted: #
|
|
1819
|
-
--faint: #
|
|
1820
|
-
--accent: #
|
|
2035
|
+
--bg-muted: #f1f3f7;
|
|
2036
|
+
--bg-inset: #f8f9fc;
|
|
2037
|
+
--line: #e4e7ec;
|
|
2038
|
+
--line-strong: #cfd4dc;
|
|
2039
|
+
--ink: #101828;
|
|
2040
|
+
--muted: #475467;
|
|
2041
|
+
--faint: #667085;
|
|
2042
|
+
--accent: #6941c6;
|
|
2043
|
+
--accent-hover: #5933b4;
|
|
1821
2044
|
--accent-ink: #ffffff;
|
|
1822
|
-
--accent-soft: #
|
|
1823
|
-
--ok: #
|
|
1824
|
-
--ok-soft: #
|
|
1825
|
-
--warn: #
|
|
1826
|
-
--warn-soft: #
|
|
1827
|
-
--err: #
|
|
1828
|
-
--err-soft: #
|
|
1829
|
-
--
|
|
2045
|
+
--accent-soft: #f4f0ff;
|
|
2046
|
+
--ok: #067647;
|
|
2047
|
+
--ok-soft: #ecfdf3;
|
|
2048
|
+
--warn: #b54708;
|
|
2049
|
+
--warn-soft: #fffaeb;
|
|
2050
|
+
--err: #b42318;
|
|
2051
|
+
--err-soft: #fef3f2;
|
|
2052
|
+
--code-key: #6941c6;
|
|
2053
|
+
--code-string: #067647;
|
|
2054
|
+
--code-number: #175cd3;
|
|
2055
|
+
--code-boolean: #c11574;
|
|
2056
|
+
--code-null: #667085;
|
|
2057
|
+
--shadow-xs: 0 1px 2px rgb(16 24 40 / 0.05);
|
|
2058
|
+
--shadow-sm: 0 1px 3px rgb(16 24 40 / 0.08), 0 1px 2px rgb(16 24 40 / 0.04);
|
|
2059
|
+
--shadow-lg: 0 18px 48px rgb(16 24 40 / 0.16), 0 4px 12px rgb(16 24 40 / 0.08);
|
|
1830
2060
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
1831
2061
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
1832
|
-
--radius:
|
|
1833
|
-
--header:
|
|
1834
|
-
--nav:
|
|
2062
|
+
--radius: 12px;
|
|
2063
|
+
--header: 72px;
|
|
2064
|
+
--nav: 224px;
|
|
1835
2065
|
}
|
|
1836
2066
|
:root[data-theme="dark"] {
|
|
1837
2067
|
color-scheme: dark;
|
|
1838
|
-
--bg: #
|
|
1839
|
-
--bg-elev: #
|
|
1840
|
-
--bg-muted: #
|
|
1841
|
-
--bg-inset: #
|
|
1842
|
-
--line: #
|
|
1843
|
-
--line-strong: #
|
|
1844
|
-
--ink: #
|
|
1845
|
-
--muted: #
|
|
1846
|
-
--faint: #
|
|
1847
|
-
--accent: #
|
|
1848
|
-
--accent-
|
|
1849
|
-
--accent-
|
|
1850
|
-
--
|
|
1851
|
-
--ok
|
|
1852
|
-
--
|
|
1853
|
-
--warn
|
|
1854
|
-
--
|
|
1855
|
-
--err
|
|
1856
|
-
--
|
|
2068
|
+
--bg: #0c0e14;
|
|
2069
|
+
--bg-elev: #151821;
|
|
2070
|
+
--bg-muted: #20232e;
|
|
2071
|
+
--bg-inset: #11141c;
|
|
2072
|
+
--line: #2b303d;
|
|
2073
|
+
--line-strong: #414858;
|
|
2074
|
+
--ink: #f5f7fa;
|
|
2075
|
+
--muted: #b6bdc9;
|
|
2076
|
+
--faint: #8d96a7;
|
|
2077
|
+
--accent: #9e77ed;
|
|
2078
|
+
--accent-hover: #b692f6;
|
|
2079
|
+
--accent-ink: #ffffff;
|
|
2080
|
+
--accent-soft: #2b2142;
|
|
2081
|
+
--ok: #75e0a7;
|
|
2082
|
+
--ok-soft: #102a20;
|
|
2083
|
+
--warn: #fec84b;
|
|
2084
|
+
--warn-soft: #302611;
|
|
2085
|
+
--err: #fda29b;
|
|
2086
|
+
--err-soft: #321817;
|
|
2087
|
+
--code-key: #c3a6ff;
|
|
2088
|
+
--code-string: #75e0a7;
|
|
2089
|
+
--code-number: #84adff;
|
|
2090
|
+
--code-boolean: #f9a8d4;
|
|
2091
|
+
--code-null: #98a2b3;
|
|
2092
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
2093
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
2094
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
1857
2095
|
}
|
|
1858
2096
|
@media (prefers-color-scheme: dark) {
|
|
1859
2097
|
:root:not([data-theme="light"]) {
|
|
1860
2098
|
color-scheme: dark;
|
|
1861
|
-
--bg: #
|
|
1862
|
-
--bg-elev: #
|
|
1863
|
-
--bg-muted: #
|
|
1864
|
-
--bg-inset: #
|
|
1865
|
-
--line: #
|
|
1866
|
-
--line-strong: #
|
|
1867
|
-
--ink: #
|
|
1868
|
-
--muted: #
|
|
1869
|
-
--faint: #
|
|
1870
|
-
--accent: #
|
|
1871
|
-
--accent-
|
|
1872
|
-
--accent-
|
|
1873
|
-
--
|
|
1874
|
-
--ok
|
|
1875
|
-
--
|
|
1876
|
-
--warn
|
|
1877
|
-
--
|
|
1878
|
-
--err
|
|
1879
|
-
--
|
|
2099
|
+
--bg: #0c0e14;
|
|
2100
|
+
--bg-elev: #151821;
|
|
2101
|
+
--bg-muted: #20232e;
|
|
2102
|
+
--bg-inset: #11141c;
|
|
2103
|
+
--line: #2b303d;
|
|
2104
|
+
--line-strong: #414858;
|
|
2105
|
+
--ink: #f5f7fa;
|
|
2106
|
+
--muted: #b6bdc9;
|
|
2107
|
+
--faint: #8d96a7;
|
|
2108
|
+
--accent: #9e77ed;
|
|
2109
|
+
--accent-hover: #b692f6;
|
|
2110
|
+
--accent-ink: #ffffff;
|
|
2111
|
+
--accent-soft: #2b2142;
|
|
2112
|
+
--ok: #75e0a7;
|
|
2113
|
+
--ok-soft: #102a20;
|
|
2114
|
+
--warn: #fec84b;
|
|
2115
|
+
--warn-soft: #302611;
|
|
2116
|
+
--err: #fda29b;
|
|
2117
|
+
--err-soft: #321817;
|
|
2118
|
+
--code-key: #c3a6ff;
|
|
2119
|
+
--code-string: #75e0a7;
|
|
2120
|
+
--code-number: #84adff;
|
|
2121
|
+
--code-boolean: #f9a8d4;
|
|
2122
|
+
--code-null: #98a2b3;
|
|
2123
|
+
--shadow-xs: 0 1px 2px rgb(0 0 0 / 0.24);
|
|
2124
|
+
--shadow-sm: 0 4px 12px rgb(0 0 0 / 0.2);
|
|
2125
|
+
--shadow-lg: 0 20px 56px rgb(0 0 0 / 0.45);
|
|
1880
2126
|
}
|
|
1881
2127
|
}
|
|
1882
2128
|
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
@@ -1894,13 +2140,17 @@ var renderAdminDocument = (service) => {
|
|
|
1894
2140
|
padding-right: env(safe-area-inset-right);
|
|
1895
2141
|
-webkit-text-size-adjust: 100%;
|
|
1896
2142
|
}
|
|
1897
|
-
|
|
2143
|
+
::selection { background: color-mix(in srgb, var(--accent) 28%, transparent); }
|
|
2144
|
+
:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px; }
|
|
2145
|
+
button, input, select, textarea { transition: border-color 140ms ease, box-shadow 140ms ease, transform 100ms ease; }
|
|
2146
|
+
button:disabled, input:disabled, select:disabled, textarea:disabled { cursor: not-allowed; opacity: 0.55; }
|
|
1898
2147
|
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
1899
2148
|
header.top {
|
|
1900
|
-
position: sticky; top: 0; z-index:
|
|
2149
|
+
position: sticky; top: 0; z-index: 20;
|
|
1901
2150
|
display: flex; align-items: center; gap: 12px;
|
|
1902
|
-
min-height: var(--header); padding:
|
|
1903
|
-
background: var(--bg-elev);
|
|
2151
|
+
min-height: var(--header); padding: 12px 20px;
|
|
2152
|
+
background: color-mix(in srgb, var(--bg-elev) 92%, transparent);
|
|
2153
|
+
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
1904
2154
|
border-bottom: 1px solid var(--line);
|
|
1905
2155
|
min-width: 0; max-width: 100%;
|
|
1906
2156
|
}
|
|
@@ -1908,12 +2158,21 @@ var renderAdminDocument = (service) => {
|
|
|
1908
2158
|
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
1909
2159
|
min-width: 0; max-width: 100%; margin-right: auto;
|
|
1910
2160
|
}
|
|
1911
|
-
.brand-
|
|
1912
|
-
.brand-
|
|
2161
|
+
.brand-lockup { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
2162
|
+
.brand-mark {
|
|
2163
|
+
width: 38px; height: 38px; display: grid; place-items: center; flex: none;
|
|
2164
|
+
border-radius: 11px; color: #fff;
|
|
2165
|
+
background: linear-gradient(145deg, #7f56d9 0%, #53389e 100%);
|
|
2166
|
+
box-shadow: 0 6px 16px rgb(105 65 198 / 0.24), inset 0 1px 0 rgb(255 255 255 / 0.25);
|
|
2167
|
+
}
|
|
2168
|
+
.brand-mark svg { width: 22px; height: 22px; }
|
|
2169
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; line-height: 1.25; }
|
|
2170
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1913
2171
|
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
1914
2172
|
.vendor {
|
|
1915
2173
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
1916
2174
|
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
2175
|
+
padding: 5px 8px; border: 1px solid var(--line); border-radius: 999px; background: var(--bg-inset);
|
|
1917
2176
|
}
|
|
1918
2177
|
.vendor[hidden] { display: none; }
|
|
1919
2178
|
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
@@ -1925,12 +2184,17 @@ var renderAdminDocument = (service) => {
|
|
|
1925
2184
|
.vendor-sep { color: var(--faint); }
|
|
1926
2185
|
.vendor-link { color: inherit; text-decoration: none; }
|
|
1927
2186
|
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
1928
|
-
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items:
|
|
1929
|
-
|
|
2187
|
+
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-end; }
|
|
2188
|
+
.sync-status { align-self: center; display: flex; align-items: center; gap: 6px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
2189
|
+
.sync-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
|
|
2190
|
+
.sync-status.busy::before { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); animation: pulse 1.2s ease-in-out infinite; }
|
|
2191
|
+
label.field { display: flex; flex-direction: column; gap: 4px; font-size: 11px; font-weight: 600; color: var(--faint); }
|
|
1930
2192
|
label.field input, label.field select, .controls input[type="password"] {
|
|
1931
|
-
background-color: var(--bg-
|
|
1932
|
-
padding:
|
|
2193
|
+
background-color: var(--bg-elev); border: 1px solid var(--line-strong); border-radius: 9px;
|
|
2194
|
+
padding: 8px 10px; min-height: 38px; line-height: 1.2; box-shadow: var(--shadow-xs);
|
|
1933
2195
|
}
|
|
2196
|
+
label.field input:hover, label.field select:hover, textarea:hover { border-color: color-mix(in srgb, var(--accent) 38%, var(--line-strong)); }
|
|
2197
|
+
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; }
|
|
1934
2198
|
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
1935
2199
|
the text fields in the same form. Paint it like those fields. */
|
|
1936
2200
|
label.field select {
|
|
@@ -1947,12 +2211,20 @@ var renderAdminDocument = (service) => {
|
|
|
1947
2211
|
dialog .field > textarea { width: 100%; }
|
|
1948
2212
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
1949
2213
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
.btn
|
|
2214
|
+
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
|
2215
|
+
border-radius: 9px; min-height: 38px; padding: 0 13px; border: 1px solid var(--line-strong);
|
|
2216
|
+
background: var(--bg-elev); color: var(--ink); font-size: 13px; font-weight: 600; line-height: 1;
|
|
2217
|
+
box-shadow: var(--shadow-xs);
|
|
2218
|
+
}
|
|
2219
|
+
.icon-btn { width: 38px; padding: 0; }
|
|
2220
|
+
.icon-btn svg, .btn svg, .btn-primary svg { width: 16px; height: 16px; }
|
|
2221
|
+
.icon-btn:hover, .btn:hover { background: var(--bg-muted); border-color: var(--line-strong); }
|
|
2222
|
+
.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; }
|
|
2223
|
+
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
2224
|
+
.btn-quiet { background: transparent; border-color: transparent; box-shadow: none; }
|
|
2225
|
+
.btn-quiet:hover { background: var(--bg-muted); }
|
|
2226
|
+
.btn-danger { color: var(--err); border-color: color-mix(in srgb, var(--err) 25%, var(--line)); }
|
|
2227
|
+
.btn-danger:hover, .btn-danger.confirming { background: var(--err-soft); border-color: color-mix(in srgb, var(--err) 45%, var(--line)); }
|
|
1956
2228
|
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
1957
2229
|
so the press itself has to stay visible. */
|
|
1958
2230
|
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
@@ -1968,93 +2240,159 @@ var renderAdminDocument = (service) => {
|
|
|
1968
2240
|
flex: 1 0 auto; min-width: 0;
|
|
1969
2241
|
}
|
|
1970
2242
|
nav.side {
|
|
1971
|
-
|
|
2243
|
+
position: sticky; top: var(--header); align-self: start; height: calc(100vh - var(--header));
|
|
2244
|
+
border-right: 1px solid var(--line); padding: 18px 12px;
|
|
1972
2245
|
display: flex; flex-direction: column; gap: 4px;
|
|
1973
|
-
background: var(--bg-elev);
|
|
2246
|
+
background: var(--bg-elev); overflow-y: auto;
|
|
1974
2247
|
}
|
|
2248
|
+
.nav-label { padding: 0 10px 8px; color: var(--faint); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
|
|
1975
2249
|
nav.side button {
|
|
2250
|
+
position: relative; display: flex; align-items: center; gap: 10px; width: 100%;
|
|
1976
2251
|
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
1977
|
-
padding: 9px 10px; min-height:
|
|
1978
|
-
}
|
|
1979
|
-
nav.side button
|
|
1980
|
-
|
|
1981
|
-
.
|
|
1982
|
-
.
|
|
1983
|
-
.
|
|
2252
|
+
padding: 9px 10px; min-height: 42px; color: var(--muted); font-size: 13px; font-weight: 550;
|
|
2253
|
+
}
|
|
2254
|
+
nav.side button:hover { background: var(--bg-muted); color: var(--ink); }
|
|
2255
|
+
nav.side button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 650; }
|
|
2256
|
+
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); }
|
|
2257
|
+
.nav-icon { width: 18px; height: 18px; display: grid; place-items: center; flex: none; }
|
|
2258
|
+
.nav-icon svg { width: 18px; height: 18px; }
|
|
2259
|
+
main { padding: 32px clamp(20px, 4vw, 48px) 64px; max-width: 1320px; width: 100%; min-width: 0; }
|
|
2260
|
+
main[aria-busy="true"] .view { opacity: 0.62; }
|
|
2261
|
+
.view { animation: view-in 180ms ease-out; }
|
|
2262
|
+
.view[hidden] { display: none; }
|
|
2263
|
+
.view-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; margin-bottom: 22px; }
|
|
2264
|
+
.view h2 { margin: 0 0 5px; font-size: clamp(24px, 3vw, 30px); line-height: 1.2; letter-spacing: -0.035em; }
|
|
2265
|
+
.view h3 { margin: 24px 0 10px; font-size: 15px; letter-spacing: -0.01em; }
|
|
2266
|
+
.lede { margin: 0; color: var(--muted); max-width: 72ch; font-size: 14px; }
|
|
2267
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 175px), 1fr)); gap: 14px; }
|
|
1984
2268
|
.card {
|
|
1985
2269
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
1986
|
-
padding:
|
|
2270
|
+
padding: 18px; box-shadow: var(--shadow-sm);
|
|
1987
2271
|
}
|
|
1988
|
-
.card
|
|
1989
|
-
.card
|
|
2272
|
+
.stat-card { position: relative; overflow: hidden; }
|
|
2273
|
+
.stat-card::after { content: ""; position: absolute; inset: 0 0 auto; height: 2px; background: linear-gradient(90deg, var(--accent), transparent 78%); opacity: 0.7; }
|
|
2274
|
+
.card .k { color: var(--faint); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
|
|
2275
|
+
.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; }
|
|
1990
2276
|
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
1991
|
-
.
|
|
2277
|
+
.section-card { background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow-xs); overflow: hidden; }
|
|
2278
|
+
.section-title { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
|
|
2279
|
+
.section-title h3 { margin: 0; }
|
|
2280
|
+
.split { display: grid; grid-template-columns: minmax(0, 250px) minmax(0, 1fr); gap: 18px; align-items: start; }
|
|
2281
|
+
#collections { padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-elev); box-shadow: var(--shadow-xs); }
|
|
1992
2282
|
.collection {
|
|
1993
2283
|
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
1994
|
-
border-radius:
|
|
2284
|
+
border-radius: 9px; padding: 10px 11px; min-height: 48px; color: var(--ink); cursor: pointer;
|
|
1995
2285
|
}
|
|
1996
|
-
.collection
|
|
2286
|
+
.collection:hover { background: var(--bg-muted); }
|
|
2287
|
+
.collection[aria-current="true"] { background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 18%, var(--line)); color: var(--accent); }
|
|
1997
2288
|
.collection small { display: block; color: var(--faint); }
|
|
1998
2289
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
1999
|
-
.chip {
|
|
2000
|
-
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
2001
|
-
border-radius: 999px; padding: 4px 8px;
|
|
2002
|
-
}
|
|
2290
|
+
.chip { font-family: var(--mono); font-size: 11px; background: var(--bg-muted); border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; }
|
|
2003
2291
|
.chip i { color: var(--faint); font-style: normal; }
|
|
2004
2292
|
.sql-layout {
|
|
2005
2293
|
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
2006
2294
|
}
|
|
2007
|
-
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
2008
|
-
.sql-tables button { text-align: left; }
|
|
2295
|
+
.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); }
|
|
2296
|
+
.sql-tables button { text-align: left; border: 0; border-radius: 8px; padding: 9px 10px; background: transparent; color: var(--muted); cursor: pointer; }
|
|
2297
|
+
.sql-tables button:hover, .sql-tables button[aria-current="true"] { background: var(--accent-soft); color: var(--accent); }
|
|
2009
2298
|
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
2010
|
-
table { width: 100%; border-
|
|
2011
|
-
|
|
2012
|
-
th {
|
|
2299
|
+
.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); }
|
|
2300
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
2301
|
+
th, td { text-align: left; padding: 11px 13px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
2302
|
+
th { color: var(--faint); background: var(--bg-inset); font-weight: 650; font-size: 11px; text-transform: uppercase; letter-spacing: 0.045em; }
|
|
2303
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
2013
2304
|
tr.clickable { cursor: pointer; }
|
|
2014
|
-
tr.clickable:hover { background: var(--
|
|
2305
|
+
tr.clickable:hover { background: var(--accent-soft); }
|
|
2015
2306
|
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
2016
2307
|
pre {
|
|
2017
2308
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
2018
|
-
background: var(--bg-inset); border-radius: 10px; padding:
|
|
2019
|
-
}
|
|
2020
|
-
.
|
|
2309
|
+
background: var(--bg-inset); border: 1px solid var(--line); border-radius: 10px; padding: 14px; font-size: 12px; line-height: 1.65;
|
|
2310
|
+
}
|
|
2311
|
+
.json-key { color: var(--code-key); }
|
|
2312
|
+
.json-string { color: var(--code-string); }
|
|
2313
|
+
.json-number { color: var(--code-number); }
|
|
2314
|
+
.json-boolean { color: var(--code-boolean); }
|
|
2315
|
+
.json-null { color: var(--code-null); font-style: italic; }
|
|
2316
|
+
.json-inline { display: block; max-width: 44rem; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
2317
|
+
.json-view { position: relative; max-height: 26rem; overflow: auto; }
|
|
2318
|
+
.json-editor { position: relative; min-height: 190px; border: 1px solid var(--line-strong); border-radius: 10px; background: var(--bg-inset); overflow: hidden; }
|
|
2319
|
+
.json-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 15%, transparent); }
|
|
2320
|
+
.json-editor.invalid { border-color: var(--err); }
|
|
2321
|
+
.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; }
|
|
2322
|
+
.json-editor pre { position: absolute; inset: 0; pointer-events: none; background: transparent; }
|
|
2323
|
+
.json-editor textarea { position: relative; z-index: 1; resize: vertical; background: transparent; color: transparent; caret-color: var(--ink); -webkit-text-fill-color: transparent; }
|
|
2324
|
+
.json-editor textarea::selection { background: color-mix(in srgb, var(--accent) 32%, transparent); }
|
|
2325
|
+
.editor-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 7px; }
|
|
2326
|
+
.json-status { display: inline-flex; align-items: center; gap: 6px; color: var(--ok); font-size: 11px; }
|
|
2327
|
+
.json-status::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
2328
|
+
.json-status.invalid { color: var(--err); }
|
|
2329
|
+
.format-json { border: 0; padding: 3px 5px; background: transparent; color: var(--accent); font-size: 11px; font-weight: 650; cursor: pointer; }
|
|
2330
|
+
.row-actions { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 8px; margin: 14px 0; }
|
|
2021
2331
|
.banner {
|
|
2022
|
-
display: none;
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2332
|
+
display: none; position: sticky; top: calc(var(--header) + 12px); z-index: 15;
|
|
2333
|
+
align-items: center; gap: 10px; margin: 0 0 16px; padding: 11px 12px 11px 14px;
|
|
2334
|
+
border: 1px solid color-mix(in srgb, var(--err) 30%, var(--line)); border-radius: 10px;
|
|
2335
|
+
background: var(--err-soft); color: var(--err); box-shadow: var(--shadow-sm); font-size: 13px;
|
|
2336
|
+
}
|
|
2337
|
+
.banner.show { display: flex; }
|
|
2338
|
+
.banner span { flex: 1; }
|
|
2339
|
+
.banner button { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 7px; background: transparent; color: currentColor; cursor: pointer; }
|
|
2340
|
+
.banner button:hover { background: color-mix(in srgb, var(--err) 10%, transparent); }
|
|
2341
|
+
.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); }
|
|
2027
2342
|
dialog {
|
|
2028
|
-
border: 1px solid var(--line); border-radius:
|
|
2029
|
-
color: var(--ink); width: min(
|
|
2030
|
-
}
|
|
2031
|
-
dialog
|
|
2032
|
-
dialog
|
|
2343
|
+
border: 1px solid var(--line); border-radius: 16px; padding: 0; background: var(--bg-elev);
|
|
2344
|
+
color: var(--ink); width: min(720px, calc(100% - 24px)); max-width: calc(100vw - 24px); box-shadow: var(--shadow-lg);
|
|
2345
|
+
}
|
|
2346
|
+
dialog[open] { animation: dialog-in 160ms ease-out; }
|
|
2347
|
+
dialog::backdrop { background: rgb(12 14 20 / 0.58); backdrop-filter: blur(3px); }
|
|
2348
|
+
dialog form, .dialog-body { padding: 22px; display: flex; flex-direction: column; gap: 14px; }
|
|
2349
|
+
.dialog-heading { display: flex; justify-content: space-between; gap: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
|
2350
|
+
.dialog-heading strong { font-size: 18px; letter-spacing: -0.02em; }
|
|
2351
|
+
.dialog-heading span { color: var(--muted); font-size: 12px; }
|
|
2033
2352
|
textarea {
|
|
2034
2353
|
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
2035
2354
|
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
2036
2355
|
font-family: var(--mono); font-size: 12px;
|
|
2037
2356
|
}
|
|
2038
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
2039
|
-
.
|
|
2357
|
+
.dialog-actions { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding-top: 4px; }
|
|
2358
|
+
.dialog-actions .btn-danger { margin-right: auto; }
|
|
2359
|
+
.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); }
|
|
2040
2360
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
2041
2361
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
2042
2362
|
.status-2 { color: var(--ok); }
|
|
2043
2363
|
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
2044
2364
|
.stack { display: flex; flex-direction: column; gap: 12px; }
|
|
2365
|
+
.form-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
2366
|
+
.form-grid .wide { grid-column: span 2; }
|
|
2045
2367
|
.route {
|
|
2046
|
-
display: grid; grid-template-columns:
|
|
2047
|
-
padding:
|
|
2048
|
-
}
|
|
2049
|
-
.
|
|
2368
|
+
display: grid; grid-template-columns: 84px minmax(0, 1fr) auto; gap: 10px; align-items: center;
|
|
2369
|
+
padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--bg-elev);
|
|
2370
|
+
}
|
|
2371
|
+
.route:first-child { border-radius: var(--radius) var(--radius) 0 0; }
|
|
2372
|
+
.route:last-child { border-bottom: 0; border-radius: 0 0 var(--radius) var(--radius); }
|
|
2373
|
+
.route-tools { display: flex; align-items: flex-end; gap: 12px; margin-bottom: 12px; }
|
|
2374
|
+
.route-tools .field { flex: 1; }
|
|
2375
|
+
.route-tools input { width: 100%; }
|
|
2376
|
+
.route-count { padding-bottom: 10px; color: var(--faint); font-size: 12px; white-space: nowrap; }
|
|
2377
|
+
#route-list { max-height: 34rem; border: 1px solid var(--line); border-radius: var(--radius); overflow: auto; box-shadow: var(--shadow-xs); }
|
|
2378
|
+
.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; }
|
|
2379
|
+
@keyframes view-in { from { transform: translateY(3px); } to { transform: translateY(0); } }
|
|
2380
|
+
@keyframes dialog-in { from { transform: translateY(8px) scale(0.99); } to { transform: translateY(0) scale(1); } }
|
|
2381
|
+
@keyframes pulse { 50% { opacity: 0.45; } }
|
|
2382
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }
|
|
2383
|
+
@media (forced-colors: active) { .json-editor pre { display: none; } .json-editor textarea { color: CanvasText; -webkit-text-fill-color: CanvasText; } }
|
|
2050
2384
|
@media (max-width: 800px) {
|
|
2385
|
+
:root { --header: 0px; }
|
|
2051
2386
|
header.top {
|
|
2052
|
-
align-items: stretch; flex-direction: column;
|
|
2053
|
-
padding
|
|
2387
|
+
position: relative; align-items: stretch; flex-direction: column;
|
|
2388
|
+
padding: max(12px, env(safe-area-inset-top)) 14px 12px;
|
|
2054
2389
|
}
|
|
2055
2390
|
input, select, textarea { font-size: 16px; }
|
|
2056
2391
|
.brand, .vendor, .controls { width: 100%; }
|
|
2392
|
+
.brand { gap: 10px; }
|
|
2393
|
+
.vendor { order: 2; border-radius: 9px; }
|
|
2057
2394
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
2395
|
+
.sync-status { display: none; }
|
|
2058
2396
|
/* The layout fills the screen. A single-column grid stretches every auto
|
|
2059
2397
|
row, so the tab bar grew with the leftover height and the buttons floated
|
|
2060
2398
|
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
@@ -2064,14 +2402,23 @@ var renderAdminDocument = (service) => {
|
|
|
2064
2402
|
align-content: start;
|
|
2065
2403
|
}
|
|
2066
2404
|
nav.side {
|
|
2067
|
-
align-self: start; height: auto;
|
|
2405
|
+
position: sticky; top: 0; z-index: 12; align-self: start; height: auto;
|
|
2068
2406
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
2069
2407
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
2070
|
-
padding: 8px;
|
|
2408
|
+
padding: 8px; box-shadow: var(--shadow-xs);
|
|
2071
2409
|
}
|
|
2072
|
-
nav
|
|
2410
|
+
.nav-label { display: none; }
|
|
2411
|
+
nav.side button { width: auto; white-space: nowrap; flex: none; }
|
|
2412
|
+
nav.side button[aria-current="true"]::before { inset: auto 9px -8px; width: auto; height: 3px; border-radius: 3px 3px 0 0; }
|
|
2073
2413
|
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
2074
|
-
|
|
2414
|
+
.form-grid { grid-template-columns: minmax(0, 1fr); }
|
|
2415
|
+
.form-grid .wide { grid-column: auto; }
|
|
2416
|
+
main { padding: 22px 14px 48px; }
|
|
2417
|
+
.view-head { margin-bottom: 18px; }
|
|
2418
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
|
|
2419
|
+
.card { padding: 14px; }
|
|
2420
|
+
.card .v { font-size: 20px; }
|
|
2421
|
+
.table-shell { border: 0; background: transparent; box-shadow: none; overflow: visible; }
|
|
2075
2422
|
table, thead, tbody, tr, th, td { display: block; }
|
|
2076
2423
|
thead { display: none; }
|
|
2077
2424
|
tr {
|
|
@@ -2086,50 +2433,63 @@ var renderAdminDocument = (service) => {
|
|
|
2086
2433
|
.route { grid-template-columns: 72px 1fr; }
|
|
2087
2434
|
.route button { grid-column: 1 / -1; }
|
|
2088
2435
|
}
|
|
2436
|
+
@media (max-width: 460px) {
|
|
2437
|
+
.controls label.field { min-width: calc(50% - 4px); }
|
|
2438
|
+
.controls .icon-btn, .controls .btn { flex: 1; }
|
|
2439
|
+
.grid { grid-template-columns: minmax(0, 1fr); }
|
|
2440
|
+
.dialog-actions { flex-wrap: wrap; }
|
|
2441
|
+
.dialog-actions button { flex: 1; }
|
|
2442
|
+
.dialog-actions .btn-danger { flex-basis: 100%; margin-right: 0; }
|
|
2443
|
+
}
|
|
2089
2444
|
</style>
|
|
2090
2445
|
</head>
|
|
2091
2446
|
<body>
|
|
2092
2447
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
2093
2448
|
<header class="top">
|
|
2094
2449
|
<div class="brand">
|
|
2095
|
-
<div class="brand-
|
|
2096
|
-
<
|
|
2097
|
-
|
|
2450
|
+
<div class="brand-lockup">
|
|
2451
|
+
<div class="brand-mark" aria-hidden="true">
|
|
2452
|
+
<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>
|
|
2453
|
+
</div>
|
|
2454
|
+
<div class="brand-id">
|
|
2455
|
+
<strong>${name}</strong>
|
|
2456
|
+
<span>Mock admin</span>
|
|
2457
|
+
</div>
|
|
2098
2458
|
</div>
|
|
2099
2459
|
<div class="vendor" id="vendor" hidden></div>
|
|
2100
2460
|
</div>
|
|
2101
2461
|
<div class="controls">
|
|
2462
|
+
<span class="sync-status" id="sync-status" role="status" aria-live="polite">Ready</span>
|
|
2102
2463
|
<label class="field">Namespace
|
|
2103
2464
|
<select id="namespace" aria-label="Namespace"></select>
|
|
2104
2465
|
</label>
|
|
2105
2466
|
<label class="field">Admin key
|
|
2106
2467
|
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
2107
2468
|
</label>
|
|
2108
|
-
<button class="icon-btn" id="theme" type="button" aria-label="
|
|
2109
|
-
<button class="btn" id="refresh" type="button">Refresh</button>
|
|
2469
|
+
<button class="icon-btn" id="theme" type="button" aria-label="Use dark theme" title="Use dark theme"></button>
|
|
2470
|
+
<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>
|
|
2110
2471
|
</div>
|
|
2111
2472
|
</header>
|
|
2112
2473
|
<div class="layout">
|
|
2113
2474
|
<nav class="side" id="nav" aria-label="Admin sections">
|
|
2114
|
-
<
|
|
2115
|
-
<button type="button" data-view="
|
|
2116
|
-
<button type="button" data-view="
|
|
2117
|
-
<button type="button" data-view="
|
|
2118
|
-
<button type="button" data-view="
|
|
2119
|
-
<button type="button" data-view="
|
|
2475
|
+
<span class="nav-label">Workspace</span>
|
|
2476
|
+
<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>
|
|
2477
|
+
<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>
|
|
2478
|
+
<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>
|
|
2479
|
+
<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>
|
|
2480
|
+
<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>
|
|
2481
|
+
<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>
|
|
2120
2482
|
</nav>
|
|
2121
2483
|
<main id="views">
|
|
2122
|
-
<
|
|
2484
|
+
<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>
|
|
2123
2485
|
<section class="view" id="view-overview">
|
|
2124
|
-
<h2>Overview</h2>
|
|
2125
|
-
<p class="lede">Health, clock, and the collections this namespace is holding.</p>
|
|
2486
|
+
<div class="view-head"><div><h2>Overview</h2><p class="lede">Health, clock, and the collections this namespace is holding.</p></div></div>
|
|
2126
2487
|
<div class="grid" id="overview-cards"></div>
|
|
2127
2488
|
<h3>Recent requests</h3>
|
|
2128
2489
|
<div id="overview-requests"></div>
|
|
2129
2490
|
</section>
|
|
2130
2491
|
<section class="view" id="view-state" hidden>
|
|
2131
|
-
<h2>State</h2>
|
|
2132
|
-
<p class="lede">Every collection in this namespace. Declared fields stay visible before any row exists; stored rows add whatever else they contain.</p>
|
|
2492
|
+
<div class="view-head"><div><h2>State</h2><p class="lede">Inspect and edit every collection in this namespace.</p></div></div>
|
|
2133
2493
|
<div class="split">
|
|
2134
2494
|
<div id="collections"></div>
|
|
2135
2495
|
<div>
|
|
@@ -2142,8 +2502,7 @@ var renderAdminDocument = (service) => {
|
|
|
2142
2502
|
</div>
|
|
2143
2503
|
</section>
|
|
2144
2504
|
<section class="view" id="view-clock" hidden>
|
|
2145
|
-
<h2>Clock</h2>
|
|
2146
|
-
<p class="lede">The clock every timestamp in this mock reads.</p>
|
|
2505
|
+
<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>
|
|
2147
2506
|
<div class="card" id="clock-readout"></div>
|
|
2148
2507
|
<div class="row-actions">
|
|
2149
2508
|
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
@@ -2162,21 +2521,22 @@ var renderAdminDocument = (service) => {
|
|
|
2162
2521
|
</form>
|
|
2163
2522
|
</section>
|
|
2164
2523
|
<section class="view" id="view-faults" hidden>
|
|
2165
|
-
<h2>Faults</h2>
|
|
2166
|
-
<p class="lede">Inject a response, a delay, or a named preset. Rules apply to this namespace.</p>
|
|
2524
|
+
<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>
|
|
2167
2525
|
<form id="fault-form" class="card stack">
|
|
2168
|
-
<
|
|
2169
|
-
<
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
<
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
<
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
<
|
|
2179
|
-
|
|
2526
|
+
<div class="form-grid">
|
|
2527
|
+
<label class="field wide">Preset
|
|
2528
|
+
<select id="fault-preset"><option value="">Custom rule</option></select>
|
|
2529
|
+
</label>
|
|
2530
|
+
<label class="field">Status
|
|
2531
|
+
<input id="fault-status" inputmode="numeric" placeholder="503">
|
|
2532
|
+
</label>
|
|
2533
|
+
<label class="field wide">Path prefix
|
|
2534
|
+
<input id="fault-path" placeholder="/v1">
|
|
2535
|
+
</label>
|
|
2536
|
+
<label class="field">Method
|
|
2537
|
+
<input id="fault-method" placeholder="GET">
|
|
2538
|
+
</label>
|
|
2539
|
+
</div>
|
|
2180
2540
|
<div class="dialog-actions">
|
|
2181
2541
|
<button class="btn-primary" type="submit">Add fault</button>
|
|
2182
2542
|
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
@@ -2185,26 +2545,31 @@ var renderAdminDocument = (service) => {
|
|
|
2185
2545
|
<div id="fault-list"></div>
|
|
2186
2546
|
</section>
|
|
2187
2547
|
<section class="view" id="view-journal" hidden>
|
|
2188
|
-
<h2>Journal</h2>
|
|
2189
|
-
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
2190
|
-
<div class="row-actions">
|
|
2191
|
-
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
2192
|
-
</div>
|
|
2548
|
+
<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>
|
|
2193
2549
|
<div id="journal"></div>
|
|
2194
2550
|
</section>
|
|
2195
2551
|
<section class="view" id="view-routes" hidden>
|
|
2196
|
-
<h2>Routes</h2>
|
|
2197
|
-
<
|
|
2552
|
+
<div class="view-head"><div><h2>Routes</h2><p class="lede">Explore shared controls and service-specific admin endpoints.</p></div></div>
|
|
2553
|
+
<div class="route-tools">
|
|
2554
|
+
<label class="field">Filter routes
|
|
2555
|
+
<input id="route-search" type="search" placeholder="Method, path, or extension" autocomplete="off">
|
|
2556
|
+
</label>
|
|
2557
|
+
<span class="route-count" id="route-count"></span>
|
|
2558
|
+
</div>
|
|
2198
2559
|
<div id="route-list"></div>
|
|
2199
|
-
<form id="route-form" class="card stack">
|
|
2560
|
+
<form id="route-form" class="card stack" style="margin-top: 18px">
|
|
2200
2561
|
<strong id="route-title">Select a route</strong>
|
|
2201
2562
|
<label class="field">JSON body
|
|
2202
|
-
<
|
|
2563
|
+
<div class="json-editor" id="route-editor" hidden>
|
|
2564
|
+
<pre aria-hidden="true"><code id="route-highlight"></code></pre>
|
|
2565
|
+
<textarea id="route-body" placeholder="{ }" spellcheck="false" aria-describedby="route-json-status" disabled></textarea>
|
|
2566
|
+
</div>
|
|
2567
|
+
<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>
|
|
2203
2568
|
</label>
|
|
2204
2569
|
<div class="dialog-actions">
|
|
2205
|
-
<button class="btn-primary" type="submit">Send</button>
|
|
2570
|
+
<button class="btn-primary" id="route-send" type="submit" disabled>Send request</button>
|
|
2206
2571
|
</div>
|
|
2207
|
-
<pre id="route-result">Response will show here.</pre>
|
|
2572
|
+
<pre class="json-view" id="route-result"><code>Response will show here.</code></pre>
|
|
2208
2573
|
</form>
|
|
2209
2574
|
</section>
|
|
2210
2575
|
</main>
|
|
@@ -2212,12 +2577,16 @@ var renderAdminDocument = (service) => {
|
|
|
2212
2577
|
</div>
|
|
2213
2578
|
<dialog id="editor">
|
|
2214
2579
|
<form method="dialog" id="editor-form">
|
|
2215
|
-
<strong id="editor-title">Record</strong>
|
|
2580
|
+
<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>
|
|
2216
2581
|
<label class="field">Id
|
|
2217
2582
|
<input id="editor-id" required>
|
|
2218
2583
|
</label>
|
|
2219
2584
|
<label class="field">JSON value
|
|
2220
|
-
<
|
|
2585
|
+
<div class="json-editor" id="record-editor">
|
|
2586
|
+
<pre aria-hidden="true"><code id="editor-highlight"></code></pre>
|
|
2587
|
+
<textarea id="editor-value" spellcheck="false" aria-describedby="editor-json-status"></textarea>
|
|
2588
|
+
</div>
|
|
2589
|
+
<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>
|
|
2221
2590
|
</label>
|
|
2222
2591
|
<div class="dialog-actions">
|
|
2223
2592
|
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
@@ -2259,8 +2628,8 @@ var describeExtension = (extension) => {
|
|
|
2259
2628
|
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
2260
2629
|
};
|
|
2261
2630
|
};
|
|
2262
|
-
var adminUiRoutes = (service, ui) => {
|
|
2263
|
-
const shell = () => renderAdminDocument(service);
|
|
2631
|
+
var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
2632
|
+
const shell = () => renderAdminDocument(service, adminPrefix);
|
|
2264
2633
|
const document2 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
2265
2634
|
return {
|
|
2266
2635
|
"GET /ui": () => htmlResponse(document2()),
|
|
@@ -2383,7 +2752,34 @@ var faultResponse = (rule) => {
|
|
|
2383
2752
|
};
|
|
2384
2753
|
var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) => {
|
|
2385
2754
|
const entries = [];
|
|
2755
|
+
let isolated = false;
|
|
2756
|
+
const scopedRngs = /* @__PURE__ */ new Map();
|
|
2757
|
+
const random = (namespace) => {
|
|
2758
|
+
if (!isolated || namespace === "default") return rng;
|
|
2759
|
+
let scoped = scopedRngs.get(namespace);
|
|
2760
|
+
if (!scoped) {
|
|
2761
|
+
scoped = createRng(rng.seed);
|
|
2762
|
+
scopedRngs.set(namespace, scoped);
|
|
2763
|
+
}
|
|
2764
|
+
return scoped;
|
|
2765
|
+
};
|
|
2386
2766
|
return {
|
|
2767
|
+
isolateNamespaces() {
|
|
2768
|
+
isolated = true;
|
|
2769
|
+
},
|
|
2770
|
+
snapshot(namespace) {
|
|
2771
|
+
return {
|
|
2772
|
+
rules: structuredClone(
|
|
2773
|
+
entries.map((entry, position) => ({
|
|
2774
|
+
...entry.rule,
|
|
2775
|
+
remaining: entry.remaining,
|
|
2776
|
+
hits: entry.hits,
|
|
2777
|
+
position
|
|
2778
|
+
})).filter((entry) => entry.namespace === namespace)
|
|
2779
|
+
),
|
|
2780
|
+
rngState: random(namespace).state()
|
|
2781
|
+
};
|
|
2782
|
+
},
|
|
2387
2783
|
add(rule) {
|
|
2388
2784
|
const existing = entries.findIndex((e) => e.rule.id === rule.id);
|
|
2389
2785
|
const entry = { rule, remaining: rule.count ?? null, hits: 0 };
|
|
@@ -2400,6 +2796,15 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
|
|
|
2400
2796
|
},
|
|
2401
2797
|
clear() {
|
|
2402
2798
|
entries.length = 0;
|
|
2799
|
+
rng.reset();
|
|
2800
|
+
scopedRngs.clear();
|
|
2801
|
+
},
|
|
2802
|
+
restore(namespace, state) {
|
|
2803
|
+
for (let i = entries.length - 1; i >= 0; i--)
|
|
2804
|
+
if (entries[i]?.rule.namespace === namespace) entries.splice(i, 1);
|
|
2805
|
+
for (const { remaining, hits, position, ...rule } of state.rules)
|
|
2806
|
+
entries.splice(Math.min(position, entries.length), 0, { rule, remaining, hits });
|
|
2807
|
+
random(namespace).setState(state.rngState);
|
|
2403
2808
|
},
|
|
2404
2809
|
async take(candidate) {
|
|
2405
2810
|
const hits = [];
|
|
@@ -2407,7 +2812,7 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
|
|
|
2407
2812
|
if (entry.remaining === 0) continue;
|
|
2408
2813
|
if (!matches(entry.rule, candidate)) continue;
|
|
2409
2814
|
const rate = entry.rule.rate ?? 1;
|
|
2410
|
-
if (
|
|
2815
|
+
if (random(candidate.namespace).next() >= rate) continue;
|
|
2411
2816
|
entry.hits++;
|
|
2412
2817
|
if (entry.remaining !== null) entry.remaining--;
|
|
2413
2818
|
const delay = entry.rule.delayMs ?? entry.rule.latencyMs;
|
|
@@ -2467,7 +2872,8 @@ var createJournal = (size = DEFAULT_JOURNAL_SIZE) => {
|
|
|
2467
2872
|
};
|
|
2468
2873
|
|
|
2469
2874
|
// ../core/src/metrics.ts
|
|
2470
|
-
var createMetrics = () => {
|
|
2875
|
+
var createMetrics = (partition = true) => {
|
|
2876
|
+
const namespaces = /* @__PURE__ */ new Map();
|
|
2471
2877
|
let requests = 0;
|
|
2472
2878
|
let faults = 0;
|
|
2473
2879
|
let totalDurationMs = 0;
|
|
@@ -2475,6 +2881,14 @@ var createMetrics = () => {
|
|
|
2475
2881
|
const unmatched = /* @__PURE__ */ new Map();
|
|
2476
2882
|
return {
|
|
2477
2883
|
record(entry) {
|
|
2884
|
+
if (partition) {
|
|
2885
|
+
let scoped = namespaces.get(entry.namespace);
|
|
2886
|
+
if (!scoped) {
|
|
2887
|
+
scoped = createMetrics(false);
|
|
2888
|
+
namespaces.set(entry.namespace, scoped);
|
|
2889
|
+
}
|
|
2890
|
+
scoped.record(entry);
|
|
2891
|
+
}
|
|
2478
2892
|
requests++;
|
|
2479
2893
|
totalDurationMs += entry.durationMs;
|
|
2480
2894
|
if (entry.faultId !== void 0) faults++;
|
|
@@ -2485,17 +2899,24 @@ var createMetrics = () => {
|
|
|
2485
2899
|
unmatched.set(route, (unmatched.get(route) ?? 0) + 1);
|
|
2486
2900
|
}
|
|
2487
2901
|
},
|
|
2488
|
-
report: () => ({
|
|
2902
|
+
report: (namespace) => namespace !== void 0 ? (namespaces.get(namespace) ?? createMetrics(false)).report() : {
|
|
2489
2903
|
requests,
|
|
2490
|
-
byOperation: Object.fromEntries(
|
|
2904
|
+
byOperation: Object.fromEntries(
|
|
2905
|
+
[...byOperation].sort(([a], [b]) => a.localeCompare(b))
|
|
2906
|
+
),
|
|
2491
2907
|
unmatched: [...unmatched].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([route, count]) => {
|
|
2492
2908
|
const space = route.indexOf(" ");
|
|
2493
2909
|
return { method: route.slice(0, space), path: route.slice(space + 1), count };
|
|
2494
2910
|
}),
|
|
2495
2911
|
faults,
|
|
2496
2912
|
totalDurationMs
|
|
2497
|
-
}
|
|
2498
|
-
reset() {
|
|
2913
|
+
},
|
|
2914
|
+
reset(namespace) {
|
|
2915
|
+
if (namespace !== void 0) {
|
|
2916
|
+
namespaces.delete(namespace);
|
|
2917
|
+
return;
|
|
2918
|
+
}
|
|
2919
|
+
namespaces.clear();
|
|
2499
2920
|
requests = 0;
|
|
2500
2921
|
faults = 0;
|
|
2501
2922
|
totalDurationMs = 0;
|
|
@@ -2519,7 +2940,7 @@ var PAGE_DEFAULT = 50;
|
|
|
2519
2940
|
var PAGE_MAX = 200;
|
|
2520
2941
|
var json3 = (status, body) => new Response(JSON.stringify(body), {
|
|
2521
2942
|
status,
|
|
2522
|
-
headers: { "content-type": "application/json" }
|
|
2943
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
2523
2944
|
});
|
|
2524
2945
|
var adminError2 = (status, message) => json3(status, { error: { type: "mockingbird_admin", message } });
|
|
2525
2946
|
var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -2743,6 +3164,7 @@ var sqlRoutes = (engine) => ({
|
|
|
2743
3164
|
}
|
|
2744
3165
|
});
|
|
2745
3166
|
var createEngineAdmin = (options) => {
|
|
3167
|
+
const adminPrefix = resolveAdminPrefix(options.adminPrefix);
|
|
2746
3168
|
const engines = /* @__PURE__ */ new Map();
|
|
2747
3169
|
const engine = (namespace) => {
|
|
2748
3170
|
const existing = engines.get(namespace);
|
|
@@ -2756,7 +3178,7 @@ var createEngineAdmin = (options) => {
|
|
|
2756
3178
|
...credentialRoutes(createCredentialRegistry()),
|
|
2757
3179
|
...stateRoutes(engine),
|
|
2758
3180
|
...sqlRoutes(engine),
|
|
2759
|
-
...adminUiRoutes(options.name, { extensions: [SQL_ADMIN_EXTENSION] })
|
|
3181
|
+
...adminUiRoutes(options.name, { extensions: [SQL_ADMIN_EXTENSION] }, adminPrefix)
|
|
2760
3182
|
};
|
|
2761
3183
|
const startedAt = Date.now();
|
|
2762
3184
|
const plane = createControlPlane({
|
|
@@ -2786,6 +3208,7 @@ var createEngineAdmin = (options) => {
|
|
|
2786
3208
|
},
|
|
2787
3209
|
describe: () => options.dialect === void 0 ? {} : { dialect: options.dialect },
|
|
2788
3210
|
routes,
|
|
3211
|
+
adminPrefix,
|
|
2789
3212
|
adminKey: options.adminKey
|
|
2790
3213
|
});
|
|
2791
3214
|
return {
|
|
@@ -19167,6 +19590,7 @@ function createAdmin(options = {}) {
|
|
|
19167
19590
|
return createEngineAdmin({
|
|
19168
19591
|
name: "sqlite",
|
|
19169
19592
|
dialect: "sqlite",
|
|
19593
|
+
...options.adminPrefix !== void 0 ? { adminPrefix: options.adminPrefix } : {},
|
|
19170
19594
|
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
19171
19595
|
open(namespace) {
|
|
19172
19596
|
if (namespace === "default" && seeded) return adapt(seeded);
|