File size: 9,919 Bytes
c04d3f9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# Developer Documentation — League Table Manager

## Table of Contents

1. [Architecture Overview](#1-architecture-overview)
2. [Module Reference](#2-module-reference)
3. [Data Model](#3-data-model)
4. [Database Setup](#4-database-setup)
5. [Running Locally](#5-running-locally)
6. [Deployment (Hugging Face Spaces)](#6-deployment-hugging-face-spaces)
7. [Adding New Features](#7-adding-new-features)
8. [UI / Styling Guide](#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.

```python
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. **Records**`render_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)

```python
[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:

```sql
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

```bash
# 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](https://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.py``calculate_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.py``render_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.py``build_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`)