@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,88 @@
|
|
|
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
|
+
};
|
|
36
|
+
|
|
37
|
+
export type RawApiUser = {
|
|
38
|
+
rno: number;
|
|
39
|
+
temname: string;
|
|
40
|
+
points: number;
|
|
41
|
+
matches: Array<{
|
|
42
|
+
matchId: number;
|
|
43
|
+
points: number;
|
|
44
|
+
}>;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type ScrapedSquadPlayer = {
|
|
48
|
+
name: string;
|
|
49
|
+
number?: string;
|
|
50
|
+
points?: number;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type ScrapedLeaderboardItem = {
|
|
54
|
+
rank: number;
|
|
55
|
+
name: string;
|
|
56
|
+
points: number;
|
|
57
|
+
lastMatchPoints?: number;
|
|
58
|
+
netPoints?: number;
|
|
59
|
+
netRank?: number;
|
|
60
|
+
transfersLeft?: number;
|
|
61
|
+
transfersUsed?: number;
|
|
62
|
+
totalTransfers?: number;
|
|
63
|
+
boostersUsed?: string;
|
|
64
|
+
captain?: ScrapedSquadPlayer | null;
|
|
65
|
+
viceCaptain?: ScrapedSquadPlayer | null;
|
|
66
|
+
players?: ScrapedSquadPlayer[];
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type ScrapedDashboardPayload = {
|
|
70
|
+
updatedAt?: string;
|
|
71
|
+
dailyTransferUpdatedAt?: string;
|
|
72
|
+
completedMatches?: number;
|
|
73
|
+
leaders: ScrapedLeaderboardItem[];
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type TransformOptions = {
|
|
77
|
+
teamAliases?: Record<string, string>;
|
|
78
|
+
totalTransfers?: number;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export type RawUsersSyncResult = {
|
|
82
|
+
status: "updated" | "unchanged" | "skipped";
|
|
83
|
+
users: RawApiUser[];
|
|
84
|
+
matchId?: number;
|
|
85
|
+
mode?: "append" | "update-latest";
|
|
86
|
+
completedMatches?: number;
|
|
87
|
+
unmatchedNames: string[];
|
|
88
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationMap": false,
|
|
8
|
+
"sourceMap": false,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"isolatedModules": true,
|
|
12
|
+
"verbatimModuleSyntax": true,
|
|
13
|
+
"rootDir": "src",
|
|
14
|
+
"outDir": "dist"
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*.ts"]
|
|
17
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import readline from "node:readline/promises";
|
|
2
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_LEAGUE_URL =
|
|
5
|
+
"https://fantasy.iplt20.com/classic/league/view/66930102";
|
|
6
|
+
const DEFAULT_OUTPUT_PATH = "ipl-auth.json";
|
|
7
|
+
|
|
8
|
+
const readEnv = (name, fallback = "") => {
|
|
9
|
+
const value = process.env[name]?.trim();
|
|
10
|
+
return value || fallback;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const run = async () => {
|
|
14
|
+
const moduleName = "playwright";
|
|
15
|
+
const { chromium } = await import(moduleName).catch(() => {
|
|
16
|
+
throw new Error(
|
|
17
|
+
'Playwright is not installed. Run "npm install --no-save playwright" first.',
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const leagueUrl = readEnv("IPL_LEAGUE_URL", DEFAULT_LEAGUE_URL);
|
|
22
|
+
const outputPath = readEnv("IPL_AUTH_OUTPUT_PATH", DEFAULT_OUTPUT_PATH);
|
|
23
|
+
const browser = await chromium.launch({ headless: false });
|
|
24
|
+
const context = await browser.newContext();
|
|
25
|
+
const page = await context.newPage();
|
|
26
|
+
const rl = readline.createInterface({ input, output });
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
await page.goto(leagueUrl, { waitUntil: "domcontentloaded", timeout: 60000 });
|
|
30
|
+
|
|
31
|
+
console.log("");
|
|
32
|
+
console.log("Playwright browser opened.");
|
|
33
|
+
console.log(`League URL: ${leagueUrl}`);
|
|
34
|
+
console.log("Log in manually in the browser window.");
|
|
35
|
+
console.log("After login fully completes and the leaderboard is visible, press Enter here.");
|
|
36
|
+
console.log("");
|
|
37
|
+
|
|
38
|
+
await rl.question("Press Enter to save auth state...");
|
|
39
|
+
|
|
40
|
+
await context.storageState({ path: outputPath });
|
|
41
|
+
console.log("");
|
|
42
|
+
console.log(`Saved Playwright auth state to ${outputPath}`);
|
|
43
|
+
console.log(`Base64 encode it for GitHub secret IPL_STORAGE_STATE_B64:`);
|
|
44
|
+
console.log(`base64 ${outputPath}`);
|
|
45
|
+
} finally {
|
|
46
|
+
rl.close();
|
|
47
|
+
await browser.close();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
run().catch((error) => {
|
|
52
|
+
console.error(error instanceof Error ? error.message : error);
|
|
53
|
+
process.exitCode = 1;
|
|
54
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
PROJECT_ID="${PROJECT_ID:?Set PROJECT_ID}"
|
|
6
|
+
REGION="${REGION:?Set REGION}"
|
|
7
|
+
REPOSITORY="${REPOSITORY:-ipl-dashboard}"
|
|
8
|
+
IMAGE_NAME="${IMAGE_NAME:-ipl-sync}"
|
|
9
|
+
JOB_NAME="${JOB_NAME:-ipl-sync}"
|
|
10
|
+
APP_BASE_URL="${APP_BASE_URL:?Set APP_BASE_URL}"
|
|
11
|
+
LEAGUE_URL="${LEAGUE_URL:-https://fantasy.iplt20.com/classic/league/view/66930102}"
|
|
12
|
+
|
|
13
|
+
IMAGE_URI="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPOSITORY}/${IMAGE_NAME}:latest"
|
|
14
|
+
|
|
15
|
+
gcloud artifacts repositories describe "${REPOSITORY}" \
|
|
16
|
+
--location="${REGION}" >/dev/null 2>&1 || \
|
|
17
|
+
gcloud artifacts repositories create "${REPOSITORY}" \
|
|
18
|
+
--repository-format=docker \
|
|
19
|
+
--location="${REGION}"
|
|
20
|
+
|
|
21
|
+
gcloud builds submit \
|
|
22
|
+
--project="${PROJECT_ID}" \
|
|
23
|
+
--tag="${IMAGE_URI}" \
|
|
24
|
+
-f Dockerfile.sync \
|
|
25
|
+
.
|
|
26
|
+
|
|
27
|
+
gcloud run jobs deploy "${JOB_NAME}" \
|
|
28
|
+
--project="${PROJECT_ID}" \
|
|
29
|
+
--region="${REGION}" \
|
|
30
|
+
--image="${IMAGE_URI}" \
|
|
31
|
+
--max-retries=1 \
|
|
32
|
+
--task-timeout=20m \
|
|
33
|
+
--set-env-vars="NODE_ENV=production,IPL_HEADLESS=1,IPL_LEAGUE_URL=${LEAGUE_URL},IPL_API_BASE_URL=${APP_BASE_URL}" \
|
|
34
|
+
--set-secrets="IPL_POST_SECRET=IPL_POST_SECRET:latest,IPL_STORAGE_STATE_B64=IPL_STORAGE_STATE_B64:latest"
|
|
35
|
+
|
|
36
|
+
echo ""
|
|
37
|
+
echo "Cloud Run job deployed:"
|
|
38
|
+
echo " Job: ${JOB_NAME}"
|
|
39
|
+
echo " Image: ${IMAGE_URI}"
|
|
40
|
+
echo ""
|
|
41
|
+
echo "Next step:"
|
|
42
|
+
echo " gcloud scheduler jobs create http ${JOB_NAME}-schedule \\"
|
|
43
|
+
echo " --project=${PROJECT_ID} \\"
|
|
44
|
+
echo " --location=${REGION} \\"
|
|
45
|
+
echo " --schedule='*/10 * * * *' \\"
|
|
46
|
+
echo " --uri=https://${REGION}-run.googleapis.com/apis/run.googleapis.com/v1/namespaces/${PROJECT_ID}/jobs/${JOB_NAME}:run \\"
|
|
47
|
+
echo " --http-method=POST \\"
|
|
48
|
+
echo " --oauth-service-account-email=YOUR_SCHEDULER_SA@${PROJECT_ID}.iam.gserviceaccount.com"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
PROJECT_ID="${PROJECT_ID:?Set PROJECT_ID}"
|
|
6
|
+
REGION="${REGION:?Set REGION}"
|
|
7
|
+
JOB_NAME="${JOB_NAME:-ipl-sync}"
|
|
8
|
+
SCHEDULER_JOB_NAME="${SCHEDULER_JOB_NAME:-ipl-sync-schedule}"
|
|
9
|
+
SCHEDULE="${SCHEDULE:-*/10 * * * *}"
|
|
10
|
+
TIME_ZONE="${TIME_ZONE:-Asia/Kolkata}"
|
|
11
|
+
SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_EMAIL:?Set SERVICE_ACCOUNT_EMAIL}"
|
|
12
|
+
|
|
13
|
+
RUN_URI="https://${REGION}-run.googleapis.com/apis/run.googleapis.com/v1/namespaces/${PROJECT_ID}/jobs/${JOB_NAME}:run"
|
|
14
|
+
|
|
15
|
+
if gcloud scheduler jobs describe "${SCHEDULER_JOB_NAME}" \
|
|
16
|
+
--project="${PROJECT_ID}" \
|
|
17
|
+
--location="${REGION}" >/dev/null 2>&1; then
|
|
18
|
+
gcloud scheduler jobs update http "${SCHEDULER_JOB_NAME}" \
|
|
19
|
+
--project="${PROJECT_ID}" \
|
|
20
|
+
--location="${REGION}" \
|
|
21
|
+
--schedule="${SCHEDULE}" \
|
|
22
|
+
--time-zone="${TIME_ZONE}" \
|
|
23
|
+
--uri="${RUN_URI}" \
|
|
24
|
+
--http-method=POST \
|
|
25
|
+
--oauth-service-account-email="${SERVICE_ACCOUNT_EMAIL}"
|
|
26
|
+
else
|
|
27
|
+
gcloud scheduler jobs create http "${SCHEDULER_JOB_NAME}" \
|
|
28
|
+
--project="${PROJECT_ID}" \
|
|
29
|
+
--location="${REGION}" \
|
|
30
|
+
--schedule="${SCHEDULE}" \
|
|
31
|
+
--time-zone="${TIME_ZONE}" \
|
|
32
|
+
--uri="${RUN_URI}" \
|
|
33
|
+
--http-method=POST \
|
|
34
|
+
--oauth-service-account-email="${SERVICE_ACCOUNT_EMAIL}"
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
echo ""
|
|
38
|
+
echo "Cloud Scheduler job configured:"
|
|
39
|
+
echo " Name: ${SCHEDULER_JOB_NAME}"
|
|
40
|
+
echo " Schedule: ${SCHEDULE}"
|
|
41
|
+
echo " Time zone: ${TIME_ZONE}"
|
|
42
|
+
echo " Target: ${RUN_URI}"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
const seedMongoIfConfigured = () => {
|
|
4
|
+
if (!process.env.MONGODB_URI) {
|
|
5
|
+
console.log("No MONGODB_URI configured. Using local snapshot/raw fallback.");
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
console.log("MONGODB_URI detected. Running seed once (best effort)...");
|
|
10
|
+
const result = spawnSync("npm", ["run", "seed:mongodb"], {
|
|
11
|
+
stdio: "inherit",
|
|
12
|
+
env: process.env,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
if (result.status !== 0) {
|
|
16
|
+
console.warn("Seed failed. Continuing with dev server.");
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
seedMongoIfConfigured();
|
|
21
|
+
|
|
22
|
+
console.log("Starting dev server...");
|
|
23
|
+
const child = spawn("next", ["dev"], {
|
|
24
|
+
stdio: "inherit",
|
|
25
|
+
env: process.env,
|
|
26
|
+
shell: true,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
child.on("exit", (code) => {
|
|
30
|
+
process.exit(code ?? 0);
|
|
31
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const reset = "\x1b[0m";
|
|
2
|
+
const bold = "\x1b[1m";
|
|
3
|
+
const dim = "\x1b[2m";
|
|
4
|
+
const cyan = "\x1b[36m";
|
|
5
|
+
const amber = "\x1b[33m";
|
|
6
|
+
const magenta = "\x1b[35m";
|
|
7
|
+
const lime = "\x1b[32m";
|
|
8
|
+
|
|
9
|
+
const cmds = [
|
|
10
|
+
{ cmd: "capture:ipl-auth", desc: "Log in to fantasy.iplt20.com and save auth state" },
|
|
11
|
+
{ cmd: "sync:ipl", desc: "Scrape live leaderboard + squad data (one-shot)" },
|
|
12
|
+
{ cmd: "sync:ipl:transfers-daily", desc: "Scrape transfer/booster data for all teams" },
|
|
13
|
+
{ cmd: "sync:ipl:watch", desc: "Watch mode — rescrape leaderboard every 60s" },
|
|
14
|
+
{ cmd: "test", desc: "Run all tests (43 total)" },
|
|
15
|
+
{ cmd: "lint", desc: "Run ESLint" },
|
|
16
|
+
{ cmd: "build", desc: "Production build" },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
console.log("");
|
|
20
|
+
console.log(` ${bold}${magenta}╭──────────────────────────────────────────╮${reset}`);
|
|
21
|
+
console.log(` ${bold}${magenta}│${reset} ${bold}🏏 IPL Dashboard Dev Server Ready${reset} ${bold}${magenta}│${reset}`);
|
|
22
|
+
console.log(` ${bold}${magenta}╰──────────────────────────────────────────╯${reset}`);
|
|
23
|
+
console.log("");
|
|
24
|
+
console.log(` ${bold}${cyan}Next Steps${reset}`);
|
|
25
|
+
console.log(` ${dim}────────────────────────────────────────────${reset}`);
|
|
26
|
+
console.log("");
|
|
27
|
+
|
|
28
|
+
for (const { cmd, desc } of cmds) {
|
|
29
|
+
console.log(` ${amber}npm run${reset} ${bold}${cmd.padEnd(25)}${reset} ${dim}${desc}${reset}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log("");
|
|
33
|
+
console.log(` ${bold}${lime}Quick Start${reset}`);
|
|
34
|
+
console.log(` ${dim} 1. Run${reset} ${amber}npm run capture:ipl-auth${reset} ${dim}(one-time login)${reset}`);
|
|
35
|
+
console.log(` ${dim} 2. Run${reset} ${amber}npm run sync:ipl:transfers-daily${reset} ${dim}(load transfer data)${reset}`);
|
|
36
|
+
console.log(` ${dim} 3. Run${reset} ${amber}npm run sync:ipl:watch${reset} ${dim}(start live sync)${reset}`);
|
|
37
|
+
console.log(` ${dim} 4. Open${reset} ${cyan}http://localhost:3000${reset} ${dim}to view the dashboard${reset}`);
|
|
38
|
+
console.log("");
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
APP_BASE_URL="${APP_BASE_URL:-}"
|
|
6
|
+
|
|
7
|
+
if [ -z "${APP_BASE_URL}" ]; then
|
|
8
|
+
echo "APP_BASE_URL is required. Example: APP_BASE_URL=https://your-app-domain" >&2
|
|
9
|
+
exit 1
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
URL="${APP_BASE_URL%/}/api/ops/status"
|
|
13
|
+
TMP_BODY="$(mktemp)"
|
|
14
|
+
TMP_HEADERS="$(mktemp)"
|
|
15
|
+
|
|
16
|
+
cleanup() {
|
|
17
|
+
rm -f "${TMP_BODY}" "${TMP_HEADERS}"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
trap cleanup EXIT
|
|
21
|
+
|
|
22
|
+
HTTP_CODE="$(
|
|
23
|
+
curl -sS \
|
|
24
|
+
-D "${TMP_HEADERS}" \
|
|
25
|
+
-o "${TMP_BODY}" \
|
|
26
|
+
-w "%{http_code}" \
|
|
27
|
+
-H "Accept: application/json" \
|
|
28
|
+
"${URL}"
|
|
29
|
+
)"
|
|
30
|
+
|
|
31
|
+
if [ "${HTTP_CODE}" -ge 500 ]; then
|
|
32
|
+
echo "monitor failure: ${URL} returned HTTP ${HTTP_CODE}" >&2
|
|
33
|
+
cat "${TMP_BODY}" >&2
|
|
34
|
+
exit 1
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
if grep -q '"status":"critical"' "${TMP_BODY}"; then
|
|
38
|
+
echo "monitor failure: critical app status reported by ${URL}" >&2
|
|
39
|
+
cat "${TMP_BODY}" >&2
|
|
40
|
+
exit 1
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
if grep -q '"status":"degraded"' "${TMP_BODY}"; then
|
|
44
|
+
echo "monitor warning: degraded app status reported by ${URL}" >&2
|
|
45
|
+
cat "${TMP_BODY}"
|
|
46
|
+
exit 2
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
echo "monitor ok: ${URL} returned HTTP ${HTTP_CODE}"
|
|
50
|
+
cat "${TMP_BODY}"
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { MongoClient } from "mongodb";
|
|
2
|
+
import {
|
|
3
|
+
buildManualDashboard,
|
|
4
|
+
normalizeRawApiUsers,
|
|
5
|
+
} from "../app/api/ipl/transform.ts";
|
|
6
|
+
import { rawApiUsers } from "../app/api/ipl/data.ts";
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
process.loadEnvFile(".env");
|
|
10
|
+
} catch {
|
|
11
|
+
// Environment variables may already be provided by the shell.
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const DB_NAME = "ipl";
|
|
15
|
+
const COLLECTION_NAME = "ipl";
|
|
16
|
+
const RAW_USERS_DOCUMENT_TYPE = "raw-users";
|
|
17
|
+
|
|
18
|
+
const args = new Set(process.argv.slice(2));
|
|
19
|
+
const dryRun = args.has("--dry-run");
|
|
20
|
+
const force = args.has("--force");
|
|
21
|
+
const reset = args.has("--reset");
|
|
22
|
+
|
|
23
|
+
const loadSeedUsers = () => {
|
|
24
|
+
const users = normalizeRawApiUsers(rawApiUsers);
|
|
25
|
+
|
|
26
|
+
if (!users) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
"Failed to normalize raw seed users from app/api/ipl/data.ts",
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return users;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const createRawUsersDocument = (users: ReturnType<typeof loadSeedUsers>) => ({
|
|
36
|
+
type: RAW_USERS_DOCUMENT_TYPE,
|
|
37
|
+
updatedAt: new Date().toISOString(),
|
|
38
|
+
users: users.map((user) => ({
|
|
39
|
+
rno: user.rno,
|
|
40
|
+
temname: user.temname,
|
|
41
|
+
points: user.points,
|
|
42
|
+
matches: [...user.matches].sort((a, b) => a.matchId - b.matchId),
|
|
43
|
+
})),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const logPlan = () => {
|
|
47
|
+
const users = loadSeedUsers();
|
|
48
|
+
const dashboard = buildManualDashboard(users);
|
|
49
|
+
|
|
50
|
+
console.log(`Seed users: ${users.length}`);
|
|
51
|
+
console.log(
|
|
52
|
+
`Latest match id: ${Math.max(
|
|
53
|
+
...users.flatMap((user) => user.matches.map((match) => match.matchId)),
|
|
54
|
+
)}`,
|
|
55
|
+
);
|
|
56
|
+
console.log(`Reset requested: ${reset ? "yes" : "no"}`);
|
|
57
|
+
console.log(`Dashboard rows: ${dashboard.daily.length}`);
|
|
58
|
+
console.log(`Leaderboard rows: ${dashboard.overall.length}`);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const run = async () => {
|
|
62
|
+
if (dryRun) {
|
|
63
|
+
logPlan();
|
|
64
|
+
console.log("Dry run only. No MongoDB writes performed.");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const uri = process.env.MONGODB_URI;
|
|
69
|
+
|
|
70
|
+
if (!uri) {
|
|
71
|
+
throw new Error("MONGODB_URI is required to seed MongoDB.");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const users = loadSeedUsers();
|
|
75
|
+
const client = new MongoClient(uri);
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
await client.connect();
|
|
79
|
+
|
|
80
|
+
const collection = client.db(DB_NAME).collection(COLLECTION_NAME);
|
|
81
|
+
|
|
82
|
+
if (reset) {
|
|
83
|
+
const result = await collection.deleteMany({});
|
|
84
|
+
console.log(`Deleted ${result.deletedCount} IPL MongoDB documents.`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const existingRawUsers = await collection.findOne({
|
|
88
|
+
type: RAW_USERS_DOCUMENT_TYPE,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (!existingRawUsers || force || reset) {
|
|
92
|
+
await collection.updateOne(
|
|
93
|
+
{ type: RAW_USERS_DOCUMENT_TYPE },
|
|
94
|
+
{ $set: createRawUsersDocument(users) },
|
|
95
|
+
{ upsert: true },
|
|
96
|
+
);
|
|
97
|
+
console.log(
|
|
98
|
+
existingRawUsers
|
|
99
|
+
? "Updated raw-users document from local seed data."
|
|
100
|
+
: "Inserted raw-users document from local seed data.",
|
|
101
|
+
);
|
|
102
|
+
} else {
|
|
103
|
+
console.log(
|
|
104
|
+
"Skipped raw-users document because it already exists. Use --force to overwrite.",
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
} finally {
|
|
108
|
+
await client.close();
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
run().catch((error) => {
|
|
113
|
+
console.error(error instanceof Error ? error.message : error);
|
|
114
|
+
process.exitCode = 1;
|
|
115
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
const steps = [
|
|
4
|
+
{
|
|
5
|
+
label: "dashboard",
|
|
6
|
+
script: "scripts/sync-ipl.mjs",
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
label: "transfers",
|
|
10
|
+
script: "scripts/sync-transfers-daily.mjs",
|
|
11
|
+
},
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const runStep = (step) =>
|
|
15
|
+
new Promise((resolve, reject) => {
|
|
16
|
+
console.log(`\n[cloud-sync] starting ${step.label} sync`);
|
|
17
|
+
|
|
18
|
+
const child = spawn(process.execPath, [step.script], {
|
|
19
|
+
stdio: "inherit",
|
|
20
|
+
env: process.env,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
child.on("error", reject);
|
|
24
|
+
child.on("exit", (code, signal) => {
|
|
25
|
+
if (code === 0) {
|
|
26
|
+
console.log(`[cloud-sync] finished ${step.label} sync`);
|
|
27
|
+
resolve();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
reject(
|
|
32
|
+
new Error(
|
|
33
|
+
`[cloud-sync] ${step.label} sync failed with ${
|
|
34
|
+
signal ? `signal ${signal}` : `exit code ${code ?? "unknown"}`
|
|
35
|
+
}`,
|
|
36
|
+
),
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const run = async () => {
|
|
42
|
+
for (const step of steps) {
|
|
43
|
+
await runStep(step);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
run().catch((error) => {
|
|
48
|
+
console.error(error instanceof Error ? error.message : error);
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
});
|