✨ 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.
121 lines
4.6 KiB
Python
121 lines
4.6 KiB
Python
from typing import Any, Dict, List
|
|
|
|
|
|
class Hook:
|
|
def run(self, *, db, integration_id: int, event: str, context: Dict[str, Any]):
|
|
raise NotImplementedError()
|
|
|
|
|
|
class SlackHook(Hook):
|
|
def __init__(self, preset_text: str | None = None):
|
|
self.preset_text = preset_text
|
|
|
|
def run(self, *, db, integration_id: int, event: str, context: Dict[str, Any]):
|
|
from .notifier import emit_sync_event
|
|
# Reuse existing slack notifier; include summary if preset_text provided
|
|
payload = {'provider': context.get('provider'), 'summary': {'count': context.get('count')}}
|
|
if self.preset_text:
|
|
payload['summary'] = {'text': self.preset_text}
|
|
try:
|
|
emit_sync_event(db, integration_id, event, payload)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
class WebhookHook(Hook):
|
|
def __init__(self, url: str, template: str | None = None, headers: Dict[str, str] | None = None):
|
|
self.url = url
|
|
self.template = template
|
|
self.headers = headers or {}
|
|
|
|
def run(self, *, db, integration_id: int, event: str, context: Dict[str, Any]):
|
|
from .notifier import send_webhook
|
|
body: Dict[str, Any]
|
|
if self.template:
|
|
try:
|
|
text = self.template.format(**context)
|
|
body = {'text': text, 'event': event, 'integration_id': integration_id}
|
|
except Exception:
|
|
body = {'event': event, 'integration_id': integration_id, 'context': context}
|
|
else:
|
|
body = {'event': event, 'integration_id': integration_id, 'context': context}
|
|
try:
|
|
send_webhook(self.url, body, headers=self.headers)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
class EmailHook(Hook):
|
|
def __init__(self, to: str, subject_template: str, body_template: str):
|
|
self.to = to
|
|
self.subject_template = subject_template
|
|
self.body_template = body_template
|
|
|
|
def run(self, *, db, integration_id: int, event: str, context: Dict[str, Any]):
|
|
from .notifier import send_email
|
|
try:
|
|
subj = self.subject_template.format(**context)
|
|
body = self.body_template.format(**context)
|
|
except Exception:
|
|
subj = f"LifeRPG {event} for integration {integration_id}"
|
|
body = str(context)
|
|
try:
|
|
send_email(self.to, subj, body)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
class HookManager:
|
|
def __init__(self, hooks_config: Dict[str, Any] | None):
|
|
self.cfg = hooks_config or {}
|
|
|
|
def _build_hooks(self, items: List[Dict[str, Any]]) -> List[Hook]:
|
|
hooks: List[Hook] = []
|
|
for it in items or []:
|
|
typ = (it.get('type') or '').lower()
|
|
if typ == 'slack':
|
|
hooks.append(SlackHook(preset_text=it.get('text')))
|
|
elif typ == 'webhook':
|
|
hooks.append(WebhookHook(url=it.get('url', ''), template=it.get('template'), headers=it.get('headers')))
|
|
elif typ == 'email':
|
|
hooks.append(EmailHook(to=it.get('to', ''), subject_template=it.get('subject', 'LifeRPG {event}'), body_template=it.get('body', '{context}')))
|
|
return hooks
|
|
|
|
def run_pre(self, *, db, integration_id: int, context: Dict[str, Any]):
|
|
pre = self._build_hooks(self.cfg.get('pre_sync', []))
|
|
for h in pre:
|
|
try:
|
|
h.run(db=db, integration_id=integration_id, event='pre_sync', context=context)
|
|
except Exception:
|
|
continue
|
|
|
|
def run_post(self, *, db, integration_id: int, status: str, context: Dict[str, Any]):
|
|
# Filter post hooks by 'on' condition (success, fail, always)
|
|
items = self.cfg.get('post_sync', [])
|
|
selected: List[Dict[str, Any]] = []
|
|
for it in items:
|
|
on = (it.get('on') or 'always').lower()
|
|
if on == 'always' or (on == 'success' and status == 'success') or (on == 'fail' and status != 'success'):
|
|
selected.append(it)
|
|
post = self._build_hooks(selected)
|
|
ev = 'post_sync_success' if status == 'success' else 'post_sync_fail'
|
|
for h in post:
|
|
try:
|
|
h.run(db=db, integration_id=integration_id, event=ev, context=context)
|
|
except Exception:
|
|
continue
|
|
|
|
|
|
def hooks_for_integration(db, integration_id: int) -> HookManager:
|
|
# Load hooks config from Integration.config.hooks
|
|
from . import models
|
|
integ = db.query(models.Integration).filter_by(id=integration_id).first()
|
|
cfg = {}
|
|
if integ and integ.config:
|
|
try:
|
|
import json as _json
|
|
cfg = _json.loads(integ.config) or {}
|
|
except Exception:
|
|
cfg = {}
|
|
return HookManager(cfg.get('hooks'))
|