feat: implement MCP server and dashboard for football data platform

Add complete Nike football data platform with:
- FastMCP server exposing football data tools over HTTP
- RapidAPI client for free-api-live-football-data integration
- Bootstrap web dashboard with live match/standings views
- REST API endpoints for dashboard consumption
- Docker support with multi-stage build
- Comprehensive README with architecture docs
- Minimal .gitignore replacing verbose Python template
This commit is contained in:
2026-03-21 18:19:42 +00:00
parent b8689d530a
commit ee8436d5b8
81 changed files with 50251 additions and 176 deletions

194
schema.sql Normal file
View File

@@ -0,0 +1,194 @@
-- Nike Football Database Schema — TheSportsDB cache layer
--
-- Caching strategy:
-- PERMANENT: leagues, teams, players (change rarely, cache aggressively)
-- SEMI-PERMANENT: finished match results, lineups, stats, timeline
-- VOLATILE (NOT cached): standings, live scores, upcoming fixtures
--
-- All IDs come from TheSportsDB (idLeague, idTeam, idEvent, idPlayer).
-- No surrogate keys — TheSportsDB IDs are the primary keys.
-- ══════════════════════════════════════════════════════════
-- Core reference tables (permanent cache)
-- ══════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS leagues (
id INTEGER PRIMARY KEY, -- TheSportsDB idLeague
name VARCHAR(255) NOT NULL, -- strLeague
sport VARCHAR(50) DEFAULT 'Soccer', -- strSport
country VARCHAR(100), -- strCountry
badge_url VARCHAR(500), -- strBadge
banner_url VARCHAR(500), -- strBanner
description TEXT, -- strDescriptionEN
is_followed BOOLEAN DEFAULT FALSE,
cached_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS teams (
id INTEGER PRIMARY KEY, -- TheSportsDB idTeam
name VARCHAR(255) NOT NULL, -- strTeam
short_name VARCHAR(10), -- strTeamShort
alternate_name VARCHAR(255), -- strTeamAlternate
league_id INTEGER, -- idLeague (primary), no FK — cached opportunistically
league_name VARCHAR(255), -- strLeague
formed_year INTEGER, -- intFormedYear
sport VARCHAR(50) DEFAULT 'Soccer',
country VARCHAR(100), -- strCountry
stadium VARCHAR(255), -- strStadium
stadium_capacity INTEGER, -- intStadiumCapacity
location VARCHAR(255), -- strLocation
venue_id INTEGER, -- idVenue
badge_url VARCHAR(500), -- strBadge
logo_url VARCHAR(500), -- strLogo
banner_url VARCHAR(500), -- strBanner
equipment_url VARCHAR(500), -- strEquipment
colour1 VARCHAR(20), -- strColour1
colour2 VARCHAR(20), -- strColour2
colour3 VARCHAR(20), -- strColour3
website VARCHAR(255), -- strWebsite
description TEXT, -- strDescriptionEN
is_followed BOOLEAN DEFAULT FALSE,
cached_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS players (
id INTEGER PRIMARY KEY, -- TheSportsDB idPlayer
team_id INTEGER, -- idTeam, no FK — cached opportunistically
team_name VARCHAR(255), -- strTeam
name VARCHAR(255) NOT NULL, -- strPlayer
nationality VARCHAR(100), -- strNationality
date_of_birth DATE, -- dateBorn
position VARCHAR(50), -- strPosition
squad_number VARCHAR(10), -- strNumber
height VARCHAR(20), -- strHeight
weight VARCHAR(20), -- strWeight
gender VARCHAR(10), -- strGender
status VARCHAR(30), -- strStatus (Active/Retired)
thumb_url VARCHAR(500), -- strThumb
cutout_url VARCHAR(500), -- strCutout
description TEXT, -- strDescriptionEN
cached_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- ══════════════════════════════════════════════════════════
-- Match results (cached when strStatus = 'Match Finished')
-- ══════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY, -- TheSportsDB idEvent
name VARCHAR(255), -- strEvent
league_id INTEGER, -- idLeague, no FK — cached opportunistically
league_name VARCHAR(255), -- strLeague
season VARCHAR(20), -- strSeason
round VARCHAR(20), -- intRound
home_team_id INTEGER, -- idHomeTeam, no FK — cached opportunistically
away_team_id INTEGER, -- idAwayTeam, no FK — cached opportunistically
home_team VARCHAR(255), -- strHomeTeam
away_team VARCHAR(255), -- strAwayTeam
home_score INTEGER, -- intHomeScore
away_score INTEGER, -- intAwayScore
event_date DATE, -- dateEvent
event_time TIME, -- strTime
event_timestamp TIMESTAMPTZ, -- strTimestamp
venue VARCHAR(255), -- strVenue
venue_id INTEGER, -- idVenue
city VARCHAR(100), -- strCity
country VARCHAR(100), -- strCountry
referee VARCHAR(255), -- strOfficial
spectators INTEGER, -- intSpectators
status VARCHAR(50), -- strStatus
postponed VARCHAR(5) DEFAULT 'no', -- strPostponed
poster_url VARCHAR(500), -- strPoster
thumb_url VARCHAR(500), -- strThumb
video_url VARCHAR(500), -- strVideo
cached_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- ── Match detail sub-tables (V2, cached for finished matches) ──
CREATE TABLE IF NOT EXISTS event_stats (
id INTEGER PRIMARY KEY, -- TheSportsDB idStatistic
event_id INTEGER NOT NULL REFERENCES events(id) ON DELETE CASCADE,
id_api_football VARCHAR(20), -- idApiFootball
event_name VARCHAR(255), -- strEvent
stat_name VARCHAR(100) NOT NULL, -- strStat (e.g. "Shots on Goal")
home_value VARCHAR(50), -- intHome
away_value VARCHAR(50), -- intAway
UNIQUE(event_id, stat_name)
);
CREATE TABLE IF NOT EXISTS event_lineup (
id INTEGER PRIMARY KEY, -- TheSportsDB idLineup
event_id INTEGER NOT NULL REFERENCES events(id) ON DELETE CASCADE,
id_api_football VARCHAR(20), -- idAPIfootball
event_name VARCHAR(255), -- strEvent
player_id INTEGER, -- idPlayer
player_name VARCHAR(255), -- strPlayer
team_id INTEGER, -- idTeam
team_name VARCHAR(255), -- strTeam
is_home VARCHAR(5), -- strHome ("Yes"/"No")
position VARCHAR(50), -- strPosition
position_short VARCHAR(5), -- strPositionShort (G/D/M/F)
formation VARCHAR(20), -- strFormation
squad_number INTEGER, -- intSquadNumber
is_substitute VARCHAR(5) DEFAULT 'No', -- strSubstitute ("Yes"/"No")
cutout_url VARCHAR(500), -- strCutout
country VARCHAR(100), -- strCountry
season VARCHAR(20), -- strSeason
UNIQUE(event_id, player_id)
);
CREATE TABLE IF NOT EXISTS event_timeline (
id INTEGER PRIMARY KEY, -- TheSportsDB idTimeline
event_id INTEGER NOT NULL REFERENCES events(id) ON DELETE CASCADE,
id_api_football VARCHAR(20), -- idAPIfootball
event_name VARCHAR(255), -- strEvent
event_type VARCHAR(100), -- strTimeline ("Goal", "subst", "Yellow Card")
event_detail VARCHAR(255), -- strTimelineDetail ("Substitution 1")
is_home VARCHAR(5), -- strHome ("Yes"/"No")
minute INTEGER, -- intTime
period VARCHAR(20), -- strPeriod
player_id INTEGER, -- idPlayer
player_name VARCHAR(255), -- strPlayer
cutout_url VARCHAR(500), -- strCutout
assist_id INTEGER, -- idAssist
assist_name VARCHAR(255), -- strAssist
team_id INTEGER, -- idTeam
team_name VARCHAR(255), -- strTeam
comment TEXT, -- strComment
event_date DATE, -- dateEvent
season VARCHAR(20), -- strSeason
UNIQUE(event_id, minute, player_id, event_type)
);
-- ══════════════════════════════════════════════════════════
-- Cache management
-- ══════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS cache_meta (
cache_key VARCHAR(255) PRIMARY KEY, -- e.g. "standings:4346:2026"
fetched_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
ttl_seconds INTEGER NOT NULL DEFAULT 3600,
data_json JSONB -- optional: store raw response
);
-- ══════════════════════════════════════════════════════════
-- Indexes
-- ══════════════════════════════════════════════════════════
CREATE INDEX IF NOT EXISTS idx_teams_league ON teams(league_id);
CREATE INDEX IF NOT EXISTS idx_teams_followed ON teams(is_followed) WHERE is_followed;
CREATE INDEX IF NOT EXISTS idx_players_team ON players(team_id);
CREATE INDEX IF NOT EXISTS idx_events_date ON events(event_date);
CREATE INDEX IF NOT EXISTS idx_events_home_team ON events(home_team_id);
CREATE INDEX IF NOT EXISTS idx_events_away_team ON events(away_team_id);
CREATE INDEX IF NOT EXISTS idx_events_league ON events(league_id);
CREATE INDEX IF NOT EXISTS idx_events_status ON events(status);
CREATE INDEX IF NOT EXISTS idx_event_stats_event ON event_stats(event_id);
CREATE INDEX IF NOT EXISTS idx_event_lineup_event ON event_lineup(event_id);
CREATE INDEX IF NOT EXISTS idx_event_tl_event ON event_timeline(event_id);
CREATE INDEX IF NOT EXISTS idx_cache_meta_fetched ON cache_meta(fetched_at);