@crvouga/mockingbird-service-livekit 1.4.0 → 1.5.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 +10 -0
- package/README.md +1 -1
- package/dist/{chunk-EZZFTKWG.js → chunk-PGZZE4OX.js} +1397 -593
- package/dist/chunk-PGZZE4OX.js.map +7 -0
- package/dist/{chunk-JCY5ZBIA.js → chunk-S5CKX37O.js} +2 -2
- package/dist/cli.js +2 -2
- package/dist/index.js +1 -1
- package/dist/server.js +2 -2
- package/package.json +3 -3
- package/dist/chunk-EZZFTKWG.js.map +0 -7
- /package/dist/{chunk-JCY5ZBIA.js.map → chunk-S5CKX37O.js.map} +0 -0
|
@@ -533,10 +533,10 @@ var OpenAPIReferenceError = class extends Error {
|
|
|
533
533
|
};
|
|
534
534
|
var unescapePointer = (segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
535
535
|
var isReference = (value) => typeof value === "object" && value !== null && typeof value.$ref === "string";
|
|
536
|
-
var resolveRef = (
|
|
536
|
+
var resolveRef = (document3, ref) => {
|
|
537
537
|
if (!ref.startsWith("#/"))
|
|
538
538
|
throw new OpenAPIReferenceError(ref);
|
|
539
|
-
let cursor =
|
|
539
|
+
let cursor = document3;
|
|
540
540
|
for (const raw of ref.slice(2).split("/")) {
|
|
541
541
|
const segment = unescapePointer(raw);
|
|
542
542
|
if (typeof cursor !== "object" || cursor === null || !(segment in cursor)) {
|
|
@@ -548,14 +548,14 @@ var resolveRef = (document2, ref) => {
|
|
|
548
548
|
throw new OpenAPIReferenceError(ref);
|
|
549
549
|
return cursor;
|
|
550
550
|
};
|
|
551
|
-
var deref = (
|
|
551
|
+
var deref = (document3, value) => {
|
|
552
552
|
let current = value;
|
|
553
553
|
const seen = /* @__PURE__ */ new Set();
|
|
554
554
|
while (isReference(current)) {
|
|
555
555
|
if (seen.has(current.$ref))
|
|
556
556
|
throw new OpenAPIReferenceError(`${current.$ref} (cycle)`);
|
|
557
557
|
seen.add(current.$ref);
|
|
558
|
-
current = resolveRef(
|
|
558
|
+
current = resolveRef(document3, current.$ref);
|
|
559
559
|
}
|
|
560
560
|
return current;
|
|
561
561
|
};
|
|
@@ -573,36 +573,36 @@ var HTTP_METHODS = [
|
|
|
573
573
|
];
|
|
574
574
|
|
|
575
575
|
// ../../openapi/core/dist/document.js
|
|
576
|
-
var mergeParameters = (
|
|
576
|
+
var mergeParameters = (document3, item, own) => {
|
|
577
577
|
const merged = /* @__PURE__ */ new Map();
|
|
578
578
|
for (const raw of item.parameters ?? []) {
|
|
579
|
-
const parameter = deref(
|
|
579
|
+
const parameter = deref(document3, raw);
|
|
580
580
|
merged.set(`${parameter.in}:${parameter.name}`, parameter);
|
|
581
581
|
}
|
|
582
582
|
for (const raw of own ?? []) {
|
|
583
|
-
const parameter = deref(
|
|
583
|
+
const parameter = deref(document3, raw);
|
|
584
584
|
merged.set(`${parameter.in}:${parameter.name}`, parameter);
|
|
585
585
|
}
|
|
586
586
|
return [...merged.values()];
|
|
587
587
|
};
|
|
588
|
-
var listOperations = (
|
|
588
|
+
var listOperations = (document3) => {
|
|
589
589
|
const operations = [];
|
|
590
|
-
for (const [path, item] of Object.entries(
|
|
590
|
+
for (const [path, item] of Object.entries(document3.paths)) {
|
|
591
591
|
for (const method of HTTP_METHODS) {
|
|
592
592
|
const operation = item[method];
|
|
593
593
|
if (operation?.operationId === void 0)
|
|
594
594
|
continue;
|
|
595
595
|
const responses = {};
|
|
596
596
|
for (const [status, response] of Object.entries(operation.responses)) {
|
|
597
|
-
responses[status] = deref(
|
|
597
|
+
responses[status] = deref(document3, response);
|
|
598
598
|
}
|
|
599
599
|
operations.push({
|
|
600
600
|
operationId: operation.operationId,
|
|
601
601
|
method,
|
|
602
602
|
path,
|
|
603
603
|
operation,
|
|
604
|
-
parameters: mergeParameters(
|
|
605
|
-
requestBody: operation.requestBody === void 0 ? void 0 : deref(
|
|
604
|
+
parameters: mergeParameters(document3, item, operation.parameters),
|
|
605
|
+
requestBody: operation.requestBody === void 0 ? void 0 : deref(document3, operation.requestBody),
|
|
606
606
|
responses
|
|
607
607
|
});
|
|
608
608
|
}
|
|
@@ -963,6 +963,1232 @@ var clearNamespace = (sqlite, namespace) => {
|
|
|
963
963
|
});
|
|
964
964
|
};
|
|
965
965
|
|
|
966
|
+
// ../../ui/dist/reset.js
|
|
967
|
+
var CSS_RESET = `*,
|
|
968
|
+
*::before,
|
|
969
|
+
*::after {
|
|
970
|
+
box-sizing: border-box;
|
|
971
|
+
margin: 0;
|
|
972
|
+
padding: 0;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
* {
|
|
976
|
+
scrollbar-width: thin;
|
|
977
|
+
scrollbar-color: color-mix(in srgb, CanvasText 35%, Canvas) Canvas;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
*::-webkit-scrollbar {
|
|
981
|
+
width: 12px;
|
|
982
|
+
height: 12px;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
*::-webkit-scrollbar-track,
|
|
986
|
+
*::-webkit-scrollbar-corner {
|
|
987
|
+
background: Canvas;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
*::-webkit-scrollbar-thumb {
|
|
991
|
+
background: color-mix(in srgb, CanvasText 35%, Canvas);
|
|
992
|
+
border: 3px solid Canvas;
|
|
993
|
+
border-radius: 999px;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
html {
|
|
997
|
+
hanging-punctuation: first allow-end last;
|
|
998
|
+
-webkit-text-size-adjust: none;
|
|
999
|
+
text-size-adjust: none;
|
|
1000
|
+
tab-size: 4;
|
|
1001
|
+
font-family:
|
|
1002
|
+
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI Variable", "Segoe UI",
|
|
1003
|
+
"Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
|
1004
|
+
font-size: 16px;
|
|
1005
|
+
line-height: 1.5;
|
|
1006
|
+
-webkit-font-smoothing: antialiased;
|
|
1007
|
+
-moz-osx-font-smoothing: grayscale;
|
|
1008
|
+
text-rendering: optimizeLegibility;
|
|
1009
|
+
-webkit-tap-highlight-color: transparent;
|
|
1010
|
+
scroll-behavior: smooth;
|
|
1011
|
+
background: Canvas;
|
|
1012
|
+
color: CanvasText;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
body {
|
|
1016
|
+
min-height: 100svh;
|
|
1017
|
+
line-height: 1.5;
|
|
1018
|
+
background: Canvas;
|
|
1019
|
+
color: CanvasText;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
h1 {
|
|
1023
|
+
font-size: 2em;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
h2 {
|
|
1027
|
+
font-size: 1.5em;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
h3 {
|
|
1031
|
+
font-size: 1.17em;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
h4 {
|
|
1035
|
+
font-size: 1em;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
h5 {
|
|
1039
|
+
font-size: 0.83em;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
h6 {
|
|
1043
|
+
font-size: 0.67em;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
h1,
|
|
1047
|
+
h2,
|
|
1048
|
+
h3,
|
|
1049
|
+
h4,
|
|
1050
|
+
h5,
|
|
1051
|
+
h6 {
|
|
1052
|
+
font-weight: 700;
|
|
1053
|
+
line-height: 1.2;
|
|
1054
|
+
text-wrap: balance;
|
|
1055
|
+
overflow-wrap: break-word;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
p,
|
|
1059
|
+
li,
|
|
1060
|
+
figcaption,
|
|
1061
|
+
dd,
|
|
1062
|
+
dt,
|
|
1063
|
+
blockquote {
|
|
1064
|
+
overflow-wrap: break-word;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
p,
|
|
1068
|
+
li,
|
|
1069
|
+
figcaption,
|
|
1070
|
+
blockquote {
|
|
1071
|
+
text-wrap: pretty;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
ul,
|
|
1075
|
+
ol,
|
|
1076
|
+
menu {
|
|
1077
|
+
list-style: none;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
a {
|
|
1081
|
+
color: inherit;
|
|
1082
|
+
text-decoration: underline;
|
|
1083
|
+
text-decoration-thickness: 1px;
|
|
1084
|
+
text-underline-offset: 0.15em;
|
|
1085
|
+
text-decoration-skip-ink: auto;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
b,
|
|
1089
|
+
strong {
|
|
1090
|
+
font-weight: 700;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
small {
|
|
1094
|
+
font-size: 0.875em;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
code,
|
|
1098
|
+
kbd,
|
|
1099
|
+
samp,
|
|
1100
|
+
pre {
|
|
1101
|
+
font-family: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
1102
|
+
font-size: 1em;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
sub,
|
|
1106
|
+
sup {
|
|
1107
|
+
font-size: 75%;
|
|
1108
|
+
line-height: 0;
|
|
1109
|
+
position: relative;
|
|
1110
|
+
vertical-align: baseline;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
sub {
|
|
1114
|
+
bottom: -0.25em;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
sup {
|
|
1118
|
+
top: -0.5em;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
abbr[title] {
|
|
1122
|
+
text-decoration: underline dotted;
|
|
1123
|
+
text-underline-offset: 0.15em;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
mark {
|
|
1127
|
+
background: Mark;
|
|
1128
|
+
color: MarkText;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
hr {
|
|
1132
|
+
height: 0;
|
|
1133
|
+
border: 0;
|
|
1134
|
+
border-top: 1px solid color-mix(in srgb, CanvasText 20%, transparent);
|
|
1135
|
+
color: inherit;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
img,
|
|
1139
|
+
picture,
|
|
1140
|
+
video,
|
|
1141
|
+
canvas {
|
|
1142
|
+
display: block;
|
|
1143
|
+
max-width: 100%;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
img,
|
|
1147
|
+
video {
|
|
1148
|
+
height: auto;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
svg {
|
|
1152
|
+
overflow: hidden;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
iframe {
|
|
1156
|
+
border: 0;
|
|
1157
|
+
max-width: 100%;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
table {
|
|
1161
|
+
border-collapse: collapse;
|
|
1162
|
+
border-spacing: 0;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
fieldset {
|
|
1166
|
+
border: 0;
|
|
1167
|
+
min-inline-size: 0;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
legend {
|
|
1171
|
+
padding: 0;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
button,
|
|
1175
|
+
input,
|
|
1176
|
+
select,
|
|
1177
|
+
textarea,
|
|
1178
|
+
optgroup {
|
|
1179
|
+
font: inherit;
|
|
1180
|
+
letter-spacing: inherit;
|
|
1181
|
+
color: inherit;
|
|
1182
|
+
margin: 0;
|
|
1183
|
+
vertical-align: middle;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
button,
|
|
1187
|
+
input[type="button"],
|
|
1188
|
+
input[type="submit"],
|
|
1189
|
+
input[type="reset"] {
|
|
1190
|
+
appearance: none;
|
|
1191
|
+
background: none;
|
|
1192
|
+
border: 0;
|
|
1193
|
+
padding: 0;
|
|
1194
|
+
cursor: pointer;
|
|
1195
|
+
text-align: center;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
input,
|
|
1199
|
+
textarea,
|
|
1200
|
+
select {
|
|
1201
|
+
background-color: Canvas;
|
|
1202
|
+
border: 1px solid color-mix(in srgb, CanvasText 28%, transparent);
|
|
1203
|
+
border-radius: 0;
|
|
1204
|
+
padding: 0.5em 0.65em;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
input[type="checkbox"],
|
|
1208
|
+
input[type="radio"],
|
|
1209
|
+
input[type="range"],
|
|
1210
|
+
input[type="file"],
|
|
1211
|
+
input[type="color"],
|
|
1212
|
+
input[type="button"],
|
|
1213
|
+
input[type="submit"],
|
|
1214
|
+
input[type="reset"],
|
|
1215
|
+
input[type="image"],
|
|
1216
|
+
input[type="hidden"] {
|
|
1217
|
+
background: none;
|
|
1218
|
+
border: 0;
|
|
1219
|
+
padding: 0;
|
|
1220
|
+
border-radius: 0;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
input[type="checkbox"],
|
|
1224
|
+
input[type="radio"] {
|
|
1225
|
+
accent-color: CanvasText;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
input[type="search"] {
|
|
1229
|
+
appearance: none;
|
|
1230
|
+
background-color: Canvas;
|
|
1231
|
+
border: 1px solid color-mix(in srgb, CanvasText 28%, transparent);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
input[type="search"]::-webkit-search-decoration,
|
|
1235
|
+
input[type="search"]::-webkit-search-cancel-button,
|
|
1236
|
+
input[type="search"]::-webkit-search-results-button,
|
|
1237
|
+
input[type="search"]::-webkit-search-results-decoration {
|
|
1238
|
+
-webkit-appearance: none;
|
|
1239
|
+
display: none;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
input[type="number"] {
|
|
1243
|
+
appearance: textfield;
|
|
1244
|
+
background-color: Canvas;
|
|
1245
|
+
border: 1px solid color-mix(in srgb, CanvasText 28%, transparent);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
input[type="number"]::-webkit-inner-spin-button,
|
|
1249
|
+
input[type="number"]::-webkit-outer-spin-button {
|
|
1250
|
+
-webkit-appearance: none;
|
|
1251
|
+
margin: 0;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
textarea {
|
|
1255
|
+
resize: vertical;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
::placeholder {
|
|
1259
|
+
opacity: 1;
|
|
1260
|
+
color: color-mix(in srgb, CanvasText 45%, transparent);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
:focus-visible {
|
|
1264
|
+
outline: 2px solid CanvasText;
|
|
1265
|
+
outline-offset: 2px;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
:focus:not(:focus-visible) {
|
|
1269
|
+
outline: none;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
:disabled,
|
|
1273
|
+
[disabled] {
|
|
1274
|
+
cursor: default;
|
|
1275
|
+
opacity: 0.6;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
label,
|
|
1279
|
+
summary,
|
|
1280
|
+
select,
|
|
1281
|
+
[role="button"] {
|
|
1282
|
+
cursor: pointer;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
summary {
|
|
1286
|
+
display: list-item;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
details {
|
|
1290
|
+
display: block;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
dialog {
|
|
1294
|
+
margin: auto;
|
|
1295
|
+
padding: 1rem;
|
|
1296
|
+
border: 1px solid color-mix(in srgb, CanvasText 20%, transparent);
|
|
1297
|
+
background: Canvas;
|
|
1298
|
+
color: CanvasText;
|
|
1299
|
+
max-width: calc(100vw - 2rem);
|
|
1300
|
+
max-height: calc(100svh - 2rem);
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
dialog:not([open]) {
|
|
1304
|
+
display: none;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
::backdrop {
|
|
1308
|
+
background: color-mix(in srgb, CanvasText 40%, transparent);
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
[hidden] {
|
|
1312
|
+
display: none !important;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
::selection {
|
|
1316
|
+
background: Highlight;
|
|
1317
|
+
color: HighlightText;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1321
|
+
*,
|
|
1322
|
+
*::before,
|
|
1323
|
+
*::after {
|
|
1324
|
+
animation-duration: 0.01ms !important;
|
|
1325
|
+
animation-iteration-count: 1 !important;
|
|
1326
|
+
transition-duration: 0.01ms !important;
|
|
1327
|
+
scroll-behavior: auto !important;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
@media (pointer: coarse) {
|
|
1332
|
+
input,
|
|
1333
|
+
select,
|
|
1334
|
+
textarea {
|
|
1335
|
+
font-size: max(16px, 1em);
|
|
1336
|
+
}
|
|
1337
|
+
}`;
|
|
1338
|
+
|
|
1339
|
+
// ../core/dist/admin-client.js
|
|
1340
|
+
function isRecord2(value) {
|
|
1341
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1342
|
+
}
|
|
1343
|
+
function mountAdminBrand(doc, host, service, catalog) {
|
|
1344
|
+
const clip = (value, max) => {
|
|
1345
|
+
if (typeof value !== "string")
|
|
1346
|
+
return null;
|
|
1347
|
+
const trimmed = value.trim();
|
|
1348
|
+
if (trimmed === "")
|
|
1349
|
+
return null;
|
|
1350
|
+
return trimmed.length > max ? trimmed.slice(0, max) : trimmed;
|
|
1351
|
+
};
|
|
1352
|
+
const httpUrl = (value) => {
|
|
1353
|
+
if (typeof value !== "string" || value.length > 2e3)
|
|
1354
|
+
return null;
|
|
1355
|
+
try {
|
|
1356
|
+
const url = new URL(value);
|
|
1357
|
+
if (url.protocol !== "https:" && url.protocol !== "http:")
|
|
1358
|
+
return null;
|
|
1359
|
+
return url.href;
|
|
1360
|
+
} catch {
|
|
1361
|
+
return null;
|
|
1362
|
+
}
|
|
1363
|
+
};
|
|
1364
|
+
const hexColor = (value) => {
|
|
1365
|
+
if (typeof value !== "string")
|
|
1366
|
+
return null;
|
|
1367
|
+
const raw = value.trim();
|
|
1368
|
+
return /^#[0-9a-fA-F]{3,8}$/.test(raw) ? raw : null;
|
|
1369
|
+
};
|
|
1370
|
+
const hostname = (value) => {
|
|
1371
|
+
try {
|
|
1372
|
+
return new URL(value).hostname.replace(/^www\./, "");
|
|
1373
|
+
} catch {
|
|
1374
|
+
return value;
|
|
1375
|
+
}
|
|
1376
|
+
};
|
|
1377
|
+
host.replaceChildren();
|
|
1378
|
+
host.hidden = true;
|
|
1379
|
+
if (!isRecord2(catalog) || !Object.hasOwn(catalog, service))
|
|
1380
|
+
return;
|
|
1381
|
+
const row = catalog[service];
|
|
1382
|
+
if (!isRecord2(row))
|
|
1383
|
+
return;
|
|
1384
|
+
const vendor = clip(row.vendor, 80);
|
|
1385
|
+
const website = httpUrl(row.website);
|
|
1386
|
+
const guide = httpUrl(row.guide);
|
|
1387
|
+
const docs = httpUrl(row.docs);
|
|
1388
|
+
const logo = httpUrl(row.logo);
|
|
1389
|
+
const color = hexColor(row.color);
|
|
1390
|
+
const description = clip(row.description, 280);
|
|
1391
|
+
if (vendor === null || website === null && guide === null && docs === null)
|
|
1392
|
+
return;
|
|
1393
|
+
const add = (node) => {
|
|
1394
|
+
host.append(node);
|
|
1395
|
+
return node;
|
|
1396
|
+
};
|
|
1397
|
+
if (logo !== null) {
|
|
1398
|
+
const img = doc.createElement("img");
|
|
1399
|
+
img.alt = "";
|
|
1400
|
+
img.src = logo;
|
|
1401
|
+
img.width = 16;
|
|
1402
|
+
img.height = 16;
|
|
1403
|
+
img.decoding = "async";
|
|
1404
|
+
img.referrerPolicy = "no-referrer";
|
|
1405
|
+
if (color !== null)
|
|
1406
|
+
img.style.borderColor = color;
|
|
1407
|
+
add(img);
|
|
1408
|
+
}
|
|
1409
|
+
const name = doc.createElement("span");
|
|
1410
|
+
name.className = "vendor-name";
|
|
1411
|
+
name.textContent = vendor;
|
|
1412
|
+
if (description !== null)
|
|
1413
|
+
name.title = description;
|
|
1414
|
+
add(name);
|
|
1415
|
+
const sep = () => {
|
|
1416
|
+
const node = doc.createElement("span");
|
|
1417
|
+
node.className = "vendor-sep";
|
|
1418
|
+
node.setAttribute("aria-hidden", "true");
|
|
1419
|
+
node.textContent = "\xB7";
|
|
1420
|
+
return node;
|
|
1421
|
+
};
|
|
1422
|
+
const link = (href, label, title) => {
|
|
1423
|
+
const node = doc.createElement("a");
|
|
1424
|
+
node.className = "vendor-link";
|
|
1425
|
+
node.href = href;
|
|
1426
|
+
node.target = "_blank";
|
|
1427
|
+
node.rel = "noopener noreferrer";
|
|
1428
|
+
node.referrerPolicy = "no-referrer";
|
|
1429
|
+
node.textContent = label;
|
|
1430
|
+
node.title = title;
|
|
1431
|
+
return node;
|
|
1432
|
+
};
|
|
1433
|
+
if (website !== null) {
|
|
1434
|
+
add(sep());
|
|
1435
|
+
add(link(website, hostname(website), `${vendor} website`));
|
|
1436
|
+
}
|
|
1437
|
+
if (guide !== null) {
|
|
1438
|
+
add(sep());
|
|
1439
|
+
add(link(guide, "Docs", `Docs for ${vendor}`));
|
|
1440
|
+
}
|
|
1441
|
+
if (docs !== null) {
|
|
1442
|
+
add(sep());
|
|
1443
|
+
add(link(docs, "API", `${vendor} API reference`));
|
|
1444
|
+
}
|
|
1445
|
+
host.removeAttribute("hidden");
|
|
1446
|
+
host.hidden = false;
|
|
1447
|
+
}
|
|
1448
|
+
function startAdminBrand(env) {
|
|
1449
|
+
const requested = env.params.get("brands");
|
|
1450
|
+
let brandsUrl = env.brandsUrl;
|
|
1451
|
+
if (requested) {
|
|
1452
|
+
try {
|
|
1453
|
+
const url = new URL(requested, env.location.href);
|
|
1454
|
+
const local = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "[::1]" || url.origin === env.location.origin;
|
|
1455
|
+
if ((url.protocol === "https:" || url.protocol === "http:") && local)
|
|
1456
|
+
brandsUrl = url.href;
|
|
1457
|
+
} catch {
|
|
1458
|
+
brandsUrl = env.brandsUrl;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
const vendorHost = env.$("vendor");
|
|
1462
|
+
const brandRoot = env.document.querySelector("[data-mockingbird-admin]");
|
|
1463
|
+
const brandService = brandRoot?.getAttribute("data-service") || "";
|
|
1464
|
+
if (vendorHost === null)
|
|
1465
|
+
return;
|
|
1466
|
+
env.fetch(brandsUrl, {
|
|
1467
|
+
headers: { accept: "application/json" },
|
|
1468
|
+
signal: AbortSignal.timeout(5e3)
|
|
1469
|
+
}).then(async (response) => {
|
|
1470
|
+
if (!response.ok)
|
|
1471
|
+
return null;
|
|
1472
|
+
const catalog = await response.json();
|
|
1473
|
+
return catalog;
|
|
1474
|
+
}).then((catalog) => {
|
|
1475
|
+
mountAdminBrand(env.document, vendorHost, brandService, catalog);
|
|
1476
|
+
}).catch(() => {
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
function faultPresetList(value) {
|
|
1480
|
+
if (!isRecord2(value))
|
|
1481
|
+
throw new Error("Fault presets response was not an object");
|
|
1482
|
+
if (!Object.hasOwn(value, "presets") || value.presets === void 0 || value.presets === null) {
|
|
1483
|
+
return { presets: [] };
|
|
1484
|
+
}
|
|
1485
|
+
if (!Array.isArray(value.presets))
|
|
1486
|
+
throw new Error("Fault presets response was not a list");
|
|
1487
|
+
return {
|
|
1488
|
+
presets: value.presets.flatMap((item) => {
|
|
1489
|
+
if (!isRecord2(item) || typeof item.name !== "string")
|
|
1490
|
+
return [];
|
|
1491
|
+
return [{ name: item.name }];
|
|
1492
|
+
})
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
function bootAdmin(config) {
|
|
1496
|
+
const fieldKinds = ["string", "number", "boolean", "null", "object", "array", "unknown"];
|
|
1497
|
+
const sources = ["declared", "inferred", "mixed"];
|
|
1498
|
+
const isFieldKind = (value) => typeof value === "string" && fieldKinds.some((kind) => kind === value);
|
|
1499
|
+
const isSource = (value) => typeof value === "string" && sources.some((source) => source === value);
|
|
1500
|
+
const readJson2 = (text) => {
|
|
1501
|
+
const value = JSON.parse(text);
|
|
1502
|
+
return value;
|
|
1503
|
+
};
|
|
1504
|
+
const esc = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => {
|
|
1505
|
+
switch (char) {
|
|
1506
|
+
case "&":
|
|
1507
|
+
return "&";
|
|
1508
|
+
case "<":
|
|
1509
|
+
return "<";
|
|
1510
|
+
case ">":
|
|
1511
|
+
return ">";
|
|
1512
|
+
case '"':
|
|
1513
|
+
return """;
|
|
1514
|
+
default:
|
|
1515
|
+
return "'";
|
|
1516
|
+
}
|
|
1517
|
+
});
|
|
1518
|
+
const el = (id, kind) => {
|
|
1519
|
+
const node = document.getElementById(id);
|
|
1520
|
+
if (!(node instanceof kind))
|
|
1521
|
+
throw new Error(`admin shell is missing #${id}`);
|
|
1522
|
+
return node;
|
|
1523
|
+
};
|
|
1524
|
+
const errorMessage = (data, fallback) => {
|
|
1525
|
+
if (!isRecord2(data))
|
|
1526
|
+
return fallback;
|
|
1527
|
+
const error2 = data.error;
|
|
1528
|
+
if (!isRecord2(error2) || typeof error2.message !== "string" || error2.message === "") {
|
|
1529
|
+
return fallback;
|
|
1530
|
+
}
|
|
1531
|
+
return error2.message;
|
|
1532
|
+
};
|
|
1533
|
+
const requireList = (value, label) => {
|
|
1534
|
+
if (value === void 0 || value === null)
|
|
1535
|
+
return [];
|
|
1536
|
+
if (!Array.isArray(value))
|
|
1537
|
+
throw new Error(`${label} was not a list`);
|
|
1538
|
+
return value;
|
|
1539
|
+
};
|
|
1540
|
+
const state = {
|
|
1541
|
+
namespace: "default",
|
|
1542
|
+
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
1543
|
+
view: "overview",
|
|
1544
|
+
collection: "",
|
|
1545
|
+
shape: null,
|
|
1546
|
+
route: null,
|
|
1547
|
+
editing: null
|
|
1548
|
+
};
|
|
1549
|
+
const standard = new Set(config.standardRoutes);
|
|
1550
|
+
const banner = el("banner", HTMLElement);
|
|
1551
|
+
const keyInput = el("admin-key", HTMLInputElement);
|
|
1552
|
+
keyInput.value = state.key;
|
|
1553
|
+
const params = new URLSearchParams(location.search);
|
|
1554
|
+
const queryKey = params.get("key");
|
|
1555
|
+
if (queryKey) {
|
|
1556
|
+
state.key = queryKey;
|
|
1557
|
+
keyInput.value = state.key;
|
|
1558
|
+
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
1559
|
+
params.delete("key");
|
|
1560
|
+
const next = params.toString();
|
|
1561
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
1562
|
+
}
|
|
1563
|
+
const queryNamespace = params.get("namespace");
|
|
1564
|
+
if (queryNamespace)
|
|
1565
|
+
state.namespace = queryNamespace;
|
|
1566
|
+
const showError = (error2) => {
|
|
1567
|
+
banner.textContent = error2 instanceof Error ? error2.message : String(error2);
|
|
1568
|
+
banner.classList.add("show");
|
|
1569
|
+
};
|
|
1570
|
+
const clearError = () => {
|
|
1571
|
+
banner.classList.remove("show");
|
|
1572
|
+
};
|
|
1573
|
+
const api = async (method, path, body) => {
|
|
1574
|
+
const headers = { accept: "application/json" };
|
|
1575
|
+
if (state.key !== "")
|
|
1576
|
+
headers[config.adminKeyHeader] = state.key;
|
|
1577
|
+
if (state.namespace !== "")
|
|
1578
|
+
headers["x-mockingbird-namespace"] = state.namespace;
|
|
1579
|
+
if (body !== void 0)
|
|
1580
|
+
headers["content-type"] = "application/json";
|
|
1581
|
+
const init = { method, headers };
|
|
1582
|
+
if (body !== void 0)
|
|
1583
|
+
init.body = JSON.stringify(body);
|
|
1584
|
+
const response = await fetch(config.adminPrefix + path, init);
|
|
1585
|
+
const text = await response.text();
|
|
1586
|
+
let data = null;
|
|
1587
|
+
if (text !== "") {
|
|
1588
|
+
try {
|
|
1589
|
+
data = readJson2(text);
|
|
1590
|
+
} catch {
|
|
1591
|
+
data = { raw: text };
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
if (response.status === 401) {
|
|
1595
|
+
keyInput.focus();
|
|
1596
|
+
throw new Error(errorMessage(data, "Admin key required"));
|
|
1597
|
+
}
|
|
1598
|
+
if (!response.ok) {
|
|
1599
|
+
throw new Error(errorMessage(data, `${response.status} ${method} ${path}`));
|
|
1600
|
+
}
|
|
1601
|
+
return data;
|
|
1602
|
+
};
|
|
1603
|
+
const originGet = async (path) => {
|
|
1604
|
+
const response = await fetch(path, { headers: { accept: "application/json" } });
|
|
1605
|
+
if (!response.ok)
|
|
1606
|
+
throw new Error(`${response.status} ${path}`);
|
|
1607
|
+
const value = await response.json();
|
|
1608
|
+
return value;
|
|
1609
|
+
};
|
|
1610
|
+
const panelApi = {
|
|
1611
|
+
get: (path) => api("GET", path),
|
|
1612
|
+
send: (method, path, body) => api(method, path, body),
|
|
1613
|
+
namespace: () => state.namespace
|
|
1614
|
+
};
|
|
1615
|
+
const isPanelScript = (value) => typeof value === "function";
|
|
1616
|
+
const setTheme = (theme) => {
|
|
1617
|
+
if (theme)
|
|
1618
|
+
document.documentElement.dataset.theme = theme;
|
|
1619
|
+
else
|
|
1620
|
+
delete document.documentElement.dataset.theme;
|
|
1621
|
+
};
|
|
1622
|
+
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
1623
|
+
if (storedTheme)
|
|
1624
|
+
setTheme(storedTheme);
|
|
1625
|
+
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
1626
|
+
const current = document.documentElement.dataset.theme;
|
|
1627
|
+
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
1628
|
+
const next = dark ? "light" : "dark";
|
|
1629
|
+
localStorage.setItem("mockingbird-admin-theme", next);
|
|
1630
|
+
setTheme(next);
|
|
1631
|
+
});
|
|
1632
|
+
const show = (view) => {
|
|
1633
|
+
state.view = view;
|
|
1634
|
+
for (const button of document.querySelectorAll("#nav button")) {
|
|
1635
|
+
if (!(button instanceof HTMLButtonElement))
|
|
1636
|
+
continue;
|
|
1637
|
+
button.setAttribute("aria-current", button.dataset.view === view ? "true" : "false");
|
|
1638
|
+
}
|
|
1639
|
+
for (const section of document.querySelectorAll("main .view")) {
|
|
1640
|
+
if (!(section instanceof HTMLElement))
|
|
1641
|
+
continue;
|
|
1642
|
+
section.hidden = section.id !== `view-${view}`;
|
|
1643
|
+
}
|
|
1644
|
+
};
|
|
1645
|
+
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
1646
|
+
const target = event.target;
|
|
1647
|
+
if (!(target instanceof Element))
|
|
1648
|
+
return;
|
|
1649
|
+
const button = target.closest("button[data-view]");
|
|
1650
|
+
if (!(button instanceof HTMLButtonElement))
|
|
1651
|
+
return;
|
|
1652
|
+
const view = button.dataset.view;
|
|
1653
|
+
if (view === void 0)
|
|
1654
|
+
return;
|
|
1655
|
+
show(view);
|
|
1656
|
+
refresh().catch(showError);
|
|
1657
|
+
});
|
|
1658
|
+
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>`;
|
|
1659
|
+
const table = (columns, rows) => {
|
|
1660
|
+
if (rows.length === 0)
|
|
1661
|
+
return '<p class="empty">Nothing here yet.</p>';
|
|
1662
|
+
const head = `<tr>${columns.map((column) => `<th>${column.label}</th>`).join("")}</tr>`;
|
|
1663
|
+
const body = rows.map((row) => {
|
|
1664
|
+
const cells = columns.map((column) => `<td data-label="${column.label}">${column.cell(row)}</td>`).join("");
|
|
1665
|
+
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
1666
|
+
return `<tr${attrs}>${cells}</tr>`;
|
|
1667
|
+
}).join("");
|
|
1668
|
+
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
1669
|
+
};
|
|
1670
|
+
const preview = (value) => {
|
|
1671
|
+
const text = JSON.stringify(value) ?? "null";
|
|
1672
|
+
return esc(text.length > 180 ? `${text.slice(0, 180)}\u2026` : text);
|
|
1673
|
+
};
|
|
1674
|
+
const when = (epoch) => {
|
|
1675
|
+
const date = new Date(epoch);
|
|
1676
|
+
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
1677
|
+
};
|
|
1678
|
+
const clockCard = (epoch) => {
|
|
1679
|
+
const date = new Date(epoch);
|
|
1680
|
+
if (Number.isNaN(date.getTime()))
|
|
1681
|
+
return esc(String(epoch));
|
|
1682
|
+
const iso = date.toISOString();
|
|
1683
|
+
return `${esc(iso.slice(0, 10))}<span class="clock-time">${esc(iso.slice(11, 19))} UTC</span>`;
|
|
1684
|
+
};
|
|
1685
|
+
const decodeClock = (value) => {
|
|
1686
|
+
if (!isRecord2(value) || typeof value.now !== "number" || typeof value.offsetMs !== "number" || typeof value.frozen !== "boolean") {
|
|
1687
|
+
throw new Error("Clock response was not a clock");
|
|
1688
|
+
}
|
|
1689
|
+
return { now: value.now, offsetMs: value.offsetMs, frozen: value.frozen };
|
|
1690
|
+
};
|
|
1691
|
+
const decodeField = (value) => {
|
|
1692
|
+
if (!isRecord2(value) || typeof value.name !== "string" || !isFieldKind(value.kind)) {
|
|
1693
|
+
return void 0;
|
|
1694
|
+
}
|
|
1695
|
+
const field = {
|
|
1696
|
+
name: value.name,
|
|
1697
|
+
kind: value.kind,
|
|
1698
|
+
optional: value.optional === true
|
|
1699
|
+
};
|
|
1700
|
+
if (typeof value.description === "string")
|
|
1701
|
+
field.description = value.description;
|
|
1702
|
+
if (Array.isArray(value.fields)) {
|
|
1703
|
+
field.fields = value.fields.flatMap((item) => {
|
|
1704
|
+
const child = decodeField(item);
|
|
1705
|
+
return child === void 0 ? [] : [child];
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
return field;
|
|
1709
|
+
};
|
|
1710
|
+
const decodeCollection = (value) => {
|
|
1711
|
+
if (!isRecord2(value) || typeof value.name !== "string")
|
|
1712
|
+
return void 0;
|
|
1713
|
+
const fields = Array.isArray(value.fields) ? value.fields.flatMap((item) => {
|
|
1714
|
+
const field = decodeField(item);
|
|
1715
|
+
return field === void 0 ? [] : [field];
|
|
1716
|
+
}) : [];
|
|
1717
|
+
const collection = {
|
|
1718
|
+
name: value.name,
|
|
1719
|
+
label: typeof value.label === "string" ? value.label : value.name,
|
|
1720
|
+
count: typeof value.count === "number" ? value.count : 0,
|
|
1721
|
+
source: isSource(value.source) ? value.source : "inferred",
|
|
1722
|
+
fields
|
|
1723
|
+
};
|
|
1724
|
+
if (typeof value.description === "string")
|
|
1725
|
+
collection.description = value.description;
|
|
1726
|
+
return collection;
|
|
1727
|
+
};
|
|
1728
|
+
const decodeState = (value) => {
|
|
1729
|
+
if (!isRecord2(value) || !Array.isArray(value.collections)) {
|
|
1730
|
+
throw new Error("State response has no collection list");
|
|
1731
|
+
}
|
|
1732
|
+
return {
|
|
1733
|
+
namespace: typeof value.namespace === "string" ? value.namespace : state.namespace,
|
|
1734
|
+
storageNamespace: typeof value.storageNamespace === "string" ? value.storageNamespace : "",
|
|
1735
|
+
collections: value.collections.flatMap((item) => {
|
|
1736
|
+
const collection = decodeCollection(item);
|
|
1737
|
+
return collection === void 0 ? [] : [collection];
|
|
1738
|
+
})
|
|
1739
|
+
};
|
|
1740
|
+
};
|
|
1741
|
+
const decodeRecords = (value) => requireList(isRecord2(value) ? value.records : void 0, "Records").flatMap((item) => {
|
|
1742
|
+
if (!isRecord2(item) || typeof item.id !== "string")
|
|
1743
|
+
return [];
|
|
1744
|
+
return [
|
|
1745
|
+
{
|
|
1746
|
+
id: item.id,
|
|
1747
|
+
seq: typeof item.seq === "number" ? item.seq : 0,
|
|
1748
|
+
value: item.value,
|
|
1749
|
+
attrs: ` class="clickable" data-id="${esc(item.id)}"`
|
|
1750
|
+
}
|
|
1751
|
+
];
|
|
1752
|
+
});
|
|
1753
|
+
const textOf = (value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? String(value) : "";
|
|
1754
|
+
const decodeJournal = (value) => requireList(isRecord2(value) ? value.requests : void 0, "Journal").flatMap((item) => {
|
|
1755
|
+
if (!isRecord2(item))
|
|
1756
|
+
return [];
|
|
1757
|
+
return [
|
|
1758
|
+
{
|
|
1759
|
+
at: textOf(item.at),
|
|
1760
|
+
method: textOf(item.method),
|
|
1761
|
+
path: textOf(item.path),
|
|
1762
|
+
status: textOf(item.status),
|
|
1763
|
+
durationMs: textOf(item.durationMs),
|
|
1764
|
+
operationId: textOf(item.operationId)
|
|
1765
|
+
}
|
|
1766
|
+
];
|
|
1767
|
+
});
|
|
1768
|
+
const decodeFaults = (value) => requireList(isRecord2(value) ? value.faults : void 0, "Faults").flatMap((item) => {
|
|
1769
|
+
if (!isRecord2(item))
|
|
1770
|
+
return [];
|
|
1771
|
+
return [
|
|
1772
|
+
{
|
|
1773
|
+
id: textOf(item.id),
|
|
1774
|
+
method: typeof item.method === "string" ? item.method : void 0,
|
|
1775
|
+
pathPrefix: typeof item.pathPrefix === "string" ? item.pathPrefix : void 0,
|
|
1776
|
+
operationId: typeof item.operationId === "string" ? item.operationId : void 0,
|
|
1777
|
+
status: typeof item.status === "number" ? item.status : void 0,
|
|
1778
|
+
effect: typeof item.effect === "string" ? item.effect : void 0,
|
|
1779
|
+
drop: typeof item.drop === "boolean" ? item.drop : void 0,
|
|
1780
|
+
latencyMs: typeof item.latencyMs === "number" ? item.latencyMs : void 0
|
|
1781
|
+
}
|
|
1782
|
+
];
|
|
1783
|
+
});
|
|
1784
|
+
const loadNamespaces = async () => {
|
|
1785
|
+
const data = await api("GET", "/namespaces");
|
|
1786
|
+
const select = el("namespace", HTMLSelectElement);
|
|
1787
|
+
const listed = requireList(isRecord2(data) ? data.namespaces : void 0, "Namespaces");
|
|
1788
|
+
const fallback = isRecord2(data) && typeof data.default === "string" && data.default !== "" ? data.default : "default";
|
|
1789
|
+
const names = (listed.length > 0 ? listed.map((name) => String(name)) : [fallback]).slice();
|
|
1790
|
+
if (!names.includes(state.namespace))
|
|
1791
|
+
names.unshift(state.namespace);
|
|
1792
|
+
select.innerHTML = names.map((name) => `<option value="${esc(name)}">${esc(name)}</option>`).join("");
|
|
1793
|
+
select.value = state.namespace;
|
|
1794
|
+
};
|
|
1795
|
+
const renderOverview = async () => {
|
|
1796
|
+
const [health, shape, clock, journal] = await Promise.all([
|
|
1797
|
+
originGet("/health"),
|
|
1798
|
+
api("GET", "/state"),
|
|
1799
|
+
api("GET", "/clock"),
|
|
1800
|
+
api("GET", "/requests?limit=8")
|
|
1801
|
+
]);
|
|
1802
|
+
if (!isRecord2(health))
|
|
1803
|
+
throw new Error("Health response was not an object");
|
|
1804
|
+
const decoded = decodeState(shape);
|
|
1805
|
+
state.shape = decoded;
|
|
1806
|
+
const time = decodeClock(clock);
|
|
1807
|
+
el("overview-cards", HTMLElement).innerHTML = cards([
|
|
1808
|
+
{
|
|
1809
|
+
k: "Service",
|
|
1810
|
+
v: esc(typeof health.service === "string" && health.service !== "" ? health.service : config.service)
|
|
1811
|
+
},
|
|
1812
|
+
{
|
|
1813
|
+
k: "Status",
|
|
1814
|
+
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
1815
|
+
},
|
|
1816
|
+
{ k: "Namespace", v: esc(state.namespace) },
|
|
1817
|
+
{ k: "Collections", v: String(decoded.collections.length) },
|
|
1818
|
+
{
|
|
1819
|
+
k: "Records",
|
|
1820
|
+
v: String(decoded.collections.reduce((sum, item) => sum + item.count, 0))
|
|
1821
|
+
},
|
|
1822
|
+
{ k: "Clock", v: clockCard(time.now) }
|
|
1823
|
+
]);
|
|
1824
|
+
el("overview-requests", HTMLElement).innerHTML = table([
|
|
1825
|
+
{ label: "Method", cell: (row) => esc(row.method) },
|
|
1826
|
+
{ label: "Path", cell: (row) => esc(row.path) },
|
|
1827
|
+
{
|
|
1828
|
+
label: "Status",
|
|
1829
|
+
cell: (row) => `<span class="status-${row.status.charAt(0)}">${esc(row.status)}</span>`
|
|
1830
|
+
},
|
|
1831
|
+
{ label: "Operation", cell: (row) => esc(row.operationId) }
|
|
1832
|
+
], decodeJournal(journal));
|
|
1833
|
+
};
|
|
1834
|
+
const schemaChips = (collection) => {
|
|
1835
|
+
if (collection.fields.length === 0) {
|
|
1836
|
+
return '<p class="empty">No fields yet. Add a record or declare the collection on the service.</p>';
|
|
1837
|
+
}
|
|
1838
|
+
return `<div class="chips">${collection.fields.map((field) => `<span class="chip">${esc(field.name)} <i>${esc(field.kind)}${field.optional ? "?" : ""}</i></span>`).join("")}</div>`;
|
|
1839
|
+
};
|
|
1840
|
+
const renderState = async () => {
|
|
1841
|
+
const shape = decodeState(await api("GET", "/state"));
|
|
1842
|
+
state.shape = shape;
|
|
1843
|
+
const host = el("collections", HTMLElement);
|
|
1844
|
+
if (shape.collections.length === 0) {
|
|
1845
|
+
host.innerHTML = '<p class="empty">This namespace has no collections yet.</p>';
|
|
1846
|
+
el("schema", HTMLElement).innerHTML = "";
|
|
1847
|
+
el("records", HTMLElement).innerHTML = "";
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
if (!shape.collections.some((item) => item.name === state.collection)) {
|
|
1851
|
+
const first = shape.collections[0];
|
|
1852
|
+
if (first === void 0)
|
|
1853
|
+
return;
|
|
1854
|
+
state.collection = first.name;
|
|
1855
|
+
}
|
|
1856
|
+
host.innerHTML = shape.collections.map((item) => `<button type="button" class="collection" data-collection="${esc(item.name)}" aria-current="${item.name === state.collection ? "true" : "false"}"><strong>${esc(item.label)}</strong><small>${esc(item.count)} \xB7 ${esc(item.source)}</small></button>`).join("");
|
|
1857
|
+
const selected = shape.collections.find((item) => item.name === state.collection);
|
|
1858
|
+
if (selected === void 0)
|
|
1859
|
+
return;
|
|
1860
|
+
const description = selected.description ? `<p class="lede">${esc(selected.description)}</p>` : "";
|
|
1861
|
+
el("schema", HTMLElement).innerHTML = `<h3>${esc(selected.label)}</h3>${description}${schemaChips(selected)}`;
|
|
1862
|
+
const page = await api("GET", `/state/${encodeURIComponent(state.collection)}?limit=50`);
|
|
1863
|
+
el("records", HTMLElement).innerHTML = table([
|
|
1864
|
+
{ label: "Id", cell: (row) => `<code>${esc(row.id)}</code>` },
|
|
1865
|
+
{ label: "Seq", cell: (row) => esc(row.seq) },
|
|
1866
|
+
{ label: "Value", cell: (row) => preview(row.value) }
|
|
1867
|
+
], decodeRecords(page));
|
|
1868
|
+
};
|
|
1869
|
+
const renderClock = async () => {
|
|
1870
|
+
const clock = decodeClock(await api("GET", "/clock"));
|
|
1871
|
+
el("clock-readout", HTMLElement).innerHTML = `<div class="k">Now</div><div class="v">${esc(when(clock.now))}</div><p>${esc(clock.now)} ms \xB7 offset ${esc(clock.offsetMs)} ms ${clock.frozen ? "<span class='badge warn'>frozen</span>" : "<span class='badge ok'>live</span>"}</p>`;
|
|
1872
|
+
const freeze = el("freeze", HTMLButtonElement);
|
|
1873
|
+
freeze.textContent = clock.frozen ? "Unfreeze" : "Freeze";
|
|
1874
|
+
freeze.dataset.frozen = clock.frozen ? "1" : "0";
|
|
1875
|
+
};
|
|
1876
|
+
const renderFaults = async () => {
|
|
1877
|
+
const [faults, presets] = await Promise.all([
|
|
1878
|
+
api("GET", "/faults"),
|
|
1879
|
+
api("GET", "/faults/presets").catch(() => ({ presets: [] }))
|
|
1880
|
+
]);
|
|
1881
|
+
const select = el("fault-preset", HTMLSelectElement);
|
|
1882
|
+
const current = select.value;
|
|
1883
|
+
const list = faultPresetList(presets).presets;
|
|
1884
|
+
select.innerHTML = `<option value="">Custom rule</option>${list.map((preset) => `<option value="${esc(preset.name)}">${esc(preset.name)}</option>`).join("")}`;
|
|
1885
|
+
select.value = current;
|
|
1886
|
+
el("fault-list", HTMLElement).innerHTML = table([
|
|
1887
|
+
{ label: "Id", cell: (row) => esc(row.id) },
|
|
1888
|
+
{
|
|
1889
|
+
label: "Match",
|
|
1890
|
+
cell: (row) => esc([row.method, row.pathPrefix, row.operationId].filter((part) => Boolean(part)).join(" ") || "all")
|
|
1891
|
+
},
|
|
1892
|
+
{
|
|
1893
|
+
label: "Effect",
|
|
1894
|
+
cell: (row) => esc(row.status ?? row.effect ?? (row.drop ? "drop" : row.latencyMs || ""))
|
|
1895
|
+
}
|
|
1896
|
+
], decodeFaults(faults));
|
|
1897
|
+
};
|
|
1898
|
+
const renderJournal = async () => {
|
|
1899
|
+
const journal = await api("GET", "/requests?limit=100");
|
|
1900
|
+
el("journal", HTMLElement).innerHTML = table([
|
|
1901
|
+
{ label: "When", cell: (row) => esc(row.at) },
|
|
1902
|
+
{ label: "Method", cell: (row) => esc(row.method) },
|
|
1903
|
+
{ label: "Path", cell: (row) => esc(row.path) },
|
|
1904
|
+
{ label: "Status", cell: (row) => esc(row.status) },
|
|
1905
|
+
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
1906
|
+
], decodeJournal(journal));
|
|
1907
|
+
};
|
|
1908
|
+
const renderRoutes = async () => {
|
|
1909
|
+
const data = await api("GET", "/");
|
|
1910
|
+
if (!isRecord2(data))
|
|
1911
|
+
throw new Error("Route list was not an object");
|
|
1912
|
+
const routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
1913
|
+
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
1914
|
+
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
1915
|
+
const parts = route.split(" ");
|
|
1916
|
+
const method = parts[0] ?? "";
|
|
1917
|
+
const path = parts.slice(1).join(" ");
|
|
1918
|
+
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>`;
|
|
1919
|
+
}).join("");
|
|
1920
|
+
};
|
|
1921
|
+
const renderers = {
|
|
1922
|
+
overview: renderOverview,
|
|
1923
|
+
state: renderState,
|
|
1924
|
+
clock: renderClock,
|
|
1925
|
+
faults: renderFaults,
|
|
1926
|
+
journal: renderJournal,
|
|
1927
|
+
routes: renderRoutes
|
|
1928
|
+
};
|
|
1929
|
+
async function refresh() {
|
|
1930
|
+
clearError();
|
|
1931
|
+
const render = renderers[state.view];
|
|
1932
|
+
if (render)
|
|
1933
|
+
await render();
|
|
1934
|
+
}
|
|
1935
|
+
el("namespace", HTMLSelectElement).addEventListener("change", () => {
|
|
1936
|
+
state.namespace = el("namespace", HTMLSelectElement).value;
|
|
1937
|
+
refresh().catch(showError);
|
|
1938
|
+
});
|
|
1939
|
+
keyInput.addEventListener("change", () => {
|
|
1940
|
+
state.key = keyInput.value.trim();
|
|
1941
|
+
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
1942
|
+
refresh().catch(showError);
|
|
1943
|
+
});
|
|
1944
|
+
el("refresh", HTMLButtonElement).addEventListener("click", () => {
|
|
1945
|
+
refresh().catch(showError);
|
|
1946
|
+
});
|
|
1947
|
+
el("collections", HTMLElement).addEventListener("click", (event) => {
|
|
1948
|
+
const target = event.target;
|
|
1949
|
+
if (!(target instanceof Element))
|
|
1950
|
+
return;
|
|
1951
|
+
const button = target.closest("[data-collection]");
|
|
1952
|
+
if (!(button instanceof HTMLElement))
|
|
1953
|
+
return;
|
|
1954
|
+
const collection = button.dataset.collection;
|
|
1955
|
+
if (collection === void 0)
|
|
1956
|
+
return;
|
|
1957
|
+
state.collection = collection;
|
|
1958
|
+
renderState().catch(showError);
|
|
1959
|
+
});
|
|
1960
|
+
el("records", HTMLElement).addEventListener("click", (event) => {
|
|
1961
|
+
const target = event.target;
|
|
1962
|
+
if (!(target instanceof Element))
|
|
1963
|
+
return;
|
|
1964
|
+
const row = target.closest("[data-id]");
|
|
1965
|
+
if (!(row instanceof HTMLElement))
|
|
1966
|
+
return;
|
|
1967
|
+
const id = row.dataset.id;
|
|
1968
|
+
if (id === void 0)
|
|
1969
|
+
return;
|
|
1970
|
+
openEditor(id).catch(showError);
|
|
1971
|
+
});
|
|
1972
|
+
const editor = el("editor", HTMLDialogElement);
|
|
1973
|
+
const editorId = el("editor-id", HTMLInputElement);
|
|
1974
|
+
const editorValue = el("editor-value", HTMLTextAreaElement);
|
|
1975
|
+
const editorDelete = el("editor-delete", HTMLButtonElement);
|
|
1976
|
+
async function openEditor(id) {
|
|
1977
|
+
state.editing = id;
|
|
1978
|
+
el("editor-title", HTMLElement).textContent = id ? "Edit record" : "New record";
|
|
1979
|
+
editorId.value = id ?? "";
|
|
1980
|
+
editorId.readOnly = id !== null;
|
|
1981
|
+
editorDelete.hidden = id === null;
|
|
1982
|
+
if (id) {
|
|
1983
|
+
const record2 = await api("GET", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`);
|
|
1984
|
+
if (!isRecord2(record2))
|
|
1985
|
+
throw new Error("Record response was not an object");
|
|
1986
|
+
editorValue.value = JSON.stringify(record2.value, null, 2) ?? "";
|
|
1987
|
+
} else {
|
|
1988
|
+
editorValue.value = "{\n \n}";
|
|
1989
|
+
}
|
|
1990
|
+
editor.showModal();
|
|
1991
|
+
}
|
|
1992
|
+
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
1993
|
+
openEditor(null).catch(showError);
|
|
1994
|
+
});
|
|
1995
|
+
el("editor-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
1996
|
+
const submitter = event.submitter;
|
|
1997
|
+
if (!(submitter instanceof HTMLButtonElement) || submitter.value !== "save")
|
|
1998
|
+
return;
|
|
1999
|
+
event.preventDefault();
|
|
2000
|
+
let value;
|
|
2001
|
+
try {
|
|
2002
|
+
value = readJson2(editorValue.value);
|
|
2003
|
+
} catch {
|
|
2004
|
+
showError(new Error("Value is not JSON"));
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
2007
|
+
const id = editorId.value.trim();
|
|
2008
|
+
const path = `/state/${encodeURIComponent(state.collection)}`;
|
|
2009
|
+
const request = state.editing ? api("PUT", `${path}/${encodeURIComponent(id)}`, { value }) : api("POST", path, { id: id || void 0, value });
|
|
2010
|
+
request.then(() => {
|
|
2011
|
+
editor.close();
|
|
2012
|
+
return renderState();
|
|
2013
|
+
}).catch(showError);
|
|
2014
|
+
});
|
|
2015
|
+
editorDelete.addEventListener("click", () => {
|
|
2016
|
+
const id = state.editing;
|
|
2017
|
+
if (!id)
|
|
2018
|
+
return;
|
|
2019
|
+
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2020
|
+
editor.close();
|
|
2021
|
+
return renderState();
|
|
2022
|
+
}).catch(showError);
|
|
2023
|
+
});
|
|
2024
|
+
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2025
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2026
|
+
continue;
|
|
2027
|
+
button.addEventListener("click", () => {
|
|
2028
|
+
const raw = button.dataset.advance;
|
|
2029
|
+
if (raw === void 0)
|
|
2030
|
+
return;
|
|
2031
|
+
api("POST", "/clock", { advance: Number(raw) }).then(() => renderClock()).catch(showError);
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
el("freeze", HTMLButtonElement).addEventListener("click", () => {
|
|
2035
|
+
const frozen = el("freeze", HTMLButtonElement).dataset.frozen === "1";
|
|
2036
|
+
api("POST", "/clock", { freeze: !frozen }).then(() => renderClock()).catch(showError);
|
|
2037
|
+
});
|
|
2038
|
+
el("clock-reset", HTMLButtonElement).addEventListener("click", () => {
|
|
2039
|
+
api("POST", "/clock", { reset: true }).then(() => renderClock()).catch(showError);
|
|
2040
|
+
});
|
|
2041
|
+
el("clock-set", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2042
|
+
event.preventDefault();
|
|
2043
|
+
const raw = el("clock-instant", HTMLInputElement).value.trim();
|
|
2044
|
+
const set = /^-?\d+$/.test(raw) ? Number(raw) : raw;
|
|
2045
|
+
api("POST", "/clock", { set }).then(() => renderClock()).catch(showError);
|
|
2046
|
+
});
|
|
2047
|
+
el("fault-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2048
|
+
event.preventDefault();
|
|
2049
|
+
const preset = el("fault-preset", HTMLSelectElement).value;
|
|
2050
|
+
if (preset !== "") {
|
|
2051
|
+
api("POST", "/faults", { preset }).then(() => renderFaults()).catch(showError);
|
|
2052
|
+
return;
|
|
2053
|
+
}
|
|
2054
|
+
const status = Number(el("fault-status", HTMLInputElement).value);
|
|
2055
|
+
if (!Number.isFinite(status)) {
|
|
2056
|
+
showError(new Error("Status must be a number, or choose a preset"));
|
|
2057
|
+
return;
|
|
2058
|
+
}
|
|
2059
|
+
const path = el("fault-path", HTMLInputElement).value;
|
|
2060
|
+
const method = el("fault-method", HTMLInputElement).value;
|
|
2061
|
+
api("POST", "/faults", {
|
|
2062
|
+
status,
|
|
2063
|
+
...path !== "" ? { pathPrefix: path } : {},
|
|
2064
|
+
...method !== "" ? { method } : {}
|
|
2065
|
+
}).then(() => renderFaults()).catch(showError);
|
|
2066
|
+
});
|
|
2067
|
+
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2068
|
+
api("DELETE", "/faults").then(() => renderFaults()).catch(showError);
|
|
2069
|
+
});
|
|
2070
|
+
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2071
|
+
api("DELETE", "/requests").then(() => renderJournal()).catch(showError);
|
|
2072
|
+
});
|
|
2073
|
+
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2074
|
+
const target = event.target;
|
|
2075
|
+
if (!(target instanceof Element))
|
|
2076
|
+
return;
|
|
2077
|
+
const button = target.closest("[data-route]");
|
|
2078
|
+
if (!(button instanceof HTMLElement))
|
|
2079
|
+
return;
|
|
2080
|
+
const route = button.dataset.route;
|
|
2081
|
+
if (route === void 0)
|
|
2082
|
+
return;
|
|
2083
|
+
state.route = route;
|
|
2084
|
+
el("route-title", HTMLElement).textContent = route;
|
|
2085
|
+
const method = route.split(" ")[0] ?? "";
|
|
2086
|
+
el("route-body", HTMLTextAreaElement).disabled = method === "GET" || method === "DELETE";
|
|
2087
|
+
});
|
|
2088
|
+
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2089
|
+
event.preventDefault();
|
|
2090
|
+
const send = async () => {
|
|
2091
|
+
if (state.route === null)
|
|
2092
|
+
throw new Error("Choose a route first");
|
|
2093
|
+
const parts = state.route.split(" ");
|
|
2094
|
+
const method = parts[0];
|
|
2095
|
+
const path = parts.slice(1).join(" ");
|
|
2096
|
+
if (method === void 0)
|
|
2097
|
+
throw new Error("Choose a route first");
|
|
2098
|
+
const concrete = path.replace(/:([A-Za-z0-9_]+)/g, (_match, key) => prompt(`Value for ${key}`) || "");
|
|
2099
|
+
let body;
|
|
2100
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
2101
|
+
if (method !== "GET" && method !== "DELETE" && routeBody.value.trim() !== "") {
|
|
2102
|
+
try {
|
|
2103
|
+
body = readJson2(routeBody.value);
|
|
2104
|
+
} catch {
|
|
2105
|
+
throw new Error("Body is not JSON");
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
const data = await api(method, concrete, body);
|
|
2109
|
+
el("route-result", HTMLElement).textContent = JSON.stringify(data, null, 2);
|
|
2110
|
+
};
|
|
2111
|
+
send().catch(showError);
|
|
2112
|
+
});
|
|
2113
|
+
const decodePanel = (value) => {
|
|
2114
|
+
if (!isRecord2(value) || typeof value.id !== "string" || typeof value.title !== "string") {
|
|
2115
|
+
return void 0;
|
|
2116
|
+
}
|
|
2117
|
+
const panel = {
|
|
2118
|
+
id: value.id,
|
|
2119
|
+
title: value.title,
|
|
2120
|
+
description: typeof value.description === "string" ? value.description : "",
|
|
2121
|
+
html: typeof value.html === "string" ? value.html : ""
|
|
2122
|
+
};
|
|
2123
|
+
if (typeof value.script === "string")
|
|
2124
|
+
panel.script = value.script;
|
|
2125
|
+
return panel;
|
|
2126
|
+
};
|
|
2127
|
+
const mountPanels = (panels) => {
|
|
2128
|
+
const nav = el("nav", HTMLElement);
|
|
2129
|
+
const views = el("views", HTMLElement);
|
|
2130
|
+
for (const panel of panels) {
|
|
2131
|
+
const button = document.createElement("button");
|
|
2132
|
+
button.type = "button";
|
|
2133
|
+
button.dataset.view = panel.id;
|
|
2134
|
+
button.textContent = panel.title;
|
|
2135
|
+
nav.append(button);
|
|
2136
|
+
const section = document.createElement("section");
|
|
2137
|
+
section.className = "view";
|
|
2138
|
+
section.id = `view-${panel.id}`;
|
|
2139
|
+
section.hidden = true;
|
|
2140
|
+
const title = document.createElement("h2");
|
|
2141
|
+
title.textContent = panel.title;
|
|
2142
|
+
const lede = document.createElement("p");
|
|
2143
|
+
lede.className = "lede";
|
|
2144
|
+
lede.textContent = panel.description;
|
|
2145
|
+
const body = document.createElement("div");
|
|
2146
|
+
body.className = "panel-body";
|
|
2147
|
+
body.id = `panel-${panel.id}`;
|
|
2148
|
+
body.innerHTML = panel.html;
|
|
2149
|
+
section.append(title, lede, body);
|
|
2150
|
+
views.append(section);
|
|
2151
|
+
if (panel.script === void 0)
|
|
2152
|
+
continue;
|
|
2153
|
+
try {
|
|
2154
|
+
const compiled = new Function("root", "api", panel.script);
|
|
2155
|
+
if (!isPanelScript(compiled))
|
|
2156
|
+
throw new Error("panel script did not compile");
|
|
2157
|
+
compiled(body, panelApi);
|
|
2158
|
+
} catch (error2) {
|
|
2159
|
+
const note = document.createElement("p");
|
|
2160
|
+
note.className = "banner show";
|
|
2161
|
+
note.textContent = error2 instanceof Error ? error2.message : String(error2);
|
|
2162
|
+
body.append(note);
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
};
|
|
2166
|
+
startAdminBrand({
|
|
2167
|
+
document,
|
|
2168
|
+
$: (id) => document.getElementById(id),
|
|
2169
|
+
params,
|
|
2170
|
+
location: { href: location.href, origin: location.origin },
|
|
2171
|
+
fetch,
|
|
2172
|
+
brandsUrl: config.brandsUrl
|
|
2173
|
+
});
|
|
2174
|
+
loadNamespaces().then(() => api("GET", "/ui/manifest")).then((manifest) => {
|
|
2175
|
+
const panels = requireList(isRecord2(manifest) ? manifest.panels : void 0, "Panels").flatMap((panel) => {
|
|
2176
|
+
const decoded = decodePanel(panel);
|
|
2177
|
+
return decoded === void 0 ? [] : [decoded];
|
|
2178
|
+
});
|
|
2179
|
+
mountPanels(panels);
|
|
2180
|
+
return refresh();
|
|
2181
|
+
}).catch(showError);
|
|
2182
|
+
}
|
|
2183
|
+
var clientPrelude = [isRecord2, mountAdminBrand, startAdminBrand, faultPresetList, bootAdmin].map((fn) => fn.toString()).join("\n");
|
|
2184
|
+
var adminClientSource = (config) => {
|
|
2185
|
+
const payload = JSON.stringify(config).replace(/</g, "\\u003c");
|
|
2186
|
+
return `<script>
|
|
2187
|
+
${clientPrelude}
|
|
2188
|
+
bootAdmin(${payload});
|
|
2189
|
+
</script>`;
|
|
2190
|
+
};
|
|
2191
|
+
|
|
966
2192
|
// ../core/dist/surface.js
|
|
967
2193
|
var defineMock = (runtime) => runtime;
|
|
968
2194
|
var STANDARD_ADMIN_ROUTES = [
|
|
@@ -1031,94 +2257,106 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
1031
2257
|
return "'";
|
|
1032
2258
|
}
|
|
1033
2259
|
});
|
|
2260
|
+
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
1034
2261
|
var renderAdminDocument = (service) => {
|
|
1035
2262
|
const name = escapeHtml(service);
|
|
1036
|
-
const
|
|
2263
|
+
const script = adminClientSource({
|
|
2264
|
+
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
2265
|
+
adminPrefix: ADMIN_PREFIX,
|
|
2266
|
+
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
2267
|
+
brandsUrl: ADMIN_BRANDS_URL,
|
|
2268
|
+
service
|
|
2269
|
+
});
|
|
1037
2270
|
return `<!DOCTYPE html>
|
|
1038
2271
|
<html lang="en">
|
|
1039
2272
|
<head>
|
|
1040
2273
|
<meta charset="utf-8">
|
|
1041
2274
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1042
2275
|
<meta name="color-scheme" content="light dark">
|
|
1043
|
-
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='
|
|
2276
|
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='2' fill='%2327272a'/%3E%3C/svg%3E">
|
|
1044
2277
|
<title>${name} admin</title>
|
|
1045
2278
|
<style>
|
|
2279
|
+
${CSS_RESET}
|
|
1046
2280
|
:root {
|
|
1047
2281
|
color-scheme: light;
|
|
1048
|
-
--bg: #
|
|
2282
|
+
--bg: #fafafa;
|
|
1049
2283
|
--bg-elev: #ffffff;
|
|
1050
|
-
--bg-muted: #
|
|
1051
|
-
--bg-inset: #
|
|
1052
|
-
--line: #
|
|
1053
|
-
--line-strong: #
|
|
1054
|
-
--ink: #
|
|
1055
|
-
--muted: #
|
|
1056
|
-
--faint: #
|
|
1057
|
-
--accent: #
|
|
1058
|
-
--accent-ink: #
|
|
1059
|
-
--accent-soft: #
|
|
1060
|
-
--ok: #
|
|
1061
|
-
--ok-soft: #
|
|
1062
|
-
--warn: #
|
|
1063
|
-
--warn-soft: #
|
|
1064
|
-
--err: #
|
|
1065
|
-
--err-soft: #
|
|
1066
|
-
--shadow:
|
|
1067
|
-
--sans: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2284
|
+
--bg-muted: #f4f4f5;
|
|
2285
|
+
--bg-inset: #f4f4f5;
|
|
2286
|
+
--line: #dedee3;
|
|
2287
|
+
--line-strong: #c4c4cc;
|
|
2288
|
+
--ink: #18181b;
|
|
2289
|
+
--muted: #62626b;
|
|
2290
|
+
--faint: #71717a;
|
|
2291
|
+
--accent: #27272a;
|
|
2292
|
+
--accent-ink: #ffffff;
|
|
2293
|
+
--accent-soft: #f4f4f5;
|
|
2294
|
+
--ok: #166534;
|
|
2295
|
+
--ok-soft: #f0fdf4;
|
|
2296
|
+
--warn: #a16207;
|
|
2297
|
+
--warn-soft: #fefce8;
|
|
2298
|
+
--err: #a82d32;
|
|
2299
|
+
--err-soft: #fef2f2;
|
|
2300
|
+
--shadow: none;
|
|
2301
|
+
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
1068
2302
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
1069
|
-
--radius:
|
|
1070
|
-
--header:
|
|
1071
|
-
--nav:
|
|
2303
|
+
--radius: 10px;
|
|
2304
|
+
--header: 56px;
|
|
2305
|
+
--nav: 200px;
|
|
1072
2306
|
}
|
|
1073
2307
|
:root[data-theme="dark"] {
|
|
1074
2308
|
color-scheme: dark;
|
|
1075
|
-
--bg: #
|
|
1076
|
-
--bg-elev: #
|
|
1077
|
-
--bg-muted: #
|
|
1078
|
-
--bg-inset: #
|
|
1079
|
-
--line: #
|
|
1080
|
-
--line-strong: #
|
|
1081
|
-
--ink: #
|
|
1082
|
-
--muted: #
|
|
1083
|
-
--faint: #
|
|
1084
|
-
--accent: #
|
|
1085
|
-
--accent-ink: #
|
|
1086
|
-
--accent-soft: #
|
|
1087
|
-
--ok: #
|
|
1088
|
-
--ok-soft: #
|
|
1089
|
-
--warn: #
|
|
1090
|
-
--warn-soft: #
|
|
1091
|
-
--err: #
|
|
1092
|
-
--err-soft: #
|
|
1093
|
-
--shadow:
|
|
2309
|
+
--bg: #111113;
|
|
2310
|
+
--bg-elev: #19191c;
|
|
2311
|
+
--bg-muted: #242428;
|
|
2312
|
+
--bg-inset: #111113;
|
|
2313
|
+
--line: #36363c;
|
|
2314
|
+
--line-strong: #52525b;
|
|
2315
|
+
--ink: #f4f4f5;
|
|
2316
|
+
--muted: #a9a9b2;
|
|
2317
|
+
--faint: #71717a;
|
|
2318
|
+
--accent: #e4e4e7;
|
|
2319
|
+
--accent-ink: #18181b;
|
|
2320
|
+
--accent-soft: #242428;
|
|
2321
|
+
--ok: #86efac;
|
|
2322
|
+
--ok-soft: #14241b;
|
|
2323
|
+
--warn: #fde68a;
|
|
2324
|
+
--warn-soft: #2a2410;
|
|
2325
|
+
--err: #ffaaaa;
|
|
2326
|
+
--err-soft: #2c1516;
|
|
2327
|
+
--shadow: none;
|
|
1094
2328
|
}
|
|
1095
2329
|
@media (prefers-color-scheme: dark) {
|
|
1096
2330
|
:root:not([data-theme="light"]) {
|
|
1097
2331
|
color-scheme: dark;
|
|
1098
|
-
--bg: #
|
|
1099
|
-
--bg-elev: #
|
|
1100
|
-
--bg-muted: #
|
|
1101
|
-
--bg-inset: #
|
|
1102
|
-
--line: #
|
|
1103
|
-
--line-strong: #
|
|
1104
|
-
--ink: #
|
|
1105
|
-
--muted: #
|
|
1106
|
-
--faint: #
|
|
1107
|
-
--accent: #
|
|
1108
|
-
--accent-ink: #
|
|
1109
|
-
--accent-soft: #
|
|
1110
|
-
--ok: #
|
|
1111
|
-
--ok-soft: #
|
|
1112
|
-
--warn: #
|
|
1113
|
-
--warn-soft: #
|
|
1114
|
-
--err: #
|
|
1115
|
-
--err-soft: #
|
|
1116
|
-
--shadow:
|
|
2332
|
+
--bg: #111113;
|
|
2333
|
+
--bg-elev: #19191c;
|
|
2334
|
+
--bg-muted: #242428;
|
|
2335
|
+
--bg-inset: #111113;
|
|
2336
|
+
--line: #36363c;
|
|
2337
|
+
--line-strong: #52525b;
|
|
2338
|
+
--ink: #f4f4f5;
|
|
2339
|
+
--muted: #a9a9b2;
|
|
2340
|
+
--faint: #71717a;
|
|
2341
|
+
--accent: #e4e4e7;
|
|
2342
|
+
--accent-ink: #18181b;
|
|
2343
|
+
--accent-soft: #242428;
|
|
2344
|
+
--ok: #86efac;
|
|
2345
|
+
--ok-soft: #14241b;
|
|
2346
|
+
--warn: #fde68a;
|
|
2347
|
+
--warn-soft: #2a2410;
|
|
2348
|
+
--err: #ffaaaa;
|
|
2349
|
+
--err-soft: #2c1516;
|
|
2350
|
+
--shadow: none;
|
|
1117
2351
|
}
|
|
1118
2352
|
}
|
|
1119
|
-
|
|
1120
|
-
html
|
|
2353
|
+
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
2354
|
+
html { height: 100%; }
|
|
1121
2355
|
body {
|
|
2356
|
+
margin: 0;
|
|
2357
|
+
min-height: 100%;
|
|
2358
|
+
display: flex;
|
|
2359
|
+
flex-direction: column;
|
|
1122
2360
|
font-family: var(--sans);
|
|
1123
2361
|
background: var(--bg);
|
|
1124
2362
|
color: var(--ink);
|
|
@@ -1127,24 +2365,37 @@ var renderAdminDocument = (service) => {
|
|
|
1127
2365
|
padding-right: env(safe-area-inset-right);
|
|
1128
2366
|
-webkit-text-size-adjust: 100%;
|
|
1129
2367
|
}
|
|
1130
|
-
button, input, select, textarea { font: inherit; color: inherit; }
|
|
1131
|
-
button { cursor: pointer; }
|
|
1132
2368
|
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
1133
|
-
|
|
1134
|
-
* { scroll-behavior: auto !important; transition: none !important; }
|
|
1135
|
-
}
|
|
1136
|
-
.app { min-height: 100%; display: flex; flex-direction: column; }
|
|
2369
|
+
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
1137
2370
|
header.top {
|
|
1138
2371
|
position: sticky; top: 0; z-index: 5;
|
|
1139
2372
|
display: flex; align-items: center; gap: 12px;
|
|
1140
2373
|
min-height: var(--header); padding: 10px 16px;
|
|
1141
|
-
background:
|
|
2374
|
+
background: var(--bg-elev);
|
|
1142
2375
|
border-bottom: 1px solid var(--line);
|
|
1143
|
-
|
|
1144
|
-
}
|
|
1145
|
-
.brand {
|
|
1146
|
-
|
|
1147
|
-
|
|
2376
|
+
min-width: 0; max-width: 100%;
|
|
2377
|
+
}
|
|
2378
|
+
.brand {
|
|
2379
|
+
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
2380
|
+
min-width: 0; max-width: 100%; margin-right: auto;
|
|
2381
|
+
}
|
|
2382
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; }
|
|
2383
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.01em; }
|
|
2384
|
+
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
2385
|
+
.vendor {
|
|
2386
|
+
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
2387
|
+
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
2388
|
+
}
|
|
2389
|
+
.vendor[hidden] { display: none; }
|
|
2390
|
+
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
2391
|
+
.vendor img {
|
|
2392
|
+
width: 16px; height: 16px; padding: 2px; box-sizing: content-box;
|
|
2393
|
+
border-radius: 4px; border: 1px solid var(--line); background: #fff; object-fit: contain;
|
|
2394
|
+
}
|
|
2395
|
+
.vendor-name { color: var(--ink); font-weight: 600; }
|
|
2396
|
+
.vendor-sep { color: var(--faint); }
|
|
2397
|
+
.vendor-link { color: inherit; text-decoration: none; }
|
|
2398
|
+
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
1148
2399
|
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
|
1149
2400
|
label.field { display: flex; flex-direction: column; gap: 2px; font-size: 11px; color: var(--faint); }
|
|
1150
2401
|
label.field input, label.field select, .controls input[type="password"] {
|
|
@@ -1162,7 +2413,7 @@ var renderAdminDocument = (service) => {
|
|
|
1162
2413
|
record or the full nav row would widen the page past the viewport. */
|
|
1163
2414
|
.layout {
|
|
1164
2415
|
display: grid; grid-template-columns: var(--nav) minmax(0, 1fr);
|
|
1165
|
-
flex: 1
|
|
2416
|
+
flex: 1 0 auto; min-width: 0;
|
|
1166
2417
|
}
|
|
1167
2418
|
nav.side {
|
|
1168
2419
|
border-right: 1px solid var(--line); padding: 12px;
|
|
@@ -1216,8 +2467,8 @@ var renderAdminDocument = (service) => {
|
|
|
1216
2467
|
.banner.show { display: block; }
|
|
1217
2468
|
.empty { color: var(--muted); padding: 24px 8px; }
|
|
1218
2469
|
dialog {
|
|
1219
|
-
border: 1px solid var(--line); border-radius:
|
|
1220
|
-
color: var(--ink); width: min(640px, calc(100vw - 24px)); box-shadow:
|
|
2470
|
+
border: 1px solid var(--line); border-radius: var(--radius); padding: 0; background: var(--bg-elev);
|
|
2471
|
+
color: var(--ink); width: min(640px, calc(100vw - 24px)); box-shadow: none;
|
|
1221
2472
|
}
|
|
1222
2473
|
dialog::backdrop { background: rgb(20 19 17 / 0.45); }
|
|
1223
2474
|
dialog form, .dialog-body { padding: 16px; display: flex; flex-direction: column; gap: 10px; }
|
|
@@ -1244,7 +2495,7 @@ var renderAdminDocument = (service) => {
|
|
|
1244
2495
|
padding-top: max(10px, env(safe-area-inset-top));
|
|
1245
2496
|
}
|
|
1246
2497
|
input, select, textarea { font-size: 16px; }
|
|
1247
|
-
.controls { width: 100%; }
|
|
2498
|
+
.brand, .vendor, .controls { width: 100%; }
|
|
1248
2499
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
1249
2500
|
.layout { grid-template-columns: minmax(0, 1fr); }
|
|
1250
2501
|
nav.side {
|
|
@@ -1275,8 +2526,11 @@ var renderAdminDocument = (service) => {
|
|
|
1275
2526
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
1276
2527
|
<header class="top">
|
|
1277
2528
|
<div class="brand">
|
|
1278
|
-
<
|
|
1279
|
-
|
|
2529
|
+
<div class="brand-id">
|
|
2530
|
+
<strong>${name}</strong>
|
|
2531
|
+
<span>Admin</span>
|
|
2532
|
+
</div>
|
|
2533
|
+
<div class="vendor" id="vendor" hidden></div>
|
|
1280
2534
|
</div>
|
|
1281
2535
|
<div class="controls">
|
|
1282
2536
|
<label class="field">Namespace
|
|
@@ -1406,460 +2660,7 @@ var renderAdminDocument = (service) => {
|
|
|
1406
2660
|
</div>
|
|
1407
2661
|
</form>
|
|
1408
2662
|
</dialog>
|
|
1409
|
-
|
|
1410
|
-
(() => {
|
|
1411
|
-
const STANDARD = new Set(${standard})
|
|
1412
|
-
const state = {
|
|
1413
|
-
namespace: "default",
|
|
1414
|
-
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
1415
|
-
view: "overview",
|
|
1416
|
-
collection: "",
|
|
1417
|
-
shape: null,
|
|
1418
|
-
route: null,
|
|
1419
|
-
editing: null
|
|
1420
|
-
}
|
|
1421
|
-
const $ = (id) => document.getElementById(id)
|
|
1422
|
-
const banner = $("banner")
|
|
1423
|
-
const keyInput = $("admin-key")
|
|
1424
|
-
keyInput.value = state.key
|
|
1425
|
-
const params = new URLSearchParams(location.search)
|
|
1426
|
-
if (params.get("key")) {
|
|
1427
|
-
state.key = params.get("key")
|
|
1428
|
-
keyInput.value = state.key
|
|
1429
|
-
sessionStorage.setItem("mockingbird-admin-key", state.key)
|
|
1430
|
-
params.delete("key")
|
|
1431
|
-
const next = params.toString()
|
|
1432
|
-
history.replaceState(null, "", location.pathname + (next ? "?" + next : ""))
|
|
1433
|
-
}
|
|
1434
|
-
if (params.get("namespace")) state.namespace = params.get("namespace")
|
|
1435
|
-
|
|
1436
|
-
const showError = (error) => {
|
|
1437
|
-
banner.textContent = error instanceof Error ? error.message : String(error)
|
|
1438
|
-
banner.classList.add("show")
|
|
1439
|
-
}
|
|
1440
|
-
const clearError = () => banner.classList.remove("show")
|
|
1441
|
-
|
|
1442
|
-
async function originGet(path) {
|
|
1443
|
-
const response = await fetch(path, { headers: { accept: "application/json" } })
|
|
1444
|
-
if (!response.ok) throw new Error(response.status + " " + path)
|
|
1445
|
-
return response.json()
|
|
1446
|
-
}
|
|
1447
|
-
async function api(method, path, body) {
|
|
1448
|
-
const headers = { accept: "application/json" }
|
|
1449
|
-
if (state.key) headers["${ADMIN_KEY_HEADER}"] = state.key
|
|
1450
|
-
if (state.namespace) headers["x-mockingbird-namespace"] = state.namespace
|
|
1451
|
-
if (body !== undefined) headers["content-type"] = "application/json"
|
|
1452
|
-
const response = await fetch("${ADMIN_PREFIX}" + path, {
|
|
1453
|
-
method,
|
|
1454
|
-
headers,
|
|
1455
|
-
body: body === undefined ? undefined : JSON.stringify(body)
|
|
1456
|
-
})
|
|
1457
|
-
const text = await response.text()
|
|
1458
|
-
let data = null
|
|
1459
|
-
if (text) {
|
|
1460
|
-
try { data = JSON.parse(text) } catch { data = { raw: text } }
|
|
1461
|
-
}
|
|
1462
|
-
if (response.status === 401) {
|
|
1463
|
-
keyInput.focus()
|
|
1464
|
-
throw new Error((data && data.error && data.error.message) || "Admin key required")
|
|
1465
|
-
}
|
|
1466
|
-
if (!response.ok) {
|
|
1467
|
-
throw new Error((data && data.error && data.error.message) || (response.status + " " + method + " " + path))
|
|
1468
|
-
}
|
|
1469
|
-
return data
|
|
1470
|
-
}
|
|
1471
|
-
const panelApi = {
|
|
1472
|
-
get: (path) => api("GET", path),
|
|
1473
|
-
send: (method, path, body) => api(method, path, body),
|
|
1474
|
-
namespace: () => state.namespace
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
|
-
function setTheme(theme) {
|
|
1478
|
-
if (theme) document.documentElement.dataset.theme = theme
|
|
1479
|
-
else delete document.documentElement.dataset.theme
|
|
1480
|
-
}
|
|
1481
|
-
const storedTheme = localStorage.getItem("mockingbird-admin-theme")
|
|
1482
|
-
if (storedTheme) setTheme(storedTheme)
|
|
1483
|
-
$("theme").addEventListener("click", () => {
|
|
1484
|
-
const current = document.documentElement.dataset.theme
|
|
1485
|
-
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches
|
|
1486
|
-
const next = dark ? "light" : "dark"
|
|
1487
|
-
localStorage.setItem("mockingbird-admin-theme", next)
|
|
1488
|
-
setTheme(next)
|
|
1489
|
-
})
|
|
1490
|
-
|
|
1491
|
-
function show(view) {
|
|
1492
|
-
state.view = view
|
|
1493
|
-
for (const button of document.querySelectorAll("#nav button")) {
|
|
1494
|
-
button.setAttribute("aria-current", button.dataset.view === view ? "true" : "false")
|
|
1495
|
-
}
|
|
1496
|
-
for (const section of document.querySelectorAll("main .view")) {
|
|
1497
|
-
section.hidden = section.id !== "view-" + view
|
|
1498
|
-
}
|
|
1499
|
-
}
|
|
1500
|
-
document.getElementById("nav").addEventListener("click", (event) => {
|
|
1501
|
-
const button = event.target.closest("button[data-view]")
|
|
1502
|
-
if (!button) return
|
|
1503
|
-
show(button.dataset.view)
|
|
1504
|
-
refresh().catch(showError)
|
|
1505
|
-
})
|
|
1506
|
-
|
|
1507
|
-
function cards(items) {
|
|
1508
|
-
return '<div class="grid">' + items.map((item) =>
|
|
1509
|
-
'<article class="card"><div class="k">' + item.k + '</div><div class="v">' + item.v + '</div></article>'
|
|
1510
|
-
).join("") + "</div>"
|
|
1511
|
-
}
|
|
1512
|
-
function table(columns, rows) {
|
|
1513
|
-
if (rows.length === 0) return '<p class="empty">Nothing here yet.</p>'
|
|
1514
|
-
const head = "<tr>" + columns.map((column) => "<th>" + column.label + "</th>").join("") + "</tr>"
|
|
1515
|
-
const body = rows.map((row) => "<tr" + (row.attrs || "") + ">" + columns.map((column) =>
|
|
1516
|
-
'<td data-label="' + column.label + '">' + column.cell(row) + "</td>"
|
|
1517
|
-
).join("") + "</tr>").join("")
|
|
1518
|
-
return "<table><thead>" + head + "</thead><tbody>" + body + "</tbody></table>"
|
|
1519
|
-
}
|
|
1520
|
-
const esc = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => ({
|
|
1521
|
-
"&": "&", "<": "<", ">": ">", '"': """, "'": "'"
|
|
1522
|
-
}[char]))
|
|
1523
|
-
const preview = (value) => {
|
|
1524
|
-
const text = JSON.stringify(value)
|
|
1525
|
-
return esc(text.length > 180 ? text.slice(0, 180) + "\u2026" : text)
|
|
1526
|
-
}
|
|
1527
|
-
const when = (epoch) => {
|
|
1528
|
-
const date = new Date(epoch)
|
|
1529
|
-
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString()
|
|
1530
|
-
}
|
|
1531
|
-
const clockCard = (epoch) => {
|
|
1532
|
-
const date = new Date(epoch)
|
|
1533
|
-
if (Number.isNaN(date.getTime())) return esc(String(epoch))
|
|
1534
|
-
const iso = date.toISOString()
|
|
1535
|
-
return esc(iso.slice(0, 10)) + '<span class="clock-time">' + esc(iso.slice(11, 19)) + " UTC</span>"
|
|
1536
|
-
}
|
|
1537
|
-
|
|
1538
|
-
async function loadNamespaces() {
|
|
1539
|
-
const data = await api("GET", "/namespaces")
|
|
1540
|
-
const select = $("namespace")
|
|
1541
|
-
const names = data.namespaces && data.namespaces.length ? data.namespaces : [data.default || "default"]
|
|
1542
|
-
if (!names.includes(state.namespace)) names.unshift(state.namespace)
|
|
1543
|
-
select.innerHTML = names.map((name) =>
|
|
1544
|
-
'<option value="' + esc(name) + '">' + esc(name) + "</option>"
|
|
1545
|
-
).join("")
|
|
1546
|
-
select.value = state.namespace
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
async function renderOverview() {
|
|
1550
|
-
const [health, shape, clock, journal] = await Promise.all([
|
|
1551
|
-
originGet("/health"),
|
|
1552
|
-
api("GET", "/state"),
|
|
1553
|
-
api("GET", "/clock"),
|
|
1554
|
-
api("GET", "/requests?limit=8")
|
|
1555
|
-
])
|
|
1556
|
-
state.shape = shape
|
|
1557
|
-
$("overview-cards").innerHTML = cards([
|
|
1558
|
-
{ k: "Service", v: esc(health.service || "${name}") },
|
|
1559
|
-
{ k: "Status", v: esc(health.status || "ok") },
|
|
1560
|
-
{ k: "Namespace", v: esc(state.namespace) },
|
|
1561
|
-
{ k: "Collections", v: String(shape.collections.length) },
|
|
1562
|
-
{ k: "Records", v: String(shape.collections.reduce((sum, item) => sum + item.count, 0)) },
|
|
1563
|
-
{ k: "Clock", v: clockCard(clock.now) }
|
|
1564
|
-
])
|
|
1565
|
-
const requests = (journal && journal.requests) || []
|
|
1566
|
-
$("overview-requests").innerHTML = table(
|
|
1567
|
-
[
|
|
1568
|
-
{ label: "Method", cell: (row) => esc(row.method) },
|
|
1569
|
-
{ label: "Path", cell: (row) => esc(row.path) },
|
|
1570
|
-
{ label: "Status", cell: (row) => '<span class="status-' + String(row.status).charAt(0) + '">' + esc(row.status) + "</span>" },
|
|
1571
|
-
{ label: "Operation", cell: (row) => esc(row.operationId || "") }
|
|
1572
|
-
],
|
|
1573
|
-
requests
|
|
1574
|
-
)
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
function schemaChips(collection) {
|
|
1578
|
-
if (!collection || collection.fields.length === 0) {
|
|
1579
|
-
return '<p class="empty">No fields yet. Add a record or declare the collection on the service.</p>'
|
|
1580
|
-
}
|
|
1581
|
-
return '<div class="chips">' + collection.fields.map((field) =>
|
|
1582
|
-
'<span class="chip">' + esc(field.name) + ' <i>' + esc(field.kind) + (field.optional ? "?" : "") + "</i></span>"
|
|
1583
|
-
).join("") + "</div>"
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
async function renderState() {
|
|
1587
|
-
const shape = await api("GET", "/state")
|
|
1588
|
-
state.shape = shape
|
|
1589
|
-
const host = $("collections")
|
|
1590
|
-
if (shape.collections.length === 0) {
|
|
1591
|
-
host.innerHTML = '<p class="empty">This namespace has no collections yet.</p>'
|
|
1592
|
-
$("schema").innerHTML = ""
|
|
1593
|
-
$("records").innerHTML = ""
|
|
1594
|
-
return
|
|
1595
|
-
}
|
|
1596
|
-
if (!shape.collections.some((item) => item.name === state.collection)) {
|
|
1597
|
-
state.collection = shape.collections[0].name
|
|
1598
|
-
}
|
|
1599
|
-
host.innerHTML = shape.collections.map((item) =>
|
|
1600
|
-
'<button type="button" class="collection" data-collection="' + esc(item.name) + '" aria-current="' +
|
|
1601
|
-
(item.name === state.collection ? "true" : "false") + '"><strong>' + esc(item.label) +
|
|
1602
|
-
'</strong><small>' + esc(item.count) + " \xB7 " + esc(item.source) + "</small></button>"
|
|
1603
|
-
).join("")
|
|
1604
|
-
const selected = shape.collections.find((item) => item.name === state.collection)
|
|
1605
|
-
$("schema").innerHTML = "<h3>" + esc(selected.label) + "</h3>" +
|
|
1606
|
-
(selected.description ? "<p class=" + '"lede">' + esc(selected.description) + "</p>" : "") +
|
|
1607
|
-
schemaChips(selected)
|
|
1608
|
-
const page = await api("GET", "/state/" + encodeURIComponent(state.collection) + "?limit=50")
|
|
1609
|
-
const rows = page.records.map((record) => ({
|
|
1610
|
-
...record,
|
|
1611
|
-
attrs: ' class="clickable" data-id="' + esc(record.id) + '"'
|
|
1612
|
-
}))
|
|
1613
|
-
$("records").innerHTML = table(
|
|
1614
|
-
[
|
|
1615
|
-
{ label: "Id", cell: (row) => "<code>" + esc(row.id) + "</code>" },
|
|
1616
|
-
{ label: "Seq", cell: (row) => esc(row.seq) },
|
|
1617
|
-
{ label: "Value", cell: (row) => preview(row.value) }
|
|
1618
|
-
],
|
|
1619
|
-
rows
|
|
1620
|
-
)
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
async function renderClock() {
|
|
1624
|
-
const clock = await api("GET", "/clock")
|
|
1625
|
-
$("clock-readout").innerHTML = "<div class='k'>Now</div><div class='v'>" + esc(when(clock.now)) +
|
|
1626
|
-
"</div><p>" + esc(clock.now) + " ms \xB7 offset " + esc(clock.offsetMs) + " ms " +
|
|
1627
|
-
(clock.frozen ? "<span class='badge warn'>frozen</span>" : "<span class='badge ok'>live</span>") + "</p>"
|
|
1628
|
-
$("freeze").textContent = clock.frozen ? "Unfreeze" : "Freeze"
|
|
1629
|
-
$("freeze").dataset.frozen = clock.frozen ? "1" : "0"
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
async function renderFaults() {
|
|
1633
|
-
const [faults, presets] = await Promise.all([
|
|
1634
|
-
api("GET", "/faults"),
|
|
1635
|
-
api("GET", "/faults/presets").catch(() => ({ presets: [] }))
|
|
1636
|
-
])
|
|
1637
|
-
const select = $("fault-preset")
|
|
1638
|
-
const current = select.value
|
|
1639
|
-
const list = presets.presets || []
|
|
1640
|
-
select.innerHTML = '<option value="">Custom rule</option>' + list.map((preset) =>
|
|
1641
|
-
'<option value="' + esc(preset.name) + '">' + esc(preset.name) + "</option>"
|
|
1642
|
-
).join("")
|
|
1643
|
-
select.value = current
|
|
1644
|
-
const rules = faults.faults || []
|
|
1645
|
-
$("fault-list").innerHTML = table(
|
|
1646
|
-
[
|
|
1647
|
-
{ label: "Id", cell: (row) => esc(row.id) },
|
|
1648
|
-
{ label: "Match", cell: (row) => esc([row.method, row.pathPrefix, row.operationId].filter(Boolean).join(" ") || "all") },
|
|
1649
|
-
{ label: "Effect", cell: (row) => esc(row.status ?? row.effect ?? (row.drop ? "drop" : row.latencyMs || "")) }
|
|
1650
|
-
],
|
|
1651
|
-
rules
|
|
1652
|
-
)
|
|
1653
|
-
}
|
|
1654
|
-
|
|
1655
|
-
async function renderJournal() {
|
|
1656
|
-
const journal = await api("GET", "/requests?limit=100")
|
|
1657
|
-
$("journal").innerHTML = table(
|
|
1658
|
-
[
|
|
1659
|
-
{ label: "When", cell: (row) => esc(row.at || "") },
|
|
1660
|
-
{ label: "Method", cell: (row) => esc(row.method) },
|
|
1661
|
-
{ label: "Path", cell: (row) => esc(row.path) },
|
|
1662
|
-
{ label: "Status", cell: (row) => esc(row.status) },
|
|
1663
|
-
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
1664
|
-
],
|
|
1665
|
-
journal.requests || []
|
|
1666
|
-
)
|
|
1667
|
-
}
|
|
1668
|
-
|
|
1669
|
-
async function renderRoutes() {
|
|
1670
|
-
const data = await api("GET", "/")
|
|
1671
|
-
const routes = data.routes || []
|
|
1672
|
-
$("route-list").innerHTML = routes.map((route) => {
|
|
1673
|
-
const extra = STANDARD.has(route) ? "" : " <span class='badge'>extension</span>"
|
|
1674
|
-
return '<div class="route"><span class="method">' + esc(route.split(" ")[0]) +
|
|
1675
|
-
'</span><span>' + esc(route.split(" ").slice(1).join(" ")) + extra +
|
|
1676
|
-
'</span><button class="btn" type="button" data-route="' + esc(route) + '">Use</button></div>'
|
|
1677
|
-
}).join("")
|
|
1678
|
-
}
|
|
1679
|
-
|
|
1680
|
-
const renderers = {
|
|
1681
|
-
overview: renderOverview,
|
|
1682
|
-
state: renderState,
|
|
1683
|
-
clock: renderClock,
|
|
1684
|
-
faults: renderFaults,
|
|
1685
|
-
journal: renderJournal,
|
|
1686
|
-
routes: renderRoutes
|
|
1687
|
-
}
|
|
1688
|
-
|
|
1689
|
-
async function refresh() {
|
|
1690
|
-
clearError()
|
|
1691
|
-
if (state.view in renderers) await renderers[state.view]()
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
$("namespace").addEventListener("change", () => {
|
|
1695
|
-
state.namespace = $("namespace").value
|
|
1696
|
-
refresh().catch(showError)
|
|
1697
|
-
})
|
|
1698
|
-
keyInput.addEventListener("change", () => {
|
|
1699
|
-
state.key = keyInput.value.trim()
|
|
1700
|
-
sessionStorage.setItem("mockingbird-admin-key", state.key)
|
|
1701
|
-
refresh().catch(showError)
|
|
1702
|
-
})
|
|
1703
|
-
$("refresh").addEventListener("click", () => refresh().catch(showError))
|
|
1704
|
-
|
|
1705
|
-
$("collections").addEventListener("click", (event) => {
|
|
1706
|
-
const button = event.target.closest("[data-collection]")
|
|
1707
|
-
if (!button) return
|
|
1708
|
-
state.collection = button.dataset.collection
|
|
1709
|
-
renderState().catch(showError)
|
|
1710
|
-
})
|
|
1711
|
-
$("records").addEventListener("click", (event) => {
|
|
1712
|
-
const row = event.target.closest("[data-id]")
|
|
1713
|
-
if (!row) return
|
|
1714
|
-
openEditor(row.dataset.id).catch(showError)
|
|
1715
|
-
})
|
|
1716
|
-
|
|
1717
|
-
const editor = $("editor")
|
|
1718
|
-
async function openEditor(id) {
|
|
1719
|
-
state.editing = id || null
|
|
1720
|
-
$("editor-title").textContent = id ? "Edit record" : "New record"
|
|
1721
|
-
$("editor-id").value = id || ""
|
|
1722
|
-
$("editor-id").readOnly = Boolean(id)
|
|
1723
|
-
$("editor-delete").hidden = !id
|
|
1724
|
-
if (id) {
|
|
1725
|
-
const record = await api("GET", "/state/" + encodeURIComponent(state.collection) + "/" + encodeURIComponent(id))
|
|
1726
|
-
$("editor-value").value = JSON.stringify(record.value, null, 2)
|
|
1727
|
-
} else {
|
|
1728
|
-
$("editor-value").value = "{\\n \\n}"
|
|
1729
|
-
}
|
|
1730
|
-
editor.showModal()
|
|
1731
|
-
}
|
|
1732
|
-
$("new-record").addEventListener("click", () => openEditor(null).catch(showError))
|
|
1733
|
-
$("editor-form").addEventListener("submit", (event) => {
|
|
1734
|
-
const submitter = event.submitter
|
|
1735
|
-
if (!submitter || submitter.value !== "save") return
|
|
1736
|
-
event.preventDefault()
|
|
1737
|
-
let value
|
|
1738
|
-
try { value = JSON.parse($("editor-value").value) }
|
|
1739
|
-
catch { showError(new Error("Value is not JSON")); return }
|
|
1740
|
-
const id = $("editor-id").value.trim()
|
|
1741
|
-
const path = "/state/" + encodeURIComponent(state.collection)
|
|
1742
|
-
const request = state.editing
|
|
1743
|
-
? api("PUT", path + "/" + encodeURIComponent(id), { value })
|
|
1744
|
-
: api("POST", path, { id: id || undefined, value })
|
|
1745
|
-
request.then(() => { editor.close(); return renderState() }).catch(showError)
|
|
1746
|
-
})
|
|
1747
|
-
$("editor-delete").addEventListener("click", () => {
|
|
1748
|
-
const id = state.editing
|
|
1749
|
-
if (!id) return
|
|
1750
|
-
api("DELETE", "/state/" + encodeURIComponent(state.collection) + "/" + encodeURIComponent(id))
|
|
1751
|
-
.then(() => { editor.close(); return renderState() })
|
|
1752
|
-
.catch(showError)
|
|
1753
|
-
})
|
|
1754
|
-
|
|
1755
|
-
document.querySelectorAll("[data-advance]").forEach((button) => {
|
|
1756
|
-
button.addEventListener("click", () => {
|
|
1757
|
-
api("POST", "/clock", { advance: Number(button.dataset.advance) }).then(renderClock).catch(showError)
|
|
1758
|
-
})
|
|
1759
|
-
})
|
|
1760
|
-
$("freeze").addEventListener("click", () => {
|
|
1761
|
-
const frozen = $("freeze").dataset.frozen === "1"
|
|
1762
|
-
api("POST", "/clock", { freeze: !frozen }).then(renderClock).catch(showError)
|
|
1763
|
-
})
|
|
1764
|
-
$("clock-reset").addEventListener("click", () => {
|
|
1765
|
-
api("POST", "/clock", { reset: true }).then(renderClock).catch(showError)
|
|
1766
|
-
})
|
|
1767
|
-
$("clock-set").addEventListener("submit", (event) => {
|
|
1768
|
-
event.preventDefault()
|
|
1769
|
-
const raw = $("clock-instant").value.trim()
|
|
1770
|
-
const set = /^-?\\d+$/.test(raw) ? Number(raw) : raw
|
|
1771
|
-
api("POST", "/clock", { set }).then(renderClock).catch(showError)
|
|
1772
|
-
})
|
|
1773
|
-
|
|
1774
|
-
$("fault-form").addEventListener("submit", (event) => {
|
|
1775
|
-
event.preventDefault()
|
|
1776
|
-
const preset = $("fault-preset").value
|
|
1777
|
-
const body = preset
|
|
1778
|
-
? { preset }
|
|
1779
|
-
: {
|
|
1780
|
-
status: Number($("fault-status").value),
|
|
1781
|
-
...($("fault-path").value ? { pathPrefix: $("fault-path").value } : {}),
|
|
1782
|
-
...($("fault-method").value ? { method: $("fault-method").value } : {})
|
|
1783
|
-
}
|
|
1784
|
-
if (!preset && !Number.isFinite(body.status)) {
|
|
1785
|
-
showError(new Error("Status must be a number, or choose a preset"))
|
|
1786
|
-
return
|
|
1787
|
-
}
|
|
1788
|
-
api("POST", "/faults", body).then(renderFaults).catch(showError)
|
|
1789
|
-
})
|
|
1790
|
-
$("fault-clear").addEventListener("click", () => api("DELETE", "/faults").then(renderFaults).catch(showError))
|
|
1791
|
-
$("journal-clear").addEventListener("click", () => api("DELETE", "/requests").then(renderJournal).catch(showError))
|
|
1792
|
-
|
|
1793
|
-
$("route-list").addEventListener("click", (event) => {
|
|
1794
|
-
const button = event.target.closest("[data-route]")
|
|
1795
|
-
if (!button) return
|
|
1796
|
-
state.route = button.dataset.route
|
|
1797
|
-
$("route-title").textContent = state.route
|
|
1798
|
-
const method = state.route.split(" ")[0]
|
|
1799
|
-
$("route-body").disabled = method === "GET" || method === "DELETE"
|
|
1800
|
-
})
|
|
1801
|
-
$("route-form").addEventListener("submit", async (event) => {
|
|
1802
|
-
event.preventDefault()
|
|
1803
|
-
if (!state.route) return showError(new Error("Choose a route first"))
|
|
1804
|
-
const [method, path] = [state.route.split(" ")[0], state.route.split(" ").slice(1).join(" ")]
|
|
1805
|
-
const concrete = path.replace(/:([A-Za-z0-9_]+)/g, (_, key) => prompt("Value for " + key) || "")
|
|
1806
|
-
let body
|
|
1807
|
-
if (method !== "GET" && method !== "DELETE" && $("route-body").value.trim()) {
|
|
1808
|
-
try { body = JSON.parse($("route-body").value) }
|
|
1809
|
-
catch { return showError(new Error("Body is not JSON")) }
|
|
1810
|
-
}
|
|
1811
|
-
try {
|
|
1812
|
-
const data = await api(method, concrete, body)
|
|
1813
|
-
$("route-result").textContent = JSON.stringify(data, null, 2)
|
|
1814
|
-
} catch (error) {
|
|
1815
|
-
showError(error)
|
|
1816
|
-
}
|
|
1817
|
-
})
|
|
1818
|
-
|
|
1819
|
-
function mountPanels(panels) {
|
|
1820
|
-
const nav = $("nav")
|
|
1821
|
-
const views = $("views")
|
|
1822
|
-
for (const panel of panels) {
|
|
1823
|
-
const button = document.createElement("button")
|
|
1824
|
-
button.type = "button"
|
|
1825
|
-
button.dataset.view = panel.id
|
|
1826
|
-
button.textContent = panel.title
|
|
1827
|
-
nav.append(button)
|
|
1828
|
-
const section = document.createElement("section")
|
|
1829
|
-
section.className = "view"
|
|
1830
|
-
section.id = "view-" + panel.id
|
|
1831
|
-
section.hidden = true
|
|
1832
|
-
const title = document.createElement("h2")
|
|
1833
|
-
title.textContent = panel.title
|
|
1834
|
-
const lede = document.createElement("p")
|
|
1835
|
-
lede.className = "lede"
|
|
1836
|
-
lede.textContent = panel.description || ""
|
|
1837
|
-
const body = document.createElement("div")
|
|
1838
|
-
body.className = "panel-body"
|
|
1839
|
-
body.id = "panel-" + panel.id
|
|
1840
|
-
body.innerHTML = panel.html || ""
|
|
1841
|
-
section.append(title, lede, body)
|
|
1842
|
-
views.append(section)
|
|
1843
|
-
if (panel.script) {
|
|
1844
|
-
try {
|
|
1845
|
-
const run = new Function("root", "api", panel.script)
|
|
1846
|
-
run(body, panelApi)
|
|
1847
|
-
} catch (error) {
|
|
1848
|
-
const note = document.createElement("p")
|
|
1849
|
-
note.className = "banner show"
|
|
1850
|
-
note.textContent = error instanceof Error ? error.message : String(error)
|
|
1851
|
-
body.append(note)
|
|
1852
|
-
}
|
|
1853
|
-
}
|
|
1854
|
-
}
|
|
1855
|
-
}
|
|
1856
|
-
|
|
1857
|
-
loadNamespaces()
|
|
1858
|
-
.then(() => api("GET", "/ui/manifest"))
|
|
1859
|
-
.then((manifest) => { mountPanels((manifest && manifest.panels) || []); return refresh() })
|
|
1860
|
-
.catch(showError)
|
|
1861
|
-
})()
|
|
1862
|
-
</script>
|
|
2663
|
+
${script}
|
|
1863
2664
|
</body>
|
|
1864
2665
|
</html>`;
|
|
1865
2666
|
};
|
|
@@ -1876,10 +2677,10 @@ var json2 = (body) => new Response(JSON.stringify(body), {
|
|
|
1876
2677
|
});
|
|
1877
2678
|
var adminUiRoutes = (service, ui) => {
|
|
1878
2679
|
const shell = () => renderAdminDocument(service);
|
|
1879
|
-
const
|
|
2680
|
+
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
1880
2681
|
return {
|
|
1881
|
-
"GET /ui": () => htmlResponse(
|
|
1882
|
-
"GET /ui/": () => htmlResponse(
|
|
2682
|
+
"GET /ui": () => htmlResponse(document3()),
|
|
2683
|
+
"GET /ui/": () => htmlResponse(document3()),
|
|
1883
2684
|
"GET /ui/manifest": () => json2({
|
|
1884
2685
|
service,
|
|
1885
2686
|
panels: (ui?.panels ?? []).map((panel) => ({
|
|
@@ -2367,7 +3168,7 @@ var SAMPLE = 25;
|
|
|
2367
3168
|
var WALK_LIMIT = 1e4;
|
|
2368
3169
|
var SKIP_CTORS = /* @__PURE__ */ new Set(["Hono", "HonoRequest", "Router"]);
|
|
2369
3170
|
var fail = (status, message) => ({ ok: false, status, message });
|
|
2370
|
-
var
|
|
3171
|
+
var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2371
3172
|
var kindOf = (value) => {
|
|
2372
3173
|
if (value === null)
|
|
2373
3174
|
return "null";
|
|
@@ -2466,7 +3267,7 @@ var absorb = (accum, value) => {
|
|
|
2466
3267
|
const kind = kindOf(value);
|
|
2467
3268
|
accum.kinds.add(kind);
|
|
2468
3269
|
accum.present++;
|
|
2469
|
-
if (!
|
|
3270
|
+
if (!isRecord3(value))
|
|
2470
3271
|
return;
|
|
2471
3272
|
for (const [key, child] of Object.entries(value)) {
|
|
2472
3273
|
const nested = accum.nested.get(key) ?? blankAccum(key);
|
|
@@ -2491,7 +3292,7 @@ var inferFields = (records) => {
|
|
|
2491
3292
|
if (records.length === 0)
|
|
2492
3293
|
return [];
|
|
2493
3294
|
const sample = records.slice(0, SAMPLE);
|
|
2494
|
-
const objects = sample.filter((record2) =>
|
|
3295
|
+
const objects = sample.filter((record2) => isRecord3(record2.value));
|
|
2495
3296
|
if (objects.length === 0) {
|
|
2496
3297
|
const kinds = new Set(sample.map((record2) => kindOf(record2.value)));
|
|
2497
3298
|
const list = [...kinds];
|
|
@@ -2678,7 +3479,7 @@ var patchStateRecord = (scope, name, id, value) => {
|
|
|
2678
3479
|
const parsed = jsonValue(value);
|
|
2679
3480
|
if (!parsed.ok)
|
|
2680
3481
|
return parsed;
|
|
2681
|
-
const next =
|
|
3482
|
+
const next = isRecord3(current.value.value) && isRecord3(parsed.value) ? { ...current.value.value, ...parsed.value } : parsed.value;
|
|
2682
3483
|
return replaceStateRecord(scope, name, id, next);
|
|
2683
3484
|
};
|
|
2684
3485
|
var deleteStateRecord = (scope, name, id) => {
|
|
@@ -2746,7 +3547,7 @@ var stateAdminRoutes = (routes) => {
|
|
|
2746
3547
|
const scope = open(namespace);
|
|
2747
3548
|
if (!scope.ok)
|
|
2748
3549
|
return fromResult(scope, 201);
|
|
2749
|
-
if (!
|
|
3550
|
+
if (!isRecord3(body) || !("value" in body)) {
|
|
2750
3551
|
return adminError2(400, 'expected {"id?": "<id>", "value": <json>}');
|
|
2751
3552
|
}
|
|
2752
3553
|
const id = body.id;
|
|
@@ -2764,7 +3565,7 @@ var stateAdminRoutes = (routes) => {
|
|
|
2764
3565
|
const scope = open(namespace);
|
|
2765
3566
|
if (!scope.ok)
|
|
2766
3567
|
return fromResult(scope, 200);
|
|
2767
|
-
if (!
|
|
3568
|
+
if (!isRecord3(body) || !("value" in body))
|
|
2768
3569
|
return adminError2(400, 'expected {"value": <json>}');
|
|
2769
3570
|
const replaced = replaceStateRecord(scope.value, params.collection ?? "", params.id ?? "", body.value);
|
|
2770
3571
|
if (replaced.ok)
|
|
@@ -2775,7 +3576,7 @@ var stateAdminRoutes = (routes) => {
|
|
|
2775
3576
|
const scope = open(namespace);
|
|
2776
3577
|
if (!scope.ok)
|
|
2777
3578
|
return fromResult(scope, 200);
|
|
2778
|
-
if (!
|
|
3579
|
+
if (!isRecord3(body) || !("value" in body))
|
|
2779
3580
|
return adminError2(400, 'expected {"value": <json>}');
|
|
2780
3581
|
const patched = patchStateRecord(scope.value, params.collection ?? "", params.id ?? "", body.value);
|
|
2781
3582
|
if (patched.ok)
|
|
@@ -2795,7 +3596,7 @@ var stateAdminRoutes = (routes) => {
|
|
|
2795
3596
|
};
|
|
2796
3597
|
|
|
2797
3598
|
// ../core/dist/version.js
|
|
2798
|
-
var PACKAGE_VERSION = true ? "1.
|
|
3599
|
+
var PACKAGE_VERSION = true ? "1.5.0" : UNRELEASED_VERSION;
|
|
2799
3600
|
|
|
2800
3601
|
// ../core/dist/signing.js
|
|
2801
3602
|
var encoder = new TextEncoder();
|
|
@@ -3132,9 +3933,9 @@ var createWebhookHub = (options) => {
|
|
|
3132
3933
|
};
|
|
3133
3934
|
var json4 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
3134
3935
|
var adminError3 = (status, message) => json4(status, { error: { type: "mockingbird_admin", message } });
|
|
3135
|
-
var
|
|
3936
|
+
var isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3136
3937
|
var parseEndpoint = (value) => {
|
|
3137
|
-
if (!
|
|
3938
|
+
if (!isRecord4(value) || typeof value.url !== "string")
|
|
3138
3939
|
return "each endpoint needs a url";
|
|
3139
3940
|
try {
|
|
3140
3941
|
new URL(value.url);
|
|
@@ -3151,12 +3952,12 @@ var parseEndpoint = (value) => {
|
|
|
3151
3952
|
const events = value.events ?? value.enabledEvents;
|
|
3152
3953
|
if (Array.isArray(events))
|
|
3153
3954
|
endpoint.events = events.map(String);
|
|
3154
|
-
if (
|
|
3955
|
+
if (isRecord4(value.tags)) {
|
|
3155
3956
|
endpoint.tags = Object.fromEntries(Object.entries(value.tags).map(([k, v]) => [k, String(v)]));
|
|
3156
3957
|
}
|
|
3157
3958
|
if (typeof value.account === "string")
|
|
3158
3959
|
endpoint.tags = { ...endpoint.tags, account: value.account };
|
|
3159
|
-
if (
|
|
3960
|
+
if (isRecord4(value.headers)) {
|
|
3160
3961
|
endpoint.headers = Object.fromEntries(Object.entries(value.headers).map(([k, v]) => [k, String(v)]));
|
|
3161
3962
|
}
|
|
3162
3963
|
return endpoint;
|
|
@@ -3183,7 +3984,7 @@ var webhookAdminRoutes = (hub) => ({
|
|
|
3183
3984
|
return json4(200, { status: "ok" });
|
|
3184
3985
|
},
|
|
3185
3986
|
"POST /webhooks/faults": ({ body, namespace }) => {
|
|
3186
|
-
if (!
|
|
3987
|
+
if (!isRecord4(body) || !["duplicate", "reorder", "drop"].includes(String(body.mode))) {
|
|
3187
3988
|
return adminError3(400, "mode must be duplicate, reorder or drop");
|
|
3188
3989
|
}
|
|
3189
3990
|
const fault = { mode: body.mode };
|
|
@@ -3199,7 +4000,7 @@ var webhookAdminRoutes = (hub) => ({
|
|
|
3199
4000
|
}))
|
|
3200
4001
|
}),
|
|
3201
4002
|
"PUT /webhook-endpoints": ({ body, namespace }) => {
|
|
3202
|
-
const list = Array.isArray(body) ? body :
|
|
4003
|
+
const list = Array.isArray(body) ? body : isRecord4(body) ? body.endpoints : void 0;
|
|
3203
4004
|
if (!Array.isArray(list))
|
|
3204
4005
|
return adminError3(400, "expected [{url, secret?, events?}]");
|
|
3205
4006
|
const parsed = [];
|
|
@@ -3267,8 +4068,8 @@ var DroppedConnectionError = class extends TypeError {
|
|
|
3267
4068
|
this.name = "TypeError";
|
|
3268
4069
|
}
|
|
3269
4070
|
};
|
|
3270
|
-
var operationMatcher = (
|
|
3271
|
-
const matchers = listOperations(
|
|
4071
|
+
var operationMatcher = (document3) => {
|
|
4072
|
+
const matchers = listOperations(document3).map((operation) => ({
|
|
3272
4073
|
operationId: operation.operationId,
|
|
3273
4074
|
method: operation.method.toUpperCase(),
|
|
3274
4075
|
pattern: new RegExp(`^${operation.path.split("/").map((segment) => segment.startsWith("{") ? "[^/]+" : segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("/")}/?$`),
|
|
@@ -3363,10 +4164,10 @@ var createRuntime = (options) => {
|
|
|
3363
4164
|
const ensureBranch = (namespace, branch2, at) => {
|
|
3364
4165
|
if (!BRANCH_PATTERN2.test(branch2))
|
|
3365
4166
|
throw new RangeError(`branch must match ${BRANCH_PATTERN2}`);
|
|
3366
|
-
const
|
|
4167
|
+
const history2 = timeline(namespace);
|
|
3367
4168
|
if (branch2 === "main") {
|
|
3368
4169
|
if (at !== void 0) {
|
|
3369
|
-
const point =
|
|
4170
|
+
const point = history2.checkout("main", at);
|
|
3370
4171
|
restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
|
|
3371
4172
|
captured.set(namespace, point.value.snapshot);
|
|
3372
4173
|
rng.setState(point.value.rngState);
|
|
@@ -3379,10 +4180,10 @@ var createRuntime = (options) => {
|
|
|
3379
4180
|
return namespace;
|
|
3380
4181
|
}
|
|
3381
4182
|
const storage = physicalBranch(namespace, branch2);
|
|
3382
|
-
if (!
|
|
4183
|
+
if (!history2.hasBranch(branch2)) {
|
|
3383
4184
|
if (at === void 0)
|
|
3384
|
-
|
|
3385
|
-
const point =
|
|
4185
|
+
history2.commit(capture(namespace));
|
|
4186
|
+
const point = history2.fork(branch2, at === void 0 ? {} : { from: at });
|
|
3386
4187
|
const branchRng = createRng(options.seed ?? 0);
|
|
3387
4188
|
if (point)
|
|
3388
4189
|
branchRng.setState(point.value.rngState);
|
|
@@ -3391,8 +4192,8 @@ var createRuntime = (options) => {
|
|
|
3391
4192
|
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
3392
4193
|
if (point)
|
|
3393
4194
|
captured.set(storage, point.value.snapshot);
|
|
3394
|
-
} else if (at !== void 0 &&
|
|
3395
|
-
const point =
|
|
4195
|
+
} else if (at !== void 0 && history2.head(branch2)?.id !== at) {
|
|
4196
|
+
const point = history2.checkout(branch2, at);
|
|
3396
4197
|
if (!instances.has(storage)) {
|
|
3397
4198
|
const branchRng = createRng(options.seed ?? 0);
|
|
3398
4199
|
branchRng.setState(point.value.rngState);
|
|
@@ -3403,7 +4204,7 @@ var createRuntime = (options) => {
|
|
|
3403
4204
|
captured.set(storage, point.value.snapshot);
|
|
3404
4205
|
} else {
|
|
3405
4206
|
if (!instances.has(storage)) {
|
|
3406
|
-
const point =
|
|
4207
|
+
const point = history2.head(branch2);
|
|
3407
4208
|
const branchRng = createRng(options.seed ?? 0);
|
|
3408
4209
|
if (point)
|
|
3409
4210
|
branchRng.setState(point.value.rngState);
|
|
@@ -3427,8 +4228,8 @@ var createRuntime = (options) => {
|
|
|
3427
4228
|
const checkout = (checkpointId, checkoutOptions = {}) => {
|
|
3428
4229
|
const namespace = checkoutOptions.namespace ?? DEFAULT_NAMESPACE;
|
|
3429
4230
|
const branchName = checkoutOptions.branch ?? "main";
|
|
3430
|
-
const
|
|
3431
|
-
const point =
|
|
4231
|
+
const history2 = timeline(namespace);
|
|
4232
|
+
const point = history2.checkout(branchName, checkpointId);
|
|
3432
4233
|
const storage = ensureBranch(namespace, branchName);
|
|
3433
4234
|
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
3434
4235
|
captured.set(storage, point.value.snapshot);
|
|
@@ -3478,9 +4279,9 @@ var createRuntime = (options) => {
|
|
|
3478
4279
|
instance(name);
|
|
3479
4280
|
restoreNamespace(sqlite, storageNamespace(name), from);
|
|
3480
4281
|
captured.set(name, from);
|
|
3481
|
-
const
|
|
3482
|
-
if (
|
|
3483
|
-
|
|
4282
|
+
const history2 = timelines.get(name);
|
|
4283
|
+
if (history2)
|
|
4284
|
+
history2.commit(capture(name), { branch: "main" });
|
|
3484
4285
|
else
|
|
3485
4286
|
timeline(name);
|
|
3486
4287
|
};
|
|
@@ -3709,10 +4510,10 @@ var createRuntime = (options) => {
|
|
|
3709
4510
|
},
|
|
3710
4511
|
release: (name, checkpointId) => timeline(name).release(checkpointId),
|
|
3711
4512
|
inspect: (name) => {
|
|
3712
|
-
const
|
|
4513
|
+
const history2 = timeline(name);
|
|
3713
4514
|
return {
|
|
3714
|
-
branches:
|
|
3715
|
-
checkpoints:
|
|
4515
|
+
branches: history2.branches(),
|
|
4516
|
+
checkpoints: history2.checkpoints().map(({ id, branch: branchName, parent, at }) => ({
|
|
3716
4517
|
id,
|
|
3717
4518
|
branch: branchName,
|
|
3718
4519
|
parent,
|
|
@@ -3727,11 +4528,11 @@ var createRuntime = (options) => {
|
|
|
3727
4528
|
} : {},
|
|
3728
4529
|
routes: {
|
|
3729
4530
|
...credentialRoutes(credentials),
|
|
3730
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
3731
4531
|
...options.webhooks ? webhookAdminRoutes(options.webhooks) : {},
|
|
3732
4532
|
...options.admin?.(runtime) ?? {},
|
|
3733
|
-
//
|
|
3734
|
-
//
|
|
4533
|
+
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
4534
|
+
// maps `presets`, and a service route must not replace that list with a record.
|
|
4535
|
+
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
3735
4536
|
...stateAdminRoutes({
|
|
3736
4537
|
open: stateScope,
|
|
3737
4538
|
afterWrite: (namespace) => {
|
|
@@ -3806,9 +4607,12 @@ var credentialRoutes = (registry) => ({
|
|
|
3806
4607
|
}
|
|
3807
4608
|
});
|
|
3808
4609
|
var presetRoutes = (presets, runtime) => ({
|
|
3809
|
-
"GET /faults/presets": () =>
|
|
3810
|
-
|
|
3811
|
-
|
|
4610
|
+
"GET /faults/presets": () => {
|
|
4611
|
+
const body = {
|
|
4612
|
+
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
4613
|
+
};
|
|
4614
|
+
return adminJson(200, body);
|
|
4615
|
+
},
|
|
3812
4616
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
3813
4617
|
const name = params.name;
|
|
3814
4618
|
if (!presets[name])
|
|
@@ -3866,7 +4670,7 @@ var verifyJwt = async (token, secrets, now) => {
|
|
|
3866
4670
|
var bodySha256 = async (body) => bytesToBase64(new Uint8Array(await crypto.subtle.digest("SHA-256", encoder2.encode(body))));
|
|
3867
4671
|
|
|
3868
4672
|
// src/generated/openapi.ts
|
|
3869
|
-
var
|
|
4673
|
+
var document2 = JSON.parse(`{"openapi":"3.1.0","info":{"title":"LiveKit Server API (Mockingbird subset)","version":"2.19.1","description":"Twirp JSON room, participant, egress, and SIP contract."},"servers":[{"url":"https://example.livekit.cloud"}],"paths":{"/twirp/livekit.RoomService/{method}":{"post":{"operationId":"RoomService","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"parameters":[{"name":"method","in":"path","required":true,"schema":{"type":"string"}},{"name":"Authorization","in":"header","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"responses":{"200":{"description":"Protobuf JSON response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"Twirp error","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/twirp/livekit.Egress/{method}":{"post":{"operationId":"EgressService","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"parameters":[{"name":"method","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"responses":{"200":{"description":"Protobuf JSON response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/twirp/livekit.SIP/{method}":{"post":{"operationId":"SipService","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"parameters":[{"name":"method","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"responses":{"200":{"description":"Protobuf JSON response","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}}}}`);
|
|
3870
4674
|
var operationIds = ["RoomService", "EgressService", "SipService"];
|
|
3871
4675
|
var supportedOperationIds = ["RoomService", "EgressService", "SipService"];
|
|
3872
4676
|
|
|
@@ -3984,7 +4788,7 @@ var createRuntime2 = (options = {}) => {
|
|
|
3984
4788
|
});
|
|
3985
4789
|
const runtime = createRuntime({
|
|
3986
4790
|
name: LIVEKIT_NAMESPACE,
|
|
3987
|
-
document,
|
|
4791
|
+
document: document2,
|
|
3988
4792
|
presets: LIVEKIT_PRESETS,
|
|
3989
4793
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
3990
4794
|
...options.clock ? { clock: options.clock } : {},
|
|
@@ -4363,7 +5167,7 @@ var LiveKitAPI = class {
|
|
|
4363
5167
|
};
|
|
4364
5168
|
|
|
4365
5169
|
export {
|
|
4366
|
-
document,
|
|
5170
|
+
document2 as document,
|
|
4367
5171
|
operationIds,
|
|
4368
5172
|
supportedOperationIds,
|
|
4369
5173
|
LIVEKIT_PRESETS,
|
|
@@ -4371,4 +5175,4 @@ export {
|
|
|
4371
5175
|
LIVEKIT_NAMESPACE,
|
|
4372
5176
|
LiveKitAPI
|
|
4373
5177
|
};
|
|
4374
|
-
//# sourceMappingURL=chunk-
|
|
5178
|
+
//# sourceMappingURL=chunk-PGZZE4OX.js.map
|