| name | system-overview |
| description | Use when you need a full technical, current-state summary of the system's backend, frontend, data model, API surface, runtime flow, and request/data lifecycle in English. Best for end-to-end architecture overviews of how the app works now, not a redesign or roadmap. |
System Overview — Leaderboard-Image
This skill contains pre-researched, concrete facts about the current codebase. Use them directly when answering architecture questions, explaining how the system works, or helping with code changes that touch multiple layers.
What the App Does
A Flask web app that ranks AI image-generation models via ELO scoring. Users vote in blind pairwise comparisons (Arena Battle), compare models side by side (Side-by-Side), and view ranked results (Leaderboard). ELO scores update on every vote and are tracked over time (ELO History chart).
File Map
| File / Directory | Role |
|---|
app.py | Flask app factory, all route handlers, startup logic |
config.py | All configuration constants and the MODELS dict |
database.py | SQLite helpers, schema init, ELO calculation |
auth.py | OAuth (Google + GitHub), @login_required, session helpers |
data/<prompt_id>/ | prompt.txt + model image files per prompt |
data/manifest.json | Image filename map used in remote/DATA_MODE (generated by generate_manifest.py) |
templates/index.html | Single HTML template, passes config into data-* attributes |
static/js/main.js | Tab switching, initializes the active mode module |
static/js/battle.js | Arena Battle UI logic |
static/js/sideBySide.js | Side-by-Side UI logic |
static/js/leaderboard.js | Leaderboard table rendering, filter logic |
static/js/eloHistory.js | ELO chart using Chart.js (category axis, Top-N slider) |
static/js/compare.js | Model comparison stats UI |
static/js/api.js | Shared fetchData() wrapper, loading indicator, 401 handling |
static/js/config.js | colorPalette, getRevealDelayMs() reads <html data-reveal-delay> |
static/js/auth.js | Login/logout button state |
Startup Sequence (app.py)
When Flask starts, the following run before the first request, inside with app.app_context():
init_db() — creates missing tables, inserts missing models into model_elo, adds frozen and user_id columns if absent.
update_frozen_models() — ranks all models by ELO ascending, freezes the bottom FROZEN_BOTTOM_COUNT (default 0, disabled).
AVAILABLE_PROMPTS is populated lazily on first request via before_request → update_available_prompts() → database.get_prompt_ids() (scans data/ for dirs with prompt.txt).
In debug mode, update_available_prompts() runs on every request. In production it runs once until the list is non-empty.
Configuration (config.py)
| Constant | Value / Source | Effect |
|---|
MODELS | Dict of 67 entries, keys model-001…model-067 | Single source of truth for all model metadata |
DATABASE | votes.db or DATABASE_PATH env var | SQLite file path |
DATA_DIR | 'data' | Root directory for prompts and images |
ALLOWED_EXTENSIONS | ['.jpg', '.jpeg', '.png', '.webp'] | Extensions tried by find_model_file() |
DEFAULT_ELO | 1500 | Starting ELO for all models |
K_FACTOR | 32 | ELO update magnitude per vote |
REVEAL_DELAY_MS | 2000 | Ms before model names reveal after vote; injected into <html data-reveal-delay> |
FROZEN_BOTTOM_COUNT | 0 (disabled) | How many bottom-ranked models to exclude from Arena |
SECRET_KEY | env SECRET_KEY | Flask session signing |
GOOGLE_CLIENT_ID/SECRET | env vars | OAuth |
GITHUB_CLIENT_ID/SECRET | env vars | OAuth |
Each entry in MODELS has: name, filename (base, no extension), open_source (bool), provider, release_date, type (image-generation / multimodal / image-editing), tags, max_resolution, pricing, api_available, speed, website.
Database Schema (database.py)
votes
id INTEGER PRIMARY KEY AUTOINCREMENT
prompt_id TEXT NOT NULL
winner TEXT NOT NULL
loser TEXT NOT NULL
voted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
user_id INTEGER
model_elo
model TEXT PRIMARY KEY
elo REAL NOT NULL
last_updated TIMESTAMP
frozen INTEGER DEFAULT 0
elo_history
id INTEGER PRIMARY KEY AUTOINCREMENT
model TEXT NOT NULL
elo REAL NOT NULL
timestamp TIMESTAMP
Written on every vote (both winner and loser). Used by the ELO History chart.
users
id INTEGER PRIMARY KEY AUTOINCREMENT
provider TEXT NOT NULL
provider_id TEXT NOT NULL
email TEXT
name TEXT
created_at TIMESTAMP
last_login TIMESTAMP
UNIQUE(provider, provider_id)
Key DB functions
get_db() — new sqlite3.connect() per call, row_factory = sqlite3.Row.
update_elo(db, winner_id, loser_id) — reads current ELOs, applies standard ELO formula with K_FACTOR=32, writes to model_elo and inserts two rows into elo_history. Returns (winner_new_elo, loser_new_elo).
init_db() — idempotent, safe to call on every startup.
Image Resolution (app.py)
find_model_file(prompt_id, model_base_name)
- Local mode: tries
data/<prompt_id>/<model_base_name><ext> for each ALLOWED_EXTENSIONS, then falls back to glob.
- DATA_MODE: reads
data/manifest.json → manifest[prompt_id][model_base_name], no filesystem access for images.
get_image_url(prompt_id, filename)
- Local: returns
/images/<prompt_id>/<filename> (served by Flask via send_from_directory).
- DATA_MODE: returns
{DATA_MODE}/{prompt_id}/{filename} (external CDN/R2 URL).
DATA_MODE is set via the DATA_MODE environment variable (set on Render.com, not locally).
API Endpoints (app.py)
| Endpoint | Method | Auth | Description |
|---|
/ | GET | — | Renders index.html, injects sorted model list and reveal_delay_ms |
/images/<prompt_id>/<filename> | GET | — | Serves local image via send_from_directory |
/static/js/<filename> | GET | — | Serves JS with correct MIME type for ES modules |
/api/battle_data | GET | — | Random prompt + 2 random non-frozen models with image URLs |
/api/side_by_side_data | GET | — | Prompt + 2–3 specified models with image URLs |
/api/get_image | GET | — | Image URL for one model + prompt combo |
/api/vote | POST | ✅ | Records vote, updates ELO. Body: {prompt_id, winner, loser} |
/api/leaderboard | GET | — | All models sorted by ELO. Query: ?model_type=all|open-source|closed-source |
/api/leaderboard/mine | GET | ✅ | ELO recalculated from the current user's votes only |
/api/elo_history | GET | — | {modelName: [{x: timestamp, y: elo}]} (may be deprecated) |
/api/elo_history_with_current_elo | GET | — | {history: {...}, current_elos: {modelName: elo}} |
/api/prompt_ids | GET | — | {prompt_ids: ['001', '002', ...]} |
/api/prompt_text | GET | — | {prompt_text: "..."} for a given ?prompt_id= |
/api/model_info | GET | — | Config data for 1–2 models (?model1=&model2=) |
/api/compare_stats | GET | — | Head-to-head stats + per-prompt breakdown for 2 models |
/auth/login/<provider> | GET | — | Starts OAuth flow (google or github) |
/auth/callback/<provider> | GET | — | OAuth callback, creates/updates user, sets session |
/auth/logout | GET | — | Clears session |
/auth/dev-login | GET | — | Debug-only instant login as dev@localhost |
@login_required (from auth.py) returns {"error": "...", "login_required": true} with HTTP 401 if the session has no user.
Frontend Architecture
templates/index.html is a single-page app. Tabs switch between modes without page reload. Configuration is injected via HTML attributes:
<html data-reveal-delay="2000">
All JS files are ES modules (type="module"). main.js imports and initializes mode modules based on the active tab.
Mode → Module → API mapping
| Mode | Module | APIs called |
|---|
| Arena Battle | battle.js | GET /api/battle_data, POST /api/vote |
| Side-by-Side | sideBySide.js | GET /api/side_by_side_data, GET /api/get_image |
| Leaderboard | leaderboard.js | GET /api/leaderboard, GET /api/leaderboard/mine |
| ELO History | eloHistory.js | GET /api/elo_history_with_current_elo |
| Compare | compare.js | GET /api/model_info, GET /api/compare_stats |
api.js — shared fetch wrapper
fetchData(url, options) shows/hides #loading-indicator, catches HTTP errors, handles 401 by showing #login-required-message, returns parsed JSON or null.
eloHistory.js — Chart.js rendering
- Calls
/api/elo_history_with_current_elo.
- Sorts models by current ELO, applies Top-N slider filter.
- Normalizes all timestamps to a sorted category axis (not a time axis) to avoid Chart.js time-scale issues.
- Colors from
config.js#colorPalette (cycles through 10 colors).
- Mouse-hold on legend fades all other series to 10% opacity.
Key Data Flows
Arena Battle vote (full cycle)
battle.js → GET /api/battle_data
- Backend: picks random prompt from
AVAILABLE_PROMPTS, picks 2 random non-frozen model IDs from model_elo WHERE frozen=0, calls find_model_file() for each, returns JSON with image URLs and prompt text. Model names are included but the frontend hides them until after the vote.
- User clicks a winner →
battle.js → POST /api/vote {prompt_id, winner, loser}
- Backend
record_vote(): inserts into votes, calls update_elo(db, winner, loser).
update_elo(): reads both ELOs → applies formula → writes updated ELOs to model_elo → inserts 2 rows into elo_history.
- Response:
{success: true, winner_new_elo, loser_new_elo}.
battle.js reveals model names for REVEAL_DELAY_MS ms, then auto-loads next battle.
Leaderboard load
leaderboard.js → GET /api/leaderboard?model_type=all
- Backend: queries
votes for win/match counts, queries model_elo for ELO + frozen flag, merges with MODELS config, sorts by ELO descending.
- Response: array of
{id, name, display, provider, wins, matches, win_rate, elo, open_source, frozen}.
Personal leaderboard (/api/leaderboard/mine)
Recalculates ELO from scratch using only the current user's votes in chronological order, ignoring the shared model_elo table. Returns {leaderboard: [...], vote_count: N}.
Runtime Branches
| Condition | Behavior |
|---|
DATA_MODE env set | Images served from remote URL; manifest.json used for file resolution; ProxyFix applied; SESSION_COOKIE_SECURE=True |
app.debug=True | AVAILABLE_PROMPTS refreshed on every request; /auth/dev-login available |
FROZEN_BOTTOM_COUNT > 0 | Bottom N models by ELO excluded from Arena Battle (still visible in Leaderboard and Side-by-Side) |
No GOOGLE_CLIENT_ID set | Google login button not shown |
No GITHUB_CLIENT_ID set | GitHub login button not shown |
| Image file not found | find_model_file() returns None; route returns HTTP 500 with error JSON |
Operations
python app.py
flask run --host=0.0.0.0
python app.py reset-votes
python database.py
python generate_manifest.py
gunicorn -c gunicorn.conf.py app:app
fix/ directory contains one-off migration scripts for historical database repairs (e.g., fixing model IDs after a rename). These are not part of normal operation.
Adding a Model
- Add entry to
MODELS in config.py with a new key (model-NNN).
- Add image files to every
data/<prompt_id>/ directory, named <filename>.<ext> where filename matches the config entry.
- Restart —
init_db() auto-inserts the new model into model_elo with DEFAULT_ELO.
- If using DATA_MODE, re-run
generate_manifest.py and commit manifest.json.
Adding a Prompt
- Create
data/<new_id>/ directory.
- Add
prompt.txt with the prompt text.
- Add one image per model named
<model_filename>.<ext>.
- Restart (or wait for
before_request refresh in debug mode).