147 lines
5.0 KiB
Python
147 lines
5.0 KiB
Python
"""
|
|
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
|
|
|
|
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._build_ui()
|
|
self._start_scheduler()
|
|
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 — replaced by real screens in later phases
|
|
self._content = ttk.Frame(self, padding=16)
|
|
self._content.pack(side="top", fill="both", expand=True)
|
|
|
|
self._content_label = ttk.Label(
|
|
self._content, text="Dashboard", font=("TkDefaultFont", 14)
|
|
)
|
|
self._content_label.pack(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)
|
|
self._content_label.config(text=screen_name)
|
|
|
|
# ── 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)
|
|
|
|
# ── 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()
|