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,887 @@
1
+ /**
2
+ * Survivor Admin Controller
3
+ * Admin operations for March Madness Survivor Pool
4
+ *
5
+ * Features:
6
+ * - Generate random bracket (for testing)
7
+ * - Manual team selection (for real Selection Sunday)
8
+ * - Simulate game results
9
+ * - Advance rounds
10
+ * - Manage deadlines
11
+ * - Track eliminations
12
+ */
13
+
14
+ const { pool } = require('../services/db');
15
+ const { NCAA_TEAMS, CONFERENCES } = require('../data/ncaaTeams');
16
+
17
+ const REGIONS = ['SOUTH', 'WEST', 'EAST', 'MIDWEST'];
18
+ const SEEDS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
19
+
20
+ const ROUND_NAMES = {
21
+ 1: 'First Four',
22
+ 2: 'Round of 64',
23
+ 3: 'Round of 32',
24
+ 4: 'Sweet 16',
25
+ 5: 'Elite 8',
26
+ 6: 'Final Four',
27
+ 7: 'Championship'
28
+ };
29
+
30
+ /**
31
+ * Get all NCAA teams for selection
32
+ */
33
+ function getAllTeams() {
34
+ return {
35
+ teams: NCAA_TEAMS,
36
+ conferences: CONFERENCES,
37
+ count: NCAA_TEAMS.length
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Generate a random 68-team bracket
43
+ * Selects 68 random teams from the 364 D1 teams and assigns them to regions/seeds
44
+ */
45
+ async function generateRandomBracket(poolId) {
46
+ // Shuffle and pick 68 teams
47
+ const shuffled = [...NCAA_TEAMS].sort(() => Math.random() - 0.5);
48
+ const selected = shuffled.slice(0, 68);
49
+
50
+ // Assign to regions and seeds
51
+ // 16 teams per region = 64 teams + 4 First Four teams
52
+ const bracket = [];
53
+ let teamIndex = 0;
54
+
55
+ for (const region of REGIONS) {
56
+ for (const seed of SEEDS) {
57
+ if (teamIndex < 64) {
58
+ bracket.push({
59
+ team: selected[teamIndex],
60
+ region,
61
+ seed,
62
+ isFirstFour: false
63
+ });
64
+ teamIndex++;
65
+ }
66
+ }
67
+ }
68
+
69
+ // First Four teams (4 extra teams for play-in games)
70
+ // In real tournament, First Four involves 4 matchups of the lowest seeds
71
+ for (let i = 64; i < 68; i++) {
72
+ bracket.push({
73
+ team: selected[i],
74
+ region: REGIONS[i - 64],
75
+ seed: 16, // First Four are typically 16 seeds or 11/12 seeds
76
+ isFirstFour: true
77
+ });
78
+ }
79
+
80
+ // Clear existing games for this pool
81
+ await pool.query('DELETE FROM survivor_tournament_games WHERE pool_id = $1', [poolId]);
82
+
83
+ // Generate Round 1 matchups (Round of 64)
84
+ // 1 vs 16, 8 vs 9, 5 vs 12, 4 vs 13, 6 vs 11, 3 vs 14, 7 vs 10, 2 vs 15
85
+ const seedMatchups = [
86
+ [1, 16], [8, 9], [5, 12], [4, 13], [6, 11], [3, 14], [7, 10], [2, 15]
87
+ ];
88
+
89
+ const games = [];
90
+ let gameId = 1;
91
+
92
+ for (const region of REGIONS) {
93
+ const regionTeams = bracket.filter(t => t.region === region && !t.isFirstFour);
94
+
95
+ for (const [seed1, seed2] of seedMatchups) {
96
+ const team1 = regionTeams.find(t => t.seed === seed1);
97
+ const team2 = regionTeams.find(t => t.seed === seed2);
98
+
99
+ if (team1 && team2) {
100
+ games.push({
101
+ poolId,
102
+ round: 2, // Round of 64
103
+ espnGameId: `sim-r64-${region.toLowerCase()}-${gameId}`,
104
+ region,
105
+ team1Id: team1.team.id,
106
+ team1Name: team1.team.name,
107
+ team1Seed: team1.seed,
108
+ team1Logo: team1.team.logo,
109
+ team2Id: team2.team.id,
110
+ team2Name: team2.team.name,
111
+ team2Seed: team2.seed,
112
+ team2Logo: team2.team.logo,
113
+ status: 'scheduled'
114
+ });
115
+ gameId++;
116
+ }
117
+ }
118
+ }
119
+
120
+ // Insert games
121
+ for (const game of games) {
122
+ await pool.query(
123
+ `INSERT INTO survivor_tournament_games
124
+ (pool_id, round, espn_game_id, region, team1_id, team1_name, team1_seed, team1_logo,
125
+ team2_id, team2_name, team2_seed, team2_logo, status)
126
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`,
127
+ [
128
+ game.poolId, game.round, game.espnGameId, game.region,
129
+ game.team1Id, game.team1Name, game.team1Seed, game.team1Logo,
130
+ game.team2Id, game.team2Name, game.team2Seed, game.team2Logo,
131
+ game.status
132
+ ]
133
+ );
134
+ }
135
+
136
+ // Update pool status to active and set to Round 2 (Round of 64)
137
+ await pool.query(
138
+ `UPDATE survivor_pools
139
+ SET status = 'active', current_round = 2, updated_at = NOW()
140
+ WHERE id = $1`,
141
+ [poolId]
142
+ );
143
+
144
+ return {
145
+ message: 'Random bracket generated',
146
+ teamsSelected: 68,
147
+ gamesCreated: games.length,
148
+ bracket: bracket.map(b => ({
149
+ team: b.team.name,
150
+ region: b.region,
151
+ seed: b.seed
152
+ }))
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Set bracket manually (for real Selection Sunday)
158
+ * teams: Array of { teamId, region, seed }
159
+ */
160
+ async function setManualBracket(poolId, teams) {
161
+ if (teams.length !== 68 && teams.length !== 64) {
162
+ throw new Error(`Expected 64 or 68 teams, got ${teams.length}`);
163
+ }
164
+
165
+ // Validate teams exist
166
+ const teamMap = new Map(NCAA_TEAMS.map(t => [t.id, t]));
167
+ const bracket = teams.map(t => {
168
+ const team = teamMap.get(t.teamId);
169
+ if (!team) {
170
+ throw new Error(`Unknown team ID: ${t.teamId}`);
171
+ }
172
+ return {
173
+ team,
174
+ region: t.region,
175
+ seed: t.seed
176
+ };
177
+ });
178
+
179
+ // Clear existing games
180
+ await pool.query('DELETE FROM survivor_tournament_games WHERE pool_id = $1', [poolId]);
181
+
182
+ // Generate matchups (same logic as random)
183
+ const seedMatchups = [
184
+ [1, 16], [8, 9], [5, 12], [4, 13], [6, 11], [3, 14], [7, 10], [2, 15]
185
+ ];
186
+
187
+ const games = [];
188
+ let gameId = 1;
189
+
190
+ for (const region of REGIONS) {
191
+ const regionTeams = bracket.filter(t => t.region === region);
192
+
193
+ for (const [seed1, seed2] of seedMatchups) {
194
+ const team1 = regionTeams.find(t => t.seed === seed1);
195
+ const team2 = regionTeams.find(t => t.seed === seed2);
196
+
197
+ if (team1 && team2) {
198
+ games.push({
199
+ poolId,
200
+ round: 2,
201
+ espnGameId: `man-r64-${region.toLowerCase()}-${gameId}`,
202
+ region,
203
+ team1Id: team1.team.id,
204
+ team1Name: team1.team.name,
205
+ team1Seed: team1.seed,
206
+ team1Logo: team1.team.logo,
207
+ team2Id: team2.team.id,
208
+ team2Name: team2.team.name,
209
+ team2Seed: team2.seed,
210
+ team2Logo: team2.team.logo,
211
+ status: 'scheduled'
212
+ });
213
+ gameId++;
214
+ }
215
+ }
216
+ }
217
+
218
+ // Insert games
219
+ for (const game of games) {
220
+ await pool.query(
221
+ `INSERT INTO survivor_tournament_games
222
+ (pool_id, round, espn_game_id, region, team1_id, team1_name, team1_seed, team1_logo,
223
+ team2_id, team2_name, team2_seed, team2_logo, status)
224
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`,
225
+ [
226
+ game.poolId, game.round, game.espnGameId, game.region,
227
+ game.team1Id, game.team1Name, game.team1Seed, game.team1Logo,
228
+ game.team2Id, game.team2Name, game.team2Seed, game.team2Logo,
229
+ game.status
230
+ ]
231
+ );
232
+ }
233
+
234
+ // Update pool status
235
+ await pool.query(
236
+ `UPDATE survivor_pools
237
+ SET status = 'active', current_round = 2, updated_at = NOW()
238
+ WHERE id = $1`,
239
+ [poolId]
240
+ );
241
+
242
+ return {
243
+ message: 'Manual bracket set',
244
+ teamsSelected: teams.length,
245
+ gamesCreated: games.length
246
+ };
247
+ }
248
+
249
+ /**
250
+ * Set round deadline
251
+ * minutes: Number of minutes from now
252
+ */
253
+ async function setDeadline(poolId, minutes) {
254
+ const deadline = new Date(Date.now() + minutes * 60 * 1000);
255
+
256
+ await pool.query(
257
+ `UPDATE survivor_pools
258
+ SET round_deadline = $2, updated_at = NOW()
259
+ WHERE id = $1`,
260
+ [poolId, deadline]
261
+ );
262
+
263
+ return {
264
+ message: `Deadline set to ${minutes} minutes from now`,
265
+ deadline: deadline.toISOString(),
266
+ deadlineLocal: deadline.toLocaleString()
267
+ };
268
+ }
269
+
270
+ /**
271
+ * Clear deadline (no deadline)
272
+ */
273
+ async function clearDeadline(poolId) {
274
+ await pool.query(
275
+ `UPDATE survivor_pools
276
+ SET round_deadline = NULL, updated_at = NOW()
277
+ WHERE id = $1`,
278
+ [poolId]
279
+ );
280
+
281
+ return { message: 'Deadline cleared' };
282
+ }
283
+
284
+ /**
285
+ * Simulate current round results
286
+ * mode: 'chalk' (favorites win), 'random' (50/50), 'chaos' (upsets likely), 'manual'
287
+ */
288
+ async function simulateRound(poolId, mode = 'random', manualResults = null) {
289
+ // Get current round
290
+ const poolResult = await pool.query(
291
+ 'SELECT current_round FROM survivor_pools WHERE id = $1',
292
+ [poolId]
293
+ );
294
+
295
+ if (poolResult.rows.length === 0) {
296
+ throw new Error('Pool not found');
297
+ }
298
+
299
+ const currentRound = poolResult.rows[0].current_round;
300
+
301
+ // Get games for current round
302
+ const gamesResult = await pool.query(
303
+ `SELECT * FROM survivor_tournament_games
304
+ WHERE pool_id = $1 AND round = $2 AND winner_id IS NULL
305
+ ORDER BY id`,
306
+ [poolId, currentRound]
307
+ );
308
+
309
+ const games = gamesResult.rows;
310
+
311
+ if (games.length === 0) {
312
+ throw new Error('No unresolved games in current round');
313
+ }
314
+
315
+ const results = [];
316
+
317
+ for (const game of games) {
318
+ let winnerId;
319
+ let winnerName;
320
+
321
+ if (mode === 'manual' && manualResults) {
322
+ // Use provided results
323
+ const result = manualResults.find(r => r.gameId === game.id || r.espnGameId === game.espn_game_id);
324
+ if (result) {
325
+ winnerId = result.winnerId;
326
+ winnerName = winnerId === game.team1_id ? game.team1_name : game.team2_name;
327
+ } else {
328
+ continue; // Skip if no manual result provided
329
+ }
330
+ } else if (mode === 'chalk') {
331
+ // Lower seed (favorite) wins
332
+ winnerId = game.team1_seed <= game.team2_seed ? game.team1_id : game.team2_id;
333
+ winnerName = game.team1_seed <= game.team2_seed ? game.team1_name : game.team2_name;
334
+ } else if (mode === 'chaos') {
335
+ // 70% chance of upset
336
+ const upset = Math.random() < 0.7;
337
+ if (upset) {
338
+ winnerId = game.team1_seed > game.team2_seed ? game.team1_id : game.team2_id;
339
+ winnerName = game.team1_seed > game.team2_seed ? game.team1_name : game.team2_name;
340
+ } else {
341
+ winnerId = game.team1_seed <= game.team2_seed ? game.team1_id : game.team2_id;
342
+ winnerName = game.team1_seed <= game.team2_seed ? game.team1_name : game.team2_name;
343
+ }
344
+ } else {
345
+ // Random 50/50
346
+ const team1Wins = Math.random() < 0.5;
347
+ winnerId = team1Wins ? game.team1_id : game.team2_id;
348
+ winnerName = team1Wins ? game.team1_name : game.team2_name;
349
+ }
350
+
351
+ // Update game with winner
352
+ await pool.query(
353
+ `UPDATE survivor_tournament_games
354
+ SET winner_id = $2, status = 'final', updated_at = NOW()
355
+ WHERE id = $1`,
356
+ [game.id, winnerId]
357
+ );
358
+
359
+ results.push({
360
+ gameId: game.id,
361
+ matchup: `${game.team1_name} (${game.team1_seed}) vs ${game.team2_name} (${game.team2_seed})`,
362
+ winner: winnerName,
363
+ upset: (winnerId === game.team1_id && game.team1_seed > game.team2_seed) ||
364
+ (winnerId === game.team2_id && game.team2_seed > game.team1_seed)
365
+ });
366
+ }
367
+
368
+ return {
369
+ message: `Simulated ${results.length} games with mode: ${mode}`,
370
+ round: currentRound,
371
+ roundName: ROUND_NAMES[currentRound],
372
+ results,
373
+ upsets: results.filter(r => r.upset).length
374
+ };
375
+ }
376
+
377
+ /**
378
+ * Process eliminations for current round
379
+ * Checks all survivor picks and eliminates those who picked losing teams
380
+ */
381
+ async function processEliminations(poolId) {
382
+ // Get current round
383
+ const poolResult = await pool.query(
384
+ 'SELECT current_round FROM survivor_pools WHERE id = $1',
385
+ [poolId]
386
+ );
387
+
388
+ const currentRound = poolResult.rows[0].current_round;
389
+
390
+ // Get all games for current round that are final
391
+ const gamesResult = await pool.query(
392
+ `SELECT * FROM survivor_tournament_games
393
+ WHERE pool_id = $1 AND round = $2 AND status = 'final'`,
394
+ [poolId, currentRound]
395
+ );
396
+
397
+ const games = gamesResult.rows;
398
+ const winnerIds = new Set(games.map(g => g.winner_id));
399
+ const loserIds = new Set();
400
+
401
+ games.forEach(g => {
402
+ if (g.winner_id === g.team1_id) {
403
+ loserIds.add(g.team2_id);
404
+ } else {
405
+ loserIds.add(g.team1_id);
406
+ }
407
+ });
408
+
409
+ // Update survivor picks for this round
410
+ // Mark picks as 'won' or 'lost'
411
+ await pool.query(
412
+ `UPDATE survivor_picks sp
413
+ SET result = 'won', updated_at = NOW()
414
+ FROM survivor_entries se
415
+ WHERE sp.entry_id = se.id
416
+ AND se.pool_id = $1
417
+ AND sp.round = $2
418
+ AND sp.team_id = ANY($3::text[])`,
419
+ [poolId, currentRound, Array.from(winnerIds)]
420
+ );
421
+
422
+ await pool.query(
423
+ `UPDATE survivor_picks sp
424
+ SET result = 'lost', updated_at = NOW()
425
+ FROM survivor_entries se
426
+ WHERE sp.entry_id = se.id
427
+ AND se.pool_id = $1
428
+ AND sp.round = $2
429
+ AND sp.team_id = ANY($3::text[])`,
430
+ [poolId, currentRound, Array.from(loserIds)]
431
+ );
432
+
433
+ // Eliminate entries whose picks lost
434
+ const eliminatedResult = await pool.query(
435
+ `UPDATE survivor_entries se
436
+ SET is_alive = false, eliminated_at_round = $2, updated_at = NOW()
437
+ FROM survivor_picks sp
438
+ WHERE se.id = sp.entry_id
439
+ AND se.pool_id = $1
440
+ AND sp.round = $2
441
+ AND sp.result = 'lost'
442
+ AND se.is_alive = true
443
+ RETURNING se.id, se.user_id`,
444
+ [poolId, currentRound]
445
+ );
446
+
447
+ // Also eliminate entries that didn't make a pick (no-pick elimination)
448
+ // First, let's see who is alive and didn't pick
449
+ const checkNoPickers = await pool.query(
450
+ `SELECT se.id, se.user_id, u.username
451
+ FROM survivor_entries se
452
+ LEFT JOIN users u ON u.id = se.user_id
453
+ WHERE se.pool_id = $1
454
+ AND se.is_alive = true
455
+ AND NOT EXISTS (
456
+ SELECT 1 FROM survivor_picks sp
457
+ WHERE sp.entry_id = se.id AND sp.round = $2
458
+ )`,
459
+ [poolId, currentRound]
460
+ );
461
+
462
+ console.log(`[Survivor] Round ${currentRound}: ${checkNoPickers.rows.length} alive entries without picks:`,
463
+ checkNoPickers.rows.map(r => ({ entryId: r.id, userId: r.user_id, username: r.username })));
464
+
465
+ const noPickers = await pool.query(
466
+ `UPDATE survivor_entries se
467
+ SET is_alive = false, eliminated_at_round = $2, updated_at = NOW()
468
+ WHERE se.pool_id = $1
469
+ AND se.is_alive = true
470
+ AND NOT EXISTS (
471
+ SELECT 1 FROM survivor_picks sp
472
+ WHERE sp.entry_id = se.id AND sp.round = $2
473
+ )
474
+ RETURNING se.id, se.user_id`,
475
+ [poolId, currentRound]
476
+ );
477
+
478
+ console.log(`[Survivor] Round ${currentRound}: Eliminated ${noPickers.rowCount} entries for no-pick`);
479
+
480
+ // Get counts
481
+ const countResult = await pool.query(
482
+ `SELECT
483
+ (SELECT COUNT(*) FROM survivor_entries WHERE pool_id = $1 AND is_alive = true) as alive,
484
+ (SELECT COUNT(*) FROM survivor_entries WHERE pool_id = $1 AND is_alive = false) as eliminated,
485
+ (SELECT COUNT(*) FROM survivor_entries WHERE pool_id = $1) as total`,
486
+ [poolId]
487
+ );
488
+
489
+ const remainingAlive = parseInt(countResult.rows[0].alive);
490
+ const totalEntries = parseInt(countResult.rows[0].total);
491
+
492
+ // Check if everyone is eliminated - pool goes to house
493
+ if (remainingAlive === 0 && totalEntries > 0) {
494
+ await pool.query(
495
+ `UPDATE survivor_pools
496
+ SET status = 'complete',
497
+ winner_type = 'house',
498
+ completed_at = NOW(),
499
+ updated_at = NOW()
500
+ WHERE id = $1`,
501
+ [poolId]
502
+ );
503
+
504
+ return {
505
+ message: 'ALL PLAYERS ELIMINATED - Pool goes to house!',
506
+ round: currentRound,
507
+ roundName: ROUND_NAMES[currentRound],
508
+ eliminatedByLoss: eliminatedResult.rowCount,
509
+ eliminatedByNoPick: noPickers.rowCount,
510
+ totalEliminated: eliminatedResult.rowCount + noPickers.rowCount,
511
+ remainingAlive: 0,
512
+ allEliminated: true,
513
+ outcome: 'house_wins',
514
+ note: 'No survivors remain. The entire pot goes to the house.'
515
+ };
516
+ }
517
+
518
+ return {
519
+ message: 'Eliminations processed',
520
+ round: currentRound,
521
+ roundName: ROUND_NAMES[currentRound],
522
+ eliminatedByLoss: eliminatedResult.rowCount,
523
+ eliminatedByNoPick: noPickers.rowCount,
524
+ totalEliminated: eliminatedResult.rowCount + noPickers.rowCount,
525
+ remainingAlive,
526
+ allEliminated: false
527
+ };
528
+ }
529
+
530
+ /**
531
+ * Advance to next round
532
+ * Creates matchups for next round from winners
533
+ */
534
+ async function advanceRound(poolId) {
535
+ // Get current round
536
+ const poolResult = await pool.query(
537
+ 'SELECT current_round FROM survivor_pools WHERE id = $1',
538
+ [poolId]
539
+ );
540
+
541
+ const currentRound = poolResult.rows[0].current_round;
542
+ const nextRound = currentRound + 1;
543
+
544
+ if (nextRound > 7) {
545
+ // Tournament complete - survivors win!
546
+ // Get count of remaining survivors
547
+ const survivorCount = await pool.query(
548
+ `SELECT COUNT(*) as count FROM survivor_entries WHERE pool_id = $1 AND is_alive = true`,
549
+ [poolId]
550
+ );
551
+ const numSurvivors = parseInt(survivorCount.rows[0].count);
552
+
553
+ console.log(`[Survivor] Tournament complete! ${numSurvivors} survivors remaining`);
554
+
555
+ await pool.query(
556
+ `UPDATE survivor_pools
557
+ SET status = 'complete',
558
+ winner_type = 'survivors',
559
+ completed_at = NOW(),
560
+ updated_at = NOW()
561
+ WHERE id = $1`,
562
+ [poolId]
563
+ );
564
+
565
+ return {
566
+ message: `Tournament complete! ${numSurvivors} survivor(s) win!`,
567
+ status: 'complete',
568
+ winner_type: 'survivors',
569
+ survivorCount: numSurvivors
570
+ };
571
+ }
572
+
573
+ // Check if there are any games in current round
574
+ const currentGamesResult = await pool.query(
575
+ `SELECT COUNT(*) as total, COUNT(*) FILTER (WHERE status = 'final') as resolved
576
+ FROM survivor_tournament_games WHERE pool_id = $1 AND round = $2`,
577
+ [poolId, currentRound]
578
+ );
579
+
580
+ const totalGames = parseInt(currentGamesResult.rows[0].total);
581
+ const resolvedGames = parseInt(currentGamesResult.rows[0].resolved);
582
+
583
+ if (totalGames === 0) {
584
+ throw new Error(`No games exist for current round (${ROUND_NAMES[currentRound]}). Seed the bracket first.`);
585
+ }
586
+
587
+ if (resolvedGames === 0) {
588
+ throw new Error(`No games have been resolved in ${ROUND_NAMES[currentRound]}. Simulate the round first.`);
589
+ }
590
+
591
+ if (resolvedGames < totalGames) {
592
+ throw new Error(`Only ${resolvedGames}/${totalGames} games resolved in ${ROUND_NAMES[currentRound]}. Resolve all games before advancing.`);
593
+ }
594
+
595
+ // Get winners from current round
596
+ const winnersResult = await pool.query(
597
+ `SELECT * FROM survivor_tournament_games
598
+ WHERE pool_id = $1 AND round = $2 AND status = 'final'
599
+ ORDER BY region, id`,
600
+ [poolId, currentRound]
601
+ );
602
+
603
+ const winners = winnersResult.rows.map(g => ({
604
+ teamId: g.winner_id,
605
+ teamName: g.winner_id === g.team1_id ? g.team1_name : g.team2_name,
606
+ teamSeed: g.winner_id === g.team1_id ? g.team1_seed : g.team2_seed,
607
+ teamLogo: g.winner_id === g.team1_id ? g.team1_logo : g.team2_logo,
608
+ region: g.region
609
+ }));
610
+
611
+ // Create next round matchups
612
+ const games = [];
613
+
614
+ if (nextRound <= 5) {
615
+ // Regional rounds (through Elite 8)
616
+ for (const region of REGIONS) {
617
+ const regionWinners = winners.filter(w => w.region === region);
618
+
619
+ // Pair winners
620
+ for (let i = 0; i < regionWinners.length; i += 2) {
621
+ if (regionWinners[i + 1]) {
622
+ games.push({
623
+ poolId,
624
+ round: nextRound,
625
+ espnGameId: `sim-r${nextRound}-${region.toLowerCase()}-${i / 2 + 1}`,
626
+ region,
627
+ team1: regionWinners[i],
628
+ team2: regionWinners[i + 1]
629
+ });
630
+ }
631
+ }
632
+ }
633
+ } else if (nextRound === 6) {
634
+ // Final Four
635
+ // South vs East, West vs Midwest
636
+ const pairings = [
637
+ ['SOUTH', 'EAST'],
638
+ ['WEST', 'MIDWEST']
639
+ ];
640
+
641
+ console.log(`[SurvivorAdmin] Creating Final Four. Winners from Elite 8:`, winners.map(w => ({ team: w.teamName, region: w.region })));
642
+
643
+ for (let i = 0; i < pairings.length; i++) {
644
+ const team1 = winners.find(w => w.region === pairings[i][0]);
645
+ const team2 = winners.find(w => w.region === pairings[i][1]);
646
+
647
+ console.log(`[SurvivorAdmin] Final Four pairing ${i + 1}: ${pairings[i][0]} vs ${pairings[i][1]}`);
648
+ console.log(`[SurvivorAdmin] Team1 (${pairings[i][0]}):`, team1 ? team1.teamName : 'NOT FOUND');
649
+ console.log(`[SurvivorAdmin] Team2 (${pairings[i][1]}):`, team2 ? team2.teamName : 'NOT FOUND');
650
+
651
+ if (team1 && team2) {
652
+ games.push({
653
+ poolId,
654
+ round: nextRound,
655
+ espnGameId: `sim-ff-${i + 1}`,
656
+ region: 'FINAL_FOUR',
657
+ team1,
658
+ team2
659
+ });
660
+ } else {
661
+ console.warn(`[SurvivorAdmin] Could not create Final Four game - missing team(s)`);
662
+ }
663
+ }
664
+ } else if (nextRound === 7) {
665
+ // Championship
666
+ if (winners.length === 2) {
667
+ games.push({
668
+ poolId,
669
+ round: nextRound,
670
+ espnGameId: 'sim-champ',
671
+ region: 'CHAMPIONSHIP',
672
+ team1: winners[0],
673
+ team2: winners[1]
674
+ });
675
+ }
676
+ }
677
+
678
+ // Validate we have games to create
679
+ if (games.length === 0) {
680
+ throw new Error(`Failed to create matchups for ${ROUND_NAMES[nextRound]}. No valid winner pairings found.`);
681
+ }
682
+
683
+ // Insert new games
684
+ for (const game of games) {
685
+ await pool.query(
686
+ `INSERT INTO survivor_tournament_games
687
+ (pool_id, round, espn_game_id, region, team1_id, team1_name, team1_seed, team1_logo,
688
+ team2_id, team2_name, team2_seed, team2_logo, status)
689
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, 'scheduled')`,
690
+ [
691
+ game.poolId, game.round, game.espnGameId, game.region,
692
+ game.team1.teamId, game.team1.teamName, game.team1.teamSeed, game.team1.teamLogo,
693
+ game.team2.teamId, game.team2.teamName, game.team2.teamSeed, game.team2.teamLogo
694
+ ]
695
+ );
696
+ }
697
+
698
+ // Update pool to next round and clear deadline
699
+ await pool.query(
700
+ `UPDATE survivor_pools
701
+ SET current_round = $2, round_deadline = NULL, updated_at = NOW()
702
+ WHERE id = $1`,
703
+ [poolId, nextRound]
704
+ );
705
+
706
+ return {
707
+ message: `Advanced to ${ROUND_NAMES[nextRound]}`,
708
+ previousRound: currentRound,
709
+ newRound: nextRound,
710
+ roundName: ROUND_NAMES[nextRound],
711
+ gamesCreated: games.length,
712
+ matchups: games.map(g => `${g.team1.teamName} vs ${g.team2.teamName}`)
713
+ };
714
+ }
715
+
716
+ /**
717
+ * Get pool admin dashboard data
718
+ */
719
+ async function getPoolDashboard(poolId) {
720
+ // Pool info
721
+ const poolResult = await pool.query(
722
+ `SELECT * FROM survivor_pools WHERE id = $1`,
723
+ [poolId]
724
+ );
725
+
726
+ if (poolResult.rows.length === 0) {
727
+ throw new Error('Pool not found');
728
+ }
729
+
730
+ const survivorPool = poolResult.rows[0];
731
+
732
+ // Entry counts
733
+ const countsResult = await pool.query(
734
+ `SELECT
735
+ COUNT(*) as total,
736
+ COUNT(*) FILTER (WHERE is_alive = true) as alive,
737
+ COUNT(*) FILTER (WHERE is_alive = false) as eliminated
738
+ FROM survivor_entries WHERE pool_id = $1`,
739
+ [poolId]
740
+ );
741
+
742
+ // Check if bracket exists (any games at all)
743
+ const bracketExistsResult = await pool.query(
744
+ `SELECT COUNT(*) as count FROM survivor_tournament_games WHERE pool_id = $1`,
745
+ [poolId]
746
+ );
747
+ const hasBracket = parseInt(bracketExistsResult.rows[0].count) > 0;
748
+
749
+ // Games for current round
750
+ const gamesResult = await pool.query(
751
+ `SELECT * FROM survivor_tournament_games
752
+ WHERE pool_id = $1 AND round = $2
753
+ ORDER BY region, id`,
754
+ [poolId, survivorPool.current_round]
755
+ );
756
+
757
+ // Picks for current round
758
+ const picksResult = await pool.query(
759
+ `SELECT sp.team_id, sp.team_name, COUNT(*) as pick_count
760
+ FROM survivor_picks sp
761
+ JOIN survivor_entries se ON se.id = sp.entry_id
762
+ WHERE se.pool_id = $1 AND sp.round = $2
763
+ GROUP BY sp.team_id, sp.team_name
764
+ ORDER BY pick_count DESC`,
765
+ [poolId, survivorPool.current_round]
766
+ );
767
+
768
+ // Entries without picks for current round
769
+ const noPicks = await pool.query(
770
+ `SELECT COUNT(*) as count
771
+ FROM survivor_entries se
772
+ WHERE se.pool_id = $1
773
+ AND se.is_alive = true
774
+ AND NOT EXISTS (
775
+ SELECT 1 FROM survivor_picks sp
776
+ WHERE sp.entry_id = se.id AND sp.round = $2
777
+ )`,
778
+ [poolId, survivorPool.current_round]
779
+ );
780
+
781
+ const totalEntries = parseInt(countsResult.rows[0].total);
782
+ const aliveEntries = parseInt(countsResult.rows[0].alive);
783
+ const eliminatedEntries = parseInt(countsResult.rows[0].eliminated);
784
+
785
+ // Determine pool outcome state
786
+ let outcomeState = null;
787
+ if (survivorPool.status === 'complete') {
788
+ if (aliveEntries === 0 && totalEntries > 0) {
789
+ outcomeState = 'house_wins';
790
+ } else if (aliveEntries > 0) {
791
+ outcomeState = 'survivors_win';
792
+ }
793
+ } else if (aliveEntries === 0 && totalEntries > 0 && hasBracket) {
794
+ // Pool should be marked complete but hasn't been yet
795
+ outcomeState = 'pending_house_wins';
796
+ }
797
+
798
+ return {
799
+ pool: {
800
+ id: survivorPool.id,
801
+ name: survivorPool.name,
802
+ status: survivorPool.status,
803
+ currentRound: survivorPool.current_round,
804
+ roundName: hasBracket ? ROUND_NAMES[survivorPool.current_round] : 'Pre-Tournament',
805
+ hasBracket,
806
+ deadline: survivorPool.round_deadline,
807
+ deadlinePassed: survivorPool.round_deadline ? new Date() > new Date(survivorPool.round_deadline) : false,
808
+ winnerType: survivorPool.winner_type || null,
809
+ completedAt: survivorPool.completed_at || null
810
+ },
811
+ entries: {
812
+ total: totalEntries,
813
+ alive: aliveEntries,
814
+ eliminated: eliminatedEntries,
815
+ noPick: parseInt(noPicks.rows[0].count)
816
+ },
817
+ outcomeState,
818
+ currentRoundGames: gamesResult.rows.map(g => ({
819
+ id: g.id,
820
+ region: g.region,
821
+ team1: { id: g.team1_id, name: g.team1_name, seed: g.team1_seed, logo: g.team1_logo },
822
+ team2: { id: g.team2_id, name: g.team2_name, seed: g.team2_seed, logo: g.team2_logo },
823
+ winner: g.winner_id,
824
+ status: g.status
825
+ })),
826
+ pickDistribution: picksResult.rows
827
+ };
828
+ }
829
+
830
+ /**
831
+ * Reset pool to initial state
832
+ */
833
+ async function resetPool(poolId, options = {}) {
834
+ const { deleteEntries = false } = options;
835
+
836
+ // Delete all games
837
+ await pool.query('DELETE FROM survivor_tournament_games WHERE pool_id = $1', [poolId]);
838
+
839
+ // Delete all picks
840
+ await pool.query(
841
+ `DELETE FROM survivor_picks sp
842
+ USING survivor_entries se
843
+ WHERE sp.entry_id = se.id AND se.pool_id = $1`,
844
+ [poolId]
845
+ );
846
+
847
+ if (deleteEntries) {
848
+ // Full reset: delete all entries
849
+ await pool.query('DELETE FROM survivor_entries WHERE pool_id = $1', [poolId]);
850
+ } else {
851
+ // Soft reset: just reset entries to alive
852
+ await pool.query(
853
+ `UPDATE survivor_entries
854
+ SET is_alive = true, eliminated_at_round = NULL, updated_at = NOW()
855
+ WHERE pool_id = $1`,
856
+ [poolId]
857
+ );
858
+ }
859
+
860
+ // Reset pool
861
+ await pool.query(
862
+ `UPDATE survivor_pools
863
+ SET status = 'open', current_round = 1, round_deadline = NULL, updated_at = NOW()
864
+ WHERE id = $1`,
865
+ [poolId]
866
+ );
867
+
868
+ const message = deleteEntries
869
+ ? 'Pool fully reset (all entries deleted)'
870
+ : 'Pool reset to initial state (entries kept)';
871
+
872
+ return { message, entriesDeleted: deleteEntries };
873
+ }
874
+
875
+ module.exports = {
876
+ getAllTeams,
877
+ generateRandomBracket,
878
+ setManualBracket,
879
+ setDeadline,
880
+ clearDeadline,
881
+ simulateRound,
882
+ processEliminations,
883
+ advanceRound,
884
+ getPoolDashboard,
885
+ resetPool,
886
+ ROUND_NAMES
887
+ };