Consolidate 3 separate applications (reports-app, data-entry-app, telegram-bot) into a unified
architecture with single backend and frontend:
Backend Changes:
- Unified FastAPI backend at backend/ with modular structure
- Modules: reports, data_entry, telegram in backend/modules/
- Centralized config.py and main.py with all routers registered
- Single worker mode (--workers 1) for Telegram bot compatibility
- Shared Oracle connection pool and JWT authentication
- Unified requirements.txt and environment configuration
Frontend Changes:
- Single Vue.js SPA with module-based routing
- Unified frontend at src/ with modules in src/modules/{reports,data-entry}/
- Shared components and stores in src/shared/
- Error boundaries for module isolation
- Dual API proxy in Vite for module communication
Infrastructure:
- New unified startup scripts: start-prod.sh, start-test.sh, start-backend.sh
- Environment templates: .env.dev.example, .env.test.example, .env.prod.example
- Updated deployment scripts for Windows IIS
- Simplified SSH tunnel management
Documentation:
- Comprehensive CLAUDE.md with architecture overview
- Module-specific docs in docs/{data-entry,telegram}/
- Architecture decision records in docs/ARCHITECTURE-DECISIONS.md
- Deployment guides consolidated in deployment/windows/docs/
This migration reduces complexity, improves maintainability, and enables easier
deployment while maintaining all existing functionality.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
399 lines
12 KiB
Python
399 lines
12 KiB
Python
"""
|
|
API Router pentru managementul cache-ului
|
|
"""
|
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
from pydantic import BaseModel
|
|
from typing import Optional, Dict, Any
|
|
# import sys # Removed - no longer needed
|
|
import os
|
|
import time
|
|
from datetime import datetime, timedelta
|
|
|
|
|
|
from shared.auth.dependencies import get_current_user
|
|
from shared.auth.models import CurrentUser
|
|
from ..cache import get_cache, get_event_monitor, toggle_event_monitor
|
|
|
|
router = APIRouter(prefix="/cache", tags=["cache"])
|
|
|
|
|
|
# Pydantic Models
|
|
|
|
class CacheStatsResponse(BaseModel):
|
|
"""Răspuns statistici cache"""
|
|
enabled: bool
|
|
global_enabled: bool
|
|
user_enabled: bool
|
|
cache_type: str
|
|
hit_rate: float
|
|
total_hits: int
|
|
total_misses: int
|
|
queries_saved: Dict[str, int]
|
|
response_times: Dict[str, Dict[str, Any]]
|
|
cache_size: Dict[str, int]
|
|
auto_invalidate: bool
|
|
last_cleanup: Optional[str] = None
|
|
|
|
|
|
class InvalidateCacheRequest(BaseModel):
|
|
"""Request pentru invalidare cache"""
|
|
company_id: Optional[int] = None
|
|
cache_type: Optional[str] = None
|
|
|
|
|
|
class ToggleUserCacheRequest(BaseModel):
|
|
"""Request pentru toggle cache per-user"""
|
|
enabled: bool
|
|
|
|
|
|
class ToggleGlobalCacheRequest(BaseModel):
|
|
"""Request pentru toggle cache global"""
|
|
enabled: bool
|
|
|
|
|
|
class ToggleAutoInvalidateRequest(BaseModel):
|
|
"""Request pentru toggle auto-invalidation"""
|
|
enabled: bool
|
|
|
|
|
|
# Helper Functions
|
|
|
|
async def _calculate_cache_stats() -> Dict[str, Any]:
|
|
"""Calculate comprehensive cache statistics"""
|
|
cache = get_cache()
|
|
if not cache:
|
|
raise HTTPException(status_code=503, detail="Cache not initialized")
|
|
|
|
# Get basic cache stats
|
|
stats = await cache.get_stats()
|
|
|
|
# Calculate hit rate
|
|
memory_stats = stats.get('memory', {})
|
|
total_hits = memory_stats.get('hits', 0)
|
|
total_misses = memory_stats.get('misses', 0)
|
|
total_requests = total_hits + total_misses
|
|
hit_rate = (total_hits / total_requests * 100) if total_requests > 0 else 0
|
|
|
|
# Calculate queries saved (from performance_log)
|
|
queries_saved = await _calculate_queries_saved(cache)
|
|
|
|
# Calculate response times per cache type
|
|
response_times = await _calculate_response_times(cache)
|
|
|
|
# Get cache sizes
|
|
cache_size = {
|
|
'memory': memory_stats.get('size', 0),
|
|
'sqlite': stats.get('sqlite', {}).get('active_entries', 0)
|
|
}
|
|
|
|
# Get event monitor status
|
|
monitor = get_event_monitor()
|
|
auto_invalidate = monitor.running if monitor else False
|
|
|
|
return {
|
|
'enabled': cache.config.enabled,
|
|
'global_enabled': cache.config.enabled,
|
|
'cache_type': cache.config.cache_type,
|
|
'hit_rate': round(hit_rate, 1),
|
|
'total_hits': total_hits,
|
|
'total_misses': total_misses,
|
|
'queries_saved': queries_saved,
|
|
'response_times': response_times,
|
|
'cache_size': cache_size,
|
|
'auto_invalidate': auto_invalidate,
|
|
'last_cleanup': None # TODO: track last cleanup time
|
|
}
|
|
|
|
|
|
async def _calculate_queries_saved(cache) -> Dict[str, int]:
|
|
"""Calculate queries saved by time period"""
|
|
import aiosqlite
|
|
|
|
try:
|
|
async with aiosqlite.connect(cache.sqlite.db_path) as db:
|
|
now = time.time()
|
|
today_start = now - 86400 # 24 hours
|
|
week_start = now - 604800 # 7 days
|
|
|
|
# Today
|
|
async with db.execute("""
|
|
SELECT COUNT(*) FROM performance_log
|
|
WHERE cache_hit = 1 AND timestamp >= ?
|
|
""", (today_start,)) as cursor:
|
|
today = (await cursor.fetchone())[0]
|
|
|
|
# This week
|
|
async with db.execute("""
|
|
SELECT COUNT(*) FROM performance_log
|
|
WHERE cache_hit = 1 AND timestamp >= ?
|
|
""", (week_start,)) as cursor:
|
|
week = (await cursor.fetchone())[0]
|
|
|
|
# All time
|
|
async with db.execute("""
|
|
SELECT COUNT(*) FROM performance_log
|
|
WHERE cache_hit = 1
|
|
""") as cursor:
|
|
total = (await cursor.fetchone())[0]
|
|
|
|
return {
|
|
'today': today,
|
|
'week': week,
|
|
'total': total
|
|
}
|
|
except Exception as e:
|
|
return {'today': 0, 'week': 0, 'total': 0}
|
|
|
|
|
|
async def _calculate_response_times(cache) -> Dict[str, Dict[str, Any]]:
|
|
"""Calculate average response times per cache type"""
|
|
import aiosqlite
|
|
|
|
try:
|
|
async with aiosqlite.connect(cache.sqlite.db_path) as db:
|
|
# Get average times per cache type
|
|
async with db.execute("""
|
|
SELECT
|
|
cache_type,
|
|
AVG(CASE WHEN cache_hit = 1 THEN response_time_ms ELSE NULL END) as avg_cached,
|
|
AVG(CASE WHEN cache_hit = 0 THEN response_time_ms ELSE NULL END) as avg_oracle
|
|
FROM performance_log
|
|
WHERE timestamp >= ?
|
|
GROUP BY cache_type
|
|
""", (time.time() - 86400,)) as cursor: # Last 24 hours
|
|
results = await cursor.fetchall()
|
|
|
|
response_times = {}
|
|
for row in results:
|
|
cache_type, avg_cached, avg_oracle = row
|
|
if avg_cached and avg_oracle:
|
|
improvement = int((avg_oracle - avg_cached) / avg_oracle * 100)
|
|
response_times[cache_type] = {
|
|
'cached': int(avg_cached),
|
|
'oracle': int(avg_oracle),
|
|
'improvement': improvement
|
|
}
|
|
|
|
return response_times
|
|
except Exception as e:
|
|
return {}
|
|
|
|
|
|
# API Endpoints
|
|
|
|
@router.get("/stats", response_model=CacheStatsResponse)
|
|
async def get_cache_stats(
|
|
current_user: CurrentUser = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Obține statistici complete cache
|
|
|
|
Returns:
|
|
- Hit rate, queries saved, response times
|
|
- Cache sizes (memory + SQLite)
|
|
- Auto-invalidation status
|
|
- Per-user cache setting
|
|
"""
|
|
try:
|
|
cache = get_cache()
|
|
if not cache:
|
|
raise HTTPException(status_code=503, detail="Cache not initialized")
|
|
|
|
# Get base stats
|
|
stats = await _calculate_cache_stats()
|
|
|
|
# Add user-specific setting
|
|
user_enabled = await cache.is_enabled_for_user(current_user.username)
|
|
stats['user_enabled'] = user_enabled
|
|
|
|
return CacheStatsResponse(**stats)
|
|
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Error retrieving cache stats: {str(e)}")
|
|
|
|
|
|
@router.post("/invalidate")
|
|
async def invalidate_cache(
|
|
request: InvalidateCacheRequest,
|
|
current_user: CurrentUser = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Invalidează cache
|
|
|
|
Args:
|
|
company_id: Opțional - invalidează doar pentru această companie
|
|
cache_type: Opțional - invalidează doar acest tip de cache
|
|
|
|
Returns:
|
|
Message de confirmare
|
|
"""
|
|
try:
|
|
cache = get_cache()
|
|
if not cache:
|
|
raise HTTPException(status_code=503, detail="Cache not initialized")
|
|
|
|
await cache.invalidate(
|
|
company_id=request.company_id,
|
|
cache_type=request.cache_type
|
|
)
|
|
|
|
if request.company_id and request.cache_type:
|
|
message = f"Cache invalidated for company {request.company_id}, type {request.cache_type}"
|
|
elif request.company_id:
|
|
message = f"Cache invalidated for company {request.company_id}"
|
|
elif request.cache_type:
|
|
message = f"Cache invalidated for type {request.cache_type}"
|
|
else:
|
|
message = "All cache invalidated"
|
|
|
|
return {
|
|
"success": True,
|
|
"message": message,
|
|
"invalidated_at": datetime.now().isoformat()
|
|
}
|
|
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Error invalidating cache: {str(e)}")
|
|
|
|
|
|
@router.post("/toggle-user")
|
|
async def toggle_user_cache(
|
|
request: ToggleUserCacheRequest,
|
|
current_user: CurrentUser = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Toggle cache per-user
|
|
|
|
Permite utilizatorului să activeze/dezactiveze cache-ul pentru el
|
|
Folosit pentru A/B testing și comparații de performanță
|
|
|
|
Args:
|
|
enabled: True pentru activare, False pentru dezactivare
|
|
|
|
Returns:
|
|
Noul status
|
|
"""
|
|
try:
|
|
cache = get_cache()
|
|
if not cache:
|
|
raise HTTPException(status_code=503, detail="Cache not initialized")
|
|
|
|
await cache.set_user_cache_enabled(current_user.username, request.enabled)
|
|
|
|
return {
|
|
"success": True,
|
|
"username": current_user.username,
|
|
"cache_enabled": request.enabled,
|
|
"message": f"Cache {'enabled' if request.enabled else 'disabled'} for user {current_user.username}"
|
|
}
|
|
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Error toggling user cache: {str(e)}")
|
|
|
|
|
|
@router.post("/toggle-global")
|
|
async def toggle_global_cache(
|
|
request: ToggleGlobalCacheRequest,
|
|
current_user: CurrentUser = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Toggle cache global (ADMIN only)
|
|
|
|
Activează/dezactivează cache-ul la nivel global pentru toți utilizatorii
|
|
|
|
Args:
|
|
enabled: True pentru activare, False pentru dezactivare
|
|
|
|
Returns:
|
|
Noul status global
|
|
"""
|
|
try:
|
|
# TODO: Add admin permission check
|
|
# For now, allow any authenticated user
|
|
|
|
cache = get_cache()
|
|
if not cache:
|
|
raise HTTPException(status_code=503, detail="Cache not initialized")
|
|
|
|
# Update config (NOTE: This is runtime only, .env needs manual update)
|
|
cache.config.enabled = request.enabled
|
|
|
|
return {
|
|
"success": True,
|
|
"global_enabled": request.enabled,
|
|
"message": f"Cache {'enabled' if request.enabled else 'disabled'} globally",
|
|
"note": "This change is runtime only. Update .env CACHE_ENABLED for persistence."
|
|
}
|
|
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Error toggling global cache: {str(e)}")
|
|
|
|
|
|
@router.post("/toggle-auto-invalidate")
|
|
async def toggle_auto_invalidation(
|
|
request: ToggleAutoInvalidateRequest,
|
|
current_user: CurrentUser = Depends(get_current_user)
|
|
):
|
|
"""
|
|
Toggle auto-invalidation monitoring
|
|
|
|
Activează/dezactivează monitorizarea automată a {schema}.act
|
|
pentru invalidarea cache-ului când se detectează modificări
|
|
|
|
Args:
|
|
enabled: True pentru activare, False pentru dezactivare
|
|
|
|
Returns:
|
|
Noul status auto-invalidation
|
|
"""
|
|
try:
|
|
# TODO: Add admin permission check
|
|
# For now, allow any authenticated user
|
|
|
|
await toggle_event_monitor(request.enabled)
|
|
|
|
return {
|
|
"success": True,
|
|
"auto_invalidate_enabled": request.enabled,
|
|
"message": f"Auto-invalidation {'enabled' if request.enabled else 'disabled'}",
|
|
"note": "Monitors max(id_act) in {schema}.act tables for changes"
|
|
}
|
|
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Error toggling auto-invalidation: {str(e)}")
|
|
|
|
|
|
@router.get("/health")
|
|
async def cache_health():
|
|
"""
|
|
Health check pentru sistemul de cache
|
|
|
|
Returns:
|
|
Status cache, mărime, și uptime
|
|
"""
|
|
try:
|
|
cache = get_cache()
|
|
if not cache:
|
|
return {
|
|
"status": "not_initialized",
|
|
"enabled": False
|
|
}
|
|
|
|
stats = await cache.get_stats()
|
|
monitor = get_event_monitor()
|
|
|
|
return {
|
|
"status": "healthy",
|
|
"enabled": cache.config.enabled,
|
|
"cache_type": cache.config.cache_type,
|
|
"memory_size": stats.get('memory', {}).get('size', 0),
|
|
"sqlite_size": stats.get('sqlite', {}).get('active_entries', 0),
|
|
"auto_invalidate_running": monitor.running if monitor else False
|
|
}
|
|
|
|
except Exception as e:
|
|
return {
|
|
"status": "error",
|
|
"error": str(e)
|
|
}
|