lakebed 0.0.9 → 0.0.11
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/README.md +6 -3
- package/package.json +1 -1
- package/src/anonymous-server.js +1059 -329
- package/src/cli.js +75 -10
- package/src/version.js +1 -1
package/src/anonymous-server.js
CHANGED
|
@@ -257,6 +257,7 @@ function quotaLimitForBucket(bucket, deploy) {
|
|
|
257
257
|
}
|
|
258
258
|
|
|
259
259
|
const USER_LIMIT_OVERRIDE_KEYS = ["requestsPerDay", "mutationsPerDay"];
|
|
260
|
+
const USER_BOOST_MULTIPLIER = 20;
|
|
260
261
|
|
|
261
262
|
function normalizeUserId(value) {
|
|
262
263
|
const userId = String(value ?? "").trim();
|
|
@@ -277,6 +278,25 @@ function normalizeLimitOverrideValue(value, key) {
|
|
|
277
278
|
return parsed;
|
|
278
279
|
}
|
|
279
280
|
|
|
281
|
+
function normalizeOptionalBoolean(value, key) {
|
|
282
|
+
if (value === undefined) {
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
if (typeof value === "boolean") {
|
|
286
|
+
return value;
|
|
287
|
+
}
|
|
288
|
+
if (typeof value === "string") {
|
|
289
|
+
const normalized = value.trim().toLowerCase();
|
|
290
|
+
if (normalized === "true") {
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
if (normalized === "false") {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
throw new Error(`${key} must be a boolean.`);
|
|
298
|
+
}
|
|
299
|
+
|
|
280
300
|
function normalizeUserLimitOverrides(value = {}) {
|
|
281
301
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
282
302
|
throw new Error("limits must be a JSON object.");
|
|
@@ -299,9 +319,19 @@ function normalizeUserLimitOverrides(value = {}) {
|
|
|
299
319
|
return overrides;
|
|
300
320
|
}
|
|
301
321
|
|
|
302
|
-
function
|
|
322
|
+
function limitsWithBoost(baseLimits, boost = false) {
|
|
323
|
+
if (!boost) {
|
|
324
|
+
return { ...baseLimits };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return Object.fromEntries(
|
|
328
|
+
Object.entries(baseLimits).map(([key, value]) => [key, Number.isFinite(value) ? value * USER_BOOST_MULTIPLIER : value])
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function limitsWithOverrides(baseLimits, limitOverrides, boost = false) {
|
|
303
333
|
return {
|
|
304
|
-
...baseLimits,
|
|
334
|
+
...limitsWithBoost(baseLimits, boost),
|
|
305
335
|
...normalizeUserLimitOverrides(limitOverrides ?? {})
|
|
306
336
|
};
|
|
307
337
|
}
|
|
@@ -310,6 +340,7 @@ function userFromOwner(owner, current = {}) {
|
|
|
310
340
|
const id = normalizeUserId(owner?.id ?? current.id);
|
|
311
341
|
return {
|
|
312
342
|
avatarUrl: owner?.avatarUrl ?? current.avatarUrl ?? null,
|
|
343
|
+
boost: Boolean(current.boost),
|
|
313
344
|
createdAt: current.createdAt ?? now(),
|
|
314
345
|
displayName: owner?.displayName ?? owner?.login ?? current.displayName ?? id,
|
|
315
346
|
id,
|
|
@@ -327,13 +358,15 @@ function adminUserSummary({ activeDeployCount = 0, deployCount = 0, usage = [],
|
|
|
327
358
|
return {
|
|
328
359
|
activeDeployCount,
|
|
329
360
|
avatarUrl: user.avatarUrl,
|
|
361
|
+
boost: Boolean(user.boost),
|
|
362
|
+
boostMultiplier: USER_BOOST_MULTIPLIER,
|
|
330
363
|
createdAt: user.createdAt,
|
|
331
364
|
defaultLimits: DEFAULT_ANONYMOUS_LIMITS,
|
|
332
365
|
deployCount,
|
|
333
366
|
displayName: user.displayName ?? user.login ?? user.id,
|
|
334
367
|
id: user.id,
|
|
335
368
|
limitOverrides,
|
|
336
|
-
limits: limitsWithOverrides(DEFAULT_ANONYMOUS_LIMITS, limitOverrides),
|
|
369
|
+
limits: limitsWithOverrides(DEFAULT_ANONYMOUS_LIMITS, limitOverrides, user.boost),
|
|
337
370
|
login: user.login,
|
|
338
371
|
provider: user.provider,
|
|
339
372
|
providerId: user.providerId,
|
|
@@ -657,6 +690,15 @@ function adminDeploySummary({ artifact, artifactBytes = 0, deploy, logBytes = 0,
|
|
|
657
690
|
logBytes,
|
|
658
691
|
logEntries,
|
|
659
692
|
name: artifact?.name ?? "Lakebed Capsule",
|
|
693
|
+
owner: deploy.owner
|
|
694
|
+
? {
|
|
695
|
+
avatarUrl: deploy.owner.avatarUrl ?? null,
|
|
696
|
+
displayName: deploy.owner.displayName ?? deploy.owner.login ?? deploy.owner.id,
|
|
697
|
+
id: deploy.owner.id,
|
|
698
|
+
login: deploy.owner.login ?? null,
|
|
699
|
+
url: deploy.owner.url ?? null
|
|
700
|
+
}
|
|
701
|
+
: null,
|
|
660
702
|
ownerId: deploy.ownerId,
|
|
661
703
|
slug: deploy.slug,
|
|
662
704
|
stateBytes,
|
|
@@ -678,16 +720,17 @@ function adminHtml() {
|
|
|
678
720
|
<style>
|
|
679
721
|
:root {
|
|
680
722
|
color-scheme: dark;
|
|
681
|
-
--bg: #
|
|
682
|
-
--panel: #
|
|
683
|
-
--
|
|
684
|
-
--line
|
|
685
|
-
--
|
|
686
|
-
--
|
|
687
|
-
--
|
|
688
|
-
--
|
|
689
|
-
--bad: #
|
|
690
|
-
--
|
|
723
|
+
--bg: #0f1115;
|
|
724
|
+
--panel: #16191f;
|
|
725
|
+
--panel-soft: #1b2028;
|
|
726
|
+
--line: #2a303a;
|
|
727
|
+
--line-strong: #3c4654;
|
|
728
|
+
--text: #e7eaf0;
|
|
729
|
+
--muted: #8f9aaa;
|
|
730
|
+
--accent: #d9e1ec;
|
|
731
|
+
--bad: #f08080;
|
|
732
|
+
--warn: #e0b55f;
|
|
733
|
+
--ink: #0c0e12;
|
|
691
734
|
}
|
|
692
735
|
|
|
693
736
|
* {
|
|
@@ -695,15 +738,11 @@ function adminHtml() {
|
|
|
695
738
|
}
|
|
696
739
|
|
|
697
740
|
body {
|
|
698
|
-
|
|
699
|
-
background:
|
|
700
|
-
linear-gradient(90deg, rgba(154, 214, 107, 0.06) 1px, transparent 1px),
|
|
701
|
-
linear-gradient(180deg, rgba(154, 214, 107, 0.04) 1px, transparent 1px),
|
|
702
|
-
var(--bg);
|
|
703
|
-
background-size: 34px 34px;
|
|
741
|
+
background: var(--bg);
|
|
704
742
|
color: var(--text);
|
|
705
|
-
font-family:
|
|
743
|
+
font-family: Inter, "Avenir Next", "Helvetica Neue", Arial, sans-serif;
|
|
706
744
|
letter-spacing: 0;
|
|
745
|
+
margin: 0;
|
|
707
746
|
}
|
|
708
747
|
|
|
709
748
|
button,
|
|
@@ -723,105 +762,164 @@ function adminHtml() {
|
|
|
723
762
|
|
|
724
763
|
.shell {
|
|
725
764
|
margin: 0 auto;
|
|
726
|
-
max-width:
|
|
765
|
+
max-width: 1440px;
|
|
727
766
|
min-height: 100vh;
|
|
728
|
-
padding:
|
|
767
|
+
padding: 18px 22px;
|
|
729
768
|
}
|
|
730
769
|
|
|
731
770
|
.topbar {
|
|
732
771
|
align-items: center;
|
|
733
772
|
display: flex;
|
|
734
|
-
gap:
|
|
773
|
+
gap: 16px;
|
|
735
774
|
justify-content: space-between;
|
|
736
|
-
margin-bottom:
|
|
775
|
+
margin-bottom: 14px;
|
|
737
776
|
}
|
|
738
777
|
|
|
739
778
|
.brand {
|
|
740
779
|
display: grid;
|
|
741
|
-
gap:
|
|
780
|
+
gap: 2px;
|
|
742
781
|
}
|
|
743
782
|
|
|
744
|
-
.eyebrow
|
|
745
|
-
|
|
783
|
+
.eyebrow,
|
|
784
|
+
.mono,
|
|
785
|
+
small,
|
|
786
|
+
th,
|
|
787
|
+
label,
|
|
788
|
+
.status-line,
|
|
789
|
+
.pager {
|
|
790
|
+
color: var(--muted);
|
|
746
791
|
font-family: "SFMono-Regular", Consolas, monospace;
|
|
747
792
|
font-size: 12px;
|
|
748
793
|
}
|
|
749
794
|
|
|
750
|
-
h1
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
line-height: 1;
|
|
795
|
+
h1,
|
|
796
|
+
h2,
|
|
797
|
+
h3 {
|
|
754
798
|
margin: 0;
|
|
755
799
|
}
|
|
756
800
|
|
|
757
|
-
|
|
801
|
+
h1 {
|
|
802
|
+
font-size: 28px;
|
|
803
|
+
line-height: 1.05;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
h2 {
|
|
807
|
+
font-size: 15px;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
h3 {
|
|
811
|
+
font-size: 14px;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
.actions,
|
|
815
|
+
.tabs,
|
|
816
|
+
.pager,
|
|
817
|
+
.limit-actions {
|
|
818
|
+
align-items: center;
|
|
758
819
|
display: flex;
|
|
759
|
-
|
|
760
|
-
|
|
820
|
+
gap: 8px;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
.actions {
|
|
761
824
|
justify-content: flex-end;
|
|
762
825
|
}
|
|
763
826
|
|
|
827
|
+
.button,
|
|
828
|
+
.tab,
|
|
829
|
+
.row-action,
|
|
830
|
+
.icon-button {
|
|
831
|
+
align-items: center;
|
|
832
|
+
border-radius: 6px;
|
|
833
|
+
cursor: pointer;
|
|
834
|
+
display: inline-flex;
|
|
835
|
+
justify-content: center;
|
|
836
|
+
}
|
|
837
|
+
|
|
764
838
|
.button {
|
|
765
839
|
background: var(--accent);
|
|
766
840
|
border: 1px solid var(--accent);
|
|
767
|
-
border-radius: 6px;
|
|
768
841
|
color: var(--ink);
|
|
769
|
-
cursor: pointer;
|
|
770
842
|
font-weight: 700;
|
|
771
|
-
min-height:
|
|
772
|
-
padding: 0
|
|
843
|
+
min-height: 34px;
|
|
844
|
+
padding: 0 12px;
|
|
773
845
|
}
|
|
774
846
|
|
|
775
|
-
.button.secondary
|
|
847
|
+
.button.secondary,
|
|
848
|
+
.tab,
|
|
849
|
+
.row-action,
|
|
850
|
+
.icon-button {
|
|
776
851
|
background: transparent;
|
|
852
|
+
border: 1px solid var(--line-strong);
|
|
777
853
|
color: var(--text);
|
|
778
854
|
}
|
|
779
855
|
|
|
780
|
-
.
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
856
|
+
.button:disabled,
|
|
857
|
+
.tab:disabled,
|
|
858
|
+
.row-action:disabled,
|
|
859
|
+
.icon-button:disabled {
|
|
860
|
+
cursor: default;
|
|
861
|
+
opacity: 0.48;
|
|
785
862
|
}
|
|
786
863
|
|
|
787
|
-
.
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
864
|
+
.tabs {
|
|
865
|
+
border-bottom: 1px solid var(--line);
|
|
866
|
+
margin-bottom: 14px;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
.tab {
|
|
870
|
+
border-bottom-left-radius: 0;
|
|
871
|
+
border-bottom-right-radius: 0;
|
|
872
|
+
border-color: transparent;
|
|
873
|
+
min-height: 34px;
|
|
874
|
+
padding: 0 12px;
|
|
792
875
|
}
|
|
793
876
|
|
|
794
|
-
.
|
|
795
|
-
|
|
877
|
+
.tab.active {
|
|
878
|
+
background: var(--panel);
|
|
879
|
+
border-color: var(--line);
|
|
880
|
+
border-bottom-color: var(--panel);
|
|
881
|
+
color: var(--text);
|
|
882
|
+
margin-bottom: -1px;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
.metrics {
|
|
886
|
+
display: grid;
|
|
887
|
+
gap: 8px;
|
|
888
|
+
grid-template-columns: repeat(6, minmax(120px, 1fr));
|
|
889
|
+
margin-bottom: 14px;
|
|
796
890
|
}
|
|
797
891
|
|
|
798
|
-
.metric
|
|
799
|
-
|
|
892
|
+
.metric {
|
|
893
|
+
background: var(--panel);
|
|
894
|
+
border: 1px solid var(--line);
|
|
895
|
+
border-radius: 6px;
|
|
896
|
+
min-height: 68px;
|
|
897
|
+
padding: 10px 12px;
|
|
800
898
|
}
|
|
801
899
|
|
|
802
900
|
.metric span {
|
|
803
901
|
color: var(--muted);
|
|
804
902
|
display: block;
|
|
805
903
|
font-family: "SFMono-Regular", Consolas, monospace;
|
|
806
|
-
font-size:
|
|
807
|
-
margin-bottom:
|
|
904
|
+
font-size: 11px;
|
|
905
|
+
margin-bottom: 7px;
|
|
808
906
|
}
|
|
809
907
|
|
|
810
908
|
.metric strong {
|
|
811
909
|
display: block;
|
|
812
|
-
font-size:
|
|
910
|
+
font-size: 20px;
|
|
813
911
|
line-height: 1.1;
|
|
814
912
|
}
|
|
815
913
|
|
|
816
914
|
.panel {
|
|
817
|
-
background:
|
|
915
|
+
background: var(--panel);
|
|
818
916
|
border: 1px solid var(--line);
|
|
819
|
-
border-radius:
|
|
917
|
+
border-radius: 6px;
|
|
820
918
|
overflow: hidden;
|
|
821
919
|
}
|
|
822
920
|
|
|
823
921
|
.panel + .panel {
|
|
824
|
-
margin-top:
|
|
922
|
+
margin-top: 12px;
|
|
825
923
|
}
|
|
826
924
|
|
|
827
925
|
.panel-head {
|
|
@@ -830,18 +928,16 @@ function adminHtml() {
|
|
|
830
928
|
display: flex;
|
|
831
929
|
gap: 12px;
|
|
832
930
|
justify-content: space-between;
|
|
833
|
-
padding:
|
|
931
|
+
padding: 10px 12px;
|
|
834
932
|
}
|
|
835
933
|
|
|
836
|
-
.panel-head
|
|
837
|
-
|
|
838
|
-
margin: 0;
|
|
934
|
+
.panel-head.tight {
|
|
935
|
+
align-items: flex-start;
|
|
839
936
|
}
|
|
840
937
|
|
|
841
|
-
.
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
font-size: 12px;
|
|
938
|
+
.panel-title {
|
|
939
|
+
display: grid;
|
|
940
|
+
gap: 3px;
|
|
845
941
|
}
|
|
846
942
|
|
|
847
943
|
.table-wrap {
|
|
@@ -850,27 +946,32 @@ function adminHtml() {
|
|
|
850
946
|
|
|
851
947
|
table {
|
|
852
948
|
border-collapse: collapse;
|
|
853
|
-
min-width:
|
|
949
|
+
min-width: 900px;
|
|
854
950
|
width: 100%;
|
|
855
951
|
}
|
|
856
952
|
|
|
953
|
+
.deploy-table {
|
|
954
|
+
min-width: 960px;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
.user-table {
|
|
958
|
+
min-width: 840px;
|
|
959
|
+
}
|
|
960
|
+
|
|
857
961
|
th,
|
|
858
962
|
td {
|
|
859
963
|
border-bottom: 1px solid var(--line);
|
|
860
|
-
padding:
|
|
964
|
+
padding: 8px 10px;
|
|
861
965
|
text-align: left;
|
|
862
|
-
vertical-align:
|
|
966
|
+
vertical-align: middle;
|
|
863
967
|
}
|
|
864
968
|
|
|
865
969
|
th {
|
|
866
|
-
color: var(--muted);
|
|
867
|
-
font-family: "SFMono-Regular", Consolas, monospace;
|
|
868
|
-
font-size: 12px;
|
|
869
970
|
font-weight: 600;
|
|
870
971
|
}
|
|
871
972
|
|
|
872
973
|
td {
|
|
873
|
-
font-size:
|
|
974
|
+
font-size: 13px;
|
|
874
975
|
white-space: nowrap;
|
|
875
976
|
}
|
|
876
977
|
|
|
@@ -878,185 +979,215 @@ function adminHtml() {
|
|
|
878
979
|
border-bottom: 0;
|
|
879
980
|
}
|
|
880
981
|
|
|
881
|
-
.
|
|
982
|
+
.stack {
|
|
882
983
|
display: grid;
|
|
883
|
-
gap:
|
|
884
|
-
min-width: 240px;
|
|
984
|
+
gap: 2px;
|
|
885
985
|
}
|
|
886
986
|
|
|
887
|
-
.
|
|
888
|
-
|
|
987
|
+
.primary-text {
|
|
988
|
+
color: var(--text);
|
|
989
|
+
font-weight: 700;
|
|
889
990
|
}
|
|
890
991
|
|
|
891
|
-
.
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
font-size: 12px;
|
|
992
|
+
.name-cell {
|
|
993
|
+
display: grid;
|
|
994
|
+
gap: 3px;
|
|
995
|
+
min-width: 160px;
|
|
896
996
|
}
|
|
897
997
|
|
|
898
|
-
.
|
|
998
|
+
.name-cell small {
|
|
899
999
|
white-space: normal;
|
|
900
1000
|
}
|
|
901
1001
|
|
|
1002
|
+
.user-cell {
|
|
1003
|
+
display: grid;
|
|
1004
|
+
gap: 3px;
|
|
1005
|
+
min-width: 150px;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
.name-line {
|
|
1009
|
+
align-items: center;
|
|
1010
|
+
display: flex;
|
|
1011
|
+
gap: 6px;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
.github-link {
|
|
1015
|
+
align-items: center;
|
|
1016
|
+
color: var(--muted);
|
|
1017
|
+
display: inline-flex;
|
|
1018
|
+
height: 18px;
|
|
1019
|
+
justify-content: center;
|
|
1020
|
+
width: 18px;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
.github-link:hover {
|
|
1024
|
+
color: var(--accent);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
.github-link svg {
|
|
1028
|
+
height: 15px;
|
|
1029
|
+
width: 15px;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
902
1032
|
.pill {
|
|
903
1033
|
border: 1px solid var(--line-strong);
|
|
904
1034
|
border-radius: 999px;
|
|
905
1035
|
display: inline-flex;
|
|
906
1036
|
font-family: "SFMono-Regular", Consolas, monospace;
|
|
907
|
-
font-size:
|
|
908
|
-
|
|
1037
|
+
font-size: 11px;
|
|
1038
|
+
line-height: 1;
|
|
1039
|
+
padding: 4px 7px;
|
|
909
1040
|
}
|
|
910
1041
|
|
|
911
1042
|
.pill.active {
|
|
912
|
-
border-color:
|
|
1043
|
+
border-color: #65758a;
|
|
913
1044
|
color: var(--accent);
|
|
914
1045
|
}
|
|
915
1046
|
|
|
916
1047
|
.pill.expired {
|
|
917
|
-
border-color: rgba(
|
|
1048
|
+
border-color: rgba(240, 128, 128, 0.75);
|
|
918
1049
|
color: var(--bad);
|
|
919
1050
|
}
|
|
920
1051
|
|
|
921
1052
|
.pill.terminated {
|
|
922
|
-
border-color: rgba(
|
|
1053
|
+
border-color: rgba(224, 181, 95, 0.75);
|
|
923
1054
|
color: var(--warn);
|
|
924
1055
|
}
|
|
925
1056
|
|
|
926
|
-
.status-cell {
|
|
927
|
-
align-items: center;
|
|
928
|
-
display: flex;
|
|
929
|
-
gap: 8px;
|
|
930
|
-
}
|
|
931
|
-
|
|
932
1057
|
.row-action {
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
border-radius: 6px;
|
|
936
|
-
color: var(--bad);
|
|
937
|
-
cursor: pointer;
|
|
938
|
-
font-family: "SFMono-Regular", Consolas, monospace;
|
|
939
|
-
font-size: 12px;
|
|
940
|
-
min-height: 32px;
|
|
941
|
-
padding: 0 10px;
|
|
1058
|
+
min-height: 30px;
|
|
1059
|
+
padding: 0 9px;
|
|
942
1060
|
}
|
|
943
1061
|
|
|
944
|
-
.
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
opacity: 0.62;
|
|
1062
|
+
.icon-button {
|
|
1063
|
+
height: 30px;
|
|
1064
|
+
padding: 0;
|
|
1065
|
+
width: 30px;
|
|
949
1066
|
}
|
|
950
1067
|
|
|
951
|
-
.
|
|
952
|
-
|
|
953
|
-
|
|
1068
|
+
.icon-button svg {
|
|
1069
|
+
height: 15px;
|
|
1070
|
+
width: 15px;
|
|
954
1071
|
}
|
|
955
1072
|
|
|
956
|
-
.
|
|
957
|
-
border-color: rgba(
|
|
958
|
-
color: var(--
|
|
1073
|
+
.icon-button.danger {
|
|
1074
|
+
border-color: rgba(240, 128, 128, 0.62);
|
|
1075
|
+
color: var(--bad);
|
|
959
1076
|
}
|
|
960
1077
|
|
|
961
|
-
.
|
|
1078
|
+
.limit-cell {
|
|
962
1079
|
display: grid;
|
|
963
1080
|
gap: 4px;
|
|
964
|
-
min-width:
|
|
1081
|
+
min-width: 112px;
|
|
965
1082
|
}
|
|
966
1083
|
|
|
967
|
-
.
|
|
968
|
-
|
|
1084
|
+
.limit-combo {
|
|
1085
|
+
display: grid;
|
|
1086
|
+
gap: 4px;
|
|
1087
|
+
min-width: 220px;
|
|
969
1088
|
}
|
|
970
1089
|
|
|
971
|
-
.limit-
|
|
1090
|
+
.limit-row {
|
|
1091
|
+
align-items: center;
|
|
972
1092
|
display: grid;
|
|
973
1093
|
gap: 6px;
|
|
974
|
-
|
|
1094
|
+
grid-template-columns: 16px 86px 1fr;
|
|
975
1095
|
}
|
|
976
1096
|
|
|
977
|
-
.limit-
|
|
978
|
-
min-height:
|
|
979
|
-
padding: 0
|
|
1097
|
+
.limit-row input {
|
|
1098
|
+
min-height: 28px;
|
|
1099
|
+
padding: 0 7px;
|
|
980
1100
|
}
|
|
981
1101
|
|
|
982
|
-
.
|
|
983
|
-
|
|
984
|
-
display: grid;
|
|
985
|
-
gap: 8px;
|
|
986
|
-
grid-template-columns: minmax(180px, 1fr) 132px 132px auto;
|
|
987
|
-
width: min(100%, 720px);
|
|
1102
|
+
.usage-cell {
|
|
1103
|
+
min-width: 128px;
|
|
988
1104
|
}
|
|
989
1105
|
|
|
990
|
-
|
|
991
|
-
|
|
1106
|
+
input {
|
|
1107
|
+
background: #10131a;
|
|
1108
|
+
border: 1px solid var(--line-strong);
|
|
1109
|
+
border-radius: 6px;
|
|
1110
|
+
color: var(--text);
|
|
1111
|
+
min-height: 30px;
|
|
1112
|
+
outline: none;
|
|
1113
|
+
padding: 0 9px;
|
|
1114
|
+
width: 100%;
|
|
992
1115
|
}
|
|
993
1116
|
|
|
994
|
-
|
|
995
|
-
|
|
1117
|
+
input:focus {
|
|
1118
|
+
border-color: #69778a;
|
|
996
1119
|
}
|
|
997
1120
|
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1121
|
+
input[type="checkbox"] {
|
|
1122
|
+
height: 16px;
|
|
1123
|
+
min-height: 0;
|
|
1124
|
+
width: 16px;
|
|
1001
1125
|
}
|
|
1002
1126
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1127
|
+
.checkbox {
|
|
1128
|
+
align-items: center;
|
|
1129
|
+
color: var(--text);
|
|
1130
|
+
display: inline-flex;
|
|
1131
|
+
gap: 7px;
|
|
1132
|
+
min-height: 30px;
|
|
1008
1133
|
}
|
|
1009
1134
|
|
|
1010
|
-
.
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1135
|
+
.limit-form,
|
|
1136
|
+
.detail-form {
|
|
1137
|
+
align-items: end;
|
|
1138
|
+
display: grid;
|
|
1139
|
+
gap: 8px;
|
|
1014
1140
|
}
|
|
1015
1141
|
|
|
1016
|
-
.
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
border-radius: 8px;
|
|
1020
|
-
max-width: 460px;
|
|
1021
|
-
padding: 24px;
|
|
1022
|
-
width: 100%;
|
|
1142
|
+
.limit-form {
|
|
1143
|
+
grid-template-columns: minmax(160px, 0.7fr) 100px 100px auto auto;
|
|
1144
|
+
width: min(100%, 650px);
|
|
1023
1145
|
}
|
|
1024
1146
|
|
|
1025
|
-
.
|
|
1026
|
-
|
|
1027
|
-
|
|
1147
|
+
.detail-form {
|
|
1148
|
+
grid-template-columns: auto 128px 128px auto auto;
|
|
1149
|
+
width: min(100%, 700px);
|
|
1028
1150
|
}
|
|
1029
1151
|
|
|
1030
1152
|
.field {
|
|
1031
1153
|
display: grid;
|
|
1032
|
-
gap:
|
|
1154
|
+
gap: 4px;
|
|
1033
1155
|
}
|
|
1034
1156
|
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1157
|
+
.pager {
|
|
1158
|
+
border-top: 1px solid var(--line);
|
|
1159
|
+
justify-content: flex-end;
|
|
1160
|
+
min-height: 40px;
|
|
1161
|
+
padding: 7px 10px;
|
|
1039
1162
|
}
|
|
1040
1163
|
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1164
|
+
.pager button {
|
|
1165
|
+
min-height: 28px;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
.login {
|
|
1169
|
+
align-items: center;
|
|
1170
|
+
display: flex;
|
|
1171
|
+
min-height: calc(100vh - 36px);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
.login-panel {
|
|
1175
|
+
background: var(--panel);
|
|
1176
|
+
border: 1px solid var(--line);
|
|
1044
1177
|
border-radius: 6px;
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
outline: none;
|
|
1048
|
-
padding: 0 12px;
|
|
1178
|
+
max-width: 420px;
|
|
1179
|
+
padding: 22px;
|
|
1049
1180
|
width: 100%;
|
|
1050
1181
|
}
|
|
1051
1182
|
|
|
1052
|
-
|
|
1053
|
-
|
|
1183
|
+
.login-panel h1 {
|
|
1184
|
+
margin-bottom: 18px;
|
|
1054
1185
|
}
|
|
1055
1186
|
|
|
1056
1187
|
.form-row {
|
|
1057
1188
|
display: flex;
|
|
1058
|
-
gap:
|
|
1059
|
-
margin-top:
|
|
1189
|
+
gap: 8px;
|
|
1190
|
+
margin-top: 14px;
|
|
1060
1191
|
}
|
|
1061
1192
|
|
|
1062
1193
|
.error {
|
|
@@ -1068,13 +1199,14 @@ function adminHtml() {
|
|
|
1068
1199
|
display: none;
|
|
1069
1200
|
}
|
|
1070
1201
|
|
|
1071
|
-
@media (max-width:
|
|
1202
|
+
@media (max-width: 960px) {
|
|
1072
1203
|
.shell {
|
|
1073
|
-
padding:
|
|
1204
|
+
padding: 14px;
|
|
1074
1205
|
}
|
|
1075
1206
|
|
|
1076
1207
|
.topbar,
|
|
1077
|
-
.panel-head
|
|
1208
|
+
.panel-head,
|
|
1209
|
+
.panel-head.tight {
|
|
1078
1210
|
align-items: flex-start;
|
|
1079
1211
|
flex-direction: column;
|
|
1080
1212
|
}
|
|
@@ -1083,19 +1215,14 @@ function adminHtml() {
|
|
|
1083
1215
|
justify-content: flex-start;
|
|
1084
1216
|
}
|
|
1085
1217
|
|
|
1086
|
-
.limit-form {
|
|
1087
|
-
grid-template-columns: 1fr;
|
|
1088
|
-
width: 100%;
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
1218
|
.metrics {
|
|
1092
1219
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1093
1220
|
}
|
|
1094
1221
|
|
|
1095
|
-
.
|
|
1096
|
-
.
|
|
1097
|
-
|
|
1098
|
-
|
|
1222
|
+
.limit-form,
|
|
1223
|
+
.detail-form {
|
|
1224
|
+
grid-template-columns: 1fr;
|
|
1225
|
+
width: 100%;
|
|
1099
1226
|
}
|
|
1100
1227
|
}
|
|
1101
1228
|
</style>
|
|
@@ -1124,12 +1251,18 @@ function adminHtml() {
|
|
|
1124
1251
|
<h1>Deploy monitor</h1>
|
|
1125
1252
|
</div>
|
|
1126
1253
|
<div class="actions">
|
|
1254
|
+
<span class="status-line" id="status-line">Loading</span>
|
|
1127
1255
|
<button class="button secondary" id="refresh-button" type="button">Refresh</button>
|
|
1128
1256
|
<button class="button secondary" id="logout-button" type="button">Lock</button>
|
|
1129
1257
|
</div>
|
|
1130
1258
|
</header>
|
|
1131
1259
|
|
|
1132
|
-
<
|
|
1260
|
+
<nav class="tabs" aria-label="Admin sections">
|
|
1261
|
+
<button class="tab" id="deployments-tab" type="button">Deployments</button>
|
|
1262
|
+
<button class="tab" id="users-tab" type="button">Users</button>
|
|
1263
|
+
</nav>
|
|
1264
|
+
|
|
1265
|
+
<section class="metrics" aria-label="Resource totals">
|
|
1133
1266
|
<div class="metric"><span>deploys</span><strong id="metric-deploys">0</strong></div>
|
|
1134
1267
|
<div class="metric"><span>artifact bytes</span><strong id="metric-artifacts">0 B</strong></div>
|
|
1135
1268
|
<div class="metric"><span>state bytes</span><strong id="metric-state">0 B</strong></div>
|
|
@@ -1138,35 +1271,40 @@ function adminHtml() {
|
|
|
1138
1271
|
<div class="metric"><span>mutations today</span><strong id="metric-mutations">0</strong></div>
|
|
1139
1272
|
</section>
|
|
1140
1273
|
|
|
1141
|
-
<section class="panel">
|
|
1274
|
+
<section class="panel" id="deployments-view">
|
|
1142
1275
|
<div class="panel-head">
|
|
1143
|
-
<
|
|
1144
|
-
|
|
1276
|
+
<div class="panel-title">
|
|
1277
|
+
<h2>Deployments</h2>
|
|
1278
|
+
<span class="mono" id="deployments-count">0 deploys</span>
|
|
1279
|
+
</div>
|
|
1145
1280
|
</div>
|
|
1146
1281
|
<div class="table-wrap">
|
|
1147
|
-
<table>
|
|
1282
|
+
<table class="deploy-table">
|
|
1148
1283
|
<thead>
|
|
1149
1284
|
<tr>
|
|
1150
1285
|
<th>Deploy</th>
|
|
1286
|
+
<th>User</th>
|
|
1151
1287
|
<th>Status</th>
|
|
1152
1288
|
<th>Created</th>
|
|
1153
1289
|
<th>Expires</th>
|
|
1154
|
-
<th>
|
|
1155
|
-
<th>
|
|
1156
|
-
<th>
|
|
1157
|
-
<th
|
|
1158
|
-
<th>Mutations</th>
|
|
1159
|
-
<th>Connections</th>
|
|
1290
|
+
<th>Usage</th>
|
|
1291
|
+
<th>Storage</th>
|
|
1292
|
+
<th>Conn</th>
|
|
1293
|
+
<th></th>
|
|
1160
1294
|
</tr>
|
|
1161
1295
|
</thead>
|
|
1162
1296
|
<tbody id="deploy-rows"></tbody>
|
|
1163
1297
|
</table>
|
|
1164
1298
|
</div>
|
|
1299
|
+
<div class="pager" id="deploy-pager"></div>
|
|
1165
1300
|
</section>
|
|
1166
1301
|
|
|
1167
|
-
<section class="panel">
|
|
1168
|
-
<div class="panel-head">
|
|
1169
|
-
<
|
|
1302
|
+
<section class="panel hidden" id="users-view">
|
|
1303
|
+
<div class="panel-head tight">
|
|
1304
|
+
<div class="panel-title">
|
|
1305
|
+
<h2>User limit overrides</h2>
|
|
1306
|
+
<span class="mono" id="users-count">0 users</span>
|
|
1307
|
+
</div>
|
|
1170
1308
|
<form class="limit-form" id="new-user-limit-form">
|
|
1171
1309
|
<div class="field">
|
|
1172
1310
|
<label for="new-user-id">User id</label>
|
|
@@ -1180,40 +1318,121 @@ function adminHtml() {
|
|
|
1180
1318
|
<label for="new-user-mutations">Mutations</label>
|
|
1181
1319
|
<input id="new-user-mutations" name="mutationsPerDay" inputmode="numeric" min="1" step="1" type="number" />
|
|
1182
1320
|
</div>
|
|
1321
|
+
<label class="checkbox" for="new-user-boost">
|
|
1322
|
+
<input id="new-user-boost" name="boost" type="checkbox" aria-label="Boost new user" />
|
|
1323
|
+
Boost
|
|
1324
|
+
</label>
|
|
1183
1325
|
<button class="button secondary" type="submit">Save</button>
|
|
1184
1326
|
</form>
|
|
1185
1327
|
</div>
|
|
1186
1328
|
<div class="table-wrap">
|
|
1187
|
-
<table>
|
|
1329
|
+
<table class="user-table">
|
|
1188
1330
|
<thead>
|
|
1189
1331
|
<tr>
|
|
1190
1332
|
<th>User</th>
|
|
1333
|
+
<th>Created</th>
|
|
1334
|
+
<th>Boost</th>
|
|
1191
1335
|
<th>Deploys</th>
|
|
1192
|
-
<th>
|
|
1193
|
-
<th>
|
|
1194
|
-
<th>Request limit</th>
|
|
1195
|
-
<th>Mutation limit</th>
|
|
1336
|
+
<th>Usage</th>
|
|
1337
|
+
<th>Limits</th>
|
|
1196
1338
|
<th>Actions</th>
|
|
1197
1339
|
</tr>
|
|
1198
1340
|
</thead>
|
|
1199
1341
|
<tbody id="user-rows"></tbody>
|
|
1200
1342
|
</table>
|
|
1201
1343
|
</div>
|
|
1344
|
+
<div class="pager" id="user-pager"></div>
|
|
1345
|
+
</section>
|
|
1346
|
+
|
|
1347
|
+
<section class="hidden" id="user-detail-view">
|
|
1348
|
+
<section class="panel">
|
|
1349
|
+
<div class="panel-head tight">
|
|
1350
|
+
<div class="panel-title">
|
|
1351
|
+
<a class="mono" data-route="users" href="/admin/users">Back to users</a>
|
|
1352
|
+
<div class="name-line">
|
|
1353
|
+
<h2 id="detail-title">User</h2>
|
|
1354
|
+
<span id="detail-github"></span>
|
|
1355
|
+
</div>
|
|
1356
|
+
<span class="mono" id="detail-meta"></span>
|
|
1357
|
+
</div>
|
|
1358
|
+
<form class="detail-form" id="user-detail-form">
|
|
1359
|
+
<label class="checkbox" for="detail-boost">
|
|
1360
|
+
<input id="detail-boost" name="boost" type="checkbox" aria-label="Boost selected user" />
|
|
1361
|
+
Boost
|
|
1362
|
+
</label>
|
|
1363
|
+
<div class="field">
|
|
1364
|
+
<label for="detail-requests">Requests</label>
|
|
1365
|
+
<input id="detail-requests" name="requestsPerDay" inputmode="numeric" min="1" step="1" type="number" />
|
|
1366
|
+
</div>
|
|
1367
|
+
<div class="field">
|
|
1368
|
+
<label for="detail-mutations">Mutations</label>
|
|
1369
|
+
<input id="detail-mutations" name="mutationsPerDay" inputmode="numeric" min="1" step="1" type="number" />
|
|
1370
|
+
</div>
|
|
1371
|
+
<button class="button secondary" type="submit">Save</button>
|
|
1372
|
+
<button class="row-action" id="detail-clear-button" type="button">Clear</button>
|
|
1373
|
+
</form>
|
|
1374
|
+
</div>
|
|
1375
|
+
</section>
|
|
1376
|
+
|
|
1377
|
+
<section class="panel">
|
|
1378
|
+
<div class="panel-head">
|
|
1379
|
+
<div class="panel-title">
|
|
1380
|
+
<h2>Deployments</h2>
|
|
1381
|
+
<span class="mono" id="user-deployments-count">0 deploys</span>
|
|
1382
|
+
</div>
|
|
1383
|
+
</div>
|
|
1384
|
+
<div class="table-wrap">
|
|
1385
|
+
<table class="deploy-table">
|
|
1386
|
+
<thead>
|
|
1387
|
+
<tr>
|
|
1388
|
+
<th>Deploy</th>
|
|
1389
|
+
<th>User</th>
|
|
1390
|
+
<th>Status</th>
|
|
1391
|
+
<th>Created</th>
|
|
1392
|
+
<th>Expires</th>
|
|
1393
|
+
<th>Usage</th>
|
|
1394
|
+
<th>Storage</th>
|
|
1395
|
+
<th>Conn</th>
|
|
1396
|
+
<th></th>
|
|
1397
|
+
</tr>
|
|
1398
|
+
</thead>
|
|
1399
|
+
<tbody id="user-deploy-rows"></tbody>
|
|
1400
|
+
</table>
|
|
1401
|
+
</div>
|
|
1402
|
+
<div class="pager" id="user-deploy-pager"></div>
|
|
1403
|
+
</section>
|
|
1202
1404
|
</section>
|
|
1203
1405
|
</section>
|
|
1204
1406
|
</main>
|
|
1205
1407
|
|
|
1206
1408
|
<script>
|
|
1409
|
+
const pageSize = 20;
|
|
1207
1410
|
const loginView = document.getElementById("login-view");
|
|
1208
1411
|
const dashboardView = document.getElementById("dashboard-view");
|
|
1209
1412
|
const loginForm = document.getElementById("login-form");
|
|
1210
1413
|
const loginError = document.getElementById("login-error");
|
|
1414
|
+
const statusLine = document.getElementById("status-line");
|
|
1415
|
+
const deploymentsView = document.getElementById("deployments-view");
|
|
1416
|
+
const usersView = document.getElementById("users-view");
|
|
1417
|
+
const userDetailView = document.getElementById("user-detail-view");
|
|
1211
1418
|
const rows = document.getElementById("deploy-rows");
|
|
1212
1419
|
const userRows = document.getElementById("user-rows");
|
|
1420
|
+
const userDeployRows = document.getElementById("user-deploy-rows");
|
|
1421
|
+
const deployPager = document.getElementById("deploy-pager");
|
|
1422
|
+
const userPager = document.getElementById("user-pager");
|
|
1423
|
+
const userDeployPager = document.getElementById("user-deploy-pager");
|
|
1213
1424
|
const newUserLimitForm = document.getElementById("new-user-limit-form");
|
|
1214
|
-
const
|
|
1215
|
-
|
|
1216
|
-
|
|
1425
|
+
const userDetailForm = document.getElementById("user-detail-form");
|
|
1426
|
+
const state = {
|
|
1427
|
+
deployPage: 1,
|
|
1428
|
+
loadingUserId: null,
|
|
1429
|
+
savingUserId: null,
|
|
1430
|
+
summary: null,
|
|
1431
|
+
terminatingDeployId: null,
|
|
1432
|
+
userDeployPage: 1,
|
|
1433
|
+
userDetail: null,
|
|
1434
|
+
userPage: 1
|
|
1435
|
+
};
|
|
1217
1436
|
|
|
1218
1437
|
function show(view) {
|
|
1219
1438
|
loginView.classList.toggle("hidden", view !== "login");
|
|
@@ -1240,21 +1459,104 @@ function adminHtml() {
|
|
|
1240
1459
|
return "unknown";
|
|
1241
1460
|
}
|
|
1242
1461
|
return new Intl.DateTimeFormat(undefined, {
|
|
1243
|
-
dateStyle: "
|
|
1462
|
+
dateStyle: "short",
|
|
1244
1463
|
timeStyle: "short"
|
|
1245
1464
|
}).format(new Date(value));
|
|
1246
1465
|
}
|
|
1247
1466
|
|
|
1467
|
+
function plural(count, label) {
|
|
1468
|
+
return formatNumber(count) + " " + label + (count === 1 ? "" : "s");
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
function shortId(value) {
|
|
1472
|
+
return String(value || "").slice(0, 8);
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1248
1475
|
function setMetric(id, value) {
|
|
1249
1476
|
document.getElementById(id).textContent = value;
|
|
1250
1477
|
}
|
|
1251
1478
|
|
|
1479
|
+
function textCell(value, className) {
|
|
1480
|
+
const td = document.createElement("td");
|
|
1481
|
+
td.textContent = value;
|
|
1482
|
+
if (className) {
|
|
1483
|
+
td.className = className;
|
|
1484
|
+
}
|
|
1485
|
+
return td;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
function stackCell(lines, className) {
|
|
1489
|
+
const td = document.createElement("td");
|
|
1490
|
+
const wrap = document.createElement("div");
|
|
1491
|
+
wrap.className = "stack" + (className ? " " + className : "");
|
|
1492
|
+
for (const line of lines) {
|
|
1493
|
+
const item = document.createElement("span");
|
|
1494
|
+
item.textContent = line;
|
|
1495
|
+
wrap.appendChild(item);
|
|
1496
|
+
}
|
|
1497
|
+
td.appendChild(wrap);
|
|
1498
|
+
return td;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
function externalLink(link) {
|
|
1502
|
+
link.target = "_blank";
|
|
1503
|
+
link.rel = "noopener noreferrer";
|
|
1504
|
+
return link;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
function githubProfileLink(url, label) {
|
|
1508
|
+
if (!url) {
|
|
1509
|
+
return null;
|
|
1510
|
+
}
|
|
1511
|
+
const link = document.createElement("a");
|
|
1512
|
+
link.className = "github-link";
|
|
1513
|
+
link.href = url;
|
|
1514
|
+
link.title = "Open GitHub profile";
|
|
1515
|
+
link.setAttribute("aria-label", "Open GitHub profile for " + label);
|
|
1516
|
+
link.innerHTML = '<svg viewBox="0 0 16 16" aria-hidden="true" fill="currentColor"><path d="M8 0C3.58 0 0 3.67 0 8.19c0 3.62 2.29 6.69 5.47 7.78.4.08.55-.18.55-.4 0-.2-.01-.84-.01-1.53-2.01.38-2.53-.5-2.69-.96-.09-.24-.48-.96-.82-1.16-.28-.15-.68-.53-.01-.54.63-.01 1.08.59 1.23.84.72 1.24 1.87.89 2.33.68.07-.53.28-.89.51-1.1-1.78-.21-3.64-.91-3.64-4.04 0-.89.31-1.62.82-2.19-.08-.21-.36-1.04.08-2.16 0 0 .67-.22 2.2.84A7.37 7.37 0 0 1 8 3.98c.68 0 1.36.09 2 .27 1.53-1.06 2.2-.84 2.2-.84.44 1.12.16 1.95.08 2.16.51.57.82 1.3.82 2.19 0 3.14-1.87 3.83-3.65 4.04.29.26.54.75.54 1.52 0 1.1-.01 1.98-.01 2.25 0 .22.15.48.55.4A8.06 8.06 0 0 0 16 8.19C16 3.67 12.42 0 8 0Z"/></svg>';
|
|
1517
|
+
return externalLink(link);
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
function routeFor(view, userId) {
|
|
1521
|
+
if (view === "user") {
|
|
1522
|
+
return "/admin/users/" + encodeURIComponent(userId);
|
|
1523
|
+
}
|
|
1524
|
+
if (view === "users") {
|
|
1525
|
+
return "/admin/users";
|
|
1526
|
+
}
|
|
1527
|
+
return "/admin/deployments";
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
function parseRoute() {
|
|
1531
|
+
const userMatch = window.location.pathname.match(/^\\/admin\\/users\\/(.+)$/);
|
|
1532
|
+
if (userMatch) {
|
|
1533
|
+
return { userId: decodeURIComponent(userMatch[1]), view: "user" };
|
|
1534
|
+
}
|
|
1535
|
+
if (window.location.pathname === "/admin/users") {
|
|
1536
|
+
return { view: "users" };
|
|
1537
|
+
}
|
|
1538
|
+
if (window.location.pathname === "/admin/deployments") {
|
|
1539
|
+
return { view: "deployments" };
|
|
1540
|
+
}
|
|
1541
|
+
if (new URLSearchParams(window.location.search).get("tab") === "deployments") {
|
|
1542
|
+
return { view: "deployments" };
|
|
1543
|
+
}
|
|
1544
|
+
if (new URLSearchParams(window.location.search).get("tab") === "users") {
|
|
1545
|
+
return { view: "users" };
|
|
1546
|
+
}
|
|
1547
|
+
return { view: "users" };
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
function navigate(view, userId) {
|
|
1551
|
+
history.pushState({}, "", routeFor(view, userId));
|
|
1552
|
+
renderCurrent();
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1252
1555
|
function parseLimitValue(value, name) {
|
|
1253
1556
|
const trimmed = String(value || "").trim();
|
|
1254
1557
|
if (!trimmed) {
|
|
1255
1558
|
return null;
|
|
1256
1559
|
}
|
|
1257
|
-
|
|
1258
1560
|
const parsed = Number(trimmed);
|
|
1259
1561
|
if (!Number.isSafeInteger(parsed) || parsed < 1) {
|
|
1260
1562
|
throw new Error(name + " must be a positive integer.");
|
|
@@ -1262,87 +1564,241 @@ function adminHtml() {
|
|
|
1262
1564
|
return parsed;
|
|
1263
1565
|
}
|
|
1264
1566
|
|
|
1265
|
-
function
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1567
|
+
function pageCount(total) {
|
|
1568
|
+
return Math.max(1, Math.ceil(total / pageSize));
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
function clampPage(page, total) {
|
|
1572
|
+
return Math.min(Math.max(1, page), pageCount(total));
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
function pageItems(items, page) {
|
|
1576
|
+
const currentPage = clampPage(page, items.length);
|
|
1577
|
+
const start = (currentPage - 1) * pageSize;
|
|
1578
|
+
return {
|
|
1579
|
+
currentPage,
|
|
1580
|
+
items: items.slice(start, start + pageSize),
|
|
1581
|
+
start
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
function renderPager(container, total, page, onPage) {
|
|
1586
|
+
container.replaceChildren();
|
|
1587
|
+
if (total <= pageSize) {
|
|
1588
|
+
return;
|
|
1270
1589
|
}
|
|
1271
|
-
|
|
1590
|
+
const currentPage = clampPage(page, total);
|
|
1591
|
+
const start = (currentPage - 1) * pageSize + 1;
|
|
1592
|
+
const end = Math.min(total, currentPage * pageSize);
|
|
1593
|
+
const label = document.createElement("span");
|
|
1594
|
+
const previous = document.createElement("button");
|
|
1595
|
+
const next = document.createElement("button");
|
|
1596
|
+
label.textContent = formatNumber(start) + "-" + formatNumber(end) + " of " + formatNumber(total);
|
|
1597
|
+
previous.className = "row-action";
|
|
1598
|
+
previous.type = "button";
|
|
1599
|
+
previous.textContent = "Prev";
|
|
1600
|
+
previous.disabled = currentPage <= 1;
|
|
1601
|
+
previous.addEventListener("click", () => onPage(currentPage - 1));
|
|
1602
|
+
next.className = "row-action";
|
|
1603
|
+
next.type = "button";
|
|
1604
|
+
next.textContent = "Next";
|
|
1605
|
+
next.disabled = currentPage >= pageCount(total);
|
|
1606
|
+
next.addEventListener("click", () => onPage(currentPage + 1));
|
|
1607
|
+
container.appendChild(label);
|
|
1608
|
+
container.appendChild(previous);
|
|
1609
|
+
container.appendChild(next);
|
|
1272
1610
|
}
|
|
1273
1611
|
|
|
1274
1612
|
function deployCell(deploy) {
|
|
1275
1613
|
const td = document.createElement("td");
|
|
1276
1614
|
const wrap = document.createElement("div");
|
|
1277
|
-
const name = document.createElement("
|
|
1278
|
-
const link = document.createElement("a");
|
|
1615
|
+
const name = document.createElement("a");
|
|
1279
1616
|
const id = document.createElement("small");
|
|
1280
|
-
wrap.className = "
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
name.
|
|
1284
|
-
|
|
1617
|
+
wrap.className = "name-cell";
|
|
1618
|
+
name.className = "primary-text";
|
|
1619
|
+
name.href = deploy.url;
|
|
1620
|
+
name.textContent = deploy.name || deploy.slug;
|
|
1621
|
+
externalLink(name);
|
|
1622
|
+
id.textContent = shortId(deploy.id) + " / " + deploy.slug;
|
|
1285
1623
|
wrap.appendChild(name);
|
|
1286
1624
|
wrap.appendChild(id);
|
|
1287
1625
|
td.appendChild(wrap);
|
|
1288
1626
|
return td;
|
|
1289
1627
|
}
|
|
1290
1628
|
|
|
1291
|
-
function
|
|
1629
|
+
function deployUserCell(deploy) {
|
|
1292
1630
|
const td = document.createElement("td");
|
|
1293
1631
|
const wrap = document.createElement("div");
|
|
1632
|
+
wrap.className = "user-cell";
|
|
1633
|
+
if (!deploy.ownerId) {
|
|
1634
|
+
const anon = document.createElement("span");
|
|
1635
|
+
anon.className = "mono";
|
|
1636
|
+
anon.textContent = "anonymous";
|
|
1637
|
+
wrap.appendChild(anon);
|
|
1638
|
+
td.appendChild(wrap);
|
|
1639
|
+
return td;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
const line = document.createElement("div");
|
|
1643
|
+
const link = document.createElement("a");
|
|
1644
|
+
const id = document.createElement("small");
|
|
1645
|
+
const label = deploy.owner?.login || deploy.owner?.displayName || deploy.ownerId;
|
|
1646
|
+
line.className = "name-line";
|
|
1647
|
+
link.className = "primary-text";
|
|
1648
|
+
link.dataset.route = "user";
|
|
1649
|
+
link.href = routeFor("user", deploy.ownerId);
|
|
1650
|
+
link.textContent = label;
|
|
1651
|
+
id.textContent = deploy.ownerId;
|
|
1652
|
+
line.appendChild(link);
|
|
1653
|
+
const github = githubProfileLink(deploy.owner?.url, label);
|
|
1654
|
+
if (github) {
|
|
1655
|
+
line.appendChild(github);
|
|
1656
|
+
}
|
|
1657
|
+
wrap.appendChild(line);
|
|
1658
|
+
wrap.appendChild(id);
|
|
1659
|
+
td.appendChild(wrap);
|
|
1660
|
+
return td;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
function statusCell(deploy) {
|
|
1664
|
+
const td = document.createElement("td");
|
|
1294
1665
|
const pill = document.createElement("span");
|
|
1295
|
-
wrap.className = "status-cell";
|
|
1296
1666
|
pill.className = "pill " + deploy.status;
|
|
1297
1667
|
pill.textContent = deploy.status;
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1668
|
+
td.appendChild(pill);
|
|
1669
|
+
return td;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
function usageCell(deploy) {
|
|
1673
|
+
return stackCell(
|
|
1674
|
+
[
|
|
1675
|
+
"R " + formatNumber(deploy.requestsToday) + " / " + formatNumber(deploy.limits.requestsPerDay),
|
|
1676
|
+
"M " + formatNumber(deploy.mutationsToday) + " / " + formatNumber(deploy.limits.mutationsPerDay)
|
|
1677
|
+
],
|
|
1678
|
+
"mono"
|
|
1679
|
+
);
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
function storageCell(deploy) {
|
|
1683
|
+
return stackCell(
|
|
1684
|
+
[
|
|
1685
|
+
formatNumber(deploy.stateRows) + " rows / " + formatBytes(deploy.stateBytes),
|
|
1686
|
+
"artifact " + formatBytes(deploy.artifactBytes)
|
|
1687
|
+
],
|
|
1688
|
+
"mono"
|
|
1689
|
+
);
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
function actionCell(deploy) {
|
|
1693
|
+
const td = document.createElement("td");
|
|
1694
|
+
if (deploy.status !== "active") {
|
|
1695
|
+
return td;
|
|
1696
|
+
}
|
|
1697
|
+
const button = document.createElement("button");
|
|
1698
|
+
button.className = "icon-button danger";
|
|
1699
|
+
button.type = "button";
|
|
1700
|
+
button.title = "Terminate deploy";
|
|
1701
|
+
button.setAttribute("aria-label", "Terminate deploy " + deploy.id);
|
|
1702
|
+
button.disabled = state.terminatingDeployId === deploy.id;
|
|
1703
|
+
button.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>';
|
|
1704
|
+
button.addEventListener("click", () => {
|
|
1705
|
+
void terminateDeploy(deploy).catch((error) => {
|
|
1706
|
+
statusLine.textContent = error instanceof Error ? error.message : String(error);
|
|
1310
1707
|
});
|
|
1311
|
-
|
|
1708
|
+
});
|
|
1709
|
+
td.appendChild(button);
|
|
1710
|
+
return td;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
function renderDeployRows(tbody, deploys, emptyText) {
|
|
1714
|
+
tbody.replaceChildren();
|
|
1715
|
+
if (!deploys.length) {
|
|
1716
|
+
const tr = document.createElement("tr");
|
|
1717
|
+
const td = textCell(emptyText, "mono");
|
|
1718
|
+
td.colSpan = 9;
|
|
1719
|
+
tr.appendChild(td);
|
|
1720
|
+
tbody.appendChild(tr);
|
|
1721
|
+
return;
|
|
1312
1722
|
}
|
|
1313
1723
|
|
|
1314
|
-
|
|
1315
|
-
|
|
1724
|
+
for (const deploy of deploys) {
|
|
1725
|
+
const tr = document.createElement("tr");
|
|
1726
|
+
tr.appendChild(deployCell(deploy));
|
|
1727
|
+
tr.appendChild(deployUserCell(deploy));
|
|
1728
|
+
tr.appendChild(statusCell(deploy));
|
|
1729
|
+
tr.appendChild(textCell(formatTime(deploy.createdAt), "mono"));
|
|
1730
|
+
tr.appendChild(textCell(formatTime(deploy.expiresAt), "mono"));
|
|
1731
|
+
tr.appendChild(usageCell(deploy));
|
|
1732
|
+
tr.appendChild(storageCell(deploy));
|
|
1733
|
+
tr.appendChild(textCell(formatNumber(deploy.connections), "mono"));
|
|
1734
|
+
tr.appendChild(actionCell(deploy));
|
|
1735
|
+
tbody.appendChild(tr);
|
|
1736
|
+
}
|
|
1316
1737
|
}
|
|
1317
1738
|
|
|
1318
1739
|
function userCell(user) {
|
|
1319
1740
|
const td = document.createElement("td");
|
|
1320
1741
|
const wrap = document.createElement("div");
|
|
1321
|
-
const
|
|
1742
|
+
const line = document.createElement("div");
|
|
1743
|
+
const name = document.createElement("a");
|
|
1322
1744
|
const id = document.createElement("small");
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
name.textContent = user.login || user.displayName || user.id;
|
|
1331
|
-
}
|
|
1332
|
-
id.className = "mono";
|
|
1745
|
+
const label = user.login || user.displayName || user.id;
|
|
1746
|
+
wrap.className = "user-cell";
|
|
1747
|
+
line.className = "name-line";
|
|
1748
|
+
name.className = "primary-text";
|
|
1749
|
+
name.dataset.route = "user";
|
|
1750
|
+
name.href = routeFor("user", user.id);
|
|
1751
|
+
name.textContent = label;
|
|
1333
1752
|
id.textContent = user.id;
|
|
1334
|
-
|
|
1753
|
+
line.appendChild(name);
|
|
1754
|
+
const github = githubProfileLink(user.url, label);
|
|
1755
|
+
if (github) {
|
|
1756
|
+
line.appendChild(github);
|
|
1757
|
+
}
|
|
1758
|
+
wrap.appendChild(line);
|
|
1335
1759
|
wrap.appendChild(id);
|
|
1336
1760
|
td.appendChild(wrap);
|
|
1337
1761
|
return td;
|
|
1338
1762
|
}
|
|
1339
1763
|
|
|
1340
|
-
function
|
|
1764
|
+
function boostCell(user) {
|
|
1341
1765
|
const td = document.createElement("td");
|
|
1342
|
-
const
|
|
1766
|
+
const label = document.createElement("label");
|
|
1767
|
+
const input = document.createElement("input");
|
|
1768
|
+
label.className = "checkbox";
|
|
1769
|
+
input.type = "checkbox";
|
|
1770
|
+
input.dataset.key = "boost";
|
|
1771
|
+
input.checked = Boolean(user.boost);
|
|
1772
|
+
input.setAttribute("aria-label", "Boost " + user.id);
|
|
1773
|
+
input.addEventListener("change", () => {
|
|
1774
|
+
void saveUserSettings(user.id, input.closest("tr")).catch((error) => {
|
|
1775
|
+
statusLine.textContent = error instanceof Error ? error.message : String(error);
|
|
1776
|
+
});
|
|
1777
|
+
});
|
|
1778
|
+
label.appendChild(input);
|
|
1779
|
+
label.appendChild(document.createTextNode("20x"));
|
|
1780
|
+
td.appendChild(label);
|
|
1781
|
+
return td;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
function userUsageCell(user) {
|
|
1785
|
+
return stackCell(
|
|
1786
|
+
[
|
|
1787
|
+
"R " + formatNumber(user.requestsToday) + " / " + formatNumber(user.limits.requestsPerDay),
|
|
1788
|
+
"M " + formatNumber(user.mutationsToday) + " / " + formatNumber(user.limits.mutationsPerDay)
|
|
1789
|
+
],
|
|
1790
|
+
"mono usage-cell"
|
|
1791
|
+
);
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
function limitControl(user, key, label, shortLabel) {
|
|
1795
|
+
const row = document.createElement("div");
|
|
1796
|
+
const prefix = document.createElement("span");
|
|
1343
1797
|
const input = document.createElement("input");
|
|
1344
1798
|
const effective = document.createElement("small");
|
|
1345
|
-
|
|
1799
|
+
row.className = "limit-row";
|
|
1800
|
+
prefix.className = "mono";
|
|
1801
|
+
prefix.textContent = shortLabel;
|
|
1346
1802
|
input.inputMode = "numeric";
|
|
1347
1803
|
input.min = "1";
|
|
1348
1804
|
input.step = "1";
|
|
@@ -1351,10 +1807,19 @@ function adminHtml() {
|
|
|
1351
1807
|
input.value = user.limitOverrides[key] ? String(user.limitOverrides[key]) : "";
|
|
1352
1808
|
input.placeholder = String(user.defaultLimits[key]);
|
|
1353
1809
|
input.setAttribute("aria-label", label + " override for " + user.id);
|
|
1354
|
-
effective.
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1810
|
+
effective.textContent = formatNumber(user.limits[key]);
|
|
1811
|
+
row.appendChild(prefix);
|
|
1812
|
+
row.appendChild(input);
|
|
1813
|
+
row.appendChild(effective);
|
|
1814
|
+
return row;
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
function userLimitsCell(user) {
|
|
1818
|
+
const td = document.createElement("td");
|
|
1819
|
+
const wrap = document.createElement("div");
|
|
1820
|
+
wrap.className = "limit-combo";
|
|
1821
|
+
wrap.appendChild(limitControl(user, "requestsPerDay", "Request limit", "R"));
|
|
1822
|
+
wrap.appendChild(limitControl(user, "mutationsPerDay", "Mutation limit", "M"));
|
|
1358
1823
|
td.appendChild(wrap);
|
|
1359
1824
|
return td;
|
|
1360
1825
|
}
|
|
@@ -1365,21 +1830,24 @@ function adminHtml() {
|
|
|
1365
1830
|
const save = document.createElement("button");
|
|
1366
1831
|
const clear = document.createElement("button");
|
|
1367
1832
|
wrap.className = "limit-actions";
|
|
1368
|
-
save.className = "row-action
|
|
1833
|
+
save.className = "row-action";
|
|
1369
1834
|
save.type = "button";
|
|
1370
|
-
save.textContent = savingUserId === user.id ? "Saving" : "Save";
|
|
1371
|
-
save.disabled = savingUserId === user.id;
|
|
1835
|
+
save.textContent = state.savingUserId === user.id ? "Saving" : "Save";
|
|
1836
|
+
save.disabled = state.savingUserId === user.id;
|
|
1372
1837
|
save.addEventListener("click", () => {
|
|
1373
|
-
void
|
|
1838
|
+
void saveUserSettings(user.id, save.closest("tr")).catch((error) => {
|
|
1374
1839
|
statusLine.textContent = error instanceof Error ? error.message : String(error);
|
|
1375
1840
|
});
|
|
1376
1841
|
});
|
|
1377
|
-
clear.className = "row-action
|
|
1842
|
+
clear.className = "row-action";
|
|
1378
1843
|
clear.type = "button";
|
|
1379
1844
|
clear.textContent = "Clear";
|
|
1380
|
-
clear.disabled = savingUserId === user.id;
|
|
1845
|
+
clear.disabled = state.savingUserId === user.id;
|
|
1381
1846
|
clear.addEventListener("click", () => {
|
|
1382
|
-
void
|
|
1847
|
+
void saveUserSettings(user.id, null, {
|
|
1848
|
+
boost: Boolean(user.boost),
|
|
1849
|
+
limits: { requestsPerDay: null, mutationsPerDay: null }
|
|
1850
|
+
}).catch((error) => {
|
|
1383
1851
|
statusLine.textContent = error instanceof Error ? error.message : String(error);
|
|
1384
1852
|
});
|
|
1385
1853
|
});
|
|
@@ -1389,7 +1857,7 @@ function adminHtml() {
|
|
|
1389
1857
|
return td;
|
|
1390
1858
|
}
|
|
1391
1859
|
|
|
1392
|
-
function
|
|
1860
|
+
function renderMetrics(summary) {
|
|
1393
1861
|
setMetric("metric-deploys", formatNumber(summary.deployCount));
|
|
1394
1862
|
setMetric("metric-artifacts", formatBytes(summary.totals.artifactBytes));
|
|
1395
1863
|
setMetric("metric-state", formatBytes(summary.totals.stateBytes));
|
|
@@ -1397,42 +1865,128 @@ function adminHtml() {
|
|
|
1397
1865
|
setMetric("metric-requests", formatNumber(summary.totals.requestsToday));
|
|
1398
1866
|
setMetric("metric-mutations", formatNumber(summary.totals.mutationsToday));
|
|
1399
1867
|
statusLine.textContent = "Updated " + formatTime(summary.generatedAt);
|
|
1400
|
-
|
|
1868
|
+
}
|
|
1401
1869
|
|
|
1402
|
-
|
|
1870
|
+
function renderDeployments() {
|
|
1871
|
+
const deploys = state.summary?.deploys || [];
|
|
1872
|
+
state.deployPage = clampPage(state.deployPage, deploys.length);
|
|
1873
|
+
const page = pageItems(deploys, state.deployPage);
|
|
1874
|
+
document.getElementById("deployments-count").textContent = plural(deploys.length, "deploy");
|
|
1875
|
+
renderDeployRows(rows, page.items, "No deployments yet.");
|
|
1876
|
+
renderPager(deployPager, deploys.length, state.deployPage, (nextPage) => {
|
|
1877
|
+
state.deployPage = nextPage;
|
|
1878
|
+
renderDeployments();
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
function renderUsers() {
|
|
1883
|
+
const users = state.summary?.users || [];
|
|
1884
|
+
state.userPage = clampPage(state.userPage, users.length);
|
|
1885
|
+
const page = pageItems(users, state.userPage);
|
|
1886
|
+
document.getElementById("users-count").textContent = plural(users.length, "user");
|
|
1887
|
+
userRows.replaceChildren();
|
|
1888
|
+
|
|
1889
|
+
if (!page.items.length) {
|
|
1403
1890
|
const tr = document.createElement("tr");
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
tr.appendChild(
|
|
1407
|
-
|
|
1408
|
-
tr.appendChild(textCell(formatBytes(deploy.artifactBytes), "mono"));
|
|
1409
|
-
tr.appendChild(textCell(formatBytes(deploy.stateBytes) + " / " + formatNumber(deploy.stateRows) + " rows", "mono"));
|
|
1410
|
-
tr.appendChild(textCell(formatBytes(deploy.logBytes) + " / " + formatNumber(deploy.logEntries) + " entries", "mono"));
|
|
1411
|
-
tr.appendChild(textCell(formatNumber(deploy.requestsToday) + " / " + formatNumber(deploy.limits.requestsPerDay), "mono"));
|
|
1412
|
-
tr.appendChild(textCell(formatNumber(deploy.mutationsToday) + " / " + formatNumber(deploy.limits.mutationsPerDay), "mono"));
|
|
1413
|
-
tr.appendChild(textCell(formatNumber(deploy.connections), "mono"));
|
|
1414
|
-
rows.appendChild(tr);
|
|
1891
|
+
const td = textCell("No claimed users or manual overrides yet.", "mono");
|
|
1892
|
+
td.colSpan = 7;
|
|
1893
|
+
tr.appendChild(td);
|
|
1894
|
+
userRows.appendChild(tr);
|
|
1415
1895
|
}
|
|
1416
1896
|
|
|
1417
|
-
|
|
1418
|
-
for (const user of summary.users || []) {
|
|
1897
|
+
for (const user of page.items) {
|
|
1419
1898
|
const tr = document.createElement("tr");
|
|
1420
1899
|
tr.appendChild(userCell(user));
|
|
1900
|
+
tr.appendChild(textCell(formatTime(user.createdAt), "mono"));
|
|
1901
|
+
tr.appendChild(boostCell(user));
|
|
1421
1902
|
tr.appendChild(textCell(formatNumber(user.activeDeployCount) + " / " + formatNumber(user.deployCount), "mono"));
|
|
1422
|
-
tr.appendChild(
|
|
1423
|
-
tr.appendChild(
|
|
1424
|
-
tr.appendChild(limitInputCell(user, "requestsPerDay", "Request limit"));
|
|
1425
|
-
tr.appendChild(limitInputCell(user, "mutationsPerDay", "Mutation limit"));
|
|
1903
|
+
tr.appendChild(userUsageCell(user));
|
|
1904
|
+
tr.appendChild(userLimitsCell(user));
|
|
1426
1905
|
tr.appendChild(userActionCell(user));
|
|
1427
1906
|
userRows.appendChild(tr);
|
|
1428
1907
|
}
|
|
1429
1908
|
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1909
|
+
renderPager(userPager, users.length, state.userPage, (nextPage) => {
|
|
1910
|
+
state.userPage = nextPage;
|
|
1911
|
+
renderUsers();
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
function fillDetailForm(user) {
|
|
1916
|
+
userDetailForm.dataset.userId = user.id;
|
|
1917
|
+
document.getElementById("detail-boost").checked = Boolean(user.boost);
|
|
1918
|
+
document.getElementById("detail-requests").value = user.limitOverrides.requestsPerDay ? String(user.limitOverrides.requestsPerDay) : "";
|
|
1919
|
+
document.getElementById("detail-requests").placeholder = String(user.defaultLimits.requestsPerDay);
|
|
1920
|
+
document.getElementById("detail-mutations").value = user.limitOverrides.mutationsPerDay ? String(user.limitOverrides.mutationsPerDay) : "";
|
|
1921
|
+
document.getElementById("detail-mutations").placeholder = String(user.defaultLimits.mutationsPerDay);
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
function renderUserDetail(userId) {
|
|
1925
|
+
const detail = state.userDetail?.user?.id === userId ? state.userDetail : null;
|
|
1926
|
+
if (!detail) {
|
|
1927
|
+
document.getElementById("detail-title").textContent = "Loading user";
|
|
1928
|
+
document.getElementById("detail-github").replaceChildren();
|
|
1929
|
+
document.getElementById("detail-meta").textContent = userId;
|
|
1930
|
+
renderDeployRows(userDeployRows, [], "Loading deployments.");
|
|
1931
|
+
userDeployPager.replaceChildren();
|
|
1932
|
+
if (state.loadingUserId !== userId) {
|
|
1933
|
+
void loadUserDetail(userId).catch((error) => {
|
|
1934
|
+
statusLine.textContent = error instanceof Error ? error.message : String(error);
|
|
1935
|
+
});
|
|
1936
|
+
}
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
const user = detail.user;
|
|
1941
|
+
const deploys = detail.deploys || [];
|
|
1942
|
+
state.userDeployPage = clampPage(state.userDeployPage, deploys.length);
|
|
1943
|
+
const page = pageItems(deploys, state.userDeployPage);
|
|
1944
|
+
const userLabel = user.login || user.displayName || user.id;
|
|
1945
|
+
const detailGithub = document.getElementById("detail-github");
|
|
1946
|
+
document.getElementById("detail-title").textContent = userLabel;
|
|
1947
|
+
detailGithub.replaceChildren();
|
|
1948
|
+
const github = githubProfileLink(user.url, userLabel);
|
|
1949
|
+
if (github) {
|
|
1950
|
+
detailGithub.appendChild(github);
|
|
1951
|
+
}
|
|
1952
|
+
document.getElementById("detail-meta").textContent =
|
|
1953
|
+
user.id +
|
|
1954
|
+
" · created " +
|
|
1955
|
+
formatTime(user.createdAt) +
|
|
1956
|
+
" · " +
|
|
1957
|
+
formatNumber(user.activeDeployCount) +
|
|
1958
|
+
" active / " +
|
|
1959
|
+
formatNumber(user.deployCount) +
|
|
1960
|
+
" total";
|
|
1961
|
+
document.getElementById("user-deployments-count").textContent = plural(deploys.length, "deploy");
|
|
1962
|
+
fillDetailForm(user);
|
|
1963
|
+
renderDeployRows(userDeployRows, page.items, "This user has no deployments.");
|
|
1964
|
+
renderPager(userDeployPager, deploys.length, state.userDeployPage, (nextPage) => {
|
|
1965
|
+
state.userDeployPage = nextPage;
|
|
1966
|
+
renderUserDetail(user.id);
|
|
1967
|
+
});
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
function renderCurrent() {
|
|
1971
|
+
const route = parseRoute();
|
|
1972
|
+
const isUsers = route.view === "users" || route.view === "user";
|
|
1973
|
+
document.getElementById("deployments-tab").classList.toggle("active", route.view === "deployments");
|
|
1974
|
+
document.getElementById("users-tab").classList.toggle("active", isUsers);
|
|
1975
|
+
deploymentsView.classList.toggle("hidden", route.view !== "deployments");
|
|
1976
|
+
usersView.classList.toggle("hidden", route.view !== "users");
|
|
1977
|
+
userDetailView.classList.toggle("hidden", route.view !== "user");
|
|
1978
|
+
|
|
1979
|
+
if (!state.summary) {
|
|
1980
|
+
return;
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
renderMetrics(state.summary);
|
|
1984
|
+
if (route.view === "deployments") {
|
|
1985
|
+
renderDeployments();
|
|
1986
|
+
} else if (route.view === "users") {
|
|
1987
|
+
renderUsers();
|
|
1988
|
+
} else {
|
|
1989
|
+
renderUserDetail(route.userId);
|
|
1436
1990
|
}
|
|
1437
1991
|
}
|
|
1438
1992
|
|
|
@@ -1450,21 +2004,47 @@ function adminHtml() {
|
|
|
1450
2004
|
if (!response.ok) {
|
|
1451
2005
|
throw new Error(await response.text());
|
|
1452
2006
|
}
|
|
1453
|
-
|
|
2007
|
+
state.summary = await response.json();
|
|
2008
|
+
state.userDetail = null;
|
|
1454
2009
|
show("dashboard");
|
|
2010
|
+
renderCurrent();
|
|
1455
2011
|
}
|
|
1456
2012
|
|
|
1457
|
-
async function
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
2013
|
+
async function loadUserDetail(userId) {
|
|
2014
|
+
state.loadingUserId = userId;
|
|
2015
|
+
try {
|
|
2016
|
+
const response = await fetch("/admin/api/users/" + encodeURIComponent(userId));
|
|
2017
|
+
if (response.status === 401) {
|
|
2018
|
+
show("login");
|
|
2019
|
+
return;
|
|
2020
|
+
}
|
|
2021
|
+
if (!response.ok) {
|
|
2022
|
+
throw new Error(await response.text());
|
|
2023
|
+
}
|
|
2024
|
+
state.userDetail = await response.json();
|
|
2025
|
+
} finally {
|
|
2026
|
+
state.loadingUserId = null;
|
|
2027
|
+
}
|
|
2028
|
+
renderCurrent();
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
function settingsFromRow(row) {
|
|
2032
|
+
return {
|
|
2033
|
+
boost: row.querySelector('input[data-key="boost"]').checked,
|
|
2034
|
+
limits: {
|
|
2035
|
+
requestsPerDay: parseLimitValue(row.querySelector('input[data-key="requestsPerDay"]').value, "Request limit"),
|
|
2036
|
+
mutationsPerDay: parseLimitValue(row.querySelector('input[data-key="mutationsPerDay"]').value, "Mutation limit")
|
|
2037
|
+
}
|
|
1461
2038
|
};
|
|
2039
|
+
}
|
|
1462
2040
|
|
|
1463
|
-
|
|
1464
|
-
|
|
2041
|
+
async function saveUserSettings(userId, row, explicitSettings) {
|
|
2042
|
+
const settings = explicitSettings || settingsFromRow(row);
|
|
2043
|
+
state.savingUserId = userId;
|
|
2044
|
+
statusLine.textContent = "Saving " + userId;
|
|
1465
2045
|
try {
|
|
1466
2046
|
const response = await fetch("/admin/api/users/" + encodeURIComponent(userId) + "/limits", {
|
|
1467
|
-
body: JSON.stringify(
|
|
2047
|
+
body: JSON.stringify(settings),
|
|
1468
2048
|
headers: { "Content-Type": "application/json" },
|
|
1469
2049
|
method: "PUT"
|
|
1470
2050
|
});
|
|
@@ -1478,7 +2058,7 @@ function adminHtml() {
|
|
|
1478
2058
|
throw new Error(await response.text());
|
|
1479
2059
|
}
|
|
1480
2060
|
} finally {
|
|
1481
|
-
savingUserId = null;
|
|
2061
|
+
state.savingUserId = null;
|
|
1482
2062
|
}
|
|
1483
2063
|
await loadSummary();
|
|
1484
2064
|
}
|
|
@@ -1488,12 +2068,12 @@ function adminHtml() {
|
|
|
1488
2068
|
return;
|
|
1489
2069
|
}
|
|
1490
2070
|
|
|
1491
|
-
terminatingDeployId = deploy.id;
|
|
2071
|
+
state.terminatingDeployId = deploy.id;
|
|
1492
2072
|
statusLine.textContent = "Terminating " + deploy.id;
|
|
1493
2073
|
const response = await fetch("/admin/api/deploys/" + encodeURIComponent(deploy.id) + "/terminate", {
|
|
1494
2074
|
method: "POST"
|
|
1495
2075
|
});
|
|
1496
|
-
terminatingDeployId = null;
|
|
2076
|
+
state.terminatingDeployId = null;
|
|
1497
2077
|
|
|
1498
2078
|
if (response.status === 401) {
|
|
1499
2079
|
show("login");
|
|
@@ -1535,16 +2115,20 @@ function adminHtml() {
|
|
|
1535
2115
|
}
|
|
1536
2116
|
|
|
1537
2117
|
try {
|
|
2118
|
+
const boost = document.getElementById("new-user-boost").checked;
|
|
1538
2119
|
const requestsPerDay = parseLimitValue(form.get("requestsPerDay"), "Request limit");
|
|
1539
2120
|
const mutationsPerDay = parseLimitValue(form.get("mutationsPerDay"), "Mutation limit");
|
|
1540
|
-
if (!requestsPerDay && !mutationsPerDay) {
|
|
1541
|
-
statusLine.textContent = "Enter
|
|
2121
|
+
if (!boost && !requestsPerDay && !mutationsPerDay) {
|
|
2122
|
+
statusLine.textContent = "Enter a custom limit or turn on boost.";
|
|
1542
2123
|
return;
|
|
1543
2124
|
}
|
|
1544
2125
|
|
|
1545
|
-
await
|
|
1546
|
-
|
|
1547
|
-
|
|
2126
|
+
await saveUserSettings(userId, null, {
|
|
2127
|
+
boost,
|
|
2128
|
+
limits: {
|
|
2129
|
+
requestsPerDay,
|
|
2130
|
+
mutationsPerDay
|
|
2131
|
+
}
|
|
1548
2132
|
});
|
|
1549
2133
|
newUserLimitForm.reset();
|
|
1550
2134
|
} catch (error) {
|
|
@@ -1552,15 +2136,60 @@ function adminHtml() {
|
|
|
1552
2136
|
}
|
|
1553
2137
|
});
|
|
1554
2138
|
|
|
2139
|
+
userDetailForm.addEventListener("submit", async (event) => {
|
|
2140
|
+
event.preventDefault();
|
|
2141
|
+
const userId = userDetailForm.dataset.userId;
|
|
2142
|
+
if (!userId) {
|
|
2143
|
+
return;
|
|
2144
|
+
}
|
|
2145
|
+
try {
|
|
2146
|
+
await saveUserSettings(userId, null, {
|
|
2147
|
+
boost: document.getElementById("detail-boost").checked,
|
|
2148
|
+
limits: {
|
|
2149
|
+
requestsPerDay: parseLimitValue(document.getElementById("detail-requests").value, "Request limit"),
|
|
2150
|
+
mutationsPerDay: parseLimitValue(document.getElementById("detail-mutations").value, "Mutation limit")
|
|
2151
|
+
}
|
|
2152
|
+
});
|
|
2153
|
+
} catch (error) {
|
|
2154
|
+
statusLine.textContent = error instanceof Error ? error.message : String(error);
|
|
2155
|
+
}
|
|
2156
|
+
});
|
|
2157
|
+
|
|
2158
|
+
document.getElementById("detail-clear-button").addEventListener("click", () => {
|
|
2159
|
+
const userId = userDetailForm.dataset.userId;
|
|
2160
|
+
if (!userId) {
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
2163
|
+
void saveUserSettings(userId, null, {
|
|
2164
|
+
boost: document.getElementById("detail-boost").checked,
|
|
2165
|
+
limits: { requestsPerDay: null, mutationsPerDay: null }
|
|
2166
|
+
}).catch((error) => {
|
|
2167
|
+
statusLine.textContent = error instanceof Error ? error.message : String(error);
|
|
2168
|
+
});
|
|
2169
|
+
});
|
|
2170
|
+
|
|
2171
|
+
document.getElementById("deployments-tab").addEventListener("click", () => navigate("deployments"));
|
|
2172
|
+
document.getElementById("users-tab").addEventListener("click", () => navigate("users"));
|
|
1555
2173
|
document.getElementById("refresh-button").addEventListener("click", () => {
|
|
1556
2174
|
void loadSummary();
|
|
1557
2175
|
});
|
|
1558
|
-
|
|
1559
2176
|
document.getElementById("logout-button").addEventListener("click", async () => {
|
|
1560
2177
|
await fetch("/admin/api/logout", { method: "POST" });
|
|
1561
2178
|
show("login");
|
|
1562
2179
|
});
|
|
1563
2180
|
|
|
2181
|
+
document.addEventListener("click", (event) => {
|
|
2182
|
+
const link = event.target.closest("a[data-route]");
|
|
2183
|
+
if (!link) {
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
event.preventDefault();
|
|
2187
|
+
history.pushState({}, "", link.href);
|
|
2188
|
+
renderCurrent();
|
|
2189
|
+
});
|
|
2190
|
+
|
|
2191
|
+
window.addEventListener("popstate", renderCurrent);
|
|
2192
|
+
|
|
1564
2193
|
void loadSummary().catch((error) => {
|
|
1565
2194
|
statusLine.textContent = error.message;
|
|
1566
2195
|
show("dashboard");
|
|
@@ -1602,7 +2231,7 @@ function developerHtml({ authConfigured, deploys = [], user }) {
|
|
|
1602
2231
|
const rows = deploys
|
|
1603
2232
|
.map(
|
|
1604
2233
|
(deploy) => `<tr>
|
|
1605
|
-
<td><a href="${escapeHtml(deploy.url)}">${escapeHtml(deploy.name)}</a><small>${escapeHtml(deploy.deployId)} / ${escapeHtml(deploy.slug)}</small></td>
|
|
2234
|
+
<td><a href="${escapeHtml(deploy.url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(deploy.name)}</a><small>${escapeHtml(deploy.deployId)} / ${escapeHtml(deploy.slug)}</small></td>
|
|
1606
2235
|
<td><span class="pill ${escapeHtml(deploy.status)}">${escapeHtml(deploy.status)}</span></td>
|
|
1607
2236
|
<td>${escapeHtml(new Date(deploy.createdAt).toLocaleString())}</td>
|
|
1608
2237
|
<td>${escapeHtml(new Date(deploy.expiresAt).toLocaleString())}</td>
|
|
@@ -1845,9 +2474,18 @@ export class MemoryAnonymousStore {
|
|
|
1845
2474
|
return normalizeUserLimitOverrides(this.users.get(normalizeUserId(userId))?.limitOverrides ?? {});
|
|
1846
2475
|
}
|
|
1847
2476
|
|
|
1848
|
-
async
|
|
2477
|
+
async getUserLimitSettings(userId) {
|
|
2478
|
+
const user = this.users.get(normalizeUserId(userId));
|
|
2479
|
+
return {
|
|
2480
|
+
boost: Boolean(user?.boost),
|
|
2481
|
+
limitOverrides: normalizeUserLimitOverrides(user?.limitOverrides ?? {})
|
|
2482
|
+
};
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
async setAdminUserSettings(userId, { boost, limitOverrides } = {}) {
|
|
1849
2486
|
const id = normalizeUserId(userId);
|
|
1850
2487
|
const current = this.users.get(id) ?? {
|
|
2488
|
+
boost: false,
|
|
1851
2489
|
createdAt: now(),
|
|
1852
2490
|
displayName: id,
|
|
1853
2491
|
id,
|
|
@@ -1855,28 +2493,36 @@ export class MemoryAnonymousStore {
|
|
|
1855
2493
|
};
|
|
1856
2494
|
const user = {
|
|
1857
2495
|
...current,
|
|
2496
|
+
boost: boost === undefined ? Boolean(current.boost) : Boolean(boost),
|
|
1858
2497
|
displayName: current.displayName ?? current.login ?? id,
|
|
1859
2498
|
id,
|
|
1860
|
-
limitOverrides:
|
|
2499
|
+
limitOverrides:
|
|
2500
|
+
limitOverrides === undefined
|
|
2501
|
+
? normalizeUserLimitOverrides(current.limitOverrides ?? {})
|
|
2502
|
+
: normalizeUserLimitOverrides(limitOverrides),
|
|
1861
2503
|
updatedAt: now()
|
|
1862
2504
|
};
|
|
1863
2505
|
this.users.set(id, user);
|
|
1864
2506
|
return this.getAdminUser(id);
|
|
1865
2507
|
}
|
|
1866
2508
|
|
|
2509
|
+
async setUserLimitOverrides(userId, limitOverrides) {
|
|
2510
|
+
return this.setAdminUserSettings(userId, { limitOverrides });
|
|
2511
|
+
}
|
|
2512
|
+
|
|
1867
2513
|
async deployWithUserLimitOverrides(deploy) {
|
|
1868
2514
|
if (!deploy?.ownerId) {
|
|
1869
2515
|
return deploy;
|
|
1870
2516
|
}
|
|
1871
2517
|
|
|
1872
|
-
const limitOverrides = await this.
|
|
1873
|
-
if (Object.keys(limitOverrides).length === 0) {
|
|
2518
|
+
const { boost, limitOverrides } = await this.getUserLimitSettings(deploy.ownerId);
|
|
2519
|
+
if (!boost && Object.keys(limitOverrides).length === 0) {
|
|
1874
2520
|
return deploy;
|
|
1875
2521
|
}
|
|
1876
2522
|
|
|
1877
2523
|
return {
|
|
1878
2524
|
...deploy,
|
|
1879
|
-
limits: limitsWithOverrides(deploy.limits, limitOverrides)
|
|
2525
|
+
limits: limitsWithOverrides(deploy.limits, limitOverrides, boost)
|
|
1880
2526
|
};
|
|
1881
2527
|
}
|
|
1882
2528
|
|
|
@@ -1919,7 +2565,11 @@ export class MemoryAnonymousStore {
|
|
|
1919
2565
|
|
|
1920
2566
|
return users
|
|
1921
2567
|
.filter(Boolean)
|
|
1922
|
-
.sort(
|
|
2568
|
+
.sort(
|
|
2569
|
+
(left, right) =>
|
|
2570
|
+
String(right.createdAt).localeCompare(String(left.createdAt)) ||
|
|
2571
|
+
String(left.login ?? left.id).localeCompare(String(right.login ?? right.id))
|
|
2572
|
+
);
|
|
1923
2573
|
}
|
|
1924
2574
|
|
|
1925
2575
|
storeArtifact({ artifact, artifactHash, clientBundleBase64, clientBundleHash, createdAt }) {
|
|
@@ -2362,15 +3012,17 @@ export class PostgresAnonymousStore {
|
|
|
2362
3012
|
display_name text,
|
|
2363
3013
|
avatar_url text,
|
|
2364
3014
|
url text,
|
|
3015
|
+
boost boolean not null default false,
|
|
2365
3016
|
limit_overrides_json jsonb not null default '{}',
|
|
2366
3017
|
created_at timestamptz not null,
|
|
2367
3018
|
updated_at timestamptz not null
|
|
2368
3019
|
)
|
|
2369
3020
|
`);
|
|
3021
|
+
await this.query("alter table users add column if not exists boost boolean not null default false");
|
|
2370
3022
|
await this.query(`
|
|
2371
3023
|
insert into users(
|
|
2372
3024
|
id, provider, provider_id, login, display_name, avatar_url, url,
|
|
2373
|
-
limit_overrides_json, created_at, updated_at
|
|
3025
|
+
boost, limit_overrides_json, created_at, updated_at
|
|
2374
3026
|
)
|
|
2375
3027
|
select distinct on (owner_id)
|
|
2376
3028
|
owner_id,
|
|
@@ -2380,6 +3032,7 @@ export class PostgresAnonymousStore {
|
|
|
2380
3032
|
coalesce(owner_json->>'displayName', owner_json->>'login', owner_id),
|
|
2381
3033
|
owner_json->>'avatarUrl',
|
|
2382
3034
|
owner_json->>'url',
|
|
3035
|
+
false,
|
|
2383
3036
|
'{}'::jsonb,
|
|
2384
3037
|
coalesce(claimed_at, created_at, now()),
|
|
2385
3038
|
coalesce(claimed_at, created_at, now())
|
|
@@ -2412,6 +3065,7 @@ export class PostgresAnonymousStore {
|
|
|
2412
3065
|
|
|
2413
3066
|
return {
|
|
2414
3067
|
avatarUrl: row.avatar_url ?? null,
|
|
3068
|
+
boost: Boolean(row.boost),
|
|
2415
3069
|
createdAt: new Date(row.created_at).toISOString(),
|
|
2416
3070
|
displayName: row.display_name ?? row.login ?? row.id,
|
|
2417
3071
|
id: row.id,
|
|
@@ -2463,35 +3117,54 @@ export class PostgresAnonymousStore {
|
|
|
2463
3117
|
return normalizeUserLimitOverrides(result.rows[0]?.limit_overrides_json ?? {});
|
|
2464
3118
|
}
|
|
2465
3119
|
|
|
2466
|
-
async
|
|
3120
|
+
async getUserLimitSettings(userId) {
|
|
3121
|
+
const result = await this.query("select boost, limit_overrides_json from users where id = $1", [normalizeUserId(userId)]);
|
|
3122
|
+
return {
|
|
3123
|
+
boost: Boolean(result.rows[0]?.boost),
|
|
3124
|
+
limitOverrides: normalizeUserLimitOverrides(result.rows[0]?.limit_overrides_json ?? {})
|
|
3125
|
+
};
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
async setAdminUserSettings(userId, { boost, limitOverrides } = {}) {
|
|
2467
3129
|
const id = normalizeUserId(userId);
|
|
3130
|
+
const current = await this.query("select boost, limit_overrides_json from users where id = $1", [id]);
|
|
3131
|
+
const nextBoost = boost === undefined ? Boolean(current.rows[0]?.boost) : Boolean(boost);
|
|
3132
|
+
const nextLimitOverrides =
|
|
3133
|
+
limitOverrides === undefined
|
|
3134
|
+
? normalizeUserLimitOverrides(current.rows[0]?.limit_overrides_json ?? {})
|
|
3135
|
+
: normalizeUserLimitOverrides(limitOverrides);
|
|
2468
3136
|
const updatedAt = now();
|
|
2469
3137
|
await this.query(
|
|
2470
3138
|
`
|
|
2471
|
-
insert into users(id, display_name, limit_overrides_json, created_at, updated_at)
|
|
2472
|
-
values($1, $2, $3::jsonb, $
|
|
3139
|
+
insert into users(id, display_name, boost, limit_overrides_json, created_at, updated_at)
|
|
3140
|
+
values($1, $2, $3, $4::jsonb, $5, $5)
|
|
2473
3141
|
on conflict(id) do update set
|
|
3142
|
+
boost = excluded.boost,
|
|
2474
3143
|
limit_overrides_json = excluded.limit_overrides_json,
|
|
2475
3144
|
updated_at = excluded.updated_at
|
|
2476
3145
|
`,
|
|
2477
|
-
[id, id, JSON.stringify(
|
|
3146
|
+
[id, id, nextBoost, JSON.stringify(nextLimitOverrides), updatedAt]
|
|
2478
3147
|
);
|
|
2479
3148
|
return this.getAdminUser(id);
|
|
2480
3149
|
}
|
|
2481
3150
|
|
|
3151
|
+
async setUserLimitOverrides(userId, limitOverrides) {
|
|
3152
|
+
return this.setAdminUserSettings(userId, { limitOverrides });
|
|
3153
|
+
}
|
|
3154
|
+
|
|
2482
3155
|
async deployWithUserLimitOverrides(deploy) {
|
|
2483
3156
|
if (!deploy?.ownerId) {
|
|
2484
3157
|
return deploy;
|
|
2485
3158
|
}
|
|
2486
3159
|
|
|
2487
|
-
const limitOverrides = await this.
|
|
2488
|
-
if (Object.keys(limitOverrides).length === 0) {
|
|
3160
|
+
const { boost, limitOverrides } = await this.getUserLimitSettings(deploy.ownerId);
|
|
3161
|
+
if (!boost && Object.keys(limitOverrides).length === 0) {
|
|
2489
3162
|
return deploy;
|
|
2490
3163
|
}
|
|
2491
3164
|
|
|
2492
3165
|
return {
|
|
2493
3166
|
...deploy,
|
|
2494
|
-
limits: limitsWithOverrides(deploy.limits, limitOverrides)
|
|
3167
|
+
limits: limitsWithOverrides(deploy.limits, limitOverrides, boost)
|
|
2495
3168
|
};
|
|
2496
3169
|
}
|
|
2497
3170
|
|
|
@@ -2580,7 +3253,7 @@ export class PostgresAnonymousStore {
|
|
|
2580
3253
|
where d.owner_id is not null
|
|
2581
3254
|
group by d.owner_id
|
|
2582
3255
|
) q on q.owner_id = u.id
|
|
2583
|
-
order by coalesce(u.login, u.id) asc
|
|
3256
|
+
order by u.created_at desc, coalesce(u.login, u.id) asc
|
|
2584
3257
|
`,
|
|
2585
3258
|
[windowStart]
|
|
2586
3259
|
);
|
|
@@ -3207,12 +3880,16 @@ export async function startAnonymousServer({
|
|
|
3207
3880
|
return counts;
|
|
3208
3881
|
}
|
|
3209
3882
|
|
|
3210
|
-
async function
|
|
3883
|
+
async function adminDeploysWithConnections() {
|
|
3211
3884
|
const connections = activeConnectionCounts();
|
|
3212
|
-
|
|
3885
|
+
return (await resolvedStore.listDeployResourceUsage()).map((deploy) => ({
|
|
3213
3886
|
...deploy,
|
|
3214
3887
|
connections: connections.get(deploy.id) ?? 0
|
|
3215
3888
|
}));
|
|
3889
|
+
}
|
|
3890
|
+
|
|
3891
|
+
async function adminSummary() {
|
|
3892
|
+
const deploys = await adminDeploysWithConnections();
|
|
3216
3893
|
const users = await resolvedStore.listAdminUsers();
|
|
3217
3894
|
const totals = deploys.reduce(
|
|
3218
3895
|
(acc, deploy) => ({
|
|
@@ -3247,6 +3924,21 @@ export async function startAnonymousServer({
|
|
|
3247
3924
|
};
|
|
3248
3925
|
}
|
|
3249
3926
|
|
|
3927
|
+
async function adminUserDetail(userId) {
|
|
3928
|
+
const user = await resolvedStore.getAdminUser(userId);
|
|
3929
|
+
if (!user) {
|
|
3930
|
+
return null;
|
|
3931
|
+
}
|
|
3932
|
+
|
|
3933
|
+
const deploys = (await adminDeploysWithConnections()).filter((deploy) => deploy.ownerId === user.id);
|
|
3934
|
+
return {
|
|
3935
|
+
deployCount: deploys.length,
|
|
3936
|
+
deploys,
|
|
3937
|
+
generatedAt: now(),
|
|
3938
|
+
user
|
|
3939
|
+
};
|
|
3940
|
+
}
|
|
3941
|
+
|
|
3250
3942
|
function currentDeveloper(req) {
|
|
3251
3943
|
return developerFromRequest(req, resolvedDeveloperSessionSecret);
|
|
3252
3944
|
}
|
|
@@ -3499,7 +4191,14 @@ export async function startAnonymousServer({
|
|
|
3499
4191
|
return;
|
|
3500
4192
|
}
|
|
3501
4193
|
|
|
3502
|
-
if (
|
|
4194
|
+
if (
|
|
4195
|
+
req.method === "GET" &&
|
|
4196
|
+
(requestUrl.pathname === "/admin" ||
|
|
4197
|
+
requestUrl.pathname === "/admin/" ||
|
|
4198
|
+
requestUrl.pathname === "/admin/deployments" ||
|
|
4199
|
+
requestUrl.pathname === "/admin/users" ||
|
|
4200
|
+
/^\/admin\/users\/[^/]+$/.test(requestUrl.pathname))
|
|
4201
|
+
) {
|
|
3503
4202
|
sendText(res, 200, adminHtml(), { "Content-Type": "text/html; charset=utf-8" });
|
|
3504
4203
|
return;
|
|
3505
4204
|
}
|
|
@@ -3540,6 +4239,29 @@ export async function startAnonymousServer({
|
|
|
3540
4239
|
return;
|
|
3541
4240
|
}
|
|
3542
4241
|
|
|
4242
|
+
const adminUserDetailMatch =
|
|
4243
|
+
req.method === "GET" ? requestUrl.pathname.match(/^\/admin\/api\/users\/([^/]+)$/) : null;
|
|
4244
|
+
if (adminUserDetailMatch) {
|
|
4245
|
+
if (!isAdminConfigured(adminPassword)) {
|
|
4246
|
+
sendJson(res, 503, { error: "Lakebed admin is not configured. Set LAKEBED_ADMIN_PASSWORD." });
|
|
4247
|
+
return;
|
|
4248
|
+
}
|
|
4249
|
+
|
|
4250
|
+
if (!isAdminAuthenticated(req, adminPassword)) {
|
|
4251
|
+
sendJson(res, 401, { error: "Admin authentication required." });
|
|
4252
|
+
return;
|
|
4253
|
+
}
|
|
4254
|
+
|
|
4255
|
+
const detail = await adminUserDetail(decodeURIComponent(adminUserDetailMatch[1]));
|
|
4256
|
+
if (!detail) {
|
|
4257
|
+
sendJson(res, 404, { error: "Unknown user." });
|
|
4258
|
+
return;
|
|
4259
|
+
}
|
|
4260
|
+
|
|
4261
|
+
sendJson(res, 200, detail);
|
|
4262
|
+
return;
|
|
4263
|
+
}
|
|
4264
|
+
|
|
3543
4265
|
const userLimitsMatch =
|
|
3544
4266
|
req.method === "PUT" ? requestUrl.pathname.match(/^\/admin\/api\/users\/([^/]+)\/limits$/) : null;
|
|
3545
4267
|
if (userLimitsMatch) {
|
|
@@ -3555,14 +4277,22 @@ export async function startAnonymousServer({
|
|
|
3555
4277
|
|
|
3556
4278
|
const body = await readJsonBody(req, 4096);
|
|
3557
4279
|
let limitOverrides;
|
|
4280
|
+
let boost;
|
|
3558
4281
|
try {
|
|
3559
|
-
|
|
4282
|
+
const rawLimitOverrides = body.limits ?? body.limitOverrides;
|
|
4283
|
+
limitOverrides = rawLimitOverrides === undefined ? undefined : normalizeUserLimitOverrides(rawLimitOverrides);
|
|
4284
|
+
boost = normalizeOptionalBoolean(body.boost, "boost");
|
|
3560
4285
|
} catch (error) {
|
|
3561
4286
|
sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
3562
4287
|
return;
|
|
3563
4288
|
}
|
|
3564
4289
|
|
|
3565
|
-
const user = await resolvedStore.
|
|
4290
|
+
const user = await (typeof resolvedStore.setAdminUserSettings === "function"
|
|
4291
|
+
? resolvedStore.setAdminUserSettings(decodeURIComponent(userLimitsMatch[1]), {
|
|
4292
|
+
boost,
|
|
4293
|
+
limitOverrides
|
|
4294
|
+
})
|
|
4295
|
+
: resolvedStore.setUserLimitOverrides(decodeURIComponent(userLimitsMatch[1]), limitOverrides ?? {}));
|
|
3566
4296
|
await refreshOwnerSubscriptions(user.id);
|
|
3567
4297
|
sendJson(res, 200, { user });
|
|
3568
4298
|
return;
|