Compare commits

..

3 Commits

Author SHA1 Message Date
Cal Corum
60b397b529 Fix sync errors: string move_id, FK constraints on connection pool
API returns move_id as string ("Season-013-Week-11-1772073335"), not
i64. Also disable foreign_keys at pool level via SqliteConnectOptions
since transactions reference players/teams that may not exist locally.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 10:50:27 -06:00
Cal Corum
a18c0431d1 Fix sync JSON parse error: Discord snowflake IDs are strings, not i64
The API returns gmid/gmid2 as quoted strings ("258104532423147520")
to avoid JavaScript precision loss. Changed types to Option<String>.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 08:07:03 -06:00
Cal Corum
bf7c3f870f Add file-based logging to avoid TUI corruption from stderr output
Errors were only visible in the truncated status bar notification.
Add tracing-appender to write to data/sba-scout.log with env-filter
support (RUST_LOG), and add tracing::error! calls alongside all
notification-only error paths for full diagnostic visibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 08:04:33 -06:00
8 changed files with 87 additions and 19 deletions

47
rust/Cargo.lock generated
View File

@ -270,6 +270,15 @@ version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-queue"
version = "0.3.12"
@ -1371,6 +1380,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "matchers"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
"regex-automata",
]
[[package]]
name = "md-5"
version = "0.10.6"
@ -2221,6 +2239,7 @@ dependencies = [
"tokio",
"toml",
"tracing",
"tracing-appender",
"tracing-subscriber",
]
@ -2903,12 +2922,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
dependencies = [
"deranged",
"itoa",
"libc",
"num-conv",
"num_threads",
"powerfmt",
"serde_core",
"time-core",
"time-macros",
]
[[package]]
@ -2917,6 +2938,16 @@ version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
[[package]]
name = "time-macros"
version = "0.2.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "tinystr"
version = "0.8.2"
@ -3112,6 +3143,18 @@ dependencies = [
"tracing-core",
]
[[package]]
name = "tracing-appender"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf"
dependencies = [
"crossbeam-channel",
"thiserror 2.0.18",
"time",
"tracing-subscriber",
]
[[package]]
name = "tracing-attributes"
version = "0.1.31"
@ -3150,10 +3193,14 @@ version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex-automata",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]

View File

@ -31,7 +31,8 @@ thiserror = "2"
# Logging
tracing = "0.1"
tracing-subscriber = "0.3"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-appender = "0.2"
# Date/time
chrono = { version = "0.4", features = ["serde"] }

View File

@ -22,8 +22,8 @@ pub async fn sync_teams(pool: &SqlitePool, season: i64, client: &LeagueApiClient
for data in response.teams {
let manager1_name = data.manager1.and_then(|m| m.name);
let manager2_name = data.manager2.and_then(|m| m.name);
let gm_discord_id = data.gm_discord_id.map(|id| id.to_string());
let gm2_discord_id = data.gm2_discord_id.map(|id| id.to_string());
let gm_discord_id = data.gm_discord_id;
let gm2_discord_id = data.gm2_discord_id;
let division_id = data.division.as_ref().map(|d| d.id).flatten();
let division_name = data.division.as_ref().and_then(|d| d.division_name.clone());
let league_abbrev = data.division.as_ref().and_then(|d| d.league_abbrev.clone());
@ -185,7 +185,6 @@ pub async fn sync_transactions(
continue;
};
let move_id_str = move_id.to_string();
let player_id = player.id;
// Schema has from_team_id/to_team_id as NOT NULL; use 0 as sentinel for missing teams
let from_team_id = data.oldteam.map(|t| t.id).unwrap_or(0);
@ -206,7 +205,7 @@ pub async fn sync_transactions(
)
.bind(season)
.bind(week)
.bind(move_id_str)
.bind(&move_id)
.bind(player_id)
.bind(from_team_id)
.bind(to_team_id)

View File

@ -59,12 +59,12 @@ pub struct TeamData {
pub stadium: Option<String>,
#[serde(default)]
pub salary_cap: Option<f64>,
/// Discord user ID of the primary GM (API sends integer, DB stores as String).
/// Discord user ID of the primary GM (API sends as string).
#[serde(rename = "gmid", default)]
pub gm_discord_id: Option<i64>,
/// Discord user ID of the secondary GM (API sends integer, DB stores as String).
pub gm_discord_id: Option<String>,
/// Discord user ID of the secondary GM (API sends as string).
#[serde(rename = "gmid2", default)]
pub gm2_discord_id: Option<i64>,
pub gm2_discord_id: Option<String>,
#[serde(default)]
pub manager1: Option<Manager>,
#[serde(default)]
@ -147,9 +147,9 @@ pub struct TransactionsResponse {
#[derive(Debug, Deserialize)]
pub struct TransactionData {
/// Transaction move ID — API field is "moveid".
/// Transaction move ID — API field is "moveid" (string like "Season-013-Week-11-1772073335").
#[serde(rename = "moveid", default)]
pub move_id: Option<i64>,
pub move_id: Option<String>,
#[serde(default)]
pub week: Option<i64>,
#[serde(default)]

View File

@ -7,7 +7,8 @@ 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);
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
.foreign_keys(false);
let pool = SqlitePoolOptions::new()
.max_connections(5)
@ -18,10 +19,6 @@ pub async fn init_pool(db_path: &Path) -> Result<SqlitePool> {
}
pub async fn create_tables(pool: &SqlitePool) -> Result<()> {
sqlx::query("PRAGMA foreign_keys = ON")
.execute(pool)
.await?;
// 1. teams — API-provided PKs (no autoincrement)
sqlx::query(
"CREATE TABLE IF NOT EXISTS teams (
@ -175,9 +172,9 @@ pub async fn create_tables(pool: &SqlitePool) -> Result<()> {
season INTEGER NOT NULL,
week INTEGER NOT NULL,
move_id TEXT NOT NULL,
player_id INTEGER NOT NULL REFERENCES players(id),
from_team_id INTEGER NOT NULL REFERENCES teams(id),
to_team_id INTEGER NOT NULL REFERENCES teams(id),
player_id INTEGER NOT NULL,
from_team_id INTEGER NOT NULL,
to_team_id INTEGER NOT NULL,
cancelled INTEGER DEFAULT 0,
frozen INTEGER DEFAULT 0,
synced_at TEXT,

View File

@ -5,11 +5,27 @@ use ratatui::DefaultTerminal;
use sqlx::sqlite::SqlitePool;
use tokio::sync::mpsc;
use tokio::time::{interval, Duration};
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::EnvFilter;
use sba_scout::app::{App, AppMessage};
use sba_scout::config;
use sba_scout::db;
fn init_logging(log_dir: &std::path::Path) -> WorkerGuard {
std::fs::create_dir_all(log_dir).ok();
let file_appender = tracing_appender::rolling::never(log_dir, "sba-scout.log");
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
tracing_subscriber::fmt()
.with_writer(non_blocking)
.with_env_filter(
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
)
.with_ansi(false)
.init();
guard
}
#[tokio::main]
async fn main() -> Result<()> {
let settings = match config::load_settings() {
@ -20,6 +36,9 @@ async fn main() -> Result<()> {
}
};
let log_dir = settings.db_path.parent().unwrap_or(std::path::Path::new("data"));
let _log_guard = init_logging(log_dir);
if let Some(parent) = settings.db_path.parent() {
std::fs::create_dir_all(parent)?;
}

View File

@ -56,6 +56,7 @@ impl DashboardState {
let _ = tx.send(AppMessage::RosterLoaded(roster));
}
Err(e) => {
tracing::error!("Failed to load roster: {e}");
let _ = tx.send(AppMessage::Notify(
format!("Failed to load roster: {e}"),
NotifyLevel::Error,
@ -119,6 +120,7 @@ impl DashboardState {
);
}
Err(e) => {
tracing::error!("Sync failed: {e}");
self.sync_state = SyncState::Error;
self.sync_message = format!("Sync failed: {e}");
}

View File

@ -157,6 +157,7 @@ impl GamedayState {
let _ = tx_c.send(AppMessage::TeamsLoaded(teams));
}
Err(e) => {
tracing::error!("Failed to load teams: {e}");
let _ = tx_c.send(AppMessage::Notify(
format!("Failed to load teams: {e}"),
NotifyLevel::Error,
@ -620,6 +621,7 @@ impl GamedayState {
let _ = tx.send(AppMessage::PitchersLoaded(pitchers));
}
Err(e) => {
tracing::error!("Failed to load pitchers: {e}");
let _ = tx.send(AppMessage::Notify(
format!("Failed to load pitchers: {e}"),
NotifyLevel::Error,
@ -661,6 +663,7 @@ impl GamedayState {
let _ = tx.send(AppMessage::MatchupsCalculated(results));
}
Err(e) => {
tracing::error!("Matchup calculation failed: {e}");
let _ = tx.send(AppMessage::Notify(
format!("Matchup calculation failed: {e}"),
NotifyLevel::Error,