@crvouga/mockingbird-service-postgres 1.7.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 +19 -1
- package/DISCOVERY.md +48 -0
- package/dist/admin.js +565 -222
- package/dist/admin.js.map +3 -3
- package/dist/wire/cli.js +49 -24
- package/dist/wire/cli.js.map +3 -3
- package/dist/wire/fleet.d.ts +1 -0
- 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(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()
|
|
@@ -1717,7 +1902,14 @@ var createControlPlane = (context) => {
|
|
|
1717
1902
|
return json(200, { status: "ok" });
|
|
1718
1903
|
}
|
|
1719
1904
|
};
|
|
1720
|
-
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]) => {
|
|
1721
1913
|
const space = key.indexOf(" ");
|
|
1722
1914
|
return { method: key.slice(0, space), pattern: key.slice(space + 1), handler };
|
|
1723
1915
|
});
|
|
@@ -1725,21 +1917,13 @@ var createControlPlane = (context) => {
|
|
|
1725
1917
|
namespaceOf: headerNamespace,
|
|
1726
1918
|
async handle(request) {
|
|
1727
1919
|
const url = new URL(request.url);
|
|
1728
|
-
if (url.pathname ===
|
|
1729
|
-
return
|
|
1730
|
-
status: "ok",
|
|
1731
|
-
service: context.name,
|
|
1732
|
-
uptimeMs: context.wallNow() - context.startedAt,
|
|
1733
|
-
clock: context.clock.state(),
|
|
1734
|
-
namespaces: context.namespaces().length,
|
|
1735
|
-
adminUi: `${ADMIN_PREFIX}/ui`,
|
|
1736
|
-
...context.describe()
|
|
1737
|
-
});
|
|
1920
|
+
if (url.pathname === `${adminPrefix}/health` && request.method === "GET") {
|
|
1921
|
+
return health();
|
|
1738
1922
|
}
|
|
1739
|
-
if (url.pathname
|
|
1923
|
+
if (!isAdminPath(url.pathname, adminPrefix)) {
|
|
1740
1924
|
return void 0;
|
|
1741
1925
|
}
|
|
1742
|
-
const path = url.pathname.slice(
|
|
1926
|
+
const path = url.pathname.slice(adminPrefix.length) || "/";
|
|
1743
1927
|
const shell = request.method === "GET" && (path === "/ui" || path === "/ui/");
|
|
1744
1928
|
if (!shell && context.adminKey !== void 0 && request.headers.get(ADMIN_KEY_HEADER) !== context.adminKey) {
|
|
1745
1929
|
return adminError(401, `missing or wrong ${ADMIN_KEY_HEADER}`);
|
|
@@ -1747,7 +1931,12 @@ var createControlPlane = (context) => {
|
|
|
1747
1931
|
for (const route of routes) {
|
|
1748
1932
|
if (route.method !== request.method)
|
|
1749
1933
|
continue;
|
|
1750
|
-
|
|
1934
|
+
let params;
|
|
1935
|
+
try {
|
|
1936
|
+
params = matchRoute(route.pattern, path);
|
|
1937
|
+
} catch {
|
|
1938
|
+
return adminError(400, "invalid path encoding");
|
|
1939
|
+
}
|
|
1751
1940
|
if (!params)
|
|
1752
1941
|
continue;
|
|
1753
1942
|
let body;
|
|
@@ -1756,15 +1945,18 @@ var createControlPlane = (context) => {
|
|
|
1756
1945
|
} catch {
|
|
1757
1946
|
return adminError(400, "request body is not valid JSON");
|
|
1758
1947
|
}
|
|
1948
|
+
const namespace = adminNamespace(request, url);
|
|
1949
|
+
if (!/^[A-Za-z0-9_.-]{1,64}$/.test(namespace))
|
|
1950
|
+
return adminError(400, "invalid namespace");
|
|
1759
1951
|
return route.handler({
|
|
1760
1952
|
request,
|
|
1761
1953
|
url,
|
|
1762
1954
|
params,
|
|
1763
|
-
namespace
|
|
1955
|
+
namespace,
|
|
1764
1956
|
body
|
|
1765
1957
|
});
|
|
1766
1958
|
}
|
|
1767
|
-
return adminError(404, `no admin route ${request.method} ${path}; GET ${
|
|
1959
|
+
return adminError(404, `no admin route ${request.method} ${path}; GET ${adminPrefix} lists them`);
|
|
1768
1960
|
}
|
|
1769
1961
|
};
|
|
1770
1962
|
};
|
|
@@ -1772,6 +1964,7 @@ var createControlPlane = (context) => {
|
|
|
1772
1964
|
// ../core/dist/surface.js
|
|
1773
1965
|
var STANDARD_ADMIN_ROUTES = [
|
|
1774
1966
|
"GET /",
|
|
1967
|
+
"GET /health",
|
|
1775
1968
|
"POST /reset",
|
|
1776
1969
|
"GET /namespaces",
|
|
1777
1970
|
"GET /clock",
|
|
@@ -1821,11 +2014,11 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
1821
2014
|
}
|
|
1822
2015
|
});
|
|
1823
2016
|
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
1824
|
-
var renderAdminDocument = (service) => {
|
|
2017
|
+
var renderAdminDocument = (service, adminPrefix = ADMIN_PREFIX) => {
|
|
1825
2018
|
const name = escapeHtml(service);
|
|
1826
2019
|
const script = adminClientSource({
|
|
1827
2020
|
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
1828
|
-
adminPrefix
|
|
2021
|
+
adminPrefix,
|
|
1829
2022
|
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
1830
2023
|
brandsUrl: ADMIN_BRANDS_URL,
|
|
1831
2024
|
service
|
|
@@ -1842,75 +2035,99 @@ var renderAdminDocument = (service) => {
|
|
|
1842
2035
|
${CSS_RESET}
|
|
1843
2036
|
:root {
|
|
1844
2037
|
color-scheme: light;
|
|
1845
|
-
--bg: #
|
|
2038
|
+
--bg: #f7f8fb;
|
|
1846
2039
|
--bg-elev: #ffffff;
|
|
1847
|
-
--bg-muted: #
|
|
1848
|
-
--bg-inset: #
|
|
1849
|
-
--line: #
|
|
1850
|
-
--line-strong: #
|
|
1851
|
-
--ink: #
|
|
1852
|
-
--muted: #
|
|
1853
|
-
--faint: #
|
|
1854
|
-
--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;
|
|
1855
2049
|
--accent-ink: #ffffff;
|
|
1856
|
-
--accent-soft: #
|
|
1857
|
-
--ok: #
|
|
1858
|
-
--ok-soft: #
|
|
1859
|
-
--warn: #
|
|
1860
|
-
--warn-soft: #
|
|
1861
|
-
--err: #
|
|
1862
|
-
--err-soft: #
|
|
1863
|
-
--
|
|
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);
|
|
1864
2065
|
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
1865
2066
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
1866
|
-
--radius:
|
|
1867
|
-
--header:
|
|
1868
|
-
--nav:
|
|
2067
|
+
--radius: 12px;
|
|
2068
|
+
--header: 72px;
|
|
2069
|
+
--nav: 224px;
|
|
1869
2070
|
}
|
|
1870
2071
|
:root[data-theme="dark"] {
|
|
1871
2072
|
color-scheme: dark;
|
|
1872
|
-
--bg: #
|
|
1873
|
-
--bg-elev: #
|
|
1874
|
-
--bg-muted: #
|
|
1875
|
-
--bg-inset: #
|
|
1876
|
-
--line: #
|
|
1877
|
-
--line-strong: #
|
|
1878
|
-
--ink: #
|
|
1879
|
-
--muted: #
|
|
1880
|
-
--faint: #
|
|
1881
|
-
--accent: #
|
|
1882
|
-
--accent-
|
|
1883
|
-
--accent-
|
|
1884
|
-
--
|
|
1885
|
-
--ok
|
|
1886
|
-
--
|
|
1887
|
-
--warn
|
|
1888
|
-
--
|
|
1889
|
-
--err
|
|
1890
|
-
--
|
|
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);
|
|
1891
2100
|
}
|
|
1892
2101
|
@media (prefers-color-scheme: dark) {
|
|
1893
2102
|
:root:not([data-theme="light"]) {
|
|
1894
2103
|
color-scheme: dark;
|
|
1895
|
-
--bg: #
|
|
1896
|
-
--bg-elev: #
|
|
1897
|
-
--bg-muted: #
|
|
1898
|
-
--bg-inset: #
|
|
1899
|
-
--line: #
|
|
1900
|
-
--line-strong: #
|
|
1901
|
-
--ink: #
|
|
1902
|
-
--muted: #
|
|
1903
|
-
--faint: #
|
|
1904
|
-
--accent: #
|
|
1905
|
-
--accent-
|
|
1906
|
-
--accent-
|
|
1907
|
-
--
|
|
1908
|
-
--ok
|
|
1909
|
-
--
|
|
1910
|
-
--warn
|
|
1911
|
-
--
|
|
1912
|
-
--err
|
|
1913
|
-
--
|
|
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);
|
|
1914
2131
|
}
|
|
1915
2132
|
}
|
|
1916
2133
|
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
@@ -1928,13 +2145,17 @@ var renderAdminDocument = (service) => {
|
|
|
1928
2145
|
padding-right: env(safe-area-inset-right);
|
|
1929
2146
|
-webkit-text-size-adjust: 100%;
|
|
1930
2147
|
}
|
|
1931
|
-
|
|
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; }
|
|
1932
2152
|
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
1933
2153
|
header.top {
|
|
1934
|
-
position: sticky; top: 0; z-index:
|
|
2154
|
+
position: sticky; top: 0; z-index: 20;
|
|
1935
2155
|
display: flex; align-items: center; gap: 12px;
|
|
1936
|
-
min-height: var(--header); padding:
|
|
1937
|
-
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);
|
|
1938
2159
|
border-bottom: 1px solid var(--line);
|
|
1939
2160
|
min-width: 0; max-width: 100%;
|
|
1940
2161
|
}
|
|
@@ -1942,12 +2163,21 @@ var renderAdminDocument = (service) => {
|
|
|
1942
2163
|
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
1943
2164
|
min-width: 0; max-width: 100%; margin-right: auto;
|
|
1944
2165
|
}
|
|
1945
|
-
.brand-
|
|
1946
|
-
.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; }
|
|
1947
2176
|
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
1948
2177
|
.vendor {
|
|
1949
2178
|
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
1950
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);
|
|
1951
2181
|
}
|
|
1952
2182
|
.vendor[hidden] { display: none; }
|
|
1953
2183
|
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
@@ -1959,12 +2189,17 @@ var renderAdminDocument = (service) => {
|
|
|
1959
2189
|
.vendor-sep { color: var(--faint); }
|
|
1960
2190
|
.vendor-link { color: inherit; text-decoration: none; }
|
|
1961
2191
|
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
1962
|
-
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items:
|
|
1963
|
-
|
|
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); }
|
|
1964
2197
|
label.field input, label.field select, .controls input[type="password"] {
|
|
1965
|
-
background-color: var(--bg-
|
|
1966
|
-
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);
|
|
1967
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; }
|
|
1968
2203
|
/* A native menu ignores min-height, so Preset rendered about half as tall as
|
|
1969
2204
|
the text fields in the same form. Paint it like those fields. */
|
|
1970
2205
|
label.field select {
|
|
@@ -1981,12 +2216,20 @@ var renderAdminDocument = (service) => {
|
|
|
1981
2216
|
dialog .field > textarea { width: 100%; }
|
|
1982
2217
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
|
|
1983
2218
|
.icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
.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)); }
|
|
1990
2233
|
/* Held after mouseup. Clearing an empty journal changes nothing on screen,
|
|
1991
2234
|
so the press itself has to stay visible. */
|
|
1992
2235
|
.icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
|
|
@@ -2002,93 +2245,159 @@ var renderAdminDocument = (service) => {
|
|
|
2002
2245
|
flex: 1 0 auto; min-width: 0;
|
|
2003
2246
|
}
|
|
2004
2247
|
nav.side {
|
|
2005
|
-
|
|
2248
|
+
position: sticky; top: var(--header); align-self: start; height: calc(100vh - var(--header));
|
|
2249
|
+
border-right: 1px solid var(--line); padding: 18px 12px;
|
|
2006
2250
|
display: flex; flex-direction: column; gap: 4px;
|
|
2007
|
-
background: var(--bg-elev);
|
|
2251
|
+
background: var(--bg-elev); overflow-y: auto;
|
|
2008
2252
|
}
|
|
2253
|
+
.nav-label { padding: 0 10px 8px; color: var(--faint); font-size: 10px; font-weight: 700; letter-spacing: 0.09em; text-transform: uppercase; }
|
|
2009
2254
|
nav.side button {
|
|
2255
|
+
position: relative; display: flex; align-items: center; gap: 10px; width: 100%;
|
|
2010
2256
|
text-align: left; border: 0; background: transparent; border-radius: 9px;
|
|
2011
|
-
padding: 9px 10px; min-height:
|
|
2012
|
-
}
|
|
2013
|
-
nav.side button
|
|
2014
|
-
|
|
2015
|
-
.
|
|
2016
|
-
.
|
|
2017
|
-
.
|
|
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; }
|
|
2018
2273
|
.card {
|
|
2019
2274
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
2020
|
-
padding:
|
|
2275
|
+
padding: 18px; box-shadow: var(--shadow-sm);
|
|
2021
2276
|
}
|
|
2022
|
-
.card
|
|
2023
|
-
.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; }
|
|
2024
2281
|
.card .v .clock-time { font-size: 14px; color: var(--muted); }
|
|
2025
|
-
.
|
|
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); }
|
|
2026
2287
|
.collection {
|
|
2027
2288
|
width: 100%; text-align: left; border: 1px solid transparent; background: transparent;
|
|
2028
|
-
border-radius:
|
|
2289
|
+
border-radius: 9px; padding: 10px 11px; min-height: 48px; color: var(--ink); cursor: pointer;
|
|
2029
2290
|
}
|
|
2030
|
-
.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); }
|
|
2031
2293
|
.collection small { display: block; color: var(--faint); }
|
|
2032
2294
|
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 14px; }
|
|
2033
|
-
.chip {
|
|
2034
|
-
font-family: var(--mono); font-size: 12px; background: var(--bg-muted);
|
|
2035
|
-
border-radius: 999px; padding: 4px 8px;
|
|
2036
|
-
}
|
|
2295
|
+
.chip { font-family: var(--mono); font-size: 11px; background: var(--bg-muted); border: 1px solid var(--line); border-radius: 999px; padding: 4px 8px; }
|
|
2037
2296
|
.chip i { color: var(--faint); font-style: normal; }
|
|
2038
2297
|
.sql-layout {
|
|
2039
2298
|
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
2040
2299
|
}
|
|
2041
|
-
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
2042
|
-
.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); }
|
|
2043
2303
|
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
2044
|
-
table { width: 100%; border-
|
|
2045
|
-
|
|
2046
|
-
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; }
|
|
2047
2309
|
tr.clickable { cursor: pointer; }
|
|
2048
|
-
tr.clickable:hover { background: var(--
|
|
2310
|
+
tr.clickable:hover { background: var(--accent-soft); }
|
|
2049
2311
|
pre, code, .mono { font-family: var(--mono); overflow-wrap: anywhere; }
|
|
2050
2312
|
pre {
|
|
2051
2313
|
margin: 0; white-space: pre-wrap; word-break: break-word;
|
|
2052
|
-
background: var(--bg-inset); border-radius: 10px; padding:
|
|
2053
|
-
}
|
|
2054
|
-
.
|
|
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; }
|
|
2055
2336
|
.banner {
|
|
2056
|
-
display: none;
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
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); }
|
|
2061
2347
|
dialog {
|
|
2062
|
-
border: 1px solid var(--line); border-radius:
|
|
2063
|
-
color: var(--ink); width: min(
|
|
2064
|
-
}
|
|
2065
|
-
dialog
|
|
2066
|
-
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; }
|
|
2067
2357
|
textarea {
|
|
2068
2358
|
width: 100%; min-height: 180px; resize: vertical; border-radius: 10px;
|
|
2069
2359
|
border: 1px solid var(--line); background: var(--bg-inset); padding: 10px;
|
|
2070
2360
|
font-family: var(--mono); font-size: 12px;
|
|
2071
2361
|
}
|
|
2072
|
-
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
2073
|
-
.
|
|
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); }
|
|
2074
2365
|
.badge.ok { background: var(--ok-soft); color: var(--ok); }
|
|
2075
2366
|
.badge.warn { background: var(--warn-soft); color: var(--warn); }
|
|
2076
2367
|
.status-2 { color: var(--ok); }
|
|
2077
2368
|
.status-4, .status-5, .status-0 { color: var(--err); }
|
|
2078
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; }
|
|
2079
2372
|
.route {
|
|
2080
|
-
display: grid; grid-template-columns:
|
|
2081
|
-
padding:
|
|
2082
|
-
}
|
|
2083
|
-
.
|
|
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; } }
|
|
2084
2389
|
@media (max-width: 800px) {
|
|
2390
|
+
:root { --header: 0px; }
|
|
2085
2391
|
header.top {
|
|
2086
|
-
align-items: stretch; flex-direction: column;
|
|
2087
|
-
padding
|
|
2392
|
+
position: relative; align-items: stretch; flex-direction: column;
|
|
2393
|
+
padding: max(12px, env(safe-area-inset-top)) 14px 12px;
|
|
2088
2394
|
}
|
|
2089
2395
|
input, select, textarea { font-size: 16px; }
|
|
2090
2396
|
.brand, .vendor, .controls { width: 100%; }
|
|
2397
|
+
.brand { gap: 10px; }
|
|
2398
|
+
.vendor { order: 2; border-radius: 9px; }
|
|
2091
2399
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
2400
|
+
.sync-status { display: none; }
|
|
2092
2401
|
/* The layout fills the screen. A single-column grid stretches every auto
|
|
2093
2402
|
row, so the tab bar grew with the leftover height and the buttons floated
|
|
2094
2403
|
in the middle of it. The bar stays one row; the page below takes the rest. */
|
|
@@ -2098,14 +2407,23 @@ var renderAdminDocument = (service) => {
|
|
|
2098
2407
|
align-content: start;
|
|
2099
2408
|
}
|
|
2100
2409
|
nav.side {
|
|
2101
|
-
align-self: start; height: auto;
|
|
2410
|
+
position: sticky; top: 0; z-index: 12; align-self: start; height: auto;
|
|
2102
2411
|
flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
|
|
2103
2412
|
border-right: 0; border-bottom: 1px solid var(--line);
|
|
2104
|
-
padding: 8px;
|
|
2413
|
+
padding: 8px; box-shadow: var(--shadow-xs);
|
|
2105
2414
|
}
|
|
2106
|
-
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; }
|
|
2107
2418
|
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
2108
|
-
|
|
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; }
|
|
2109
2427
|
table, thead, tbody, tr, th, td { display: block; }
|
|
2110
2428
|
thead { display: none; }
|
|
2111
2429
|
tr {
|
|
@@ -2120,50 +2438,63 @@ var renderAdminDocument = (service) => {
|
|
|
2120
2438
|
.route { grid-template-columns: 72px 1fr; }
|
|
2121
2439
|
.route button { grid-column: 1 / -1; }
|
|
2122
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
|
+
}
|
|
2123
2449
|
</style>
|
|
2124
2450
|
</head>
|
|
2125
2451
|
<body>
|
|
2126
2452
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
2127
2453
|
<header class="top">
|
|
2128
2454
|
<div class="brand">
|
|
2129
|
-
<div class="brand-
|
|
2130
|
-
<
|
|
2131
|
-
|
|
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>
|
|
2132
2463
|
</div>
|
|
2133
2464
|
<div class="vendor" id="vendor" hidden></div>
|
|
2134
2465
|
</div>
|
|
2135
2466
|
<div class="controls">
|
|
2467
|
+
<span class="sync-status" id="sync-status" role="status" aria-live="polite">Ready</span>
|
|
2136
2468
|
<label class="field">Namespace
|
|
2137
2469
|
<select id="namespace" aria-label="Namespace"></select>
|
|
2138
2470
|
</label>
|
|
2139
2471
|
<label class="field">Admin key
|
|
2140
2472
|
<input id="admin-key" type="password" autocomplete="off" spellcheck="false" aria-label="Admin key">
|
|
2141
2473
|
</label>
|
|
2142
|
-
<button class="icon-btn" id="theme" type="button" aria-label="
|
|
2143
|
-
<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>
|
|
2144
2476
|
</div>
|
|
2145
2477
|
</header>
|
|
2146
2478
|
<div class="layout">
|
|
2147
2479
|
<nav class="side" id="nav" aria-label="Admin sections">
|
|
2148
|
-
<
|
|
2149
|
-
<button type="button" data-view="
|
|
2150
|
-
<button type="button" data-view="
|
|
2151
|
-
<button type="button" data-view="
|
|
2152
|
-
<button type="button" data-view="
|
|
2153
|
-
<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>
|
|
2154
2487
|
</nav>
|
|
2155
2488
|
<main id="views">
|
|
2156
|
-
<
|
|
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>
|
|
2157
2490
|
<section class="view" id="view-overview">
|
|
2158
|
-
<h2>Overview</h2>
|
|
2159
|
-
<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>
|
|
2160
2492
|
<div class="grid" id="overview-cards"></div>
|
|
2161
2493
|
<h3>Recent requests</h3>
|
|
2162
2494
|
<div id="overview-requests"></div>
|
|
2163
2495
|
</section>
|
|
2164
2496
|
<section class="view" id="view-state" hidden>
|
|
2165
|
-
<h2>State</h2>
|
|
2166
|
-
<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>
|
|
2167
2498
|
<div class="split">
|
|
2168
2499
|
<div id="collections"></div>
|
|
2169
2500
|
<div>
|
|
@@ -2176,8 +2507,7 @@ var renderAdminDocument = (service) => {
|
|
|
2176
2507
|
</div>
|
|
2177
2508
|
</section>
|
|
2178
2509
|
<section class="view" id="view-clock" hidden>
|
|
2179
|
-
<h2>Clock</h2>
|
|
2180
|
-
<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>
|
|
2181
2511
|
<div class="card" id="clock-readout"></div>
|
|
2182
2512
|
<div class="row-actions">
|
|
2183
2513
|
<button class="btn" type="button" data-advance="60000">+1m</button>
|
|
@@ -2196,21 +2526,22 @@ var renderAdminDocument = (service) => {
|
|
|
2196
2526
|
</form>
|
|
2197
2527
|
</section>
|
|
2198
2528
|
<section class="view" id="view-faults" hidden>
|
|
2199
|
-
<h2>Faults</h2>
|
|
2200
|
-
<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>
|
|
2201
2530
|
<form id="fault-form" class="card stack">
|
|
2202
|
-
<
|
|
2203
|
-
<
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
<
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
<
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
<
|
|
2213
|
-
|
|
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>
|
|
2214
2545
|
<div class="dialog-actions">
|
|
2215
2546
|
<button class="btn-primary" type="submit">Add fault</button>
|
|
2216
2547
|
<button class="btn-danger" id="fault-clear" type="button">Clear all</button>
|
|
@@ -2219,26 +2550,31 @@ var renderAdminDocument = (service) => {
|
|
|
2219
2550
|
<div id="fault-list"></div>
|
|
2220
2551
|
</section>
|
|
2221
2552
|
<section class="view" id="view-journal" hidden>
|
|
2222
|
-
<h2>Journal</h2>
|
|
2223
|
-
<p class="lede">Requests this namespace has seen. Bodies are not stored.</p>
|
|
2224
|
-
<div class="row-actions">
|
|
2225
|
-
<button class="btn-danger" id="journal-clear" type="button">Clear journal</button>
|
|
2226
|
-
</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>
|
|
2227
2554
|
<div id="journal"></div>
|
|
2228
2555
|
</section>
|
|
2229
2556
|
<section class="view" id="view-routes" hidden>
|
|
2230
|
-
<h2>Routes</h2>
|
|
2231
|
-
<
|
|
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>
|
|
2232
2564
|
<div id="route-list"></div>
|
|
2233
|
-
<form id="route-form" class="card stack">
|
|
2565
|
+
<form id="route-form" class="card stack" style="margin-top: 18px">
|
|
2234
2566
|
<strong id="route-title">Select a route</strong>
|
|
2235
2567
|
<label class="field">JSON body
|
|
2236
|
-
<
|
|
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>
|
|
2237
2573
|
</label>
|
|
2238
2574
|
<div class="dialog-actions">
|
|
2239
|
-
<button class="btn-primary" type="submit">Send</button>
|
|
2575
|
+
<button class="btn-primary" id="route-send" type="submit" disabled>Send request</button>
|
|
2240
2576
|
</div>
|
|
2241
|
-
<pre id="route-result">Response will show here.</pre>
|
|
2577
|
+
<pre class="json-view" id="route-result"><code>Response will show here.</code></pre>
|
|
2242
2578
|
</form>
|
|
2243
2579
|
</section>
|
|
2244
2580
|
</main>
|
|
@@ -2246,12 +2582,16 @@ var renderAdminDocument = (service) => {
|
|
|
2246
2582
|
</div>
|
|
2247
2583
|
<dialog id="editor">
|
|
2248
2584
|
<form method="dialog" id="editor-form">
|
|
2249
|
-
<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>
|
|
2250
2586
|
<label class="field">Id
|
|
2251
2587
|
<input id="editor-id" required>
|
|
2252
2588
|
</label>
|
|
2253
2589
|
<label class="field">JSON value
|
|
2254
|
-
<
|
|
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>
|
|
2255
2595
|
</label>
|
|
2256
2596
|
<div class="dialog-actions">
|
|
2257
2597
|
<button class="btn-danger" id="editor-delete" type="button" value="delete">Delete</button>
|
|
@@ -2293,8 +2633,8 @@ var describeExtension = (extension) => {
|
|
|
2293
2633
|
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
2294
2634
|
};
|
|
2295
2635
|
};
|
|
2296
|
-
var adminUiRoutes = (service, ui) => {
|
|
2297
|
-
const shell = () => renderAdminDocument(service);
|
|
2636
|
+
var adminUiRoutes = (service, ui, adminPrefix = ADMIN_PREFIX) => {
|
|
2637
|
+
const shell = () => renderAdminDocument(service, adminPrefix);
|
|
2298
2638
|
const document2 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
2299
2639
|
return {
|
|
2300
2640
|
"GET /ui": () => htmlResponse(document2()),
|
|
@@ -2625,7 +2965,7 @@ var PAGE_DEFAULT = 50;
|
|
|
2625
2965
|
var PAGE_MAX = 200;
|
|
2626
2966
|
var json3 = (status, body) => new Response(JSON.stringify(body), {
|
|
2627
2967
|
status,
|
|
2628
|
-
headers: { "content-type": "application/json" }
|
|
2968
|
+
headers: { "content-type": "application/json; charset=utf-8" }
|
|
2629
2969
|
});
|
|
2630
2970
|
var adminError2 = (status, message) => json3(status, { error: { type: "mockingbird_admin", message } });
|
|
2631
2971
|
var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -2878,6 +3218,7 @@ var sqlRoutes = (engine) => ({
|
|
|
2878
3218
|
}
|
|
2879
3219
|
});
|
|
2880
3220
|
var createEngineAdmin = (options) => {
|
|
3221
|
+
const adminPrefix = resolveAdminPrefix(options.adminPrefix);
|
|
2881
3222
|
const engines = /* @__PURE__ */ new Map();
|
|
2882
3223
|
const engine = (namespace) => {
|
|
2883
3224
|
const existing = engines.get(namespace);
|
|
@@ -2892,7 +3233,7 @@ var createEngineAdmin = (options) => {
|
|
|
2892
3233
|
...credentialRoutes(createCredentialRegistry()),
|
|
2893
3234
|
...stateRoutes(engine),
|
|
2894
3235
|
...sqlRoutes(engine),
|
|
2895
|
-
...adminUiRoutes(options.name, { extensions: [SQL_ADMIN_EXTENSION] })
|
|
3236
|
+
...adminUiRoutes(options.name, { extensions: [SQL_ADMIN_EXTENSION] }, adminPrefix)
|
|
2896
3237
|
};
|
|
2897
3238
|
const startedAt = Date.now();
|
|
2898
3239
|
const plane = createControlPlane({
|
|
@@ -2923,6 +3264,7 @@ var createEngineAdmin = (options) => {
|
|
|
2923
3264
|
},
|
|
2924
3265
|
describe: () => options.dialect === void 0 ? {} : { dialect: options.dialect },
|
|
2925
3266
|
routes,
|
|
3267
|
+
adminPrefix,
|
|
2926
3268
|
adminKey: options.adminKey
|
|
2927
3269
|
});
|
|
2928
3270
|
return {
|
|
@@ -26762,6 +27104,7 @@ function createAdmin(options = {}) {
|
|
|
26762
27104
|
return createEngineAdmin({
|
|
26763
27105
|
name: "postgres",
|
|
26764
27106
|
dialect: "postgres",
|
|
27107
|
+
...options.adminPrefix !== void 0 ? { adminPrefix: options.adminPrefix } : {},
|
|
26765
27108
|
...options.adminKey !== void 0 ? { adminKey: options.adminKey } : {},
|
|
26766
27109
|
open(namespace) {
|
|
26767
27110
|
if (namespace === "default" && seeded) return adapt(seeded);
|