lakebed 0.0.8 → 0.0.10
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 +9 -1
- package/package.json +5 -2
- package/src/anonymous-server.js +1066 -329
- package/src/anonymous.js +34 -13
- package/src/cli.js +57 -6
- package/src/client.d.ts +1 -0
- package/src/client.js +79 -16
- package/src/source-runtime-loader.mjs +31 -0
- package/src/source-runtime-worker.js +454 -0
- package/src/source-runtime.js +668 -0
- package/src/version.js +1 -1
package/src/anonymous-server.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
validateAnonymousDeployPayload
|
|
13
13
|
} from "./anonymous.js";
|
|
14
14
|
import { authFromUrl as resolveAuthFromUrl, createGuestAuth, requestOrigin, shooBaseUrlFromEnv } from "./auth.js";
|
|
15
|
+
import { createSourceRuntimeFromEnv } from "./source-runtime.js";
|
|
15
16
|
import { WebSocketServer } from "ws";
|
|
16
17
|
|
|
17
18
|
function now() {
|
|
@@ -256,6 +257,7 @@ function quotaLimitForBucket(bucket, deploy) {
|
|
|
256
257
|
}
|
|
257
258
|
|
|
258
259
|
const USER_LIMIT_OVERRIDE_KEYS = ["requestsPerDay", "mutationsPerDay"];
|
|
260
|
+
const USER_BOOST_MULTIPLIER = 20;
|
|
259
261
|
|
|
260
262
|
function normalizeUserId(value) {
|
|
261
263
|
const userId = String(value ?? "").trim();
|
|
@@ -276,6 +278,25 @@ function normalizeLimitOverrideValue(value, key) {
|
|
|
276
278
|
return parsed;
|
|
277
279
|
}
|
|
278
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
|
+
|
|
279
300
|
function normalizeUserLimitOverrides(value = {}) {
|
|
280
301
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
281
302
|
throw new Error("limits must be a JSON object.");
|
|
@@ -298,9 +319,19 @@ function normalizeUserLimitOverrides(value = {}) {
|
|
|
298
319
|
return overrides;
|
|
299
320
|
}
|
|
300
321
|
|
|
301
|
-
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) {
|
|
302
333
|
return {
|
|
303
|
-
...baseLimits,
|
|
334
|
+
...limitsWithBoost(baseLimits, boost),
|
|
304
335
|
...normalizeUserLimitOverrides(limitOverrides ?? {})
|
|
305
336
|
};
|
|
306
337
|
}
|
|
@@ -309,6 +340,7 @@ function userFromOwner(owner, current = {}) {
|
|
|
309
340
|
const id = normalizeUserId(owner?.id ?? current.id);
|
|
310
341
|
return {
|
|
311
342
|
avatarUrl: owner?.avatarUrl ?? current.avatarUrl ?? null,
|
|
343
|
+
boost: Boolean(current.boost),
|
|
312
344
|
createdAt: current.createdAt ?? now(),
|
|
313
345
|
displayName: owner?.displayName ?? owner?.login ?? current.displayName ?? id,
|
|
314
346
|
id,
|
|
@@ -326,13 +358,15 @@ function adminUserSummary({ activeDeployCount = 0, deployCount = 0, usage = [],
|
|
|
326
358
|
return {
|
|
327
359
|
activeDeployCount,
|
|
328
360
|
avatarUrl: user.avatarUrl,
|
|
361
|
+
boost: Boolean(user.boost),
|
|
362
|
+
boostMultiplier: USER_BOOST_MULTIPLIER,
|
|
329
363
|
createdAt: user.createdAt,
|
|
330
364
|
defaultLimits: DEFAULT_ANONYMOUS_LIMITS,
|
|
331
365
|
deployCount,
|
|
332
366
|
displayName: user.displayName ?? user.login ?? user.id,
|
|
333
367
|
id: user.id,
|
|
334
368
|
limitOverrides,
|
|
335
|
-
limits: limitsWithOverrides(DEFAULT_ANONYMOUS_LIMITS, limitOverrides),
|
|
369
|
+
limits: limitsWithOverrides(DEFAULT_ANONYMOUS_LIMITS, limitOverrides, user.boost),
|
|
336
370
|
login: user.login,
|
|
337
371
|
provider: user.provider,
|
|
338
372
|
providerId: user.providerId,
|
|
@@ -656,6 +690,15 @@ function adminDeploySummary({ artifact, artifactBytes = 0, deploy, logBytes = 0,
|
|
|
656
690
|
logBytes,
|
|
657
691
|
logEntries,
|
|
658
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,
|
|
659
702
|
ownerId: deploy.ownerId,
|
|
660
703
|
slug: deploy.slug,
|
|
661
704
|
stateBytes,
|
|
@@ -677,16 +720,17 @@ function adminHtml() {
|
|
|
677
720
|
<style>
|
|
678
721
|
:root {
|
|
679
722
|
color-scheme: dark;
|
|
680
|
-
--bg: #
|
|
681
|
-
--panel: #
|
|
682
|
-
--
|
|
683
|
-
--line
|
|
684
|
-
--
|
|
685
|
-
--
|
|
686
|
-
--
|
|
687
|
-
--
|
|
688
|
-
--bad: #
|
|
689
|
-
--
|
|
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;
|
|
690
734
|
}
|
|
691
735
|
|
|
692
736
|
* {
|
|
@@ -694,15 +738,11 @@ function adminHtml() {
|
|
|
694
738
|
}
|
|
695
739
|
|
|
696
740
|
body {
|
|
697
|
-
|
|
698
|
-
background:
|
|
699
|
-
linear-gradient(90deg, rgba(154, 214, 107, 0.06) 1px, transparent 1px),
|
|
700
|
-
linear-gradient(180deg, rgba(154, 214, 107, 0.04) 1px, transparent 1px),
|
|
701
|
-
var(--bg);
|
|
702
|
-
background-size: 34px 34px;
|
|
741
|
+
background: var(--bg);
|
|
703
742
|
color: var(--text);
|
|
704
|
-
font-family:
|
|
743
|
+
font-family: Inter, "Avenir Next", "Helvetica Neue", Arial, sans-serif;
|
|
705
744
|
letter-spacing: 0;
|
|
745
|
+
margin: 0;
|
|
706
746
|
}
|
|
707
747
|
|
|
708
748
|
button,
|
|
@@ -722,105 +762,164 @@ function adminHtml() {
|
|
|
722
762
|
|
|
723
763
|
.shell {
|
|
724
764
|
margin: 0 auto;
|
|
725
|
-
max-width:
|
|
765
|
+
max-width: 1440px;
|
|
726
766
|
min-height: 100vh;
|
|
727
|
-
padding:
|
|
767
|
+
padding: 18px 22px;
|
|
728
768
|
}
|
|
729
769
|
|
|
730
770
|
.topbar {
|
|
731
771
|
align-items: center;
|
|
732
772
|
display: flex;
|
|
733
|
-
gap:
|
|
773
|
+
gap: 16px;
|
|
734
774
|
justify-content: space-between;
|
|
735
|
-
margin-bottom:
|
|
775
|
+
margin-bottom: 14px;
|
|
736
776
|
}
|
|
737
777
|
|
|
738
778
|
.brand {
|
|
739
779
|
display: grid;
|
|
740
|
-
gap:
|
|
780
|
+
gap: 2px;
|
|
741
781
|
}
|
|
742
782
|
|
|
743
|
-
.eyebrow
|
|
744
|
-
|
|
783
|
+
.eyebrow,
|
|
784
|
+
.mono,
|
|
785
|
+
small,
|
|
786
|
+
th,
|
|
787
|
+
label,
|
|
788
|
+
.status-line,
|
|
789
|
+
.pager {
|
|
790
|
+
color: var(--muted);
|
|
745
791
|
font-family: "SFMono-Regular", Consolas, monospace;
|
|
746
792
|
font-size: 12px;
|
|
747
793
|
}
|
|
748
794
|
|
|
749
|
-
h1
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
line-height: 1;
|
|
795
|
+
h1,
|
|
796
|
+
h2,
|
|
797
|
+
h3 {
|
|
753
798
|
margin: 0;
|
|
754
799
|
}
|
|
755
800
|
|
|
756
|
-
|
|
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;
|
|
757
819
|
display: flex;
|
|
758
|
-
|
|
759
|
-
|
|
820
|
+
gap: 8px;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
.actions {
|
|
760
824
|
justify-content: flex-end;
|
|
761
825
|
}
|
|
762
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
|
+
|
|
763
838
|
.button {
|
|
764
839
|
background: var(--accent);
|
|
765
840
|
border: 1px solid var(--accent);
|
|
766
|
-
border-radius: 6px;
|
|
767
841
|
color: var(--ink);
|
|
768
|
-
cursor: pointer;
|
|
769
842
|
font-weight: 700;
|
|
770
|
-
min-height:
|
|
771
|
-
padding: 0
|
|
843
|
+
min-height: 34px;
|
|
844
|
+
padding: 0 12px;
|
|
772
845
|
}
|
|
773
846
|
|
|
774
|
-
.button.secondary
|
|
847
|
+
.button.secondary,
|
|
848
|
+
.tab,
|
|
849
|
+
.row-action,
|
|
850
|
+
.icon-button {
|
|
775
851
|
background: transparent;
|
|
852
|
+
border: 1px solid var(--line-strong);
|
|
776
853
|
color: var(--text);
|
|
777
854
|
}
|
|
778
855
|
|
|
779
|
-
.
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
856
|
+
.button:disabled,
|
|
857
|
+
.tab:disabled,
|
|
858
|
+
.row-action:disabled,
|
|
859
|
+
.icon-button:disabled {
|
|
860
|
+
cursor: default;
|
|
861
|
+
opacity: 0.48;
|
|
784
862
|
}
|
|
785
863
|
|
|
786
|
-
.
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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;
|
|
791
875
|
}
|
|
792
876
|
|
|
793
|
-
.
|
|
794
|
-
|
|
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;
|
|
795
890
|
}
|
|
796
891
|
|
|
797
|
-
.metric
|
|
798
|
-
|
|
892
|
+
.metric {
|
|
893
|
+
background: var(--panel);
|
|
894
|
+
border: 1px solid var(--line);
|
|
895
|
+
border-radius: 6px;
|
|
896
|
+
min-height: 68px;
|
|
897
|
+
padding: 10px 12px;
|
|
799
898
|
}
|
|
800
899
|
|
|
801
900
|
.metric span {
|
|
802
901
|
color: var(--muted);
|
|
803
902
|
display: block;
|
|
804
903
|
font-family: "SFMono-Regular", Consolas, monospace;
|
|
805
|
-
font-size:
|
|
806
|
-
margin-bottom:
|
|
904
|
+
font-size: 11px;
|
|
905
|
+
margin-bottom: 7px;
|
|
807
906
|
}
|
|
808
907
|
|
|
809
908
|
.metric strong {
|
|
810
909
|
display: block;
|
|
811
|
-
font-size:
|
|
910
|
+
font-size: 20px;
|
|
812
911
|
line-height: 1.1;
|
|
813
912
|
}
|
|
814
913
|
|
|
815
914
|
.panel {
|
|
816
|
-
background:
|
|
915
|
+
background: var(--panel);
|
|
817
916
|
border: 1px solid var(--line);
|
|
818
|
-
border-radius:
|
|
917
|
+
border-radius: 6px;
|
|
819
918
|
overflow: hidden;
|
|
820
919
|
}
|
|
821
920
|
|
|
822
921
|
.panel + .panel {
|
|
823
|
-
margin-top:
|
|
922
|
+
margin-top: 12px;
|
|
824
923
|
}
|
|
825
924
|
|
|
826
925
|
.panel-head {
|
|
@@ -829,18 +928,16 @@ function adminHtml() {
|
|
|
829
928
|
display: flex;
|
|
830
929
|
gap: 12px;
|
|
831
930
|
justify-content: space-between;
|
|
832
|
-
padding:
|
|
931
|
+
padding: 10px 12px;
|
|
833
932
|
}
|
|
834
933
|
|
|
835
|
-
.panel-head
|
|
836
|
-
|
|
837
|
-
margin: 0;
|
|
934
|
+
.panel-head.tight {
|
|
935
|
+
align-items: flex-start;
|
|
838
936
|
}
|
|
839
937
|
|
|
840
|
-
.
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
font-size: 12px;
|
|
938
|
+
.panel-title {
|
|
939
|
+
display: grid;
|
|
940
|
+
gap: 3px;
|
|
844
941
|
}
|
|
845
942
|
|
|
846
943
|
.table-wrap {
|
|
@@ -849,27 +946,32 @@ function adminHtml() {
|
|
|
849
946
|
|
|
850
947
|
table {
|
|
851
948
|
border-collapse: collapse;
|
|
852
|
-
min-width:
|
|
949
|
+
min-width: 900px;
|
|
853
950
|
width: 100%;
|
|
854
951
|
}
|
|
855
952
|
|
|
953
|
+
.deploy-table {
|
|
954
|
+
min-width: 960px;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
.user-table {
|
|
958
|
+
min-width: 840px;
|
|
959
|
+
}
|
|
960
|
+
|
|
856
961
|
th,
|
|
857
962
|
td {
|
|
858
963
|
border-bottom: 1px solid var(--line);
|
|
859
|
-
padding:
|
|
964
|
+
padding: 8px 10px;
|
|
860
965
|
text-align: left;
|
|
861
|
-
vertical-align:
|
|
966
|
+
vertical-align: middle;
|
|
862
967
|
}
|
|
863
968
|
|
|
864
969
|
th {
|
|
865
|
-
color: var(--muted);
|
|
866
|
-
font-family: "SFMono-Regular", Consolas, monospace;
|
|
867
|
-
font-size: 12px;
|
|
868
970
|
font-weight: 600;
|
|
869
971
|
}
|
|
870
972
|
|
|
871
973
|
td {
|
|
872
|
-
font-size:
|
|
974
|
+
font-size: 13px;
|
|
873
975
|
white-space: nowrap;
|
|
874
976
|
}
|
|
875
977
|
|
|
@@ -877,185 +979,215 @@ function adminHtml() {
|
|
|
877
979
|
border-bottom: 0;
|
|
878
980
|
}
|
|
879
981
|
|
|
880
|
-
.
|
|
982
|
+
.stack {
|
|
881
983
|
display: grid;
|
|
882
|
-
gap:
|
|
883
|
-
min-width: 240px;
|
|
984
|
+
gap: 2px;
|
|
884
985
|
}
|
|
885
986
|
|
|
886
|
-
.
|
|
887
|
-
|
|
987
|
+
.primary-text {
|
|
988
|
+
color: var(--text);
|
|
989
|
+
font-weight: 700;
|
|
888
990
|
}
|
|
889
991
|
|
|
890
|
-
.
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
font-size: 12px;
|
|
992
|
+
.name-cell {
|
|
993
|
+
display: grid;
|
|
994
|
+
gap: 3px;
|
|
995
|
+
min-width: 160px;
|
|
895
996
|
}
|
|
896
997
|
|
|
897
|
-
.
|
|
998
|
+
.name-cell small {
|
|
898
999
|
white-space: normal;
|
|
899
1000
|
}
|
|
900
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
|
+
|
|
901
1032
|
.pill {
|
|
902
1033
|
border: 1px solid var(--line-strong);
|
|
903
1034
|
border-radius: 999px;
|
|
904
1035
|
display: inline-flex;
|
|
905
1036
|
font-family: "SFMono-Regular", Consolas, monospace;
|
|
906
|
-
font-size:
|
|
907
|
-
|
|
1037
|
+
font-size: 11px;
|
|
1038
|
+
line-height: 1;
|
|
1039
|
+
padding: 4px 7px;
|
|
908
1040
|
}
|
|
909
1041
|
|
|
910
1042
|
.pill.active {
|
|
911
|
-
border-color:
|
|
1043
|
+
border-color: #65758a;
|
|
912
1044
|
color: var(--accent);
|
|
913
1045
|
}
|
|
914
1046
|
|
|
915
1047
|
.pill.expired {
|
|
916
|
-
border-color: rgba(
|
|
1048
|
+
border-color: rgba(240, 128, 128, 0.75);
|
|
917
1049
|
color: var(--bad);
|
|
918
1050
|
}
|
|
919
1051
|
|
|
920
1052
|
.pill.terminated {
|
|
921
|
-
border-color: rgba(
|
|
1053
|
+
border-color: rgba(224, 181, 95, 0.75);
|
|
922
1054
|
color: var(--warn);
|
|
923
1055
|
}
|
|
924
1056
|
|
|
925
|
-
.status-cell {
|
|
926
|
-
align-items: center;
|
|
927
|
-
display: flex;
|
|
928
|
-
gap: 8px;
|
|
929
|
-
}
|
|
930
|
-
|
|
931
1057
|
.row-action {
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
border-radius: 6px;
|
|
935
|
-
color: var(--bad);
|
|
936
|
-
cursor: pointer;
|
|
937
|
-
font-family: "SFMono-Regular", Consolas, monospace;
|
|
938
|
-
font-size: 12px;
|
|
939
|
-
min-height: 32px;
|
|
940
|
-
padding: 0 10px;
|
|
1058
|
+
min-height: 30px;
|
|
1059
|
+
padding: 0 9px;
|
|
941
1060
|
}
|
|
942
1061
|
|
|
943
|
-
.
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
opacity: 0.62;
|
|
1062
|
+
.icon-button {
|
|
1063
|
+
height: 30px;
|
|
1064
|
+
padding: 0;
|
|
1065
|
+
width: 30px;
|
|
948
1066
|
}
|
|
949
1067
|
|
|
950
|
-
.
|
|
951
|
-
|
|
952
|
-
|
|
1068
|
+
.icon-button svg {
|
|
1069
|
+
height: 15px;
|
|
1070
|
+
width: 15px;
|
|
953
1071
|
}
|
|
954
1072
|
|
|
955
|
-
.
|
|
956
|
-
border-color: rgba(
|
|
957
|
-
color: var(--
|
|
1073
|
+
.icon-button.danger {
|
|
1074
|
+
border-color: rgba(240, 128, 128, 0.62);
|
|
1075
|
+
color: var(--bad);
|
|
958
1076
|
}
|
|
959
1077
|
|
|
960
|
-
.
|
|
1078
|
+
.limit-cell {
|
|
961
1079
|
display: grid;
|
|
962
1080
|
gap: 4px;
|
|
963
|
-
min-width:
|
|
1081
|
+
min-width: 112px;
|
|
964
1082
|
}
|
|
965
1083
|
|
|
966
|
-
.
|
|
967
|
-
|
|
1084
|
+
.limit-combo {
|
|
1085
|
+
display: grid;
|
|
1086
|
+
gap: 4px;
|
|
1087
|
+
min-width: 220px;
|
|
968
1088
|
}
|
|
969
1089
|
|
|
970
|
-
.limit-
|
|
1090
|
+
.limit-row {
|
|
1091
|
+
align-items: center;
|
|
971
1092
|
display: grid;
|
|
972
1093
|
gap: 6px;
|
|
973
|
-
|
|
1094
|
+
grid-template-columns: 16px 86px 1fr;
|
|
974
1095
|
}
|
|
975
1096
|
|
|
976
|
-
.limit-
|
|
977
|
-
min-height:
|
|
978
|
-
padding: 0
|
|
1097
|
+
.limit-row input {
|
|
1098
|
+
min-height: 28px;
|
|
1099
|
+
padding: 0 7px;
|
|
979
1100
|
}
|
|
980
1101
|
|
|
981
|
-
.
|
|
982
|
-
|
|
983
|
-
display: grid;
|
|
984
|
-
gap: 8px;
|
|
985
|
-
grid-template-columns: minmax(180px, 1fr) 132px 132px auto;
|
|
986
|
-
width: min(100%, 720px);
|
|
1102
|
+
.usage-cell {
|
|
1103
|
+
min-width: 128px;
|
|
987
1104
|
}
|
|
988
1105
|
|
|
989
|
-
|
|
990
|
-
|
|
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%;
|
|
991
1115
|
}
|
|
992
1116
|
|
|
993
|
-
|
|
994
|
-
|
|
1117
|
+
input:focus {
|
|
1118
|
+
border-color: #69778a;
|
|
995
1119
|
}
|
|
996
1120
|
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1121
|
+
input[type="checkbox"] {
|
|
1122
|
+
height: 16px;
|
|
1123
|
+
min-height: 0;
|
|
1124
|
+
width: 16px;
|
|
1000
1125
|
}
|
|
1001
1126
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1127
|
+
.checkbox {
|
|
1128
|
+
align-items: center;
|
|
1129
|
+
color: var(--text);
|
|
1130
|
+
display: inline-flex;
|
|
1131
|
+
gap: 7px;
|
|
1132
|
+
min-height: 30px;
|
|
1007
1133
|
}
|
|
1008
1134
|
|
|
1009
|
-
.
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1135
|
+
.limit-form,
|
|
1136
|
+
.detail-form {
|
|
1137
|
+
align-items: end;
|
|
1138
|
+
display: grid;
|
|
1139
|
+
gap: 8px;
|
|
1013
1140
|
}
|
|
1014
1141
|
|
|
1015
|
-
.
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
border-radius: 8px;
|
|
1019
|
-
max-width: 460px;
|
|
1020
|
-
padding: 24px;
|
|
1021
|
-
width: 100%;
|
|
1142
|
+
.limit-form {
|
|
1143
|
+
grid-template-columns: minmax(160px, 0.7fr) 100px 100px auto auto;
|
|
1144
|
+
width: min(100%, 650px);
|
|
1022
1145
|
}
|
|
1023
1146
|
|
|
1024
|
-
.
|
|
1025
|
-
|
|
1026
|
-
|
|
1147
|
+
.detail-form {
|
|
1148
|
+
grid-template-columns: auto 128px 128px auto auto;
|
|
1149
|
+
width: min(100%, 700px);
|
|
1027
1150
|
}
|
|
1028
1151
|
|
|
1029
1152
|
.field {
|
|
1030
1153
|
display: grid;
|
|
1031
|
-
gap:
|
|
1154
|
+
gap: 4px;
|
|
1032
1155
|
}
|
|
1033
1156
|
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1157
|
+
.pager {
|
|
1158
|
+
border-top: 1px solid var(--line);
|
|
1159
|
+
justify-content: flex-end;
|
|
1160
|
+
min-height: 40px;
|
|
1161
|
+
padding: 7px 10px;
|
|
1038
1162
|
}
|
|
1039
1163
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
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);
|
|
1043
1177
|
border-radius: 6px;
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
outline: none;
|
|
1047
|
-
padding: 0 12px;
|
|
1178
|
+
max-width: 420px;
|
|
1179
|
+
padding: 22px;
|
|
1048
1180
|
width: 100%;
|
|
1049
1181
|
}
|
|
1050
1182
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1183
|
+
.login-panel h1 {
|
|
1184
|
+
margin-bottom: 18px;
|
|
1053
1185
|
}
|
|
1054
1186
|
|
|
1055
1187
|
.form-row {
|
|
1056
1188
|
display: flex;
|
|
1057
|
-
gap:
|
|
1058
|
-
margin-top:
|
|
1189
|
+
gap: 8px;
|
|
1190
|
+
margin-top: 14px;
|
|
1059
1191
|
}
|
|
1060
1192
|
|
|
1061
1193
|
.error {
|
|
@@ -1067,13 +1199,14 @@ function adminHtml() {
|
|
|
1067
1199
|
display: none;
|
|
1068
1200
|
}
|
|
1069
1201
|
|
|
1070
|
-
@media (max-width:
|
|
1202
|
+
@media (max-width: 960px) {
|
|
1071
1203
|
.shell {
|
|
1072
|
-
padding:
|
|
1204
|
+
padding: 14px;
|
|
1073
1205
|
}
|
|
1074
1206
|
|
|
1075
1207
|
.topbar,
|
|
1076
|
-
.panel-head
|
|
1208
|
+
.panel-head,
|
|
1209
|
+
.panel-head.tight {
|
|
1077
1210
|
align-items: flex-start;
|
|
1078
1211
|
flex-direction: column;
|
|
1079
1212
|
}
|
|
@@ -1082,19 +1215,14 @@ function adminHtml() {
|
|
|
1082
1215
|
justify-content: flex-start;
|
|
1083
1216
|
}
|
|
1084
1217
|
|
|
1085
|
-
.limit-form {
|
|
1086
|
-
grid-template-columns: 1fr;
|
|
1087
|
-
width: 100%;
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
1218
|
.metrics {
|
|
1091
1219
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1092
1220
|
}
|
|
1093
1221
|
|
|
1094
|
-
.
|
|
1095
|
-
.
|
|
1096
|
-
|
|
1097
|
-
|
|
1222
|
+
.limit-form,
|
|
1223
|
+
.detail-form {
|
|
1224
|
+
grid-template-columns: 1fr;
|
|
1225
|
+
width: 100%;
|
|
1098
1226
|
}
|
|
1099
1227
|
}
|
|
1100
1228
|
</style>
|
|
@@ -1123,12 +1251,18 @@ function adminHtml() {
|
|
|
1123
1251
|
<h1>Deploy monitor</h1>
|
|
1124
1252
|
</div>
|
|
1125
1253
|
<div class="actions">
|
|
1254
|
+
<span class="status-line" id="status-line">Loading</span>
|
|
1126
1255
|
<button class="button secondary" id="refresh-button" type="button">Refresh</button>
|
|
1127
1256
|
<button class="button secondary" id="logout-button" type="button">Lock</button>
|
|
1128
1257
|
</div>
|
|
1129
1258
|
</header>
|
|
1130
1259
|
|
|
1131
|
-
<
|
|
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">
|
|
1132
1266
|
<div class="metric"><span>deploys</span><strong id="metric-deploys">0</strong></div>
|
|
1133
1267
|
<div class="metric"><span>artifact bytes</span><strong id="metric-artifacts">0 B</strong></div>
|
|
1134
1268
|
<div class="metric"><span>state bytes</span><strong id="metric-state">0 B</strong></div>
|
|
@@ -1137,35 +1271,40 @@ function adminHtml() {
|
|
|
1137
1271
|
<div class="metric"><span>mutations today</span><strong id="metric-mutations">0</strong></div>
|
|
1138
1272
|
</section>
|
|
1139
1273
|
|
|
1140
|
-
<section class="panel">
|
|
1274
|
+
<section class="panel" id="deployments-view">
|
|
1141
1275
|
<div class="panel-head">
|
|
1142
|
-
<
|
|
1143
|
-
|
|
1276
|
+
<div class="panel-title">
|
|
1277
|
+
<h2>Deployments</h2>
|
|
1278
|
+
<span class="mono" id="deployments-count">0 deploys</span>
|
|
1279
|
+
</div>
|
|
1144
1280
|
</div>
|
|
1145
1281
|
<div class="table-wrap">
|
|
1146
|
-
<table>
|
|
1282
|
+
<table class="deploy-table">
|
|
1147
1283
|
<thead>
|
|
1148
1284
|
<tr>
|
|
1149
1285
|
<th>Deploy</th>
|
|
1286
|
+
<th>User</th>
|
|
1150
1287
|
<th>Status</th>
|
|
1151
1288
|
<th>Created</th>
|
|
1152
1289
|
<th>Expires</th>
|
|
1153
|
-
<th>
|
|
1154
|
-
<th>
|
|
1155
|
-
<th>
|
|
1156
|
-
<th
|
|
1157
|
-
<th>Mutations</th>
|
|
1158
|
-
<th>Connections</th>
|
|
1290
|
+
<th>Usage</th>
|
|
1291
|
+
<th>Storage</th>
|
|
1292
|
+
<th>Conn</th>
|
|
1293
|
+
<th></th>
|
|
1159
1294
|
</tr>
|
|
1160
1295
|
</thead>
|
|
1161
1296
|
<tbody id="deploy-rows"></tbody>
|
|
1162
1297
|
</table>
|
|
1163
1298
|
</div>
|
|
1299
|
+
<div class="pager" id="deploy-pager"></div>
|
|
1164
1300
|
</section>
|
|
1165
1301
|
|
|
1166
|
-
<section class="panel">
|
|
1167
|
-
<div class="panel-head">
|
|
1168
|
-
<
|
|
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>
|
|
1169
1308
|
<form class="limit-form" id="new-user-limit-form">
|
|
1170
1309
|
<div class="field">
|
|
1171
1310
|
<label for="new-user-id">User id</label>
|
|
@@ -1179,40 +1318,121 @@ function adminHtml() {
|
|
|
1179
1318
|
<label for="new-user-mutations">Mutations</label>
|
|
1180
1319
|
<input id="new-user-mutations" name="mutationsPerDay" inputmode="numeric" min="1" step="1" type="number" />
|
|
1181
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>
|
|
1182
1325
|
<button class="button secondary" type="submit">Save</button>
|
|
1183
1326
|
</form>
|
|
1184
1327
|
</div>
|
|
1185
1328
|
<div class="table-wrap">
|
|
1186
|
-
<table>
|
|
1329
|
+
<table class="user-table">
|
|
1187
1330
|
<thead>
|
|
1188
1331
|
<tr>
|
|
1189
1332
|
<th>User</th>
|
|
1333
|
+
<th>Created</th>
|
|
1334
|
+
<th>Boost</th>
|
|
1190
1335
|
<th>Deploys</th>
|
|
1191
|
-
<th>
|
|
1192
|
-
<th>
|
|
1193
|
-
<th>Request limit</th>
|
|
1194
|
-
<th>Mutation limit</th>
|
|
1336
|
+
<th>Usage</th>
|
|
1337
|
+
<th>Limits</th>
|
|
1195
1338
|
<th>Actions</th>
|
|
1196
1339
|
</tr>
|
|
1197
1340
|
</thead>
|
|
1198
1341
|
<tbody id="user-rows"></tbody>
|
|
1199
1342
|
</table>
|
|
1200
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>
|
|
1201
1404
|
</section>
|
|
1202
1405
|
</section>
|
|
1203
1406
|
</main>
|
|
1204
1407
|
|
|
1205
1408
|
<script>
|
|
1409
|
+
const pageSize = 20;
|
|
1206
1410
|
const loginView = document.getElementById("login-view");
|
|
1207
1411
|
const dashboardView = document.getElementById("dashboard-view");
|
|
1208
1412
|
const loginForm = document.getElementById("login-form");
|
|
1209
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");
|
|
1210
1418
|
const rows = document.getElementById("deploy-rows");
|
|
1211
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");
|
|
1212
1424
|
const newUserLimitForm = document.getElementById("new-user-limit-form");
|
|
1213
|
-
const
|
|
1214
|
-
|
|
1215
|
-
|
|
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
|
+
};
|
|
1216
1436
|
|
|
1217
1437
|
function show(view) {
|
|
1218
1438
|
loginView.classList.toggle("hidden", view !== "login");
|
|
@@ -1239,21 +1459,104 @@ function adminHtml() {
|
|
|
1239
1459
|
return "unknown";
|
|
1240
1460
|
}
|
|
1241
1461
|
return new Intl.DateTimeFormat(undefined, {
|
|
1242
|
-
dateStyle: "
|
|
1462
|
+
dateStyle: "short",
|
|
1243
1463
|
timeStyle: "short"
|
|
1244
1464
|
}).format(new Date(value));
|
|
1245
1465
|
}
|
|
1246
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
|
+
|
|
1247
1475
|
function setMetric(id, value) {
|
|
1248
1476
|
document.getElementById(id).textContent = value;
|
|
1249
1477
|
}
|
|
1250
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
|
+
|
|
1251
1555
|
function parseLimitValue(value, name) {
|
|
1252
1556
|
const trimmed = String(value || "").trim();
|
|
1253
1557
|
if (!trimmed) {
|
|
1254
1558
|
return null;
|
|
1255
1559
|
}
|
|
1256
|
-
|
|
1257
1560
|
const parsed = Number(trimmed);
|
|
1258
1561
|
if (!Number.isSafeInteger(parsed) || parsed < 1) {
|
|
1259
1562
|
throw new Error(name + " must be a positive integer.");
|
|
@@ -1261,87 +1564,241 @@ function adminHtml() {
|
|
|
1261
1564
|
return parsed;
|
|
1262
1565
|
}
|
|
1263
1566
|
|
|
1264
|
-
function
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
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;
|
|
1269
1589
|
}
|
|
1270
|
-
|
|
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);
|
|
1271
1610
|
}
|
|
1272
1611
|
|
|
1273
1612
|
function deployCell(deploy) {
|
|
1274
1613
|
const td = document.createElement("td");
|
|
1275
1614
|
const wrap = document.createElement("div");
|
|
1276
|
-
const name = document.createElement("
|
|
1277
|
-
const link = document.createElement("a");
|
|
1615
|
+
const name = document.createElement("a");
|
|
1278
1616
|
const id = document.createElement("small");
|
|
1279
|
-
wrap.className = "
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
name.
|
|
1283
|
-
|
|
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;
|
|
1284
1623
|
wrap.appendChild(name);
|
|
1285
1624
|
wrap.appendChild(id);
|
|
1286
1625
|
td.appendChild(wrap);
|
|
1287
1626
|
return td;
|
|
1288
1627
|
}
|
|
1289
1628
|
|
|
1290
|
-
function
|
|
1629
|
+
function deployUserCell(deploy) {
|
|
1291
1630
|
const td = document.createElement("td");
|
|
1292
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");
|
|
1293
1665
|
const pill = document.createElement("span");
|
|
1294
|
-
wrap.className = "status-cell";
|
|
1295
1666
|
pill.className = "pill " + deploy.status;
|
|
1296
1667
|
pill.textContent = deploy.status;
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
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);
|
|
1309
1707
|
});
|
|
1310
|
-
|
|
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;
|
|
1311
1722
|
}
|
|
1312
1723
|
|
|
1313
|
-
|
|
1314
|
-
|
|
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
|
+
}
|
|
1315
1737
|
}
|
|
1316
1738
|
|
|
1317
1739
|
function userCell(user) {
|
|
1318
1740
|
const td = document.createElement("td");
|
|
1319
1741
|
const wrap = document.createElement("div");
|
|
1320
|
-
const
|
|
1742
|
+
const line = document.createElement("div");
|
|
1743
|
+
const name = document.createElement("a");
|
|
1321
1744
|
const id = document.createElement("small");
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
name.textContent = user.login || user.displayName || user.id;
|
|
1330
|
-
}
|
|
1331
|
-
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;
|
|
1332
1752
|
id.textContent = user.id;
|
|
1333
|
-
|
|
1753
|
+
line.appendChild(name);
|
|
1754
|
+
const github = githubProfileLink(user.url, label);
|
|
1755
|
+
if (github) {
|
|
1756
|
+
line.appendChild(github);
|
|
1757
|
+
}
|
|
1758
|
+
wrap.appendChild(line);
|
|
1334
1759
|
wrap.appendChild(id);
|
|
1335
1760
|
td.appendChild(wrap);
|
|
1336
1761
|
return td;
|
|
1337
1762
|
}
|
|
1338
1763
|
|
|
1339
|
-
function
|
|
1764
|
+
function boostCell(user) {
|
|
1340
1765
|
const td = document.createElement("td");
|
|
1341
|
-
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");
|
|
1342
1797
|
const input = document.createElement("input");
|
|
1343
1798
|
const effective = document.createElement("small");
|
|
1344
|
-
|
|
1799
|
+
row.className = "limit-row";
|
|
1800
|
+
prefix.className = "mono";
|
|
1801
|
+
prefix.textContent = shortLabel;
|
|
1345
1802
|
input.inputMode = "numeric";
|
|
1346
1803
|
input.min = "1";
|
|
1347
1804
|
input.step = "1";
|
|
@@ -1350,10 +1807,19 @@ function adminHtml() {
|
|
|
1350
1807
|
input.value = user.limitOverrides[key] ? String(user.limitOverrides[key]) : "";
|
|
1351
1808
|
input.placeholder = String(user.defaultLimits[key]);
|
|
1352
1809
|
input.setAttribute("aria-label", label + " override for " + user.id);
|
|
1353
|
-
effective.
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
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"));
|
|
1357
1823
|
td.appendChild(wrap);
|
|
1358
1824
|
return td;
|
|
1359
1825
|
}
|
|
@@ -1364,21 +1830,24 @@ function adminHtml() {
|
|
|
1364
1830
|
const save = document.createElement("button");
|
|
1365
1831
|
const clear = document.createElement("button");
|
|
1366
1832
|
wrap.className = "limit-actions";
|
|
1367
|
-
save.className = "row-action
|
|
1833
|
+
save.className = "row-action";
|
|
1368
1834
|
save.type = "button";
|
|
1369
|
-
save.textContent = savingUserId === user.id ? "Saving" : "Save";
|
|
1370
|
-
save.disabled = savingUserId === user.id;
|
|
1835
|
+
save.textContent = state.savingUserId === user.id ? "Saving" : "Save";
|
|
1836
|
+
save.disabled = state.savingUserId === user.id;
|
|
1371
1837
|
save.addEventListener("click", () => {
|
|
1372
|
-
void
|
|
1838
|
+
void saveUserSettings(user.id, save.closest("tr")).catch((error) => {
|
|
1373
1839
|
statusLine.textContent = error instanceof Error ? error.message : String(error);
|
|
1374
1840
|
});
|
|
1375
1841
|
});
|
|
1376
|
-
clear.className = "row-action
|
|
1842
|
+
clear.className = "row-action";
|
|
1377
1843
|
clear.type = "button";
|
|
1378
1844
|
clear.textContent = "Clear";
|
|
1379
|
-
clear.disabled = savingUserId === user.id;
|
|
1845
|
+
clear.disabled = state.savingUserId === user.id;
|
|
1380
1846
|
clear.addEventListener("click", () => {
|
|
1381
|
-
void
|
|
1847
|
+
void saveUserSettings(user.id, null, {
|
|
1848
|
+
boost: Boolean(user.boost),
|
|
1849
|
+
limits: { requestsPerDay: null, mutationsPerDay: null }
|
|
1850
|
+
}).catch((error) => {
|
|
1382
1851
|
statusLine.textContent = error instanceof Error ? error.message : String(error);
|
|
1383
1852
|
});
|
|
1384
1853
|
});
|
|
@@ -1388,7 +1857,7 @@ function adminHtml() {
|
|
|
1388
1857
|
return td;
|
|
1389
1858
|
}
|
|
1390
1859
|
|
|
1391
|
-
function
|
|
1860
|
+
function renderMetrics(summary) {
|
|
1392
1861
|
setMetric("metric-deploys", formatNumber(summary.deployCount));
|
|
1393
1862
|
setMetric("metric-artifacts", formatBytes(summary.totals.artifactBytes));
|
|
1394
1863
|
setMetric("metric-state", formatBytes(summary.totals.stateBytes));
|
|
@@ -1396,42 +1865,128 @@ function adminHtml() {
|
|
|
1396
1865
|
setMetric("metric-requests", formatNumber(summary.totals.requestsToday));
|
|
1397
1866
|
setMetric("metric-mutations", formatNumber(summary.totals.mutationsToday));
|
|
1398
1867
|
statusLine.textContent = "Updated " + formatTime(summary.generatedAt);
|
|
1399
|
-
|
|
1868
|
+
}
|
|
1400
1869
|
|
|
1401
|
-
|
|
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) {
|
|
1402
1890
|
const tr = document.createElement("tr");
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
tr.appendChild(
|
|
1406
|
-
|
|
1407
|
-
tr.appendChild(textCell(formatBytes(deploy.artifactBytes), "mono"));
|
|
1408
|
-
tr.appendChild(textCell(formatBytes(deploy.stateBytes) + " / " + formatNumber(deploy.stateRows) + " rows", "mono"));
|
|
1409
|
-
tr.appendChild(textCell(formatBytes(deploy.logBytes) + " / " + formatNumber(deploy.logEntries) + " entries", "mono"));
|
|
1410
|
-
tr.appendChild(textCell(formatNumber(deploy.requestsToday) + " / " + formatNumber(deploy.limits.requestsPerDay), "mono"));
|
|
1411
|
-
tr.appendChild(textCell(formatNumber(deploy.mutationsToday) + " / " + formatNumber(deploy.limits.mutationsPerDay), "mono"));
|
|
1412
|
-
tr.appendChild(textCell(formatNumber(deploy.connections), "mono"));
|
|
1413
|
-
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);
|
|
1414
1895
|
}
|
|
1415
1896
|
|
|
1416
|
-
|
|
1417
|
-
for (const user of summary.users || []) {
|
|
1897
|
+
for (const user of page.items) {
|
|
1418
1898
|
const tr = document.createElement("tr");
|
|
1419
1899
|
tr.appendChild(userCell(user));
|
|
1900
|
+
tr.appendChild(textCell(formatTime(user.createdAt), "mono"));
|
|
1901
|
+
tr.appendChild(boostCell(user));
|
|
1420
1902
|
tr.appendChild(textCell(formatNumber(user.activeDeployCount) + " / " + formatNumber(user.deployCount), "mono"));
|
|
1421
|
-
tr.appendChild(
|
|
1422
|
-
tr.appendChild(
|
|
1423
|
-
tr.appendChild(limitInputCell(user, "requestsPerDay", "Request limit"));
|
|
1424
|
-
tr.appendChild(limitInputCell(user, "mutationsPerDay", "Mutation limit"));
|
|
1903
|
+
tr.appendChild(userUsageCell(user));
|
|
1904
|
+
tr.appendChild(userLimitsCell(user));
|
|
1425
1905
|
tr.appendChild(userActionCell(user));
|
|
1426
1906
|
userRows.appendChild(tr);
|
|
1427
1907
|
}
|
|
1428
1908
|
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
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);
|
|
1435
1990
|
}
|
|
1436
1991
|
}
|
|
1437
1992
|
|
|
@@ -1449,21 +2004,47 @@ function adminHtml() {
|
|
|
1449
2004
|
if (!response.ok) {
|
|
1450
2005
|
throw new Error(await response.text());
|
|
1451
2006
|
}
|
|
1452
|
-
|
|
2007
|
+
state.summary = await response.json();
|
|
2008
|
+
state.userDetail = null;
|
|
1453
2009
|
show("dashboard");
|
|
2010
|
+
renderCurrent();
|
|
1454
2011
|
}
|
|
1455
2012
|
|
|
1456
|
-
async function
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
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
|
+
}
|
|
1460
2038
|
};
|
|
2039
|
+
}
|
|
1461
2040
|
|
|
1462
|
-
|
|
1463
|
-
|
|
2041
|
+
async function saveUserSettings(userId, row, explicitSettings) {
|
|
2042
|
+
const settings = explicitSettings || settingsFromRow(row);
|
|
2043
|
+
state.savingUserId = userId;
|
|
2044
|
+
statusLine.textContent = "Saving " + userId;
|
|
1464
2045
|
try {
|
|
1465
2046
|
const response = await fetch("/admin/api/users/" + encodeURIComponent(userId) + "/limits", {
|
|
1466
|
-
body: JSON.stringify(
|
|
2047
|
+
body: JSON.stringify(settings),
|
|
1467
2048
|
headers: { "Content-Type": "application/json" },
|
|
1468
2049
|
method: "PUT"
|
|
1469
2050
|
});
|
|
@@ -1477,7 +2058,7 @@ function adminHtml() {
|
|
|
1477
2058
|
throw new Error(await response.text());
|
|
1478
2059
|
}
|
|
1479
2060
|
} finally {
|
|
1480
|
-
savingUserId = null;
|
|
2061
|
+
state.savingUserId = null;
|
|
1481
2062
|
}
|
|
1482
2063
|
await loadSummary();
|
|
1483
2064
|
}
|
|
@@ -1487,12 +2068,12 @@ function adminHtml() {
|
|
|
1487
2068
|
return;
|
|
1488
2069
|
}
|
|
1489
2070
|
|
|
1490
|
-
terminatingDeployId = deploy.id;
|
|
2071
|
+
state.terminatingDeployId = deploy.id;
|
|
1491
2072
|
statusLine.textContent = "Terminating " + deploy.id;
|
|
1492
2073
|
const response = await fetch("/admin/api/deploys/" + encodeURIComponent(deploy.id) + "/terminate", {
|
|
1493
2074
|
method: "POST"
|
|
1494
2075
|
});
|
|
1495
|
-
terminatingDeployId = null;
|
|
2076
|
+
state.terminatingDeployId = null;
|
|
1496
2077
|
|
|
1497
2078
|
if (response.status === 401) {
|
|
1498
2079
|
show("login");
|
|
@@ -1534,16 +2115,20 @@ function adminHtml() {
|
|
|
1534
2115
|
}
|
|
1535
2116
|
|
|
1536
2117
|
try {
|
|
2118
|
+
const boost = document.getElementById("new-user-boost").checked;
|
|
1537
2119
|
const requestsPerDay = parseLimitValue(form.get("requestsPerDay"), "Request limit");
|
|
1538
2120
|
const mutationsPerDay = parseLimitValue(form.get("mutationsPerDay"), "Mutation limit");
|
|
1539
|
-
if (!requestsPerDay && !mutationsPerDay) {
|
|
1540
|
-
statusLine.textContent = "Enter
|
|
2121
|
+
if (!boost && !requestsPerDay && !mutationsPerDay) {
|
|
2122
|
+
statusLine.textContent = "Enter a custom limit or turn on boost.";
|
|
1541
2123
|
return;
|
|
1542
2124
|
}
|
|
1543
2125
|
|
|
1544
|
-
await
|
|
1545
|
-
|
|
1546
|
-
|
|
2126
|
+
await saveUserSettings(userId, null, {
|
|
2127
|
+
boost,
|
|
2128
|
+
limits: {
|
|
2129
|
+
requestsPerDay,
|
|
2130
|
+
mutationsPerDay
|
|
2131
|
+
}
|
|
1547
2132
|
});
|
|
1548
2133
|
newUserLimitForm.reset();
|
|
1549
2134
|
} catch (error) {
|
|
@@ -1551,15 +2136,60 @@ function adminHtml() {
|
|
|
1551
2136
|
}
|
|
1552
2137
|
});
|
|
1553
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"));
|
|
1554
2173
|
document.getElementById("refresh-button").addEventListener("click", () => {
|
|
1555
2174
|
void loadSummary();
|
|
1556
2175
|
});
|
|
1557
|
-
|
|
1558
2176
|
document.getElementById("logout-button").addEventListener("click", async () => {
|
|
1559
2177
|
await fetch("/admin/api/logout", { method: "POST" });
|
|
1560
2178
|
show("login");
|
|
1561
2179
|
});
|
|
1562
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
|
+
|
|
1563
2193
|
void loadSummary().catch((error) => {
|
|
1564
2194
|
statusLine.textContent = error.message;
|
|
1565
2195
|
show("dashboard");
|
|
@@ -1601,7 +2231,7 @@ function developerHtml({ authConfigured, deploys = [], user }) {
|
|
|
1601
2231
|
const rows = deploys
|
|
1602
2232
|
.map(
|
|
1603
2233
|
(deploy) => `<tr>
|
|
1604
|
-
<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>
|
|
1605
2235
|
<td><span class="pill ${escapeHtml(deploy.status)}">${escapeHtml(deploy.status)}</span></td>
|
|
1606
2236
|
<td>${escapeHtml(new Date(deploy.createdAt).toLocaleString())}</td>
|
|
1607
2237
|
<td>${escapeHtml(new Date(deploy.expiresAt).toLocaleString())}</td>
|
|
@@ -1844,9 +2474,18 @@ export class MemoryAnonymousStore {
|
|
|
1844
2474
|
return normalizeUserLimitOverrides(this.users.get(normalizeUserId(userId))?.limitOverrides ?? {});
|
|
1845
2475
|
}
|
|
1846
2476
|
|
|
1847
|
-
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 } = {}) {
|
|
1848
2486
|
const id = normalizeUserId(userId);
|
|
1849
2487
|
const current = this.users.get(id) ?? {
|
|
2488
|
+
boost: false,
|
|
1850
2489
|
createdAt: now(),
|
|
1851
2490
|
displayName: id,
|
|
1852
2491
|
id,
|
|
@@ -1854,28 +2493,36 @@ export class MemoryAnonymousStore {
|
|
|
1854
2493
|
};
|
|
1855
2494
|
const user = {
|
|
1856
2495
|
...current,
|
|
2496
|
+
boost: boost === undefined ? Boolean(current.boost) : Boolean(boost),
|
|
1857
2497
|
displayName: current.displayName ?? current.login ?? id,
|
|
1858
2498
|
id,
|
|
1859
|
-
limitOverrides:
|
|
2499
|
+
limitOverrides:
|
|
2500
|
+
limitOverrides === undefined
|
|
2501
|
+
? normalizeUserLimitOverrides(current.limitOverrides ?? {})
|
|
2502
|
+
: normalizeUserLimitOverrides(limitOverrides),
|
|
1860
2503
|
updatedAt: now()
|
|
1861
2504
|
};
|
|
1862
2505
|
this.users.set(id, user);
|
|
1863
2506
|
return this.getAdminUser(id);
|
|
1864
2507
|
}
|
|
1865
2508
|
|
|
2509
|
+
async setUserLimitOverrides(userId, limitOverrides) {
|
|
2510
|
+
return this.setAdminUserSettings(userId, { limitOverrides });
|
|
2511
|
+
}
|
|
2512
|
+
|
|
1866
2513
|
async deployWithUserLimitOverrides(deploy) {
|
|
1867
2514
|
if (!deploy?.ownerId) {
|
|
1868
2515
|
return deploy;
|
|
1869
2516
|
}
|
|
1870
2517
|
|
|
1871
|
-
const limitOverrides = await this.
|
|
1872
|
-
if (Object.keys(limitOverrides).length === 0) {
|
|
2518
|
+
const { boost, limitOverrides } = await this.getUserLimitSettings(deploy.ownerId);
|
|
2519
|
+
if (!boost && Object.keys(limitOverrides).length === 0) {
|
|
1873
2520
|
return deploy;
|
|
1874
2521
|
}
|
|
1875
2522
|
|
|
1876
2523
|
return {
|
|
1877
2524
|
...deploy,
|
|
1878
|
-
limits: limitsWithOverrides(deploy.limits, limitOverrides)
|
|
2525
|
+
limits: limitsWithOverrides(deploy.limits, limitOverrides, boost)
|
|
1879
2526
|
};
|
|
1880
2527
|
}
|
|
1881
2528
|
|
|
@@ -1918,7 +2565,11 @@ export class MemoryAnonymousStore {
|
|
|
1918
2565
|
|
|
1919
2566
|
return users
|
|
1920
2567
|
.filter(Boolean)
|
|
1921
|
-
.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
|
+
);
|
|
1922
2573
|
}
|
|
1923
2574
|
|
|
1924
2575
|
storeArtifact({ artifact, artifactHash, clientBundleBase64, clientBundleHash, createdAt }) {
|
|
@@ -2361,15 +3012,17 @@ export class PostgresAnonymousStore {
|
|
|
2361
3012
|
display_name text,
|
|
2362
3013
|
avatar_url text,
|
|
2363
3014
|
url text,
|
|
3015
|
+
boost boolean not null default false,
|
|
2364
3016
|
limit_overrides_json jsonb not null default '{}',
|
|
2365
3017
|
created_at timestamptz not null,
|
|
2366
3018
|
updated_at timestamptz not null
|
|
2367
3019
|
)
|
|
2368
3020
|
`);
|
|
3021
|
+
await this.query("alter table users add column if not exists boost boolean not null default false");
|
|
2369
3022
|
await this.query(`
|
|
2370
3023
|
insert into users(
|
|
2371
3024
|
id, provider, provider_id, login, display_name, avatar_url, url,
|
|
2372
|
-
limit_overrides_json, created_at, updated_at
|
|
3025
|
+
boost, limit_overrides_json, created_at, updated_at
|
|
2373
3026
|
)
|
|
2374
3027
|
select distinct on (owner_id)
|
|
2375
3028
|
owner_id,
|
|
@@ -2379,6 +3032,7 @@ export class PostgresAnonymousStore {
|
|
|
2379
3032
|
coalesce(owner_json->>'displayName', owner_json->>'login', owner_id),
|
|
2380
3033
|
owner_json->>'avatarUrl',
|
|
2381
3034
|
owner_json->>'url',
|
|
3035
|
+
false,
|
|
2382
3036
|
'{}'::jsonb,
|
|
2383
3037
|
coalesce(claimed_at, created_at, now()),
|
|
2384
3038
|
coalesce(claimed_at, created_at, now())
|
|
@@ -2411,6 +3065,7 @@ export class PostgresAnonymousStore {
|
|
|
2411
3065
|
|
|
2412
3066
|
return {
|
|
2413
3067
|
avatarUrl: row.avatar_url ?? null,
|
|
3068
|
+
boost: Boolean(row.boost),
|
|
2414
3069
|
createdAt: new Date(row.created_at).toISOString(),
|
|
2415
3070
|
displayName: row.display_name ?? row.login ?? row.id,
|
|
2416
3071
|
id: row.id,
|
|
@@ -2462,35 +3117,54 @@ export class PostgresAnonymousStore {
|
|
|
2462
3117
|
return normalizeUserLimitOverrides(result.rows[0]?.limit_overrides_json ?? {});
|
|
2463
3118
|
}
|
|
2464
3119
|
|
|
2465
|
-
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 } = {}) {
|
|
2466
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);
|
|
2467
3136
|
const updatedAt = now();
|
|
2468
3137
|
await this.query(
|
|
2469
3138
|
`
|
|
2470
|
-
insert into users(id, display_name, limit_overrides_json, created_at, updated_at)
|
|
2471
|
-
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)
|
|
2472
3141
|
on conflict(id) do update set
|
|
3142
|
+
boost = excluded.boost,
|
|
2473
3143
|
limit_overrides_json = excluded.limit_overrides_json,
|
|
2474
3144
|
updated_at = excluded.updated_at
|
|
2475
3145
|
`,
|
|
2476
|
-
[id, id, JSON.stringify(
|
|
3146
|
+
[id, id, nextBoost, JSON.stringify(nextLimitOverrides), updatedAt]
|
|
2477
3147
|
);
|
|
2478
3148
|
return this.getAdminUser(id);
|
|
2479
3149
|
}
|
|
2480
3150
|
|
|
3151
|
+
async setUserLimitOverrides(userId, limitOverrides) {
|
|
3152
|
+
return this.setAdminUserSettings(userId, { limitOverrides });
|
|
3153
|
+
}
|
|
3154
|
+
|
|
2481
3155
|
async deployWithUserLimitOverrides(deploy) {
|
|
2482
3156
|
if (!deploy?.ownerId) {
|
|
2483
3157
|
return deploy;
|
|
2484
3158
|
}
|
|
2485
3159
|
|
|
2486
|
-
const limitOverrides = await this.
|
|
2487
|
-
if (Object.keys(limitOverrides).length === 0) {
|
|
3160
|
+
const { boost, limitOverrides } = await this.getUserLimitSettings(deploy.ownerId);
|
|
3161
|
+
if (!boost && Object.keys(limitOverrides).length === 0) {
|
|
2488
3162
|
return deploy;
|
|
2489
3163
|
}
|
|
2490
3164
|
|
|
2491
3165
|
return {
|
|
2492
3166
|
...deploy,
|
|
2493
|
-
limits: limitsWithOverrides(deploy.limits, limitOverrides)
|
|
3167
|
+
limits: limitsWithOverrides(deploy.limits, limitOverrides, boost)
|
|
2494
3168
|
};
|
|
2495
3169
|
}
|
|
2496
3170
|
|
|
@@ -2579,7 +3253,7 @@ export class PostgresAnonymousStore {
|
|
|
2579
3253
|
where d.owner_id is not null
|
|
2580
3254
|
group by d.owner_id
|
|
2581
3255
|
) q on q.owner_id = u.id
|
|
2582
|
-
order by coalesce(u.login, u.id) asc
|
|
3256
|
+
order by u.created_at desc, coalesce(u.login, u.id) asc
|
|
2583
3257
|
`,
|
|
2584
3258
|
[windowStart]
|
|
2585
3259
|
);
|
|
@@ -3185,6 +3859,7 @@ export async function startAnonymousServer({
|
|
|
3185
3859
|
publicRootUrl,
|
|
3186
3860
|
quiet = false,
|
|
3187
3861
|
shooBaseUrl = shooBaseUrlFromEnv(),
|
|
3862
|
+
sourceRuntime,
|
|
3188
3863
|
store
|
|
3189
3864
|
} = {}) {
|
|
3190
3865
|
const resolvedAppBaseDomain = normalizeAppBaseDomain(appBaseDomain);
|
|
@@ -3193,6 +3868,7 @@ export async function startAnonymousServer({
|
|
|
3193
3868
|
const resolvedDeveloperSessionSecret =
|
|
3194
3869
|
developerSessionSecret || resolvedGithubOAuth?.sessionSecret || resolvedGithubOAuth?.clientSecret || adminPassword || "";
|
|
3195
3870
|
const resolvedStore = store ?? (await createAnonymousStoreFromEnv());
|
|
3871
|
+
const resolvedSourceRuntime = sourceRuntime === undefined ? createSourceRuntimeFromEnv() : sourceRuntime;
|
|
3196
3872
|
await resolvedStore.initialize();
|
|
3197
3873
|
const subscriptions = new Map();
|
|
3198
3874
|
|
|
@@ -3204,12 +3880,16 @@ export async function startAnonymousServer({
|
|
|
3204
3880
|
return counts;
|
|
3205
3881
|
}
|
|
3206
3882
|
|
|
3207
|
-
async function
|
|
3883
|
+
async function adminDeploysWithConnections() {
|
|
3208
3884
|
const connections = activeConnectionCounts();
|
|
3209
|
-
|
|
3885
|
+
return (await resolvedStore.listDeployResourceUsage()).map((deploy) => ({
|
|
3210
3886
|
...deploy,
|
|
3211
3887
|
connections: connections.get(deploy.id) ?? 0
|
|
3212
3888
|
}));
|
|
3889
|
+
}
|
|
3890
|
+
|
|
3891
|
+
async function adminSummary() {
|
|
3892
|
+
const deploys = await adminDeploysWithConnections();
|
|
3213
3893
|
const users = await resolvedStore.listAdminUsers();
|
|
3214
3894
|
const totals = deploys.reduce(
|
|
3215
3895
|
(acc, deploy) => ({
|
|
@@ -3244,6 +3924,21 @@ export async function startAnonymousServer({
|
|
|
3244
3924
|
};
|
|
3245
3925
|
}
|
|
3246
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
|
+
|
|
3247
3942
|
function currentDeveloper(req) {
|
|
3248
3943
|
return developerFromRequest(req, resolvedDeveloperSessionSecret);
|
|
3249
3944
|
}
|
|
@@ -3304,6 +3999,7 @@ export async function startAnonymousServer({
|
|
|
3304
3999
|
auth: subscription.auth,
|
|
3305
4000
|
deployId,
|
|
3306
4001
|
name,
|
|
4002
|
+
sourceRuntime: resolvedSourceRuntime,
|
|
3307
4003
|
state: resolvedStore
|
|
3308
4004
|
});
|
|
3309
4005
|
websocketSend(ws, { data, name, op: "query.result" });
|
|
@@ -3495,7 +4191,14 @@ export async function startAnonymousServer({
|
|
|
3495
4191
|
return;
|
|
3496
4192
|
}
|
|
3497
4193
|
|
|
3498
|
-
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
|
+
) {
|
|
3499
4202
|
sendText(res, 200, adminHtml(), { "Content-Type": "text/html; charset=utf-8" });
|
|
3500
4203
|
return;
|
|
3501
4204
|
}
|
|
@@ -3536,6 +4239,29 @@ export async function startAnonymousServer({
|
|
|
3536
4239
|
return;
|
|
3537
4240
|
}
|
|
3538
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
|
+
|
|
3539
4265
|
const userLimitsMatch =
|
|
3540
4266
|
req.method === "PUT" ? requestUrl.pathname.match(/^\/admin\/api\/users\/([^/]+)\/limits$/) : null;
|
|
3541
4267
|
if (userLimitsMatch) {
|
|
@@ -3551,14 +4277,22 @@ export async function startAnonymousServer({
|
|
|
3551
4277
|
|
|
3552
4278
|
const body = await readJsonBody(req, 4096);
|
|
3553
4279
|
let limitOverrides;
|
|
4280
|
+
let boost;
|
|
3554
4281
|
try {
|
|
3555
|
-
|
|
4282
|
+
const rawLimitOverrides = body.limits ?? body.limitOverrides;
|
|
4283
|
+
limitOverrides = rawLimitOverrides === undefined ? undefined : normalizeUserLimitOverrides(rawLimitOverrides);
|
|
4284
|
+
boost = normalizeOptionalBoolean(body.boost, "boost");
|
|
3556
4285
|
} catch (error) {
|
|
3557
4286
|
sendJson(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
3558
4287
|
return;
|
|
3559
4288
|
}
|
|
3560
4289
|
|
|
3561
|
-
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 ?? {}));
|
|
3562
4296
|
await refreshOwnerSubscriptions(user.id);
|
|
3563
4297
|
sendJson(res, 200, { user });
|
|
3564
4298
|
return;
|
|
@@ -3762,6 +4496,7 @@ export async function startAnonymousServer({
|
|
|
3762
4496
|
auth: subscription.auth,
|
|
3763
4497
|
deployId: subscription.deploy.id,
|
|
3764
4498
|
name: message.name,
|
|
4499
|
+
sourceRuntime: resolvedSourceRuntime,
|
|
3765
4500
|
state: resolvedStore
|
|
3766
4501
|
});
|
|
3767
4502
|
websocketSend(ws, { data, id: message.id, name: message.name, ok: true, op: "query.result" });
|
|
@@ -3779,7 +4514,9 @@ export async function startAnonymousServer({
|
|
|
3779
4514
|
artifact: subscription.artifact,
|
|
3780
4515
|
auth: subscription.auth,
|
|
3781
4516
|
deployId: subscription.deploy.id,
|
|
4517
|
+
limits: subscription.deploy.limits,
|
|
3782
4518
|
name: message.name,
|
|
4519
|
+
sourceRuntime: resolvedSourceRuntime,
|
|
3783
4520
|
state: resolvedStore
|
|
3784
4521
|
});
|
|
3785
4522
|
websocketSend(ws, { id: message.id, ok: true, op: "mutation.result", result });
|