05/23 Phase 18,19

This commit is contained in:
2026-05-23 16:31:55 -04:00
parent 7506e94d61
commit 2553406254
9 changed files with 674 additions and 5 deletions
+36 -1
View File
@@ -15,7 +15,7 @@ from datetime import date, timedelta
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
from core.analyzer import frequency_analysis, gap_analysis
from ui.widgets import BallsBar
# Weekday indices: Monday=0 … Sunday=6
@@ -97,6 +97,7 @@ class DashboardScreen(ttk.Frame):
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)")
self._overdue_body = self._section_header("Most Overdue Numbers")
# ── Refresh ───────────────────────────────────────────────────────────────
@@ -104,6 +105,7 @@ class DashboardScreen(ttk.Frame):
self._refresh_last_draws()
self._refresh_db_summary()
self._refresh_hot_numbers()
self._refresh_overdue()
def _refresh_last_draws(self):
for w in self._last_draw_body.winfo_children():
@@ -194,3 +196,36 @@ class DashboardScreen(ttk.Frame):
counts_str = " ".join(f"({freq[n]}×)" for n in top5)
ttk.Label(row, text=counts_str, foreground="#aaaaaa",
font=_CARD_FONT).pack(side="left")
def _refresh_overdue(self):
for w in self._overdue_body.winfo_children():
w.destroy()
games = get_all_games(active_only=True)
if not games:
ttk.Label(self._overdue_body, text="No active games.",
foreground="#aaaaaa").pack(anchor="w")
return
_ORANGE = ("#e67e22", "#ffffff")
for game in games:
gaps = gap_analysis(game["id"]) # {number: gap}
row = ttk.Frame(self._overdue_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 gaps:
ttk.Label(row, text="No data yet.", foreground="#aaaaaa",
font=_CARD_FONT).pack(side="left")
continue
top5 = sorted(sorted(gaps, key=gaps.get, reverse=True)[:5])
highlights = {n: _ORANGE for n in top5}
BallsBar(row, numbers=top5, radius=11,
highlights=highlights).pack(side="left", padx=(0, 6))
gaps_str = " ".join(f"({gaps[n]} ago)" for n in top5)
ttk.Label(row, text=gaps_str, foreground="#aaaaaa",
font=_CARD_FONT).pack(side="left")
+65 -3
View File
@@ -7,15 +7,16 @@ on_fetch: callable injected by main.py to trigger the shared fetch thread.
"""
import tkinter as tk
from tkinter import ttk, messagebox
from tkinter import ttk, messagebox, filedialog
import logging
from db.database import get_db_stats
from db.database import get_db_stats, backup_db, restore_db
from db.models import (
get_all_games, get_draw_count, set_game_active,
get_last_fetch_per_source, get_predictions,
add_game, delete_game, _BUILTIN_GAMES,
)
from core.importer import import_draws_csv
logger = logging.getLogger(__name__)
@@ -80,6 +81,7 @@ class SettingsScreen(ttk.Frame):
self._sources_body = self._section("Data Sources")
self._fetch_body = self._build_fetch_section()
self._db_body = self._section("Database")
self._build_db_actions()
def _build_fetch_section(self):
body = self._section("Fetch Schedule")
@@ -132,11 +134,16 @@ class SettingsScreen(ttk.Frame):
foreground="#777777",
).pack(side="left", padx=(16, 0))
ttk.Button(
row, text="Import CSV",
command=lambda gid=game["id"], gname=game["name"]: self._import_csv(gid, gname),
).pack(side="left", padx=(12, 0))
if game["name"] not in _BUILTIN_GAMES:
ttk.Button(
row, text="Delete",
command=lambda gid=game["id"], gname=game["name"]: self._delete_game(gid, gname),
).pack(side="left", padx=(12, 0))
).pack(side="left", padx=(6, 0))
ttk.Button(
self._games_body, text="+ Add Custom Game",
@@ -187,6 +194,13 @@ class SettingsScreen(ttk.Frame):
ttk.Label(row, text="Predictions:", width=18, anchor="w").pack(side="left")
ttk.Label(row, text=str(pred_count), foreground="#555555").pack(side="left")
def _build_db_actions(self):
body = self._section("Database Actions")
row = ttk.Frame(body)
row.pack(fill="x")
ttk.Button(row, text="Backup DB", command=self._backup_db ).pack(side="left", padx=(0, 8))
ttk.Button(row, text="Restore DB", command=self._restore_db).pack(side="left")
# ── Actions ───────────────────────────────────────────────────────────────
def _toggle_game(self, game_id: int, var: tk.BooleanVar):
@@ -203,6 +217,54 @@ class SettingsScreen(ttk.Frame):
else:
self._fetch_msg_var.set("Fetch not available.")
def _import_csv(self, game_id: int, game_name: str):
fp = filedialog.askopenfilename(
title=f"Import draws for {game_name}",
filetypes=[("CSV files", "*.csv"), ("All files", "*.*")],
)
if not fp:
return
result = import_draws_csv(game_id, fp)
added = result["added"]
skipped = result["skipped"]
errors = result["errors"]
msg = f"Added {added:,} draw{'s' if added != 1 else ''}."
if skipped:
msg += f"\nSkipped {skipped:,} duplicate{'s' if skipped != 1 else ''}."
if errors:
preview = "\n".join(errors[:5])
suffix = f"\n… and {len(errors) - 5} more." if len(errors) > 5 else ""
msg += f"\n\n{len(errors)} row error(s):\n{preview}{suffix}"
messagebox.showinfo("Import Complete", msg)
self._refresh_games()
def _backup_db(self):
try:
dest = backup_db()
messagebox.showinfo("Backup Complete", f"Database backed up to:\n{dest}")
except Exception as e:
messagebox.showerror("Backup Failed", str(e))
def _restore_db(self):
fp = filedialog.askopenfilename(
title="Select backup file to restore",
filetypes=[("SQLite DB", "*.db"), ("All files", "*.*")],
)
if not fp:
return
if not messagebox.askyesno(
"Confirm Restore",
"Restoring will overwrite the current database.\n"
"This cannot be undone. Continue?",
):
return
try:
restore_db(fp)
messagebox.showinfo("Restore Complete",
"Database restored. Please restart the app.")
except Exception as e:
messagebox.showerror("Restore Failed", str(e))
def _open_add_game_dialog(self):
_AddGameDialog(self, on_save=self._refresh_games)