cito-mcp 0.4.4 → 0.4.6
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 +7 -7
- package/dist/client.js +39 -35
- package/dist/envelope.js +3 -13
- package/dist/http.js +0 -16
- package/dist/index.js +0 -39
- package/dist/install.js +0 -44
- package/dist/instructions.js +76 -80
- package/dist/scrub.js +111 -0
- package/dist/tools/cs2.js +1 -48
- package/dist/tools/index.js +0 -11
- package/dist/tools/insight.js +42 -174
- package/dist/tools/leaderboard.js +0 -21
- package/dist/tools/live.js +36 -127
- package/dist/tools/match.js +29 -184
- package/dist/tools/meta.js +7 -52
- package/dist/tools/normalize.js +6 -229
- package/dist/tools/odds.js +0 -66
- package/dist/tools/player.js +45 -179
- package/dist/tools/rankings.js +0 -38
- package/dist/tools/resolve.js +0 -136
- package/dist/tools/schedule.js +0 -55
- package/dist/tools/standings.js +21 -65
- package/dist/tools/team.js +2 -83
- package/dist/tools/tournaments.js +0 -37
- package/dist/tools/types.js +0 -17
- package/dist/version.js +0 -17
- package/package.json +22 -2
package/dist/tools/live.js
CHANGED
|
@@ -1,26 +1,15 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* live_matches + upcoming_schedule
|
|
3
|
-
*/
|
|
4
1
|
import { clampInt, decodeCursor, encodeCursor, extractRows, fetchJson, gameNotIncludedHint, asRecord, pickString, } from '../client.js';
|
|
5
2
|
import { DEFAULT_PAGE_LIMIT, MAX_PAGE_LIMIT, errorEnvelope, mapHttpToCode, newRequestId, partialFromRejection, successEnvelope, } from '../envelope.js';
|
|
6
3
|
import { normalizeMatch, presentSides } from './normalize.js';
|
|
7
4
|
import { boolSchema, gameSchema, isPrimaryGame, limitSchema, parseGame, PRIMARY_GAMES, stringSchema, } from './types.js';
|
|
8
5
|
const LIVE_PATHS = {
|
|
9
6
|
lol: '/lol/live',
|
|
10
|
-
cs2: '/cs2/live',
|
|
7
|
+
cs2: '/cs2/live',
|
|
11
8
|
dota2: '/dota2/matches/live',
|
|
12
9
|
cod: '/cod/matches/live',
|
|
13
10
|
ufc: '/ufc/live',
|
|
14
11
|
tennis: '/tennis/matches/live',
|
|
15
12
|
};
|
|
16
|
-
/**
|
|
17
|
-
* Extract live match/bout rows. UFC /ufc/live returns
|
|
18
|
-
* { liveBouts, events, ... } — events are supervisor shells without fighters.
|
|
19
|
-
* Prefer liveBouts (and nested tracking on events) over plain events[].
|
|
20
|
-
* Never promote bare events[] to match rows (avoids fake "? vs ?").
|
|
21
|
-
* Nested tracking with status armed is excluded from live items (use nextCard).
|
|
22
|
-
* Exported for offline UFC projection tests.
|
|
23
|
-
*/
|
|
24
13
|
export function extractLiveRows(data, game) {
|
|
25
14
|
const root = asRecord(data);
|
|
26
15
|
const payload = asRecord(root?.data) ?? root;
|
|
@@ -31,7 +20,6 @@ export function extractLiveRows(data, game) {
|
|
|
31
20
|
for (const t of tracking) {
|
|
32
21
|
const tr = asRecord(t) ?? {};
|
|
33
22
|
const status = String(pickString(tr.status) ?? '').toLowerCase();
|
|
34
|
-
// Board items = live|watching only; armed is surfaced as nextCard, not items.
|
|
35
23
|
if (status === 'armed' || status === 'completed' || status === 'stale')
|
|
36
24
|
continue;
|
|
37
25
|
out.push({
|
|
@@ -41,12 +29,10 @@ export function extractLiveRows(data, game) {
|
|
|
41
29
|
});
|
|
42
30
|
}
|
|
43
31
|
};
|
|
44
|
-
// Prefer liveBouts when the key is present (including empty [] — honest empty board).
|
|
45
32
|
if (Array.isArray(payload.liveBouts)) {
|
|
46
33
|
const liveBouts = payload.liveBouts;
|
|
47
34
|
if (liveBouts.length)
|
|
48
35
|
return liveBouts;
|
|
49
|
-
// Empty liveBouts: still check nested tracking, but never bare events[]
|
|
50
36
|
const eventsWithEmptyLive = Array.isArray(payload.events)
|
|
51
37
|
? payload.events
|
|
52
38
|
: [];
|
|
@@ -56,9 +42,8 @@ export function extractLiveRows(data, game) {
|
|
|
56
42
|
const tracking = Array.isArray(er?.tracking) ? er.tracking : [];
|
|
57
43
|
pushLiveTracking(tracking, er, nested);
|
|
58
44
|
}
|
|
59
|
-
return nested;
|
|
45
|
+
return nested;
|
|
60
46
|
}
|
|
61
|
-
// liveBouts key missing — try nested tracking under events
|
|
62
47
|
const events = Array.isArray(payload.events) ? payload.events : [];
|
|
63
48
|
const fromTracking = [];
|
|
64
49
|
for (const ev of events) {
|
|
@@ -68,21 +53,15 @@ export function extractLiveRows(data, game) {
|
|
|
68
53
|
}
|
|
69
54
|
if (fromTracking.length)
|
|
70
55
|
return fromTracking;
|
|
71
|
-
// Last resort: empty live board rather than fake ? vs ? event shells
|
|
72
56
|
return [];
|
|
73
57
|
}
|
|
74
58
|
return extractRows(data);
|
|
75
59
|
}
|
|
76
|
-
/** Machine emptyReason codes from API health / meta (or inferred). */
|
|
77
60
|
export function inferUfcEmptyReason(events, health, liveCount) {
|
|
78
61
|
if (liveCount > 0)
|
|
79
62
|
return undefined;
|
|
80
63
|
const pulses = events.map((ev) => asRecord(ev) ?? {});
|
|
81
64
|
const hotNow = pulses.some((p) => ['live', 'warming', 'between_bouts', 'degraded'].includes(String(pickString(p.status, p.eventStatus) ?? '').toLowerCase()));
|
|
82
|
-
// The API's own health block is trusted first, EXCEPT when it blames the
|
|
83
|
-
// worker on a day nothing is live. It computes worker_stale from heartbeat
|
|
84
|
-
// lag alone, so an idle Thursday reads the same as a dead worker on fight
|
|
85
|
-
// night. With no hot card there is nothing to be stale about.
|
|
86
65
|
const fromHealth = pickString(health?.emptyReason);
|
|
87
66
|
if (fromHealth && !(['worker_stale', 'worker_down_inferred'].includes(fromHealth) && !hotNow)) {
|
|
88
67
|
return fromHealth;
|
|
@@ -100,10 +79,6 @@ export function inferUfcEmptyReason(events, health, liveCount) {
|
|
|
100
79
|
const freshest = lags.length ? Math.min(...lags) : null;
|
|
101
80
|
const workerAlive = health?.workerAlive ?? health?.workerStarted;
|
|
102
81
|
const hot = statuses.some((s) => ['live', 'warming', 'between_bouts', 'degraded'].includes(s));
|
|
103
|
-
// Only a HOT card can be stale. On an idle day every event is "scheduled"
|
|
104
|
-
// and the heartbeat lags because there is nothing to beat about; reporting
|
|
105
|
-
// that as worker_stale made a quiet Thursday indistinguishable from a dead
|
|
106
|
-
// worker on fight night. freshest stays computed for the health block.
|
|
107
82
|
void freshest;
|
|
108
83
|
if (workerAlive === false && hot)
|
|
109
84
|
return 'worker_stale';
|
|
@@ -119,7 +94,6 @@ export function inferUfcEmptyReason(events, health, liveCount) {
|
|
|
119
94
|
return 'between_bouts';
|
|
120
95
|
return 'not_fight_night';
|
|
121
96
|
}
|
|
122
|
-
/** Human note for empty UFC live section — never invent matchups. */
|
|
123
97
|
export function humanUfcEmptyNote(emptyReason, health) {
|
|
124
98
|
const alive = health?.workerAlive ?? health?.workerStarted;
|
|
125
99
|
const lag = typeof health?.freshestHeartbeatLagSeconds === 'number'
|
|
@@ -136,8 +110,6 @@ export function humanUfcEmptyNote(emptyReason, health) {
|
|
|
136
110
|
supervisor_warming: 'Card open / pre-gate; no bout live yet',
|
|
137
111
|
between_bouts: 'Between bouts; next may be armed',
|
|
138
112
|
armed_only: 'Next bout armed; not started',
|
|
139
|
-
// Customer-facing. The pm2/VPS operator hint used to be in here and went
|
|
140
|
-
// out in every empty board; health{} already carries the numbers for us.
|
|
141
113
|
worker_stale: 'Live feed is behind; scores may lag',
|
|
142
114
|
worker_down_inferred: 'Live feed unavailable right now',
|
|
143
115
|
event_degraded: 'Sources empty clock / event degraded; not inventing stats',
|
|
@@ -174,8 +146,6 @@ function slimUfcNextCard(payload) {
|
|
|
174
146
|
status: pickString(first.status) ?? 'armed',
|
|
175
147
|
boutId: pickString(first.boutId, first.id) ?? null,
|
|
176
148
|
eventSlug: pickString(first.eventSlug) ?? null,
|
|
177
|
-
fightMetricId: first.fightMetricId ?? null,
|
|
178
|
-
// Names only if API already provided them — never invent.
|
|
179
149
|
red: first.red ?? null,
|
|
180
150
|
blue: first.blue ?? null,
|
|
181
151
|
degradedReason: pickString(first.degradedReason) ?? null,
|
|
@@ -184,20 +154,20 @@ function slimUfcNextCard(payload) {
|
|
|
184
154
|
}
|
|
185
155
|
export const liveMatches = {
|
|
186
156
|
name: 'live_matches',
|
|
187
|
-
description: `Live matches board across primary games, or a single game filter. Normalized labels, scores, and matchIds.
|
|
188
|
-
|
|
189
|
-
When to use:
|
|
190
|
-
- "What's live right now?"
|
|
191
|
-
- Ops/dashboard live strip
|
|
192
|
-
|
|
193
|
-
Prefer over: sequential per-game call_api live probes.
|
|
194
|
-
|
|
195
|
-
Do not use when: user wants upcoming fixtures → upcoming_schedule; historical results → match_summary.
|
|
196
|
-
|
|
197
|
-
CS2 live path is /cs2/live; UFC is included in multi-game fan-out.
|
|
198
|
-
UFC empty board: section.note + emptyReason + health (workerAlive/lag) + optional supervisor/nextCard (non-live); never fake match items from events[].
|
|
199
|
-
|
|
200
|
-
Parallel-safe: yes. Upstream cost: 1–5 (allSettled).
|
|
157
|
+
description: `Live matches board across primary games, or a single game filter. Normalized labels, scores, and matchIds.
|
|
158
|
+
|
|
159
|
+
When to use:
|
|
160
|
+
- "What's live right now?"
|
|
161
|
+
- Ops/dashboard live strip
|
|
162
|
+
|
|
163
|
+
Prefer over: sequential per-game call_api live probes.
|
|
164
|
+
|
|
165
|
+
Do not use when: user wants upcoming fixtures → upcoming_schedule; historical results → match_summary.
|
|
166
|
+
|
|
167
|
+
CS2 live path is /cs2/live; UFC is included in multi-game fan-out.
|
|
168
|
+
UFC empty board: section.note + emptyReason + health (workerAlive/lag) + optional supervisor/nextCard (non-live); never fake match items from events[].
|
|
169
|
+
|
|
170
|
+
Parallel-safe: yes. Upstream cost: 1–5 (allSettled).
|
|
201
171
|
Example: { "game": "all", "limitPerGame": 10 }`,
|
|
202
172
|
inputSchema: {
|
|
203
173
|
type: 'object',
|
|
@@ -260,7 +230,6 @@ Example: { "game": "all", "limitPerGame": 10 }`,
|
|
|
260
230
|
});
|
|
261
231
|
continue;
|
|
262
232
|
}
|
|
263
|
-
// Prefer real match/bout rows over supervisor event shells (UFC /ufc/live).
|
|
264
233
|
let rows = extractLiveRows(res.data, game);
|
|
265
234
|
const root = asRecord(res.data);
|
|
266
235
|
const payload = asRecord(root?.data) ?? root;
|
|
@@ -270,9 +239,6 @@ Example: { "game": "all", "limitPerGame": 10 }`,
|
|
|
270
239
|
const normalized = rows
|
|
271
240
|
.slice(0, limitPerGame)
|
|
272
241
|
.map((row) => normalizeMatch(game, row, 'live'))
|
|
273
|
-
// Drop hollow UFC rows agents cannot use (placeholder labels / no sides).
|
|
274
|
-
// normalizeMatch already enriches label from eventSlug/weight/bout id when
|
|
275
|
-
// fighters are pending — keep those; drop pure "? vs ?" only.
|
|
276
242
|
.filter((m) => {
|
|
277
243
|
if (game !== 'ufc')
|
|
278
244
|
return true;
|
|
@@ -281,7 +247,6 @@ Example: { "game": "all", "limitPerGame": 10 }`,
|
|
|
281
247
|
return false;
|
|
282
248
|
if (m.team1?.name || m.team2?.name)
|
|
283
249
|
return true;
|
|
284
|
-
// Identity-only live row (enriched Bout {id} / event name) is ok
|
|
285
250
|
return Boolean(m.matchId && m.matchId !== 'unknown' && !String(m.matchId).startsWith('event'));
|
|
286
251
|
});
|
|
287
252
|
const items = labelsOnly
|
|
@@ -292,17 +257,10 @@ Example: { "game": "all", "limitPerGame": 10 }`,
|
|
|
292
257
|
label: m.label,
|
|
293
258
|
startTime: m.startTime,
|
|
294
259
|
}))
|
|
295
|
-
// Tennis has players, not teams: rename team1/team2 -> player1/player2
|
|
296
|
-
// at this boundary only. Every other game is untouched.
|
|
297
260
|
: normalized.map((m) => presentSides(m, m.game));
|
|
298
261
|
if (game === 'ufc') {
|
|
299
262
|
const health = asRecord(meta?.health) ?? asRecord(payload?.health);
|
|
300
263
|
const events = Array.isArray(payload?.events) ? payload.events : [];
|
|
301
|
-
// One decision point. This used to short-circuit on the API's own
|
|
302
|
-
// emptyReason and only fall back to inferUfcEmptyReason when the API
|
|
303
|
-
// said nothing, so the idle-day rule inside it never ran: the API
|
|
304
|
-
// blames the worker on heartbeat lag alone and an empty Thursday came
|
|
305
|
-
// back as worker_stale. Hand the hint in and let the function weigh it.
|
|
306
264
|
const upstreamReason = pickString(meta?.emptyReason, health?.emptyReason);
|
|
307
265
|
const emptyReason = normalized.length === 0
|
|
308
266
|
? inferUfcEmptyReason(events, { ...(health ?? {}), ...(upstreamReason ? { emptyReason: upstreamReason } : {}) }, normalized.length)
|
|
@@ -325,12 +283,6 @@ Example: { "game": "all", "limitPerGame": 10 }`,
|
|
|
325
283
|
: undefined;
|
|
326
284
|
const supervisor = normalized.length === 0 && events.length ? slimUfcSupervisorEvents(events) : undefined;
|
|
327
285
|
const nextCard = normalized.length === 0 ? slimUfcNextCard(payload) : undefined;
|
|
328
|
-
// A one-word answer to "is anything wrong?". health{} is the worker's
|
|
329
|
-
// raw view and on an idle day it reads ok:false / workerAlive:false,
|
|
330
|
-
// which looks broken. state says what the board means: idle (nothing
|
|
331
|
-
// on, nothing due), live, stale (a hot card with a dead feed), or
|
|
332
|
-
// degraded. nextEvent tells an idle board what comes next, fetched
|
|
333
|
-
// only when the board is empty so a live night pays nothing for it.
|
|
334
286
|
const state = normalized.length > 0 ? 'live'
|
|
335
287
|
: emptyReason === 'worker_stale' || emptyReason === 'worker_down_inferred' ? 'stale'
|
|
336
288
|
: emptyReason === 'event_degraded' ? 'degraded'
|
|
@@ -371,7 +323,6 @@ Example: { "game": "all", "limitPerGame": 10 }`,
|
|
|
371
323
|
});
|
|
372
324
|
}
|
|
373
325
|
const totalLive = sections.reduce((sum, s) => sum + (s.ok ? s.count : 0), 0);
|
|
374
|
-
// Top-level note when single-game UFC empty so agents don't need deep section walk.
|
|
375
326
|
const ufcSection = sections.find((s) => s.game === 'ufc');
|
|
376
327
|
const dataNote = games.length === 1 && games[0] === 'ufc' && ufcSection && ufcSection.count === 0
|
|
377
328
|
? ufcSection.note
|
|
@@ -396,24 +347,24 @@ Example: { "game": "all", "limitPerGame": 10 }`,
|
|
|
396
347
|
};
|
|
397
348
|
export const upcomingSchedule = {
|
|
398
349
|
name: 'upcoming_schedule',
|
|
399
|
-
description: `Upcoming matches/events for one game, with game-specific filters.
|
|
400
|
-
|
|
401
|
-
When to use:
|
|
402
|
-
- "What's on this week?"
|
|
403
|
-
- Calendar UI; team next matches
|
|
404
|
-
|
|
405
|
-
Prefer over: live_matches for not-yet-started fixtures.
|
|
406
|
-
|
|
407
|
-
Do not use when: only in-progress matches needed → live_matches.
|
|
408
|
-
|
|
409
|
-
Filter support (unsupported params are ignored with meta.warnings — do not assume filtering worked):
|
|
410
|
-
- lol: hours, team (slug), league (slug)
|
|
411
|
-
- cs2: team, from, to (ISO); hours not applied upstream
|
|
412
|
-
- cod: team, tournamentId
|
|
413
|
-
- dota2: limit/cursor primarily; team may be client-filtered where data allows
|
|
414
|
-
- ufc: hours / from / to applied client-side after bout expansion (API has no hours); event shells labeled by event name; bouts use fighters[] corners
|
|
415
|
-
|
|
416
|
-
Parallel-safe: yes. Upstream cost: 1–2.
|
|
350
|
+
description: `Upcoming matches/events for one game, with game-specific filters.
|
|
351
|
+
|
|
352
|
+
When to use:
|
|
353
|
+
- "What's on this week?"
|
|
354
|
+
- Calendar UI; team next matches
|
|
355
|
+
|
|
356
|
+
Prefer over: live_matches for not-yet-started fixtures.
|
|
357
|
+
|
|
358
|
+
Do not use when: only in-progress matches needed → live_matches.
|
|
359
|
+
|
|
360
|
+
Filter support (unsupported params are ignored with meta.warnings — do not assume filtering worked):
|
|
361
|
+
- lol: hours, team (slug), league (slug)
|
|
362
|
+
- cs2: team, from, to (ISO); hours not applied upstream
|
|
363
|
+
- cod: team, tournamentId
|
|
364
|
+
- dota2: limit/cursor primarily; team may be client-filtered where data allows
|
|
365
|
+
- ufc: hours / from / to applied client-side after bout expansion (API has no hours); event shells labeled by event name; bouts use fighters[] corners
|
|
366
|
+
|
|
367
|
+
Parallel-safe: yes. Upstream cost: 1–2.
|
|
417
368
|
Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
|
|
418
369
|
inputSchema: {
|
|
419
370
|
type: 'object',
|
|
@@ -466,19 +417,10 @@ Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
|
|
|
466
417
|
warnings.push(`Ignored filter "${param}": ${reason}`);
|
|
467
418
|
};
|
|
468
419
|
let path = '';
|
|
469
|
-
// Set when this tool, not the API, is responsible for the team filter. It
|
|
470
|
-
// also moves paging client-side, because an upstream offset would be
|
|
471
|
-
// counted against rows we are about to discard.
|
|
472
420
|
let clientTeamFilter = false;
|
|
473
|
-
// Set when this tool, not the upstream, is responsible for paging.
|
|
474
421
|
let clientDatePaging = false;
|
|
475
422
|
let query = { limit, offset };
|
|
476
423
|
lolBranch: if (game === 'lol') {
|
|
477
|
-
// from/to means a date WINDOW, which /lol/schedule/upcoming cannot serve:
|
|
478
|
-
// it only knows `hours` forward from now, so asking for last month
|
|
479
|
-
// silently returned next week's fixtures with an "ignored filter"
|
|
480
|
-
// warning. /lol/schedule does accept from/to, so route there instead and
|
|
481
|
-
// let callers list completed matches.
|
|
482
424
|
if (from || to) {
|
|
483
425
|
path = '/lol/schedule';
|
|
484
426
|
clientDatePaging = true;
|
|
@@ -496,17 +438,8 @@ Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
|
|
|
496
438
|
break lolBranch;
|
|
497
439
|
}
|
|
498
440
|
path = '/lol/schedule/upcoming';
|
|
499
|
-
// The upstream accepts teamSlug but does not apply it: asking for t1
|
|
500
|
-
// returned Dplus KIA, Team WE and NightBirds. That is a silently wrong
|
|
501
|
-
// answer, which is worse than an error, so the filter is enforced below.
|
|
502
|
-
// Filtering client-side means the upstream offset would be counted
|
|
503
|
-
// against unfiltered rows, so when a team is named we over-fetch and page
|
|
504
|
-
// here instead.
|
|
505
441
|
if (team)
|
|
506
442
|
clientTeamFilter = true;
|
|
507
|
-
// The upstream accepts `offset` and ignores it, so page two came back
|
|
508
|
-
// byte for byte identical to page one while the cursor advanced in the
|
|
509
|
-
// metadata. Over-fetch and slice here instead of trusting it.
|
|
510
443
|
clientDatePaging = true;
|
|
511
444
|
query = {
|
|
512
445
|
hours: String(hours),
|
|
@@ -571,13 +504,11 @@ Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
|
|
|
571
504
|
}
|
|
572
505
|
else if (game === 'ufc') {
|
|
573
506
|
path = '/ufc/events/upcoming';
|
|
574
|
-
// Request more events than limit so client-side hours filter still has a card pool.
|
|
575
507
|
query = {
|
|
576
508
|
limit: Math.min(50, Math.max(limit * 3, limit)),
|
|
577
509
|
page: Math.floor(offset / limit) + 1,
|
|
578
510
|
includeBouts: true,
|
|
579
511
|
};
|
|
580
|
-
// hours / from / to applied client-side after expand (below) — do not warn as ignored.
|
|
581
512
|
if (team)
|
|
582
513
|
noteIgnored('team', 'UFC uses fighter filters via resolve/event_card, not team on schedule');
|
|
583
514
|
if (league)
|
|
@@ -586,8 +517,6 @@ Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
|
|
|
586
517
|
noteIgnored('tournamentId', 'pass event slug via event_card instead');
|
|
587
518
|
}
|
|
588
519
|
else if (game === 'tennis') {
|
|
589
|
-
// Scheduled fixtures for ~3 days out; ids are stable across
|
|
590
|
-
// upcoming -> live -> finished (same s365_{gid} id-space).
|
|
591
520
|
path = '/tennis/matches/upcoming';
|
|
592
521
|
query = { limit: Math.min(500, Math.max(limit, 1)) };
|
|
593
522
|
if (league)
|
|
@@ -596,7 +525,6 @@ Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
|
|
|
596
525
|
noteIgnored('tournamentId', 'tennis fixtures have no tournamentId filter');
|
|
597
526
|
if (team)
|
|
598
527
|
noteIgnored('team', 'tennis fixtures have no team filter; match player names client-side');
|
|
599
|
-
// hours / from / to applied client-side after fetch (below) — not warned as ignored.
|
|
600
528
|
}
|
|
601
529
|
const res = await fetchJson(ctx, path, { query });
|
|
602
530
|
if (!res.ok) {
|
|
@@ -613,7 +541,6 @@ Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
|
|
|
613
541
|
});
|
|
614
542
|
}
|
|
615
543
|
let rows = extractRows(res.data);
|
|
616
|
-
// UFC events → expand to bout rows (fighters[] shape) so labels are not "? vs ?"
|
|
617
544
|
if (game === 'ufc') {
|
|
618
545
|
const expanded = [];
|
|
619
546
|
for (const event of rows) {
|
|
@@ -636,7 +563,6 @@ Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
|
|
|
636
563
|
}
|
|
637
564
|
}
|
|
638
565
|
else {
|
|
639
|
-
// Keep event shell as a schedule card with a real label (not fighter matchup)
|
|
640
566
|
expanded.push({
|
|
641
567
|
id: eventId ?? eventSlug,
|
|
642
568
|
matchId: eventId ?? eventSlug,
|
|
@@ -652,7 +578,6 @@ Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
|
|
|
652
578
|
}
|
|
653
579
|
}
|
|
654
580
|
rows = expanded;
|
|
655
|
-
// Client-side hours / from / to window (API list has no hours param)
|
|
656
581
|
const now = Date.now();
|
|
657
582
|
const fromMs = from ? Date.parse(from) : now;
|
|
658
583
|
const toMs = to
|
|
@@ -666,7 +591,7 @@ Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
|
|
|
666
591
|
const r = asRecord(row) ?? {};
|
|
667
592
|
const ts = pickString(r.startTime, r.date, r.startsAt, r.scheduledAt);
|
|
668
593
|
if (!ts)
|
|
669
|
-
return true;
|
|
594
|
+
return true;
|
|
670
595
|
const t = Date.parse(ts);
|
|
671
596
|
if (!Number.isFinite(t))
|
|
672
597
|
return true;
|
|
@@ -677,7 +602,6 @@ Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
|
|
|
677
602
|
}
|
|
678
603
|
}
|
|
679
604
|
}
|
|
680
|
-
// Tennis: client-side hours / from / to window (fixtures list has no time params)
|
|
681
605
|
if (game === 'tennis' && (args.hours !== undefined || from || to)) {
|
|
682
606
|
const now = Date.now();
|
|
683
607
|
const fromMs = from ? Date.parse(from) : now;
|
|
@@ -688,7 +612,7 @@ Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
|
|
|
688
612
|
const r = asRecord(row) ?? {};
|
|
689
613
|
const ts = pickString(r.startTime, r.start_time, r.startsAt);
|
|
690
614
|
if (!ts)
|
|
691
|
-
return true;
|
|
615
|
+
return true;
|
|
692
616
|
const t = Date.parse(ts);
|
|
693
617
|
if (!Number.isFinite(t))
|
|
694
618
|
return true;
|
|
@@ -699,16 +623,10 @@ Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
|
|
|
699
623
|
}
|
|
700
624
|
}
|
|
701
625
|
}
|
|
702
|
-
// Client-side team filter when API ignored it
|
|
703
626
|
let beforeTeamFilter = rows.length;
|
|
704
627
|
if (team && (game === 'dota2' || game === 'cs2' || clientTeamFilter)) {
|
|
705
628
|
beforeTeamFilter = rows.length;
|
|
706
629
|
const t = team.toLowerCase();
|
|
707
|
-
// Exact identity first. A joined-substring match made "t1" hit "T1
|
|
708
|
-
// Esports Academy", so asking for T1 returned the academy's fixtures --
|
|
709
|
-
// a different team, quietly. Substring is still useful for partial names
|
|
710
|
-
// ("vitality"), so it stays as a fallback only when nothing matched
|
|
711
|
-
// exactly.
|
|
712
630
|
const sides = (row) => {
|
|
713
631
|
const m = normalizeMatch(game, row, 'upcoming');
|
|
714
632
|
return [m.team1, m.team2];
|
|
@@ -718,11 +636,6 @@ Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
|
|
|
718
636
|
rows = exact;
|
|
719
637
|
}
|
|
720
638
|
else if (t.length <= 4) {
|
|
721
|
-
// A short term is an identifier, not a search phrase, so it is exact or
|
|
722
|
-
// nothing. T1 had no fixture in the window and the substring fallback
|
|
723
|
-
// answered with "T1 Esports Academy" (slugs t1a and t1-challengers) --
|
|
724
|
-
// a different team, presented as if it were the one asked for. An empty
|
|
725
|
-
// list is the correct answer here.
|
|
726
639
|
rows = [];
|
|
727
640
|
}
|
|
728
641
|
else {
|
|
@@ -738,8 +651,6 @@ Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
|
|
|
738
651
|
const windowed = pageHere
|
|
739
652
|
? rows.slice(Number(offset), Number(offset) + Number(limit))
|
|
740
653
|
: rows.slice(0, limit);
|
|
741
|
-
// A date window can contain finished matches, so do not force "upcoming"
|
|
742
|
-
// onto rows that already carry a real state.
|
|
743
654
|
const items = windowed.map((row) => from || to ? normalizeMatch(game, row) : normalizeMatch(game, row, 'upcoming'));
|
|
744
655
|
const obj = asRecord(res.data);
|
|
745
656
|
const total = typeof obj?.total === 'number' ? obj.total : null;
|
|
@@ -761,8 +672,6 @@ Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
|
|
|
761
672
|
nextCursor: hasMore ? encodeCursor({ offset: offset + limit }) : null,
|
|
762
673
|
prevCursor: offset > 0 ? encodeCursor({ offset: Math.max(0, offset - limit) }) : null,
|
|
763
674
|
},
|
|
764
|
-
// Tennis has players, not teams: rename team1/team2 -> player1/player2
|
|
765
|
-
// at this boundary only. Every other game is untouched.
|
|
766
675
|
data: { items: items.map((m) => presentSides(m, game)) },
|
|
767
676
|
});
|
|
768
677
|
},
|