asvs's picture
Refactor: split monolithic app.py into maintainable modules
c04d3f9

A newer version of the Gradio SDK is available: 6.14.0

Upgrade

Developer Documentation — League Table Manager

Table of Contents

  1. Architecture Overview
  2. Module Reference
  3. Data Model
  4. Database Setup
  5. Running Locally
  6. Deployment (Hugging Face Spaces)
  7. Adding New Features
  8. 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:

  1. Standings — Displays render_league_table_html + collapsible column guide
  2. Add Match — Split layout: add form (left) + match history with delete/edit accordions (right)
  3. Head to Head — Team dropdowns → H2H stats card + filtered match history
  4. Recordsrender_stat_cards grid

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)

  1. Create a new Gradio Space on huggingface.co/spaces
  2. Set app_file: app.py in the Space metadata (already set in README.md header)
  3. Add SUPABASE_URL and SUPABASE_KEY as Space secrets (Settings → Repository secrets)
  4. Push the repo — HF will install from pyproject.toml and run app.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

  1. In data.pycalculate_table(): add the new key to the table dict initializer, compute its value in the loops, and include it in the df column list.
  2. In renderers.pyrender_league_table_html(): add a _rank_map call 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

  1. In interface.pybuild_interface(): add a new with gr.Tab("Name"): block inside the with gr.Tabs(): context.
  2. Add the relevant gr.HTML component and wire up event handlers.
  3. If the tab needs to refresh on page load, add its output component to the demo.load outputs list and return its rendered value from refresh_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)