@vatvaghool/create-ipl-dashboard 0.1.18 → 0.1.23
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 +38 -129
- package/package.json +2 -3
- package/src/index.mjs +18 -22
- package/src/prompts.mjs +20 -112
- package/src/scaffold.mjs +23 -35
- package/template/README.md +23 -200
- package/template/app/api/ops/seed/route.ts +76 -0
- package/template/app/api/ops/status/route.ts +0 -2
- package/template/app/lib/config.ts +0 -4
- package/template/app/lib/matchStatus.ts +0 -12
- package/template/app/lib/storage/index.ts +1 -10
- package/template/package.json +1 -6
- package/template/scripts/dev-simple.js +1 -1
- package/template/scripts/dev-welcome.mjs +39 -18
- package/template/scripts/seed-league.mjs +0 -55
- package/screenshots/ai-roasting.png +0 -0
- package/screenshots/captain-board.png +0 -0
- package/screenshots/dashboard-overview.png +0 -0
- package/screenshots/ledger-table.png +0 -0
- package/screenshots/match-scrubber.png +0 -0
- package/screenshots/performance-tracker.png +0 -0
- package/src/scraper.mjs +0 -79
- package/template/app/hooks/dashboardPolling.ts +0 -53
- package/template/app/hooks/snapshotCache.ts +0 -47
- package/template/app/lib/storage/google-sheets-storage.ts +0 -147
- package/template/app/lib/utils/diff.ts +0 -24
- package/template/app/lib/utils/time.ts +0 -40
- package/template/screenshots/ai-roasting.png +0 -0
- package/template/screenshots/captain-board.png +0 -0
- package/template/screenshots/dashboard-overview.png +0 -0
- package/template/screenshots/ledger-table.png +0 -0
- package/template/screenshots/match-scrubber.png +0 -0
- package/template/screenshots/performance-tracker.png +0 -0
- package/template/tests/coverage-gaps.test.ts +0 -290
- package/template/tests/dashboard-polling.test.ts +0 -96
- package/template/tests/utils-and-cache.test.ts +0 -267
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { afterEach, describe, it, mock } from "node:test";
|
|
3
|
-
import type { DashboardData } from "../app/types.ts";
|
|
4
|
-
import {
|
|
5
|
-
createDashboardPoller,
|
|
6
|
-
DASHBOARD_POLL_INTERVAL_MS,
|
|
7
|
-
} from "../app/hooks/dashboardPolling.ts";
|
|
8
|
-
|
|
9
|
-
const flushPromises = () => new Promise<void>((resolve) => queueMicrotask(resolve));
|
|
10
|
-
|
|
11
|
-
const createResponse = (data: DashboardData) =>
|
|
12
|
-
({
|
|
13
|
-
ok: true,
|
|
14
|
-
status: 200,
|
|
15
|
-
json: async () => data,
|
|
16
|
-
}) as Response;
|
|
17
|
-
|
|
18
|
-
describe("dashboard polling", () => {
|
|
19
|
-
afterEach(() => {
|
|
20
|
-
mock.timers.reset();
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("fetches fresh dashboard data again after every polling interval", async () => {
|
|
24
|
-
mock.timers.enable({ apis: ["Date", "setInterval"], now: 0 });
|
|
25
|
-
|
|
26
|
-
const first: DashboardData = {
|
|
27
|
-
source: "database",
|
|
28
|
-
updatedAt: "2026-04-26T10:00:00.000Z",
|
|
29
|
-
overall: [{ rank: 1, name: "Alpha", points: 100 }],
|
|
30
|
-
daily: [{ day: "Match 1", Alpha: 100 }],
|
|
31
|
-
};
|
|
32
|
-
const second: DashboardData = {
|
|
33
|
-
source: "database",
|
|
34
|
-
updatedAt: "2026-04-26T10:02:00.000Z",
|
|
35
|
-
overall: [{ rank: 1, name: "Alpha", points: 125 }],
|
|
36
|
-
daily: [{ day: "Match 1", Alpha: 125 }],
|
|
37
|
-
};
|
|
38
|
-
const responses = [first, second];
|
|
39
|
-
const received: DashboardData[] = [];
|
|
40
|
-
const fetchCalls: Array<{ url: string; options?: RequestInit }> = [];
|
|
41
|
-
const fetcher = (async (url: string, options?: RequestInit) => {
|
|
42
|
-
fetchCalls.push({ url, options });
|
|
43
|
-
return createResponse(responses.shift() ?? second);
|
|
44
|
-
}) as typeof fetch;
|
|
45
|
-
|
|
46
|
-
const stop = createDashboardPoller({
|
|
47
|
-
fetcher,
|
|
48
|
-
onData: (data) => received.push(data),
|
|
49
|
-
onError: (error) => assert.fail(String(error)),
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
await flushPromises();
|
|
53
|
-
|
|
54
|
-
assert.equal(fetchCalls.length, 1);
|
|
55
|
-
assert.equal(received.length, 1);
|
|
56
|
-
assert.equal(received[0].overall[0].points, 100);
|
|
57
|
-
|
|
58
|
-
mock.timers.tick(DASHBOARD_POLL_INTERVAL_MS);
|
|
59
|
-
await flushPromises();
|
|
60
|
-
|
|
61
|
-
assert.equal(fetchCalls.length, 2);
|
|
62
|
-
assert.equal(received.length, 2);
|
|
63
|
-
assert.equal(received[1].overall[0].points, 125);
|
|
64
|
-
assert.equal(fetchCalls[1].url, `/api/ipl?t=${DASHBOARD_POLL_INTERVAL_MS}`);
|
|
65
|
-
assert.deepEqual(fetchCalls[1].options, { cache: "no-store" });
|
|
66
|
-
|
|
67
|
-
stop();
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("does not emit unchanged data on the next interval", async () => {
|
|
71
|
-
mock.timers.enable({ apis: ["Date", "setInterval"], now: 0 });
|
|
72
|
-
|
|
73
|
-
const dashboard: DashboardData = {
|
|
74
|
-
source: "database",
|
|
75
|
-
updatedAt: "2026-04-26T10:00:00.000Z",
|
|
76
|
-
overall: [{ rank: 1, name: "Alpha", points: 100 }],
|
|
77
|
-
daily: [{ day: "Match 1", Alpha: 100 }],
|
|
78
|
-
};
|
|
79
|
-
const received: DashboardData[] = [];
|
|
80
|
-
const fetcher = (async () => createResponse(dashboard)) as typeof fetch;
|
|
81
|
-
|
|
82
|
-
const stop = createDashboardPoller({
|
|
83
|
-
fetcher,
|
|
84
|
-
onData: (data) => received.push(data),
|
|
85
|
-
onError: (error) => assert.fail(String(error)),
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
await flushPromises();
|
|
89
|
-
mock.timers.tick(DASHBOARD_POLL_INTERVAL_MS);
|
|
90
|
-
await flushPromises();
|
|
91
|
-
|
|
92
|
-
assert.equal(received.length, 1);
|
|
93
|
-
|
|
94
|
-
stop();
|
|
95
|
-
});
|
|
96
|
-
});
|
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { afterEach, describe, it, mock } from "node:test";
|
|
3
|
-
|
|
4
|
-
import { readDashboardCache, writeDashboardCache } from "../app/hooks/dashboardCache.ts";
|
|
5
|
-
import { readSnapshotCache, writeSnapshotCache } from "../app/hooks/snapshotCache.ts";
|
|
6
|
-
import {
|
|
7
|
-
getDisplayLiveMatchId,
|
|
8
|
-
getLiveUpdateRow,
|
|
9
|
-
getPlayedMatchRows,
|
|
10
|
-
hasMeaningfulMatchData,
|
|
11
|
-
parseMatchId,
|
|
12
|
-
} from "../app/lib/dashboardData.ts";
|
|
13
|
-
import { getMatchViewState } from "../app/lib/dashboardView.ts";
|
|
14
|
-
import { getLatestMatchRow, hasLiveMatchRow } from "../app/lib/dashboardView.ts";
|
|
15
|
-
import { matches } from "../app/lib/matches.ts";
|
|
16
|
-
import { getDiff } from "../app/lib/utils/diff.ts";
|
|
17
|
-
import { getStdDeviation } from "../app/lib/utils/getStdDeviation.ts";
|
|
18
|
-
import { formatMatchTime, getMatchStatus, getTimeLeft } from "../app/lib/utils/time.ts";
|
|
19
|
-
import { splitTeamName, trimTeamName } from "../app/lib/utils.ts";
|
|
20
|
-
|
|
21
|
-
const createStorage = () => {
|
|
22
|
-
const store = new Map<string, string>();
|
|
23
|
-
|
|
24
|
-
return {
|
|
25
|
-
getItem: (key: string) => store.get(key) ?? null,
|
|
26
|
-
setItem: (key: string, value: string) => {
|
|
27
|
-
store.set(key, value);
|
|
28
|
-
},
|
|
29
|
-
removeItem: (key: string) => {
|
|
30
|
-
store.delete(key);
|
|
31
|
-
},
|
|
32
|
-
clear: () => {
|
|
33
|
-
store.clear();
|
|
34
|
-
},
|
|
35
|
-
};
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
describe("utility and cache coverage", () => {
|
|
39
|
-
afterEach(() => {
|
|
40
|
-
delete (globalThis as { window?: unknown }).window;
|
|
41
|
-
mock.timers.reset();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("trims and splits team names across all branches", () => {
|
|
45
|
-
assert.equal(trimTeamName("Short"), "Short");
|
|
46
|
-
assert.equal(trimTeamName("VeryLongTeamName", 8), "VeryLong...");
|
|
47
|
-
assert.deepEqual(splitTeamName("Alpha"), ["Alpha", ""]);
|
|
48
|
-
assert.deepEqual(splitTeamName("Alpha Beta", 7), ["Alpha", "Beta"]);
|
|
49
|
-
assert.deepEqual(splitTeamName("NoSpacesHere", 4), ["NoSp", "acesHere"]);
|
|
50
|
-
assert.deepEqual(
|
|
51
|
-
splitTeamName("A very very long team name", 8),
|
|
52
|
-
["A very", "very lon..."],
|
|
53
|
-
);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("computes diffs and standard deviation", () => {
|
|
57
|
-
assert.deepEqual(
|
|
58
|
-
getDiff(
|
|
59
|
-
{ a: 1, nested: { left: 2, same: 1 } },
|
|
60
|
-
{ a: 2, nested: { left: 3, same: 1 }, added: true },
|
|
61
|
-
),
|
|
62
|
-
{
|
|
63
|
-
a: 2,
|
|
64
|
-
"nested.left": 3,
|
|
65
|
-
added: true,
|
|
66
|
-
},
|
|
67
|
-
);
|
|
68
|
-
assert.equal(getStdDeviation([2, 4, 4, 4, 5, 5, 7, 9]), 2);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("formats match times and covers all time status helpers", () => {
|
|
72
|
-
mock.timers.enable({ apis: ["Date"], now: new Date("2026-04-28T10:00:00.000Z") });
|
|
73
|
-
|
|
74
|
-
assert.match(
|
|
75
|
-
formatMatchTime("2026-04-28T14:00:00.000Z", "UTC"),
|
|
76
|
-
/28 Apr.*02:00 pm/i,
|
|
77
|
-
);
|
|
78
|
-
assert.equal(getMatchStatus("2026-04-28T05:30:00.000Z"), "ENDED");
|
|
79
|
-
assert.equal(getMatchStatus("2026-04-28T09:30:00.000Z"), "LIVE");
|
|
80
|
-
assert.equal(getMatchStatus("2026-04-28T10:20:00.000Z"), "STARTING SOON");
|
|
81
|
-
assert.equal(getMatchStatus("2026-04-28T12:00:00.000Z"), "UPCOMING");
|
|
82
|
-
assert.equal(getTimeLeft("2026-04-28T09:59:00.000Z"), "Started");
|
|
83
|
-
assert.equal(getTimeLeft("2026-04-28T10:45:00.000Z"), "45m");
|
|
84
|
-
assert.equal(getTimeLeft("2026-04-28T12:15:00.000Z"), "2h 15m");
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("normalizes dashboard daily rows for played matches and live match labels", () => {
|
|
88
|
-
const daily = [
|
|
89
|
-
{ day: "Match 1", Alpha: 120, Beta: 90 },
|
|
90
|
-
{ day: "Match 2", Alpha: 0, Beta: 0 },
|
|
91
|
-
{ day: "Match 3", Alpha: 65, Beta: 55 },
|
|
92
|
-
{ day: "Live Update", Alpha: 20, Beta: 18 },
|
|
93
|
-
];
|
|
94
|
-
|
|
95
|
-
assert.equal(parseMatchId("Match 12"), 12);
|
|
96
|
-
assert.equal(parseMatchId("Live Update"), undefined);
|
|
97
|
-
assert.equal(hasMeaningfulMatchData(daily[0]), true);
|
|
98
|
-
assert.equal(hasMeaningfulMatchData(daily[1]), false);
|
|
99
|
-
assert.deepEqual(
|
|
100
|
-
getPlayedMatchRows(daily).map((row) => row.day),
|
|
101
|
-
["Match 1", "Match 3"],
|
|
102
|
-
);
|
|
103
|
-
assert.equal(getLiveUpdateRow(daily)?.day, "Live Update");
|
|
104
|
-
assert.equal(getDisplayLiveMatchId(daily), 4);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("falls back to the latest played row when no live match row exists", () => {
|
|
108
|
-
const daily = [
|
|
109
|
-
{ day: "Match 1", Alpha: 10, Beta: 20 },
|
|
110
|
-
{ day: "Match 2", Alpha: 15, Beta: 12 },
|
|
111
|
-
];
|
|
112
|
-
|
|
113
|
-
assert.equal(hasLiveMatchRow(daily), false);
|
|
114
|
-
assert.equal(getLatestMatchRow(daily)?.day, "Match 2");
|
|
115
|
-
assert.equal(getMatchViewState(daily).isLive, false);
|
|
116
|
-
assert.equal(getMatchViewState(daily).latestRow?.day, "Match 2");
|
|
117
|
-
assert.equal(getMatchViewState(daily).hasMatches, true);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("reads and writes dashboard cache with invalid payload safeguards", () => {
|
|
121
|
-
assert.equal(readDashboardCache(), null);
|
|
122
|
-
assert.doesNotThrow(() =>
|
|
123
|
-
writeDashboardCache({
|
|
124
|
-
source: "database",
|
|
125
|
-
overall: [],
|
|
126
|
-
daily: [],
|
|
127
|
-
}),
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
const storage = createStorage();
|
|
131
|
-
(globalThis as { window?: unknown }).window = {
|
|
132
|
-
localStorage: storage,
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
assert.equal(readDashboardCache(), null);
|
|
136
|
-
|
|
137
|
-
writeDashboardCache({
|
|
138
|
-
source: "database",
|
|
139
|
-
updatedAt: "2026-04-28T09:20:00.000Z",
|
|
140
|
-
snapshot: {
|
|
141
|
-
updatedAt: "2026-04-28T09:20:00.000Z",
|
|
142
|
-
leaders: [{ rank: 1, name: "Alpha", points: 120 }],
|
|
143
|
-
},
|
|
144
|
-
overall: [{ rank: 1, name: "Alpha", points: 120 }],
|
|
145
|
-
daily: [{ day: "Match 1", Alpha: 120 }],
|
|
146
|
-
});
|
|
147
|
-
const cached = readDashboardCache();
|
|
148
|
-
assert.ok(cached);
|
|
149
|
-
assert.equal(cached.data.overall[0].name, "Alpha");
|
|
150
|
-
assert.ok(cached.cachedAt);
|
|
151
|
-
|
|
152
|
-
storage.setItem(
|
|
153
|
-
"ipl:dashboard-cache:v1",
|
|
154
|
-
JSON.stringify({
|
|
155
|
-
data: { source: "database", overall: [], daily: [] },
|
|
156
|
-
}),
|
|
157
|
-
);
|
|
158
|
-
const fallbackCached = readDashboardCache();
|
|
159
|
-
assert.ok(fallbackCached);
|
|
160
|
-
assert.equal(typeof fallbackCached.cachedAt, "string");
|
|
161
|
-
|
|
162
|
-
storage.setItem(
|
|
163
|
-
"ipl:dashboard-cache:v1",
|
|
164
|
-
JSON.stringify({
|
|
165
|
-
cachedAt: "2026-04-28T09:20:00.000Z",
|
|
166
|
-
data: {
|
|
167
|
-
source: "database",
|
|
168
|
-
updatedAt: "2026-04-28T09:20:00.000Z",
|
|
169
|
-
overall: [],
|
|
170
|
-
daily: [],
|
|
171
|
-
},
|
|
172
|
-
}),
|
|
173
|
-
);
|
|
174
|
-
assert.equal(readDashboardCache(), null);
|
|
175
|
-
|
|
176
|
-
storage.setItem("ipl:dashboard-cache:v1", "{\"cachedAt\":1}");
|
|
177
|
-
assert.equal(readDashboardCache(), null);
|
|
178
|
-
|
|
179
|
-
storage.setItem("ipl:dashboard-cache:v1", "null");
|
|
180
|
-
assert.equal(readDashboardCache(), null);
|
|
181
|
-
|
|
182
|
-
storage.setItem("ipl:dashboard-cache:v1", "1");
|
|
183
|
-
assert.equal(readDashboardCache(), null);
|
|
184
|
-
|
|
185
|
-
storage.setItem("ipl:dashboard-cache:v1", JSON.stringify({ data: 1 }));
|
|
186
|
-
assert.equal(readDashboardCache(), null);
|
|
187
|
-
|
|
188
|
-
storage.setItem("ipl:dashboard-cache:v1", "not-json");
|
|
189
|
-
assert.equal(readDashboardCache(), null);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it("reads and writes snapshot cache with fallback timestamps", () => {
|
|
193
|
-
assert.equal(readSnapshotCache(), null);
|
|
194
|
-
assert.doesNotThrow(() =>
|
|
195
|
-
writeSnapshotCache({
|
|
196
|
-
leaders: [],
|
|
197
|
-
}),
|
|
198
|
-
);
|
|
199
|
-
|
|
200
|
-
const storage = createStorage();
|
|
201
|
-
(globalThis as { window?: unknown }).window = {
|
|
202
|
-
localStorage: storage,
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
assert.equal(readSnapshotCache(), null);
|
|
206
|
-
|
|
207
|
-
writeSnapshotCache({
|
|
208
|
-
updatedAt: "2026-04-28T09:15:00.000Z",
|
|
209
|
-
completedMatches: 41,
|
|
210
|
-
leaders: [{ rank: 1, name: "Alpha", points: 110 }],
|
|
211
|
-
});
|
|
212
|
-
const cached = readSnapshotCache();
|
|
213
|
-
assert.ok(cached);
|
|
214
|
-
assert.equal(cached.snapshot.leaders[0].name, "Alpha");
|
|
215
|
-
assert.ok(cached.cachedAt);
|
|
216
|
-
|
|
217
|
-
storage.setItem(
|
|
218
|
-
"ipl:snapshot-cache:v1",
|
|
219
|
-
JSON.stringify({
|
|
220
|
-
snapshot: { updatedAt: "2026-04-28T09:15:00.000Z", leaders: [] },
|
|
221
|
-
}),
|
|
222
|
-
);
|
|
223
|
-
const fallbackCached = readSnapshotCache();
|
|
224
|
-
assert.ok(fallbackCached);
|
|
225
|
-
assert.equal(typeof fallbackCached.cachedAt, "string");
|
|
226
|
-
|
|
227
|
-
storage.setItem("ipl:snapshot-cache:v1", "{\"cachedAt\":1}");
|
|
228
|
-
assert.equal(readSnapshotCache(), null);
|
|
229
|
-
|
|
230
|
-
storage.setItem("ipl:snapshot-cache:v1", "1");
|
|
231
|
-
assert.equal(readSnapshotCache(), null);
|
|
232
|
-
|
|
233
|
-
storage.setItem("ipl:snapshot-cache:v1", JSON.stringify({ snapshot: 1 }));
|
|
234
|
-
assert.equal(readSnapshotCache(), null);
|
|
235
|
-
|
|
236
|
-
storage.setItem("ipl:snapshot-cache:v1", "not-json");
|
|
237
|
-
assert.equal(readSnapshotCache(), null);
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it("gracefully ignores cache write failures and exposes upcoming match data", () => {
|
|
241
|
-
(globalThis as { window?: unknown }).window = {
|
|
242
|
-
localStorage: {
|
|
243
|
-
getItem: () => null,
|
|
244
|
-
setItem: () => {
|
|
245
|
-
throw new Error("quota");
|
|
246
|
-
},
|
|
247
|
-
},
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
assert.doesNotThrow(() =>
|
|
251
|
-
writeDashboardCache({
|
|
252
|
-
source: "database",
|
|
253
|
-
overall: [],
|
|
254
|
-
daily: [],
|
|
255
|
-
}),
|
|
256
|
-
);
|
|
257
|
-
assert.doesNotThrow(() =>
|
|
258
|
-
writeSnapshotCache({
|
|
259
|
-
leaders: [],
|
|
260
|
-
}),
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
assert.equal(matches[0].id, 51);
|
|
264
|
-
assert.equal(matches.at(-1)?.id, 70);
|
|
265
|
-
assert.equal(matches.every((match) => match.team1 && match.team2 && match.date), true);
|
|
266
|
-
});
|
|
267
|
-
});
|