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 <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2026-01-28 13:47:03 -06:00
parent 9c59c79c98
commit 3adc064a42
3 changed files with 108 additions and 8 deletions

90
CLAUDE.md Normal file
View File

@ -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)

View File

@ -1,6 +1,4 @@
def main():
print("Hello from sba-scout!")
from sba_scout.app import main
if __name__ == "__main__":
main()

View File

@ -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
# =========================================================================