From 3adc064a429bb7b27b9fcbc89e4f562f5c4b1ed0 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Wed, 28 Jan 2026 13:47:03 -0600 Subject: [PATCH] Fix Gameday lineup row selection and deselect behavior - Fix bug where clicking to select a player in the middle of the lineup would operate on the last added player instead of the clicked row - Deselect now requires clicking the same row twice (for screenshots) - Clicking the table after deselect re-enables selection mode - Fix main.py to actually launch the TUI app - Add CLAUDE.md with codebase documentation Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 90 ++++++++++++++++++++++++++++++++ main.py | 4 +- src/sba_scout/screens/gameday.py | 22 ++++++-- 3 files changed, 108 insertions(+), 8 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c518428 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,90 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +SBA Scout is a TUI (Terminal User Interface) application for SBA fantasy baseball scouting and team management. Built with Textual for the interface, SQLAlchemy for async database operations, and pydantic-settings for configuration. + +## Commands + +```bash +# Install dependencies (use uv, not pip) +uv pip install -e . + +# Run the application +sba-scout # via console script +python main.py # direct execution + +# Lint code +ruff check src/ +ruff check --fix src/ # auto-fix + +# Format code +ruff format src/ +``` + +There is no test suite currently (tests/ directory exists but is empty). + +## Architecture + +### Layer Structure + +``` +src/sba_scout/ +├── api/ # External API integration +├── calc/ # Scoring and statistics calculations +├── db/ # SQLAlchemy models and queries +├── screens/ # Textual UI screens +├── app.py # Main application and DashboardScreen +└── config.py # Pydantic settings management +``` + +### Data Flow + +1. **API Layer** (`api/`) syncs data from League API → SQLite database +2. **CSV Import** (`api/importer.py`) loads batter/pitcher card data from spreadsheets +3. **Database Layer** (`db/`) stores teams, players, cards, lineups +4. **Calc Layer** (`calc/`) computes standardized matchup scores using league stats +5. **UI Layer** (`screens/`, `app.py`) displays data via Textual TUI + +### Key Patterns + +- **Async throughout**: SQLAlchemy async with aiosqlite, httpx async client +- **Lazy singletons**: `get_settings()` returns global Settings, database engine initialized on first use +- **Context managers**: API client and database sessions use `async with` +- **Layered config**: Pydantic defaults → .env file → data/settings.yaml (user editable) + +### Database Models (db/models.py) + +Core models: `Team`, `Player`, `BatterCard`, `PitcherCard`, `Lineup`, `MatchupCache`, `StandardizedScoreCache`, `SyncStatus`, `Transaction` + +### Screens (screens/) + +- `roster.py` - Tabbed roster view (majors/minors/IL) +- `matchup.py` - Batter vs pitcher analysis +- `lineup.py` - Lineup builder with drag/drop +- `gameday.py` - Combined matchup + lineup view +- `settings.py` - Configuration UI with YAML persistence + +### Matchup Scoring System (calc/) + +- Standardizes stats to -3 to +3 range based on league averages/stdev +- Handedness-aware (vLHP/vRHP for batters, vLHB/vRHB for pitchers) +- Weighted composite scores with tier assignment (A/B/C/D/F) +- Cached for performance in `StandardizedScoreCache` + +## Configuration + +Environment variables use `SBA_SCOUT_` prefix with `__` for nesting: +- `SBA_SCOUT_API__BASE_URL` +- `SBA_SCOUT_API__API_KEY` +- `SBA_SCOUT_TEAM__TEAM_ABBREV` + +User settings saved to `data/settings.yaml` (editable via Settings screen or directly). + +## Code Style + +- Line length: 100 characters +- Python 3.12+ +- Ruff rules: E, F, I, N, W, UP (standard + imports + naming + upgrades) diff --git a/main.py b/main.py index 2b333c0..034688d 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,4 @@ -def main(): - print("Hello from sba-scout!") - +from sba_scout.app import main if __name__ == "__main__": main() diff --git a/src/sba_scout/screens/gameday.py b/src/sba_scout/screens/gameday.py index ab8debb..e6027f6 100644 --- a/src/sba_scout/screens/gameday.py +++ b/src/sba_scout/screens/gameday.py @@ -12,9 +12,10 @@ import logging from dataclasses import dataclass from typing import ClassVar, Optional -from textual.app import ComposeResult +from textual.app import ComposeResult, on from textual.binding import Binding from textual.containers import Horizontal, ScrollableContainer, Vertical +from textual.events import Click from textual.screen import Screen from textual.widgets import ( Button, @@ -91,6 +92,7 @@ class GamedayScreen(Screen): lineup_slots: list[LineupSlot] = [] saved_lineups: list[Lineup] = [] current_lineup_name: str = "" + _last_lineup_row: int | None = None # Track last selected row for deselect toggle def compose(self) -> ComposeResult: """Compose the gameday layout with side-by-side panels.""" @@ -465,15 +467,17 @@ class GamedayScreen(Screen): await self.action_save_lineup() def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None: - """Handle row selection - toggle cursor off if clicking same row.""" + """Handle row selection - toggle cursor off only if clicking same row twice.""" table = event.data_table if table.id == "lineup-table": - # If cursor is "row" type, switch to "none" to hide highlight - # User can click again or use arrow keys to restore - if table.cursor_type == "row": + current_row = event.cursor_row + # Only toggle off if clicking the same row that was already selected + if table.cursor_type == "row" and current_row == self._last_lineup_row: table.cursor_type = "none" + self._last_lineup_row = None else: table.cursor_type = "row" + self._last_lineup_row = current_row def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None: """Re-enable cursor when navigating with keys.""" @@ -482,6 +486,14 @@ class GamedayScreen(Screen): # Re-enable cursor when user navigates table.cursor_type = "row" + @on(Click, "#lineup-table") + def on_lineup_table_click(self, event: Click) -> None: + """Re-enable cursor when lineup table is clicked while deselected.""" + lineup_table = self.query_one("#lineup-table", DataTable) + if lineup_table.cursor_type == "none": + lineup_table.cursor_type = "row" + self._last_lineup_row = None + # ========================================================================= # Actions # =========================================================================