@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 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 | Display name for your league |
38
- | Teams | Team names (and optional owners) in your league
37
+ | League name | Used as collection/table name in storage |
39
38
 
40
- If `--scrape` is provided, the CLI attempts to extract team names from the league page HTML. If that fails, it falls back to manual entry.
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 collection name (e.g. `ipl_2025_friends_league`) and the new league will be stored in its own collection — data stays fully isolated.
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
- Connect to the MongoDB instance with any MongoDB client. Each league appears as a separate collection containing a document with `type: "league"` and all the metadata (name, URL, teams, timestamps).
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. The `MONGODB_URI` and `COLLECTION_NAME` are already populated in the scaffolded `.env`.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vatvaghool/create-ipl-dashboard",
3
- "version": "0.1.15",
3
+ "version": "0.1.18",
4
4
  "description": "Scaffold an IPL fantasy cricket dashboard project",
5
5
  "type": "module",
6
6
  "bin": {
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, getMongoUri, getStorageBackend, getGoogleSheetsConfig, getLeagueUrl, getCollectionName, getLeagueName, getTeams, close } from "./prompts.mjs";
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 storageBackend = await getStorageBackend();
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, { mongoUri, storageBackend, googleSheets, leagueUrl, collectionName, leagueName, teams, skipInstall: flags.skipInstall });
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
- export async function getProjectName(args) {
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
- export async function getMongoUri(defaultUri) {
30
+ async function select(message, choices, valueMap) {
37
31
  if (!isTTY) {
38
32
  await consumeAllLines();
39
- return (pipedLines[promptIndex++] || defaultUri || "").trim();
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
- const answer = (await prompt(`MongoDB URI (press Enter for default): `)).trim();
42
- return answer || defaultUri || "";
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 getLeagueUrl() {
108
+ export async function getProjectName(args) {
109
+ if (args[0]) return args[0];
46
110
  if (!isTTY) {
47
111
  await consumeAllLines();
48
- return (pipedLines[promptIndex++] || "").trim();
112
+ return pipedLines[promptIndex++] || "ipl-dashboard";
49
113
  }
50
- return (await prompt("IPL fantasy league URL (or press Enter to skip, can be set later): ")).trim();
114
+ return (await prompt("Project name: ")).trim() || "ipl-dashboard";
51
115
  }
52
116
 
53
117
  export async function getStorageBackend() {
54
- if (!isTTY) {
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 getGoogleSheetsConfig() {
63
- if (!isTTY) {
64
- await consumeAllLines();
65
- const sheetId = (pipedLines[promptIndex++] || "").trim();
66
- const email = (pipedLines[promptIndex++] || "").trim();
67
- const key = (pipedLines[promptIndex++] || "").trim();
68
- return { sheetId, serviceAccountEmail: email, privateKey: key };
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
- export async function getCollectionName() {
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++] || "league_data").trim();
138
+ return { backend, uri: (pipedLines[promptIndex++] || DEFAULT_MONGODB_URI || "").trim() };
81
139
  }
82
- return (await prompt("Collection/sheet name for this league's data (e.g. ipl_2025_office_league): ")).trim() || "league_data";
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 getLeagueName() {
144
+ export async function getLeagueUrl() {
86
145
  if (!isTTY) {
87
146
  await consumeAllLines();
88
- return (pipedLines[promptIndex++] || "My IPL League").trim();
147
+ return (pipedLines[promptIndex++] || "").trim();
89
148
  }
90
- return (await prompt("League name (e.g. Office Fantasy League): ")).trim() || "My IPL League";
149
+ return (await prompt("IPL fantasy league URL: ")).trim();
91
150
  }
92
151
 
93
- export async function getTeams() {
152
+ export async function getLeagueName() {
94
153
  if (!isTTY) {
95
154
  await consumeAllLines();
96
- const count = Math.max(1, parseInt(pipedLines[promptIndex++], 10) || 9);
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 teams;
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
- { mongoUri, storageBackend, googleSheets, leagueUrl, collectionName, leagueName, teams, skipInstall = false },
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
- mongoUri,
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, storageBackend);
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}" "${collectionName}"`, {
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
- { mongoUri, storageBackend, googleSheets, leagueUrl, collectionName, leagueName },
81
+ { conn, leagueUrl, leagueName },
85
82
  ) {
86
83
  const envPath = join(projectPath, ".env");
87
84
  const lines = [
88
- `STORAGE_BACKEND=${storageBackend || "mongodb"}`,
89
- `MONGODB_URI=${mongoUri || ""}`,
90
- `COLLECTION_NAME=${collectionName || "league_data"}`,
91
- `GOOGLE_SHEET_ID=${(googleSheets && googleSheets.sheetId) || ""}`,
92
- `GOOGLE_SERVICE_ACCOUNT_EMAIL=${(googleSheets && googleSheets.serviceAccountEmail) || ""}`,
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
 
@@ -12,15 +12,7 @@ npx @vatvaghool/create-ipl-dashboard my-league
12
12
  npm install
13
13
  ```
14
14
 
15
- Create a `.env` file:
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. MongoDB raw users (if MONGODB_URI configured)
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. MongoDB live snapshot (if MONGODB_URI configured)
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
- | `MONGODB_URI` | - | MongoDB connection string |
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
- ```bash
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
- | `MONGODB_URI` | Yes | MongoDB connection string |
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
- - **MongoDB** is required in production for POST endpoints (returns `503` without it)
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