Architecture
The Player Wellness App is a single-page Streamlit application backed by a PostgreSQL database. It is intentionally simple — the registration app captures data, and all analysis and reporting happens in a separate reporting application.
File structure
player_wellness_registration/
├── main.py # Streamlit app — UI only, no DB logic
├── database.py # All database interaction
├── models.py # SQLAlchemy ORM table definitions
├── requirements.txt # Python dependencies
├── images/
│ └── BORG_RPE_scale.png # Static reference image
├── .streamlit/
│ └── secrets.toml # Database connection string (not in git)
└── docs/ # MkDocs documentation source
├── mkdocs.yml
└── docs/
├── index.md
├── user-guide/
└── developer/
Layer responsibilities
| File | Responsibility |
|---|---|
main.py |
Page config, tab layout, all Streamlit UI widgets |
database.py |
Engine creation, player queries, wellness and RPE inserts |
models.py |
SQLAlchemy ORM class definitions, table constraints |
main.py only imports from database.py. It never touches SQLAlchemy directly. This keeps the UI layer clean and makes the database layer independently testable.
Request flow
Player opens app
→ get_engine() creates SQLAlchemy engine (cached for session)
→ Base.metadata.create_all() verifies / creates tables
→ get_player_ids() populates the player ID selectbox
Player submits Pre-Training form
→ insert_wellness() writes a row to player_wellness
→ duplicate submission (same player_id + session_id) is silently ignored
Player submits Post-Training form
→ insert_rpe() calculates session_load = rpe_score × training_minutes
→ writes a row to player_rpe
→ duplicate submission is silently ignored
Session ID
The session_id is a string key derived in the UI from the selected date and the first two digits of the player ID:
session_id = f"{date.strftime('%Y%m%d')}U{str(player_id)[:2]}"
# Example: player 7, date 3 Sep 2024 → "20240903U07"
This key links player registrations to sessions in the reporting app. Session records in the sessions table are created separately by coaching staff. Player registrations are never blocked by a missing session — the link is resolved at reporting time.
Idempotent inserts
Both insert_wellness and insert_rpe are idempotent. If a player submits the same form twice for the same session, the second submission is silently ignored via a UNIQUE constraint on (player_id, session_id). The player sees a success message either way.