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,278 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Regenerate EPL Matchup Images Script
4
+ *
5
+ * Regenerates ALL EPL matchup images in S3 with the latest team name overrides.
6
+ * Forces overwrite of existing images and updates database URLs with cache-busting.
7
+ *
8
+ * Usage:
9
+ * node scripts/regenerate-epl-images.js # Process all EPL games
10
+ * node scripts/regenerate-epl-images.js --dry-run # Preview without making changes
11
+ * node scripts/regenerate-epl-images.js --limit 10 # Process only 10 games
12
+ * node scripts/regenerate-epl-images.js --game-id sport-xxx # Process specific game
13
+ */
14
+
15
+ require('dotenv').config();
16
+ const { Pool } = require('pg');
17
+
18
+ // Lazy load services
19
+ let matchupImageService = null;
20
+ let s3Service = null;
21
+
22
+ function getMatchupImageService() {
23
+ if (!matchupImageService) {
24
+ try {
25
+ matchupImageService = require('../services/matchupImageService');
26
+ console.log('βœ… Matchup image service loaded');
27
+ } catch (err) {
28
+ console.error('❌ Failed to load matchup image service:', err.message);
29
+ console.error(' Install canvas: npm install canvas');
30
+ process.exit(1);
31
+ }
32
+ }
33
+ return matchupImageService;
34
+ }
35
+
36
+ function getS3Service() {
37
+ if (!s3Service) {
38
+ try {
39
+ const S3Service = require('../services/s3Service');
40
+ s3Service = new S3Service();
41
+ if (!s3Service.isConfigured()) {
42
+ console.error('❌ S3 credentials not configured');
43
+ console.error(' Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in .env');
44
+ process.exit(1);
45
+ }
46
+ console.log('βœ… S3 service loaded');
47
+ } catch (err) {
48
+ console.error('❌ Failed to load S3 service:', err.message);
49
+ process.exit(1);
50
+ }
51
+ }
52
+ return s3Service;
53
+ }
54
+
55
+ // Database connection
56
+ const pool = new Pool({
57
+ connectionString: process.env.DATABASE_URL,
58
+ ssl: process.env.DATABASE_URL?.includes('amazonaws') || process.env.DATABASE_URL?.includes('heroku')
59
+ ? { rejectUnauthorized: false }
60
+ : false
61
+ });
62
+
63
+ // Track which team combos we've already processed (to avoid regenerating same image multiple times)
64
+ const processedCombos = new Set();
65
+
66
+ async function processGame(game, dryRun = false) {
67
+ const service = getMatchupImageService();
68
+ const s3 = getS3Service();
69
+
70
+ const sportsEvent = game.sports_event;
71
+ const homeTeam = sportsEvent?.strHomeTeam;
72
+ const awayTeam = sportsEvent?.strAwayTeam;
73
+ const league = 'EPL'; // Force EPL
74
+
75
+ if (!homeTeam || !awayTeam) {
76
+ console.log(` ⚠️ Skipping ${game.game_id} - missing team names`);
77
+ return { skipped: true };
78
+ }
79
+
80
+ // Create a combo key to avoid regenerating the same image
81
+ const comboKey = s3.getMatchupImageKey(awayTeam, homeTeam, league);
82
+ const alreadyProcessed = processedCombos.has(comboKey);
83
+
84
+ console.log(` πŸ“ ${homeTeam} vs ${awayTeam}`);
85
+
86
+ if (dryRun) {
87
+ console.log(` πŸ” DRY RUN - Would regenerate image for ${comboKey}`);
88
+ return { success: true, dryRun: true };
89
+ }
90
+
91
+ try {
92
+ let publicUrl;
93
+
94
+ if (alreadyProcessed) {
95
+ // Image already regenerated in this run, just get the URL
96
+ const cacheBuster = Date.now();
97
+ publicUrl = `https://${s3.bucketName}.s3.${s3.region}.amazonaws.com/${comboKey}?v=${cacheBuster}`;
98
+ console.log(` ♻️ Reusing already regenerated image`);
99
+ } else {
100
+ // Generate new image
101
+ const result = await service.generateMatchupImage({
102
+ homeTeam,
103
+ awayTeam,
104
+ league,
105
+ width: 600,
106
+ height: 315
107
+ });
108
+
109
+ // Upload to S3 with forceOverwrite = true
110
+ const uploadResult = await s3.uploadMatchupImage(awayTeam, homeTeam, league, result.buffer, true);
111
+ publicUrl = uploadResult.publicUrl;
112
+
113
+ // Mark this combo as processed
114
+ processedCombos.add(comboKey);
115
+
116
+ console.log(` πŸ”„ Regenerated: ${comboKey}`);
117
+ }
118
+
119
+ // Update database with new URL (includes cache-buster)
120
+ await pool.query(`
121
+ UPDATE games
122
+ SET matchup_image_url = $1, updated_at = NOW()
123
+ WHERE game_id = $2
124
+ `, [publicUrl, game.game_id]);
125
+
126
+ console.log(` βœ… Updated: ${game.game_id}`);
127
+ return { success: true, url: publicUrl, regenerated: !alreadyProcessed };
128
+
129
+ } catch (err) {
130
+ console.error(` ❌ Failed: ${err.message}`);
131
+ return { error: err.message };
132
+ }
133
+ }
134
+
135
+ async function main() {
136
+ console.log('');
137
+ console.log('🎨 EPL Matchup Image Regeneration Script');
138
+ console.log('=========================================');
139
+ console.log('');
140
+
141
+ // Parse arguments
142
+ const args = process.argv.slice(2);
143
+ let limit = null;
144
+ let gameId = null;
145
+ let dryRun = false;
146
+
147
+ for (let i = 0; i < args.length; i++) {
148
+ if (args[i] === '--limit' && args[i + 1]) {
149
+ limit = parseInt(args[i + 1], 10);
150
+ }
151
+ if (args[i] === '--game-id' && args[i + 1]) {
152
+ gameId = args[i + 1];
153
+ }
154
+ if (args[i] === '--dry-run') {
155
+ dryRun = true;
156
+ }
157
+ }
158
+
159
+ if (dryRun) {
160
+ console.log('πŸ” DRY RUN MODE - No changes will be made');
161
+ console.log('');
162
+ }
163
+
164
+ // Initialize services
165
+ getMatchupImageService();
166
+ if (!dryRun) {
167
+ getS3Service();
168
+ }
169
+
170
+ try {
171
+ // Query for EPL games
172
+ let query;
173
+ let params;
174
+
175
+ if (gameId) {
176
+ query = `
177
+ SELECT game_id, sports_event
178
+ FROM games
179
+ WHERE game_id = $1 AND game_mode = 4
180
+ `;
181
+ params = [gameId];
182
+ } else {
183
+ // Get ALL EPL games (including those with existing matchup images)
184
+ query = `
185
+ SELECT game_id, sports_event
186
+ FROM games
187
+ WHERE game_mode = 4
188
+ AND sports_event IS NOT NULL
189
+ AND (
190
+ sports_event->>'strLeague' ILIKE '%premier%'
191
+ OR sports_event->>'strLeague' = 'EPL'
192
+ )
193
+ ORDER BY created_at DESC
194
+ ${limit ? `LIMIT ${limit}` : ''}
195
+ `;
196
+ params = [];
197
+ }
198
+
199
+ const result = await pool.query(query, params);
200
+ const games = result.rows;
201
+
202
+ console.log(`πŸ“Š Found ${games.length} EPL games to process`);
203
+ console.log('');
204
+
205
+ if (games.length === 0) {
206
+ console.log('βœ… No EPL games found!');
207
+ process.exit(0);
208
+ }
209
+
210
+ // Collect unique team combinations
211
+ const uniqueCombos = new Set();
212
+ for (const game of games) {
213
+ const s3 = dryRun ? null : getS3Service();
214
+ const sportsEvent = game.sports_event;
215
+ if (sportsEvent?.strHomeTeam && sportsEvent?.strAwayTeam) {
216
+ // Use a simple key for counting
217
+ const key = `${sportsEvent.strAwayTeam}_${sportsEvent.strHomeTeam}`;
218
+ uniqueCombos.add(key);
219
+ }
220
+ }
221
+ console.log(`πŸ”’ Unique team combinations: ${uniqueCombos.size}`);
222
+ console.log('');
223
+
224
+ // Process games
225
+ let processed = 0;
226
+ let succeeded = 0;
227
+ let failed = 0;
228
+ let skipped = 0;
229
+ let regenerated = 0;
230
+
231
+ for (const game of games) {
232
+ console.log(`[${processed + 1}/${games.length}] ${game.game_id}`);
233
+
234
+ const result = await processGame(game, dryRun);
235
+ processed++;
236
+
237
+ if (result.success) {
238
+ succeeded++;
239
+ if (result.regenerated) regenerated++;
240
+ }
241
+ else if (result.skipped) skipped++;
242
+ else failed++;
243
+
244
+ // Small delay to avoid rate limiting
245
+ if (!dryRun) {
246
+ await new Promise(r => setTimeout(r, 100));
247
+ }
248
+ }
249
+
250
+ console.log('');
251
+ console.log('πŸ“Š Summary');
252
+ console.log('==========');
253
+ console.log(` Total processed: ${processed}`);
254
+ console.log(` βœ… Succeeded: ${succeeded}`);
255
+ console.log(` πŸ”„ Regenerated: ${regenerated} unique images`);
256
+ console.log(` ⚠️ Skipped: ${skipped}`);
257
+ console.log(` ❌ Failed: ${failed}`);
258
+ console.log('');
259
+
260
+ if (dryRun) {
261
+ console.log('πŸ” DRY RUN complete - no changes were made');
262
+ console.log(' Run without --dry-run to apply changes');
263
+ }
264
+
265
+ process.exit(failed > 0 ? 1 : 0);
266
+
267
+ } catch (err) {
268
+ console.error('❌ Fatal error:', err);
269
+ process.exit(1);
270
+ } finally {
271
+ await pool.end();
272
+ }
273
+ }
274
+
275
+ main().catch(err => {
276
+ console.error('❌ Unhandled error:', err);
277
+ process.exit(1);
278
+ });
@@ -0,0 +1,374 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Resize S3 Matchup Images Script
4
+ *
5
+ * Converts existing PNG matchup images on S3 to JPEG for better compression.
6
+ * Resizes images larger than 600x315 down to 600x315 (maintains full quality).
7
+ * Images already at or below 600x315 are just converted to JPEG without resizing.
8
+ * Processes both us-east-1 (dev) and us-east-2 (prod) buckets.
9
+ *
10
+ * Usage:
11
+ * node scripts/resize-s3-matchup-images.js # Dry run (preview only)
12
+ * node scripts/resize-s3-matchup-images.js --execute # Actually convert and upload
13
+ * node scripts/resize-s3-matchup-images.js --limit 5 # Process only 5 images per bucket
14
+ * node scripts/resize-s3-matchup-images.js --execute --limit 10 # Convert 10 images per bucket
15
+ */
16
+
17
+ require('dotenv').config();
18
+ const { S3Client, ListObjectsV2Command, GetObjectCommand, PutObjectCommand } = require('@aws-sdk/client-s3');
19
+ const sharp = require('sharp');
20
+
21
+ // Configuration - processes both buckets
22
+ const BUCKETS = [
23
+ { name: 'dubs-avatars-dev', region: 'us-east-1' },
24
+ { name: 'dubs-avatars-prod', region: 'us-east-2' },
25
+ ];
26
+ const MATCHUPS_PREFIX = 'matchups/';
27
+
28
+ // Target size for optimization (images larger than this will be resized down)
29
+ const TARGET_WIDTH = 600;
30
+ const TARGET_HEIGHT = 315;
31
+
32
+ // Parse command line arguments
33
+ const args = process.argv.slice(2);
34
+ const EXECUTE_MODE = args.includes('--execute');
35
+ const limitIndex = args.indexOf('--limit');
36
+ const LIMIT = limitIndex !== -1 ? parseInt(args[limitIndex + 1], 10) : null;
37
+
38
+ // Initialize S3 client
39
+ function getS3Client(region) {
40
+ if (!process.env.AWS_ACCESS_KEY_ID || !process.env.AWS_SECRET_ACCESS_KEY) {
41
+ console.error('❌ AWS credentials not set');
42
+ console.error(' Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in .env');
43
+ process.exit(1);
44
+ }
45
+
46
+ return new S3Client({
47
+ region: region,
48
+ credentials: {
49
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID,
50
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
51
+ },
52
+ });
53
+ }
54
+
55
+ /**
56
+ * List all matchup images in the bucket
57
+ */
58
+ async function listMatchupImages(s3Client, bucketName) {
59
+ const images = [];
60
+ let continuationToken = null;
61
+
62
+ console.log(`πŸ“‚ Listing images in s3://${bucketName}/${MATCHUPS_PREFIX}...`);
63
+
64
+ do {
65
+ const command = new ListObjectsV2Command({
66
+ Bucket: bucketName,
67
+ Prefix: MATCHUPS_PREFIX,
68
+ ContinuationToken: continuationToken,
69
+ });
70
+
71
+ const response = await s3Client.send(command);
72
+
73
+ if (response.Contents) {
74
+ for (const obj of response.Contents) {
75
+ // Only process PNG files
76
+ if (obj.Key.endsWith('.png')) {
77
+ images.push({
78
+ key: obj.Key,
79
+ size: obj.Size,
80
+ lastModified: obj.LastModified,
81
+ });
82
+ }
83
+ }
84
+ }
85
+
86
+ continuationToken = response.NextContinuationToken;
87
+ } while (continuationToken);
88
+
89
+ return images;
90
+ }
91
+
92
+ /**
93
+ * Download image from S3
94
+ */
95
+ async function downloadImage(s3Client, bucketName, key) {
96
+ const command = new GetObjectCommand({
97
+ Bucket: bucketName,
98
+ Key: key,
99
+ });
100
+
101
+ const response = await s3Client.send(command);
102
+
103
+ // Convert stream to buffer
104
+ const chunks = [];
105
+ for await (const chunk of response.Body) {
106
+ chunks.push(chunk);
107
+ }
108
+ return Buffer.concat(chunks);
109
+ }
110
+
111
+ /**
112
+ * Convert image to JPEG and resize if larger than target size
113
+ * If image is already at or below 600x315, just converts to JPEG without resizing
114
+ */
115
+ async function resizeImage(imageBuffer) {
116
+ // Get original dimensions
117
+ const metadata = await sharp(imageBuffer).metadata();
118
+ const originalWidth = metadata.width;
119
+ const originalHeight = metadata.height;
120
+
121
+ // Check if image is already at or below target size (no resize needed)
122
+ const isAlreadyOptimal = originalWidth <= TARGET_WIDTH && originalHeight <= TARGET_HEIGHT;
123
+
124
+ let newWidth, newHeight;
125
+ if (isAlreadyOptimal) {
126
+ // Already optimal size, just convert to JPEG without resizing
127
+ newWidth = originalWidth;
128
+ newHeight = originalHeight;
129
+ } else {
130
+ // Image is larger than target, resize down to target size
131
+ newWidth = TARGET_WIDTH;
132
+ newHeight = TARGET_HEIGHT;
133
+ }
134
+
135
+ // Convert to JPEG (resize only if needed)
136
+ let processedBuffer = sharp(imageBuffer);
137
+
138
+ if (!isAlreadyOptimal) {
139
+ processedBuffer = processedBuffer.resize(newWidth, newHeight, {
140
+ fit: 'inside',
141
+ withoutEnlargement: true,
142
+ });
143
+ }
144
+
145
+ const resizedBuffer = await processedBuffer
146
+ .jpeg({
147
+ quality: 85, // Good balance between quality and file size
148
+ mozjpeg: true, // Use mozjpeg encoder for better compression
149
+ })
150
+ .toBuffer();
151
+
152
+ return {
153
+ buffer: resizedBuffer,
154
+ originalWidth,
155
+ originalHeight,
156
+ newWidth,
157
+ newHeight,
158
+ wasAlreadyResized: isAlreadyOptimal,
159
+ };
160
+ }
161
+
162
+ /**
163
+ * Upload resized image back to S3
164
+ * Converts PNG keys to JPEG keys
165
+ */
166
+ async function uploadImage(s3Client, bucketName, key, imageBuffer) {
167
+ // Convert .png extension to .jpg
168
+ const newKey = key.replace(/\.png$/i, '.jpg');
169
+
170
+ const command = new PutObjectCommand({
171
+ Bucket: bucketName,
172
+ Key: newKey,
173
+ Body: imageBuffer,
174
+ ContentType: 'image/jpeg',
175
+ CacheControl: 'public, max-age=31536000',
176
+ });
177
+
178
+ await s3Client.send(command);
179
+
180
+ return newKey;
181
+ }
182
+
183
+ /**
184
+ * Format bytes to human readable
185
+ */
186
+ function formatBytes(bytes) {
187
+ if (bytes < 1024) return `${bytes} B`;
188
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
189
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
190
+ }
191
+
192
+ /**
193
+ * Process images for a single bucket
194
+ */
195
+ async function processBucket(bucketName, region) {
196
+ console.log('');
197
+ console.log('πŸ–ΌοΈ S3 Matchup Image Resizer');
198
+ console.log('============================');
199
+ console.log('');
200
+ console.log(`πŸ“ Target: s3://${bucketName}/${MATCHUPS_PREFIX}`);
201
+ console.log(`πŸ“ Region: ${region}`);
202
+ console.log(`πŸ“ Target size: ${TARGET_WIDTH}x${TARGET_HEIGHT} (full quality)`);
203
+ console.log(`πŸ“ Format: PNG β†’ JPEG (better compression)`);
204
+ console.log(`πŸ“ Mode: ${EXECUTE_MODE ? 'πŸ”₯ EXECUTE (will modify images!)' : 'πŸ‘€ DRY RUN (preview only)'}`);
205
+ if (LIMIT) console.log(`πŸ“ Limit: ${LIMIT} images`);
206
+ console.log('');
207
+
208
+ const s3Client = getS3Client(region);
209
+
210
+ // List all matchup images
211
+ let images = await listMatchupImages(s3Client, bucketName);
212
+ console.log(`πŸ“Š Found ${images.length} matchup images`);
213
+
214
+ if (images.length === 0) {
215
+ console.log('βœ… No images to process!');
216
+ return {
217
+ processed: 0,
218
+ succeeded: 0,
219
+ failed: 0,
220
+ totalOriginalSize: 0,
221
+ totalNewSize: 0,
222
+ };
223
+ }
224
+
225
+ // Apply limit if specified
226
+ if (LIMIT && images.length > LIMIT) {
227
+ console.log(`πŸ“Š Limiting to ${LIMIT} images`);
228
+ images = images.slice(0, LIMIT);
229
+ }
230
+
231
+ console.log('');
232
+
233
+ // Process each image
234
+ let processed = 0;
235
+ let succeeded = 0;
236
+ let failed = 0;
237
+ let totalOriginalSize = 0;
238
+ let totalNewSize = 0;
239
+
240
+ for (const image of images) {
241
+ processed++;
242
+ const progress = `[${processed}/${images.length}]`;
243
+
244
+ try {
245
+ console.log(`${progress} Processing: ${image.key}`);
246
+ console.log(` Original size: ${formatBytes(image.size)}`);
247
+
248
+ // Download the image
249
+ const originalBuffer = await downloadImage(s3Client, bucketName, image.key);
250
+ totalOriginalSize += originalBuffer.length;
251
+
252
+ // Resize the image
253
+ const result = await resizeImage(originalBuffer);
254
+ totalNewSize += result.buffer.length;
255
+
256
+ const sizeSaved = originalBuffer.length - result.buffer.length;
257
+ const percentSaved = ((sizeSaved / originalBuffer.length) * 100).toFixed(1);
258
+
259
+ if (result.wasAlreadyResized) {
260
+ console.log(` Dimensions: ${result.originalWidth}x${result.originalHeight} (already optimal size, converting to JPEG)`);
261
+ } else {
262
+ console.log(` Dimensions: ${result.originalWidth}x${result.originalHeight} β†’ ${result.newWidth}x${result.newHeight}`);
263
+ }
264
+ console.log(` Format: PNG β†’ JPEG`);
265
+ console.log(` New size: ${formatBytes(result.buffer.length)} (saved ${formatBytes(sizeSaved)}, ${percentSaved}%)`);
266
+
267
+ if (EXECUTE_MODE) {
268
+ // Upload the resized and converted image
269
+ const newKey = await uploadImage(s3Client, bucketName, image.key, result.buffer);
270
+ console.log(` βœ… Uploaded as: ${newKey}`);
271
+ } else {
272
+ console.log(` ⏸️ Skipped (dry run)`);
273
+ }
274
+
275
+ succeeded++;
276
+ } catch (err) {
277
+ console.error(` ❌ Failed: ${err.message}`);
278
+ failed++;
279
+ }
280
+
281
+ console.log('');
282
+
283
+ // Small delay to avoid rate limiting
284
+ await new Promise(r => setTimeout(r, 50));
285
+ }
286
+
287
+ // Summary for this bucket
288
+ console.log('');
289
+ console.log(`πŸ“Š Summary for ${bucketName}`);
290
+ console.log('==========');
291
+ console.log(` Total processed: ${processed}`);
292
+ console.log(` βœ… Succeeded: ${succeeded}`);
293
+ console.log(` ❌ Failed: ${failed}`);
294
+ console.log('');
295
+ console.log(` πŸ“¦ Original total size: ${formatBytes(totalOriginalSize)}`);
296
+ console.log(` πŸ“¦ New total size: ${formatBytes(totalNewSize)}`);
297
+ if (totalOriginalSize > 0) {
298
+ console.log(` πŸ’Ύ Total saved: ${formatBytes(totalOriginalSize - totalNewSize)} (${((1 - totalNewSize / totalOriginalSize) * 100).toFixed(1)}%)`);
299
+ }
300
+ console.log('');
301
+
302
+ return {
303
+ processed,
304
+ succeeded,
305
+ failed,
306
+ totalOriginalSize,
307
+ totalNewSize,
308
+ };
309
+ }
310
+
311
+ /**
312
+ * Main function
313
+ */
314
+ async function main() {
315
+ console.log('');
316
+ console.log('πŸ–ΌοΈ S3 Matchup Image Resizer - Multi-Bucket');
317
+ console.log('============================================');
318
+ console.log('');
319
+ console.log(`πŸ“ Processing ${BUCKETS.length} buckets:`);
320
+ BUCKETS.forEach(bucket => {
321
+ console.log(` - ${bucket.name} (${bucket.region})`);
322
+ });
323
+ console.log(`πŸ“ Target size: ${TARGET_WIDTH}x${TARGET_HEIGHT} (full quality)`);
324
+ console.log(`πŸ“ Format: PNG β†’ JPEG (better compression)`);
325
+ console.log(`πŸ“ Mode: ${EXECUTE_MODE ? 'πŸ”₯ EXECUTE (will modify images!)' : 'πŸ‘€ DRY RUN (preview only)'}`);
326
+ if (LIMIT) console.log(`πŸ“ Limit: ${LIMIT} images per bucket`);
327
+ console.log('');
328
+
329
+ // Process each bucket
330
+ const allResults = [];
331
+ for (const bucket of BUCKETS) {
332
+ const result = await processBucket(bucket.name, bucket.region);
333
+ allResults.push({ bucket: bucket.name, region: bucket.region, ...result });
334
+
335
+ // Add separator between buckets
336
+ if (bucket !== BUCKETS[BUCKETS.length - 1]) {
337
+ console.log('');
338
+ console.log('─'.repeat(60));
339
+ }
340
+ }
341
+
342
+ // Overall summary
343
+ console.log('');
344
+ console.log('πŸ“Š Overall Summary');
345
+ console.log('=================');
346
+ const totalProcessed = allResults.reduce((sum, r) => sum + r.processed, 0);
347
+ const totalSucceeded = allResults.reduce((sum, r) => sum + r.succeeded, 0);
348
+ const totalFailed = allResults.reduce((sum, r) => sum + r.failed, 0);
349
+ const totalOriginalSize = allResults.reduce((sum, r) => sum + r.totalOriginalSize, 0);
350
+ const totalNewSize = allResults.reduce((sum, r) => sum + r.totalNewSize, 0);
351
+
352
+ console.log(` Total processed: ${totalProcessed}`);
353
+ console.log(` βœ… Succeeded: ${totalSucceeded}`);
354
+ console.log(` ❌ Failed: ${totalFailed}`);
355
+ console.log('');
356
+ console.log(` πŸ“¦ Original total size: ${formatBytes(totalOriginalSize)}`);
357
+ console.log(` πŸ“¦ New total size: ${formatBytes(totalNewSize)}`);
358
+ if (totalOriginalSize > 0) {
359
+ console.log(` πŸ’Ύ Total saved: ${formatBytes(totalOriginalSize - totalNewSize)} (${((1 - totalNewSize / totalOriginalSize) * 100).toFixed(1)}%)`);
360
+ }
361
+ console.log('');
362
+
363
+ if (!EXECUTE_MODE) {
364
+ console.log('⚠️ This was a DRY RUN. To actually resize images, run:');
365
+ console.log(' node scripts/resize-s3-matchup-images.js --execute');
366
+ console.log('');
367
+ }
368
+ }
369
+
370
+ main().catch(err => {
371
+ console.error('❌ Fatal error:', err);
372
+ process.exit(1);
373
+ });
374
+