dubs-server 1.0.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.
Files changed (304) hide show
  1. package/.claude/settings.local.json +280 -0
  2. package/CLAUDE.md +46 -0
  3. package/CONNECT4_PRODUCTION_DEPLOY.md +155 -0
  4. package/CURRENT_SESSION.md +171 -0
  5. package/CURRENT_SESSION_DRAW.md +516 -0
  6. package/MARCH_MADNESS_SURVIVOR.md +254 -0
  7. package/PANDA.md +166 -0
  8. package/Procfile +4 -0
  9. package/README.md +476 -0
  10. package/controllers/livescoresController.js +376 -0
  11. package/controllers/pickemController.js +554 -0
  12. package/controllers/survivorAdminController.js +887 -0
  13. package/controllers/survivorController.js +623 -0
  14. package/cron/oracleMonitor.js +77 -0
  15. package/cron/pickemOracleMonitor.js +73 -0
  16. package/data/jackpot-history.json +952 -0
  17. package/data/ncaaTeams.js +406 -0
  18. package/documentation/API_SECURITY_GUIDE.md +327 -0
  19. package/documentation/ARCADE_API.md +593 -0
  20. package/documentation/ARCADE_IMPLEMENTATION_SUMMARY.md +399 -0
  21. package/documentation/ARCADE_QUICKSTART.md +242 -0
  22. package/documentation/AUTOMATIC_MODE_ORACLE.md +321 -0
  23. package/documentation/BUG_FIX_COHORT_DATE_DISPLAY.md +171 -0
  24. package/documentation/CLAIM_MIGRATION_INSTRUCTIONS.md +52 -0
  25. package/documentation/CLAIM_STATUS_FIX.md +67 -0
  26. package/documentation/CLI_TOOL_GUIDE.md +372 -0
  27. package/documentation/COHORT_RETENTION_ANALYSIS.md +295 -0
  28. package/documentation/COHORT_RETENTION_IMPLEMENTATION_COMPLETE.md +461 -0
  29. package/documentation/COHORT_RETENTION_SUMMARY.md +204 -0
  30. package/documentation/COMPLETE_PROJECT_SUMMARY.md +490 -0
  31. package/documentation/DATABASE_QUERIES.md +269 -0
  32. package/documentation/DATABASE_RETENTION_POLICY.md +390 -0
  33. package/documentation/DATABASE_SETUP_GUIDE.md +361 -0
  34. package/documentation/DATABASE_SETUP_SUMMARY.md +247 -0
  35. package/documentation/DEMO_API_CURL_COMMANDS.md +656 -0
  36. package/documentation/DEPLOYMENT_SUMMARY.txt +100 -0
  37. package/documentation/DUPLICATE_NOTIFICATIONS_FIXED.md +201 -0
  38. package/documentation/EXCHANGE_RATES_INTEGRATION.md +371 -0
  39. package/documentation/FINAL_API_PROTECTION_TABLE.md +175 -0
  40. package/documentation/GAME_START_NOTIFICATIONS_DEPLOYMENT.md +256 -0
  41. package/documentation/GAME_START_NOTIFICATIONS_INTEGRATION.md +275 -0
  42. package/documentation/HEROKU_DEPLOYMENT.md +134 -0
  43. package/documentation/HEROKU_SCHEDULER_SETUP.md +271 -0
  44. package/documentation/JACKPOT_API.md +521 -0
  45. package/documentation/JACKPOT_DEPLOYMENT_GUIDE.md +362 -0
  46. package/documentation/JWT_IMPLEMENTATION_SUMMARY.md +373 -0
  47. package/documentation/JWT_QUICK_SETUP.md +268 -0
  48. package/documentation/JWT_TESTING_GUIDE.md +404 -0
  49. package/documentation/KEEPER_RECOVERY_GUIDE.md +381 -0
  50. package/documentation/KEEPER_SETUP.md +206 -0
  51. package/documentation/KEEPER_STATE_MACHINE.md +423 -0
  52. package/documentation/LATEST_PRODUCTION_SETUP.md +387 -0
  53. package/documentation/LOCAL_VOTING_TEST.md +279 -0
  54. package/documentation/ORACLE_FIXES_SUMMARY.md +188 -0
  55. package/documentation/ORACLE_POSTGRESQL_UPDATE.md +202 -0
  56. package/documentation/PAYMENT_DEPLOYMENT.md +209 -0
  57. package/documentation/PNL_TRACKING_SETUP.md +189 -0
  58. package/documentation/PREVENTING_LOCKUP_ERRORS.md +472 -0
  59. package/documentation/PRODUCTION_READY_SUMMARY.md +227 -0
  60. package/documentation/PUBLIC_VS_PRIVATE_ENDPOINTS.md +278 -0
  61. package/documentation/QUICK_AUTH_SETUP.md +99 -0
  62. package/documentation/QUICK_DEPLOY.md +224 -0
  63. package/documentation/QUICK_FIX.md +114 -0
  64. package/documentation/QUICK_START.md +152 -0
  65. package/documentation/REFEREE_MODE_GUIDE.md +392 -0
  66. package/documentation/RETENTION_CORE_ACTION_UPDATE.md +313 -0
  67. package/documentation/RETENTION_UPDATE_SUMMARY.md +108 -0
  68. package/documentation/RUN_MIGRATION_NOW.md +39 -0
  69. package/documentation/SCRIPTS_UPDATE_SUMMARY.md +251 -0
  70. package/documentation/SETUP_GUIDE.md +184 -0
  71. package/documentation/STATE_MACHINE_IMPLEMENTATION.md +250 -0
  72. package/documentation/TELEGRAM_NOTIFICATIONS_DIAGNOSIS.md +361 -0
  73. package/documentation/UNIFIED_ARCHITECTURE.md +231 -0
  74. package/documentation/VOTING_DEPLOYMENT_SUMMARY.md +392 -0
  75. package/documentation/WEBSOCKET_ARCHITECTURE.md +881 -0
  76. package/documentation/WHAT_WE_BUILT_TODAY.md +369 -0
  77. package/documentation/latest/LATEST_PRODUCTION_SETUP.md +865 -0
  78. package/ecosystem.config.js +65 -0
  79. package/env.template +125 -0
  80. package/middleware/apiKeyAuth.js +136 -0
  81. package/middleware/authenticate.js +214 -0
  82. package/middleware/developerUserAuth.js +76 -0
  83. package/middleware/socketAuth.js +69 -0
  84. package/package.json +49 -0
  85. package/postman/Dubs-API-v1-With-Voting.postman_collection.json +555 -0
  86. package/postman/Dubs-API-v1.postman_collection.json +205 -0
  87. package/postman/Dubs_Developer_API.postman_collection.json +662 -0
  88. package/postman/QUICKSTART.md +118 -0
  89. package/postman/QUICK_REFERENCE.md +246 -0
  90. package/postman/README.md +71 -0
  91. package/postman/VOTING_API_GUIDE.md +426 -0
  92. package/refactor/Animations.md +148 -0
  93. package/refactor/Chat.md +252 -0
  94. package/routes/actionsRoutes.js +699 -0
  95. package/routes/adminRoutes.js +370 -0
  96. package/routes/analyticsRoutes.js +1262 -0
  97. package/routes/arcadeRoutes.js +557 -0
  98. package/routes/authRoutes.js +2310 -0
  99. package/routes/avatarRoutes.js +85 -0
  100. package/routes/botRoutes.js +211 -0
  101. package/routes/chatRoutes.js +377 -0
  102. package/routes/cryptoPriceRoutes.js +105 -0
  103. package/routes/developerRoutes.js +4201 -0
  104. package/routes/deviceRoutes.js +214 -0
  105. package/routes/dmRoutes.js +167 -0
  106. package/routes/esportsRoutes.js +806 -0
  107. package/routes/exchangeRateRoutes.js +233 -0
  108. package/routes/gamesRoutes.js +3028 -0
  109. package/routes/jackpotRoutes.js +754 -0
  110. package/routes/keeperMonitoringRoutes.js +156 -0
  111. package/routes/keeperWebhookRoutes.js +466 -0
  112. package/routes/livescoresRoutes.js +31 -0
  113. package/routes/pickemAdminRoutes.js +199 -0
  114. package/routes/pickemRoutes.js +231 -0
  115. package/routes/playerStatsRoutes.js +147 -0
  116. package/routes/portfolioRoutes.js +217 -0
  117. package/routes/promoRoutes.js +418 -0
  118. package/routes/referralEarningsRoutes.js +392 -0
  119. package/routes/socialRoutes.js +459 -0
  120. package/routes/sportsRoutes.js +1271 -0
  121. package/routes/survivorAdminRoutes.js +345 -0
  122. package/routes/survivorRoutes.js +756 -0
  123. package/routes/uploadRoutes.js +256 -0
  124. package/routes/userProfileRoutes.js +244 -0
  125. package/routes/whatsNewRoutes.js +331 -0
  126. package/scripts/.claude/settings.local.json +15 -0
  127. package/scripts/README.md +170 -0
  128. package/scripts/RESTART_EVERYTHING.sh +104 -0
  129. package/scripts/add-claim-columns.sql +48 -0
  130. package/scripts/add-crypto-prices-cache.sql +27 -0
  131. package/scripts/add-exchange-rates-cache.sql +40 -0
  132. package/scripts/add-game-invite-column.sql +23 -0
  133. package/scripts/add-game-invite-notification.sql +33 -0
  134. package/scripts/add-game-invite-telegram-pref.sql +16 -0
  135. package/scripts/add-game-joined-notification.sql +16 -0
  136. package/scripts/add-game-joined-pref.js +40 -0
  137. package/scripts/add-game-joined-preference.sql +6 -0
  138. package/scripts/add-game-start-notifications.sql +41 -0
  139. package/scripts/add-notification-flags-to-games.sql +55 -0
  140. package/scripts/add-pending-game-dismissals.sql +19 -0
  141. package/scripts/add-preferred-currency.sql +34 -0
  142. package/scripts/add-winner-columns.js +61 -0
  143. package/scripts/add_mention_system.sql +53 -0
  144. package/scripts/add_payment_system.sql +96 -0
  145. package/scripts/add_sports_event_id_column.sql +22 -0
  146. package/scripts/analyze-cohort-data-heroku.js +276 -0
  147. package/scripts/analyze-cohort-data.js +295 -0
  148. package/scripts/analyze-prod-cohorts.sh +10 -0
  149. package/scripts/backfill-matchup-images.js +245 -0
  150. package/scripts/backfill-missing-signatures.js +175 -0
  151. package/scripts/backfill-referral-earnings.js +202 -0
  152. package/scripts/check-chat-schema.js +130 -0
  153. package/scripts/check-db.sh +14 -0
  154. package/scripts/check_oracle_in_game.js +54 -0
  155. package/scripts/cleanup-database.js +193 -0
  156. package/scripts/clear-notification-cache.js +85 -0
  157. package/scripts/convert-mnemonic.js +50 -0
  158. package/scripts/create-users-table.sql +44 -0
  159. package/scripts/debug-cohort-counts.js +248 -0
  160. package/scripts/debug-winner-calc.js +84 -0
  161. package/scripts/deploy-payment-system.sh +118 -0
  162. package/scripts/deploy-to-heroku.sh +63 -0
  163. package/scripts/diagnose-locked-round.js +143 -0
  164. package/scripts/dubs-cli.js +720 -0
  165. package/scripts/dump-account.js +65 -0
  166. package/scripts/find-vrf-offset.js +48 -0
  167. package/scripts/fix-chat-notifications-constraint.sql +122 -0
  168. package/scripts/fix-claim-columns.js +124 -0
  169. package/scripts/fix-constraint-now.js +44 -0
  170. package/scripts/fix-lock-timestamps.js +96 -0
  171. package/scripts/fix-locked-round.sh +126 -0
  172. package/scripts/fix-missing-badges.sql +91 -0
  173. package/scripts/fix-payment-notifications.sql +41 -0
  174. package/scripts/force-new-round.js +55 -0
  175. package/scripts/force-resolve-and-claim.js +278 -0
  176. package/scripts/important/README.md +115 -0
  177. package/scripts/important/authority-force-lock.js +197 -0
  178. package/scripts/important/authority-resolve-game.js +267 -0
  179. package/scripts/important/check-game-status.js +373 -0
  180. package/scripts/important/list-pending-games-by-version.js +270 -0
  181. package/scripts/important/reconcile-v1-v2-payouts.js +270 -0
  182. package/scripts/initialize-jackpot.js +111 -0
  183. package/scripts/jackpot/.claude/settings.local.json +10 -0
  184. package/scripts/jackpot/force-reset.js +84 -0
  185. package/scripts/jackpot/initialize-mainnet.js +100 -0
  186. package/scripts/jackpot/keeper.js +742 -0
  187. package/scripts/jackpot/status.js +107 -0
  188. package/scripts/jackpot/update-round-duration.js +143 -0
  189. package/scripts/keeper-bot.js +112 -0
  190. package/scripts/list-pending-games.js +131 -0
  191. package/scripts/migrate-chat-v2.js +127 -0
  192. package/scripts/migrate-chat-winners.js +84 -0
  193. package/scripts/migrate-chat.sh +17 -0
  194. package/scripts/migrate-game-invite.js +83 -0
  195. package/scripts/migrate-heroku-game-notifications.sh +159 -0
  196. package/scripts/migrations/001_analytics_tables.sql +422 -0
  197. package/scripts/migrations/002_add_matchup_image_url.sql +14 -0
  198. package/scripts/migrations/003_referral_earnings.sql +208 -0
  199. package/scripts/migrations/004_add_whats_new_notification_type.sql +62 -0
  200. package/scripts/migrations/005_add_connect4_your_turn_notification.sql +61 -0
  201. package/scripts/migrations/005_push_notifications.sql +55 -0
  202. package/scripts/migrations/006_add_draw_team_players.sql +28 -0
  203. package/scripts/migrations/006_add_game_cancelled_notification.sql +62 -0
  204. package/scripts/migrations/007_add_gif_url.sql +8 -0
  205. package/scripts/migrations/008_add_connect4_columns.sql +139 -0
  206. package/scripts/migrations/008_add_pool_tracking.sql +22 -0
  207. package/scripts/migrations/009_create_survivor_pool_tables.sql +174 -0
  208. package/scripts/migrations/010_add_survivor_pool_outcome.sql +28 -0
  209. package/scripts/migrations/011_create_developer_tables.sql +67 -0
  210. package/scripts/migrations/011_fix_keeper_tables.sql +85 -0
  211. package/scripts/migrations/012_create_developer_webhooks.sql +31 -0
  212. package/scripts/migrations/013_add_network_mode.sql +18 -0
  213. package/scripts/migrations/014_create_developer_app_users.sql +19 -0
  214. package/scripts/migrations/015_add_ui_config.sql +4 -0
  215. package/scripts/migrations/016_add_resolution_secret.sql +4 -0
  216. package/scripts/migrations/017_add_external_game_id.sql +3 -0
  217. package/scripts/migrations/018_create_pickem_tables.sql +115 -0
  218. package/scripts/migrations/019_expo_push_tokens.sql +19 -0
  219. package/scripts/migrations/create_whats_new_tables.sql +88 -0
  220. package/scripts/migrations/drop_live_games_tables.sql +34 -0
  221. package/scripts/open-jackpot-round.js +85 -0
  222. package/scripts/purge-all-data.sh +329 -0
  223. package/scripts/purge-all-data.sql +142 -0
  224. package/scripts/purge-heroku-data.sh +149 -0
  225. package/scripts/purge-heroku-data.sql +62 -0
  226. package/scripts/rebuild-heroku-database.sh +113 -0
  227. package/scripts/recover-funds.js +357 -0
  228. package/scripts/regenerate-epl-images.js +278 -0
  229. package/scripts/resize-s3-matchup-images.js +374 -0
  230. package/scripts/resolve-direct.js +88 -0
  231. package/scripts/resolve-mock-game.js +124 -0
  232. package/scripts/resolve-pickem-game.js +55 -0
  233. package/scripts/resolve-round-manual.js +83 -0
  234. package/scripts/resolve-stuck-game.js +382 -0
  235. package/scripts/resolve-stuck-round.js +42 -0
  236. package/scripts/run-connect4-migration.sh +16 -0
  237. package/scripts/run-mention-migration.sh +32 -0
  238. package/scripts/run-payment-migration.sh +51 -0
  239. package/scripts/run-preferred-currency-migration.sh +31 -0
  240. package/scripts/run-referral-earnings-migration.sh +32 -0
  241. package/scripts/run-survivor-outcome-migration.sh +16 -0
  242. package/scripts/seed-test-users.js +346 -0
  243. package/scripts/setup-auth-tables.js +78 -0
  244. package/scripts/setup-complete-database.sql +992 -0
  245. package/scripts/setup-database-fresh.sh +359 -0
  246. package/scripts/setup-heroku-keeper.sh +48 -0
  247. package/scripts/setup-keeper-database.js +83 -0
  248. package/scripts/setup-keeper-state-db.sql +110 -0
  249. package/scripts/setup-oracle.sh +39 -0
  250. package/scripts/setup-pnl-tracking.js +111 -0
  251. package/scripts/start-devnet.sh +14 -0
  252. package/scripts/test-arcade-devnet.sh +160 -0
  253. package/scripts/test-arcade-match.sh +109 -0
  254. package/scripts/test-automatic-mode.sh +239 -0
  255. package/scripts/test-connect4-cancel-claim.js +370 -0
  256. package/scripts/test-connect4-e2e.js +369 -0
  257. package/scripts/test-connect4-resolve.js +369 -0
  258. package/scripts/test-game-state-endpoint.js +136 -0
  259. package/scripts/test-invite-notification.js +86 -0
  260. package/scripts/test-jackpot-api.sh +71 -0
  261. package/scripts/test-poll-confirmation.js +267 -0
  262. package/scripts/test-resolve-game.js +271 -0
  263. package/scripts/test-resolve-signature.js +223 -0
  264. package/scripts/test-signature-preservation.js +124 -0
  265. package/scripts/test-state-machine.js +291 -0
  266. package/scripts/test-webhook-receiver.js +60 -0
  267. package/scripts/update-notification-constraint.js +52 -0
  268. package/scripts/verify-account-layout.js +145 -0
  269. package/scripts/verify-winner-algorithm.js +278 -0
  270. package/server.js +5259 -0
  271. package/services/arcadeMatchService.js +763 -0
  272. package/services/automaticGameOracle.js +1596 -0
  273. package/services/chatService.js +1612 -0
  274. package/services/connect4GameService.js +1049 -0
  275. package/services/connect4NotificationService.js +374 -0
  276. package/services/cryptoPriceService.js +223 -0
  277. package/services/customGameResolver.js +260 -0
  278. package/services/db.js +79 -0
  279. package/services/directMessageService.js +389 -0
  280. package/services/discordNotifications.js +160 -0
  281. package/services/exchangeRateService.js +289 -0
  282. package/services/expoPushService.js +314 -0
  283. package/services/gamesCacheService.js +539 -0
  284. package/services/jackpotHistory.js +331 -0
  285. package/services/jackpotService.js +856 -0
  286. package/services/keeperStateService.js +355 -0
  287. package/services/matchupImageService.js +591 -0
  288. package/services/notificationCacheService.js +407 -0
  289. package/services/pickemOracle.js +440 -0
  290. package/services/playerStatsService.js +389 -0
  291. package/services/portfolioService.js +555 -0
  292. package/services/promoService.js +757 -0
  293. package/services/promoTreasuryService.js +239 -0
  294. package/services/pushNotifications.js +353 -0
  295. package/services/redisService.js +422 -0
  296. package/services/referralEarningsService.js +728 -0
  297. package/services/s3Service.js +396 -0
  298. package/services/socialService.js +1202 -0
  299. package/services/survivorOracle.js +469 -0
  300. package/services/survivorSimulator.js +475 -0
  301. package/services/telegramNotifications.js +461 -0
  302. package/services/userProfileStatsService.js +1185 -0
  303. package/services/whatsNewService.js +388 -0
  304. package/utils/urlHelper.js +95 -0
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Keeper Monitoring Routes
3
+ *
4
+ * Endpoints for viewing keeper health, stuck rounds, and logs
5
+ */
6
+
7
+ const express = require('express');
8
+ const router = express.Router();
9
+ const KeeperStateService = require('../services/keeperStateService');
10
+
11
+ const state = new KeeperStateService();
12
+
13
+ /**
14
+ * GET /api/keeper/health
15
+ * Returns overall keeper health summary
16
+ */
17
+ router.get('/health', async (req, res) => {
18
+ try {
19
+ const summary = await state.getHealthSummary();
20
+ const stuckRounds = await state.getStuckRounds();
21
+
22
+ const health = {
23
+ healthy: stuckRounds.length === 0 && (summary?.stuck_rounds || 0) === 0,
24
+ summary,
25
+ stuckRounds,
26
+ timestamp: new Date().toISOString()
27
+ };
28
+
29
+ res.json(health);
30
+ } catch (error) {
31
+ res.status(500).json({ error: error.message });
32
+ }
33
+ });
34
+
35
+ /**
36
+ * GET /api/keeper/rounds/stuck
37
+ * Returns list of stuck rounds
38
+ */
39
+ router.get('/rounds/stuck', async (req, res) => {
40
+ try {
41
+ const stuck = await state.getStuckRounds();
42
+ res.json({ stuck, count: stuck.length });
43
+ } catch (error) {
44
+ res.status(500).json({ error: error.message });
45
+ }
46
+ });
47
+
48
+ /**
49
+ * GET /api/keeper/rounds/:roundId
50
+ * Returns detailed state for a specific round
51
+ */
52
+ router.get('/rounds/:roundId', async (req, res) => {
53
+ try {
54
+ const roundId = req.params.roundId;
55
+ const round = await state.getRound(roundId);
56
+ const actions = await state.getRoundActions(roundId, 50);
57
+
58
+ if (!round) {
59
+ return res.status(404).json({ error: 'Round not found in database' });
60
+ }
61
+
62
+ res.json({
63
+ round,
64
+ actions,
65
+ isStuck: state.isRoundStuck(round)
66
+ });
67
+ } catch (error) {
68
+ res.status(500).json({ error: error.message });
69
+ }
70
+ });
71
+
72
+ /**
73
+ * GET /api/keeper/rounds/:roundId/actions
74
+ * Returns action log for a specific round
75
+ */
76
+ router.get('/rounds/:roundId/actions', async (req, res) => {
77
+ try {
78
+ const roundId = req.params.roundId;
79
+ const limit = parseInt(req.query.limit) || 100;
80
+ const actions = await state.getRoundActions(roundId, limit);
81
+
82
+ res.json({ roundId, actions, count: actions.length });
83
+ } catch (error) {
84
+ res.status(500).json({ error: error.message });
85
+ }
86
+ });
87
+
88
+ /**
89
+ * GET /api/keeper/dashboard
90
+ * Returns comprehensive dashboard data
91
+ */
92
+ router.get('/dashboard', async (req, res) => {
93
+ try {
94
+ const summary = await state.getHealthSummary();
95
+ const stuckRounds = await state.getStuckRounds();
96
+
97
+ // Get recent rounds (last 20)
98
+ const { rows: recentRounds } = await state.pool.query(`
99
+ SELECT round_id, status, entry_count, total_pot, winner_pubkey, win_amount,
100
+ created_at, locked_at, revealed_at, resolved_at, reset_at, retry_count
101
+ FROM keeper_rounds
102
+ ORDER BY round_id DESC
103
+ LIMIT 20
104
+ `);
105
+
106
+ // Get health metrics (last 24 hours)
107
+ const { rows: healthMetrics } = await state.pool.query(`
108
+ SELECT *
109
+ FROM keeper_health
110
+ WHERE timestamp > NOW() - INTERVAL '24 hours'
111
+ ORDER BY timestamp DESC
112
+ LIMIT 100
113
+ `);
114
+
115
+ // Calculate success rate
116
+ const totalRounds = summary?.resolved_rounds || 0;
117
+ const retriedRounds = summary?.rounds_with_retries || 0;
118
+ const successRate = totalRounds > 0 ? ((totalRounds - retriedRounds) / totalRounds * 100).toFixed(1) : 100;
119
+
120
+ const dashboard = {
121
+ summary: {
122
+ ...summary,
123
+ successRate: `${successRate}%`,
124
+ healthy: stuckRounds.length === 0
125
+ },
126
+ stuckRounds,
127
+ recentRounds: recentRounds.map(r => ({
128
+ ...r,
129
+ total_pot_sol: r.total_pot ? (Number(r.total_pot) / 1e9).toFixed(4) : '0',
130
+ win_amount_sol: r.win_amount ? (Number(r.win_amount) / 1e9).toFixed(4) : '0'
131
+ })),
132
+ healthMetrics,
133
+ timestamp: new Date().toISOString()
134
+ };
135
+
136
+ res.json(dashboard);
137
+ } catch (error) {
138
+ res.status(500).json({ error: error.message });
139
+ }
140
+ });
141
+
142
+ /**
143
+ * POST /api/keeper/cleanup
144
+ * Manually trigger log cleanup
145
+ */
146
+ router.post('/cleanup', async (req, res) => {
147
+ try {
148
+ await state.cleanupOldLogs();
149
+ res.json({ success: true, message: 'Cleanup completed' });
150
+ } catch (error) {
151
+ res.status(500).json({ error: error.message });
152
+ }
153
+ });
154
+
155
+ module.exports = router;
156
+
@@ -0,0 +1,466 @@
1
+ /**
2
+ * Keeper Webhook Routes
3
+ *
4
+ * The keeper (separate dyno) calls these endpoints to trigger WebSocket broadcasts
5
+ */
6
+
7
+ const express = require('express');
8
+ const router = express.Router();
9
+ const { instance: history } = require('../services/jackpotHistory');
10
+ const notificationCacheService = require('../services/notificationCacheService');
11
+
12
+ // Store service references (will be injected)
13
+ let chatServiceInstance = null;
14
+ let playerStatsServiceInstance = null;
15
+ let jackpotServiceInstance = null;
16
+ let userProfileStatsServiceInstance = null;
17
+
18
+ // Inject dependencies
19
+ router.setChatService = (chatService) => {
20
+ chatServiceInstance = chatService;
21
+ };
22
+
23
+ router.setPlayerStatsService = (playerStatsService) => {
24
+ playerStatsServiceInstance = playerStatsService;
25
+ };
26
+
27
+ router.setJackpotService = (jackpotService) => {
28
+ jackpotServiceInstance = jackpotService;
29
+ };
30
+
31
+ router.setUserProfileStatsService = (service) => {
32
+ userProfileStatsServiceInstance = service;
33
+ };
34
+
35
+ /**
36
+ * POST /api/keeper-webhook/timer-update
37
+ * Keeper calls this every 5s with current round state
38
+ */
39
+ router.post('/timer-update', async (req, res) => {
40
+ try {
41
+ const { roundId, timeRemaining, entryCount, totalPot, status } = req.body;
42
+
43
+ if (global.io) {
44
+ global.io.to(`round_${roundId}`).emit('timer_update', {
45
+ roundId,
46
+ timeRemaining,
47
+ entryCount,
48
+ totalPot,
49
+ status,
50
+ timestamp: Date.now()
51
+ });
52
+ }
53
+
54
+ res.json({ success: true });
55
+ } catch (error) {
56
+ res.status(500).json({ error: error.message });
57
+ }
58
+ });
59
+
60
+ /**
61
+ * POST /api/keeper-webhook/round-locked
62
+ * Keeper calls this when round locks
63
+ */
64
+ router.post('/round-locked', async (req, res) => {
65
+ try {
66
+ const { roundId } = req.body;
67
+
68
+ console.log(`📢 Webhook: Round ${roundId} locked`);
69
+
70
+ if (global.io) {
71
+ global.io.to(`round_${roundId}`).emit('round_locked', {
72
+ roundId,
73
+ timestamp: Date.now()
74
+ });
75
+ console.log(`⚡ Broadcasted: round_locked`);
76
+ }
77
+
78
+ res.json({ success: true, broadcasted: true });
79
+ } catch (error) {
80
+ console.error('Webhook error:', error);
81
+ res.status(500).json({ error: error.message });
82
+ }
83
+ });
84
+
85
+ /**
86
+ * POST /api/keeper-webhook/winner-selected
87
+ * Keeper calls this when winner is selected
88
+ * 🔥 NOW WITH EPIC CHAT ANNOUNCEMENT! 🔥
89
+ */
90
+ router.post('/winner-selected', async (req, res) => {
91
+ try {
92
+ const { roundId, winner, winAmount, totalPot, entryCount, signature, serverSeed, serverSeedHash, oracleSeed } = req.body;
93
+
94
+ console.log('━'.repeat(80));
95
+ console.log(`📢 WINNER WEBHOOK CALLED!`);
96
+ console.log(` Round: ${roundId}`);
97
+ console.log(` Winner: ${winner}`);
98
+ console.log(` Amount (lamports): ${winAmount}`);
99
+ console.log(` Amount (SOL): ${winAmount / 1_000_000_000}`);
100
+ console.log(` Total Pot: ${totalPot}`);
101
+ console.log(` Entry Count: ${entryCount}`);
102
+ console.log(` VRF Data: ${serverSeed ? '✅' : '❌'} seed, ${serverSeedHash ? '✅' : '❌'} hash, ${oracleSeed ? '✅' : '❌'} oracle`);
103
+ console.log(` Chat Service: ${chatServiceInstance ? '✅ AVAILABLE' : '❌ NOT AVAILABLE'}`);
104
+ console.log(` Socket.IO: ${global.io ? '✅ AVAILABLE' : '❌ NOT AVAILABLE'}`);
105
+ console.log('━'.repeat(80));
106
+
107
+ // 💾 SAVE TO HISTORY with VRF verification data
108
+ try {
109
+ await history.addRound({
110
+ roundId: roundId.toString(),
111
+ winner,
112
+ winAmount: winAmount.toString(),
113
+ totalPot: totalPot.toString(),
114
+ entryCount,
115
+ timestamp: new Date().toISOString(),
116
+ signature: signature || null,
117
+ serverSeed: serverSeed || null,
118
+ serverSeedHash: serverSeedHash || null,
119
+ oracleSeed: oracleSeed || null,
120
+ });
121
+ console.log(`💾 Winner saved to history: ${winner.slice(0, 8)}... (VRF: ${serverSeed ? 'YES' : 'NO'})`);
122
+ } catch (historyError) {
123
+ console.error('⚠️ Failed to save winner to history:', historyError.message);
124
+ }
125
+
126
+ // 📊 TRACK WINNER IN PLAYER STATS! 📊
127
+ let winnerStreak = 1;
128
+ if (playerStatsServiceInstance) {
129
+ try {
130
+ console.log('📊 Recording win in player stats...');
131
+ const result = await playerStatsServiceInstance.recordWin({
132
+ walletAddress: winner,
133
+ roundId,
134
+ winAmount
135
+ });
136
+ winnerStreak = result?.newStreak || 1;
137
+ console.log(`✅ Win recorded for ${winner.slice(0, 8)}... (streak: ${winnerStreak}🔥)`);
138
+ } catch (statsError) {
139
+ console.error('⚠️ Failed to record win in stats:', statsError.message);
140
+ // Don't fail the whole request if stats tracking fails
141
+ }
142
+ }
143
+
144
+ // 🏆 POST EPIC WINNER ANNOUNCEMENT TO CHAT! 🏆
145
+ if (chatServiceInstance) {
146
+ try {
147
+ console.log('🔥 Attempting to post winner announcement...');
148
+
149
+ const chatMessage = await chatServiceInstance.postWinnerAnnouncement({
150
+ winner,
151
+ winAmount: winAmount / 1_000_000_000, // Convert lamports to SOL
152
+ roundId
153
+ });
154
+
155
+ console.log('✅ Chat message created:', chatMessage);
156
+
157
+ // Broadcast the epic message to chat via /chat namespace with correct event name
158
+ if (global.io) {
159
+ const chatNamespace = global.io.of('/chat');
160
+ chatNamespace.emit('new_message', chatMessage);
161
+ console.log(`🔥 EPIC WINNER ANNOUNCEMENT broadcasted to ALL chat clients!`);
162
+ console.log(` Message ID: ${chatMessage.id}`);
163
+ console.log(` Winner: ${chatMessage.walletAddress}`);
164
+ console.log(` Amount: ◎${chatMessage.winAmount}`);
165
+ } else {
166
+ console.warn('⚠️ Socket.IO not available - message saved but not broadcasted');
167
+ }
168
+ } catch (chatError) {
169
+ console.error('❌ Failed to post winner announcement to chat:');
170
+ console.error(' Error:', chatError.message);
171
+ console.error(' Stack:', chatError.stack);
172
+ // Don't fail the whole request if chat fails
173
+ }
174
+ } else {
175
+ console.warn('⚠️ Chat service not available - skipping winner announcement');
176
+ }
177
+
178
+ // Look up winner's username/avatar/id for animation display + notifications
179
+ let winnerUsername = winner.slice(0, 4) + '...' + winner.slice(-4);
180
+ let winnerAvatar = null;
181
+ let winnerUserId = null;
182
+ if (chatServiceInstance?.pool) {
183
+ try {
184
+ const userResult = await chatServiceInstance.pool.query(
185
+ 'SELECT id, username, avatar FROM users WHERE wallet_address = $1',
186
+ [winner]
187
+ );
188
+ if (userResult.rows.length > 0) {
189
+ winnerUserId = userResult.rows[0].id;
190
+ winnerUsername = userResult.rows[0].username || winnerUsername;
191
+ winnerAvatar = userResult.rows[0].avatar || null;
192
+ }
193
+ } catch (lookupErr) {
194
+ console.warn('⚠️ Failed to look up winner profile:', lookupErr.message);
195
+ }
196
+ }
197
+
198
+ // 🔔 SEND WIN NOTIFICATION TO WINNER (in-app + Telegram)
199
+ if (winnerUserId && chatServiceInstance?.pool) {
200
+ try {
201
+ const pool = chatServiceInstance.pool;
202
+ const winAmountSOL = (winAmount / 1_000_000_000).toFixed(4);
203
+ const totalPotSOL = (totalPot / 1_000_000_000).toFixed(4);
204
+ const notifMessage = `You won ${winAmountSOL} SOL in Jackpot Round #${roundId}!`;
205
+
206
+ const notificationData = {
207
+ message: notifMessage,
208
+ gameInvite: {
209
+ gameId: `jackpot-round-${roundId}`,
210
+ title: 'Jackpot',
211
+ buyIn: 0,
212
+ gameType: 'jackpot',
213
+ },
214
+ jackpotWin: {
215
+ roundId,
216
+ winAmount: winAmountSOL,
217
+ totalPot: totalPotSOL,
218
+ entryCount,
219
+ winnerUsername,
220
+ },
221
+ };
222
+
223
+ // Insert into chat_notifications
224
+ const notifResult = await pool.query(
225
+ `INSERT INTO chat_notifications (
226
+ user_id, notification_type, notification_data, read, created_at
227
+ ) VALUES ($1, $2, $3, false, NOW())
228
+ RETURNING id`,
229
+ [winnerUserId, 'game_won', JSON.stringify(notificationData)]
230
+ );
231
+ const notificationId = notifResult.rows[0].id;
232
+
233
+ const winnerNotifPayload = {
234
+ id: notificationId,
235
+ type: 'game_won',
236
+ senderUsername: 'Jackpot',
237
+ senderWallet: 'system',
238
+ message: notifMessage,
239
+ gameInvite: notificationData.gameInvite,
240
+ jackpotWin: notificationData.jackpotWin,
241
+ createdAt: new Date().toISOString(),
242
+ read: false,
243
+ };
244
+
245
+ // Update Redis notification cache
246
+ notificationCacheService.cacheNotification(winnerUserId, winnerNotifPayload);
247
+
248
+ // Broadcast via WebSocket
249
+ if (global.io) {
250
+ const chatNamespace = global.io.of('/chat');
251
+ chatNamespace.to(`user-${winnerUserId}`).emit('notification', winnerNotifPayload);
252
+ }
253
+
254
+ // Forward to Telegram
255
+ try {
256
+ const { forwardChatNotification } = require('../services/telegramNotifications');
257
+ const telegramMsg = `🏆 You won the Jackpot!\n\nRound #${roundId}\nWinnings: ◎${winAmountSOL}\nTotal Pot: ◎${totalPotSOL}\n${entryCount} entries`;
258
+ await forwardChatNotification(pool, winnerUserId, 'game_won', 'Jackpot', telegramMsg, { gameId: `jackpot-round-${roundId}` });
259
+ } catch (tgErr) {
260
+ console.warn('⚠️ Telegram notification failed:', tgErr.message);
261
+ }
262
+
263
+ console.log(`🔔 Jackpot win notification sent to ${winnerUsername} (user-${winnerUserId})`);
264
+ } catch (notifError) {
265
+ console.error('⚠️ Failed to send jackpot win notification:', notifError.message);
266
+ }
267
+ }
268
+
269
+ // 📢 SEND LOSS NOTIFICATIONS TO ALL NON-WINNERS (in-app + Telegram)
270
+ if (jackpotServiceInstance && chatServiceInstance?.pool) {
271
+ try {
272
+ const entries = await jackpotServiceInstance.getRoundEntries(roundId);
273
+ // Deduplicate by wallet address (same player can enter multiple times)
274
+ const uniqueWallets = [...new Set(entries.map(e => e.player))];
275
+ // Exclude the winner
276
+ const loserWallets = uniqueWallets.filter(w => w !== winner);
277
+
278
+ if (loserWallets.length > 0) {
279
+ const pool = chatServiceInstance.pool;
280
+ const winAmountSOL = (winAmount / 1_000_000_000).toFixed(4);
281
+ const totalPotSOL = (totalPot / 1_000_000_000).toFixed(4);
282
+
283
+ // Batch lookup all loser user IDs
284
+ const userLookup = await pool.query(
285
+ 'SELECT id, username, wallet_address FROM users WHERE wallet_address = ANY($1)',
286
+ [loserWallets]
287
+ );
288
+ const userMap = {};
289
+ for (const row of userLookup.rows) {
290
+ userMap[row.wallet_address] = { id: row.id, username: row.username };
291
+ }
292
+
293
+ let lossNotifCount = 0;
294
+ for (const loserWallet of loserWallets) {
295
+ const user = userMap[loserWallet];
296
+ if (!user) continue; // Unregistered wallet, skip
297
+
298
+ const lossNotifData = {
299
+ message: `Jackpot Round #${roundId} won by @${winnerUsername}`,
300
+ gameInvite: {
301
+ gameId: `jackpot-round-${roundId}`,
302
+ title: 'Jackpot',
303
+ buyIn: 0,
304
+ gameType: 'jackpot',
305
+ },
306
+ jackpotWin: {
307
+ roundId,
308
+ winAmount: winAmountSOL,
309
+ totalPot: totalPotSOL,
310
+ entryCount,
311
+ winnerUsername,
312
+ },
313
+ };
314
+
315
+ try {
316
+ // Insert into chat_notifications
317
+ const notifResult = await pool.query(
318
+ `INSERT INTO chat_notifications (
319
+ user_id, notification_type, notification_data, read, created_at
320
+ ) VALUES ($1, $2, $3, false, NOW())
321
+ RETURNING id`,
322
+ [user.id, 'game_lost', JSON.stringify(lossNotifData)]
323
+ );
324
+ const notificationId = notifResult.rows[0].id;
325
+
326
+ const loserNotifPayload = {
327
+ id: notificationId,
328
+ type: 'game_lost',
329
+ senderUsername: 'Jackpot',
330
+ senderWallet: 'system',
331
+ message: lossNotifData.message,
332
+ gameInvite: lossNotifData.gameInvite,
333
+ jackpotWin: lossNotifData.jackpotWin,
334
+ createdAt: new Date().toISOString(),
335
+ read: false,
336
+ };
337
+
338
+ // Update Redis notification cache
339
+ notificationCacheService.cacheNotification(user.id, loserNotifPayload);
340
+
341
+ // Broadcast via WebSocket
342
+ if (global.io) {
343
+ const chatNamespace = global.io.of('/chat');
344
+ chatNamespace.to(`user-${user.id}`).emit('notification', loserNotifPayload);
345
+ }
346
+
347
+ // Forward to Telegram
348
+ try {
349
+ const { forwardChatNotification } = require('../services/telegramNotifications');
350
+ const telegramMsg = `😔 Jackpot Round #${roundId}\n\nWon by @${winnerUsername}\nPot: ◎${totalPotSOL}\n${entryCount} entries\n\nBetter luck next time!`;
351
+ await forwardChatNotification(pool, user.id, 'game_lost', 'Jackpot', telegramMsg, { gameId: `jackpot-round-${roundId}` });
352
+ } catch (tgErr) {
353
+ // Telegram failures are non-critical
354
+ }
355
+
356
+ lossNotifCount++;
357
+ } catch (insertErr) {
358
+ console.warn(`⚠️ Failed to send loss notification to ${user.username}:`, insertErr.message);
359
+ }
360
+ }
361
+ console.log(`📢 Jackpot loss notifications sent to ${lossNotifCount}/${loserWallets.length} losers`);
362
+ }
363
+ } catch (entriesErr) {
364
+ console.error('⚠️ Failed to send jackpot loss notifications:', entriesErr.message);
365
+ }
366
+ }
367
+
368
+ // Invalidate profile stats cache for all round participants
369
+ if (userProfileStatsServiceInstance && jackpotServiceInstance) {
370
+ try {
371
+ const entries = await jackpotServiceInstance.getRoundEntries(roundId);
372
+ const wallets = [...new Set(entries.map(e => e.player))];
373
+ for (const wallet of wallets) {
374
+ userProfileStatsServiceInstance.invalidateCache(wallet);
375
+ }
376
+ console.log(`📊 Profile stats cache invalidated for ${wallets.length} participants`);
377
+ } catch (cacheErr) {
378
+ console.warn('⚠️ Failed to invalidate profile stats cache:', cacheErr.message);
379
+ }
380
+ }
381
+
382
+ // Broadcast winner_selected event to all clients (globally, not just room)
383
+ if (global.io) {
384
+ const winnerEvent = {
385
+ roundId,
386
+ winner,
387
+ winAmount,
388
+ totalPot,
389
+ entryCount,
390
+ winStreak: winnerStreak,
391
+ winnerUsername,
392
+ winnerAvatar,
393
+ timestamp: Date.now()
394
+ };
395
+ // Emit to specific room (for players in that round)
396
+ global.io.to(`round_${roundId}`).emit('winner_selected', winnerEvent);
397
+ // Also emit globally so LastWinner widget updates for everyone
398
+ global.io.emit('winner_selected', winnerEvent);
399
+ console.log(`⚡ Broadcasted: winner_selected globally to ALL clients (streak: ${winnerStreak}🔥)`);
400
+ }
401
+
402
+ res.json({ success: true, broadcasted: true });
403
+ } catch (error) {
404
+ console.error('Webhook error:', error);
405
+ res.status(500).json({ error: error.message });
406
+ }
407
+ });
408
+
409
+ /**
410
+ * POST /api/keeper-webhook/player-entry
411
+ * Track player entries for PNL stats
412
+ */
413
+ router.post('/player-entry', async (req, res) => {
414
+ try {
415
+ const { walletAddress, roundId, amount } = req.body;
416
+
417
+ console.log(`📢 Webhook: Player entry - ${walletAddress.slice(0, 8)}... wagered ${amount} lamports`);
418
+
419
+ // 📊 TRACK ENTRY IN PLAYER STATS! 📊
420
+ if (playerStatsServiceInstance) {
421
+ try {
422
+ await playerStatsServiceInstance.recordEntry({
423
+ walletAddress,
424
+ roundId,
425
+ amount
426
+ });
427
+ console.log(`✅ Entry recorded in stats`);
428
+ } catch (statsError) {
429
+ console.error('⚠️ Failed to record entry in stats:', statsError.message);
430
+ }
431
+ }
432
+
433
+ res.json({ success: true });
434
+ } catch (error) {
435
+ console.error('Webhook error:', error);
436
+ res.status(500).json({ error: error.message });
437
+ }
438
+ });
439
+
440
+ /**
441
+ * POST /api/keeper-webhook/round-opened
442
+ * Keeper calls this when new round opens
443
+ */
444
+ router.post('/round-opened', async (req, res) => {
445
+ try {
446
+ const { roundId } = req.body;
447
+
448
+ console.log(`📢 Webhook: New round ${roundId} opened`);
449
+
450
+ if (global.io) {
451
+ global.io.emit('round_opened', {
452
+ roundId,
453
+ timestamp: Date.now()
454
+ });
455
+ console.log(`⚡ Broadcasted: round_opened globally`);
456
+ }
457
+
458
+ res.json({ success: true, broadcasted: true });
459
+ } catch (error) {
460
+ console.error('Webhook error:', error);
461
+ res.status(500).json({ error: error.message });
462
+ }
463
+ });
464
+
465
+ module.exports = router;
466
+
@@ -0,0 +1,31 @@
1
+ /**
2
+ * 🏀 Live Scores Routes
3
+ *
4
+ * Public endpoints for fetching live sports scores
5
+ * No authentication required - public ESPN data
6
+ */
7
+
8
+ const express = require('express');
9
+ const router = express.Router();
10
+ const {
11
+ getAllLivescores,
12
+ getLivescoresByLeague
13
+ } = require('../controllers/livescoresController');
14
+
15
+ /**
16
+ * GET /api/livescores
17
+ * Get live scores for all leagues (MLB, NBA, NHL, NFL)
18
+ * @public No authentication required
19
+ */
20
+ router.get('/', getAllLivescores);
21
+
22
+ /**
23
+ * GET /api/livescores/:league
24
+ * Get live scores for a specific league
25
+ * @param {string} league - League name (MLB, NBA, NHL, NFL)
26
+ * @public No authentication required
27
+ */
28
+ router.get('/:league', getLivescoresByLeague);
29
+
30
+ module.exports = router;
31
+