""" 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 os 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 core.exporter import create_icon_png from core.paths import bundled_asset, user_data_dir 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=bundled_asset("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() icon_path = bundled_asset("assets/icon.png") if not os.path.exists(icon_path): os.makedirs(os.path.dirname(icon_path), exist_ok=True) create_icon_png(icon_path) app = LottoSightApp() app.protocol("WM_DELETE_WINDOW", app.on_close) app.mainloop() if __name__ == "__main__": main()