05/23 Phase 18,19
This commit is contained in:
+36
-1
@@ -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
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user