""" main.py ------- LottoSight entry point. Creates the main window with toolbar, content area, and status bar. Wires auto-fetch on launch (background thread) and 24hr APScheduler job. """ import logging import threading import tkinter as tk from tkinter import ttk from apscheduler.schedulers.background import BackgroundScheduler from db.database import init_db from core.fetcher import fetch_all from ui.statusbar import StatusBar from ui.history import HistoryScreen from ui.analysis import AnalysisScreen from ui.predictor_ui import PredictorScreen from ui.settings import SettingsScreen logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) logger = logging.getLogger(__name__) class LottoSightApp(tk.Tk): def __init__(self): super().__init__() self.title("LottoSight") self.minsize(900, 600) try: self.iconphoto(True, tk.PhotoImage(file="assets/icon.png")) except Exception: pass self._fetch_lock = threading.Lock() self._scheduler: BackgroundScheduler | None = None self._current_screen: tk.Widget | None = None self._screens: dict[str, tk.Widget] = {} self._build_ui() self._start_scheduler() self._navigate("History") # open History as default for now self._launch_fetch() # auto-fetch on startup # ── UI construction ─────────────────────────────────────────────────────── def _build_ui(self): # Toolbar toolbar = ttk.Frame(self, relief="raised", padding=(8, 4)) toolbar.pack(side="top", fill="x") ttk.Label( toolbar, text="LottoSight", font=("TkDefaultFont", 13, "bold") ).pack(side="left", padx=(0, 16)) for name in ("Dashboard", "History", "Analysis", "Predictor", "Settings"): ttk.Button( toolbar, text=name, width=10, command=lambda n=name: self._navigate(n), ).pack(side="left", padx=2) self._fetch_btn = ttk.Button( toolbar, text="Fetch Now", command=self._manual_fetch ) self._fetch_btn.pack(side="right", padx=4) # Content area — screens are packed/unpacked inside here self._content = ttk.Frame(self) self._content.pack(side="top", fill="both", expand=True) # Status bar self._statusbar = StatusBar(self) self._statusbar.pack(side="bottom", fill="x") # ── Navigation ──────────────────────────────────────────────────────────── def _navigate(self, screen_name: str): logger.info("Navigate → %s", screen_name) if self._current_screen is not None: self._current_screen.pack_forget() if screen_name not in self._screens: self._screens[screen_name] = self._make_screen(screen_name) self._current_screen = self._screens[screen_name] self._current_screen.pack(fill="both", expand=True) # Refresh data-bearing screens each time they're shown if hasattr(self._current_screen, "refresh"): self._current_screen.refresh() def _make_screen(self, name: str) -> tk.Widget: if name == "History": return HistoryScreen(self._content) if name == "Analysis": return AnalysisScreen(self._content) if name == "Predictor": return PredictorScreen(self._content) if name == "Settings": return SettingsScreen(self._content, on_fetch=self._manual_fetch) # Placeholder for screens added in later phases placeholder = ttk.Label( self._content, text=f"{name} — coming soon", font=("TkDefaultFont", 14), anchor="center", ) return placeholder # ── Fetch wiring ────────────────────────────────────────────────────────── def _start_scheduler(self): self._scheduler = BackgroundScheduler() self._scheduler.add_job( self._launch_fetch, "interval", hours=24, id="auto_fetch_24h" ) self._scheduler.start() logger.info("[FETCH] 24hr scheduler started") def _launch_fetch(self): """Start a background fetch thread (skips if one is already running).""" thread = threading.Thread( target=self._run_fetch, daemon=True, name="lottosight-fetch" ) thread.start() def _manual_fetch(self): self._launch_fetch() def _run_fetch(self): """Worker: fetch all sources, then update UI on the main thread.""" if not self._fetch_lock.acquire(blocking=False): logger.info("[FETCH] Fetch already in progress — skipping") return try: self.after(0, self._on_fetch_start) results = fetch_all() self.after(0, lambda r=results: self._on_fetch_done(r)) except Exception as e: logger.error("[ERROR] fetch_all raised unexpectedly: %s", e, exc_info=True) self.after(0, self._statusbar.set_ready) finally: self._fetch_lock.release() def _on_fetch_start(self): self._fetch_btn.config(state="disabled", text="Fetching…") self._statusbar.set_fetching() def _on_fetch_done(self, results: list): self._fetch_btn.config(state="normal", text="Fetch Now") self._statusbar.update_fetch_results(results) # Refresh the current screen if it can show new data if self._current_screen and hasattr(self._current_screen, "refresh"): self._current_screen.refresh() # ── Lifecycle ───────────────────────────────────────────────────────────── def on_close(self): if self._scheduler: self._scheduler.shutdown(wait=False) self.destroy() def main(): init_db() app = LottoSightApp() app.protocol("WM_DELETE_WINDOW", app.on_close) app.mainloop() if __name__ == "__main__": main()