✨ 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.
94 lines
3.6 KiB
Python
94 lines
3.6 KiB
Python
import os
|
|
from typing import List, Dict, Optional
|
|
import json
|
|
|
|
|
|
def getenv_bool(name: str, default: bool = False) -> bool:
|
|
val = os.getenv(name)
|
|
if val is None:
|
|
return default
|
|
return str(val).strip().lower() in {"1", "true", "yes", "on"}
|
|
|
|
|
|
def parse_csv_env(name: str) -> List[str]:
|
|
raw = os.getenv(name)
|
|
if not raw:
|
|
return []
|
|
return [part.strip() for part in raw.split(',') if part.strip()]
|
|
|
|
|
|
class Settings:
|
|
def __init__(self) -> None:
|
|
# CORS / Origins
|
|
origins = parse_csv_env("FRONTEND_ORIGINS")
|
|
if not origins:
|
|
single = os.getenv("FRONTEND_ORIGIN", "http://localhost:5173")
|
|
origins = [single]
|
|
self.FRONTEND_ORIGINS: List[str] = origins
|
|
|
|
# HTTPS and cookies
|
|
self.FORCE_HTTPS: bool = getenv_bool("FORCE_HTTPS", False)
|
|
self.HSTS_ENABLE: bool = getenv_bool("HSTS_ENABLE", False)
|
|
self.COOKIE_SECURE: bool = getenv_bool("COOKIE_SECURE", False)
|
|
self.COOKIE_SAMESITE: str = os.getenv("COOKIE_SAMESITE", "lax")
|
|
|
|
# CSP extras
|
|
extra = parse_csv_env("CSP_CONNECT_EXTRA")
|
|
if not extra:
|
|
extra = ["https://www.googleapis.com"]
|
|
self.CSP_CONNECT_EXTRA: List[str] = extra
|
|
|
|
# CSRF
|
|
self.CSRF_ENABLE: bool = getenv_bool("CSRF_ENABLE", False)
|
|
self.CSRF_HEADER_NAME: str = os.getenv("CSRF_HEADER_NAME", "x-csrf-token")
|
|
self.CSRF_COOKIE_NAME: str = os.getenv("CSRF_COOKIE_NAME", "csrf_token")
|
|
|
|
# Integrations behavior
|
|
self.INTEGRATION_CLOSE_MODE: str = os.getenv("INTEGRRATION_CLOSE_MODE", "archive").lower() if os.getenv("INTEGRRATION_CLOSE_MODE") else os.getenv("INTEGRATION_CLOSE_MODE", "archive").lower()
|
|
|
|
# Email / SMTP
|
|
self.EMAIL_TRANSPORT: str = os.getenv("LIFERPG_EMAIL_TRANSPORT", "console").lower() # console|smtp|disabled
|
|
self.SMTP_HOST: Optional[str] = os.getenv("SMTP_HOST")
|
|
self.SMTP_PORT: int = int(os.getenv("SMTP_PORT", "587"))
|
|
self.SMTP_USERNAME: Optional[str] = os.getenv("SMTP_USERNAME")
|
|
self.SMTP_PASSWORD: Optional[str] = os.getenv("SMTP_PASSWORD")
|
|
self.SMTP_USE_TLS: bool = getenv_bool("SMTP_USE_TLS", True)
|
|
self.SMTP_FROM: Optional[str] = os.getenv("SMTP_FROM", os.getenv("SMTP_USER", None))
|
|
|
|
# Provider concurrency caps (optional per-provider overrides)
|
|
# Example env: SYNC_PROVIDER_CAPS='{"todoist":2,"github":3}'
|
|
caps_raw = os.getenv("SYNC_PROVIDER_CAPS")
|
|
caps: Dict[str, int] = {}
|
|
if caps_raw:
|
|
try:
|
|
data = json.loads(caps_raw)
|
|
if isinstance(data, dict):
|
|
for k, v in data.items():
|
|
try:
|
|
iv = int(v)
|
|
if iv > 0:
|
|
caps[str(k)] = iv
|
|
except Exception:
|
|
continue
|
|
except Exception:
|
|
caps = {}
|
|
self.PROVIDER_CAPS: Dict[str, int] = caps
|
|
self.DEFAULT_PROVIDER_CAP: int = int(os.getenv('SYNC_MAX_CONCURRENCY_PER_PROVIDER', '4'))
|
|
|
|
def csp_header(self) -> str:
|
|
connect_src = " ".join(["'self'", *self.CSP_CONNECT_EXTRA])
|
|
# Allow inline styles in dev to keep things simple; consider removing in prod
|
|
return "; ".join([
|
|
"default-src 'self'",
|
|
"frame-ancestors 'none'",
|
|
"base-uri 'self'",
|
|
"object-src 'none'",
|
|
"img-src 'self' data:",
|
|
f"connect-src {connect_src}",
|
|
"script-src 'self'",
|
|
"style-src 'self' 'unsafe-inline'",
|
|
])
|
|
|
|
|
|
settings = Settings()
|