Spaces:
Running
A newer version of the Gradio SDK is available: 6.14.0
Developer Documentation — League Table Manager
Table of Contents
- Architecture Overview
- Module Reference
- Data Model
- Database Setup
- Running Locally
- Deployment (Hugging Face Spaces)
- Adding New Features
- UI / Styling Guide
1. Architecture Overview
The app is a Gradio web interface backed by a Supabase (PostgreSQL) database. It follows a simple layered structure:
Browser <--> Gradio (interface.py)
|
renderers.py (HTML output)
crud.py (writes to DB + cache)
data.py (reads from DB, holds cache)
|
config.py (Supabase client, env vars)
|
Supabase DB (persistent storage)
In-memory cache: data.matches is a module-level list that acts as a read-through cache. It is populated on startup by load_matches() and kept in sync by the CRUD functions. Every UI callback reads from data.matches — it never queries Supabase on every render.
No background threads / queues. All operations are synchronous. Gradio handles concurrency at the request level; the shared data.matches list is only safe here because Gradio's default queue serialises event handlers in a single process.
2. Module Reference
app.py
Entry point. Calls build_interface() and launches the Gradio app with the custom theme and CSS.
from interface import build_interface
from theme import LeagueTheme, CSS
demo = build_interface()
demo.launch(theme=LeagueTheme(), css=CSS)
config.py
Sets up shared infrastructure that every other module imports.
| Symbol | Type | Purpose |
|---|---|---|
logger |
logging.Logger |
App-wide logger |
IST |
datetime.timezone |
UTC+5:30, used for timestamping matches |
SUPABASE_URL |
str |
Read from SUPABASE_URL env var |
SUPABASE_KEY |
str |
Read from SUPABASE_KEY env var |
supabase |
supabase.Client |
Initialized Supabase client |
data.py
Owns the in-memory match cache and all read/compute operations.
| Symbol | Purpose |
|---|---|
matches |
Module-level list. Each entry: [id, home, away, home_goals, away_goals, datetime_str] |
load_matches() |
Fetches all rows from Supabase matches table, rebuilds data.matches |
get_teams_from_matches() |
Returns sorted list of unique team names from data.matches |
calculate_table(matches_list) |
Returns a pandas DataFrame with all league stats (P, W, D, L, GF, GA, GD, Pts, GPM, GAM, GDM, WP, #WW, #5GM) |
_parse_datetime(dt) |
Parses ISO datetime strings with variable microsecond precision |
format_datetime(dt) |
Converts a UTC datetime string to a human-readable IST string |
Important: Other modules must access the cache as data.matches (not from data import matches) so that the reference stays live after load_matches() replaces the list object.
theme.py
Contains the visual design system.
| Symbol | Purpose |
|---|---|
LeagueTheme |
Gradio Base subclass — dark slate/green color scheme using Inter font |
CSS |
Global CSS string injected into the Gradio app for table headers, buttons, inputs, etc. |
renderers.py
Pure functions that take data and return HTML strings. No side effects, no Supabase calls.
| Function | Output |
|---|---|
render_league_table_html(matches_list) |
Full standings table with medal/color highlights |
render_match_history_html(matches_list) |
Scrollable chronological match list |
render_stat_cards(matches_list) |
Grid of record cards (highest scoring, streaks, milestones, etc.) |
render_h2h_stats_html(team1, team2, matches_list) |
Hero card with tri-color bar, form dots, mini records, and stats table |
get_h2h_match_history_html(team1, team2, matches_list) |
Filtered match history for H2H pair |
make_status(msg) |
Green success / red error banner HTML |
update_score_preview(home, away, hg, ag) |
Live score preview card HTML |
crud.py
Handles all writes to Supabase and keeps data.matches in sync.
| Function | Description |
|---|---|
add_match(home, away, home_goals, away_goals) |
Validates inputs, inserts into Supabase, appends to data.matches. Returns status string. |
delete_match_by_id(match_id) |
Deletes from Supabase, removes from data.matches. Returns True/False. |
update_match(row_number, new_home, new_away, new_home_goals, new_away_goals) |
Updates Supabase row and mutates the entry in data.matches. row_number is the 1-based display row from match history (sorted newest-first). Returns status string. |
interface.py
Builds the Gradio Blocks layout. All UI event handlers (.click, .change, demo.load) are defined here as closures inside build_interface().
Tabs:
- Standings — Displays
render_league_table_html+ collapsible column guide - Add Match — Split layout: add form (left) + match history with delete/edit accordions (right)
- Head to Head — Team dropdowns → H2H stats card + filtered match history
- Records —
render_stat_cardsgrid
Page load refresh: demo.load calls load_matches() then re-renders all HTML components and refreshes all dropdown choices. This ensures the UI is always up to date on each browser load.
3. Data Model
Match record (in-memory)
[id, home, away, home_goals, away_goals, datetime_str]
# index: 0 1 2 3 4 5
All indices are used directly throughout the codebase — no named fields. The datetime string is an ISO 8601 string stored in UTC by Supabase.
League table columns
| Column | Formula |
|---|---|
| WP (Win Rate %) | W / P * 100 |
| GPM (Goals Per Match) | GF / P |
| GAM (Goals Against per Match) | GA / P |
| GDM (Goal Difference per Match) | (GF - GA) / P |
| #WW (Whitewash Wins) | Wins where opponent scored 0 |
| #5GM (5-Goal Matches) | Matches where this team scored >= 5 |
Standings are sorted by WP descending.
4. Database Setup
You need a Supabase project with the following table:
create table matches (
id bigint primary key generated always as identity,
home text not null,
away text not null,
home_goals integer not null default 0,
away_goals integer not null default 0,
datetime timestamptz not null default now(),
updated_at timestamptz
);
Enable Row Level Security as appropriate for your use case. The anon key is sufficient for read/write if RLS is disabled or permissive policies are set.
Credentials go in .env.local (local) or Space secrets (HF deployment):
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_KEY=your-anon-key-here
5. Running Locally
# Install uv if not already installed
curl -LsSf https://astral.sh/uv/install.sh | sh
# Install project dependencies
uv sync
# Set credentials
cp .env.example .env.local
# Edit .env.local
# Run
./local_run_space.sh
# or directly:
uv run app.py
The app starts on http://localhost:7860 by default.
6. Deployment (Hugging Face Spaces)
- Create a new Gradio Space on huggingface.co/spaces
- Set
app_file: app.pyin the Space metadata (already set in README.md header) - Add
SUPABASE_URLandSUPABASE_KEYas Space secrets (Settings → Repository secrets) - Push the repo — HF will install from
pyproject.tomland runapp.py
The requirements.txt file is kept for compatibility; primary dependency management is via pyproject.toml + uv.lock.
7. Adding New Features
Adding a new stat to the standings table
- In
data.py→calculate_table(): add the new key to thetabledict initializer, compute its value in the loops, and include it in thedfcolumn list. - In
renderers.py→render_league_table_html(): add a_rank_mapcall for the new column, compute its style via_medal_style, add a<td>to the row template, and a<th>to the header.
Adding a new tab
- In
interface.py→build_interface(): add a newwith gr.Tab("Name"):block inside thewith gr.Tabs():context. - Add the relevant
gr.HTMLcomponent and wire up event handlers. - If the tab needs to refresh on page load, add its output component to the
demo.loadoutputs list and return its rendered value fromrefresh_all().
Adding a new renderer
Add a new function to renderers.py. It should:
- Accept
matches_list(or a subset) as its primary input - Return an HTML string
- Have no side effects (no Supabase calls, no mutations to
data.matches)
8. UI / Styling Guide
Color palette
| Token | Hex | Usage |
|---|---|---|
| Background | #0f172a |
Page and input backgrounds |
| Surface | #1e293b |
Cards, table rows |
| Border | #334155 |
All borders |
| Text primary | #f1f5f9 |
Main text |
| Text secondary | #94a3b8 |
Labels, metadata |
| Text muted | #64748b |
Empty states, placeholders |
| Green | #22c55e |
Wins, positive values, accent |
| Red | #ef4444 |
Losses, errors |
| Yellow | #eab308 |
Draws, warnings |
| Blue | #3b82f6 |
Team 1 in H2H |
| Gold | #f59e0b |
Rank 1 medal |
| Silver | #b0b8c4 |
Rank 2 medal |
| Bronze | #cd7f32 |
Rank 3 medal |
Fonts
The app uses Inter (via Google Fonts) with fallbacks to ui-sans-serif, system-ui, sans-serif. All inline styles reference font-family:Inter,ui-sans-serif,sans-serif or font-family:Inter,sans-serif.
Row highlights (standings)
- WP >= 60%: green left border (
#22c55e) - WP >= 40%: yellow left border (
#eab308) - WP < 40%: red left border (
#ef4444)