05/23 Phase 2
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user