✨ Major Features Added: - Complete magical theming and rebranding from LifeRPG to The Wizard's Grimoire - Production-grade React frontend with Tailwind CSS v4 and magical aesthetics - Comprehensive analytics dashboard with Recharts integration (ScryingPortal) - Push notifications system with PWA service worker support - Drag & drop functionality using @dnd-kit for habit reordering - Social features with friends system and leaderboards - Performance optimization tools and monitoring - Mobile app enhancement with PWA installation support 🏗️ Technical Infrastructure: - Advanced service worker with offline support and background sync - Zustand state management for scalable application state - Production-ready UI component system with enhanced Button, Card, Input - Progressive Web App (PWA) with manifest and app installation - FastAPI backend with comprehensive API endpoints - Docker containerization and CI/CD pipeline setup 📱 Progressive Web App Features: - Offline functionality with intelligent caching - Push notification support for habit reminders - App installation on mobile and desktop platforms - Background sync for offline data management - Performance monitoring and optimization tools 🎨 User Experience: - Magical wizard/grimoire theming throughout application - Responsive design optimized for all device sizes - Drag & drop habit management with smooth animations - Interactive analytics with multiple chart types - Social connectivity with friends and competitive features - Comprehensive notification and performance settings 🔧 Developer Experience: - Modern development stack with Vite and React - Comprehensive testing setup and CI/CD pipelines - Code quality tools with pre-commit hooks - Docker development environment - Detailed documentation and implementation guides This represents a complete transformation from prototype to production-ready application with enterprise-grade features and magical user experience.
326 lines
11 KiB
Python
326 lines
11 KiB
Python
"""
|
|
Analytics module for LifeRPG - habit tracking insights and visualizations.
|
|
"""
|
|
from datetime import datetime, timedelta, timezone
|
|
from typing import Dict, List, Optional
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import func, and_
|
|
import models
|
|
import json
|
|
|
|
def get_habit_heatmap(db: Session, user_id: int, days: int = 365) -> Dict:
|
|
"""Generate habit completion heatmap data for the last N days."""
|
|
end_date = datetime.now(timezone.utc)
|
|
start_date = end_date - timedelta(days=days)
|
|
|
|
# Get all completions in the date range
|
|
completions = db.query(
|
|
func.date(models.Log.timestamp).label('date'),
|
|
func.count(models.Log.id).label('count')
|
|
).filter(
|
|
models.Log.user_id == user_id,
|
|
models.Log.action == 'complete',
|
|
models.Log.timestamp >= start_date
|
|
).group_by(
|
|
func.date(models.Log.timestamp)
|
|
).all()
|
|
|
|
# Create a map of date -> completion count
|
|
completion_map = {str(comp.date): comp.count for comp in completions}
|
|
|
|
# Generate full date range with completion counts
|
|
heatmap_data = []
|
|
current_date = start_date.date()
|
|
end_date_only = end_date.date()
|
|
|
|
while current_date <= end_date_only:
|
|
date_str = current_date.isoformat()
|
|
count = completion_map.get(date_str, 0)
|
|
heatmap_data.append({
|
|
'date': date_str,
|
|
'count': count,
|
|
'level': min(4, count) # 0-4 intensity levels for visualization
|
|
})
|
|
current_date += timedelta(days=1)
|
|
|
|
return {
|
|
'data': heatmap_data,
|
|
'total_days': days,
|
|
'completion_days': len(completion_map),
|
|
'total_completions': sum(completion_map.values()),
|
|
'start_date': start_date.date().isoformat(),
|
|
'end_date': end_date.date().isoformat()
|
|
}
|
|
|
|
def get_habit_trends(db: Session, user_id: int, habit_id: Optional[int] = None, days: int = 30) -> Dict:
|
|
"""Get habit completion trends over time."""
|
|
end_date = datetime.now(timezone.utc)
|
|
start_date = end_date - timedelta(days=days)
|
|
|
|
# Base query
|
|
query = db.query(
|
|
func.date(models.Log.timestamp).label('date'),
|
|
func.count(models.Log.id).label('completions')
|
|
).filter(
|
|
models.Log.user_id == user_id,
|
|
models.Log.action == 'complete',
|
|
models.Log.timestamp >= start_date
|
|
)
|
|
|
|
# Filter by specific habit if provided
|
|
if habit_id:
|
|
query = query.filter(models.Log.habit_id == habit_id)
|
|
|
|
trends = query.group_by(
|
|
func.date(models.Log.timestamp)
|
|
).order_by(
|
|
func.date(models.Log.timestamp)
|
|
).all()
|
|
|
|
# Fill in missing dates with 0
|
|
trend_data = []
|
|
current_date = start_date.date()
|
|
trend_map = {str(trend.date): trend.completions for trend in trends}
|
|
|
|
while current_date <= end_date.date():
|
|
date_str = current_date.isoformat()
|
|
trend_data.append({
|
|
'date': date_str,
|
|
'completions': trend_map.get(date_str, 0)
|
|
})
|
|
current_date += timedelta(days=1)
|
|
|
|
# Calculate some basic stats
|
|
total_completions = sum(trend_map.values())
|
|
active_days = len([d for d in trend_data if d['completions'] > 0])
|
|
avg_per_day = total_completions / days if days > 0 else 0
|
|
|
|
return {
|
|
'data': trend_data,
|
|
'stats': {
|
|
'total_completions': total_completions,
|
|
'active_days': active_days,
|
|
'average_per_day': round(avg_per_day, 2),
|
|
'completion_rate': round((active_days / days) * 100, 1) if days > 0 else 0
|
|
},
|
|
'period': {
|
|
'days': days,
|
|
'start_date': start_date.date().isoformat(),
|
|
'end_date': end_date.date().isoformat()
|
|
}
|
|
}
|
|
|
|
def get_habit_breakdown(db: Session, user_id: int, days: int = 30) -> Dict:
|
|
"""Get breakdown of completions by habit."""
|
|
end_date = datetime.now(timezone.utc)
|
|
start_date = end_date - timedelta(days=days)
|
|
|
|
# Get completions by habit
|
|
results = db.query(
|
|
models.Habit.id,
|
|
models.Habit.title,
|
|
func.count(models.Log.id).label('completions')
|
|
).join(
|
|
models.Log, models.Habit.id == models.Log.habit_id
|
|
).filter(
|
|
models.Habit.user_id == user_id,
|
|
models.Log.action == 'complete',
|
|
models.Log.timestamp >= start_date
|
|
).group_by(
|
|
models.Habit.id, models.Habit.title
|
|
).order_by(
|
|
func.count(models.Log.id).desc()
|
|
).all()
|
|
|
|
habit_data = []
|
|
total_completions = 0
|
|
|
|
for result in results:
|
|
completions = result.completions
|
|
total_completions += completions
|
|
habit_data.append({
|
|
'habit_id': result.id,
|
|
'habit_title': result.title,
|
|
'completions': completions
|
|
})
|
|
|
|
# Calculate percentages
|
|
for habit in habit_data:
|
|
habit['percentage'] = round((habit['completions'] / total_completions) * 100, 1) if total_completions > 0 else 0
|
|
|
|
return {
|
|
'habits': habit_data,
|
|
'total_completions': total_completions,
|
|
'period': {
|
|
'days': days,
|
|
'start_date': start_date.date().isoformat(),
|
|
'end_date': end_date.date().isoformat()
|
|
}
|
|
}
|
|
|
|
def get_streak_history(db: Session, user_id: int, days: int = 90) -> Dict:
|
|
"""Calculate streak history over time."""
|
|
end_date = datetime.now(timezone.utc)
|
|
start_date = end_date - timedelta(days=days)
|
|
|
|
# Get all completion dates
|
|
completion_dates = db.query(
|
|
func.date(models.Log.timestamp).label('date')
|
|
).filter(
|
|
models.Log.user_id == user_id,
|
|
models.Log.action == 'complete',
|
|
models.Log.timestamp >= start_date
|
|
).group_by(
|
|
func.date(models.Log.timestamp)
|
|
).order_by(
|
|
func.date(models.Log.timestamp)
|
|
).all()
|
|
|
|
# Convert to set for fast lookup
|
|
completion_dates_set = {comp.date for comp in completion_dates}
|
|
|
|
# Calculate streak for each day
|
|
streak_data = []
|
|
current_date = start_date.date()
|
|
current_streak = 0
|
|
|
|
while current_date <= end_date.date():
|
|
if current_date in completion_dates_set:
|
|
current_streak += 1
|
|
else:
|
|
current_streak = 0
|
|
|
|
streak_data.append({
|
|
'date': current_date.isoformat(),
|
|
'streak': current_streak,
|
|
'completed': current_date in completion_dates_set
|
|
})
|
|
|
|
current_date += timedelta(days=1)
|
|
|
|
# Find longest streak in period
|
|
max_streak = max((day['streak'] for day in streak_data), default=0)
|
|
|
|
return {
|
|
'data': streak_data,
|
|
'max_streak': max_streak,
|
|
'current_streak': streak_data[-1]['streak'] if streak_data else 0,
|
|
'period': {
|
|
'days': days,
|
|
'start_date': start_date.date().isoformat(),
|
|
'end_date': end_date.date().isoformat()
|
|
}
|
|
}
|
|
|
|
def get_weekly_summary(db: Session, user_id: int, weeks: int = 12) -> Dict:
|
|
"""Get weekly completion summary."""
|
|
end_date = datetime.now(timezone.utc)
|
|
start_date = end_date - timedelta(weeks=weeks)
|
|
|
|
# Get completions grouped by week
|
|
results = db.query(
|
|
func.strftime('%Y-%W', models.Log.timestamp).label('week'),
|
|
func.count(models.Log.id).label('completions')
|
|
).filter(
|
|
models.Log.user_id == user_id,
|
|
models.Log.action == 'complete',
|
|
models.Log.timestamp >= start_date
|
|
).group_by(
|
|
func.strftime('%Y-%W', models.Log.timestamp)
|
|
).order_by(
|
|
func.strftime('%Y-%W', models.Log.timestamp)
|
|
).all()
|
|
|
|
weekly_data = []
|
|
for result in results:
|
|
# Parse week string (YYYY-WW format)
|
|
year_week = result.week
|
|
completions = result.completions
|
|
|
|
weekly_data.append({
|
|
'week': year_week,
|
|
'completions': completions
|
|
})
|
|
|
|
return {
|
|
'data': weekly_data,
|
|
'total_weeks': weeks,
|
|
'period': {
|
|
'weeks': weeks,
|
|
'start_date': start_date.date().isoformat(),
|
|
'end_date': end_date.date().isoformat()
|
|
}
|
|
}
|
|
|
|
def get_performance_insights(db: Session, user_id: int) -> Dict:
|
|
"""Generate performance insights and recommendations."""
|
|
# Get basic stats
|
|
total_habits = db.query(models.Habit).filter(models.Habit.user_id == user_id).count()
|
|
active_habits = db.query(models.Habit).filter(
|
|
models.Habit.user_id == user_id,
|
|
models.Habit.status == 'active'
|
|
).count()
|
|
|
|
# Get completion data for last 30 days
|
|
thirty_days_ago = datetime.now(timezone.utc) - timedelta(days=30)
|
|
recent_completions = db.query(models.Log).filter(
|
|
models.Log.user_id == user_id,
|
|
models.Log.action == 'complete',
|
|
models.Log.timestamp >= thirty_days_ago
|
|
).count()
|
|
|
|
# Calculate completion rate
|
|
expected_completions = active_habits * 30 # Assuming daily habits
|
|
completion_rate = (recent_completions / expected_completions) * 100 if expected_completions > 0 else 0
|
|
|
|
# Get streak info
|
|
from . import gamification
|
|
current_streak = gamification.calculate_current_streak(db, user_id)
|
|
longest_streak = gamification.calculate_longest_streak(db, user_id)
|
|
|
|
# Generate insights
|
|
insights = []
|
|
|
|
if completion_rate < 50:
|
|
insights.append({
|
|
'type': 'warning',
|
|
'title': 'Low Completion Rate',
|
|
'message': f'Your completion rate is {completion_rate:.1f}%. Consider reducing the number of active habits or adjusting your routine.',
|
|
'action': 'Review your habits and focus on the most important ones.'
|
|
})
|
|
elif completion_rate > 80:
|
|
insights.append({
|
|
'type': 'success',
|
|
'title': 'Excellent Performance',
|
|
'message': f'Great job! You have a {completion_rate:.1f}% completion rate.',
|
|
'action': 'Consider adding new challenges or increasing habit difficulty.'
|
|
})
|
|
|
|
if current_streak == 0 and longest_streak > 0:
|
|
insights.append({
|
|
'type': 'motivation',
|
|
'title': 'Get Back on Track',
|
|
'message': f'You had a {longest_streak}-day streak before. You can do it again!',
|
|
'action': 'Start with one small habit to rebuild momentum.'
|
|
})
|
|
|
|
if current_streak >= 7:
|
|
insights.append({
|
|
'type': 'celebration',
|
|
'title': 'Great Streak!',
|
|
'message': f'You\'re on a {current_streak}-day streak. Keep it up!',
|
|
'action': 'Maintain consistency to reach the next milestone.'
|
|
})
|
|
|
|
return {
|
|
'stats': {
|
|
'total_habits': total_habits,
|
|
'active_habits': active_habits,
|
|
'completion_rate': round(completion_rate, 1),
|
|
'recent_completions': recent_completions,
|
|
'current_streak': current_streak,
|
|
'longest_streak': longest_streak
|
|
},
|
|
'insights': insights
|
|
}
|