Add Rust project scaffold for TUI rewrite
Initialize rust/ subdirectory with ratatui + tokio + sqlx stack, mirroring the Python module structure. Includes all DB models, config loader, matchup scoring logic, and screen stubs that compile cleanly with cargo check. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7afe4a5f55
commit
6ddbd82f7c
1
rust/.gitignore
vendored
Normal file
1
rust/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
||||
4019
rust/Cargo.lock
generated
Normal file
4019
rust/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
37
rust/Cargo.toml
Normal file
37
rust/Cargo.toml
Normal file
@ -0,0 +1,37 @@
|
||||
[package]
|
||||
name = "sba-scout"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
# TUI
|
||||
ratatui = "0.30"
|
||||
crossterm = { version = "0.28", features = ["event-stream"] }
|
||||
|
||||
# Async runtime
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
# Database
|
||||
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] }
|
||||
|
||||
# HTTP client
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
|
||||
# Config
|
||||
figment = { version = "0.10", features = ["toml", "env"] }
|
||||
|
||||
# Serialization
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
toml = "0.8"
|
||||
|
||||
# Error handling
|
||||
anyhow = "1"
|
||||
thiserror = "2"
|
||||
|
||||
# Logging
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
|
||||
# Date/time
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
23
rust/src/api/client.rs
Normal file
23
rust/src/api/client.rs
Normal file
@ -0,0 +1,23 @@
|
||||
use anyhow::Result;
|
||||
use reqwest::Client;
|
||||
use std::time::Duration;
|
||||
|
||||
pub struct LeagueApiClient {
|
||||
client: Client,
|
||||
base_url: String,
|
||||
api_key: String,
|
||||
}
|
||||
|
||||
impl LeagueApiClient {
|
||||
pub fn new(base_url: &str, api_key: &str, timeout_secs: u64) -> Result<Self> {
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(timeout_secs))
|
||||
.build()?;
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
base_url: base_url.trim_end_matches('/').to_string(),
|
||||
api_key: api_key.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
1
rust/src/api/mod.rs
Normal file
1
rust/src/api/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod client;
|
||||
69
rust/src/app.rs
Normal file
69
rust/src/app.rs
Normal file
@ -0,0 +1,69 @@
|
||||
use crossterm::event::KeyEvent;
|
||||
use ratatui::{
|
||||
layout::{Constraint, Layout},
|
||||
text::Text,
|
||||
widgets::{Block, Borders, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Screen {
|
||||
Dashboard,
|
||||
Roster,
|
||||
Matchup,
|
||||
Lineup,
|
||||
Gameday,
|
||||
Settings,
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
pub current_screen: Screen,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
current_screen: Screen::Dashboard,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_key(&mut self, key: KeyEvent) {
|
||||
use crossterm::event::KeyCode;
|
||||
match key.code {
|
||||
KeyCode::Char('1') => self.current_screen = Screen::Dashboard,
|
||||
KeyCode::Char('2') => self.current_screen = Screen::Roster,
|
||||
KeyCode::Char('3') => self.current_screen = Screen::Matchup,
|
||||
KeyCode::Char('4') => self.current_screen = Screen::Lineup,
|
||||
KeyCode::Char('5') => self.current_screen = Screen::Gameday,
|
||||
KeyCode::Char('6') => self.current_screen = Screen::Settings,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&self, frame: &mut Frame) {
|
||||
let area = frame.area();
|
||||
|
||||
let chunks = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]).split(area);
|
||||
|
||||
// Navigation bar
|
||||
let nav = Paragraph::new(Text::raw(
|
||||
" [1] Dashboard [2] Roster [3] Matchup [4] Lineup [5] Gameday [6] Settings [q] Quit",
|
||||
))
|
||||
.block(Block::default().borders(Borders::ALL).title("SBA Scout"));
|
||||
frame.render_widget(nav, chunks[0]);
|
||||
|
||||
// Screen content
|
||||
let content = match self.current_screen {
|
||||
Screen::Dashboard => "Dashboard - Press a number key to navigate",
|
||||
Screen::Roster => "Roster Screen (not yet implemented)",
|
||||
Screen::Matchup => "Matchup Screen (not yet implemented)",
|
||||
Screen::Lineup => "Lineup Screen (not yet implemented)",
|
||||
Screen::Gameday => "Gameday Screen (not yet implemented)",
|
||||
Screen::Settings => "Settings Screen (not yet implemented)",
|
||||
};
|
||||
|
||||
let body = Paragraph::new(Text::raw(content))
|
||||
.block(Block::default().borders(Borders::ALL).title(format!("{:?}", self.current_screen)));
|
||||
frame.render_widget(body, chunks[1]);
|
||||
}
|
||||
}
|
||||
6
rust/src/calc/league_stats.rs
Normal file
6
rust/src/calc/league_stats.rs
Normal file
@ -0,0 +1,6 @@
|
||||
/// Distribution statistics for a single stat across the league.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StatDistribution {
|
||||
pub avg: f64,
|
||||
pub stdev: f64,
|
||||
}
|
||||
43
rust/src/calc/matchup.rs
Normal file
43
rust/src/calc/matchup.rs
Normal file
@ -0,0 +1,43 @@
|
||||
use super::league_stats::StatDistribution;
|
||||
use super::weights::StatWeight;
|
||||
|
||||
/// Convert a raw stat value to a standardized score (-3 to +3).
|
||||
///
|
||||
/// Uses standard deviation thresholds from the league mean.
|
||||
/// A value of 0 always returns +3 (best possible).
|
||||
pub fn standardize_value(value: f64, distribution: &StatDistribution, high_is_better: bool) -> i32 {
|
||||
if value == 0.0 {
|
||||
return 3;
|
||||
}
|
||||
|
||||
let avg = distribution.avg;
|
||||
let stdev = distribution.stdev;
|
||||
|
||||
let base_score = if value > avg + 2.0 * stdev {
|
||||
-3
|
||||
} else if value > avg + stdev {
|
||||
-2
|
||||
} else if value > avg + 0.33 * stdev {
|
||||
-1
|
||||
} else if value > avg - 0.33 * stdev {
|
||||
0
|
||||
} else if value > avg - stdev {
|
||||
1
|
||||
} else if value > avg - 2.0 * stdev {
|
||||
2
|
||||
} else {
|
||||
3
|
||||
};
|
||||
|
||||
if high_is_better { -base_score } else { base_score }
|
||||
}
|
||||
|
||||
/// Calculate weighted score for a single stat.
|
||||
pub fn calculate_weighted_score(
|
||||
value: f64,
|
||||
distribution: &StatDistribution,
|
||||
stat_weight: &StatWeight,
|
||||
) -> f64 {
|
||||
let std_score = standardize_value(value, distribution, stat_weight.high_is_better);
|
||||
std_score as f64 * stat_weight.weight as f64
|
||||
}
|
||||
3
rust/src/calc/mod.rs
Normal file
3
rust/src/calc/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod league_stats;
|
||||
pub mod matchup;
|
||||
pub mod weights;
|
||||
60
rust/src/calc/weights.rs
Normal file
60
rust/src/calc/weights.rs
Normal file
@ -0,0 +1,60 @@
|
||||
/// Weight and direction for a single stat in matchup scoring.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct StatWeight {
|
||||
pub weight: i32,
|
||||
pub high_is_better: bool,
|
||||
}
|
||||
|
||||
impl StatWeight {
|
||||
const fn new(weight: i32, high_is_better: bool) -> Self {
|
||||
Self {
|
||||
weight,
|
||||
high_is_better,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Batter stat weights for matchup calculation.
|
||||
/// Order: so, bb, hit, ob, tb, hr, bphr, bp1b, dp
|
||||
pub const BATTER_WEIGHTS: &[(&str, StatWeight)] = &[
|
||||
("so", StatWeight::new(1, false)), // Strikeouts - low is better
|
||||
("bb", StatWeight::new(1, true)), // Walks - high is better
|
||||
("hit", StatWeight::new(2, true)), // Hits - high is better
|
||||
("ob", StatWeight::new(5, true)), // On-base - high is better
|
||||
("tb", StatWeight::new(5, true)), // Total bases - high is better
|
||||
("hr", StatWeight::new(2, true)), // Home runs - high is better
|
||||
("bphr", StatWeight::new(3, true)), // Ballpark HR - high is better
|
||||
("bp1b", StatWeight::new(1, true)), // Ballpark 1B - high is better
|
||||
("dp", StatWeight::new(2, false)), // Double plays - low is better
|
||||
];
|
||||
|
||||
/// Pitcher stat weights for matchup calculation.
|
||||
/// Note: directions are inverted vs batter (pitcher wants low hits, high K's).
|
||||
pub const PITCHER_WEIGHTS: &[(&str, StatWeight)] = &[
|
||||
("so", StatWeight::new(3, true)), // Strikeouts - high is better for pitcher
|
||||
("bb", StatWeight::new(1, false)), // Walks - low is better for pitcher
|
||||
("hit", StatWeight::new(2, false)), // Hits - low is better for pitcher
|
||||
("ob", StatWeight::new(5, false)), // On-base - low is better for pitcher
|
||||
("tb", StatWeight::new(2, false)), // Total bases - low is better for pitcher
|
||||
("hr", StatWeight::new(5, false)), // Home runs - low is better for pitcher
|
||||
("bphr", StatWeight::new(2, false)), // Ballpark HR - low is better for pitcher
|
||||
("bp1b", StatWeight::new(1, false)), // Ballpark 1B - low is better for pitcher
|
||||
("dp", StatWeight::new(2, true)), // Double plays - high is better for pitcher
|
||||
];
|
||||
|
||||
/// Max possible batter component score: sum of (3 * weight) for all stats.
|
||||
pub const fn max_batter_score() -> i32 {
|
||||
// 3 * (1+1+2+5+5+2+3+1+2) = 3 * 22 = 66
|
||||
66
|
||||
}
|
||||
|
||||
/// Max possible pitcher component score: sum of (3 * weight) for all stats.
|
||||
pub const fn max_pitcher_score() -> i32 {
|
||||
// 3 * (3+1+2+5+2+5+2+1+2) = 3 * 23 = 69
|
||||
69
|
||||
}
|
||||
|
||||
/// Max possible combined matchup score.
|
||||
pub const fn max_matchup_score() -> i32 {
|
||||
max_batter_score() + max_pitcher_score() // 135
|
||||
}
|
||||
112
rust/src/config.rs
Normal file
112
rust/src/config.rs
Normal file
@ -0,0 +1,112 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use figment::{
|
||||
providers::{Env, Format, Toml},
|
||||
Figment,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Settings {
|
||||
pub db_path: PathBuf,
|
||||
pub api: ApiSettings,
|
||||
pub team: TeamSettings,
|
||||
pub ui: UiSettings,
|
||||
pub rating_weights: RatingWeights,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct ApiSettings {
|
||||
pub base_url: String,
|
||||
pub api_key: String,
|
||||
pub timeout: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct TeamSettings {
|
||||
pub abbrev: String,
|
||||
pub season: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct UiSettings {
|
||||
pub theme: String,
|
||||
pub refresh_interval: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct RatingWeights {
|
||||
pub hit: f64,
|
||||
pub on_base: f64,
|
||||
pub total_bases: f64,
|
||||
pub home_run: f64,
|
||||
pub walk: f64,
|
||||
pub strikeout: f64,
|
||||
pub double_play: f64,
|
||||
pub bp_home_run: f64,
|
||||
pub bp_single: f64,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
db_path: PathBuf::from("data/sba_scout.db"),
|
||||
api: ApiSettings::default(),
|
||||
team: TeamSettings::default(),
|
||||
ui: UiSettings::default(),
|
||||
rating_weights: RatingWeights::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ApiSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
base_url: "https://sba.manticorum.com/".to_string(),
|
||||
api_key: String::new(),
|
||||
timeout: 30,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TeamSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
abbrev: "WV".to_string(),
|
||||
season: 13,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for UiSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
theme: "dark".to_string(),
|
||||
refresh_interval: 300,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RatingWeights {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
hit: 1.0,
|
||||
on_base: 0.8,
|
||||
total_bases: 0.5,
|
||||
home_run: 1.5,
|
||||
walk: 0.6,
|
||||
strikeout: -0.3,
|
||||
double_play: -0.5,
|
||||
bp_home_run: 0.3,
|
||||
bp_single: 0.1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_settings() -> Result<Settings, figment::Error> {
|
||||
Figment::new()
|
||||
.merge(figment::providers::Serialized::defaults(Settings::default()))
|
||||
.merge(Toml::file("settings.toml"))
|
||||
.merge(Env::prefixed("SBA_SCOUT_").split("__"))
|
||||
.extract()
|
||||
}
|
||||
3
rust/src/db/mod.rs
Normal file
3
rust/src/db/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod models;
|
||||
pub mod queries;
|
||||
pub mod schema;
|
||||
253
rust/src/db/models.rs
Normal file
253
rust/src/db/models.rs
Normal file
@ -0,0 +1,253 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
|
||||
// =============================================================================
|
||||
// Core Entities (synced from league API)
|
||||
// =============================================================================
|
||||
|
||||
#[derive(Debug, FromRow, Serialize, Deserialize)]
|
||||
pub struct Team {
|
||||
pub id: i64,
|
||||
pub abbrev: String,
|
||||
pub short_name: String,
|
||||
pub long_name: String,
|
||||
pub season: i64,
|
||||
pub manager1_name: Option<String>,
|
||||
pub manager2_name: Option<String>,
|
||||
pub gm_discord_id: Option<String>,
|
||||
pub gm2_discord_id: Option<String>,
|
||||
pub division_id: Option<i64>,
|
||||
pub division_name: Option<String>,
|
||||
pub league_abbrev: Option<String>,
|
||||
pub thumbnail: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub dice_color: Option<String>,
|
||||
pub stadium: Option<String>,
|
||||
pub salary_cap: Option<f64>,
|
||||
pub synced_at: Option<NaiveDateTime>,
|
||||
}
|
||||
|
||||
#[derive(Debug, FromRow, Serialize, Deserialize)]
|
||||
pub struct Player {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub season: i64,
|
||||
pub team_id: Option<i64>,
|
||||
pub swar: Option<f64>,
|
||||
pub card_image: Option<String>,
|
||||
pub card_image_alt: Option<String>,
|
||||
pub headshot: Option<String>,
|
||||
pub vanity_card: Option<String>,
|
||||
pub pos_1: Option<String>,
|
||||
pub pos_2: Option<String>,
|
||||
pub pos_3: Option<String>,
|
||||
pub pos_4: Option<String>,
|
||||
pub pos_5: Option<String>,
|
||||
pub pos_6: Option<String>,
|
||||
pub pos_7: Option<String>,
|
||||
pub pos_8: Option<String>,
|
||||
pub hand: Option<String>,
|
||||
pub injury_rating: Option<String>,
|
||||
pub il_return: Option<String>,
|
||||
pub demotion_week: Option<i64>,
|
||||
pub strat_code: Option<String>,
|
||||
pub bbref_id: Option<String>,
|
||||
pub sbaplayer_id: Option<i64>,
|
||||
pub last_game: Option<String>,
|
||||
pub last_game2: Option<String>,
|
||||
pub synced_at: Option<NaiveDateTime>,
|
||||
}
|
||||
|
||||
impl Player {
|
||||
pub fn positions(&self) -> Vec<&str> {
|
||||
[
|
||||
self.pos_1.as_deref(),
|
||||
self.pos_2.as_deref(),
|
||||
self.pos_3.as_deref(),
|
||||
self.pos_4.as_deref(),
|
||||
self.pos_5.as_deref(),
|
||||
self.pos_6.as_deref(),
|
||||
self.pos_7.as_deref(),
|
||||
self.pos_8.as_deref(),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn is_pitcher(&self) -> bool {
|
||||
self.positions()
|
||||
.iter()
|
||||
.any(|p| matches!(*p, "SP" | "RP" | "CP"))
|
||||
}
|
||||
|
||||
pub fn is_batter(&self) -> bool {
|
||||
self.positions()
|
||||
.iter()
|
||||
.any(|p| matches!(*p, "C" | "1B" | "2B" | "3B" | "SS" | "LF" | "CF" | "RF" | "DH"))
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Card Data (imported from Strat-o-Matic)
|
||||
// =============================================================================
|
||||
|
||||
#[derive(Debug, FromRow, Serialize, Deserialize)]
|
||||
pub struct BatterCard {
|
||||
pub id: i64,
|
||||
pub player_id: i64,
|
||||
// vs Left-Handed Pitchers
|
||||
pub so_vlhp: f64,
|
||||
pub bb_vlhp: f64,
|
||||
pub hit_vlhp: f64,
|
||||
pub ob_vlhp: f64,
|
||||
pub tb_vlhp: f64,
|
||||
pub hr_vlhp: f64,
|
||||
pub dp_vlhp: f64,
|
||||
// vs Right-Handed Pitchers
|
||||
pub so_vrhp: f64,
|
||||
pub bb_vrhp: f64,
|
||||
pub hit_vrhp: f64,
|
||||
pub ob_vrhp: f64,
|
||||
pub tb_vrhp: f64,
|
||||
pub hr_vrhp: f64,
|
||||
pub dp_vrhp: f64,
|
||||
// Ballpark modifiers
|
||||
pub bphr_vlhp: f64,
|
||||
pub bphr_vrhp: f64,
|
||||
pub bp1b_vlhp: f64,
|
||||
pub bp1b_vrhp: f64,
|
||||
// Running game
|
||||
pub stealing: Option<String>,
|
||||
pub steal_rating: Option<String>,
|
||||
pub speed: Option<i64>,
|
||||
// Batting extras
|
||||
pub bunt: Option<String>,
|
||||
pub hit_run: Option<String>,
|
||||
// Fielding
|
||||
pub fielding: Option<String>,
|
||||
// Catcher-specific
|
||||
pub catcher_arm: Option<i64>,
|
||||
pub catcher_pb: Option<i64>,
|
||||
pub catcher_t: Option<i64>,
|
||||
// Computed ratings
|
||||
pub rating_vl: Option<f64>,
|
||||
pub rating_vr: Option<f64>,
|
||||
pub rating_overall: Option<f64>,
|
||||
// Import metadata
|
||||
pub imported_at: Option<NaiveDateTime>,
|
||||
pub source: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, FromRow, Serialize, Deserialize)]
|
||||
pub struct PitcherCard {
|
||||
pub id: i64,
|
||||
pub player_id: i64,
|
||||
// vs Left-Handed Batters
|
||||
pub so_vlhb: f64,
|
||||
pub bb_vlhb: f64,
|
||||
pub hit_vlhb: f64,
|
||||
pub ob_vlhb: f64,
|
||||
pub tb_vlhb: f64,
|
||||
pub hr_vlhb: f64,
|
||||
pub dp_vlhb: f64,
|
||||
pub bphr_vlhb: f64,
|
||||
pub bp1b_vlhb: f64,
|
||||
// vs Right-Handed Batters
|
||||
pub so_vrhb: f64,
|
||||
pub bb_vrhb: f64,
|
||||
pub hit_vrhb: f64,
|
||||
pub ob_vrhb: f64,
|
||||
pub tb_vrhb: f64,
|
||||
pub hr_vrhb: f64,
|
||||
pub dp_vrhb: f64,
|
||||
pub bphr_vrhb: f64,
|
||||
pub bp1b_vrhb: f64,
|
||||
// Pitcher attributes
|
||||
pub hold_rating: Option<i64>,
|
||||
pub endurance_start: Option<i64>,
|
||||
pub endurance_relief: Option<i64>,
|
||||
pub endurance_close: Option<i64>,
|
||||
pub fielding_range: Option<i64>,
|
||||
pub fielding_error: Option<i64>,
|
||||
pub wild_pitch: Option<i64>,
|
||||
pub balk: Option<i64>,
|
||||
pub batting_rating: Option<String>,
|
||||
// Computed ratings
|
||||
pub rating_vlhb: Option<f64>,
|
||||
pub rating_vrhb: Option<f64>,
|
||||
pub rating_overall: Option<f64>,
|
||||
// Import metadata
|
||||
pub imported_at: Option<NaiveDateTime>,
|
||||
pub source: Option<String>,
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Transactions (synced from league API)
|
||||
// =============================================================================
|
||||
|
||||
#[derive(Debug, FromRow, Serialize, Deserialize)]
|
||||
pub struct Transaction {
|
||||
pub id: i64,
|
||||
pub season: i64,
|
||||
pub week: i64,
|
||||
pub move_id: String,
|
||||
pub player_id: i64,
|
||||
pub from_team_id: i64,
|
||||
pub to_team_id: i64,
|
||||
pub cancelled: bool,
|
||||
pub frozen: bool,
|
||||
pub synced_at: Option<NaiveDateTime>,
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// User Data (local only)
|
||||
// =============================================================================
|
||||
|
||||
#[derive(Debug, FromRow, Serialize, Deserialize)]
|
||||
pub struct Lineup {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub lineup_type: String,
|
||||
pub batting_order: Option<String>, // JSON string
|
||||
pub positions: Option<String>, // JSON string
|
||||
pub starting_pitcher_id: Option<i64>,
|
||||
pub created_at: Option<NaiveDateTime>,
|
||||
pub updated_at: Option<NaiveDateTime>,
|
||||
}
|
||||
|
||||
#[derive(Debug, FromRow, Serialize, Deserialize)]
|
||||
pub struct MatchupCache {
|
||||
pub id: i64,
|
||||
pub batter_id: i64,
|
||||
pub pitcher_id: i64,
|
||||
pub rating: f64,
|
||||
pub tier: Option<String>,
|
||||
pub details: Option<String>, // JSON string
|
||||
pub computed_at: Option<NaiveDateTime>,
|
||||
pub weights_hash: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, FromRow, Serialize, Deserialize)]
|
||||
pub struct StandardizedScoreCache {
|
||||
pub id: i64,
|
||||
pub batter_card_id: Option<i64>,
|
||||
pub pitcher_card_id: Option<i64>,
|
||||
pub split: String,
|
||||
pub total_score: f64,
|
||||
pub stat_scores: String, // JSON string
|
||||
pub computed_at: Option<NaiveDateTime>,
|
||||
pub weights_hash: Option<String>,
|
||||
pub league_stats_hash: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, FromRow, Serialize, Deserialize)]
|
||||
pub struct SyncStatus {
|
||||
pub id: i64,
|
||||
pub entity_type: String,
|
||||
pub last_sync: Option<NaiveDateTime>,
|
||||
pub last_sync_count: Option<i64>,
|
||||
pub last_error: Option<String>,
|
||||
}
|
||||
2
rust/src/db/queries.rs
Normal file
2
rust/src/db/queries.rs
Normal file
@ -0,0 +1,2 @@
|
||||
// Database query functions will be implemented here.
|
||||
// Each function will use sqlx::query_as! to map results to model structs.
|
||||
18
rust/src/db/schema.rs
Normal file
18
rust/src/db/schema.rs
Normal file
@ -0,0 +1,18 @@
|
||||
use anyhow::Result;
|
||||
use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions};
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub async fn init_pool(db_path: &Path) -> Result<SqlitePool> {
|
||||
let db_url = format!("sqlite://{}?mode=rwc", db_path.display());
|
||||
let options = SqliteConnectOptions::from_str(&db_url)?
|
||||
.create_if_missing(true)
|
||||
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal);
|
||||
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect_with(options)
|
||||
.await?;
|
||||
|
||||
Ok(pool)
|
||||
}
|
||||
39
rust/src/main.rs
Normal file
39
rust/src/main.rs
Normal file
@ -0,0 +1,39 @@
|
||||
mod api;
|
||||
mod app;
|
||||
mod calc;
|
||||
mod config;
|
||||
mod db;
|
||||
mod screens;
|
||||
|
||||
use anyhow::Result;
|
||||
use crossterm::event::{self, Event, KeyCode};
|
||||
use ratatui::DefaultTerminal;
|
||||
|
||||
use app::App;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let mut terminal = ratatui::init();
|
||||
let result = run(&mut terminal).await;
|
||||
ratatui::restore();
|
||||
result
|
||||
}
|
||||
|
||||
async fn run(terminal: &mut DefaultTerminal) -> Result<()> {
|
||||
let mut app = App::new();
|
||||
|
||||
loop {
|
||||
terminal.draw(|frame| app.render(frame))?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.code == KeyCode::Char('q') {
|
||||
break;
|
||||
}
|
||||
app.handle_key(key);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
6
rust/src/screens/dashboard.rs
Normal file
6
rust/src/screens/dashboard.rs
Normal file
@ -0,0 +1,6 @@
|
||||
use ratatui::{layout::Rect, widgets::Paragraph, Frame};
|
||||
|
||||
pub fn render(frame: &mut Frame, area: Rect) {
|
||||
let widget = Paragraph::new("Dashboard");
|
||||
frame.render_widget(widget, area);
|
||||
}
|
||||
6
rust/src/screens/gameday.rs
Normal file
6
rust/src/screens/gameday.rs
Normal file
@ -0,0 +1,6 @@
|
||||
use ratatui::{layout::Rect, widgets::Paragraph, Frame};
|
||||
|
||||
pub fn render(frame: &mut Frame, area: Rect) {
|
||||
let widget = Paragraph::new("Gameday");
|
||||
frame.render_widget(widget, area);
|
||||
}
|
||||
6
rust/src/screens/lineup.rs
Normal file
6
rust/src/screens/lineup.rs
Normal file
@ -0,0 +1,6 @@
|
||||
use ratatui::{layout::Rect, widgets::Paragraph, Frame};
|
||||
|
||||
pub fn render(frame: &mut Frame, area: Rect) {
|
||||
let widget = Paragraph::new("Lineup");
|
||||
frame.render_widget(widget, area);
|
||||
}
|
||||
6
rust/src/screens/matchup.rs
Normal file
6
rust/src/screens/matchup.rs
Normal file
@ -0,0 +1,6 @@
|
||||
use ratatui::{layout::Rect, widgets::Paragraph, Frame};
|
||||
|
||||
pub fn render(frame: &mut Frame, area: Rect) {
|
||||
let widget = Paragraph::new("Matchup");
|
||||
frame.render_widget(widget, area);
|
||||
}
|
||||
6
rust/src/screens/mod.rs
Normal file
6
rust/src/screens/mod.rs
Normal file
@ -0,0 +1,6 @@
|
||||
pub mod dashboard;
|
||||
pub mod gameday;
|
||||
pub mod lineup;
|
||||
pub mod matchup;
|
||||
pub mod roster;
|
||||
pub mod settings;
|
||||
6
rust/src/screens/roster.rs
Normal file
6
rust/src/screens/roster.rs
Normal file
@ -0,0 +1,6 @@
|
||||
use ratatui::{layout::Rect, widgets::Paragraph, Frame};
|
||||
|
||||
pub fn render(frame: &mut Frame, area: Rect) {
|
||||
let widget = Paragraph::new("Roster");
|
||||
frame.render_widget(widget, area);
|
||||
}
|
||||
6
rust/src/screens/settings.rs
Normal file
6
rust/src/screens/settings.rs
Normal file
@ -0,0 +1,6 @@
|
||||
use ratatui::{layout::Rect, widgets::Paragraph, Frame};
|
||||
|
||||
pub fn render(frame: &mut Frame, area: Rect) {
|
||||
let widget = Paragraph::new("Settings");
|
||||
frame.render_widget(widget, area);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user