- Replace inline __import__("datetime").timedelta hack with proper import
- Remove unused import random in anki_export.py
- Add error handling for Claude CLI subprocess failures in ai.py
- Fix hardcoded absolute path in stt.py with relative Path resolution
- Fix N+1 DB queries in vocab.get_flashcard_batch and dashboard.get_category_breakdown
by adding db.get_all_word_progress() batch query
- Wire Ollama model and Whisper size settings to actually update config
via ai.set_ollama_model() and stt.set_whisper_size()
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
85 lines
2.5 KiB
Python
85 lines
2.5 KiB
Python
"""Dashboard: progress stats, charts, and overview."""
|
|
|
|
import db
|
|
from modules.vocab import load_vocab, get_categories
|
|
|
|
|
|
def get_overview():
|
|
"""Return overview stats: total words, seen, mastered, due today."""
|
|
vocab = load_vocab()
|
|
counts = db.get_word_counts(total_vocab_size=len(vocab))
|
|
stats = db.get_stats()
|
|
counts["streak"] = stats["streak"]
|
|
counts["total_reviews"] = stats["total_reviews"]
|
|
counts["total_quizzes"] = stats["total_quizzes"]
|
|
return counts
|
|
|
|
|
|
def get_category_breakdown():
|
|
"""Return progress per category as list of dicts."""
|
|
vocab = load_vocab()
|
|
categories = get_categories()
|
|
all_progress = db.get_all_word_progress()
|
|
|
|
breakdown = []
|
|
for cat in categories:
|
|
cat_words = [e for e in vocab if e["category"] == cat]
|
|
total = len(cat_words)
|
|
|
|
seen = 0
|
|
mastered = 0
|
|
for e in cat_words:
|
|
progress = all_progress.get(e["id"])
|
|
if progress:
|
|
seen += 1
|
|
if progress["stability"] and progress["stability"] > 10:
|
|
mastered += 1
|
|
|
|
breakdown.append({
|
|
"Category": cat,
|
|
"Total": total,
|
|
"Seen": seen,
|
|
"Mastered": mastered,
|
|
"Progress": f"{seen}/{total}" if total > 0 else "0/0",
|
|
})
|
|
|
|
return breakdown
|
|
|
|
|
|
def get_recent_quizzes(limit=10):
|
|
"""Return recent quiz results as list of dicts for display."""
|
|
stats = db.get_stats()
|
|
quizzes = stats["recent_quizzes"][:limit]
|
|
result = []
|
|
for q in quizzes:
|
|
result.append({
|
|
"Date": q["timestamp"],
|
|
"Category": q["category"] or "All",
|
|
"Score": f"{q['correct']}/{q['total_questions']}",
|
|
"Duration": f"{q['duration_seconds'] or 0}s",
|
|
})
|
|
return result
|
|
|
|
|
|
def format_overview_markdown():
|
|
"""Format overview stats as a markdown string for display."""
|
|
o = get_overview()
|
|
pct = (o["seen"] / o["total"] * 100) if o["total"] > 0 else 0
|
|
bar_filled = int(pct / 5)
|
|
bar_empty = 20 - bar_filled
|
|
progress_bar = "█" * bar_filled + "░" * bar_empty
|
|
|
|
lines = [
|
|
"## Dashboard",
|
|
"",
|
|
f"**Words studied:** {o['seen']} / {o['total']} ({pct:.0f}%)",
|
|
f"`{progress_bar}`",
|
|
"",
|
|
f"**Due today:** {o['due']}",
|
|
f"**Mastered:** {o['mastered']}",
|
|
f"**Daily streak:** {o['streak']} day{'s' if o['streak'] != 1 else ''}",
|
|
f"**Total reviews:** {o['total_reviews']}",
|
|
f"**Quiz sessions:** {o['total_quizzes']}",
|
|
]
|
|
return "\n".join(lines)
|