@vatvaghool/create-ipl-dashboard 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +75 -0
  2. package/package.json +27 -0
  3. package/src/generate-template.mjs +73 -0
  4. package/src/index.mjs +98 -0
  5. package/src/prompts.mjs +78 -0
  6. package/src/scaffold.mjs +129 -0
  7. package/src/scraper.mjs +79 -0
  8. package/template/.dockerignore +13 -0
  9. package/template/AGENTS.md +5 -0
  10. package/template/Dockerfile.sync +14 -0
  11. package/template/README.md +160 -0
  12. package/template/app/api/ipl/data.ts +24 -0
  13. package/template/app/api/ipl/route.ts +505 -0
  14. package/template/app/api/ipl/transfers/route.ts +261 -0
  15. package/template/app/api/ipl/transfers/transform.ts +156 -0
  16. package/template/app/api/ipl/transform.ts +20 -0
  17. package/template/app/api/ipl/upcoming-matches/route.ts +18 -0
  18. package/template/app/api/ops/status/route.ts +225 -0
  19. package/template/app/components/AIRoasting.tsx +278 -0
  20. package/template/app/components/ColorWave.tsx +193 -0
  21. package/template/app/components/CrownBattle.tsx +207 -0
  22. package/template/app/components/DashboardContent.tsx +377 -0
  23. package/template/app/components/FantasyStockTicker.tsx +192 -0
  24. package/template/app/components/FireworksBurst.tsx +225 -0
  25. package/template/app/components/LiveMatchTicker.tsx +117 -0
  26. package/template/app/components/MatchRecapScroll.tsx +135 -0
  27. package/template/app/components/MatchStoryScrubber.tsx +274 -0
  28. package/template/app/components/PerformanceTracker.tsx +132 -0
  29. package/template/app/components/ProgressGlowRings.tsx +157 -0
  30. package/template/app/components/TeamDNAScanner.tsx +238 -0
  31. package/template/app/components/ThemeToggle.tsx +74 -0
  32. package/template/app/components/dashboard/CaptainBoard.tsx +138 -0
  33. package/template/app/components/dashboard/ChartBoard.tsx +162 -0
  34. package/template/app/components/dashboard/LatestBadge.tsx +23 -0
  35. package/template/app/components/dashboard/LedgerTable.tsx +385 -0
  36. package/template/app/components/dashboard/SectionCard.tsx +59 -0
  37. package/template/app/components/dashboard/StickyMini.tsx +20 -0
  38. package/template/app/components/dashboard/index.ts +6 -0
  39. package/template/app/components/ui/DashboardChartFrame.tsx +74 -0
  40. package/template/app/components/ui/DoodleSpinner.tsx +15 -0
  41. package/template/app/components/ui/TeamPills.tsx +41 -0
  42. package/template/app/data/match-points.ts +3 -0
  43. package/template/app/data/teams.ts +32 -0
  44. package/template/app/globals.css +1267 -0
  45. package/template/app/hooks/dashboard/index.ts +1 -0
  46. package/template/app/hooks/dashboard/useDashboardModel.ts +25 -0
  47. package/template/app/hooks/dashboardCache.ts +53 -0
  48. package/template/app/hooks/dashboardPolling.ts +53 -0
  49. package/template/app/hooks/snapshotCache.ts +47 -0
  50. package/template/app/hooks/useDashboardData.ts +28 -0
  51. package/template/app/layout.tsx +75 -0
  52. package/template/app/lib/aiAgent.ts +444 -0
  53. package/template/app/lib/config.ts +29 -0
  54. package/template/app/lib/dashboard/index.ts +1 -0
  55. package/template/app/lib/dashboard/model.ts +257 -0
  56. package/template/app/lib/dashboardData.ts +50 -0
  57. package/template/app/lib/dashboardView.ts +22 -0
  58. package/template/app/lib/detailedData.ts +112 -0
  59. package/template/app/lib/matchStatus.ts +28 -0
  60. package/template/app/lib/matches.ts +131 -0
  61. package/template/app/lib/teamBadges.ts +223 -0
  62. package/template/app/lib/upcomingMatches.ts +154 -0
  63. package/template/app/lib/useDb.ts +29 -0
  64. package/template/app/lib/utils/diff.ts +24 -0
  65. package/template/app/lib/utils/getChartColor.ts +17 -0
  66. package/template/app/lib/utils/getStdDeviation.ts +6 -0
  67. package/template/app/lib/utils/time.ts +40 -0
  68. package/template/app/lib/utils.ts +70 -0
  69. package/template/app/page.tsx +15 -0
  70. package/template/app/store/dashboardStore.ts +85 -0
  71. package/template/app/types/dashboard.ts +75 -0
  72. package/template/app/types.ts +130 -0
  73. package/template/app/utils/dashboard/index.ts +72 -0
  74. package/template/eslint.config.mjs +18 -0
  75. package/template/infra/cloud-run/README.md +68 -0
  76. package/template/infra/cloud-run/sync-job.yaml +32 -0
  77. package/template/infra/cutover/README.md +84 -0
  78. package/template/infra/vercel/README.md +57 -0
  79. package/template/next.config.ts +7 -0
  80. package/template/package-lock.json +7330 -0
  81. package/template/package.json +47 -0
  82. package/template/packages/ipl-dashboard-utils/README.md +316 -0
  83. package/template/packages/ipl-dashboard-utils/package.json +34 -0
  84. package/template/packages/ipl-dashboard-utils/src/index.ts +22 -0
  85. package/template/packages/ipl-dashboard-utils/src/transform.ts +687 -0
  86. package/template/packages/ipl-dashboard-utils/src/types.ts +88 -0
  87. package/template/packages/ipl-dashboard-utils/tsconfig.build.json +17 -0
  88. package/template/postcss.config.mjs +7 -0
  89. package/template/scripts/capture-ipl-auth.mjs +54 -0
  90. package/template/scripts/deploy-cloud-run-sync.sh +48 -0
  91. package/template/scripts/deploy-cloud-scheduler.sh +42 -0
  92. package/template/scripts/dev-simple.js +31 -0
  93. package/template/scripts/dev-welcome.mjs +38 -0
  94. package/template/scripts/monitor-ops-status.sh +50 -0
  95. package/template/scripts/seed-mongodb.ts +115 -0
  96. package/template/scripts/sync-cloud.mjs +50 -0
  97. package/template/scripts/sync-ipl.mjs +238 -0
  98. package/template/scripts/sync-transfers-daily.mjs +175 -0
  99. package/template/scripts/verify-production.mjs +108 -0
  100. package/template/tests/coverage-gaps.test.ts +290 -0
  101. package/template/tests/dashboard-polling.test.ts +96 -0
  102. package/template/tests/detailed-data.test.ts +60 -0
  103. package/template/tests/ipl-transform.test.ts +590 -0
  104. package/template/tests/transfers-route.test.ts +109 -0
  105. package/template/tests/upcoming-matches.test.ts +34 -0
  106. package/template/tests/utils-and-cache.test.ts +267 -0
  107. package/template/tsconfig.json +35 -0
  108. package/template/vercel.json +7 -0
@@ -0,0 +1,238 @@
1
+ const DEFAULT_LEAGUE_URL =
2
+ "https://fantasy.iplt20.com/classic/league/view/66930102";
3
+ const DEFAULT_API_URL = "http://localhost:3000/api/ipl";
4
+
5
+ const { readFileSync } = await import("node:fs");
6
+
7
+ const TEAM_ALIASES = {
8
+ "Team RJ": "Bat Bowl XI", "Pratik Falcon11": "Pratik Gurnani",
9
+ "Rahul XI": "RSAwesome 11", WATAPI11: "Watapi",
10
+ "RKs STALLIONS": "Ravi Kiran Guthula",
11
+ "Ghost Riders": "RushS01", "VATVAGHOOL XI": "VATVAGHOOL XI",
12
+ "Aditya Raut": "SquadSeven9", "PKs 11": "PKs11",
13
+ "Nilesh Birajdar": "Nilesh Birajdar",
14
+ };
15
+
16
+ const readEnv = (name, fallback = "") => process.env[name]?.trim() || fallback;
17
+ const isWatchMode = process.argv.includes("--watch") || readEnv("IPL_WATCH") === "1";
18
+ const WATCH_INTERVAL_MS = 60000;
19
+ const isHeadless = readEnv("IPL_HEADLESS", "1") !== "0";
20
+ const defaultApiUrl = `${readEnv("IPL_API_BASE_URL", "").replace(/\/$/, "") || DEFAULT_API_URL}`;
21
+
22
+ const getCompletedMatches = (text) => {
23
+ const matches = [...String(text ?? "").toLowerCase().matchAll(/match\s*(\d{1,2})/g)]
24
+ .map((m) => Number(m[1])).filter((v) => Number.isFinite(v) && v > 0 && v <= 74);
25
+ return matches.length ? Math.max(...matches) : undefined;
26
+ };
27
+
28
+ const getStorageState = () => {
29
+ const inline = readEnv("IPL_STORAGE_STATE_JSON");
30
+ if (inline) return JSON.parse(inline);
31
+ const encoded = readEnv("IPL_STORAGE_STATE_B64");
32
+ if (encoded) return JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
33
+ try { return JSON.parse(readFileSync("ipl-auth.json", "utf8")); } catch { return undefined; }
34
+ };
35
+
36
+ const postJson = async (url, payload) => {
37
+ const headers = { "Content-Type": "application/json" };
38
+ const token = readEnv("IPL_POST_SECRET");
39
+ if (token) headers.Authorization = `Bearer ${token}`;
40
+ const response = await fetch(url, { method: "POST", headers, body: JSON.stringify(payload) });
41
+ const body = await response.text();
42
+ if (!response.ok) throw new Error(`POST ${url} failed with ${response.status}: ${body}`);
43
+ return body;
44
+ };
45
+
46
+ const scrape = async (page) => {
47
+ if (!(await page.locator("#leadersList").count())) {
48
+ throw new Error("Leaderboard not found (#leadersList).");
49
+ }
50
+ if (!(await page.locator(".m11c-live").count())) {
51
+ console.log("No live match found. Exiting without sync.");
52
+ return null;
53
+ }
54
+
55
+ const completedMatches = getCompletedMatches(await page.locator("body").innerText());
56
+ const items = page.locator("#leadersList li");
57
+ const total = await items.count();
58
+ const leaders = [];
59
+
60
+ for (let index = 0; index < total; index += 1) {
61
+ const row = items.nth(index);
62
+ const base = await row.evaluate((node, aliases) => {
63
+ const getText = (s) => String(node.querySelector(s)?.textContent ?? "").trim();
64
+ const parseNumber = (v) => { const n = Number(String(v ?? "").replace(/,/g, "").trim()); return Number.isFinite(n) ? n : undefined; };
65
+ const rawName = getText(".m11c-tbl__cell--name span");
66
+ const rank = parseNumber(getText(".m11c-matchCount"));
67
+ const points = parseNumber(getText(".m11c-tbl__cell--pts span"));
68
+ if (!rawName || rank === undefined || points === undefined) return null;
69
+ return { rank, name: aliases[rawName] || rawName, points };
70
+ }, TEAM_ALIASES);
71
+
72
+ if (!base) continue;
73
+
74
+ await row.scrollIntoViewIfNeeded();
75
+ await row.click();
76
+ await page.waitForTimeout(1200);
77
+
78
+ const details = await page.evaluate(() => {
79
+ const parseNumber = (v) => {
80
+ const text = String(v ?? "").replace(/,/g, " ").trim();
81
+ const direct = Number(text);
82
+ if (Number.isFinite(direct)) return direct;
83
+ const match = text.match(/-?\d+(?:\.\d+)?/);
84
+ if (!match) return undefined;
85
+ const parsed = Number(match[0]);
86
+ return Number.isFinite(parsed) ? parsed : undefined;
87
+ };
88
+ const readPointText = (item) => {
89
+ const valueNode = item.querySelector(".m11c-pitch__plyr-num span");
90
+ if (valueNode?.textContent) {
91
+ return valueNode.textContent;
92
+ }
93
+
94
+ const fallbackNode = item.querySelector(
95
+ ".m11c-pitch__plyr-pts, .m11c-pitch__pts, .m11c-plyr-pts, .m11c-plyr__pts, .m11c-pitch__score, .m11c-score, .m11c-pitch__plyr-num, em",
96
+ );
97
+ return fallbackNode?.textContent;
98
+ };
99
+ const pitchArea = document.querySelector(".m11c-pitch__area");
100
+ const players = pitchArea
101
+ ? [...pitchArea.querySelectorAll(".m11c-pitch__plyr")].map((item) => {
102
+ const name = String(item.querySelector(".m11c-pitch__plyr-name")?.textContent ?? "").trim();
103
+ if (!name) return null;
104
+ const rawPointText = readPointText(item);
105
+ const points = parseNumber(rawPointText);
106
+
107
+ return {
108
+ name,
109
+ points,
110
+ rawPointText: String(rawPointText ?? "").trim(),
111
+ isCaptain: item.classList.contains("m11c-cap"),
112
+ isViceCaptain: item.classList.contains("m11c-vcap"),
113
+ };
114
+ }).filter(Boolean)
115
+ : [];
116
+
117
+ const captain = players.find((player) => player.isCaptain);
118
+ const viceCaptain = players.find((player) => player.isViceCaptain);
119
+
120
+ return {
121
+ lastMatchPoints: parseNumber(document.querySelector(".m11c-pitch__fix-rgt em")?.textContent),
122
+ captain: captain ? { name: captain.name, points: captain.points } : undefined,
123
+ viceCaptain: viceCaptain ? { name: viceCaptain.name, points: viceCaptain.points } : undefined,
124
+ players: players.map(({ name, points, rawPointText }) => (
125
+ points === undefined ? { name, rawPointText } : { name, points, rawPointText }
126
+ )),
127
+ };
128
+ });
129
+
130
+ const headerMeta = await page.evaluate(() => {
131
+ const parseNumber = (v) => { const n = Number(String(v ?? "").replace(/,/g, "").trim()); return Number.isFinite(n) ? n : undefined; };
132
+ const result = {};
133
+ const head = document.querySelector(".m11c-transfer__head");
134
+ if (!head) return result;
135
+ head.querySelectorAll("span").forEach((span) => {
136
+ const label = String(span.childNodes?.[0]?.textContent ?? span.textContent ?? "").trim().toLowerCase();
137
+ const value = String(span.querySelector("em")?.textContent ?? "").trim();
138
+ if (label.includes("matches played")) { const v = parseNumber(value); if (v !== undefined) result.matchesPlayed = v; }
139
+ if (label.includes("transfers left")) {
140
+ const [leftText, totalText] = value.split("/");
141
+ const left = parseNumber(leftText); const total = parseNumber(totalText);
142
+ if (left !== undefined) result.transfersLeft = left;
143
+ if (total !== undefined) result.totalTransfers = total;
144
+ if (left !== undefined && total !== undefined && total >= left) result.transfersUsed = total - left;
145
+ }
146
+ if (label.includes("boosters used") && value) result.boostersUsed = value;
147
+ });
148
+ return result;
149
+ });
150
+
151
+ leaders.push({
152
+ rank: base.rank, name: base.name, points: base.points,
153
+ matchesPlayed: headerMeta.matchesPlayed,
154
+ lastMatchPoints: details.lastMatchPoints,
155
+ transfersLeft: headerMeta.transfersLeft, transfersUsed: headerMeta.transfersUsed,
156
+ totalTransfers: headerMeta.totalTransfers, boostersUsed: headerMeta.boostersUsed,
157
+ captain: details.captain, viceCaptain: details.viceCaptain, players: details.players,
158
+ });
159
+
160
+ console.log(
161
+ JSON.stringify(
162
+ {
163
+ team: base.name,
164
+ captain: details.captain ?? null,
165
+ viceCaptain: details.viceCaptain ?? null,
166
+ captainCard: details.players.find((player) => player.name === details.captain?.name) ?? null,
167
+ viceCaptainCard: details.players.find((player) => player.name === details.viceCaptain?.name) ?? null,
168
+ },
169
+ null,
170
+ 2,
171
+ ),
172
+ );
173
+
174
+ await page.evaluate(() => {
175
+ const overall = [...document.querySelectorAll("li.swiper-slide, li")].find(
176
+ (n) => String(n.textContent ?? "").trim().toUpperCase() === "OVERALL",
177
+ );
178
+ if (overall instanceof HTMLElement) overall.click();
179
+ });
180
+ await page.waitForTimeout(800);
181
+ }
182
+
183
+ if (!leaders.length) throw new Error("No leaderboard entries were scraped.");
184
+
185
+ return { updatedAt: new Date().toISOString(), completedMatches, leaders };
186
+ };
187
+
188
+ const run = async () => {
189
+ const { chromium } = await import("playwright").catch(() => {
190
+ throw new Error('Playwright not installed. Run "npm install --no-save playwright" first.');
191
+ });
192
+
193
+ const leagueUrl = readEnv("IPL_LEAGUE_URL", DEFAULT_LEAGUE_URL);
194
+ const apiUrl = readEnv("IPL_API_URL", defaultApiUrl);
195
+ const storageState = getStorageState();
196
+
197
+ const browser = await chromium.launch({ headless: isHeadless });
198
+ const context = await browser.newContext(storageState ? { storageState } : {});
199
+
200
+ try {
201
+ const page = await context.newPage();
202
+ await page.goto(leagueUrl, { waitUntil: "networkidle", timeout: 60000 });
203
+
204
+ if (isWatchMode) {
205
+ console.log("Watch mode enabled. Scraping every 60s. Press Ctrl+C to stop.");
206
+ }
207
+
208
+ const doScrape = async () => {
209
+ const payload = await scrape(page);
210
+ if (!payload) return;
211
+ const result = await postJson(apiUrl, payload);
212
+ console.log(JSON.stringify({
213
+ ok: true, leagueUrl, apiUrl, completedMatches: payload.completedMatches,
214
+ leaders: payload.leaders.length, response: result,
215
+ }, null, 2));
216
+ };
217
+
218
+ await doScrape();
219
+
220
+ if (isWatchMode) {
221
+ const loop = async () => {
222
+ await page.goto(leagueUrl, { waitUntil: "networkidle", timeout: 60000 });
223
+ await doScrape();
224
+ setTimeout(loop, WATCH_INTERVAL_MS);
225
+ };
226
+ setTimeout(loop, WATCH_INTERVAL_MS);
227
+ await new Promise(() => {});
228
+ }
229
+ } finally {
230
+ await context.close();
231
+ await browser.close();
232
+ }
233
+ };
234
+
235
+ run().catch((error) => {
236
+ console.error(error instanceof Error ? error.message : error);
237
+ process.exitCode = 1;
238
+ });
@@ -0,0 +1,175 @@
1
+ const DEFAULT_LEAGUE_URL =
2
+ "https://fantasy.iplt20.com/classic/league/view/66930102";
3
+ const DEFAULT_API_URL = "http://localhost:3000/api/ipl/transfers";
4
+
5
+ const { readFileSync } = await import("node:fs");
6
+
7
+ const TEAM_ALIASES = {
8
+ "Team RJ": "Bat Bowl XI", "Pratik Falcon11": "Pratik Gurnani",
9
+ "Rahul XI": "RSAwesome 11", WATAPI11: "Watapi",
10
+ "RKs STALLIONS": "Ravi Kiran Guthula",
11
+ "Ghost Riders": "RushS01", "VATVAGHOOL XI": "VATVAGHOOL XI",
12
+ "Aditya Raut": "SquadSeven9", "PKs 11": "PKs11",
13
+ "Nilesh Birajdar": "Nilesh Birajdar",
14
+ };
15
+
16
+ const readEnv = (name, fallback = "") => process.env[name]?.trim() || fallback;
17
+ const isHeadless = readEnv("IPL_HEADLESS", "1") !== "0";
18
+ const defaultApiUrl = `${readEnv("IPL_API_BASE_URL", "").replace(/\/$/, "") || DEFAULT_API_URL}${
19
+ readEnv("IPL_API_BASE_URL", "") ? "/api/ipl/transfers" : ""
20
+ }`;
21
+
22
+ const getStorageState = () => {
23
+ const inline = readEnv("IPL_STORAGE_STATE_JSON");
24
+ if (inline) return JSON.parse(inline);
25
+ const encoded = readEnv("IPL_STORAGE_STATE_B64");
26
+ if (encoded) return JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
27
+ try { return JSON.parse(readFileSync("ipl-auth.json", "utf8")); } catch { return undefined; }
28
+ };
29
+
30
+ const postJson = async (url, payload) => {
31
+ const headers = { "Content-Type": "application/json" };
32
+ const token = readEnv("IPL_POST_SECRET");
33
+ if (token) headers.Authorization = `Bearer ${token}`;
34
+ const response = await fetch(url, { method: "POST", headers, body: JSON.stringify(payload) });
35
+ const body = await response.text();
36
+ if (!response.ok) throw new Error(`POST ${url} failed with ${response.status}: ${body}`);
37
+ return body;
38
+ };
39
+
40
+ const closePanel = async (page) => {
41
+ const closed = await page.evaluate(() => {
42
+ const closeBtn = document.querySelector(".m11c-cmo__rhs .m11ci-close");
43
+ if (closeBtn instanceof HTMLElement) { closeBtn.click(); return true; }
44
+ return false;
45
+ });
46
+ if (!closed) {
47
+ await page.keyboard.press("Escape");
48
+ }
49
+ };
50
+
51
+ const clickOverallTab = async (page) => {
52
+ const clicked = await page.evaluate(() => {
53
+ const tabs = document.querySelectorAll(".m11c-cmo__rhs li.swiper-slide, .m11c-cmo__rhs li");
54
+ const overall = [...tabs].find(
55
+ (n) => String(n.textContent ?? "").trim().toUpperCase() === "OVERALL",
56
+ );
57
+ if (overall instanceof HTMLElement) { overall.click(); return true; }
58
+ return false;
59
+ });
60
+ return clicked;
61
+ };
62
+
63
+ const run = async () => {
64
+ const { chromium } = await import("playwright").catch(() => {
65
+ throw new Error('Playwright not installed. Run "npm install --no-save playwright" first.');
66
+ });
67
+
68
+ const leagueUrl = readEnv("IPL_LEAGUE_URL", DEFAULT_LEAGUE_URL);
69
+ const apiUrl = readEnv("IPL_TRANSFERS_API_URL", readEnv("IPL_API_URL", defaultApiUrl));
70
+ const storageState = getStorageState();
71
+
72
+ const browser = await chromium.launch({ headless: isHeadless });
73
+ const context = await browser.newContext(storageState ? { storageState } : {});
74
+ const page = await context.newPage();
75
+
76
+ try {
77
+ await page.goto(leagueUrl, { waitUntil: "networkidle", timeout: 60000 });
78
+
79
+ if (!(await page.locator("#leadersList").count())) {
80
+ throw new Error("Leaderboard not found (#leadersList).");
81
+ }
82
+
83
+ const items = page.locator("#leadersList li");
84
+ const total = await items.count();
85
+ const capturedAt = new Date().toISOString();
86
+ const teams = [];
87
+
88
+ console.log(`Found ${total} teams`);
89
+
90
+ for (let index = 0; index < total; index += 1) {
91
+ const row = items.nth(index);
92
+ const nameEl = row.locator(".m11c-tbl__cell--name span");
93
+ const rawName = await nameEl.textContent();
94
+ const teamName = TEAM_ALIASES[rawName?.trim() || ""] || (rawName?.trim() || "");
95
+
96
+ if (!teamName) continue;
97
+
98
+ await row.scrollIntoViewIfNeeded();
99
+ await nameEl.click();
100
+ await page.waitForTimeout(1500);
101
+
102
+ const overallClicked = await clickOverallTab(page);
103
+ if (overallClicked) {
104
+ await page.waitForTimeout(1000);
105
+ }
106
+
107
+ const meta = await page.evaluate(() => {
108
+ const parseNumber = (v) => { const n = Number(String(v ?? "").replace(/,/g, "").trim()); return Number.isFinite(n) ? n : undefined; };
109
+ const result = {};
110
+
111
+ const rankSpan = document.querySelector(".m11c__selfRankStats span");
112
+ if (rankSpan) {
113
+ const txt = String(rankSpan.textContent ?? "").trim();
114
+ const m = txt.match(/[\d.]+/);
115
+ if (m) result.totalPoints = parseNumber(m[0]);
116
+ }
117
+
118
+ const head = document.querySelector(".m11c-transfer__head");
119
+ if (head) {
120
+ head.querySelectorAll("span").forEach((span) => {
121
+ const label = String(span.childNodes?.[0]?.textContent ?? span.textContent ?? "").trim().toLowerCase();
122
+ const value = String(span.querySelector("em")?.textContent ?? "").trim();
123
+ if (label.includes("matches played")) { const v = parseNumber(value); if (v !== undefined) result.matchesPlayed = v; }
124
+ if (label.includes("transfers left")) {
125
+ const left = parseNumber(value.split("/")[0]);
126
+ if (left !== undefined) result.transfersLeft = left;
127
+ }
128
+ if (label.includes("boosters used") && value) result.boostersUsed = Number(value);
129
+ });
130
+ }
131
+ return result;
132
+ });
133
+
134
+ if (meta.matchesPlayed !== undefined) {
135
+ const used = 160 - meta.transfersLeft;
136
+ const eff = meta.totalPoints && used ? (meta.totalPoints / used).toFixed(2) : "-";
137
+ teams.push({ team: teamName, ...meta, updatedAt: capturedAt, eff, usedTransfers: used });
138
+ }
139
+
140
+ await closePanel(page);
141
+ await page.waitForTimeout(1000);
142
+ }
143
+
144
+ if (!teams.length) {
145
+ console.log("\nNo transfer data found.");
146
+ return;
147
+ }
148
+
149
+ console.log(`\n${"Team".padEnd(20)} ${"M".padStart(3)} ${"Trn".padEnd(4)} ${"Bst".padEnd(4)} ${"Pts".padEnd(10)} ${"Used".padStart(5)} ${"Eff".padStart(8)}`);
150
+ console.log("".padEnd(58, "─"));
151
+ for (const t of teams) {
152
+ console.log(`${t.team.padEnd(20)} ${String(t.matchesPlayed).padStart(3)} ${String(t.transfersLeft).padEnd(4)} ${String(t.boostersUsed).padEnd(4)} ${String(t.totalPoints).padEnd(10)} ${String(t.usedTransfers).padStart(5)} ${String(t.eff).padStart(8)}`);
153
+ }
154
+
155
+ if (!teams.length) {
156
+ console.log("\nNo transfer data found on leaderboard. The page may not have transfer info available right now.");
157
+ return;
158
+ }
159
+
160
+ console.log(`\nPosting ${teams.length} team records to ${apiUrl}...`);
161
+ const payload = { updatedAt: capturedAt, dailyTransferUpdatedAt: capturedAt, teams };
162
+ const result = await postJson(apiUrl, payload);
163
+
164
+ console.log(JSON.stringify({ ok: true, leagueUrl, apiUrl, teams: teams.length, response: result }, null, 2));
165
+ } finally {
166
+ await page.close();
167
+ await context.close();
168
+ await browser.close();
169
+ }
170
+ };
171
+
172
+ run().catch((error) => {
173
+ console.error(error instanceof Error ? error.message : error);
174
+ process.exitCode = 1;
175
+ });
@@ -0,0 +1,108 @@
1
+ const readEnv = (name, fallback = "") => process.env[name]?.trim() || fallback;
2
+
3
+ const baseUrl = readEnv("APP_BASE_URL");
4
+
5
+ if (!baseUrl) {
6
+ throw new Error("APP_BASE_URL is required. Example: https://your-app-domain");
7
+ }
8
+
9
+ const normalizedBaseUrl = baseUrl.replace(/\/$/, "");
10
+
11
+ const checks = [
12
+ {
13
+ name: "ops status",
14
+ path: "/api/ops/status",
15
+ validate: (payload, response) => {
16
+ if (!payload || typeof payload !== "object") {
17
+ throw new Error("Invalid JSON payload.");
18
+ }
19
+ if (!("mongoConfigured" in payload)) {
20
+ throw new Error("Missing mongoConfigured field.");
21
+ }
22
+ if (!response.ok) {
23
+ const statusLabel =
24
+ payload && typeof payload === "object" && "status" in payload
25
+ ? String(payload.status ?? "unknown")
26
+ : "unknown";
27
+ throw new Error(
28
+ `Ops status reported HTTP ${response.status} with status ${statusLabel}.`,
29
+ );
30
+ }
31
+ },
32
+ },
33
+ {
34
+ name: "dashboard",
35
+ path: "/api/ipl",
36
+ validate: (payload) => {
37
+ if (!payload || typeof payload !== "object") {
38
+ throw new Error("Invalid JSON payload.");
39
+ }
40
+ if (!("overall" in payload) && !("error" in payload)) {
41
+ throw new Error("Expected overall data or error payload.");
42
+ }
43
+ },
44
+ },
45
+ {
46
+ name: "snapshot",
47
+ path: "/api/ipl?format=snapshot",
48
+ validate: (payload) => {
49
+ if (!payload || typeof payload !== "object") {
50
+ throw new Error("Invalid JSON payload.");
51
+ }
52
+ if (!("leaders" in payload) && !("error" in payload)) {
53
+ throw new Error("Expected leaders data or error payload.");
54
+ }
55
+ },
56
+ },
57
+ {
58
+ name: "transfers",
59
+ path: "/api/ipl/transfers",
60
+ validate: (payload) => {
61
+ if (!payload || typeof payload !== "object") {
62
+ throw new Error("Invalid JSON payload.");
63
+ }
64
+ if (!("teams" in payload) && !("error" in payload)) {
65
+ throw new Error("Expected transfer snapshot or error payload.");
66
+ }
67
+ },
68
+ },
69
+ ];
70
+
71
+ const run = async () => {
72
+ for (const check of checks) {
73
+ const url = `${normalizedBaseUrl}${check.path}`;
74
+ const response = await fetch(url, {
75
+ headers: {
76
+ Accept: "application/json",
77
+ },
78
+ });
79
+
80
+ const rawBody = await response.text();
81
+ let payload;
82
+ try {
83
+ payload = JSON.parse(rawBody);
84
+ } catch {
85
+ throw new Error(`[verify-production] ${check.name} returned non-JSON: ${rawBody}`);
86
+ }
87
+
88
+ check.validate(payload, response);
89
+
90
+ console.log(
91
+ JSON.stringify(
92
+ {
93
+ check: check.name,
94
+ url,
95
+ status: response.status,
96
+ ok: response.ok,
97
+ },
98
+ null,
99
+ 2,
100
+ ),
101
+ );
102
+ }
103
+ };
104
+
105
+ run().catch((error) => {
106
+ console.error(error instanceof Error ? error.message : error);
107
+ process.exitCode = 1;
108
+ });