@vatvaghool/create-ipl-dashboard 0.1.15 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -12
- package/package.json +1 -1
- package/src/index.mjs +3 -20
- package/src/prompts.mjs +102 -61
- package/src/scaffold.mjs +13 -17
- package/template/README.md +41 -30
package/README.md
CHANGED
|
@@ -34,10 +34,9 @@ npx @vatvaghool/create-ipl-dashboard [project-name] [options]
|
|
|
34
34
|
| Private key | Google service account private key (only if google_sheets chosen) |
|
|
35
35
|
| League URL | The fantasy.iplt20.com league page URL |
|
|
36
36
|
| Collection/sheet name | MongoDB collection or Google sheet for this league's data |
|
|
37
|
-
| League name |
|
|
38
|
-
| Teams | Team names (and optional owners) in your league
|
|
37
|
+
| League name | Used as collection/table name in storage |
|
|
39
38
|
|
|
40
|
-
|
|
39
|
+
Set team names later via `app/data/teams.ts` or use `--scrape` to auto-detect from the league URL during scaffolding.
|
|
41
40
|
|
|
42
41
|
## Screenshots
|
|
43
42
|
|
|
@@ -85,16 +84,31 @@ cd my-league
|
|
|
85
84
|
|
|
86
85
|
# Start the dev server
|
|
87
86
|
npm run dev:simple
|
|
88
|
-
|
|
89
|
-
# Capture auth state for scrapers (one-time setup)
|
|
90
|
-
npm run capture:ipl-auth
|
|
91
|
-
|
|
92
|
-
# Scrape live leaderboard data
|
|
93
|
-
npm run sync:ipl
|
|
94
87
|
```
|
|
95
88
|
|
|
96
89
|
Open http://localhost:3000 to see your dashboard.
|
|
97
90
|
|
|
91
|
+
### Available commands
|
|
92
|
+
|
|
93
|
+
| Command | Description |
|
|
94
|
+
|---------|-------------|
|
|
95
|
+
| `npm run dev` | Dev server with welcome splash |
|
|
96
|
+
| `npm run dev:simple` | Dev server (simple, no splash) |
|
|
97
|
+
| `npm run build` | Production build |
|
|
98
|
+
| `npm start` | Production server |
|
|
99
|
+
| `npm run capture:ipl-auth` | Capture Playwright login state (one-time) |
|
|
100
|
+
| `npm run sync:ipl` | Scrape live leaderboard snapshot |
|
|
101
|
+
| `npm run sync:ipl:watch` | Scrape leaderboard in watch mode (polls every 2 min) |
|
|
102
|
+
| `npm run sync:ipl:transfers-daily` | Scrape transfer/booster data |
|
|
103
|
+
| `npm run sync:cloud` | Run both leaderboard + transfer sync (for cloud jobs) |
|
|
104
|
+
| `npm run seed:league` | Seed league metadata into storage |
|
|
105
|
+
| `npm run seed:mongodb` | Seed initial raw user data from `data.ts` |
|
|
106
|
+
| `npm run seed:mongodb:reset` | Reset and re-seed MongoDB data |
|
|
107
|
+
| `npm run verify:production` | Verify production setup |
|
|
108
|
+
| `npm run monitor:ops` | Check ops health status |
|
|
109
|
+
| `npm run test` | Run test suite |
|
|
110
|
+
| `npm run lint` | Run linter |
|
|
111
|
+
|
|
98
112
|
## How it works
|
|
99
113
|
|
|
100
114
|
The CLI:
|
|
@@ -121,11 +135,13 @@ The template includes all dashboard components, API endpoints, scrapers, and tes
|
|
|
121
135
|
npx @vatvaghool/create-ipl-dashboard another-league
|
|
122
136
|
```
|
|
123
137
|
|
|
124
|
-
Provide a different
|
|
138
|
+
Provide a different league name and the new league will be stored in its own collection/sheet — data stays fully isolated.
|
|
125
139
|
|
|
126
140
|
### Viewing seeded data
|
|
127
141
|
|
|
128
|
-
|
|
142
|
+
**MongoDB:** Connect with any MongoDB client. Each league appears as a separate collection with `type: "league"`.
|
|
143
|
+
|
|
144
|
+
**Google Sheets:** Open your spreadsheet in a browser. Each league has its own sheet (tab) with league metadata in row 2.
|
|
129
145
|
|
|
130
146
|
### Production deployment
|
|
131
147
|
|
|
@@ -135,4 +151,23 @@ npm run build
|
|
|
135
151
|
npx vercel --prod
|
|
136
152
|
```
|
|
137
153
|
|
|
138
|
-
Set `IPL_POST_SECRET` in your Vercel dashboard.
|
|
154
|
+
Set `IPL_POST_SECRET` in your Vercel dashboard. Storage credentials are already populated in the scaffolded `.env`.
|
|
155
|
+
|
|
156
|
+
### Data sync workflow
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
# 1. Capture browser login (one-time)
|
|
160
|
+
npm run capture:ipl-auth
|
|
161
|
+
|
|
162
|
+
# 2. Scrape leaderboard (manual or watch mode)
|
|
163
|
+
npm run sync:ipl
|
|
164
|
+
npm run sync:ipl:watch # auto-poll every 2 min
|
|
165
|
+
|
|
166
|
+
# 3. Scrape transfer/booster data
|
|
167
|
+
npm run sync:ipl:transfers-daily
|
|
168
|
+
|
|
169
|
+
# 4. Run all scrapers (for cloud jobs)
|
|
170
|
+
npm run sync:cloud
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
See the [Available commands](#quick-start-after-scaffold) table above for all options.
|
package/package.json
CHANGED
package/src/index.mjs
CHANGED
|
@@ -3,12 +3,10 @@
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
5
|
import { mkdir } from "node:fs/promises";
|
|
6
|
-
import { getProjectName,
|
|
6
|
+
import { getProjectName, getConnectionUrl, getLeagueUrl, getLeagueName, close } from "./prompts.mjs";
|
|
7
7
|
import { scaffoldProject } from "./scaffold.mjs";
|
|
8
8
|
import { scrapeTeamsFromUrl } from "./scraper.mjs";
|
|
9
9
|
|
|
10
|
-
const DEFAULT_MONGODB_URI = "mongodb+srv://admin:admin@cricket.ptjjrub.mongodb.net/?appName=cricket";
|
|
11
|
-
|
|
12
10
|
function printHelp(exitCode) {
|
|
13
11
|
console.log(`
|
|
14
12
|
create-ipl-dashboard [project-name] [options]
|
|
@@ -61,18 +59,8 @@ async function main() {
|
|
|
61
59
|
|
|
62
60
|
await mkdir(projectPath, { recursive: true });
|
|
63
61
|
|
|
64
|
-
const
|
|
65
|
-
let mongoUri = "";
|
|
66
|
-
let googleSheets = {};
|
|
67
|
-
|
|
68
|
-
if (storageBackend === "google_sheets") {
|
|
69
|
-
googleSheets = await getGoogleSheetsConfig();
|
|
70
|
-
} else {
|
|
71
|
-
mongoUri = await getMongoUri(DEFAULT_MONGODB_URI);
|
|
72
|
-
}
|
|
73
|
-
|
|
62
|
+
const conn = await getConnectionUrl();
|
|
74
63
|
const leagueUrl = await getLeagueUrl();
|
|
75
|
-
const collectionName = await getCollectionName();
|
|
76
64
|
const leagueName = await getLeagueName();
|
|
77
65
|
let teams;
|
|
78
66
|
|
|
@@ -81,17 +69,12 @@ async function main() {
|
|
|
81
69
|
teams = await scrapeTeamsFromUrl(leagueUrl);
|
|
82
70
|
if (teams && teams.length > 0) {
|
|
83
71
|
console.log(` Found ${teams.length} team(s): ${teams.map((t) => t.name).join(", ")}`);
|
|
84
|
-
} else {
|
|
85
|
-
console.log(" Could not auto-detect teams. Enter them manually:");
|
|
86
|
-
teams = await getTeams();
|
|
87
72
|
}
|
|
88
|
-
} else {
|
|
89
|
-
teams = await getTeams();
|
|
90
73
|
}
|
|
91
74
|
|
|
92
75
|
close();
|
|
93
76
|
|
|
94
|
-
await scaffoldProject(projectPath, {
|
|
77
|
+
await scaffoldProject(projectPath, { conn, leagueUrl, leagueName, teams, skipInstall: flags.skipInstall });
|
|
95
78
|
|
|
96
79
|
console.log("");
|
|
97
80
|
console.log(" Next steps:");
|
package/src/prompts.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createInterface } from "node:readline";
|
|
2
2
|
import { stdin as input, stdout as output, stdin } from "node:process";
|
|
3
|
+
import * as readline from "node:readline";
|
|
3
4
|
|
|
4
5
|
const isTTY = stdin.isTTY;
|
|
5
6
|
|
|
@@ -24,96 +25,136 @@ function prompt(query) {
|
|
|
24
25
|
});
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
if (args[0]) return args[0];
|
|
29
|
-
if (!isTTY) {
|
|
30
|
-
await consumeAllLines();
|
|
31
|
-
return pipedLines[promptIndex++] || "ipl-dashboard";
|
|
32
|
-
}
|
|
33
|
-
return (await prompt("Project name: ")).trim() || "ipl-dashboard";
|
|
34
|
-
}
|
|
28
|
+
const BACKEND_VALUES = { mongodb: "mongodb", google_sheets: "google_sheets" };
|
|
35
29
|
|
|
36
|
-
|
|
30
|
+
async function select(message, choices, valueMap) {
|
|
37
31
|
if (!isTTY) {
|
|
38
32
|
await consumeAllLines();
|
|
39
|
-
|
|
33
|
+
const answer = (pipedLines[promptIndex++] || "").trim().toLowerCase().replace(/[\s_-]+/g, "");
|
|
34
|
+
const match = choices.find((c) => c.toLowerCase().replace(/[\s_-]+/g, "").startsWith(answer));
|
|
35
|
+
const idx = match ? choices.indexOf(match) : 0;
|
|
36
|
+
return valueMap ? valueMap[Object.keys(valueMap)[idx]] : choices[idx];
|
|
40
37
|
}
|
|
41
|
-
|
|
42
|
-
return
|
|
38
|
+
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
let selected = 0;
|
|
41
|
+
const rl = createInterface({ input, output });
|
|
42
|
+
|
|
43
|
+
function render(n) {
|
|
44
|
+
for (let i = 0; i < n; i++) {
|
|
45
|
+
readline.moveCursor(output, 0, -1);
|
|
46
|
+
readline.clearLine(output, 0);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
process.stdout.write(`${message}\n`);
|
|
51
|
+
choices.forEach((c, i) => {
|
|
52
|
+
process.stdout.write(i === selected ? `\x1B[7m ${c} \x1B[0m\n` : ` ${c}\n`);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const numLines = choices.length;
|
|
56
|
+
|
|
57
|
+
readline.emitKeypressEvents(input);
|
|
58
|
+
if (input.isTTY) input.setRawMode(true);
|
|
59
|
+
|
|
60
|
+
function cleanup() {
|
|
61
|
+
input.setRawMode(false);
|
|
62
|
+
input.pause();
|
|
63
|
+
input.removeListener("keypress", handler);
|
|
64
|
+
rl.close();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function handler(str, key) {
|
|
68
|
+
if (!key) return;
|
|
69
|
+
if (key.name === "up" && selected > 0) {
|
|
70
|
+
selected--;
|
|
71
|
+
for (let i = 0; i < numLines; i++) readline.moveCursor(output, 0, -1);
|
|
72
|
+
choices.forEach((c, i) => {
|
|
73
|
+
readline.clearLine(output, 0);
|
|
74
|
+
readline.cursorTo(output, 0);
|
|
75
|
+
output.write(i === selected ? `\x1B[7m ${c} \x1B[0m` : ` ${c} `);
|
|
76
|
+
if (i < numLines - 1) output.write("\n");
|
|
77
|
+
});
|
|
78
|
+
output.write("\n");
|
|
79
|
+
} else if (key.name === "down" && selected < numLines - 1) {
|
|
80
|
+
selected++;
|
|
81
|
+
for (let i = 0; i < numLines; i++) readline.moveCursor(output, 0, -1);
|
|
82
|
+
choices.forEach((c, i) => {
|
|
83
|
+
readline.clearLine(output, 0);
|
|
84
|
+
readline.cursorTo(output, 0);
|
|
85
|
+
output.write(i === selected ? `\x1B[7m ${c} \x1B[0m` : ` ${c} `);
|
|
86
|
+
if (i < numLines - 1) output.write("\n");
|
|
87
|
+
});
|
|
88
|
+
output.write("\n");
|
|
89
|
+
} else if (key.name === "return") {
|
|
90
|
+
cleanup();
|
|
91
|
+
for (let i = 0; i < numLines + 1; i++) {
|
|
92
|
+
readline.moveCursor(output, 0, -1);
|
|
93
|
+
readline.clearLine(output, 0);
|
|
94
|
+
}
|
|
95
|
+
output.write(`${message} \x1B[32m${choices[selected]}\x1B[0m\n`);
|
|
96
|
+
const keys = Object.keys(valueMap || {});
|
|
97
|
+
resolve(valueMap ? valueMap[keys[selected]] : choices[selected]);
|
|
98
|
+
} else if (key.name === "c" && key.ctrl) {
|
|
99
|
+
cleanup();
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
input.on("keypress", handler);
|
|
105
|
+
});
|
|
43
106
|
}
|
|
44
107
|
|
|
45
|
-
export async function
|
|
108
|
+
export async function getProjectName(args) {
|
|
109
|
+
if (args[0]) return args[0];
|
|
46
110
|
if (!isTTY) {
|
|
47
111
|
await consumeAllLines();
|
|
48
|
-
return
|
|
112
|
+
return pipedLines[promptIndex++] || "ipl-dashboard";
|
|
49
113
|
}
|
|
50
|
-
return (await prompt("
|
|
114
|
+
return (await prompt("Project name: ")).trim() || "ipl-dashboard";
|
|
51
115
|
}
|
|
52
116
|
|
|
53
117
|
export async function getStorageBackend() {
|
|
54
|
-
|
|
55
|
-
await consumeAllLines();
|
|
56
|
-
return (pipedLines[promptIndex++] || "mongodb").trim();
|
|
57
|
-
}
|
|
58
|
-
const answer = (await prompt("Storage backend (mongodb / google_sheets, default: mongodb): ")).trim().toLowerCase();
|
|
59
|
-
return answer === "google_sheets" ? "google_sheets" : "mongodb";
|
|
118
|
+
return select("Storage backend:", ["MongoDB", "Google Sheets"], BACKEND_VALUES);
|
|
60
119
|
}
|
|
61
120
|
|
|
62
|
-
export async function
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
121
|
+
export async function getConnectionUrl() {
|
|
122
|
+
const backend = await getStorageBackend();
|
|
123
|
+
if (backend === "google_sheets") {
|
|
124
|
+
if (!isTTY) {
|
|
125
|
+
await consumeAllLines();
|
|
126
|
+
return { backend, sheetId: (pipedLines[promptIndex++] || "").trim(), serviceAccountEmail: (pipedLines[promptIndex++] || "").trim(), privateKey: (pipedLines[promptIndex++] || "").trim() };
|
|
127
|
+
}
|
|
128
|
+
console.log("\n Google Sheets credentials:\n");
|
|
129
|
+
const sheetId = (await prompt(" Google Sheet ID: ")).trim();
|
|
130
|
+
const serviceAccountEmail = (await prompt(" Service account email: ")).trim();
|
|
131
|
+
const privateKey = (await prompt(" Private key (including -----BEGIN/END-----): ")).trim();
|
|
132
|
+
return { backend, sheetId, serviceAccountEmail, privateKey };
|
|
69
133
|
}
|
|
70
|
-
console.log("\n Google Sheets configuration:\n");
|
|
71
|
-
const sheetId = (await prompt(" Google Sheet ID: ")).trim();
|
|
72
|
-
const serviceAccountEmail = (await prompt(" Service account email: ")).trim();
|
|
73
|
-
const privateKey = (await prompt(" Private key (paste the full key, including -----BEGIN/END-----): ")).trim();
|
|
74
|
-
return { sheetId, serviceAccountEmail, privateKey };
|
|
75
|
-
}
|
|
76
134
|
|
|
77
|
-
|
|
135
|
+
const DEFAULT_MONGODB_URI = "mongodb+srv://admin:admin@cricket.ptjjrub.mongodb.net/?appName=cricket";
|
|
78
136
|
if (!isTTY) {
|
|
79
137
|
await consumeAllLines();
|
|
80
|
-
return (pipedLines[promptIndex++] || "
|
|
138
|
+
return { backend, uri: (pipedLines[promptIndex++] || DEFAULT_MONGODB_URI || "").trim() };
|
|
81
139
|
}
|
|
82
|
-
|
|
140
|
+
const answer = (await prompt("MongoDB URI (press Enter for default): ")).trim();
|
|
141
|
+
return { backend, uri: answer || DEFAULT_MONGODB_URI || "" };
|
|
83
142
|
}
|
|
84
143
|
|
|
85
|
-
export async function
|
|
144
|
+
export async function getLeagueUrl() {
|
|
86
145
|
if (!isTTY) {
|
|
87
146
|
await consumeAllLines();
|
|
88
|
-
return (pipedLines[promptIndex++] || "
|
|
147
|
+
return (pipedLines[promptIndex++] || "").trim();
|
|
89
148
|
}
|
|
90
|
-
return (await prompt("
|
|
149
|
+
return (await prompt("IPL fantasy league URL: ")).trim();
|
|
91
150
|
}
|
|
92
151
|
|
|
93
|
-
export async function
|
|
152
|
+
export async function getLeagueName() {
|
|
94
153
|
if (!isTTY) {
|
|
95
154
|
await consumeAllLines();
|
|
96
|
-
|
|
97
|
-
const teams = [];
|
|
98
|
-
for (let i = 0; i < count; i++) {
|
|
99
|
-
const line = pipedLines[promptIndex++] || "";
|
|
100
|
-
const parts = line.split(",").map((s) => s.trim());
|
|
101
|
-
teams.push({ id: i + 1, name: parts[0] || `Team ${i + 1}`, owner: parts[1] || parts[0] || `Team ${i + 1}` });
|
|
102
|
-
}
|
|
103
|
-
return teams;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const countAnswer = await prompt("How many teams in your league? (default: 9): ");
|
|
107
|
-
const count = Math.max(1, parseInt(countAnswer, 10) || 9);
|
|
108
|
-
|
|
109
|
-
console.log(`\nEnter ${count} team(s) — at minimum a name, optionally an owner:\n`);
|
|
110
|
-
const teams = [];
|
|
111
|
-
for (let i = 0; i < count; i++) {
|
|
112
|
-
const line = await prompt(` Team ${i + 1} (Name or "Name,Owner"): `);
|
|
113
|
-
const parts = line.split(",").map((s) => s.trim());
|
|
114
|
-
teams.push({ id: i + 1, name: parts[0] || `Team ${i + 1}`, owner: parts[1] || parts[0] || `Team ${i + 1}` });
|
|
155
|
+
return (pipedLines[promptIndex++] || "my_league").trim();
|
|
115
156
|
}
|
|
116
|
-
return
|
|
157
|
+
return (await prompt("League name (used as collection/table name, e.g. my_office_league): ")).trim() || "my_league";
|
|
117
158
|
}
|
|
118
159
|
|
|
119
160
|
export function close() {}
|
package/src/scaffold.mjs
CHANGED
|
@@ -10,7 +10,7 @@ const TEMPLATE_DIR = join(PACKAGE_ROOT, "template");
|
|
|
10
10
|
|
|
11
11
|
export async function scaffoldProject(
|
|
12
12
|
projectPath,
|
|
13
|
-
{
|
|
13
|
+
{ conn, leagueUrl, leagueName, teams, skipInstall = false },
|
|
14
14
|
) {
|
|
15
15
|
console.log(`\nCreating project at ${projectPath}...`);
|
|
16
16
|
|
|
@@ -34,17 +34,14 @@ export async function scaffoldProject(
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
await writeEnvFile(projectPath, {
|
|
37
|
-
|
|
38
|
-
storageBackend,
|
|
39
|
-
googleSheets,
|
|
37
|
+
conn,
|
|
40
38
|
leagueUrl,
|
|
41
|
-
collectionName,
|
|
42
39
|
leagueName,
|
|
43
40
|
});
|
|
44
|
-
await writeTeamData(projectPath, teams);
|
|
45
|
-
await writeLeagueData(projectPath, { leagueName, leagueUrl, teams });
|
|
41
|
+
await writeTeamData(projectPath, teams || []);
|
|
42
|
+
await writeLeagueData(projectPath, { leagueName, leagueUrl, teams: teams || [] });
|
|
46
43
|
await writeMatchPointsPlaceholder(projectPath);
|
|
47
|
-
await updatePackageJson(projectPath,
|
|
44
|
+
await updatePackageJson(projectPath, conn.backend);
|
|
48
45
|
|
|
49
46
|
for (const f of [
|
|
50
47
|
"app/api/ipl/live-snapshot.json",
|
|
@@ -62,7 +59,7 @@ export async function scaffoldProject(
|
|
|
62
59
|
if (leagueName) {
|
|
63
60
|
console.log(" Seeding league metadata...");
|
|
64
61
|
try {
|
|
65
|
-
execSync(`npm run seed:league -- "${leagueName}" "${
|
|
62
|
+
execSync(`npm run seed:league -- "${leagueName}" "${leagueName}"`, {
|
|
66
63
|
cwd: projectPath,
|
|
67
64
|
stdio: "inherit",
|
|
68
65
|
});
|
|
@@ -81,16 +78,15 @@ export async function scaffoldProject(
|
|
|
81
78
|
|
|
82
79
|
async function writeEnvFile(
|
|
83
80
|
projectPath,
|
|
84
|
-
{
|
|
81
|
+
{ conn, leagueUrl, leagueName },
|
|
85
82
|
) {
|
|
86
83
|
const envPath = join(projectPath, ".env");
|
|
87
84
|
const lines = [
|
|
88
|
-
`STORAGE_BACKEND=${
|
|
89
|
-
`
|
|
90
|
-
`
|
|
91
|
-
`
|
|
92
|
-
`
|
|
93
|
-
`GOOGLE_PRIVATE_KEY=${(googleSheets && googleSheets.privateKey) || ""}`,
|
|
85
|
+
`STORAGE_BACKEND=${conn.backend || "mongodb"}`,
|
|
86
|
+
`COLLECTION_NAME=${leagueName || "my_league"}`,
|
|
87
|
+
conn.backend === "google_sheets" ? `GOOGLE_SHEET_ID=${conn.sheetId || ""}` : `MONGODB_URI=${conn.uri || ""}`,
|
|
88
|
+
conn.backend === "google_sheets" ? `GOOGLE_SERVICE_ACCOUNT_EMAIL=${conn.serviceAccountEmail || ""}` : "",
|
|
89
|
+
conn.backend === "google_sheets" ? `GOOGLE_PRIVATE_KEY=${conn.privateKey || ""}` : "",
|
|
94
90
|
`IPL_LEAGUE_URL=${leagueUrl || ""}`,
|
|
95
91
|
`IPL_LEAGUE_NAME=${leagueName || ""}`,
|
|
96
92
|
`IPL_POST_SECRET=`,
|
|
@@ -100,7 +96,7 @@ async function writeEnvFile(
|
|
|
100
96
|
`IPL_API_LOG_PAYLOAD=0`,
|
|
101
97
|
`IPL_WRITE_SEED_DATA_FILE=1`,
|
|
102
98
|
``,
|
|
103
|
-
];
|
|
99
|
+
].filter(Boolean);
|
|
104
100
|
await writeFile(envPath, lines.join("\n"));
|
|
105
101
|
}
|
|
106
102
|
|
package/template/README.md
CHANGED
|
@@ -12,15 +12,7 @@ npx @vatvaghool/create-ipl-dashboard my-league
|
|
|
12
12
|
npm install
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
```env
|
|
18
|
-
MONGODB_URI=your_mongodb_connection_string
|
|
19
|
-
IPL_LEAGUE_URL=https://fantasy.iplt20.com/classic/league/view/your_league_id
|
|
20
|
-
IPL_POST_SECRET=your_secret_here
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
Start the app:
|
|
15
|
+
Start the dev server:
|
|
24
16
|
|
|
25
17
|
```bash
|
|
26
18
|
npm run dev:simple
|
|
@@ -28,6 +20,27 @@ npm run dev:simple
|
|
|
28
20
|
|
|
29
21
|
Open http://localhost:3000
|
|
30
22
|
|
|
23
|
+
### Available commands
|
|
24
|
+
|
|
25
|
+
| Command | Description |
|
|
26
|
+
|---------|-------------|
|
|
27
|
+
| `npm run dev` | Dev server with welcome splash |
|
|
28
|
+
| `npm run dev:simple` | Dev server (simple, no splash) |
|
|
29
|
+
| `npm run build` | Production build |
|
|
30
|
+
| `npm start` | Production server |
|
|
31
|
+
| `npm run capture:ipl-auth` | Capture Playwright login state (one-time) |
|
|
32
|
+
| `npm run sync:ipl` | Scrape live leaderboard snapshot |
|
|
33
|
+
| `npm run sync:ipl:watch` | Scrape leaderboard in watch mode (polls every 2 min) |
|
|
34
|
+
| `npm run sync:ipl:transfers-daily` | Scrape transfer/booster data |
|
|
35
|
+
| `npm run sync:cloud` | Run both leaderboard + transfer sync (for cloud jobs) |
|
|
36
|
+
| `npm run seed:league` | Seed league metadata into storage |
|
|
37
|
+
| `npm run seed:mongodb` | Seed initial raw user data from `data.ts` |
|
|
38
|
+
| `npm run seed:mongodb:reset` | Reset and re-seed MongoDB data |
|
|
39
|
+
| `npm run verify:production` | Verify production setup |
|
|
40
|
+
| `npm run monitor:ops` | Check ops health status |
|
|
41
|
+
| `npm run test` | Run test suite |
|
|
42
|
+
| `npm run lint` | Run linter |
|
|
43
|
+
|
|
31
44
|
---
|
|
32
45
|
|
|
33
46
|
## Screenshots
|
|
@@ -116,9 +129,9 @@ Open http://localhost:3000
|
|
|
116
129
|
|
|
117
130
|
```
|
|
118
131
|
GET /api/ipl resolution order:
|
|
119
|
-
1.
|
|
132
|
+
1. Storage raw users (if configured — MongoDB or Google Sheets)
|
|
120
133
|
2. Fallback: local seed data (app/api/ipl/data.ts)
|
|
121
|
-
3.
|
|
134
|
+
3. Storage live snapshot (if configured)
|
|
122
135
|
4. Fallback: local snapshot file (app/api/ipl/live-snapshot.json)
|
|
123
136
|
```
|
|
124
137
|
|
|
@@ -130,11 +143,16 @@ All hardcoded values are centralized in `app/lib/config.ts` and overridable via
|
|
|
130
143
|
|
|
131
144
|
| Variable | Default | Description |
|
|
132
145
|
|----------|---------|-------------|
|
|
133
|
-
| `
|
|
146
|
+
| `STORAGE_BACKEND` | `mongodb` | Storage backend: `mongodb` or `google_sheets` |
|
|
147
|
+
| `MONGODB_URI` | - | MongoDB connection string (for MongoDB backend) |
|
|
148
|
+
| `GOOGLE_SHEET_ID` | - | Google Sheet ID (for Google Sheets backend) |
|
|
149
|
+
| `GOOGLE_SERVICE_ACCOUNT_EMAIL` | - | Google service account email |
|
|
150
|
+
| `GOOGLE_PRIVATE_KEY` | - | Google service account private key |
|
|
151
|
+
| `LEAGUE_NAME` | - | League name (collection/sheet name) |
|
|
134
152
|
| `IPL_LEAGUE_URL` | fantasy.iplt20.com/... | Fantasy league page URL |
|
|
135
153
|
| `IPL_POST_SECRET` | - | Bearer token for POST endpoints |
|
|
136
|
-
| `IPL_DB_NAME` | `ipl` | MongoDB database name |
|
|
137
|
-
| `IPL_COLLECTION_NAME` | `ipl` | MongoDB collection name |
|
|
154
|
+
| `IPL_DB_NAME` | `ipl` | MongoDB database name (MongoDB only) |
|
|
155
|
+
| `IPL_COLLECTION_NAME` | `ipl` | MongoDB collection name (MongoDB only) |
|
|
138
156
|
| `IPL_TOTAL_TRANSFERS` | `160` | Total transfers per season |
|
|
139
157
|
| `IPL_SYNC_INTERVAL_MS` | `120000` | Dashboard polling interval |
|
|
140
158
|
| `IPL_DASHBOARD_STALE_MINUTES` | `20` (prod) / `180` (dev) | Staleness threshold |
|
|
@@ -144,19 +162,7 @@ All hardcoded values are centralized in `app/lib/config.ts` and overridable via
|
|
|
144
162
|
|
|
145
163
|
## Automation
|
|
146
164
|
|
|
147
|
-
|
|
148
|
-
# Capture login state for Playwright scrapers
|
|
149
|
-
npm run capture:ipl-auth
|
|
150
|
-
|
|
151
|
-
# Scrape live leaderboard snapshot
|
|
152
|
-
npm run sync:ipl
|
|
153
|
-
|
|
154
|
-
# Scrape transfer/booster data
|
|
155
|
-
npm run sync:ipl:transfers
|
|
156
|
-
|
|
157
|
-
# Run both in sequence (for cloud jobs)
|
|
158
|
-
npm run sync:cloud
|
|
159
|
-
```
|
|
165
|
+
See the [commands table](#available-commands) above for all options.
|
|
160
166
|
|
|
161
167
|
Also supports a browser bookmarklet — visit `/api/ipl/bookmarklet` while the app is running, copy the returned `javascript:` code, and save it as a bookmark. Click it on the fantasy leaderboard page to sync data.
|
|
162
168
|
|
|
@@ -178,7 +184,12 @@ Set these environment variables in your Vercel project dashboard:
|
|
|
178
184
|
|
|
179
185
|
| Variable | Required | Description |
|
|
180
186
|
|----------|----------|-------------|
|
|
181
|
-
| `
|
|
187
|
+
| `STORAGE_BACKEND` | Yes | `mongodb` or `google_sheets` |
|
|
188
|
+
| `MONGODB_URI` | Yes* | MongoDB connection string (required if `mongodb`) |
|
|
189
|
+
| `GOOGLE_SHEET_ID` | Yes* | Google Sheet ID (required if `google_sheets`) |
|
|
190
|
+
| `GOOGLE_SERVICE_ACCOUNT_EMAIL` | Yes* | Service account email (required if `google_sheets`) |
|
|
191
|
+
| `GOOGLE_PRIVATE_KEY` | Yes* | Private key (required if `google_sheets`) |
|
|
192
|
+
| `LEAGUE_NAME` | Yes | Collection or sheet name |
|
|
182
193
|
| `IPL_POST_SECRET` | Yes | Bearer token protecting write endpoints |
|
|
183
194
|
| `IPL_LEAGUE_URL` | Yes | Your fantasy league page URL |
|
|
184
195
|
| `IPL_DASHBOARD_STALE_MINUTES` | No | Staleness threshold for health checks (default: `20`) |
|
|
@@ -204,7 +215,7 @@ The app runs as a standard Node.js server on `process.env.PORT` (default `3000`)
|
|
|
204
215
|
|
|
205
216
|
### Production requirements
|
|
206
217
|
|
|
207
|
-
- **
|
|
218
|
+
- **Storage** (MongoDB or Google Sheets) is required in production for POST endpoints (returns `503` without it)
|
|
208
219
|
- **IPL_POST_SECRET** should be a strong random value set in your hosting environment
|
|
209
220
|
- The scraper should run outside the web app (Cloud Run, GitHub Actions, cron) — never in the same process
|
|
210
221
|
|
|
@@ -218,6 +229,6 @@ The app runs as a standard Node.js server on `process.env.PORT` (default `3000`)
|
|
|
218
229
|
- **Tailwind CSS** 4
|
|
219
230
|
- **Recharts** — charts
|
|
220
231
|
- **Framer Motion** — animations
|
|
221
|
-
- **MongoDB** — data persistence
|
|
232
|
+
- **MongoDB** or **Google Sheets** — data persistence
|
|
222
233
|
- **Zustand** — client state
|
|
223
234
|
- **Playwright** — scraper automation
|