git-watchtower 2.0.3 → 2.1.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 +293 -0
- package/src/server/web-ui/html.js +21 -0
- package/src/server/web-ui/js.js +259 -0
- package/src/server/web.js +8 -0
package/package.json
CHANGED
package/src/server/web-ui/css.js
CHANGED
|
@@ -909,6 +909,299 @@ function getDashboardCss() {
|
|
|
909
909
|
.pref-btn:hover { background: var(--bg-surface-hover); color: var(--text-dim); border-color: var(--text-muted); }
|
|
910
910
|
.pref-btn.active { background: var(--accent-dim); color: #fff; border-color: var(--accent-dim); }
|
|
911
911
|
|
|
912
|
+
/* ── Session Stats Card (sidebar) ─────────────────────────────── */
|
|
913
|
+
.session-stats-card {
|
|
914
|
+
padding: 10px 16px 12px;
|
|
915
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
916
|
+
font-size: 11px;
|
|
917
|
+
color: var(--text-dim);
|
|
918
|
+
display: grid;
|
|
919
|
+
grid-template-columns: auto 1fr;
|
|
920
|
+
gap: 4px 10px;
|
|
921
|
+
background: var(--bg);
|
|
922
|
+
}
|
|
923
|
+
.session-stats-card .stat-k {
|
|
924
|
+
color: var(--text-muted);
|
|
925
|
+
text-transform: uppercase;
|
|
926
|
+
letter-spacing: 0.6px;
|
|
927
|
+
font-size: 10px;
|
|
928
|
+
font-weight: 600;
|
|
929
|
+
align-self: center;
|
|
930
|
+
}
|
|
931
|
+
.session-stats-card .stat-v {
|
|
932
|
+
color: var(--text);
|
|
933
|
+
font-family: var(--font-mono);
|
|
934
|
+
font-size: 12px;
|
|
935
|
+
text-align: right;
|
|
936
|
+
}
|
|
937
|
+
.session-stats-card .stat-v .sep { color: var(--text-muted); }
|
|
938
|
+
.session-stats-card .stat-v .added { color: var(--green); }
|
|
939
|
+
.session-stats-card .stat-v .deleted { color: var(--red); }
|
|
940
|
+
.session-stats-card .stat-v .accent { color: var(--accent); }
|
|
941
|
+
|
|
942
|
+
/* ── Casino Mode ──────────────────────────────────────────────── */
|
|
943
|
+
.casino-layer {
|
|
944
|
+
position: fixed;
|
|
945
|
+
inset: 0;
|
|
946
|
+
pointer-events: none;
|
|
947
|
+
z-index: 90;
|
|
948
|
+
opacity: 0;
|
|
949
|
+
transition: opacity 0.3s;
|
|
950
|
+
}
|
|
951
|
+
body.casino-active .casino-layer { opacity: 1; }
|
|
952
|
+
|
|
953
|
+
/* Marquee: neon border that cycles hues around the viewport. Pure CSS
|
|
954
|
+
so we don't burn a JS timer for something a single keyframe can do. */
|
|
955
|
+
.casino-marquee {
|
|
956
|
+
position: absolute;
|
|
957
|
+
inset: 0;
|
|
958
|
+
border: 4px solid transparent;
|
|
959
|
+
border-radius: 0;
|
|
960
|
+
box-shadow:
|
|
961
|
+
inset 0 0 24px rgba(255, 64, 180, 0.45),
|
|
962
|
+
inset 0 0 60px rgba(255, 220, 64, 0.2);
|
|
963
|
+
background:
|
|
964
|
+
linear-gradient(var(--bg), var(--bg)) padding-box,
|
|
965
|
+
conic-gradient(
|
|
966
|
+
from 0deg,
|
|
967
|
+
#ff2d7a, #ffd400, #30ff9c, #29d4ff, #b070ff, #ff2d7a
|
|
968
|
+
) border-box;
|
|
969
|
+
animation: casino-marquee-spin 6s linear infinite;
|
|
970
|
+
}
|
|
971
|
+
@keyframes casino-marquee-spin {
|
|
972
|
+
to { filter: hue-rotate(360deg); }
|
|
973
|
+
}
|
|
974
|
+
/* Running chase lights along the top and bottom edges */
|
|
975
|
+
.casino-marquee::before,
|
|
976
|
+
.casino-marquee::after {
|
|
977
|
+
content: '';
|
|
978
|
+
position: absolute;
|
|
979
|
+
left: 0;
|
|
980
|
+
right: 0;
|
|
981
|
+
height: 6px;
|
|
982
|
+
background-image: repeating-linear-gradient(
|
|
983
|
+
90deg,
|
|
984
|
+
#ffd400 0 10px,
|
|
985
|
+
transparent 10px 20px,
|
|
986
|
+
#ff2d7a 20px 30px,
|
|
987
|
+
transparent 30px 40px,
|
|
988
|
+
#29d4ff 40px 50px,
|
|
989
|
+
transparent 50px 60px
|
|
990
|
+
);
|
|
991
|
+
background-size: 60px 6px;
|
|
992
|
+
opacity: 0.85;
|
|
993
|
+
}
|
|
994
|
+
.casino-marquee::before { top: 0; animation: casino-chase-right 1.2s linear infinite; }
|
|
995
|
+
.casino-marquee::after { bottom: 0; animation: casino-chase-left 1.2s linear infinite; }
|
|
996
|
+
@keyframes casino-chase-right { to { background-position: 60px 0; } }
|
|
997
|
+
@keyframes casino-chase-left { to { background-position: -60px 0; } }
|
|
998
|
+
|
|
999
|
+
/* Flashing "MAX ADDICTION" header badge */
|
|
1000
|
+
.casino-badge {
|
|
1001
|
+
position: absolute;
|
|
1002
|
+
top: 10px;
|
|
1003
|
+
right: 140px;
|
|
1004
|
+
padding: 3px 10px;
|
|
1005
|
+
border-radius: 10px;
|
|
1006
|
+
font-size: 10px;
|
|
1007
|
+
font-weight: 800;
|
|
1008
|
+
letter-spacing: 0.8px;
|
|
1009
|
+
text-transform: uppercase;
|
|
1010
|
+
color: #fff200;
|
|
1011
|
+
background: linear-gradient(90deg, #b10096, #ff2d7a);
|
|
1012
|
+
border: 1px solid rgba(255, 255, 255, 0.25);
|
|
1013
|
+
box-shadow: 0 0 14px rgba(255, 45, 122, 0.6);
|
|
1014
|
+
animation: casino-badge-flash 0.6s steps(2, end) infinite;
|
|
1015
|
+
}
|
|
1016
|
+
@keyframes casino-badge-flash {
|
|
1017
|
+
0%, 100% {
|
|
1018
|
+
background: linear-gradient(90deg, #b10096, #ff2d7a);
|
|
1019
|
+
color: #fff200;
|
|
1020
|
+
}
|
|
1021
|
+
50% {
|
|
1022
|
+
background: linear-gradient(90deg, #ffd400, #ff9a00);
|
|
1023
|
+
color: #b10096;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/* Slot reels — sits just under the header */
|
|
1028
|
+
.casino-reels {
|
|
1029
|
+
position: absolute;
|
|
1030
|
+
top: 57px;
|
|
1031
|
+
left: 50%;
|
|
1032
|
+
transform: translateX(-50%) translateY(-20px);
|
|
1033
|
+
display: flex;
|
|
1034
|
+
gap: 6px;
|
|
1035
|
+
padding: 8px 14px;
|
|
1036
|
+
background: linear-gradient(180deg, #1a1022, #120818);
|
|
1037
|
+
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
1038
|
+
border-radius: 10px;
|
|
1039
|
+
box-shadow:
|
|
1040
|
+
0 10px 24px rgba(0, 0, 0, 0.45),
|
|
1041
|
+
0 0 18px rgba(255, 45, 122, 0.35);
|
|
1042
|
+
opacity: 0;
|
|
1043
|
+
visibility: hidden;
|
|
1044
|
+
transition: opacity 0.2s, transform 0.25s;
|
|
1045
|
+
}
|
|
1046
|
+
.casino-reels.active {
|
|
1047
|
+
opacity: 1;
|
|
1048
|
+
visibility: visible;
|
|
1049
|
+
transform: translateX(-50%) translateY(0);
|
|
1050
|
+
}
|
|
1051
|
+
.casino-reel {
|
|
1052
|
+
width: 40px;
|
|
1053
|
+
height: 40px;
|
|
1054
|
+
display: flex;
|
|
1055
|
+
align-items: center;
|
|
1056
|
+
justify-content: center;
|
|
1057
|
+
font-size: 22px;
|
|
1058
|
+
line-height: 1;
|
|
1059
|
+
background: #fff;
|
|
1060
|
+
border-radius: 6px;
|
|
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);
|
|
1064
|
+
}
|
|
1065
|
+
.casino-reels.spinning .casino-reel {
|
|
1066
|
+
animation: casino-reel-blur 0.1s linear infinite;
|
|
1067
|
+
}
|
|
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; }
|
|
1072
|
+
@keyframes casino-reel-blur {
|
|
1073
|
+
0% { transform: translateY(-2px); filter: blur(0.6px); }
|
|
1074
|
+
50% { transform: translateY(2px); filter: blur(0.6px); }
|
|
1075
|
+
100% { transform: translateY(-2px); filter: blur(0.6px); }
|
|
1076
|
+
}
|
|
1077
|
+
.casino-reels.win .casino-reel {
|
|
1078
|
+
animation: casino-reel-winflash 0.24s steps(2, end) infinite;
|
|
1079
|
+
}
|
|
1080
|
+
@keyframes casino-reel-winflash {
|
|
1081
|
+
0%, 100% { background: #fff; }
|
|
1082
|
+
50% { background: #ffd400; }
|
|
1083
|
+
}
|
|
1084
|
+
.casino-reel-label {
|
|
1085
|
+
position: absolute;
|
|
1086
|
+
top: calc(100% + 6px);
|
|
1087
|
+
left: 50%;
|
|
1088
|
+
transform: translateX(-50%);
|
|
1089
|
+
font-size: 11px;
|
|
1090
|
+
font-weight: 800;
|
|
1091
|
+
letter-spacing: 1.2px;
|
|
1092
|
+
text-transform: uppercase;
|
|
1093
|
+
color: var(--text-dim);
|
|
1094
|
+
white-space: nowrap;
|
|
1095
|
+
text-shadow: 0 0 10px currentColor;
|
|
1096
|
+
opacity: 0;
|
|
1097
|
+
transition: opacity 0.2s;
|
|
1098
|
+
}
|
|
1099
|
+
.casino-reels.result .casino-reel-label { opacity: 1; }
|
|
1100
|
+
|
|
1101
|
+
/* Centered win / loss overlay flashing banner */
|
|
1102
|
+
.casino-overlay {
|
|
1103
|
+
position: absolute;
|
|
1104
|
+
top: 45%;
|
|
1105
|
+
left: 50%;
|
|
1106
|
+
transform: translate(-50%, -50%) scale(0.85);
|
|
1107
|
+
padding: 18px 48px;
|
|
1108
|
+
font-size: 32px;
|
|
1109
|
+
font-weight: 900;
|
|
1110
|
+
letter-spacing: 2px;
|
|
1111
|
+
text-transform: uppercase;
|
|
1112
|
+
color: #fff200;
|
|
1113
|
+
background: linear-gradient(135deg, #b10096, #ff2d7a);
|
|
1114
|
+
border: 3px solid rgba(255, 255, 255, 0.35);
|
|
1115
|
+
border-radius: 14px;
|
|
1116
|
+
box-shadow:
|
|
1117
|
+
0 0 40px rgba(255, 45, 122, 0.7),
|
|
1118
|
+
0 0 80px rgba(255, 220, 64, 0.4);
|
|
1119
|
+
text-shadow: 0 2px 0 rgba(0,0,0,0.3);
|
|
1120
|
+
opacity: 0;
|
|
1121
|
+
visibility: hidden;
|
|
1122
|
+
pointer-events: none;
|
|
1123
|
+
transition: opacity 0.15s, transform 0.2s;
|
|
1124
|
+
}
|
|
1125
|
+
.casino-overlay.active {
|
|
1126
|
+
opacity: 1;
|
|
1127
|
+
visibility: visible;
|
|
1128
|
+
transform: translate(-50%, -50%) scale(1);
|
|
1129
|
+
animation: casino-overlay-flash 0.2s steps(2, end) infinite;
|
|
1130
|
+
}
|
|
1131
|
+
@keyframes casino-overlay-flash {
|
|
1132
|
+
0%, 100% { filter: brightness(1); }
|
|
1133
|
+
50% { filter: brightness(1.35) saturate(1.3); }
|
|
1134
|
+
}
|
|
1135
|
+
.casino-overlay.level-small { background: linear-gradient(135deg, #116b2a, #3fb950); }
|
|
1136
|
+
.casino-overlay.level-medium { background: linear-gradient(135deg, #7a5600, #ffd400); color: #2a1200; }
|
|
1137
|
+
.casino-overlay.level-large { background: linear-gradient(135deg, #ba6a00, #ff9a00); color: #2a1200; }
|
|
1138
|
+
.casino-overlay.level-huge { background: linear-gradient(135deg, #7a00ba, #bc8cff); }
|
|
1139
|
+
.casino-overlay.level-jackpot {
|
|
1140
|
+
background: linear-gradient(135deg, #007a82, #29d4ff);
|
|
1141
|
+
color: #fff;
|
|
1142
|
+
animation-duration: 0.12s;
|
|
1143
|
+
}
|
|
1144
|
+
.casino-overlay.level-mega {
|
|
1145
|
+
background: linear-gradient(135deg, #b10000, #ff2d2d);
|
|
1146
|
+
animation-duration: 0.08s;
|
|
1147
|
+
font-size: 40px;
|
|
1148
|
+
}
|
|
1149
|
+
.casino-overlay.loss {
|
|
1150
|
+
background: linear-gradient(135deg, #2a0000, #b10000);
|
|
1151
|
+
color: #fff;
|
|
1152
|
+
font-size: 26px;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
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
|
+
|
|
912
1205
|
@media (max-width: 900px) {
|
|
913
1206
|
}
|
|
914
1207
|
`;
|
|
@@ -39,6 +39,7 @@ function getDashboardHtml() {
|
|
|
39
39
|
|
|
40
40
|
<div class="side-panel" id="side-panel">
|
|
41
41
|
<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>
|
|
42
43
|
<div class="activity-log" id="activity-log"></div>
|
|
43
44
|
</div>
|
|
44
45
|
|
|
@@ -59,6 +60,26 @@ function getDashboardHtml() {
|
|
|
59
60
|
</div>
|
|
60
61
|
</div>
|
|
61
62
|
|
|
63
|
+
<!-- Casino Mode overlay layer. Everything inside is hidden by default and
|
|
64
|
+
only becomes visible when body has the .casino-active class (driven by
|
|
65
|
+
state.casinoModeEnabled). Pointer-events are off so it never blocks
|
|
66
|
+
clicks on the real dashboard underneath. -->
|
|
67
|
+
<div class="casino-layer" id="casino-layer">
|
|
68
|
+
<div class="casino-marquee" id="casino-marquee"></div>
|
|
69
|
+
<div class="casino-reels" id="casino-reels">
|
|
70
|
+
<div class="casino-reel" data-reel="0">🎰</div>
|
|
71
|
+
<div class="casino-reel" data-reel="1">🎰</div>
|
|
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>
|
|
77
|
+
<div class="casino-overlay" id="casino-win-overlay"></div>
|
|
78
|
+
<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
|
+
</div>
|
|
82
|
+
|
|
62
83
|
<div class="flash" id="flash"></div>
|
|
63
84
|
<div class="confirm-overlay" id="confirm-overlay">
|
|
64
85
|
<div class="confirm-box" id="confirm-box"></div>
|
package/src/server/web-ui/js.js
CHANGED
|
@@ -40,6 +40,8 @@ function getDashboardJs() {
|
|
|
40
40
|
// from the server-pushed 'state' above.
|
|
41
41
|
const ui = {
|
|
42
42
|
prevBranches: null,
|
|
43
|
+
prevPollingStatus: 'idle',
|
|
44
|
+
prevCasinoEnabled: false,
|
|
43
45
|
selectedIndex: 0,
|
|
44
46
|
searchMode: false,
|
|
45
47
|
searchQuery: '',
|
|
@@ -60,6 +62,35 @@ function getDashboardJs() {
|
|
|
60
62
|
remoteTabPollTimer: null,
|
|
61
63
|
};
|
|
62
64
|
|
|
65
|
+
// Casino mode client state. Kept separate so it can be fully torn down
|
|
66
|
+
// on disable without touching unrelated UI state.
|
|
67
|
+
const casino = {
|
|
68
|
+
reelSpinTimer: null,
|
|
69
|
+
reelFrame: 0,
|
|
70
|
+
reelResultClearTimer: null,
|
|
71
|
+
winFlashTimer: null,
|
|
72
|
+
winFlashFrames: 0,
|
|
73
|
+
lossFlashTimer: null,
|
|
74
|
+
lossFlashFrames: 0,
|
|
75
|
+
lastTotalPollsWithUpdates: null,
|
|
76
|
+
};
|
|
77
|
+
const CASINO_SYMBOLS = ['\u{1f352}','\u{1f34b}','\u{1f34a}','\u{1f347}','\u{1f514}','\u{1f48e}','7⃣','\u{1f3b0}'];
|
|
78
|
+
const CASINO_WIN_LEVELS = [
|
|
79
|
+
{ key: 'small', min: 1, max: 49, label: '✨ WIN', color: '#3fb950' },
|
|
80
|
+
{ key: 'medium', min: 50, max: 199, label: '\u{1f389} NICE WIN!', color: '#ffd400' },
|
|
81
|
+
{ key: 'large', min: 200, max: 499, label: '\u{1f525} BIG WIN!', color: '#ff9a00' },
|
|
82
|
+
{ key: 'huge', min: 500, max: 999, label: '\u{1f4a5} HUGE WIN!', color: '#bc8cff' },
|
|
83
|
+
{ key: 'jackpot', min: 1000, max: 4999, label: '\u{1f4b0} JACKPOT! \u{1f4b0}', color: '#29d4ff' },
|
|
84
|
+
{ key: 'mega', min: 5000, max: Infinity, label: '\u{1f3b0} MEGA JACKPOT!!! \u{1f3b0}', color: '#ff2d2d' },
|
|
85
|
+
];
|
|
86
|
+
function getCasinoWinLevel(totalLines) {
|
|
87
|
+
for (let i = 0; i < CASINO_WIN_LEVELS.length; i++) {
|
|
88
|
+
const lvl = CASINO_WIN_LEVELS[i];
|
|
89
|
+
if (totalLines >= lvl.min && totalLines <= lvl.max) return lvl;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
63
94
|
// ── Persistent Preferences (localStorage) ─────────────────────
|
|
64
95
|
const PREFS_KEY = 'git-watchtower-prefs';
|
|
65
96
|
function loadPrefs() {
|
|
@@ -218,7 +249,12 @@ function getDashboardJs() {
|
|
|
218
249
|
diffBranchesForNotifications(state.branches, newState.branches || []);
|
|
219
250
|
}
|
|
220
251
|
ui.prevBranches = state ? state.branches : null;
|
|
252
|
+
// Casino effects key off the edge between two SSE frames — grab
|
|
253
|
+
// the transition BEFORE the new state replaces the old.
|
|
254
|
+
const prevBranchesForCasino = state ? state.branches : null;
|
|
221
255
|
state = newState;
|
|
256
|
+
onStateTransition(newState, prevBranchesForCasino);
|
|
257
|
+
ui.prevPollingStatus = newState.pollingStatus;
|
|
222
258
|
} else {
|
|
223
259
|
if (state) {
|
|
224
260
|
state.projects = newState.projects;
|
|
@@ -476,6 +512,200 @@ ${pureFnBlock}
|
|
|
476
512
|
});
|
|
477
513
|
};
|
|
478
514
|
|
|
515
|
+
// ── Casino Mode ────────────────────────────────────────────────
|
|
516
|
+
// The terminal drives reel/win animations from server-side timers; in
|
|
517
|
+
// the browser we drive them off state transitions instead. That lets us
|
|
518
|
+
// avoid pushing a frame-by-frame stream over SSE and keeps effects local
|
|
519
|
+
// to each connected client.
|
|
520
|
+
|
|
521
|
+
function casinoCleanup() {
|
|
522
|
+
if (casino.reelSpinTimer) { clearInterval(casino.reelSpinTimer); casino.reelSpinTimer = null; }
|
|
523
|
+
if (casino.reelResultClearTimer) { clearTimeout(casino.reelResultClearTimer); casino.reelResultClearTimer = null; }
|
|
524
|
+
if (casino.winFlashTimer) { clearInterval(casino.winFlashTimer); casino.winFlashTimer = null; }
|
|
525
|
+
if (casino.lossFlashTimer) { clearInterval(casino.lossFlashTimer); casino.lossFlashTimer = null; }
|
|
526
|
+
casino.winFlashFrames = 0;
|
|
527
|
+
casino.lossFlashFrames = 0;
|
|
528
|
+
const reels = document.getElementById('casino-reels');
|
|
529
|
+
if (reels) reels.className = 'casino-reels';
|
|
530
|
+
const win = document.getElementById('casino-win-overlay');
|
|
531
|
+
if (win) win.className = 'casino-overlay';
|
|
532
|
+
const loss = document.getElementById('casino-loss-overlay');
|
|
533
|
+
if (loss) loss.className = 'casino-overlay loss';
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function casinoSetReelSymbol(idx, emoji) {
|
|
537
|
+
const el = document.querySelector('.casino-reel[data-reel="' + idx + '"]');
|
|
538
|
+
if (el) el.textContent = emoji;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function casinoStartSpinning() {
|
|
542
|
+
if (casino.reelSpinTimer) return;
|
|
543
|
+
if (casino.reelResultClearTimer) {
|
|
544
|
+
clearTimeout(casino.reelResultClearTimer);
|
|
545
|
+
casino.reelResultClearTimer = null;
|
|
546
|
+
}
|
|
547
|
+
const reels = document.getElementById('casino-reels');
|
|
548
|
+
if (!reels) return;
|
|
549
|
+
reels.className = 'casino-reels active spinning';
|
|
550
|
+
const label = document.getElementById('casino-reel-label');
|
|
551
|
+
if (label) label.textContent = '';
|
|
552
|
+
casino.reelSpinTimer = setInterval(() => {
|
|
553
|
+
casino.reelFrame++;
|
|
554
|
+
for (let i = 0; i < 5; i++) {
|
|
555
|
+
const idx = (casino.reelFrame + i * 3) % CASINO_SYMBOLS.length;
|
|
556
|
+
casinoSetReelSymbol(i, CASINO_SYMBOLS[idx]);
|
|
557
|
+
}
|
|
558
|
+
}, 100);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function casinoStopSpinning(hadUpdates, totalLines) {
|
|
562
|
+
if (casino.reelSpinTimer) { clearInterval(casino.reelSpinTimer); casino.reelSpinTimer = null; }
|
|
563
|
+
const reels = document.getElementById('casino-reels');
|
|
564
|
+
const label = document.getElementById('casino-reel-label');
|
|
565
|
+
if (!reels || !label) return;
|
|
566
|
+
const winLevel = hadUpdates ? getCasinoWinLevel(totalLines || 1) : null;
|
|
567
|
+
|
|
568
|
+
if (hadUpdates && winLevel) {
|
|
569
|
+
const isJackpot = winLevel.key === 'jackpot' || winLevel.key === 'mega';
|
|
570
|
+
const sym = isJackpot ? '7⃣' : CASINO_SYMBOLS[Math.floor(Math.random() * (CASINO_SYMBOLS.length - 1))];
|
|
571
|
+
for (let i = 0; i < 5; i++) casinoSetReelSymbol(i, sym);
|
|
572
|
+
reels.className = 'casino-reels active result win';
|
|
573
|
+
label.textContent = winLevel.label.replace(/^[^A-Z0-9]+/, '').trim() || 'WIN';
|
|
574
|
+
label.style.color = winLevel.color;
|
|
575
|
+
// Fire the centered banner.
|
|
576
|
+
casinoTriggerWin(winLevel);
|
|
577
|
+
// Let the flashing linger a beat, then settle.
|
|
578
|
+
casino.reelResultClearTimer = setTimeout(() => {
|
|
579
|
+
casino.reelResultClearTimer = null;
|
|
580
|
+
reels.className = 'casino-reels';
|
|
581
|
+
}, isJackpot ? 4000 : 2400);
|
|
582
|
+
} else {
|
|
583
|
+
// No updates — show a random losing line and auto-fade.
|
|
584
|
+
for (let i = 0; i < 5; i++) {
|
|
585
|
+
casinoSetReelSymbol(i, CASINO_SYMBOLS[(casino.reelFrame + i * 3) % CASINO_SYMBOLS.length]);
|
|
586
|
+
}
|
|
587
|
+
reels.className = 'casino-reels active result';
|
|
588
|
+
label.textContent = '\u{1f634} NOTHING';
|
|
589
|
+
label.style.color = '#8b949e';
|
|
590
|
+
casino.reelResultClearTimer = setTimeout(() => {
|
|
591
|
+
casino.reelResultClearTimer = null;
|
|
592
|
+
reels.className = 'casino-reels';
|
|
593
|
+
}, 2000);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function casinoTriggerWin(winLevel) {
|
|
598
|
+
const overlay = document.getElementById('casino-win-overlay');
|
|
599
|
+
if (!overlay) return;
|
|
600
|
+
overlay.textContent = winLevel.label;
|
|
601
|
+
overlay.className = 'casino-overlay active level-' + winLevel.key;
|
|
602
|
+
if (casino.winFlashTimer) clearInterval(casino.winFlashTimer);
|
|
603
|
+
casino.winFlashFrames = 0;
|
|
604
|
+
const maxFrames = (winLevel.key === 'jackpot' || winLevel.key === 'mega') ? 30 : 16;
|
|
605
|
+
casino.winFlashTimer = setInterval(() => {
|
|
606
|
+
casino.winFlashFrames++;
|
|
607
|
+
if (casino.winFlashFrames >= maxFrames) {
|
|
608
|
+
clearInterval(casino.winFlashTimer);
|
|
609
|
+
casino.winFlashTimer = null;
|
|
610
|
+
overlay.className = 'casino-overlay';
|
|
611
|
+
}
|
|
612
|
+
}, 120);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function casinoTriggerLoss(message) {
|
|
616
|
+
const overlay = document.getElementById('casino-loss-overlay');
|
|
617
|
+
if (!overlay) return;
|
|
618
|
+
overlay.textContent = '\u{1f480} ' + (message || 'BUST!') + ' \u{1f480}';
|
|
619
|
+
overlay.className = 'casino-overlay loss active';
|
|
620
|
+
if (casino.lossFlashTimer) clearInterval(casino.lossFlashTimer);
|
|
621
|
+
casino.lossFlashFrames = 0;
|
|
622
|
+
casino.lossFlashTimer = setInterval(() => {
|
|
623
|
+
casino.lossFlashFrames++;
|
|
624
|
+
if (casino.lossFlashFrames >= 12) {
|
|
625
|
+
clearInterval(casino.lossFlashTimer);
|
|
626
|
+
casino.lossFlashTimer = null;
|
|
627
|
+
overlay.className = 'casino-overlay loss';
|
|
628
|
+
}
|
|
629
|
+
}, 130);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Sum up the line churn from branches that just transitioned to
|
|
633
|
+
// justUpdated. Mirrors how the terminal decides a poll was a "win".
|
|
634
|
+
function casinoMeasureUpdate(prevBranches, newBranches, newAheadBehind) {
|
|
635
|
+
if (!newBranches) return { hadUpdates: false, totalLines: 0 };
|
|
636
|
+
const prevMap = {};
|
|
637
|
+
if (prevBranches) {
|
|
638
|
+
for (let i = 0; i < prevBranches.length; i++) prevMap[prevBranches[i].name] = prevBranches[i];
|
|
639
|
+
}
|
|
640
|
+
let hadUpdates = false;
|
|
641
|
+
let totalLines = 0;
|
|
642
|
+
for (let i = 0; i < newBranches.length; i++) {
|
|
643
|
+
const nb = newBranches[i];
|
|
644
|
+
const ob = prevMap[nb.name];
|
|
645
|
+
const transitioned = nb.justUpdated && (!ob || !ob.justUpdated);
|
|
646
|
+
if (transitioned) {
|
|
647
|
+
hadUpdates = true;
|
|
648
|
+
const ab = newAheadBehind ? newAheadBehind[nb.name] : null;
|
|
649
|
+
if (ab) totalLines += (ab.linesAdded || 0) + (ab.linesDeleted || 0);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return { hadUpdates, totalLines };
|
|
653
|
+
}
|
|
654
|
+
|
|
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
|
+
// Apply/tear down casino mode based on state.casinoModeEnabled.
|
|
678
|
+
function reconcileCasinoMode() {
|
|
679
|
+
if (!state) return;
|
|
680
|
+
const enabled = !!state.casinoModeEnabled;
|
|
681
|
+
document.body.classList.toggle('casino-active', enabled);
|
|
682
|
+
if (!enabled) {
|
|
683
|
+
if (ui.prevCasinoEnabled) {
|
|
684
|
+
casinoCleanup();
|
|
685
|
+
ui.prevCasinoEnabled = false;
|
|
686
|
+
}
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
ui.prevCasinoEnabled = true;
|
|
690
|
+
renderCasinoStats();
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Drive reel spin/stop from the polling status transition, and fire win
|
|
694
|
+
// effects when new updates land. Called from the SSE state handler so
|
|
695
|
+
// we see each edge exactly once.
|
|
696
|
+
function onStateTransition(newState, prevBranches) {
|
|
697
|
+
if (!newState.casinoModeEnabled) return;
|
|
698
|
+
const prevStatus = ui.prevPollingStatus;
|
|
699
|
+
const newStatus = newState.pollingStatus;
|
|
700
|
+
if (newStatus === 'fetching' && prevStatus !== 'fetching') {
|
|
701
|
+
casinoStartSpinning();
|
|
702
|
+
} else if (prevStatus === 'fetching' && newStatus !== 'fetching') {
|
|
703
|
+
const { hadUpdates, totalLines } = casinoMeasureUpdate(prevBranches, newState.branches, newState.aheadBehindCache);
|
|
704
|
+
casinoStopSpinning(hadUpdates, totalLines);
|
|
705
|
+
}
|
|
706
|
+
if (newState.hasMergeConflict) casinoTriggerLoss('MERGE CONFLICT!');
|
|
707
|
+
}
|
|
708
|
+
|
|
479
709
|
// ── Render ─────────────────────────────────────────────────────
|
|
480
710
|
function render() {
|
|
481
711
|
if (!state) return;
|
|
@@ -510,7 +740,9 @@ ${pureFnBlock}
|
|
|
510
740
|
renderBranches();
|
|
511
741
|
renderActivityLog();
|
|
512
742
|
renderSessionStats();
|
|
743
|
+
renderSessionStatsCard();
|
|
513
744
|
renderPrefsBar();
|
|
745
|
+
reconcileCasinoMode();
|
|
514
746
|
|
|
515
747
|
// Auto-show update notification (once per session)
|
|
516
748
|
if (state.updateAvailable && !ui.updateNotificationShown && !anyModalOpen()) {
|
|
@@ -1014,6 +1246,33 @@ ${pureFnBlock}
|
|
|
1014
1246
|
bar.innerHTML = html;
|
|
1015
1247
|
}
|
|
1016
1248
|
|
|
1249
|
+
// ── Session Stats Card (sidebar) ───────────────────────────────
|
|
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;
|
|
1255
|
+
const s = state && state.sessionStats;
|
|
1256
|
+
if (!s) { card.innerHTML = ''; return; }
|
|
1257
|
+
const branches = (state && state.branches) || [];
|
|
1258
|
+
let activeCount = 0;
|
|
1259
|
+
let staleCount = 0;
|
|
1260
|
+
for (let i = 0; i < branches.length; i++) {
|
|
1261
|
+
const b = branches[i];
|
|
1262
|
+
if (b.justUpdated || b.name === state.currentBranch) activeCount++;
|
|
1263
|
+
else staleCount++;
|
|
1264
|
+
}
|
|
1265
|
+
let html = '';
|
|
1266
|
+
html += '<span class="stat-k">Session</span><span class="stat-v">' + escHtml(s.sessionDuration || '0m') + '</span>';
|
|
1267
|
+
html += '<span class="stat-k">Lines</span><span class="stat-v"><span class="added">+' + fmtCompact(s.linesAdded || 0) + '</span> <span class="sep">/</span> <span class="deleted">-' + fmtCompact(s.linesDeleted || 0) + '</span></span>';
|
|
1268
|
+
html += '<span class="stat-k">Polls</span><span class="stat-v">' + (s.totalPolls || 0) + ' <span class="sep">·</span> <span class="accent">' + (s.hitRate || 0) + '%</span> hit</span>';
|
|
1269
|
+
if (s.lastUpdate) {
|
|
1270
|
+
html += '<span class="stat-k">Last hit</span><span class="stat-v">' + escHtml(s.lastUpdate) + '</span>';
|
|
1271
|
+
}
|
|
1272
|
+
html += '<span class="stat-k">Branches</span><span class="stat-v">' + activeCount + ' <span class="sep">active</span> <span class="sep">·</span> ' + staleCount + ' <span class="sep">stale</span></span>';
|
|
1273
|
+
card.innerHTML = html;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1017
1276
|
// ── Error Toast with Stash Hint ────────────────────────────────
|
|
1018
1277
|
function showErrorToastWithHint(message, hint) {
|
|
1019
1278
|
const container = document.getElementById('toast-container');
|
package/src/server/web.js
CHANGED
|
@@ -14,6 +14,7 @@ const http = require('http');
|
|
|
14
14
|
const { getWebDashboardHtml } = require('./web-ui');
|
|
15
15
|
const { version: PACKAGE_VERSION } = require('../../package.json');
|
|
16
16
|
const sessionStats = require('../stats/session');
|
|
17
|
+
const casino = require('../casino');
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Default web dashboard port
|
|
@@ -122,6 +123,13 @@ class WebDashboardServer {
|
|
|
122
123
|
|
|
123
124
|
// UI
|
|
124
125
|
soundEnabled: s.soundEnabled,
|
|
126
|
+
casinoModeEnabled: s.casinoModeEnabled,
|
|
127
|
+
// Casino stats track server-side regardless of which surface toggled
|
|
128
|
+
// the mode on, so the web dashboard can render the same winnings box
|
|
129
|
+
// the terminal does. Null when disabled — keeps payload small and
|
|
130
|
+
// avoids ticking Math.random()/Date.now() into every SSE push when
|
|
131
|
+
// nobody's asked for the effect.
|
|
132
|
+
casinoStats: s.casinoModeEnabled ? casino.getStats() : null,
|
|
125
133
|
projectName: s.projectName,
|
|
126
134
|
|
|
127
135
|
// Activity
|