05/23 Phase 9

This commit is contained in:
2026-05-23 12:11:14 -04:00
parent 524cdfda9a
commit 3ac30787d5
6 changed files with 342 additions and 4 deletions
+175
View File
@@ -0,0 +1,175 @@
"""
ui/dashboard.py
---------------
Dashboard — the home screen shown on app launch.
Sections:
• Last Draw Results — most-recent draw card per active game
• Database Summary — draw counts + prediction total
• Hot Numbers — top-5 most frequent numbers per game (last 100 draws)
"""
import tkinter as tk
from tkinter import ttk
from db.database import get_db_stats
from db.models import get_all_games, get_last_draw, get_draw_count, get_predictions
from core.analyzer import frequency_analysis
_HEADER_FONT = ("TkDefaultFont", 10, "bold")
_CARD_FONT = ("TkDefaultFont", 9)
_NUMBER_FONT = ("TkDefaultFont", 12, "bold")
_SECTION_PAD = (0, 20) # top, bottom
def _fmt_numbers(raw: str) -> str:
"""'1,13,36,61,69''01 13 36 61 69'"""
if not raw:
return ""
return " ".join(f"{int(n):02d}" for n in raw.split(","))
class DashboardScreen(ttk.Frame):
def __init__(self, parent, on_fetch=None, **kwargs):
super().__init__(parent, **kwargs)
self._on_fetch = on_fetch
self._build_ui()
# ── Layout ────────────────────────────────────────────────────────────────
def _build_ui(self):
# Scrollable canvas so the screen stays clean at small sizes
canvas = tk.Canvas(self, highlightthickness=0)
vsb = ttk.Scrollbar(self, orient="vertical", command=canvas.yview)
canvas.configure(yscrollcommand=vsb.set)
vsb.pack(side="right", fill="y")
canvas.pack(side="left", fill="both", expand=True)
inner = ttk.Frame(canvas, padding=(28, 20, 28, 20))
win = canvas.create_window((0, 0), window=inner, anchor="nw")
def _resize(event=None):
canvas.configure(scrollregion=canvas.bbox("all"))
canvas.itemconfig(win, width=canvas.winfo_width())
inner.bind("<Configure>", lambda e: _resize())
canvas.bind("<Configure>", lambda e: _resize())
canvas.bind("<Enter>",
lambda e: canvas.bind_all("<MouseWheel>",
lambda ev: canvas.yview_scroll(-1 * (ev.delta // 120), "units")))
canvas.bind("<Leave>",
lambda e: canvas.unbind_all("<MouseWheel>"))
self._inner = inner
self._draw_sections()
def _section_header(self, title: str):
f = ttk.Frame(self._inner)
f.pack(fill="x", pady=(0, 8))
ttk.Label(f, text=title, font=_HEADER_FONT).pack(anchor="w")
ttk.Separator(f, orient="horizontal").pack(fill="x", pady=(3, 0))
body = ttk.Frame(self._inner)
body.pack(fill="x", pady=_SECTION_PAD)
return body
def _draw_sections(self):
self._last_draw_body = self._section_header("Last Draw Results")
self._db_body = self._section_header("Database Summary")
self._hot_body = self._section_header("Hot Numbers (last 100 draws)")
# ── Refresh ───────────────────────────────────────────────────────────────
def refresh(self):
self._refresh_last_draws()
self._refresh_db_summary()
self._refresh_hot_numbers()
def _refresh_last_draws(self):
for w in self._last_draw_body.winfo_children():
w.destroy()
games = get_all_games(active_only=True)
if not games:
ttk.Label(self._last_draw_body, text="No active games.",
foreground="#aaaaaa").pack(anchor="w")
return
# Lay cards out in a horizontal row that wraps via grid
row_frame = ttk.Frame(self._last_draw_body)
row_frame.pack(fill="x")
for col_idx, game in enumerate(games):
draw = get_last_draw(game["id"])
card = ttk.LabelFrame(
row_frame, text=game["name"], padding=(14, 10, 14, 12)
)
card.grid(row=0, column=col_idx, padx=(0, 16), sticky="nw")
if draw is None:
ttk.Label(card, text="No draws yet.", foreground="#aaaaaa",
font=_CARD_FONT).pack(anchor="w")
else:
ttk.Label(card, text=draw["draw_date"],
foreground="#555555", font=_CARD_FONT).pack(anchor="w")
ttk.Label(card,
text=_fmt_numbers(draw["numbers"]),
font=_NUMBER_FONT, foreground="#1a1a2e").pack(anchor="w", pady=(6, 2))
if game["bonus_count"] > 0 and draw["bonus"]:
bonus_label = "Bonus" if game["name"] != "Powerball" else "Powerball"
ttk.Label(card, text=f"{bonus_label}: {draw['bonus']}",
foreground="#2471a3", font=_CARD_FONT).pack(anchor="w")
if draw["multiplier"]:
ttk.Label(card, text=f"Multiplier: {draw['multiplier']}",
foreground="#777777", font=_CARD_FONT).pack(anchor="w")
def _refresh_db_summary(self):
for w in self._db_body.winfo_children():
w.destroy()
stats = get_db_stats() # {game_name: count}
for name, count in sorted(stats.items()):
row = ttk.Frame(self._db_body)
row.pack(fill="x", pady=1)
ttk.Label(row, text=f"{name}:", width=18, anchor="w",
font=_CARD_FONT).pack(side="left")
ttk.Label(row, text=f"{count:,} draws",
foreground="#555555", font=_CARD_FONT).pack(side="left")
pred_total = len(get_predictions(limit=100_000))
row = ttk.Frame(self._db_body)
row.pack(fill="x", pady=(4, 0))
ttk.Label(row, text="Saved predictions:", width=18, anchor="w",
font=_CARD_FONT).pack(side="left")
ttk.Label(row, text=str(pred_total),
foreground="#555555", font=_CARD_FONT).pack(side="left")
def _refresh_hot_numbers(self):
for w in self._hot_body.winfo_children():
w.destroy()
games = get_all_games(active_only=True)
if not games:
ttk.Label(self._hot_body, text="No active games.",
foreground="#aaaaaa").pack(anchor="w")
return
for game in games:
freq = frequency_analysis(game["id"], last_n=100)
row = ttk.Frame(self._hot_body)
row.pack(fill="x", pady=3)
ttk.Label(row, text=f"{game['name']}:", width=18, anchor="w",
font=_CARD_FONT).pack(side="left")
if not freq:
ttk.Label(row, text="No data yet.", foreground="#aaaaaa",
font=_CARD_FONT).pack(side="left")
continue
top5 = sorted(freq, key=freq.get, reverse=True)[:5]
nums_str = " ".join(f"{n:02d}" for n in sorted(top5))
ttk.Label(row, text=nums_str, font=_CARD_FONT,
foreground="#1a5276").pack(side="left")
counts_str = " ".join(f"({freq[n]}×)" for n in sorted(top5))
ttk.Label(row, text=f" {counts_str}", foreground="#aaaaaa",
font=_CARD_FONT).pack(side="left")