@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.
- package/README.md +75 -0
- package/package.json +27 -0
- package/src/generate-template.mjs +73 -0
- package/src/index.mjs +98 -0
- package/src/prompts.mjs +78 -0
- package/src/scaffold.mjs +129 -0
- package/src/scraper.mjs +79 -0
- package/template/.dockerignore +13 -0
- package/template/AGENTS.md +5 -0
- package/template/Dockerfile.sync +14 -0
- package/template/README.md +160 -0
- package/template/app/api/ipl/data.ts +24 -0
- package/template/app/api/ipl/route.ts +505 -0
- package/template/app/api/ipl/transfers/route.ts +261 -0
- package/template/app/api/ipl/transfers/transform.ts +156 -0
- package/template/app/api/ipl/transform.ts +20 -0
- package/template/app/api/ipl/upcoming-matches/route.ts +18 -0
- package/template/app/api/ops/status/route.ts +225 -0
- package/template/app/components/AIRoasting.tsx +278 -0
- package/template/app/components/ColorWave.tsx +193 -0
- package/template/app/components/CrownBattle.tsx +207 -0
- package/template/app/components/DashboardContent.tsx +377 -0
- package/template/app/components/FantasyStockTicker.tsx +192 -0
- package/template/app/components/FireworksBurst.tsx +225 -0
- package/template/app/components/LiveMatchTicker.tsx +117 -0
- package/template/app/components/MatchRecapScroll.tsx +135 -0
- package/template/app/components/MatchStoryScrubber.tsx +274 -0
- package/template/app/components/PerformanceTracker.tsx +132 -0
- package/template/app/components/ProgressGlowRings.tsx +157 -0
- package/template/app/components/TeamDNAScanner.tsx +238 -0
- package/template/app/components/ThemeToggle.tsx +74 -0
- package/template/app/components/dashboard/CaptainBoard.tsx +138 -0
- package/template/app/components/dashboard/ChartBoard.tsx +162 -0
- package/template/app/components/dashboard/LatestBadge.tsx +23 -0
- package/template/app/components/dashboard/LedgerTable.tsx +385 -0
- package/template/app/components/dashboard/SectionCard.tsx +59 -0
- package/template/app/components/dashboard/StickyMini.tsx +20 -0
- package/template/app/components/dashboard/index.ts +6 -0
- package/template/app/components/ui/DashboardChartFrame.tsx +74 -0
- package/template/app/components/ui/DoodleSpinner.tsx +15 -0
- package/template/app/components/ui/TeamPills.tsx +41 -0
- package/template/app/data/match-points.ts +3 -0
- package/template/app/data/teams.ts +32 -0
- package/template/app/globals.css +1267 -0
- package/template/app/hooks/dashboard/index.ts +1 -0
- package/template/app/hooks/dashboard/useDashboardModel.ts +25 -0
- package/template/app/hooks/dashboardCache.ts +53 -0
- package/template/app/hooks/dashboardPolling.ts +53 -0
- package/template/app/hooks/snapshotCache.ts +47 -0
- package/template/app/hooks/useDashboardData.ts +28 -0
- package/template/app/layout.tsx +75 -0
- package/template/app/lib/aiAgent.ts +444 -0
- package/template/app/lib/config.ts +29 -0
- package/template/app/lib/dashboard/index.ts +1 -0
- package/template/app/lib/dashboard/model.ts +257 -0
- package/template/app/lib/dashboardData.ts +50 -0
- package/template/app/lib/dashboardView.ts +22 -0
- package/template/app/lib/detailedData.ts +112 -0
- package/template/app/lib/matchStatus.ts +28 -0
- package/template/app/lib/matches.ts +131 -0
- package/template/app/lib/teamBadges.ts +223 -0
- package/template/app/lib/upcomingMatches.ts +154 -0
- package/template/app/lib/useDb.ts +29 -0
- package/template/app/lib/utils/diff.ts +24 -0
- package/template/app/lib/utils/getChartColor.ts +17 -0
- package/template/app/lib/utils/getStdDeviation.ts +6 -0
- package/template/app/lib/utils/time.ts +40 -0
- package/template/app/lib/utils.ts +70 -0
- package/template/app/page.tsx +15 -0
- package/template/app/store/dashboardStore.ts +85 -0
- package/template/app/types/dashboard.ts +75 -0
- package/template/app/types.ts +130 -0
- package/template/app/utils/dashboard/index.ts +72 -0
- package/template/eslint.config.mjs +18 -0
- package/template/infra/cloud-run/README.md +68 -0
- package/template/infra/cloud-run/sync-job.yaml +32 -0
- package/template/infra/cutover/README.md +84 -0
- package/template/infra/vercel/README.md +57 -0
- package/template/next.config.ts +7 -0
- package/template/package-lock.json +7330 -0
- package/template/package.json +47 -0
- package/template/packages/ipl-dashboard-utils/README.md +316 -0
- package/template/packages/ipl-dashboard-utils/package.json +34 -0
- package/template/packages/ipl-dashboard-utils/src/index.ts +22 -0
- package/template/packages/ipl-dashboard-utils/src/transform.ts +687 -0
- package/template/packages/ipl-dashboard-utils/src/types.ts +88 -0
- package/template/packages/ipl-dashboard-utils/tsconfig.build.json +17 -0
- package/template/postcss.config.mjs +7 -0
- package/template/scripts/capture-ipl-auth.mjs +54 -0
- package/template/scripts/deploy-cloud-run-sync.sh +48 -0
- package/template/scripts/deploy-cloud-scheduler.sh +42 -0
- package/template/scripts/dev-simple.js +31 -0
- package/template/scripts/dev-welcome.mjs +38 -0
- package/template/scripts/monitor-ops-status.sh +50 -0
- package/template/scripts/seed-mongodb.ts +115 -0
- package/template/scripts/sync-cloud.mjs +50 -0
- package/template/scripts/sync-ipl.mjs +238 -0
- package/template/scripts/sync-transfers-daily.mjs +175 -0
- package/template/scripts/verify-production.mjs +108 -0
- package/template/tests/coverage-gaps.test.ts +290 -0
- package/template/tests/dashboard-polling.test.ts +96 -0
- package/template/tests/detailed-data.test.ts +60 -0
- package/template/tests/ipl-transform.test.ts +590 -0
- package/template/tests/transfers-route.test.ts +109 -0
- package/template/tests/upcoming-matches.test.ts +34 -0
- package/template/tests/utils-and-cache.test.ts +267 -0
- package/template/tsconfig.json +35 -0
- package/template/vercel.json +7 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import type { ComponentType } from "react";
|
|
3
|
+
|
|
4
|
+
export interface InsightCard {
|
|
5
|
+
id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
note: string;
|
|
8
|
+
accent: string;
|
|
9
|
+
icon: ComponentType<{ className?: string }>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SummaryCard {
|
|
13
|
+
id: string;
|
|
14
|
+
title: string;
|
|
15
|
+
value: string;
|
|
16
|
+
note: string;
|
|
17
|
+
accent: string;
|
|
18
|
+
icon: ComponentType<{ className?: string }>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ChartItem {
|
|
22
|
+
label: string;
|
|
23
|
+
value: number;
|
|
24
|
+
sublabel: string;
|
|
25
|
+
difference?: number;
|
|
26
|
+
fill?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface DashboardModel {
|
|
30
|
+
leader: any; // From OverallChartItem
|
|
31
|
+
topTeams: any[];
|
|
32
|
+
normalizedOverall: any[];
|
|
33
|
+
teamBadges: any;
|
|
34
|
+
insights: InsightCard[];
|
|
35
|
+
summaryCards: SummaryCard[];
|
|
36
|
+
barLeaderboard: ChartItem[];
|
|
37
|
+
liveBars: ChartItem[];
|
|
38
|
+
topTeamsChart: ChartItem[];
|
|
39
|
+
captainTeams: any[];
|
|
40
|
+
tableRows: any[];
|
|
41
|
+
liveLeader: any;
|
|
42
|
+
liveLabel: string;
|
|
43
|
+
liveChartTitle: string;
|
|
44
|
+
matchCount: number;
|
|
45
|
+
hottest: any;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface SectionCardProps {
|
|
49
|
+
title: string;
|
|
50
|
+
note: string;
|
|
51
|
+
children: React.ReactNode;
|
|
52
|
+
className?: string;
|
|
53
|
+
index: number;
|
|
54
|
+
icon: ComponentType<{ className?: string }>;
|
|
55
|
+
accent?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface StickyMiniProps {
|
|
59
|
+
title: string;
|
|
60
|
+
value: string;
|
|
61
|
+
note: string;
|
|
62
|
+
variant: "yellow" | "blue" | "pink";
|
|
63
|
+
icon: ComponentType<{ className?: string }>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface ChartBoardProps {
|
|
67
|
+
items: ChartItem[];
|
|
68
|
+
valueLabel: string;
|
|
69
|
+
emptyLabel?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface LatestBadgeProps {
|
|
73
|
+
pts: number;
|
|
74
|
+
isTop: boolean;
|
|
75
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
export type OverallChartItem = {
|
|
2
|
+
name: string;
|
|
3
|
+
points: number;
|
|
4
|
+
rank: number;
|
|
5
|
+
previousRank?: number;
|
|
6
|
+
previousPoints?: number;
|
|
7
|
+
lastMatchPoints?: number;
|
|
8
|
+
gapToNext?: number;
|
|
9
|
+
gapPercent?: number;
|
|
10
|
+
movement?: "up" | "down" | "same" | "new";
|
|
11
|
+
transfersLeft?: number;
|
|
12
|
+
transfersUsed?: number;
|
|
13
|
+
totalTransfers?: number;
|
|
14
|
+
boostersUsed?: string;
|
|
15
|
+
efficiency?: number;
|
|
16
|
+
isLastMatchLeader?: boolean;
|
|
17
|
+
captain?: ScrapedSquadPlayer | null;
|
|
18
|
+
viceCaptain?: ScrapedSquadPlayer | null;
|
|
19
|
+
players?: ScrapedSquadPlayer[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type DailyChartRow = {
|
|
23
|
+
day: string;
|
|
24
|
+
[teamName: string]: string | number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type DashboardData = {
|
|
28
|
+
overall: OverallChartItem[];
|
|
29
|
+
daily: DailyChartRow[];
|
|
30
|
+
updatedAt?: string;
|
|
31
|
+
dailyTransferUpdatedAt?: string;
|
|
32
|
+
completedMatches?: number;
|
|
33
|
+
source?: "database";
|
|
34
|
+
snapshot?: ScrapedDashboardPayload;
|
|
35
|
+
transfers?: ScrapedTransferSnapshot;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type RawApiUser = {
|
|
39
|
+
rno: number;
|
|
40
|
+
temname: string;
|
|
41
|
+
points: number;
|
|
42
|
+
matches: Array<{
|
|
43
|
+
matchId: number;
|
|
44
|
+
points: number;
|
|
45
|
+
}>;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type TeamRecord = {
|
|
49
|
+
id: number;
|
|
50
|
+
name: string;
|
|
51
|
+
owner: string;
|
|
52
|
+
fantasyNames: string[];
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type TeamMatchPointsRecord = {
|
|
56
|
+
teamId: number;
|
|
57
|
+
matchId: number;
|
|
58
|
+
points: number;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type LeaderSnapshotRecord = {
|
|
62
|
+
teamId: number;
|
|
63
|
+
rank: number;
|
|
64
|
+
totalPoints: number;
|
|
65
|
+
lastMatchPoints?: number;
|
|
66
|
+
transfersLeft?: number;
|
|
67
|
+
transfersUsed?: number;
|
|
68
|
+
totalTransfers?: number;
|
|
69
|
+
boostersUsed?: string;
|
|
70
|
+
captain?: ScrapedSquadPlayer | null;
|
|
71
|
+
viceCaptain?: ScrapedSquadPlayer | null;
|
|
72
|
+
players?: ScrapedSquadPlayer[];
|
|
73
|
+
capturedAt?: string;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type ScrapedSquadPlayer = {
|
|
77
|
+
name: string;
|
|
78
|
+
number?: string;
|
|
79
|
+
points?: number;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export type ScrapedLeaderboardItem = {
|
|
83
|
+
rank: number;
|
|
84
|
+
name: string;
|
|
85
|
+
points: number;
|
|
86
|
+
matchesPlayed?: number;
|
|
87
|
+
lastMatchPoints?: number;
|
|
88
|
+
netPoints?: number;
|
|
89
|
+
netRank?: number;
|
|
90
|
+
transfersLeft?: number;
|
|
91
|
+
transfersUsed?: number;
|
|
92
|
+
totalTransfers?: number;
|
|
93
|
+
boostersUsed?: string;
|
|
94
|
+
captain?: ScrapedSquadPlayer | null;
|
|
95
|
+
viceCaptain?: ScrapedSquadPlayer | null;
|
|
96
|
+
players?: ScrapedSquadPlayer[];
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export type ScrapedDashboardPayload = {
|
|
100
|
+
updatedAt?: string;
|
|
101
|
+
dailyTransferUpdatedAt?: string;
|
|
102
|
+
completedMatches?: number;
|
|
103
|
+
leaders: ScrapedLeaderboardItem[];
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export type ScrapedTransferItem = {
|
|
107
|
+
team: string;
|
|
108
|
+
matchesPlayed: number;
|
|
109
|
+
boostersUsed: number;
|
|
110
|
+
transfersLeft: string;
|
|
111
|
+
updatedAt: string;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export type ScrapedTransferSnapshot = {
|
|
115
|
+
updatedAt?: string;
|
|
116
|
+
teams: ScrapedTransferItem[];
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export type RawOverviewUser = {
|
|
120
|
+
name: string;
|
|
121
|
+
totalPoints: number;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export type RawDailyUser = {
|
|
125
|
+
name: string;
|
|
126
|
+
daily: Array<{
|
|
127
|
+
day: string;
|
|
128
|
+
points: number;
|
|
129
|
+
}>;
|
|
130
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { getChartColor } from "../../lib/utils/getChartColor";
|
|
2
|
+
|
|
3
|
+
export function formatCompactNumber(value: number): string {
|
|
4
|
+
if (value >= 1000000) {
|
|
5
|
+
return `${(value / 1000000).toFixed(1)}M`;
|
|
6
|
+
}
|
|
7
|
+
if (value >= 1000) {
|
|
8
|
+
return `${(value / 1000).toFixed(1)}K`;
|
|
9
|
+
}
|
|
10
|
+
return value.toString();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function formatLedgerNumber(value?: number): string {
|
|
14
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
15
|
+
return "-";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return value.toLocaleString("en-IN", {
|
|
19
|
+
maximumFractionDigits: value % 1 === 0 ? 0 : 1,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function formatDateTime(value?: string | null): string {
|
|
24
|
+
if (!value) {
|
|
25
|
+
return "just now";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const date = new Date(value);
|
|
29
|
+
if (Number.isNaN(date.getTime())) {
|
|
30
|
+
return "just now";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return date.toLocaleString("en-IN", {
|
|
34
|
+
day: "2-digit",
|
|
35
|
+
month: "short",
|
|
36
|
+
hour: "2-digit",
|
|
37
|
+
minute: "2-digit",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getBoosterCount(value?: string): number {
|
|
42
|
+
if (!value) {
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const numeric = Number(value.trim());
|
|
47
|
+
return Number.isFinite(numeric) && numeric > 0 ? numeric : 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getTeamColor(index: number, total: number): string {
|
|
51
|
+
return getChartColor(index, total);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function createChartData(
|
|
55
|
+
items: Array<{
|
|
56
|
+
label: string;
|
|
57
|
+
value: number;
|
|
58
|
+
sublabel: string;
|
|
59
|
+
difference?: number;
|
|
60
|
+
}>,
|
|
61
|
+
): Array<{
|
|
62
|
+
label: string;
|
|
63
|
+
value: number;
|
|
64
|
+
sublabel: string;
|
|
65
|
+
difference?: number;
|
|
66
|
+
fill: string;
|
|
67
|
+
}> {
|
|
68
|
+
return items.map((item, index) => ({
|
|
69
|
+
...item,
|
|
70
|
+
fill: getChartColor(index, items.length),
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineConfig, globalIgnores } from "eslint/config";
|
|
2
|
+
import nextVitals from "eslint-config-next/core-web-vitals";
|
|
3
|
+
import nextTs from "eslint-config-next/typescript";
|
|
4
|
+
|
|
5
|
+
const eslintConfig = defineConfig([
|
|
6
|
+
...nextVitals,
|
|
7
|
+
...nextTs,
|
|
8
|
+
// Override default ignores of eslint-config-next.
|
|
9
|
+
globalIgnores([
|
|
10
|
+
// Default ignores of eslint-config-next:
|
|
11
|
+
".next/**",
|
|
12
|
+
"out/**",
|
|
13
|
+
"build/**",
|
|
14
|
+
"next-env.d.ts",
|
|
15
|
+
]),
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export default eslintConfig;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Cloud Run Scraper Job
|
|
2
|
+
|
|
3
|
+
This folder contains the deployment scaffold for moving the Playwright sync off local machines and scheduled GitHub Actions.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
- `sync-job.yaml`: example Cloud Run Job manifest
|
|
8
|
+
|
|
9
|
+
The container image is built from the repo root `Dockerfile.sync`.
|
|
10
|
+
|
|
11
|
+
## Required secrets
|
|
12
|
+
|
|
13
|
+
Create these in Google Secret Manager before deploying:
|
|
14
|
+
|
|
15
|
+
- `IPL_POST_SECRET`
|
|
16
|
+
- `IPL_STORAGE_STATE_B64`
|
|
17
|
+
|
|
18
|
+
`IPL_STORAGE_STATE_B64` should contain the base64-encoded contents of `ipl-auth.json`.
|
|
19
|
+
|
|
20
|
+
## Required env vars
|
|
21
|
+
|
|
22
|
+
- `IPL_API_BASE_URL`
|
|
23
|
+
- `IPL_LEAGUE_URL`
|
|
24
|
+
- `IPL_HEADLESS=1`
|
|
25
|
+
|
|
26
|
+
## Deploy
|
|
27
|
+
|
|
28
|
+
From the repo root:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
PROJECT_ID=your-gcp-project \
|
|
32
|
+
REGION=asia-south1 \
|
|
33
|
+
APP_BASE_URL=https://your-app-domain \
|
|
34
|
+
bash scripts/deploy-cloud-run-sync.sh
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Schedule
|
|
38
|
+
|
|
39
|
+
After the job exists, attach Cloud Scheduler to it on a 5-10 minute cadence:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
PROJECT_ID=your-gcp-project \
|
|
43
|
+
REGION=asia-south1 \
|
|
44
|
+
SERVICE_ACCOUNT_EMAIL=your-scheduler-sa@your-gcp-project.iam.gserviceaccount.com \
|
|
45
|
+
bash scripts/deploy-cloud-scheduler.sh
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Notes
|
|
49
|
+
|
|
50
|
+
- The web app should already be deployed with `MONGODB_URI` and `IPL_POST_SECRET`.
|
|
51
|
+
- The scraper job is intentionally separate from the Next.js runtime so browser automation does not compete with web traffic.
|
|
52
|
+
- Run `APP_BASE_URL=https://your-app-domain npm run verify:production` before wiring the scheduler.
|
|
53
|
+
|
|
54
|
+
## Alerting hook
|
|
55
|
+
|
|
56
|
+
Use `/api/ops/status` as the primary freshness endpoint.
|
|
57
|
+
|
|
58
|
+
Suggested alert behavior:
|
|
59
|
+
|
|
60
|
+
- alert immediately if it returns `503`
|
|
61
|
+
- alert if Cloud Run job executions start failing repeatedly
|
|
62
|
+
- alert if Cloud Scheduler stops invoking the job on schedule
|
|
63
|
+
|
|
64
|
+
Quick manual check:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
APP_BASE_URL=https://your-app-domain npm run monitor:ops
|
|
68
|
+
```
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
apiVersion: run.googleapis.com/v1
|
|
2
|
+
kind: Job
|
|
3
|
+
metadata:
|
|
4
|
+
name: ipl-sync
|
|
5
|
+
spec:
|
|
6
|
+
template:
|
|
7
|
+
spec:
|
|
8
|
+
template:
|
|
9
|
+
spec:
|
|
10
|
+
timeoutSeconds: 1200
|
|
11
|
+
maxRetries: 1
|
|
12
|
+
containers:
|
|
13
|
+
- image: REGION-docker.pkg.dev/PROJECT_ID/ipl-dashboard/ipl-sync:latest
|
|
14
|
+
env:
|
|
15
|
+
- name: NODE_ENV
|
|
16
|
+
value: production
|
|
17
|
+
- name: IPL_HEADLESS
|
|
18
|
+
value: "1"
|
|
19
|
+
- name: IPL_LEAGUE_URL
|
|
20
|
+
value: https://fantasy.iplt20.com/classic/league/view/66930102
|
|
21
|
+
- name: IPL_API_BASE_URL
|
|
22
|
+
value: https://YOUR_APP_DOMAIN
|
|
23
|
+
- name: IPL_POST_SECRET
|
|
24
|
+
valueFrom:
|
|
25
|
+
secretKeyRef:
|
|
26
|
+
name: IPL_POST_SECRET
|
|
27
|
+
key: latest
|
|
28
|
+
- name: IPL_STORAGE_STATE_B64
|
|
29
|
+
valueFrom:
|
|
30
|
+
secretKeyRef:
|
|
31
|
+
name: IPL_STORAGE_STATE_B64
|
|
32
|
+
key: latest
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Production Cutover Checklist
|
|
2
|
+
|
|
3
|
+
Use this checklist when moving from local/dev operation to the cloud-first production setup.
|
|
4
|
+
|
|
5
|
+
## 1. Prepare secrets
|
|
6
|
+
|
|
7
|
+
- Create `MONGODB_URI` for the Vercel project
|
|
8
|
+
- Create `IPL_POST_SECRET` for both Vercel and the scraper job
|
|
9
|
+
- Create `IPL_STORAGE_STATE_B64` in Google Secret Manager
|
|
10
|
+
|
|
11
|
+
## 2. Deploy the web app
|
|
12
|
+
|
|
13
|
+
- Import the repo into Vercel
|
|
14
|
+
- Set production env vars
|
|
15
|
+
- Deploy the app
|
|
16
|
+
- Attach the final custom domain
|
|
17
|
+
|
|
18
|
+
Reference:
|
|
19
|
+
|
|
20
|
+
- `infra/vercel/README.md`
|
|
21
|
+
|
|
22
|
+
## 3. Verify the deployed app
|
|
23
|
+
|
|
24
|
+
Run:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
APP_BASE_URL=https://your-app-domain npm run verify:production
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Expected:
|
|
31
|
+
|
|
32
|
+
- `/api/ops/status` returns JSON
|
|
33
|
+
- `/api/ipl` returns dashboard data or a structured error
|
|
34
|
+
- `/api/ipl?format=snapshot` returns snapshot data or a structured error
|
|
35
|
+
- `/api/ipl/transfers` returns transfer data or a structured error
|
|
36
|
+
|
|
37
|
+
## 4. Deploy the scraper job
|
|
38
|
+
|
|
39
|
+
Run:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
PROJECT_ID=your-gcp-project \
|
|
43
|
+
REGION=asia-south1 \
|
|
44
|
+
APP_BASE_URL=https://your-app-domain \
|
|
45
|
+
bash scripts/deploy-cloud-run-sync.sh
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Reference:
|
|
49
|
+
|
|
50
|
+
- `infra/cloud-run/README.md`
|
|
51
|
+
|
|
52
|
+
## 5. Attach the scheduler
|
|
53
|
+
|
|
54
|
+
Run:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
PROJECT_ID=your-gcp-project \
|
|
58
|
+
REGION=asia-south1 \
|
|
59
|
+
SERVICE_ACCOUNT_EMAIL=your-scheduler-sa@your-gcp-project.iam.gserviceaccount.com \
|
|
60
|
+
bash scripts/deploy-cloud-scheduler.sh
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## 6. Run one manual sync
|
|
64
|
+
|
|
65
|
+
Trigger the Cloud Run job once manually and confirm:
|
|
66
|
+
|
|
67
|
+
- `/api/ops/status` shows Mongo connected
|
|
68
|
+
- `/api/ipl?format=snapshot` returns leaders
|
|
69
|
+
- `/api/ipl/transfers` returns teams
|
|
70
|
+
|
|
71
|
+
## 7. Disable legacy paths
|
|
72
|
+
|
|
73
|
+
- Keep GitHub Actions sync as manual fallback only
|
|
74
|
+
- Do not rely on local file persistence in production
|
|
75
|
+
- Keep `IPL_WRITE_SEED_DATA_FILE` unset in production
|
|
76
|
+
|
|
77
|
+
## 8. Monitor after cutover
|
|
78
|
+
|
|
79
|
+
Watch:
|
|
80
|
+
|
|
81
|
+
- Vercel function logs
|
|
82
|
+
- Cloud Run job executions
|
|
83
|
+
- Cloud Scheduler execution history
|
|
84
|
+
- MongoDB Atlas metrics
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Vercel Deployment
|
|
2
|
+
|
|
3
|
+
This app is intended to run on Vercel as the web tier and MongoDB Atlas as the live datastore.
|
|
4
|
+
|
|
5
|
+
## Recommended production shape
|
|
6
|
+
|
|
7
|
+
- Vercel hosts the Next.js app
|
|
8
|
+
- MongoDB Atlas stores dashboard, raw-user, and transfer data
|
|
9
|
+
- Cloud Run Job runs the Playwright scraper
|
|
10
|
+
- Cloud Scheduler triggers the scraper job
|
|
11
|
+
|
|
12
|
+
## Required Vercel environment variables
|
|
13
|
+
|
|
14
|
+
Set these in the Vercel project for the `Production` environment:
|
|
15
|
+
|
|
16
|
+
- `MONGODB_URI`
|
|
17
|
+
- `IPL_POST_SECRET`
|
|
18
|
+
- `IPL_API_LOG=0`
|
|
19
|
+
- `IPL_API_LOG_PAYLOAD=0`
|
|
20
|
+
- `IPL_DASHBOARD_STALE_MINUTES=20`
|
|
21
|
+
- `IPL_TRANSFERS_STALE_MINUTES=720`
|
|
22
|
+
|
|
23
|
+
Optional:
|
|
24
|
+
|
|
25
|
+
- `IPL_WRITE_SEED_DATA_FILE=0`
|
|
26
|
+
|
|
27
|
+
## Deployment checklist
|
|
28
|
+
|
|
29
|
+
1. Import the Git repository into Vercel.
|
|
30
|
+
2. Confirm the framework preset is `Next.js`.
|
|
31
|
+
3. Set the root directory to the repository root.
|
|
32
|
+
4. Add the production environment variables listed above.
|
|
33
|
+
5. Deploy once to create the initial project URL.
|
|
34
|
+
6. Add your custom domain.
|
|
35
|
+
7. Verify `GET /api/ops/status` on the deployed domain.
|
|
36
|
+
8. Point the Cloud Run Job to the final app base URL.
|
|
37
|
+
|
|
38
|
+
## Post-deploy verification
|
|
39
|
+
|
|
40
|
+
Run:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
APP_BASE_URL=https://your-app-domain npm run verify:production
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Expected production behavior:
|
|
47
|
+
|
|
48
|
+
- live snapshot writes require MongoDB
|
|
49
|
+
- local snapshot files are ignored
|
|
50
|
+
- the app remains stateless at the web tier
|
|
51
|
+
- `/api/ops/status` returns `503` when dashboard freshness is critically stale
|
|
52
|
+
|
|
53
|
+
## Notes
|
|
54
|
+
|
|
55
|
+
- Preview deployments can use a separate MongoDB database if you want realistic previews.
|
|
56
|
+
- Keep `IPL_POST_SECRET` identical between Vercel and the scraper job.
|
|
57
|
+
- The repo root `vercel.json` keeps the deployment contract explicit for the project.
|