05/23 Phase 22
This commit is contained in:
@@ -19,7 +19,9 @@
|
|||||||
"Bash(python -m pytest --tb=short -q)",
|
"Bash(python -m pytest --tb=short -q)",
|
||||||
"Bash(python -m pytest tests/test_wheeling.py -v)",
|
"Bash(python -m pytest tests/test_wheeling.py -v)",
|
||||||
"WebFetch(domain:www.valottery.com)",
|
"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
|
### ✅ 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 `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
|
- [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
|
import requests
|
||||||
from datetime import datetime
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -262,6 +262,10 @@ def _fetch_va_lottery(game_name: str, source: str, url: str) -> dict:
|
|||||||
game_id = game["id"]
|
game_id = game["id"]
|
||||||
added = skipped = 0
|
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:
|
try:
|
||||||
resp = requests.get(url, timeout=30)
|
resp = requests.get(url, timeout=30)
|
||||||
resp.raise_for_status()
|
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:
|
if draw_date is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Data comes newest-first; stop when we reach already-stored dates
|
||||||
|
if last_date and draw_date <= last_date:
|
||||||
|
break
|
||||||
|
|
||||||
try:
|
try:
|
||||||
p1 = parts[1].strip()
|
p1 = parts[1].strip()
|
||||||
p1_lower = p1.lower()
|
p1_lower = p1.lower()
|
||||||
|
|||||||
Binary file not shown.
+25
-9
@@ -50,7 +50,8 @@ def init_db():
|
|||||||
main_max INTEGER NOT NULL,
|
main_max INTEGER NOT NULL,
|
||||||
bonus_count INTEGER NOT NULL DEFAULT 0,
|
bonus_count INTEGER NOT NULL DEFAULT 0,
|
||||||
bonus_max 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()
|
conn.commit()
|
||||||
logger.info("[DB] Tables created/verified OK")
|
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.
|
Insert default game configs if they don't already exist.
|
||||||
Uses INSERT OR IGNORE to be idempotent on every launch.
|
Uses INSERT OR IGNORE to be idempotent on every launch.
|
||||||
|
Also back-fills top_prize for any existing rows where it is empty.
|
||||||
"""
|
"""
|
||||||
defaults = [
|
defaults = [
|
||||||
# name main_count main_max bonus_count bonus_max active
|
# name mc mm bc bm active top_prize
|
||||||
("Powerball", 5, 69, 1, 26, 1),
|
("Powerball", 5, 69, 1, 26, 1, "Jackpot (variable)"),
|
||||||
("Mega Millions", 5, 70, 1, 25, 1),
|
("Mega Millions", 5, 70, 1, 25, 1, "Jackpot (variable)"),
|
||||||
("Cash 5", 5, 45, 0, 0, 1),
|
("Cash 5", 5, 45, 0, 0, 1, "Jackpot from $200K"),
|
||||||
("Millionaire for Life", 5, 58, 1, 5, 1),
|
("Millionaire for Life", 5, 58, 1, 5, 1, "$1M/yr for life"),
|
||||||
("Bank a Million", 6, 40, 1, 40, 1),
|
("Bank a Million", 6, 40, 1, 40, 1, "$1M after taxes"),
|
||||||
]
|
]
|
||||||
|
|
||||||
for row in defaults:
|
for row in defaults:
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
INSERT OR IGNORE INTO games
|
INSERT OR IGNORE INTO games
|
||||||
(name, main_count, main_max, bonus_count, bonus_max, active)
|
(name, main_count, main_max, bonus_count, bonus_max, active, top_prize)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
""", row)
|
""", 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
|
inserted = conn.total_changes
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|||||||
@@ -167,10 +167,42 @@ class LottoSightApp(tk.Tk):
|
|||||||
def _on_fetch_done(self, results: list):
|
def _on_fetch_done(self, results: list):
|
||||||
self._fetch_btn.config(state="normal", text="Fetch Now")
|
self._fetch_btn.config(state="normal", text="Fetch Now")
|
||||||
self._statusbar.update_fetch_results(results)
|
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
|
# Refresh the current screen if it can show new data
|
||||||
if self._current_screen and hasattr(self._current_screen, "refresh"):
|
if self._current_screen and hasattr(self._current_screen, "refresh"):
|
||||||
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 ─────────────────────────────────────────────────────────────
|
# ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def on_close(self):
|
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)):
|
with patch("core.fetcher.requests.get", return_value=_text_resp(CASH5_VA_TEXT)):
|
||||||
result = fetch_cash5_va()
|
result = fetch_cash5_va()
|
||||||
assert result["added"] == 0
|
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):
|
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)):
|
with patch("core.fetcher.requests.get", return_value=_text_resp(BANKAMIL_VA_TEXT)):
|
||||||
result = fetch_bankamillion_va()
|
result = fetch_bankamillion_va()
|
||||||
assert result["added"] == 0
|
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):
|
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):
|
def __init__(self, parent, on_fetch=None, **kwargs):
|
||||||
super().__init__(parent, **kwargs)
|
super().__init__(parent, **kwargs)
|
||||||
self._on_fetch = on_fetch
|
self._on_fetch = on_fetch
|
||||||
|
self._filter_var = tk.StringVar(value="All Games")
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
|
|
||||||
# ── Layout ────────────────────────────────────────────────────────────────
|
# ── Layout ────────────────────────────────────────────────────────────────
|
||||||
@@ -66,7 +67,7 @@ class DashboardScreen(ttk.Frame):
|
|||||||
vsb.pack(side="right", fill="y")
|
vsb.pack(side="right", fill="y")
|
||||||
canvas.pack(side="left", fill="both", expand=True)
|
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")
|
win = canvas.create_window((0, 0), window=inner, anchor="nw")
|
||||||
|
|
||||||
def _resize(event=None):
|
def _resize(event=None):
|
||||||
@@ -82,6 +83,18 @@ class DashboardScreen(ttk.Frame):
|
|||||||
lambda e: canvas.unbind_all("<MouseWheel>"))
|
lambda e: canvas.unbind_all("<MouseWheel>"))
|
||||||
|
|
||||||
self._inner = inner
|
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()
|
self._draw_sections()
|
||||||
|
|
||||||
def _section_header(self, title: str):
|
def _section_header(self, title: str):
|
||||||
@@ -101,7 +114,22 @@ class DashboardScreen(ttk.Frame):
|
|||||||
|
|
||||||
# ── Refresh ───────────────────────────────────────────────────────────────
|
# ── 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):
|
def refresh(self):
|
||||||
|
self._update_filter_options()
|
||||||
self._refresh_last_draws()
|
self._refresh_last_draws()
|
||||||
self._refresh_db_summary()
|
self._refresh_db_summary()
|
||||||
self._refresh_hot_numbers()
|
self._refresh_hot_numbers()
|
||||||
@@ -111,13 +139,12 @@ class DashboardScreen(ttk.Frame):
|
|||||||
for w in self._last_draw_body.winfo_children():
|
for w in self._last_draw_body.winfo_children():
|
||||||
w.destroy()
|
w.destroy()
|
||||||
|
|
||||||
games = get_all_games(active_only=True)
|
games = self._filtered_games()
|
||||||
if not games:
|
if not games:
|
||||||
ttk.Label(self._last_draw_body, text="No active games.",
|
ttk.Label(self._last_draw_body, text="No active games.",
|
||||||
foreground="#aaaaaa").pack(anchor="w")
|
foreground="#aaaaaa").pack(anchor="w")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Lay cards out in a horizontal row that wraps via grid
|
|
||||||
row_frame = ttk.Frame(self._last_draw_body)
|
row_frame = ttk.Frame(self._last_draw_body)
|
||||||
row_frame.pack(fill="x")
|
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")
|
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:
|
if draw is None:
|
||||||
ttk.Label(card, text="No draws yet.", foreground="#aaaaaa",
|
ttk.Label(card, text="No draws yet.", foreground="#aaaaaa",
|
||||||
font=_CARD_FONT).pack(anchor="w")
|
font=_CARD_FONT).pack(anchor="w")
|
||||||
@@ -172,7 +204,7 @@ class DashboardScreen(ttk.Frame):
|
|||||||
for w in self._hot_body.winfo_children():
|
for w in self._hot_body.winfo_children():
|
||||||
w.destroy()
|
w.destroy()
|
||||||
|
|
||||||
games = get_all_games(active_only=True)
|
games = self._filtered_games()
|
||||||
if not games:
|
if not games:
|
||||||
ttk.Label(self._hot_body, text="No active games.",
|
ttk.Label(self._hot_body, text="No active games.",
|
||||||
foreground="#aaaaaa").pack(anchor="w")
|
foreground="#aaaaaa").pack(anchor="w")
|
||||||
@@ -201,7 +233,7 @@ class DashboardScreen(ttk.Frame):
|
|||||||
for w in self._overdue_body.winfo_children():
|
for w in self._overdue_body.winfo_children():
|
||||||
w.destroy()
|
w.destroy()
|
||||||
|
|
||||||
games = get_all_games(active_only=True)
|
games = self._filtered_games()
|
||||||
if not games:
|
if not games:
|
||||||
ttk.Label(self._overdue_body, text="No active games.",
|
ttk.Label(self._overdue_body, text="No active games.",
|
||||||
foreground="#aaaaaa").pack(anchor="w")
|
foreground="#aaaaaa").pack(anchor="w")
|
||||||
|
|||||||
+10
-2
@@ -13,6 +13,9 @@ _SOURCE_NAMES = {
|
|||||||
"powerball_ny": "Powerball",
|
"powerball_ny": "Powerball",
|
||||||
"megamillions_ny": "Mega Millions (NY)",
|
"megamillions_ny": "Mega Millions (NY)",
|
||||||
"megamillions_tx": "Mega Millions (TX)",
|
"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']}")
|
parts.append(f"{name} — ERROR: {r['message']}")
|
||||||
else:
|
else:
|
||||||
parts.append(f"{name} — {r['added']} added, {r['skipped']} skipped")
|
parts.append(f"{name} — {r['added']} added, {r['skipped']} skipped")
|
||||||
text = "Last fetch: " + " | ".join(parts) + f" | {now}"
|
self._last_fetch_text = "Last fetch: " + " | ".join(parts) + f" | {now}"
|
||||||
self.set_text(text)
|
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