Skip to content

feat: add sports variant panels and feeds#2814

Open
FayezBast wants to merge 4 commits intomainfrom
feat/sports-monitor
Open

feat: add sports variant panels and feeds#2814
FayezBast wants to merge 4 commits intomainfrom
feat/sports-monitor

Conversation

@FayezBast
Copy link
Copy Markdown
Collaborator

@FayezBast FayezBast commented Apr 8, 2026

Summary

V1 of sports news , I need it to be reviewed , if code is good (good architecture) then I add my changes, no need for merge now

Adds a new sports variant with dedicated sports headlines, fixtures, league tables, major tournament views, player search, and transfer news. Also adds the sports data edge proxy, sports-specific feed wiring on the client and server, variant/build metadata, and guardrail coverage for the new digest and panel paths.

todo

  • still need to add more sports analytical panels (football, nba,f1)
    -add other famous sports (cricket - etc)
    -add live feeds
    -for the map I need ur help , im thinking of removing it and add interactive ui for the users , or keep and add layers to it

Type of change

  • New feature
  • New data source / feed

Affected areas

  • Map / Globe
  • News panels / RSS feeds
  • API endpoints (/api/*)
  • Config / Settings
  • Other: Sports variant / sports dashboard

Checklist

  • No API keys or secrets committed
  • TypeScript compiles without errors (npm run typecheck)

Local validation

  • npm run typecheck
  • npm run typecheck:api
  • npm run build:sports
  • npm run test:data
  • node --test tests/panel-config-guardrails.test.mjs tests/sports-digest-guardrails.test.mjs

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 8, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
worldmonitor Ignored Ignored Preview Apr 13, 2026 6:18am

Request Review

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 8, 2026

Greptile Summary

Adds the sports variant end-to-end: a new edge-function proxy (api/sports-data.js) with a shared allowlist config, 14 panel components covering fixtures, tables, live tracking, NBA, motorsport, player search and transfers, sports-specific RSS feeds, map marker hydration, and a headline noise filter. The previous concerns about unguarded scheduleRefresh calls and silent provider fallback are resolved in this revision.

Confidence Score: 5/5

Safe to merge for review purposes; all three remaining findings are P2 and do not block the primary user path.

All P0/P1 concerns from the previous review round are resolved. The three new findings are P2: an architectural boundary nit (api/ import that works today but should move to shared/), a missing inFlight dedup in an interactive popup path (causes redundant fetches, not data loss), and missing query exclusions in server-side digest feeds (only affects the digest endpoint, not the main UI).

src/services/sports.ts (api/ boundary import + inFlight dedup), server/worldmonitor/news/v1/_feeds.ts (missing political exclusions in digest feeds)

Important Files Changed

Filename Overview
src/services/sports.ts 3146-line new service; two issues: (1) imports CITY_COORDS from api/data/city-coords.ts (only src/→api/ import in the codebase); (2) fetchSportsFixturePopupContext lacks inFlight deduplication unlike every other cache helper in the file.
server/worldmonitor/news/v1/_feeds.ts Sports variant added to VARIANT_FEEDS; Reuters Sports and AP Sports feeds missing -politics -election -government exclusion terms present in the client-side feeds.ts counterparts.
api/sports-data.js New edge function proxy; correctly validates provider/path against allowlist, handles CORS, timeouts, and error codes.
api/_sports-data-config.js Shared allowlist config for the sports proxy; validates endpoint/param symmetry at startup. Clean and self-contained.
src/app/data-loader.ts Sports variant correctly wrapped in isSportsVariant guard; loadSportsFixturesLayer added with proper error handling; applyCategoryQualityFilters wired for headline noise filtering.
src/App.ts Sports scheduleRefresh calls correctly gated inside if (SITE_VARIANT === 'sports'); skipsGeneralRefreshes updated to include sports variant.
src/services/sports-headline-filter.ts New headline noise filter; conservative two-pass approach correctly retains sports-political crossover articles.
src/config/variants/sports.ts New sports variant config with 14 panels, all non-sports map layers disabled. Clean and well-structured.
src/config/feeds.ts SPORTS_FEEDS added with political exclusion terms in Reuters/AP queries; correctly wired into the FEEDS ternary chain.

Reviews (2): Last reviewed commit: "feat(sport): make sports fixture date mo..." | Re-trigger Greptile

Comment on lines 1429 to 1476
REFRESH_INTERVALS.cotPositioning,
() => this.isPanelNearViewport('cot-positioning')
);
this.refreshScheduler.scheduleRefresh(
'sports-fixtures',
() => (this.state.panels['sports-fixtures'] as SportsFixturesPanel).fetchData(),
REFRESH_INTERVALS.sports,
() => this.isPanelNearViewport('sports-fixtures')
);
this.refreshScheduler.scheduleRefresh(
'sports-tables',
() => (this.state.panels['sports-tables'] as SportsTablesPanel).fetchData(),
REFRESH_INTERVALS.sports,
() => this.isPanelNearViewport('sports-tables')
);
this.refreshScheduler.scheduleRefresh(
'sports-stats',
() => (this.state.panels['sports-stats'] as SportsStatsPanel).fetchData(),
REFRESH_INTERVALS.sports,
() => this.isPanelNearViewport('sports-stats')
);
this.refreshScheduler.scheduleRefresh(
'sports-tournaments',
() => (this.state.panels['sports-tournaments'] as SportsMajorTournamentsPanel).fetchData(),
REFRESH_INTERVALS.sports,
() => this.isPanelNearViewport('sports-tournaments')
);
this.refreshScheduler.scheduleRefresh(
'sports-nba',
() => (this.state.panels['sports-nba'] as SportsNbaPanel).fetchData(),
REFRESH_INTERVALS.sports,
() => this.isPanelNearViewport('sports-nba')
);
this.refreshScheduler.scheduleRefresh(
'sports-motorsport-standings',
() => (this.state.panels['sports-motorsport-standings'] as SportsMotorsportPanel).fetchData(),
REFRESH_INTERVALS.sports,
() => this.isPanelNearViewport('sports-motorsport-standings')
);
this.refreshScheduler.scheduleRefresh(
'sports-transfers',
() => (this.state.panels['sports-transfers'] as SportsTransferNewsPanel).fetchData(),
REFRESH_INTERVALS.sports,
() => this.isPanelNearViewport('sports-transfers')
);

// Refresh intelligence signals for CII (geopolitical variant only)
if (SITE_VARIANT === 'full') {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Sports refresh intervals registered for all variants

These 7 scheduleRefresh calls are added outside any SITE_VARIANT === 'sports' guard, so they register timers on every variant (full, tech, finance, etc.). The visibility check (isPanelNearViewport) will return false for missing panels, so no crash occurs, but it wastes scheduler slots and diverges from the explicit if (!skipsGeneralRefreshes) pattern used two dozen lines above for the equivalent conditional block.

Suggested change
REFRESH_INTERVALS.cotPositioning,
() => this.isPanelNearViewport('cot-positioning')
);
this.refreshScheduler.scheduleRefresh(
'sports-fixtures',
() => (this.state.panels['sports-fixtures'] as SportsFixturesPanel).fetchData(),
REFRESH_INTERVALS.sports,
() => this.isPanelNearViewport('sports-fixtures')
);
this.refreshScheduler.scheduleRefresh(
'sports-tables',
() => (this.state.panels['sports-tables'] as SportsTablesPanel).fetchData(),
REFRESH_INTERVALS.sports,
() => this.isPanelNearViewport('sports-tables')
);
this.refreshScheduler.scheduleRefresh(
'sports-stats',
() => (this.state.panels['sports-stats'] as SportsStatsPanel).fetchData(),
REFRESH_INTERVALS.sports,
() => this.isPanelNearViewport('sports-stats')
);
this.refreshScheduler.scheduleRefresh(
'sports-tournaments',
() => (this.state.panels['sports-tournaments'] as SportsMajorTournamentsPanel).fetchData(),
REFRESH_INTERVALS.sports,
() => this.isPanelNearViewport('sports-tournaments')
);
this.refreshScheduler.scheduleRefresh(
'sports-nba',
() => (this.state.panels['sports-nba'] as SportsNbaPanel).fetchData(),
REFRESH_INTERVALS.sports,
() => this.isPanelNearViewport('sports-nba')
);
this.refreshScheduler.scheduleRefresh(
'sports-motorsport-standings',
() => (this.state.panels['sports-motorsport-standings'] as SportsMotorsportPanel).fetchData(),
REFRESH_INTERVALS.sports,
() => this.isPanelNearViewport('sports-motorsport-standings')
);
this.refreshScheduler.scheduleRefresh(
'sports-transfers',
() => (this.state.panels['sports-transfers'] as SportsTransferNewsPanel).fetchData(),
REFRESH_INTERVALS.sports,
() => this.isPanelNearViewport('sports-transfers')
);
// Refresh intelligence signals for CII (geopolitical variant only)
if (SITE_VARIANT === 'full') {
if (SITE_VARIANT === 'sports') {
this.refreshScheduler.scheduleRefresh(
'sports-fixtures',
() => (this.state.panels['sports-fixtures'] as SportsFixturesPanel).fetchData(),
REFRESH_INTERVALS.sports,
() => this.isPanelNearViewport('sports-fixtures')
);
// … remaining six sports schedule calls
}

Comment on lines +1162 to +1172
function extractEspnFittState(html: string): Record<string, unknown> | null {
const match = html.match(/window\['__espnfitt__'\]=(\{.*?\});<\/script>/s);
if (!match?.[1]) return null;

try {
const parsed = JSON.parse(match[1]);
return isRecord(parsed) ? parsed : null;
} catch {
return null;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Regex-based JSON extraction from ESPN HTML is fragile

/window\['__espnfitt__'\]=(\{.*?\});<\/script>/s uses non-greedy dotAll matching to pull the inline JSON bundle. If ESPN ever wraps the assignment in a self-executing function, adds a newline before </script>, or escapes a };</script> substring inside a JSON string value, the match will fail silently and fetchNbaStandingsData returns null. A more robust approach would look for the known key boundary and use JSON.parse with a structured try/catch, or switch to the documented ESPN API endpoint that returns JSON directly.

vite.config.ts Outdated
Comment on lines +510 to +520
};
}

function sportsDataProxyPlugin(): Plugin {
const PROVIDERS = {
thesportsdb: {
baseUrl: 'https://www.thesportsdb.com/api/v1/json/123',
endpoints: new Set([
'/all_leagues.php',
'/lookupleague.php',
'/search_all_seasons.php',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Provider/allowlist duplicated between vite.config.ts and api/sports-data.js

The entire PROVIDERS table — with all endpoints, TTLs, and allowed params — is defined twice: once here for the local dev proxy and once verbatim in api/sports-data.js for the edge function. When a new endpoint is added to one file (e.g. a new ESPN league path), it's easy to forget to update the other. Consider extracting the shared allowlist into a shared/sports-providers.json (or a generated constant) that both consume, similar to how shared/ is already used for other config fixtures.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4a51754921

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

src/App.ts Outdated
Comment on lines +340 to +343
if (shouldPrime('sports-fixtures')) {
const panel = this.state.panels['sports-fixtures'] as SportsFixturesPanel | undefined;
if (panel) primeTask('sports-fixtures', () => panel.fetchData());
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate sports panel priming to the sports variant

These sports primeTask calls are unconditional once shouldPrime is true, and App.init() invokes primeVisiblePanelData(true), which makes shouldPrime(...) true for all panels at startup. That means non-sports variants now eagerly fetch sports fixtures/tables/stats/etc. even when those panels are not part of the active variant, adding avoidable startup latency and upstream traffic. Wrap the sports priming block in a SITE_VARIANT === 'sports' guard (or avoid forcing these IDs when forceAll is used).

Useful? React with 👍 / 👎.

Comment on lines +939 to +940
const rawPayload = await fetchEspnSiteJson<Record<string, unknown>>(buildEspnSiteScoreboardPath(spec), 5 * 60 * 1000);
const events = await fetchEspnCompetitionEvents(spec);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reuse fetched scoreboard payload for tournament events

This function fetches the same ESPN scoreboard twice: once into rawPayload and immediately again via fetchEspnCompetitionEvents(spec). Besides doubling request cost and latency, it adds a second failure point where the function can fail even though the first scoreboard request already succeeded. Parse events from rawPayload (or pass it to the helper) so the panel only depends on a single upstream call.

Useful? React with 👍 / 👎.

@koala73 koala73 marked this pull request as draft April 8, 2026 04:59
@koala73
Copy link
Copy Markdown
Owner

koala73 commented Apr 8, 2026

Combined Review

Validated in isolated worktree: typecheck, typecheck:api, build:sports, test:data, and the new guardrail tests all pass. test:sidecar fails on the same test as base commit aa01033db, so pre-existing. Live upstream checks on April 8: ESPN site API, ESPN NBA standings page/regex, Jolpica, OpenF1, and TheSportsDB all responded.


P1 (Blocking)

Sports panel priming and refresh not variant-gated
src/App.ts: shouldPrime('sports-*') blocks (~line 340) and refreshScheduler.scheduleRefresh('sports-*') (~line 1432) are not gated to SITE_VARIANT === 'sports'. Because startup calls primeVisiblePanelData(true), non-sports variants will eagerly hit ESPN/TheSportsDB/Jolpica on boot, and every variant gets extra sports refresh timers. This is a real cross-variant regression, not just dead code.

Fix: wrap both sections in if (SITE_VARIANT === 'sports') { ... }.

Feed curation is not production-grade
On April 8, the Reuters/AP "sports" Google News searches returned non-sports/politics items, the transfer query (SportsTransferNewsPanel.ts:8) returned college football transfer-portal stories, the basketball "analysis" query returned college/fantasy noise, and the ESPN combat query returned a basketball transfer-portal item. Since the main value of this variant is feed quality, this needs tightening before merge. Specific fixes:

Feed Problem Fix
Reuters/AP sports Returns politics Add -politics -election -government
(NBA OR basketball) analysis College/fantasy noise Scope to site:theathletic.com NBA -fantasy -college
site:espn.com NBA College transfer portal Add -college -fantasy -"transfer portal"
site:espn.com MMA OR boxing Basketball items Add -basketball -football -"transfer portal"
(football OR soccer) transfer College transfer portal Add -%22transfer+portal%22 -college -NCAA
site:theguardian.com football Fragile GN scrape Use direct RSS https://www.theguardian.com/football/rss
(MLB OR baseball) trade OR playoffs Too broad Replace with MLB standings OR scores OR recap -college -fantasy

P2 (Fix Required)

Double ESPN scoreboard fetch in fetchMajorTournamentCenterData
src/services/sports.ts:939: fetches the same ESPN scoreboard twice: once directly via fetchEspnSiteJson(buildEspnSiteScoreboardPath(spec)) and once again through fetchEspnCompetitionEvents(spec). The in-flight cache deduplicates the HTTP request, but the code still parses the payload twice. Extract events directly from the first fetch.

Dev/prod divergence for invalid provider
api/sports-data.js:106: production silently falls back to thesportsdb for an invalid provider (PROVIDERS[providerKey] || PROVIDERS.thesportsdb), while vite.config.ts returns 400. This makes bad requests harder to debug and can route errors to the wrong upstream. Align both to return 400 for invalid providers.

Provider/allowlist table duplicated
vite.config.ts:513 and api/sports-data.js:104 duplicate the full provider/endpoint/param allowlist. The invalid-provider drift already proves this is not holding as a single source of truth. Consider extracting a shared JSON or at minimum adding a test that asserts parity.

Edge proxy violates gold standard architecture
api/sports-data.js makes direct external API calls from Vercel Edge to 5 upstream providers. Per the project's gold standard, Vercel reads Redis only and Railway makes all external API calls. For live scoreboards with short TTLs this is defensible, but slow-changing data (league lists @ 6h, player lookups @ 1h, standings @ 10min) should be Railway-seeded. Additionally, TheSportsDB API key 123 is the free demo key with severe rate limits (currently returns only 10 leagues, all soccer).

ESPN HTML scraping is fragile
extractEspnFittState() parses window['__espnfitt__'] from ESPN's rendered HTML for NBA standings. ESPN changes page structure frequently. The ESPN site API (site.api.espn.com/apis/v2/sports/basketball/nba/standings) should work as a JSON endpoint instead.


P3 (Suggestions)

F1 team aliases stale for 2026
src/services/sports.ts: F1_TEAM_ASSETS is missing Audi (Sauber rebrand) and Cadillac F1 Team. Add 'audi', 'audi f1 team' to the Sauber entry, and add a Cadillac entry with SVG.

Map not earning priority-1 slot
src/config/variants/sports.ts:10,30: map is enabled at priority 1 but only exposes dayNight. Either give it sports-native overlays/interaction, or demote/remove it until that exists.

Config duplication
src/config/variants/sports.ts duplicates SPORTS_PANELS and SPORTS_MAP_LAYERS already defined in src/config/panels.ts. Other variants reference the panels config directly.

Large files

  • SportsTablesPanel.ts (417 lines), SportsPlayerSearchPanel.ts (382 lines), sportsPanelShared.ts (418 lines) exceed the 150-line guideline.
  • src/services/sports.ts (1451 lines) handles 5 providers. Natural split: sports-espn.ts, sports-motorsport.ts, sports-player.ts.

What's Good

  • Threat classifier correctly bypasses sports variant (both server _classifier.ts and client threat-classifier.ts)
  • Sports excluded from CLASSIFY_VARIANTS in ais-relay.cjs with clear comment
  • Edge proxy has solid allowlist-based SSRF protection (endpoint + param validation)
  • Client-side request deduplication via inFlight Map
  • parseEventTimestamp handles multiple date formats gracefully
  • Guardrail tests cover the critical integration points
  • Data loader correctly skips geopolitical/financial data for sports variant
  • CDN cache headers are well-tuned per endpoint (live summaries @ 2min, tables @ 10min)

Direction

  • Agree: single sports variant, no sub-mid variant
  • The P1 items (variant gating + feed quality) need fixing before merge. The rest can be iterative.

# Conflicts:
#	src/App.ts
#	src/app/data-loader.ts
#	src/app/panel-layout.ts
#	src/components/index.ts
#	src/config/variants/base.ts
@FayezBast
Copy link
Copy Markdown
Collaborator Author

did the improvements, ur call if u want sports variant for now or later in the future.

@FayezBast FayezBast marked this pull request as ready for review April 13, 2026 06:23
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants