✨ New Features: - AI-powered habit creation with natural language processing - HuggingFace transformers integration for sentiment analysis (tracked via Git LFS) - Advanced predictive analytics and behavioral insights - Voice & image input capabilities for hands-free habit tracking - Real-time notifications and community features - Plugin system with extensible architecture 🔧 Technical Improvements: - Comprehensive FastAPI backend with 30+ endpoints - React frontend with PWA capabilities - Advanced authentication with 2FA support - RBAC authorization system - Comprehensive security features (CSRF, rate limiting, audit logging) - Database migrations and health monitoring - Docker containerization support - Git LFS configured for large AI model files (2+ GB) 📚 Documentation & DevOps: - Complete deployment guides for multiple platforms - Professional README with feature highlights - GitHub Actions CI/CD workflows - Comprehensive API documentation - Security audit roadmap and compliance framework - Setup scripts for development environment 🧪 Testing & Quality: - Comprehensive test suite with 20+ test modules - Setup verification scripts - Working development environment with both backend and frontend - Health checks and monitoring systems 🌟 Ready for: - Portfolio showcasing - Community contributions - Production deployment - Professional presentation
388 lines
14 KiB
Python
388 lines
14 KiB
Python
"""
|
|
Enhanced Momentum System - Matching AHK's time-decay momentum mechanics
|
|
|
|
This module implements the momentum system that matches the legacy AutoHotkey
|
|
version, including daily decay and completion-based momentum boosts.
|
|
"""
|
|
from datetime import datetime, timedelta
|
|
from typing import Dict, List, Optional
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import and_
|
|
import models
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class MomentumService:
|
|
"""Service for managing user momentum with time-based decay and boosts."""
|
|
|
|
# Constants matching AHK version
|
|
DAILY_DECAY_RATE = 15 # 15% daily decay
|
|
COMPLETION_BOOST = 5 # 5 points per completion
|
|
MAX_MOMENTUM = 100 # Maximum momentum level
|
|
MIN_MOMENTUM = 0 # Minimum momentum level
|
|
|
|
def __init__(self, db: Session):
|
|
self.db = db
|
|
|
|
def get_user_momentum(self, user_id: int) -> Dict:
|
|
"""Get current user momentum with calculation if needed."""
|
|
user = (self.db.query(models.User)
|
|
.filter(models.User.id == user_id).first())
|
|
if not user:
|
|
raise ValueError(f"User {user_id} not found")
|
|
|
|
# Get or create momentum record
|
|
momentum_record = self._get_or_create_momentum_record(user_id)
|
|
|
|
# Calculate current momentum with decay
|
|
current_momentum = self._calculate_current_momentum(momentum_record)
|
|
|
|
# Update if changed
|
|
if current_momentum != momentum_record.momentum:
|
|
momentum_record.momentum = current_momentum
|
|
momentum_record.last_updated = datetime.utcnow()
|
|
self.db.commit()
|
|
|
|
return {
|
|
'user_id': user_id,
|
|
'momentum': current_momentum,
|
|
'last_updated': momentum_record.last_updated.isoformat(),
|
|
'momentum_color': self._get_momentum_color(current_momentum),
|
|
'momentum_level': self._get_momentum_level(current_momentum),
|
|
'days_since_update': (
|
|
datetime.utcnow() - momentum_record.last_updated
|
|
).days
|
|
}
|
|
|
|
def update_momentum_for_completion(
|
|
self, user_id: int, habit_difficulty: int = 1
|
|
) -> Dict:
|
|
"""Update momentum when a habit is completed."""
|
|
momentum_record = self._get_or_create_momentum_record(user_id)
|
|
|
|
# First apply any pending decay
|
|
current_momentum = self._calculate_current_momentum(momentum_record)
|
|
|
|
# Apply completion boost (scaled by difficulty)
|
|
boost = self.COMPLETION_BOOST * habit_difficulty
|
|
new_momentum = min(self.MAX_MOMENTUM, current_momentum + boost)
|
|
|
|
# Update record
|
|
momentum_record.momentum = new_momentum
|
|
momentum_record.last_updated = datetime.utcnow()
|
|
self.db.commit()
|
|
|
|
logger.info(
|
|
f"Momentum updated for user {user_id}: "
|
|
f"{current_momentum} -> {new_momentum} (+{boost})"
|
|
)
|
|
|
|
return {
|
|
'previous_momentum': current_momentum,
|
|
'new_momentum': new_momentum,
|
|
'boost_applied': boost,
|
|
'momentum_color': self._get_momentum_color(new_momentum),
|
|
'momentum_level': self._get_momentum_level(new_momentum)
|
|
}
|
|
|
|
def apply_daily_momentum_decay(
|
|
self, user_id: Optional[int] = None
|
|
) -> List[Dict]:
|
|
"""Apply daily momentum decay. If user_id is None, applies to all."""
|
|
if user_id:
|
|
users = [
|
|
self.db.query(models.User)
|
|
.filter(models.User.id == user_id).first()
|
|
]
|
|
else:
|
|
users = self.db.query(models.User).all()
|
|
|
|
results = []
|
|
|
|
for user in users:
|
|
if not user:
|
|
continue
|
|
|
|
momentum_record = self._get_or_create_momentum_record(user.id)
|
|
old_momentum = momentum_record.momentum
|
|
new_momentum = self._calculate_current_momentum(momentum_record)
|
|
|
|
if new_momentum != old_momentum:
|
|
momentum_record.momentum = new_momentum
|
|
momentum_record.last_updated = datetime.utcnow()
|
|
|
|
results.append({
|
|
'user_id': user.id,
|
|
'old_momentum': old_momentum,
|
|
'new_momentum': new_momentum,
|
|
'decay_applied': old_momentum - new_momentum
|
|
})
|
|
|
|
self.db.commit()
|
|
return results
|
|
|
|
def get_momentum_history(self, user_id: int, days: int = 30) -> Dict:
|
|
"""Get momentum history for visualization."""
|
|
end_date = datetime.utcnow()
|
|
start_date = end_date - timedelta(days=days)
|
|
|
|
# Get habit completions for the period
|
|
completions = self.db.query(models.Log).filter(
|
|
and_(
|
|
models.Log.user_id == user_id,
|
|
models.Log.action == 'completed',
|
|
models.Log.created_at >= start_date
|
|
)
|
|
).order_by(models.Log.created_at).all()
|
|
|
|
# Simulate momentum over time
|
|
momentum_history = []
|
|
current_momentum = self._get_or_create_momentum_record(user_id).momentum
|
|
|
|
# Work backwards from current momentum
|
|
completion_dates = [c.created_at.date() for c in completions]
|
|
|
|
for i in range(days):
|
|
date = (end_date - timedelta(days=i)).date()
|
|
|
|
# Count completions on this date
|
|
completions_count = completion_dates.count(date)
|
|
|
|
# Estimate momentum for this date
|
|
if i == 0:
|
|
momentum = current_momentum
|
|
else:
|
|
# Apply reverse decay and subtract completion boosts
|
|
momentum = momentum_history[i-1]['momentum']
|
|
momentum += self.DAILY_DECAY_RATE # Add back the decay
|
|
momentum -= completions_count * self.COMPLETION_BOOST # Subtract the boost
|
|
momentum = max(self.MIN_MOMENTUM, min(self.MAX_MOMENTUM, momentum))
|
|
|
|
momentum_history.append({
|
|
'date': date.isoformat(),
|
|
'momentum': max(0, momentum),
|
|
'completions': completions_count,
|
|
'momentum_level': self._get_momentum_level(momentum)
|
|
})
|
|
|
|
# Reverse to get chronological order
|
|
momentum_history.reverse()
|
|
|
|
return {
|
|
'user_id': user_id,
|
|
'period_days': days,
|
|
'history': momentum_history,
|
|
'average_momentum': sum(h['momentum'] for h in momentum_history) / len(momentum_history),
|
|
'total_completions': sum(h['completions'] for h in momentum_history)
|
|
}
|
|
|
|
def get_momentum_insights(self, user_id: int) -> Dict:
|
|
"""Get momentum insights and recommendations."""
|
|
current_data = self.get_user_momentum(user_id)
|
|
history = self.get_momentum_history(user_id, 7) # Last week
|
|
|
|
insights = []
|
|
recommendations = []
|
|
|
|
# Analyze current momentum level
|
|
momentum = current_data['momentum']
|
|
if momentum >= 80:
|
|
insights.append("🔥 You're on fire! Your momentum is excellent.")
|
|
recommendations.append("Keep up the great work and maintain consistency.")
|
|
elif momentum >= 60:
|
|
insights.append("💪 Strong momentum! You're building good habits.")
|
|
recommendations.append("Try to complete habits daily to maintain this level.")
|
|
elif momentum >= 40:
|
|
insights.append("⚡ Moderate momentum. Room for improvement.")
|
|
recommendations.append("Focus on completing at least one habit daily.")
|
|
elif momentum >= 20:
|
|
insights.append("📈 Low momentum. Time to get back on track.")
|
|
recommendations.append("Start with easier habits to rebuild momentum.")
|
|
else:
|
|
insights.append("🎯 Fresh start! Let's build momentum together.")
|
|
recommendations.append("Begin with one simple habit and complete it daily.")
|
|
|
|
# Analyze recent trend
|
|
recent_momentum = [h['momentum'] for h in history['history'][-3:]]
|
|
if len(recent_momentum) >= 2:
|
|
trend = recent_momentum[-1] - recent_momentum[0]
|
|
if trend > 10:
|
|
insights.append("📈 Your momentum is trending upward!")
|
|
elif trend < -10:
|
|
insights.append("📉 Your momentum has been declining recently.")
|
|
|
|
# Days without decay
|
|
days_since_update = current_data['days_since_update']
|
|
if days_since_update >= 3:
|
|
recommendations.append(f"It's been {days_since_update} days since your last activity. Complete a habit to prevent further momentum decay.")
|
|
|
|
return {
|
|
'user_id': user_id,
|
|
'current_momentum': momentum,
|
|
'momentum_level': current_data['momentum_level'],
|
|
'insights': insights,
|
|
'recommendations': recommendations,
|
|
'days_since_update': days_since_update,
|
|
'weekly_average': history['average_momentum']
|
|
}
|
|
|
|
def _get_or_create_momentum_record(self, user_id: int) -> models.UserMomentum:
|
|
"""Get or create momentum record for user."""
|
|
momentum_record = self.db.query(models.UserMomentum).filter(
|
|
models.UserMomentum.user_id == user_id
|
|
).first()
|
|
|
|
if not momentum_record:
|
|
momentum_record = models.UserMomentum(
|
|
user_id=user_id,
|
|
momentum=50, # Start with moderate momentum
|
|
last_updated=datetime.utcnow()
|
|
)
|
|
self.db.add(momentum_record)
|
|
self.db.commit()
|
|
|
|
return momentum_record
|
|
|
|
def _calculate_current_momentum(self, momentum_record: models.UserMomentum) -> float:
|
|
"""Calculate current momentum with time-based decay."""
|
|
if not momentum_record.last_updated:
|
|
return momentum_record.momentum
|
|
|
|
# Calculate days since last update
|
|
now = datetime.utcnow()
|
|
days_elapsed = (now - momentum_record.last_updated).days
|
|
|
|
if days_elapsed == 0:
|
|
return momentum_record.momentum
|
|
|
|
# Apply daily decay (15% per day, matching AHK)
|
|
current_momentum = momentum_record.momentum
|
|
for _ in range(days_elapsed):
|
|
decay = current_momentum * (self.DAILY_DECAY_RATE / 100)
|
|
current_momentum = max(self.MIN_MOMENTUM, current_momentum - decay)
|
|
|
|
return round(current_momentum, 2)
|
|
|
|
def _get_momentum_color(self, momentum: float) -> str:
|
|
"""Get color code for momentum level (matching AHK HUD colors)."""
|
|
if momentum >= 70:
|
|
return 'green'
|
|
elif momentum >= 40:
|
|
return 'yellow'
|
|
else:
|
|
return 'red'
|
|
|
|
def _get_momentum_level(self, momentum: float) -> str:
|
|
"""Get descriptive level for momentum."""
|
|
if momentum >= 90:
|
|
return 'Legendary'
|
|
elif momentum >= 80:
|
|
return 'Excellent'
|
|
elif momentum >= 70:
|
|
return 'Great'
|
|
elif momentum >= 60:
|
|
return 'Good'
|
|
elif momentum >= 50:
|
|
return 'Fair'
|
|
elif momentum >= 40:
|
|
return 'Moderate'
|
|
elif momentum >= 30:
|
|
return 'Low'
|
|
elif momentum >= 20:
|
|
return 'Poor'
|
|
else:
|
|
return 'Critical'
|
|
|
|
|
|
# Add momentum model if it doesn't exist
|
|
def add_momentum_model_if_needed():
|
|
"""Helper to add momentum model to models.py if not present."""
|
|
momentum_model = '''
|
|
class UserMomentum(Base):
|
|
__tablename__ = 'user_momentum'
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
|
|
momentum = Column(Float, default=50.0) # Current momentum level (0-100)
|
|
last_updated = Column(DateTime, server_default=func.current_timestamp())
|
|
|
|
user = relationship("User", back_populates="momentum")
|
|
|
|
__table_args__ = (
|
|
Index('idx_user_momentum_user_id', 'user_id'),
|
|
)
|
|
'''
|
|
return momentum_model
|
|
|
|
|
|
# FastAPI endpoints for momentum system
|
|
def get_momentum_endpoints():
|
|
"""Return FastAPI endpoints for momentum system."""
|
|
endpoints = '''
|
|
@app.get('/api/v1/momentum/{user_id}')
|
|
def get_user_momentum_endpoint(
|
|
user_id: int,
|
|
user=Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Get current user momentum."""
|
|
if user.id != user_id and not user.is_admin:
|
|
raise HTTPException(403, "Access denied")
|
|
|
|
momentum_service = MomentumService(db)
|
|
return momentum_service.get_user_momentum(user_id)
|
|
|
|
@app.post('/api/v1/momentum/{user_id}/boost')
|
|
def boost_momentum_endpoint(
|
|
user_id: int,
|
|
difficulty: int = 1,
|
|
user=Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Boost momentum for habit completion."""
|
|
if user.id != user_id:
|
|
raise HTTPException(403, "Access denied")
|
|
|
|
momentum_service = MomentumService(db)
|
|
return momentum_service.update_momentum_for_completion(user_id, difficulty)
|
|
|
|
@app.get('/api/v1/momentum/{user_id}/history')
|
|
def get_momentum_history_endpoint(
|
|
user_id: int,
|
|
days: int = 30,
|
|
user=Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Get momentum history for visualization."""
|
|
if user.id != user_id and not user.is_admin:
|
|
raise HTTPException(403, "Access denied")
|
|
|
|
momentum_service = MomentumService(db)
|
|
return momentum_service.get_momentum_history(user_id, days)
|
|
|
|
@app.get('/api/v1/momentum/{user_id}/insights')
|
|
def get_momentum_insights_endpoint(
|
|
user_id: int,
|
|
user=Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Get momentum insights and recommendations."""
|
|
if user.id != user_id and not user.is_admin:
|
|
raise HTTPException(403, "Access denied")
|
|
|
|
momentum_service = MomentumService(db)
|
|
return momentum_service.get_momentum_insights(user_id)
|
|
|
|
@app.post('/api/v1/admin/momentum/decay')
|
|
def apply_momentum_decay_endpoint(
|
|
admin_user=Depends(require_admin),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Apply momentum decay to all users (admin only)."""
|
|
momentum_service = MomentumService(db)
|
|
results = momentum_service.apply_daily_momentum_decay()
|
|
return {"updated_users": len(results), "results": results}
|
|
'''
|
|
return endpoints |