git-watchtower 2.2.3 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/server/web-ui/css.js +207 -199
- package/src/server/web-ui/html.js +28 -18
- package/src/server/web-ui/js.js +79 -87
package/package.json
CHANGED
package/src/server/web-ui/css.js
CHANGED
|
@@ -63,9 +63,11 @@ function getDashboardCss() {
|
|
|
63
63
|
border-bottom: 1px solid rgba(255,255,255,0.08);
|
|
64
64
|
user-select: none;
|
|
65
65
|
box-shadow: 0 1px 8px rgba(0,0,0,0.3);
|
|
66
|
+
/* position:relative so .casino-reels-header (absolute) anchors here. */
|
|
66
67
|
position: relative;
|
|
67
68
|
z-index: 10;
|
|
68
69
|
}
|
|
70
|
+
.header-icon { display: inline-block; }
|
|
69
71
|
.header-left {
|
|
70
72
|
display: flex;
|
|
71
73
|
align-items: center;
|
|
@@ -113,7 +115,10 @@ function getDashboardCss() {
|
|
|
113
115
|
.layout {
|
|
114
116
|
display: grid;
|
|
115
117
|
grid-template-columns: 1fr 320px;
|
|
116
|
-
|
|
118
|
+
/* Row 1: branch panel + sidebar.
|
|
119
|
+
Row 2: dashboard-stats (full-width).
|
|
120
|
+
Row 3: keyboard-shortcut footer (full-width). */
|
|
121
|
+
grid-template-rows: 1fr auto auto;
|
|
117
122
|
height: calc(100vh - 49px);
|
|
118
123
|
min-height: 0;
|
|
119
124
|
gap: 0;
|
|
@@ -759,24 +764,6 @@ function getDashboardCss() {
|
|
|
759
764
|
.info-label { color: var(--text-muted); font-weight: 500; }
|
|
760
765
|
.info-value { color: var(--text); font-family: var(--font-mono); }
|
|
761
766
|
|
|
762
|
-
/* ── Session Stats (footer) ──────────────────────────────────── */
|
|
763
|
-
.stats-bar {
|
|
764
|
-
display: flex;
|
|
765
|
-
gap: 16px;
|
|
766
|
-
align-items: center;
|
|
767
|
-
font-size: 11px;
|
|
768
|
-
color: var(--text-muted);
|
|
769
|
-
font-family: var(--font-mono);
|
|
770
|
-
flex-wrap: wrap;
|
|
771
|
-
}
|
|
772
|
-
.stats-bar .stat-item {
|
|
773
|
-
display: flex;
|
|
774
|
-
align-items: center;
|
|
775
|
-
gap: 4px;
|
|
776
|
-
}
|
|
777
|
-
.stats-bar .stat-value { color: var(--text-dim); font-weight: 600; }
|
|
778
|
-
.stats-bar .stat-label { font-family: var(--font); }
|
|
779
|
-
|
|
780
767
|
/* ── Cleanup Modal ───────────────────────────────────────────── */
|
|
781
768
|
.cleanup-branch-list {
|
|
782
769
|
display: flex;
|
|
@@ -909,172 +896,238 @@ function getDashboardCss() {
|
|
|
909
896
|
.pref-btn:hover { background: var(--bg-surface-hover); color: var(--text-dim); border-color: var(--text-muted); }
|
|
910
897
|
.pref-btn.active { background: var(--accent-dim); color: #fff; border-color: var(--accent-dim); }
|
|
911
898
|
|
|
912
|
-
/* ──
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
899
|
+
/* ── Dashboard Stats Bar (always-on, above the keyboard footer) ──
|
|
900
|
+
This is the canonical place for live session stats. The same
|
|
901
|
+
element re-skins to "casino winnings" when state.casinoModeEnabled
|
|
902
|
+
flips on, so users get the same row in both modes. */
|
|
903
|
+
.dashboard-stats {
|
|
904
|
+
grid-column: 1 / -1;
|
|
905
|
+
padding: 8px 20px;
|
|
906
|
+
background: var(--bg-surface);
|
|
907
|
+
border-top: 1px solid var(--border);
|
|
908
|
+
display: flex;
|
|
909
|
+
flex-wrap: wrap;
|
|
910
|
+
gap: 8px 24px;
|
|
911
|
+
align-items: center;
|
|
912
|
+
justify-content: space-between;
|
|
916
913
|
font-size: 11px;
|
|
917
914
|
color: var(--text-dim);
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
915
|
+
transition: background 0.25s, border-color 0.25s, box-shadow 0.25s;
|
|
916
|
+
}
|
|
917
|
+
.dashboard-stats .stats-group {
|
|
918
|
+
display: flex;
|
|
919
|
+
flex-wrap: wrap;
|
|
920
|
+
align-items: center;
|
|
921
|
+
gap: 14px;
|
|
922
|
+
}
|
|
923
|
+
/* Title pill anchors the bar — makes it obvious these are global
|
|
924
|
+
session metrics, not row-specific. */
|
|
925
|
+
.dashboard-stats .stats-title {
|
|
926
|
+
font-size: 10px;
|
|
927
|
+
font-weight: 800;
|
|
928
|
+
letter-spacing: 1.2px;
|
|
929
|
+
text-transform: uppercase;
|
|
930
|
+
color: var(--text-muted);
|
|
931
|
+
padding: 3px 10px;
|
|
932
|
+
border-radius: 999px;
|
|
921
933
|
background: var(--bg);
|
|
934
|
+
border: 1px solid var(--border);
|
|
935
|
+
display: inline-flex;
|
|
936
|
+
align-items: center;
|
|
937
|
+
gap: 6px;
|
|
938
|
+
}
|
|
939
|
+
.dashboard-stats .stat {
|
|
940
|
+
display: inline-flex;
|
|
941
|
+
align-items: baseline;
|
|
942
|
+
gap: 6px;
|
|
943
|
+
white-space: nowrap;
|
|
922
944
|
}
|
|
923
|
-
.
|
|
945
|
+
.dashboard-stats .stat-k {
|
|
924
946
|
color: var(--text-muted);
|
|
925
947
|
text-transform: uppercase;
|
|
926
948
|
letter-spacing: 0.6px;
|
|
927
949
|
font-size: 10px;
|
|
928
950
|
font-weight: 600;
|
|
929
|
-
align-self: center;
|
|
930
951
|
}
|
|
931
|
-
.
|
|
952
|
+
.dashboard-stats .stat-v {
|
|
932
953
|
color: var(--text);
|
|
933
954
|
font-family: var(--font-mono);
|
|
934
955
|
font-size: 12px;
|
|
935
|
-
|
|
956
|
+
font-weight: 600;
|
|
936
957
|
}
|
|
937
|
-
.
|
|
938
|
-
.
|
|
939
|
-
.
|
|
940
|
-
.
|
|
958
|
+
.dashboard-stats .stat-v .added { color: var(--green); }
|
|
959
|
+
.dashboard-stats .stat-v .deleted { color: var(--red); }
|
|
960
|
+
.dashboard-stats .stat-v .sep { color: var(--text-muted); }
|
|
961
|
+
.dashboard-stats .stat-v .accent { color: var(--accent); }
|
|
962
|
+
/* Casino skin: same row, neon-pulsed, brighter title. */
|
|
963
|
+
.dashboard-stats.casino-mode {
|
|
964
|
+
background: linear-gradient(90deg, #1a0a24 0%, #2a0a36 50%, #1a0a24 100%);
|
|
965
|
+
border-top: 2px solid #ff2d7a;
|
|
966
|
+
box-shadow: inset 0 0 24px rgba(255, 45, 122, 0.25);
|
|
967
|
+
}
|
|
968
|
+
.dashboard-stats.casino-mode .stats-title {
|
|
969
|
+
color: #ffd400;
|
|
970
|
+
background: rgba(177, 0, 150, 0.4);
|
|
971
|
+
border-color: #ff2d7a;
|
|
972
|
+
text-shadow: 0 0 6px rgba(255, 220, 64, 0.6);
|
|
973
|
+
}
|
|
974
|
+
.dashboard-stats.casino-mode .stat-k { color: #ffd400; }
|
|
975
|
+
.dashboard-stats.casino-mode .stat-v { color: var(--text); }
|
|
976
|
+
.dashboard-stats.casino-mode .stat-v .pos { color: #3fb950; }
|
|
977
|
+
.dashboard-stats.casino-mode .stat-v .neg { color: #f85149; }
|
|
978
|
+
.dashboard-stats.casino-mode .stat-v .gold { color: #ffd400; }
|
|
979
|
+
.dashboard-stats.casino-mode .stat-v .neon { color: #29d4ff; }
|
|
980
|
+
|
|
981
|
+
/* ── Casino Mode ────────────────────────────────────────────────
|
|
982
|
+
Edge strips, header reskin, header reels, win/loss overlays. The
|
|
983
|
+
stats live in .dashboard-stats above; nothing floats over the
|
|
984
|
+
dashboard content anymore. */
|
|
941
985
|
|
|
942
|
-
/* ── Casino Mode ──────────────────────────────────────────────── */
|
|
943
986
|
.casino-layer {
|
|
944
987
|
position: fixed;
|
|
945
988
|
inset: 0;
|
|
946
989
|
pointer-events: none;
|
|
947
990
|
z-index: 90;
|
|
948
991
|
opacity: 0;
|
|
949
|
-
|
|
992
|
+
visibility: hidden;
|
|
993
|
+
transition: opacity 0.25s;
|
|
994
|
+
}
|
|
995
|
+
body.casino-active .casino-layer {
|
|
996
|
+
opacity: 1;
|
|
997
|
+
visibility: visible;
|
|
950
998
|
}
|
|
951
|
-
body.casino-active .casino-layer { opacity: 1; }
|
|
952
999
|
|
|
953
|
-
/* Marquee:
|
|
954
|
-
|
|
955
|
-
.casino-
|
|
1000
|
+
/* Marquee: four solid neon strips at the viewport edges. Each strip
|
|
1001
|
+
pulses its own hue so the whole frame chases colours together. */
|
|
1002
|
+
.casino-edge {
|
|
956
1003
|
position: absolute;
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
/*
|
|
975
|
-
|
|
976
|
-
|
|
1004
|
+
background: #ff2d7a;
|
|
1005
|
+
box-shadow: 0 0 18px rgba(255, 45, 122, 0.7);
|
|
1006
|
+
animation: casino-edge-pulse 0.9s ease-in-out infinite;
|
|
1007
|
+
overflow: hidden;
|
|
1008
|
+
}
|
|
1009
|
+
.casino-edge.top { top: 0; left: 0; right: 0; height: 8px; }
|
|
1010
|
+
.casino-edge.bottom { bottom: 0; left: 0; right: 0; height: 8px; animation-delay: 0.45s; }
|
|
1011
|
+
.casino-edge.left { top: 0; bottom: 0; left: 0; width: 8px; animation-delay: 0.225s; }
|
|
1012
|
+
.casino-edge.right { top: 0; bottom: 0; right: 0; width: 8px; animation-delay: 0.675s; }
|
|
1013
|
+
@keyframes casino-edge-pulse {
|
|
1014
|
+
0% { background: #ff2d7a; box-shadow: 0 0 18px rgba(255, 45, 122, 0.7); }
|
|
1015
|
+
25% { background: #ffd400; box-shadow: 0 0 18px rgba(255, 220, 64, 0.7); }
|
|
1016
|
+
50% { background: #29d4ff; box-shadow: 0 0 18px rgba(41, 212, 255, 0.7); }
|
|
1017
|
+
75% { background: #b070ff; box-shadow: 0 0 18px rgba(176, 112, 255, 0.7); }
|
|
1018
|
+
100% { background: #ff2d7a; box-shadow: 0 0 18px rgba(255, 45, 122, 0.7); }
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/* Chase-light stripes — horizontal pattern on top/bottom, vertical
|
|
1022
|
+
pattern on left/right, all flowing in opposite directions so the
|
|
1023
|
+
marquee reads as a closed loop. */
|
|
1024
|
+
.casino-edge::after {
|
|
977
1025
|
content: '';
|
|
978
1026
|
position: absolute;
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1027
|
+
inset: 0;
|
|
1028
|
+
}
|
|
1029
|
+
.casino-edge.top::after,
|
|
1030
|
+
.casino-edge.bottom::after {
|
|
982
1031
|
background-image: repeating-linear-gradient(
|
|
983
1032
|
90deg,
|
|
984
|
-
|
|
985
|
-
transparent
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1033
|
+
rgba(255, 255, 255, 0.85) 0 8px,
|
|
1034
|
+
transparent 8px 24px
|
|
1035
|
+
);
|
|
1036
|
+
background-size: 24px 100%;
|
|
1037
|
+
animation: casino-chase-x 0.9s linear infinite;
|
|
1038
|
+
}
|
|
1039
|
+
.casino-edge.bottom::after { animation-direction: reverse; }
|
|
1040
|
+
.casino-edge.left::after,
|
|
1041
|
+
.casino-edge.right::after {
|
|
1042
|
+
background-image: repeating-linear-gradient(
|
|
1043
|
+
0deg,
|
|
1044
|
+
rgba(255, 255, 255, 0.85) 0 8px,
|
|
1045
|
+
transparent 8px 24px
|
|
990
1046
|
);
|
|
991
|
-
background-size:
|
|
992
|
-
|
|
1047
|
+
background-size: 100% 24px;
|
|
1048
|
+
animation: casino-chase-y 0.9s linear infinite;
|
|
993
1049
|
}
|
|
994
|
-
.casino-
|
|
995
|
-
|
|
996
|
-
@keyframes casino-chase-
|
|
997
|
-
@keyframes casino-chase-left { to { background-position: -60px 0; } }
|
|
1050
|
+
.casino-edge.right::after { animation-direction: reverse; }
|
|
1051
|
+
@keyframes casino-chase-x { to { background-position: 24px 0; } }
|
|
1052
|
+
@keyframes casino-chase-y { to { background-position: 0 24px; } }
|
|
998
1053
|
|
|
999
|
-
/*
|
|
1000
|
-
.casino-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
}
|
|
1054
|
+
/* Header reskin: rainbow text + animated icon + slot reels in-place. */
|
|
1055
|
+
body.casino-active .header-text {
|
|
1056
|
+
background: linear-gradient(
|
|
1057
|
+
90deg,
|
|
1058
|
+
#ff2d7a, #ffd400, #30ff9c, #29d4ff, #b070ff, #ff2d7a
|
|
1059
|
+
);
|
|
1060
|
+
background-size: 200% 100%;
|
|
1061
|
+
-webkit-background-clip: text;
|
|
1062
|
+
background-clip: text;
|
|
1063
|
+
-webkit-text-fill-color: transparent;
|
|
1064
|
+
color: transparent;
|
|
1065
|
+
animation: casino-rainbow-slide 3s linear infinite;
|
|
1066
|
+
/* Drop shadow with a coloured tint compensates for transparent fill. */
|
|
1067
|
+
filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.4));
|
|
1068
|
+
}
|
|
1069
|
+
@keyframes casino-rainbow-slide {
|
|
1070
|
+
to { background-position: 200% 0; }
|
|
1071
|
+
}
|
|
1072
|
+
body.casino-active .header-icon {
|
|
1073
|
+
display: inline-block;
|
|
1074
|
+
animation: casino-icon-spin 2.4s linear infinite;
|
|
1075
|
+
}
|
|
1076
|
+
@keyframes casino-icon-spin {
|
|
1077
|
+
0%, 100% { filter: hue-rotate(0deg) drop-shadow(0 0 4px rgba(255, 220, 64, 0.6)); }
|
|
1078
|
+
50% { filter: hue-rotate(180deg) drop-shadow(0 0 6px rgba(255, 45, 122, 0.8)); }
|
|
1025
1079
|
}
|
|
1026
1080
|
|
|
1027
|
-
/* Slot reels —
|
|
1028
|
-
|
|
1081
|
+
/* Slot reels — sit centred inside the header banner, hidden until
|
|
1082
|
+
casino mode is on. Sized small enough to fit in the existing
|
|
1083
|
+
header height without disturbing surrounding controls. */
|
|
1084
|
+
.casino-reels-header {
|
|
1029
1085
|
position: absolute;
|
|
1030
|
-
top:
|
|
1086
|
+
top: 50%;
|
|
1031
1087
|
left: 50%;
|
|
1032
|
-
transform:
|
|
1088
|
+
transform: translate(-50%, -50%);
|
|
1033
1089
|
display: flex;
|
|
1034
|
-
gap:
|
|
1035
|
-
padding: 8px
|
|
1036
|
-
background:
|
|
1037
|
-
border: 1px solid
|
|
1038
|
-
border-radius:
|
|
1039
|
-
box-shadow:
|
|
1040
|
-
0 10px 24px rgba(0, 0, 0, 0.45),
|
|
1041
|
-
0 0 18px rgba(255, 45, 122, 0.35);
|
|
1090
|
+
gap: 4px;
|
|
1091
|
+
padding: 4px 8px;
|
|
1092
|
+
background: rgba(20, 8, 30, 0.9);
|
|
1093
|
+
border: 1px solid #ff2d7a;
|
|
1094
|
+
border-radius: 8px;
|
|
1095
|
+
box-shadow: 0 0 14px rgba(255, 45, 122, 0.55);
|
|
1042
1096
|
opacity: 0;
|
|
1043
1097
|
visibility: hidden;
|
|
1044
|
-
|
|
1098
|
+
pointer-events: none;
|
|
1099
|
+
transition: opacity 0.25s;
|
|
1100
|
+
z-index: 1;
|
|
1045
1101
|
}
|
|
1046
|
-
.casino-
|
|
1102
|
+
body.casino-active .casino-reels-header {
|
|
1047
1103
|
opacity: 1;
|
|
1048
1104
|
visibility: visible;
|
|
1049
|
-
transform: translateX(-50%) translateY(0);
|
|
1050
1105
|
}
|
|
1051
1106
|
.casino-reel {
|
|
1052
|
-
width:
|
|
1053
|
-
height:
|
|
1107
|
+
width: 26px;
|
|
1108
|
+
height: 26px;
|
|
1054
1109
|
display: flex;
|
|
1055
1110
|
align-items: center;
|
|
1056
1111
|
justify-content: center;
|
|
1057
|
-
font-size:
|
|
1112
|
+
font-size: 16px;
|
|
1058
1113
|
line-height: 1;
|
|
1059
1114
|
background: #fff;
|
|
1060
|
-
border-radius:
|
|
1061
|
-
box-shadow:
|
|
1062
|
-
inset 0 -6px 10px rgba(0, 0, 0, 0.1),
|
|
1063
|
-
inset 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
1115
|
+
border-radius: 4px;
|
|
1116
|
+
box-shadow: inset 0 -3px 6px rgba(0, 0, 0, 0.12), inset 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
1064
1117
|
}
|
|
1065
|
-
.casino-reels.spinning .casino-reel {
|
|
1118
|
+
.casino-reels-header.spinning .casino-reel {
|
|
1066
1119
|
animation: casino-reel-blur 0.1s linear infinite;
|
|
1067
1120
|
}
|
|
1068
|
-
.casino-reels.spinning .casino-reel[data-reel="1"] { animation-delay: 0.03s; }
|
|
1069
|
-
.casino-reels.spinning .casino-reel[data-reel="2"] { animation-delay: 0.06s; }
|
|
1070
|
-
.casino-reels.spinning .casino-reel[data-reel="3"] { animation-delay: 0.09s; }
|
|
1071
|
-
.casino-reels.spinning .casino-reel[data-reel="4"] { animation-delay: 0.12s; }
|
|
1121
|
+
.casino-reels-header.spinning .casino-reel[data-reel="1"] { animation-delay: 0.03s; }
|
|
1122
|
+
.casino-reels-header.spinning .casino-reel[data-reel="2"] { animation-delay: 0.06s; }
|
|
1123
|
+
.casino-reels-header.spinning .casino-reel[data-reel="3"] { animation-delay: 0.09s; }
|
|
1124
|
+
.casino-reels-header.spinning .casino-reel[data-reel="4"] { animation-delay: 0.12s; }
|
|
1072
1125
|
@keyframes casino-reel-blur {
|
|
1073
|
-
0% { transform: translateY(-
|
|
1074
|
-
50% { transform: translateY(
|
|
1075
|
-
100% { transform: translateY(-
|
|
1126
|
+
0% { transform: translateY(-1.5px); filter: blur(0.6px); }
|
|
1127
|
+
50% { transform: translateY(1.5px); filter: blur(0.6px); }
|
|
1128
|
+
100% { transform: translateY(-1.5px); filter: blur(0.6px); }
|
|
1076
1129
|
}
|
|
1077
|
-
.casino-reels.win .casino-reel {
|
|
1130
|
+
.casino-reels-header.win .casino-reel {
|
|
1078
1131
|
animation: casino-reel-winflash 0.24s steps(2, end) infinite;
|
|
1079
1132
|
}
|
|
1080
1133
|
@keyframes casino-reel-winflash {
|
|
@@ -1083,22 +1136,22 @@ function getDashboardCss() {
|
|
|
1083
1136
|
}
|
|
1084
1137
|
.casino-reel-label {
|
|
1085
1138
|
position: absolute;
|
|
1086
|
-
top: calc(100% +
|
|
1139
|
+
top: calc(100% + 4px);
|
|
1087
1140
|
left: 50%;
|
|
1088
1141
|
transform: translateX(-50%);
|
|
1089
|
-
font-size:
|
|
1142
|
+
font-size: 10px;
|
|
1090
1143
|
font-weight: 800;
|
|
1091
1144
|
letter-spacing: 1.2px;
|
|
1092
1145
|
text-transform: uppercase;
|
|
1093
|
-
color:
|
|
1146
|
+
color: #ffd400;
|
|
1094
1147
|
white-space: nowrap;
|
|
1095
|
-
text-shadow: 0 0
|
|
1148
|
+
text-shadow: 0 0 8px currentColor;
|
|
1096
1149
|
opacity: 0;
|
|
1097
1150
|
transition: opacity 0.2s;
|
|
1098
1151
|
}
|
|
1099
|
-
.casino-reels.result .casino-reel-label { opacity: 1; }
|
|
1152
|
+
.casino-reels-header.result .casino-reel-label { opacity: 1; }
|
|
1100
1153
|
|
|
1101
|
-
/*
|
|
1154
|
+
/* Centred win / loss overlay banner — solid, opaque, hard to miss. */
|
|
1102
1155
|
.casino-overlay {
|
|
1103
1156
|
position: absolute;
|
|
1104
1157
|
top: 45%;
|
|
@@ -1110,17 +1163,18 @@ function getDashboardCss() {
|
|
|
1110
1163
|
letter-spacing: 2px;
|
|
1111
1164
|
text-transform: uppercase;
|
|
1112
1165
|
color: #fff200;
|
|
1113
|
-
background:
|
|
1114
|
-
border: 3px solid
|
|
1166
|
+
background: #b10096;
|
|
1167
|
+
border: 3px solid #ffd400;
|
|
1115
1168
|
border-radius: 14px;
|
|
1116
1169
|
box-shadow:
|
|
1117
1170
|
0 0 40px rgba(255, 45, 122, 0.7),
|
|
1118
1171
|
0 0 80px rgba(255, 220, 64, 0.4);
|
|
1119
|
-
text-shadow: 0 2px 0 rgba(0,0,0,0.3);
|
|
1172
|
+
text-shadow: 0 2px 0 rgba(0, 0, 0, 0.3);
|
|
1120
1173
|
opacity: 0;
|
|
1121
1174
|
visibility: hidden;
|
|
1122
1175
|
pointer-events: none;
|
|
1123
1176
|
transition: opacity 0.15s, transform 0.2s;
|
|
1177
|
+
z-index: 3;
|
|
1124
1178
|
}
|
|
1125
1179
|
.casino-overlay.active {
|
|
1126
1180
|
opacity: 1;
|
|
@@ -1132,76 +1186,30 @@ function getDashboardCss() {
|
|
|
1132
1186
|
0%, 100% { filter: brightness(1); }
|
|
1133
1187
|
50% { filter: brightness(1.35) saturate(1.3); }
|
|
1134
1188
|
}
|
|
1135
|
-
.casino-overlay.level-small { background:
|
|
1136
|
-
.casino-overlay.level-medium { background:
|
|
1137
|
-
.casino-overlay.level-large { background:
|
|
1138
|
-
.casino-overlay.level-huge { background:
|
|
1189
|
+
.casino-overlay.level-small { background: #238636; border-color: #3fb950; }
|
|
1190
|
+
.casino-overlay.level-medium { background: #ffd400; color: #2a1200; border-color: #ff9a00; }
|
|
1191
|
+
.casino-overlay.level-large { background: #ff9a00; color: #2a1200; border-color: #ffd400; }
|
|
1192
|
+
.casino-overlay.level-huge { background: #7a00ba; color: #fff; border-color: #bc8cff; }
|
|
1139
1193
|
.casino-overlay.level-jackpot {
|
|
1140
|
-
background:
|
|
1141
|
-
color: #
|
|
1194
|
+
background: #29d4ff;
|
|
1195
|
+
color: #04293a;
|
|
1196
|
+
border-color: #fff;
|
|
1142
1197
|
animation-duration: 0.12s;
|
|
1143
1198
|
}
|
|
1144
1199
|
.casino-overlay.level-mega {
|
|
1145
|
-
background:
|
|
1200
|
+
background: #b10000;
|
|
1201
|
+
color: #fff200;
|
|
1202
|
+
border-color: #ffd400;
|
|
1146
1203
|
animation-duration: 0.08s;
|
|
1147
1204
|
font-size: 40px;
|
|
1148
1205
|
}
|
|
1149
1206
|
.casino-overlay.loss {
|
|
1150
|
-
background:
|
|
1207
|
+
background: #b10000;
|
|
1151
1208
|
color: #fff;
|
|
1152
1209
|
font-size: 26px;
|
|
1210
|
+
border-color: #ff2d2d;
|
|
1153
1211
|
}
|
|
1154
1212
|
|
|
1155
|
-
/* Floating stats panel. Slides in from the right when casino is on. */
|
|
1156
|
-
.casino-stats-panel {
|
|
1157
|
-
position: absolute;
|
|
1158
|
-
right: 16px;
|
|
1159
|
-
bottom: 56px;
|
|
1160
|
-
width: 340px;
|
|
1161
|
-
padding: 12px 14px;
|
|
1162
|
-
background: linear-gradient(160deg, #1a0a24, #0f0616);
|
|
1163
|
-
border: 2px solid #ff2d7a;
|
|
1164
|
-
border-radius: 10px;
|
|
1165
|
-
box-shadow:
|
|
1166
|
-
0 0 20px rgba(255, 45, 122, 0.45),
|
|
1167
|
-
0 8px 28px rgba(0, 0, 0, 0.6);
|
|
1168
|
-
color: var(--text);
|
|
1169
|
-
font-size: 12px;
|
|
1170
|
-
transform: translateX(calc(100% + 32px));
|
|
1171
|
-
transition: transform 0.35s cubic-bezier(0.2, 0.9, 0.3, 1.1);
|
|
1172
|
-
pointer-events: auto;
|
|
1173
|
-
}
|
|
1174
|
-
body.casino-active .casino-stats-panel { transform: translateX(0); }
|
|
1175
|
-
.casino-stats-panel .cstats-title {
|
|
1176
|
-
font-weight: 800;
|
|
1177
|
-
letter-spacing: 1px;
|
|
1178
|
-
text-transform: uppercase;
|
|
1179
|
-
font-size: 11px;
|
|
1180
|
-
color: #ffd400;
|
|
1181
|
-
margin-bottom: 8px;
|
|
1182
|
-
text-align: center;
|
|
1183
|
-
text-shadow: 0 0 8px rgba(255, 220, 64, 0.6);
|
|
1184
|
-
}
|
|
1185
|
-
.casino-stats-panel .cstats-row {
|
|
1186
|
-
display: flex;
|
|
1187
|
-
justify-content: space-between;
|
|
1188
|
-
align-items: center;
|
|
1189
|
-
padding: 3px 0;
|
|
1190
|
-
border-bottom: 1px dashed rgba(255, 255, 255, 0.08);
|
|
1191
|
-
font-family: var(--font-mono);
|
|
1192
|
-
}
|
|
1193
|
-
.casino-stats-panel .cstats-row:last-child { border-bottom: none; }
|
|
1194
|
-
.casino-stats-panel .cstats-k {
|
|
1195
|
-
color: var(--text-dim);
|
|
1196
|
-
font-family: var(--font);
|
|
1197
|
-
font-size: 11px;
|
|
1198
|
-
}
|
|
1199
|
-
.casino-stats-panel .cstats-v { font-weight: 700; }
|
|
1200
|
-
.casino-stats-panel .cstats-v.pos { color: #3fb950; }
|
|
1201
|
-
.casino-stats-panel .cstats-v.neg { color: #f85149; }
|
|
1202
|
-
.casino-stats-panel .cstats-v.gold { color: #ffd400; }
|
|
1203
|
-
.casino-stats-panel .cstats-v.neon { color: #29d4ff; }
|
|
1204
|
-
|
|
1205
1213
|
@media (max-width: 900px) {
|
|
1206
1214
|
}
|
|
1207
1215
|
`;
|
|
@@ -12,10 +12,23 @@ function getDashboardHtml() {
|
|
|
12
12
|
return `
|
|
13
13
|
<div class="header">
|
|
14
14
|
<div class="header-left">
|
|
15
|
-
<span class="header-title"
|
|
15
|
+
<span class="header-title">
|
|
16
|
+
<span class="header-icon" id="header-icon">🏰</span>
|
|
17
|
+
<span class="header-text">Git Watchtower</span>
|
|
18
|
+
</span>
|
|
16
19
|
<span class="header-version" id="version"></span>
|
|
17
20
|
<span class="header-project" id="project-name">-</span>
|
|
18
21
|
</div>
|
|
22
|
+
<!-- Slot reels live in the header so they fit inside the blue banner
|
|
23
|
+
without dropping over the branch list. Hidden until casino-active. -->
|
|
24
|
+
<div class="casino-reels-header" id="casino-reels">
|
|
25
|
+
<div class="casino-reel" data-reel="0">🍒</div>
|
|
26
|
+
<div class="casino-reel" data-reel="1">🍋</div>
|
|
27
|
+
<div class="casino-reel" data-reel="2">🍊</div>
|
|
28
|
+
<div class="casino-reel" data-reel="3">🍇</div>
|
|
29
|
+
<div class="casino-reel" data-reel="4">🎰</div>
|
|
30
|
+
<div class="casino-reel-label" id="casino-reel-label"></div>
|
|
31
|
+
</div>
|
|
19
32
|
<div class="header-right">
|
|
20
33
|
<button class="notif-btn" id="notif-btn" title="Enable desktop notifications">notifications</button>
|
|
21
34
|
<span class="badge" id="status-badge">connecting</span>
|
|
@@ -39,10 +52,14 @@ function getDashboardHtml() {
|
|
|
39
52
|
|
|
40
53
|
<div class="side-panel" id="side-panel">
|
|
41
54
|
<div class="panel-header">Activity Log <button class="sidebar-toggle" id="sidebar-toggle" title="Toggle sidebar">▶</button></div>
|
|
42
|
-
<div class="session-stats-card" id="session-stats-card"></div>
|
|
43
55
|
<div class="activity-log" id="activity-log"></div>
|
|
44
56
|
</div>
|
|
45
57
|
|
|
58
|
+
<!-- Permanent stats row, sits above the keyboard-shortcut footer.
|
|
59
|
+
Hosts grounded session stats by default; the same element re-skins
|
|
60
|
+
to casino-style winnings when state.casinoModeEnabled flips on. -->
|
|
61
|
+
<div class="dashboard-stats" id="dashboard-stats"></div>
|
|
62
|
+
|
|
46
63
|
<div class="footer" id="footer">
|
|
47
64
|
<span><kbd>j</kbd><kbd>k</kbd> navigate</span>
|
|
48
65
|
<span><kbd>Enter</kbd> switch</span>
|
|
@@ -55,29 +72,22 @@ function getDashboardHtml() {
|
|
|
55
72
|
<span><kbd>S</kbd> stash</span>
|
|
56
73
|
<span><kbd>d</kbd> cleanup</span>
|
|
57
74
|
<span><kbd>h</kbd> history</span>
|
|
75
|
+
<span><kbd>c</kbd> casino</span>
|
|
58
76
|
<span><kbd>Esc</kbd> close</span>
|
|
59
|
-
<span class="stats-bar" id="stats-bar"></span>
|
|
60
77
|
</div>
|
|
61
78
|
</div>
|
|
62
79
|
|
|
63
|
-
<!-- Casino Mode overlay layer
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
80
|
+
<!-- Casino Mode overlay layer — viewport-edge effects only. The reels
|
|
81
|
+
and stats live inline in the dashboard now (header / bottom bar);
|
|
82
|
+
this layer is just the marquee strips and the centred win/loss
|
|
83
|
+
banners. Pointer-events stay off so it never blocks clicks. -->
|
|
67
84
|
<div class="casino-layer" id="casino-layer">
|
|
68
|
-
<div class="casino-
|
|
69
|
-
<div class="casino-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
<div class="casino-reel" data-reel="2">🎰</div>
|
|
73
|
-
<div class="casino-reel" data-reel="3">🎰</div>
|
|
74
|
-
<div class="casino-reel" data-reel="4">🎰</div>
|
|
75
|
-
<div class="casino-reel-label" id="casino-reel-label"></div>
|
|
76
|
-
</div>
|
|
85
|
+
<div class="casino-edge top"></div>
|
|
86
|
+
<div class="casino-edge bottom"></div>
|
|
87
|
+
<div class="casino-edge left"></div>
|
|
88
|
+
<div class="casino-edge right"></div>
|
|
77
89
|
<div class="casino-overlay" id="casino-win-overlay"></div>
|
|
78
90
|
<div class="casino-overlay loss" id="casino-loss-overlay"></div>
|
|
79
|
-
<div class="casino-badge" id="casino-badge">🎰 MAX ADDICTION 🎰</div>
|
|
80
|
-
<div class="casino-stats-panel" id="casino-stats-panel"></div>
|
|
81
91
|
</div>
|
|
82
92
|
|
|
83
93
|
<div class="flash" id="flash"></div>
|
package/src/server/web-ui/js.js
CHANGED
|
@@ -525,8 +525,10 @@ ${pureFnBlock}
|
|
|
525
525
|
if (casino.lossFlashTimer) { clearInterval(casino.lossFlashTimer); casino.lossFlashTimer = null; }
|
|
526
526
|
casino.winFlashFrames = 0;
|
|
527
527
|
casino.lossFlashFrames = 0;
|
|
528
|
+
// Reels stay in DOM but drop the dynamic state classes — the casino
|
|
529
|
+
// layer's opacity transition handles hiding them visually.
|
|
528
530
|
const reels = document.getElementById('casino-reels');
|
|
529
|
-
if (reels) reels.className = 'casino-reels';
|
|
531
|
+
if (reels) reels.className = 'casino-reels-header';
|
|
530
532
|
const win = document.getElementById('casino-win-overlay');
|
|
531
533
|
if (win) win.className = 'casino-overlay';
|
|
532
534
|
const loss = document.getElementById('casino-loss-overlay');
|
|
@@ -546,7 +548,7 @@ ${pureFnBlock}
|
|
|
546
548
|
}
|
|
547
549
|
const reels = document.getElementById('casino-reels');
|
|
548
550
|
if (!reels) return;
|
|
549
|
-
reels.className = 'casino-reels
|
|
551
|
+
reels.className = 'casino-reels-header spinning';
|
|
550
552
|
const label = document.getElementById('casino-reel-label');
|
|
551
553
|
if (label) label.textContent = '';
|
|
552
554
|
casino.reelSpinTimer = setInterval(() => {
|
|
@@ -569,7 +571,7 @@ ${pureFnBlock}
|
|
|
569
571
|
const isJackpot = winLevel.key === 'jackpot' || winLevel.key === 'mega';
|
|
570
572
|
const sym = isJackpot ? '7⃣' : CASINO_SYMBOLS[Math.floor(Math.random() * (CASINO_SYMBOLS.length - 1))];
|
|
571
573
|
for (let i = 0; i < 5; i++) casinoSetReelSymbol(i, sym);
|
|
572
|
-
reels.className = 'casino-reels
|
|
574
|
+
reels.className = 'casino-reels-header result win';
|
|
573
575
|
label.textContent = winLevel.label.replace(/^[^A-Z0-9]+/, '').trim() || 'WIN';
|
|
574
576
|
label.style.color = winLevel.color;
|
|
575
577
|
// Fire the centered banner.
|
|
@@ -577,19 +579,19 @@ ${pureFnBlock}
|
|
|
577
579
|
// Let the flashing linger a beat, then settle.
|
|
578
580
|
casino.reelResultClearTimer = setTimeout(() => {
|
|
579
581
|
casino.reelResultClearTimer = null;
|
|
580
|
-
reels.className = 'casino-reels';
|
|
582
|
+
reels.className = 'casino-reels-header';
|
|
581
583
|
}, isJackpot ? 4000 : 2400);
|
|
582
584
|
} else {
|
|
583
585
|
// No updates — show a random losing line and auto-fade.
|
|
584
586
|
for (let i = 0; i < 5; i++) {
|
|
585
587
|
casinoSetReelSymbol(i, CASINO_SYMBOLS[(casino.reelFrame + i * 3) % CASINO_SYMBOLS.length]);
|
|
586
588
|
}
|
|
587
|
-
reels.className = 'casino-reels
|
|
589
|
+
reels.className = 'casino-reels-header result';
|
|
588
590
|
label.textContent = '\u{1f634} NOTHING';
|
|
589
591
|
label.style.color = '#8b949e';
|
|
590
592
|
casino.reelResultClearTimer = setTimeout(() => {
|
|
591
593
|
casino.reelResultClearTimer = null;
|
|
592
|
-
reels.className = 'casino-reels';
|
|
594
|
+
reels.className = 'casino-reels-header';
|
|
593
595
|
}, 2000);
|
|
594
596
|
}
|
|
595
597
|
}
|
|
@@ -652,42 +654,19 @@ ${pureFnBlock}
|
|
|
652
654
|
return { hadUpdates, totalLines };
|
|
653
655
|
}
|
|
654
656
|
|
|
655
|
-
function renderCasinoStats() {
|
|
656
|
-
const panel = document.getElementById('casino-stats-panel');
|
|
657
|
-
if (!panel) return;
|
|
658
|
-
const cs = state && state.casinoStats;
|
|
659
|
-
if (!cs) { panel.innerHTML = ''; return; }
|
|
660
|
-
const netClass = cs.netWinnings >= 0 ? 'pos' : 'neg';
|
|
661
|
-
const netSign = cs.netWinnings >= 0 ? '+' : '';
|
|
662
|
-
let html = '<div class="cstats-title">\u{1f3b0} CASINO WINNINGS \u{1f3b0}</div>';
|
|
663
|
-
html += '<div class="cstats-row"><span class="cstats-k">\u{1f4dd} Line Changes</span><span class="cstats-v"><span class="pos">+' + (cs.totalLinesAdded || 0) + '</span> / <span class="neg">-' + (cs.totalLinesDeleted || 0) + '</span> = <span class="gold">$' + (cs.totalLines || 0) + '</span></span></div>';
|
|
664
|
-
html += '<div class="cstats-row"><span class="cstats-k">\u{1f4b8} Poll Cost</span><span class="cstats-v neg">$' + (cs.totalPolls || 0) + '</span></div>';
|
|
665
|
-
html += '<div class="cstats-row"><span class="cstats-k">\u{1f4b0} Net Earnings</span><span class="cstats-v ' + netClass + '">' + netSign + '$' + (cs.netWinnings || 0) + '</span></div>';
|
|
666
|
-
html += '<div class="cstats-row"><span class="cstats-k">\u{1f3b0} House Edge</span><span class="cstats-v neon">' + (cs.houseEdge || 0) + '%</span></div>';
|
|
667
|
-
html += '<div class="cstats-row"><span class="cstats-k">\u{1f60e} Vibes</span><span class="cstats-v">' + escHtml(cs.vibesQuality || '') + '</span></div>';
|
|
668
|
-
html += '<div class="cstats-row"><span class="cstats-k">\u{1f3b2} Luck</span><span class="cstats-v gold">' + (cs.luckMeter || 0) + '%</span></div>';
|
|
669
|
-
html += '<div class="cstats-row"><span class="cstats-k">\u{1f9e0} Dopamine Hits</span><span class="cstats-v pos">' + (cs.dopamineHits || 0) + '</span></div>';
|
|
670
|
-
if (cs.consecutivePolls > 1) {
|
|
671
|
-
html += '<div class="cstats-row"><span class="cstats-k">\u{1f525} Streak</span><span class="cstats-v gold">' + cs.consecutivePolls + 'x</span></div>';
|
|
672
|
-
}
|
|
673
|
-
html += '<div class="cstats-row"><span class="cstats-k">⏱ Session</span><span class="cstats-v">' + escHtml(cs.sessionDuration || '') + '</span></div>';
|
|
674
|
-
panel.innerHTML = html;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
657
|
// Apply/tear down casino mode based on state.casinoModeEnabled.
|
|
658
|
+
// Stats rendering is handled by renderDashboardStats — this function
|
|
659
|
+
// only owns the body class flip, header icon swap, and timer cleanup.
|
|
678
660
|
function reconcileCasinoMode() {
|
|
679
661
|
if (!state) return;
|
|
680
662
|
const enabled = !!state.casinoModeEnabled;
|
|
681
663
|
document.body.classList.toggle('casino-active', enabled);
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
}
|
|
687
|
-
return;
|
|
664
|
+
const icon = document.getElementById('header-icon');
|
|
665
|
+
if (icon) icon.innerHTML = enabled ? '\u{1f3b0}' : '\u{1f3f0}';
|
|
666
|
+
if (!enabled && ui.prevCasinoEnabled) {
|
|
667
|
+
casinoCleanup();
|
|
688
668
|
}
|
|
689
|
-
ui.prevCasinoEnabled =
|
|
690
|
-
renderCasinoStats();
|
|
669
|
+
ui.prevCasinoEnabled = enabled;
|
|
691
670
|
}
|
|
692
671
|
|
|
693
672
|
// Drive reel spin/stop from the polling status transition, and fire win
|
|
@@ -710,6 +689,10 @@ ${pureFnBlock}
|
|
|
710
689
|
function render() {
|
|
711
690
|
if (!state) return;
|
|
712
691
|
|
|
692
|
+
// Apply casino mode FIRST so a later renderer throwing (the SSE
|
|
693
|
+
// handler wraps render() in try/catch) can't swallow casino effects.
|
|
694
|
+
reconcileCasinoMode();
|
|
695
|
+
|
|
713
696
|
// Header — hide project name pill when tabs are showing it
|
|
714
697
|
const projectEl = document.getElementById('project-name');
|
|
715
698
|
const hasTabs = state.projects && state.projects.length > 1;
|
|
@@ -739,10 +722,8 @@ ${pureFnBlock}
|
|
|
739
722
|
|
|
740
723
|
renderBranches();
|
|
741
724
|
renderActivityLog();
|
|
742
|
-
|
|
743
|
-
renderSessionStatsCard();
|
|
725
|
+
renderDashboardStats();
|
|
744
726
|
renderPrefsBar();
|
|
745
|
-
reconcileCasinoMode();
|
|
746
727
|
|
|
747
728
|
// Auto-show update notification (once per session)
|
|
748
729
|
if (state.updateAvailable && !ui.updateNotificationShown && !anyModalOpen()) {
|
|
@@ -890,7 +871,7 @@ ${pureFnBlock}
|
|
|
890
871
|
let html = '';
|
|
891
872
|
for (let i = 0; i < log.length; i++) {
|
|
892
873
|
const entry = log[i];
|
|
893
|
-
|
|
874
|
+
let t = '';
|
|
894
875
|
if (entry.timestamp) {
|
|
895
876
|
const d = new Date(entry.timestamp);
|
|
896
877
|
t = isNaN(d.getTime()) ? '' : d.toLocaleTimeString();
|
|
@@ -1217,60 +1198,71 @@ ${pureFnBlock}
|
|
|
1217
1198
|
|
|
1218
1199
|
function hideUpdate() { updateModal.hide(); }
|
|
1219
1200
|
|
|
1220
|
-
// ──
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
const
|
|
1226
|
-
|
|
1227
|
-
if (state.
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
} else {
|
|
1234
|
-
staleBranches++;
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
let html = '';
|
|
1239
|
-
html += '<span class="stat-item"><span class="stat-label">Session:</span> <span class="stat-value">' + escHtml(s.sessionDuration || '0m') + '</span></span>';
|
|
1240
|
-
html += '<span class="stat-item"><span class="stat-label">Lines:</span> <span class="stat-value">+' + (s.linesAdded || 0) + '/-' + (s.linesDeleted || 0) + '</span></span>';
|
|
1241
|
-
html += '<span class="stat-item"><span class="stat-label">Polls:</span> <span class="stat-value">' + (s.totalPolls || 0) + '</span> <span class="stat-label">(' + (s.hitRate || 0) + '% hit)</span></span>';
|
|
1242
|
-
if (s.lastUpdate) {
|
|
1243
|
-
html += '<span class="stat-item"><span class="stat-label">Last update:</span> <span class="stat-value">' + escHtml(s.lastUpdate) + '</span></span>';
|
|
1201
|
+
// ── Dashboard Stats Bar ────────────────────────────────────────
|
|
1202
|
+
// Permanent row above the keyboard footer. Default: grounded session
|
|
1203
|
+
// metrics. When casino mode is on the same row re-skins to "casino
|
|
1204
|
+
// winnings" — same DOM, different content + .casino-mode class.
|
|
1205
|
+
function renderDashboardStats() {
|
|
1206
|
+
const bar = document.getElementById('dashboard-stats');
|
|
1207
|
+
if (!bar) return;
|
|
1208
|
+
if (state && state.casinoModeEnabled && state.casinoStats) {
|
|
1209
|
+
bar.className = 'dashboard-stats casino-mode';
|
|
1210
|
+
bar.innerHTML = renderCasinoStatsRow(state.casinoStats);
|
|
1211
|
+
} else {
|
|
1212
|
+
bar.className = 'dashboard-stats';
|
|
1213
|
+
bar.innerHTML = renderSessionStatsRow();
|
|
1244
1214
|
}
|
|
1245
|
-
html += '<span class="stat-item"><span class="stat-label">Active:</span> <span class="stat-value">' + activeBranches + '</span> <span class="stat-label">Stale:</span> <span class="stat-value">' + staleBranches + '</span></span>';
|
|
1246
|
-
bar.innerHTML = html;
|
|
1247
1215
|
}
|
|
1248
1216
|
|
|
1249
|
-
|
|
1250
|
-
// Lives at the top of the activity log. Real, grounded numbers the
|
|
1251
|
-
// dashboard always shows — not dependent on casino mode.
|
|
1252
|
-
function renderSessionStatsCard() {
|
|
1253
|
-
const card = document.getElementById('session-stats-card');
|
|
1254
|
-
if (!card) return;
|
|
1217
|
+
function renderSessionStatsRow() {
|
|
1255
1218
|
const s = state && state.sessionStats;
|
|
1256
|
-
if (!s)
|
|
1219
|
+
if (!s) return '';
|
|
1257
1220
|
const branches = (state && state.branches) || [];
|
|
1258
|
-
let
|
|
1259
|
-
let
|
|
1221
|
+
let active = 0;
|
|
1222
|
+
let stale = 0;
|
|
1260
1223
|
for (let i = 0; i < branches.length; i++) {
|
|
1261
1224
|
const b = branches[i];
|
|
1262
|
-
if (b.justUpdated || b.name === state.currentBranch)
|
|
1263
|
-
else
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1225
|
+
if (b.justUpdated || b.name === state.currentBranch) active++;
|
|
1226
|
+
else stale++;
|
|
1227
|
+
}
|
|
1228
|
+
const stat = (k, v) => '<span class="stat"><span class="stat-k">' + k + '</span><span class="stat-v">' + v + '</span></span>';
|
|
1229
|
+
// Left = identity / what session this is.
|
|
1230
|
+
let left = '<div class="stats-group">';
|
|
1231
|
+
left += '<span class="stats-title">\u{1f4ca} Session Stats</span>';
|
|
1232
|
+
left += stat('Duration', escHtml(s.sessionDuration || '0m'));
|
|
1233
|
+
left += stat('Branches', active + ' <span class="sep">active</span> <span class="sep">·</span> ' + stale + ' <span class="sep">stale</span>');
|
|
1234
|
+
left += '</div>';
|
|
1235
|
+
// Right = live activity readouts.
|
|
1236
|
+
let right = '<div class="stats-group">';
|
|
1237
|
+
right += stat('Lines', '<span class="added">+' + fmtCompact(s.linesAdded || 0) + '</span> <span class="sep">/</span> <span class="deleted">-' + fmtCompact(s.linesDeleted || 0) + '</span>');
|
|
1238
|
+
right += stat('Polls', (s.totalPolls || 0) + ' <span class="sep">·</span> <span class="accent">' + (s.hitRate || 0) + '%</span> hit');
|
|
1239
|
+
if (s.lastUpdate) right += stat('Last hit', escHtml(s.lastUpdate));
|
|
1240
|
+
right += '</div>';
|
|
1241
|
+
return left + right;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function renderCasinoStatsRow(cs) {
|
|
1245
|
+
const netClass = cs.netWinnings >= 0 ? 'pos' : 'neg';
|
|
1246
|
+
const netSign = cs.netWinnings >= 0 ? '+' : '';
|
|
1247
|
+
const stat = (k, v) => '<span class="stat"><span class="stat-k">' + k + '</span><span class="stat-v">' + v + '</span></span>';
|
|
1248
|
+
const dollar = '$'; // avoid bare $ in generated JS — see js.dom.test.js
|
|
1249
|
+
// Left = identity + the underlying churn that drove the winnings.
|
|
1250
|
+
let left = '<div class="stats-group">';
|
|
1251
|
+
left += '<span class="stats-title">\u{1f3b0} Casino Stats</span>';
|
|
1252
|
+
left += stat('Session', escHtml(cs.sessionDuration || ''));
|
|
1253
|
+
left += stat('\u{1f4dd} Lines', '<span class="pos">+' + (cs.totalLinesAdded || 0) + '</span> <span class="sep">/</span> <span class="neg">-' + (cs.totalLinesDeleted || 0) + '</span> <span class="sep">=</span> <span class="gold">' + dollar + (cs.totalLines || 0) + '</span>');
|
|
1254
|
+
left += '</div>';
|
|
1255
|
+
// Right = the gambling readouts (fast-changing, attention-grabbing).
|
|
1256
|
+
let right = '<div class="stats-group">';
|
|
1257
|
+
right += stat('\u{1f4b8} Cost', '<span class="neg">' + dollar + (cs.totalPolls || 0) + '</span>');
|
|
1258
|
+
right += stat('\u{1f4b0} Net', '<span class="' + netClass + '">' + netSign + dollar + (cs.netWinnings || 0) + '</span>');
|
|
1259
|
+
right += stat('\u{1f3b0} Edge', '<span class="neon">' + (cs.houseEdge || 0) + '%</span>');
|
|
1260
|
+
right += stat('\u{1f3b2} Luck', '<span class="gold">' + (cs.luckMeter || 0) + '%</span>');
|
|
1261
|
+
right += stat('\u{1f60e} Vibes', escHtml(cs.vibesQuality || ''));
|
|
1262
|
+
right += stat('\u{1f9e0} Hits', '<span class="pos">' + (cs.dopamineHits || 0) + '</span>');
|
|
1263
|
+
if (cs.consecutivePolls > 1) right += stat('\u{1f525} Streak', '<span class="gold">' + cs.consecutivePolls + 'x</span>');
|
|
1264
|
+
right += '</div>';
|
|
1265
|
+
return left + right;
|
|
1274
1266
|
}
|
|
1275
1267
|
|
|
1276
1268
|
// ── Error Toast with Stash Hint ────────────────────────────────
|