05/23 Phase 22
This commit is contained in:
@@ -19,7 +19,9 @@
|
||||
"Bash(python -m pytest --tb=short -q)",
|
||||
"Bash(python -m pytest tests/test_wheeling.py -v)",
|
||||
"WebFetch(domain:www.valottery.com)",
|
||||
"Bash(pip install *)"
|
||||
"Bash(pip install *)",
|
||||
"Bash(python -m pytest tests/ -x -q)",
|
||||
"Bash(python -m pytest tests/test_phase22.py -v)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,6 +376,33 @@ All actions are logged to console and optionally to a log file:
|
||||
|
||||
---
|
||||
|
||||
### ✅ Phase 22 — Incremental VA Fetch + Top Prize + Dashboard Filter + Auto-Check
|
||||
- [x] Incremental VA fetch in `core/fetcher.py`
|
||||
- [x] `_fetch_va_lottery()` calls `get_last_draw()` for last known date
|
||||
- [x] Breaks out of parsing loop when `draw_date <= last_date` (data is newest-first)
|
||||
- [x] Result: subsequent fetches only download new records, not all 10K+ rows
|
||||
- [x] `top_prize` column in `db/database.py`
|
||||
- [x] Added `top_prize TEXT NOT NULL DEFAULT ''` to `CREATE TABLE games`
|
||||
- [x] ALTER TABLE migration for existing databases (checks via `PRAGMA table_info`)
|
||||
- [x] Updated `_seed_games()` with prize strings for all 5 builtin games
|
||||
- [x] Back-fills `top_prize` for existing rows where empty (safe to re-run)
|
||||
- [x] Dashboard game filter in `ui/dashboard.py`
|
||||
- [x] `_filter_var` combobox at top ("All Games" + active game names)
|
||||
- [x] `_update_filter_options()` — keeps combobox in sync with active games
|
||||
- [x] `_filtered_games()` — returns subset based on selection
|
||||
- [x] Last Draws, Hot Numbers, Overdue sections all respect filter
|
||||
- [x] Top prize display in last-draw cards (`ui/dashboard.py`)
|
||||
- [x] Shows "Top prize: $X" in purple above draw date if `top_prize` is set
|
||||
- [x] VA source names added to `ui/statusbar.py` `_SOURCE_NAMES`
|
||||
- [x] `append_match_alert(msg)` method added to `StatusBar`
|
||||
- [x] Auto-check predictions after fetch in `main.py`
|
||||
- [x] `_check_predictions_vs_latest()` — compares all saved predictions against newest draw per game
|
||||
- [x] Called from `_on_fetch_done()` when `total_added > 0`
|
||||
- [x] Status bar shows alert when ≥1 prediction matches ≥2 main numbers or bonus
|
||||
- [x] `tests/test_phase22.py` — 14 tests (403/403 total passing)
|
||||
|
||||
---
|
||||
|
||||
### ✅ Phase 19 — Dashboard Overdue Alert + DB Backup
|
||||
- [x] Add `backup_db(dest_dir=None) -> str` to `db/database.py` — copies live DB to timestamped file, creates dest dir if needed
|
||||
- [x] Add `restore_db(source_path: str) -> None` to `db/database.py` — overwrites live DB with backup file, raises FileNotFoundError if missing
|
||||
|
||||
+9
-1
@@ -14,7 +14,7 @@ import threading
|
||||
import requests
|
||||
from datetime import datetime
|
||||
|
||||
from db.models import get_game_by_name, insert_draw, insert_fetch_log
|
||||
from db.models import get_game_by_name, get_last_draw, insert_draw, insert_fetch_log
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -262,6 +262,10 @@ def _fetch_va_lottery(game_name: str, source: str, url: str) -> dict:
|
||||
game_id = game["id"]
|
||||
added = skipped = 0
|
||||
|
||||
# Incremental fetch: VA data is newest-first; stop once we reach known dates
|
||||
last_draw = get_last_draw(game_id)
|
||||
last_date = last_draw["draw_date"] if last_draw else None
|
||||
|
||||
try:
|
||||
resp = requests.get(url, timeout=30)
|
||||
resp.raise_for_status()
|
||||
@@ -279,6 +283,10 @@ def _fetch_va_lottery(game_name: str, source: str, url: str) -> dict:
|
||||
if draw_date is None:
|
||||
continue
|
||||
|
||||
# Data comes newest-first; stop when we reach already-stored dates
|
||||
if last_date and draw_date <= last_date:
|
||||
break
|
||||
|
||||
try:
|
||||
p1 = parts[1].strip()
|
||||
p1_lower = p1.lower()
|
||||
|
||||
Binary file not shown.
+25
-9
@@ -50,7 +50,8 @@ def init_db():
|
||||
main_max INTEGER NOT NULL,
|
||||
bonus_count INTEGER NOT NULL DEFAULT 0,
|
||||
bonus_max INTEGER NOT NULL DEFAULT 0,
|
||||
active INTEGER NOT NULL DEFAULT 1
|
||||
active INTEGER NOT NULL DEFAULT 1,
|
||||
top_prize TEXT NOT NULL DEFAULT ''
|
||||
)
|
||||
""")
|
||||
|
||||
@@ -105,6 +106,15 @@ def init_db():
|
||||
)
|
||||
""")
|
||||
|
||||
# Migration: add top_prize column to existing databases
|
||||
cursor.execute("PRAGMA table_info(games)")
|
||||
cols = {r["name"] for r in cursor.fetchall()}
|
||||
if "top_prize" not in cols:
|
||||
cursor.execute(
|
||||
"ALTER TABLE games ADD COLUMN top_prize TEXT NOT NULL DEFAULT ''"
|
||||
)
|
||||
logger.info("[DB] Migrated: added top_prize column to games")
|
||||
|
||||
conn.commit()
|
||||
logger.info("[DB] Tables created/verified OK")
|
||||
|
||||
@@ -122,22 +132,28 @@ def _seed_games(cursor, conn):
|
||||
"""
|
||||
Insert default game configs if they don't already exist.
|
||||
Uses INSERT OR IGNORE to be idempotent on every launch.
|
||||
Also back-fills top_prize for any existing rows where it is empty.
|
||||
"""
|
||||
defaults = [
|
||||
# name main_count main_max bonus_count bonus_max active
|
||||
("Powerball", 5, 69, 1, 26, 1),
|
||||
("Mega Millions", 5, 70, 1, 25, 1),
|
||||
("Cash 5", 5, 45, 0, 0, 1),
|
||||
("Millionaire for Life", 5, 58, 1, 5, 1),
|
||||
("Bank a Million", 6, 40, 1, 40, 1),
|
||||
# name mc mm bc bm active top_prize
|
||||
("Powerball", 5, 69, 1, 26, 1, "Jackpot (variable)"),
|
||||
("Mega Millions", 5, 70, 1, 25, 1, "Jackpot (variable)"),
|
||||
("Cash 5", 5, 45, 0, 0, 1, "Jackpot from $200K"),
|
||||
("Millionaire for Life", 5, 58, 1, 5, 1, "$1M/yr for life"),
|
||||
("Bank a Million", 6, 40, 1, 40, 1, "$1M after taxes"),
|
||||
]
|
||||
|
||||
for row in defaults:
|
||||
cursor.execute("""
|
||||
INSERT OR IGNORE INTO games
|
||||
(name, main_count, main_max, bonus_count, bonus_max, active)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
(name, main_count, main_max, bonus_count, bonus_max, active, top_prize)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""", row)
|
||||
# Back-fill top_prize for rows inserted before this column existed
|
||||
cursor.execute(
|
||||
"UPDATE games SET top_prize = ? WHERE name = ? AND top_prize = ''",
|
||||
(row[6], row[0]),
|
||||
)
|
||||
|
||||
inserted = conn.total_changes
|
||||
conn.commit()
|
||||
|
||||
@@ -167,10 +167,42 @@ class LottoSightApp(tk.Tk):
|
||||
def _on_fetch_done(self, results: list):
|
||||
self._fetch_btn.config(state="normal", text="Fetch Now")
|
||||
self._statusbar.update_fetch_results(results)
|
||||
total_added = sum(r.get("added", 0) for r in results)
|
||||
if total_added > 0:
|
||||
self._check_predictions_vs_latest()
|
||||
# Refresh the current screen if it can show new data
|
||||
if self._current_screen and hasattr(self._current_screen, "refresh"):
|
||||
self._current_screen.refresh()
|
||||
|
||||
def _check_predictions_vs_latest(self):
|
||||
"""Compare all saved predictions against the most recent draw per game."""
|
||||
from db.models import get_predictions, get_last_draw
|
||||
preds = get_predictions(limit=100_000)
|
||||
if not preds:
|
||||
return
|
||||
|
||||
best = 0
|
||||
hit_count = 0
|
||||
for pred in preds:
|
||||
draw = get_last_draw(pred["game_id"])
|
||||
if not draw:
|
||||
continue
|
||||
pred_nums = {int(n) for n in pred["numbers"].split(",") if n.strip().isdigit()}
|
||||
draw_nums = {int(n) for n in draw["numbers"].split(",") if n.strip().isdigit()}
|
||||
matches = len(pred_nums & draw_nums)
|
||||
bonus_hit = (
|
||||
pred.get("bonus") and draw.get("bonus")
|
||||
and str(pred["bonus"]) == str(draw["bonus"])
|
||||
)
|
||||
if matches >= 2 or bonus_hit:
|
||||
hit_count += 1
|
||||
best = max(best, matches)
|
||||
|
||||
if hit_count > 0:
|
||||
self._statusbar.append_match_alert(
|
||||
f"{hit_count} saved prediction(s) matched ≥2 numbers (best: {best}/main)"
|
||||
)
|
||||
|
||||
# ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
|
||||
def on_close(self):
|
||||
|
||||
@@ -254,7 +254,8 @@ def test_cash5_va_skips_duplicates(tmp_db):
|
||||
with patch("core.fetcher.requests.get", return_value=_text_resp(CASH5_VA_TEXT)):
|
||||
result = fetch_cash5_va()
|
||||
assert result["added"] == 0
|
||||
assert result["skipped"] == 2
|
||||
# Incremental fetch breaks early once last known date is reached — skipped stays 0
|
||||
assert result["skipped"] == 0
|
||||
|
||||
|
||||
def test_cash5_va_parses_date_and_numbers(tmp_db):
|
||||
@@ -331,7 +332,8 @@ def test_bankamil_va_skips_duplicates(tmp_db):
|
||||
with patch("core.fetcher.requests.get", return_value=_text_resp(BANKAMIL_VA_TEXT)):
|
||||
result = fetch_bankamillion_va()
|
||||
assert result["added"] == 0
|
||||
assert result["skipped"] == 2
|
||||
# Incremental fetch breaks early once last known date is reached — skipped stays 0
|
||||
assert result["skipped"] == 0
|
||||
|
||||
|
||||
def test_bankamil_va_network_error(tmp_db):
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
"""
|
||||
tests/test_phase22.py
|
||||
----------------------
|
||||
Tests for Phase 22 features:
|
||||
- Incremental VA fetch (break on already-stored dates)
|
||||
- top_prize column in games table
|
||||
- Dashboard game filter
|
||||
- Auto-check predictions after fetch (match detection logic)
|
||||
"""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from db.database import get_db_stats, init_db
|
||||
from db.models import (
|
||||
get_all_games,
|
||||
get_game_by_name,
|
||||
get_last_draw,
|
||||
insert_draw,
|
||||
insert_prediction,
|
||||
get_predictions,
|
||||
)
|
||||
from core.fetcher import fetch_cash5_va, fetch_bankamillion_va
|
||||
|
||||
|
||||
# ── Helpers ────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _text_resp(text, status=200):
|
||||
m = MagicMock()
|
||||
m.status_code = status
|
||||
m.raise_for_status = MagicMock()
|
||||
m.text = text
|
||||
return m
|
||||
|
||||
|
||||
CASH5_VA_TEXT_OLD = (
|
||||
"5/22/2026; 15,29,30,34,36\n"
|
||||
"5/21/2026; 1,2,5,38,44\n"
|
||||
)
|
||||
|
||||
CASH5_VA_TEXT_NEWER = (
|
||||
"5/23/2026; 10,11,12,13,14\n" # new record
|
||||
"5/22/2026; 15,29,30,34,36\n" # already in DB
|
||||
"5/21/2026; 1,2,5,38,44\n" # already in DB
|
||||
)
|
||||
|
||||
|
||||
# ── top_prize column ───────────────────────────────────────────────────────────
|
||||
|
||||
def test_top_prize_seeded_powerball(tmp_db):
|
||||
row = get_game_by_name("Powerball")
|
||||
assert row["top_prize"] == "Jackpot (variable)"
|
||||
|
||||
|
||||
def test_top_prize_seeded_mega_millions(tmp_db):
|
||||
row = get_game_by_name("Mega Millions")
|
||||
assert row["top_prize"] == "Jackpot (variable)"
|
||||
|
||||
|
||||
def test_top_prize_seeded_cash5(tmp_db):
|
||||
row = get_game_by_name("Cash 5")
|
||||
assert row["top_prize"] == "Jackpot from $200K"
|
||||
|
||||
|
||||
def test_top_prize_seeded_millionaire_for_life(tmp_db):
|
||||
row = get_game_by_name("Millionaire for Life")
|
||||
assert row["top_prize"] == "$1M/yr for life"
|
||||
|
||||
|
||||
def test_top_prize_seeded_bank_a_million(tmp_db):
|
||||
row = get_game_by_name("Bank a Million")
|
||||
assert row["top_prize"] == "$1M after taxes"
|
||||
|
||||
|
||||
def test_top_prize_all_games_non_empty(tmp_db):
|
||||
games = get_all_games()
|
||||
for g in games:
|
||||
# Builtin games must have a top_prize; custom games may be empty
|
||||
assert "top_prize" in g.keys()
|
||||
|
||||
|
||||
# ── Incremental VA fetch ───────────────────────────────────────────────────────
|
||||
|
||||
def test_incremental_fetch_only_new_records_added(tmp_db):
|
||||
"""Second fetch with a new leading record only inserts that one record."""
|
||||
with patch("core.fetcher.requests.get", return_value=_text_resp(CASH5_VA_TEXT_OLD)):
|
||||
r1 = fetch_cash5_va()
|
||||
assert r1["added"] == 2
|
||||
|
||||
with patch("core.fetcher.requests.get", return_value=_text_resp(CASH5_VA_TEXT_NEWER)):
|
||||
r2 = fetch_cash5_va()
|
||||
assert r2["added"] == 1
|
||||
assert r2["skipped"] == 0 # stopped before re-attempting stored dates
|
||||
|
||||
|
||||
def test_incremental_fetch_no_new_data(tmp_db):
|
||||
"""Second fetch of identical data adds nothing and doesn't error."""
|
||||
with patch("core.fetcher.requests.get", return_value=_text_resp(CASH5_VA_TEXT_OLD)):
|
||||
fetch_cash5_va()
|
||||
with patch("core.fetcher.requests.get", return_value=_text_resp(CASH5_VA_TEXT_OLD)):
|
||||
r = fetch_cash5_va()
|
||||
assert r["status"] == "success"
|
||||
assert r["added"] == 0
|
||||
|
||||
|
||||
def test_incremental_fetch_db_count_correct(tmp_db):
|
||||
"""Total draws in DB after incremental fetch equals unique dates only."""
|
||||
with patch("core.fetcher.requests.get", return_value=_text_resp(CASH5_VA_TEXT_OLD)):
|
||||
fetch_cash5_va()
|
||||
with patch("core.fetcher.requests.get", return_value=_text_resp(CASH5_VA_TEXT_NEWER)):
|
||||
fetch_cash5_va()
|
||||
|
||||
game = get_game_by_name("Cash 5")
|
||||
stats = get_db_stats()
|
||||
assert stats["Cash 5"] == 3 # 2026-05-21, 05-22, 05-23
|
||||
|
||||
|
||||
def test_incremental_fetch_bank_a_million(tmp_db):
|
||||
"""Bank a Million incremental fetch behaves the same as Cash 5."""
|
||||
bam_old = (
|
||||
"Results for Bank a Million\n"
|
||||
"5/20/2026; 14,20,21,24,33,35; Bonus Ball: 18\n"
|
||||
"5/16/2026; 6,8,17,20,29,39; Bonus Ball: 38\n"
|
||||
)
|
||||
bam_newer = (
|
||||
"Results for Bank a Million\n"
|
||||
"5/23/2026; 1,5,10,20,35,38; Bonus Ball: 7\n"
|
||||
"5/20/2026; 14,20,21,24,33,35; Bonus Ball: 18\n"
|
||||
"5/16/2026; 6,8,17,20,29,39; Bonus Ball: 38\n"
|
||||
)
|
||||
with patch("core.fetcher.requests.get", return_value=_text_resp(bam_old)):
|
||||
r1 = fetch_bankamillion_va()
|
||||
assert r1["added"] == 2
|
||||
|
||||
with patch("core.fetcher.requests.get", return_value=_text_resp(bam_newer)):
|
||||
r2 = fetch_bankamillion_va()
|
||||
assert r2["added"] == 1
|
||||
assert r2["skipped"] == 0
|
||||
|
||||
|
||||
# ── Match detection logic (unit-level) ────────────────────────────────────────
|
||||
|
||||
def _count_matches(pred_numbers: str, draw_numbers: str) -> int:
|
||||
pred = {int(n) for n in pred_numbers.split(",") if n.strip().isdigit()}
|
||||
draw = {int(n) for n in draw_numbers.split(",") if n.strip().isdigit()}
|
||||
return len(pred & draw)
|
||||
|
||||
|
||||
def test_match_detection_exact(tmp_db):
|
||||
pb = get_game_by_name("Powerball")
|
||||
insert_draw(pb["id"], "2026-05-22", [1, 13, 36, 61, 69], bonus=7)
|
||||
last = get_last_draw(pb["id"])
|
||||
|
||||
pred_id = insert_prediction(pb["id"], "Hot Numbers", [1, 13, 36, 61, 69], bonus=7)
|
||||
preds = get_predictions(game_id=pb["id"])
|
||||
pred = preds[0]
|
||||
|
||||
matches = _count_matches(pred["numbers"], last["numbers"])
|
||||
assert matches == 5
|
||||
|
||||
|
||||
def test_match_detection_partial(tmp_db):
|
||||
pb = get_game_by_name("Powerball")
|
||||
insert_draw(pb["id"], "2026-05-22", [1, 13, 36, 61, 69], bonus=7)
|
||||
last = get_last_draw(pb["id"])
|
||||
|
||||
insert_prediction(pb["id"], "Hot Numbers", [1, 13, 5, 6, 7], bonus=99)
|
||||
preds = get_predictions(game_id=pb["id"])
|
||||
|
||||
matches = _count_matches(preds[0]["numbers"], last["numbers"])
|
||||
assert matches == 2
|
||||
|
||||
|
||||
def test_match_detection_no_match(tmp_db):
|
||||
pb = get_game_by_name("Powerball")
|
||||
insert_draw(pb["id"], "2026-05-22", [1, 13, 36, 61, 69], bonus=7)
|
||||
last = get_last_draw(pb["id"])
|
||||
|
||||
insert_prediction(pb["id"], "Due Numbers", [2, 4, 6, 8, 10], bonus=99)
|
||||
preds = get_predictions(game_id=pb["id"])
|
||||
|
||||
matches = _count_matches(preds[0]["numbers"], last["numbers"])
|
||||
assert matches == 0
|
||||
|
||||
|
||||
def test_match_alert_threshold_is_two(tmp_db):
|
||||
"""Only predictions with ≥2 main matches (or bonus hit) count toward alert."""
|
||||
pb = get_game_by_name("Powerball")
|
||||
insert_draw(pb["id"], "2026-05-22", [1, 13, 36, 61, 69], bonus=7)
|
||||
last = get_last_draw(pb["id"])
|
||||
|
||||
preds_data = [
|
||||
([1, 2, 3, 4, 5], 99), # 1 match — below threshold
|
||||
([1, 13, 2, 3, 4], 99), # 2 matches — at threshold
|
||||
([1, 13, 36, 2, 3], 99), # 3 matches — above threshold
|
||||
]
|
||||
for nums, bonus in preds_data:
|
||||
insert_prediction(pb["id"], "Test", nums, bonus=bonus)
|
||||
|
||||
preds = get_predictions(game_id=pb["id"])
|
||||
draw_nums = {int(n) for n in last["numbers"].split(",") if n.strip().isdigit()}
|
||||
|
||||
qualifying = [
|
||||
p for p in preds
|
||||
if len({int(n) for n in p["numbers"].split(",") if n.strip().isdigit()} & draw_nums) >= 2
|
||||
]
|
||||
assert len(qualifying) == 2
|
||||
+37
-5
@@ -54,6 +54,7 @@ class DashboardScreen(ttk.Frame):
|
||||
def __init__(self, parent, on_fetch=None, **kwargs):
|
||||
super().__init__(parent, **kwargs)
|
||||
self._on_fetch = on_fetch
|
||||
self._filter_var = tk.StringVar(value="All Games")
|
||||
self._build_ui()
|
||||
|
||||
# ── Layout ────────────────────────────────────────────────────────────────
|
||||
@@ -66,7 +67,7 @@ class DashboardScreen(ttk.Frame):
|
||||
vsb.pack(side="right", fill="y")
|
||||
canvas.pack(side="left", fill="both", expand=True)
|
||||
|
||||
inner = ttk.Frame(canvas, padding=(28, 20, 28, 20))
|
||||
inner = ttk.Frame(canvas, padding=(28, 16, 28, 20))
|
||||
win = canvas.create_window((0, 0), window=inner, anchor="nw")
|
||||
|
||||
def _resize(event=None):
|
||||
@@ -82,6 +83,18 @@ class DashboardScreen(ttk.Frame):
|
||||
lambda e: canvas.unbind_all("<MouseWheel>"))
|
||||
|
||||
self._inner = inner
|
||||
|
||||
# Game filter bar
|
||||
filter_bar = ttk.Frame(inner)
|
||||
filter_bar.pack(fill="x", pady=(0, 12))
|
||||
ttk.Label(filter_bar, text="Show game:").pack(side="left", padx=(0, 6))
|
||||
self._game_filter = ttk.Combobox(
|
||||
filter_bar, textvariable=self._filter_var,
|
||||
state="readonly", width=22,
|
||||
)
|
||||
self._game_filter.pack(side="left")
|
||||
self._game_filter.bind("<<ComboboxSelected>>", lambda _e: self.refresh())
|
||||
|
||||
self._draw_sections()
|
||||
|
||||
def _section_header(self, title: str):
|
||||
@@ -101,7 +114,22 @@ class DashboardScreen(ttk.Frame):
|
||||
|
||||
# ── Refresh ───────────────────────────────────────────────────────────────
|
||||
|
||||
def _update_filter_options(self):
|
||||
games = get_all_games(active_only=True)
|
||||
names = ["All Games"] + [g["name"] for g in games]
|
||||
self._game_filter["values"] = names
|
||||
if self._filter_var.get() not in names:
|
||||
self._filter_var.set("All Games")
|
||||
|
||||
def _filtered_games(self):
|
||||
games = get_all_games(active_only=True)
|
||||
sel = self._filter_var.get()
|
||||
if not sel or sel == "All Games":
|
||||
return games
|
||||
return [g for g in games if g["name"] == sel]
|
||||
|
||||
def refresh(self):
|
||||
self._update_filter_options()
|
||||
self._refresh_last_draws()
|
||||
self._refresh_db_summary()
|
||||
self._refresh_hot_numbers()
|
||||
@@ -111,13 +139,12 @@ class DashboardScreen(ttk.Frame):
|
||||
for w in self._last_draw_body.winfo_children():
|
||||
w.destroy()
|
||||
|
||||
games = get_all_games(active_only=True)
|
||||
games = self._filtered_games()
|
||||
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")
|
||||
|
||||
@@ -128,6 +155,11 @@ class DashboardScreen(ttk.Frame):
|
||||
)
|
||||
card.grid(row=0, column=col_idx, padx=(0, 16), sticky="nw")
|
||||
|
||||
top_prize = game["top_prize"] if "top_prize" in game.keys() else ""
|
||||
if top_prize:
|
||||
ttk.Label(card, text=f"Top prize: {top_prize}",
|
||||
foreground="#8e44ad", font=_CARD_FONT).pack(anchor="w")
|
||||
|
||||
if draw is None:
|
||||
ttk.Label(card, text="No draws yet.", foreground="#aaaaaa",
|
||||
font=_CARD_FONT).pack(anchor="w")
|
||||
@@ -172,7 +204,7 @@ class DashboardScreen(ttk.Frame):
|
||||
for w in self._hot_body.winfo_children():
|
||||
w.destroy()
|
||||
|
||||
games = get_all_games(active_only=True)
|
||||
games = self._filtered_games()
|
||||
if not games:
|
||||
ttk.Label(self._hot_body, text="No active games.",
|
||||
foreground="#aaaaaa").pack(anchor="w")
|
||||
@@ -201,7 +233,7 @@ class DashboardScreen(ttk.Frame):
|
||||
for w in self._overdue_body.winfo_children():
|
||||
w.destroy()
|
||||
|
||||
games = get_all_games(active_only=True)
|
||||
games = self._filtered_games()
|
||||
if not games:
|
||||
ttk.Label(self._overdue_body, text="No active games.",
|
||||
foreground="#aaaaaa").pack(anchor="w")
|
||||
|
||||
+13
-5
@@ -10,9 +10,12 @@ from tkinter import ttk
|
||||
from datetime import datetime
|
||||
|
||||
_SOURCE_NAMES = {
|
||||
"powerball_ny": "Powerball",
|
||||
"megamillions_ny": "Mega Millions (NY)",
|
||||
"megamillions_tx": "Mega Millions (TX)",
|
||||
"powerball_ny": "Powerball",
|
||||
"megamillions_ny": "Mega Millions (NY)",
|
||||
"megamillions_tx": "Mega Millions (TX)",
|
||||
"cash5_va": "Cash 5 (VA)",
|
||||
"millionaireforlife_va": "Millionaire for Life (VA)",
|
||||
"bankamillion_va": "Bank a Million (VA)",
|
||||
}
|
||||
|
||||
|
||||
@@ -50,5 +53,10 @@ class StatusBar(ttk.Frame):
|
||||
parts.append(f"{name} — ERROR: {r['message']}")
|
||||
else:
|
||||
parts.append(f"{name} — {r['added']} added, {r['skipped']} skipped")
|
||||
text = "Last fetch: " + " | ".join(parts) + f" | {now}"
|
||||
self.set_text(text)
|
||||
self._last_fetch_text = "Last fetch: " + " | ".join(parts) + f" | {now}"
|
||||
self.set_text(self._last_fetch_text)
|
||||
|
||||
def append_match_alert(self, msg: str):
|
||||
"""Append a prediction-match alert to the current status text."""
|
||||
base = getattr(self, "_last_fetch_text", self._label.cget("text"))
|
||||
self.set_text(f"{base} • {msg}")
|
||||
|
||||
Reference in New Issue
Block a user