Files
roa2web-service-auto/reports-app/backend/app/main.py
Marius Mutu 1378ee1e6a Implement hybrid two-tier cache system with full monitoring and Telegram bot enhancements
Cache System (Backend):
- Implemented two-tier hybrid cache: L1 (in-memory) + L2 (SQLite)
- L1 cache: Fast dictionary-based with 5-minute TTL for hot data
- L2 cache: Persistent SQLite with 1-hour TTL for warm data
- Cache decorator with automatic tier management and fallback
- Cache key generation with per-user isolation
- Event monitoring system for cache statistics
- Cache benchmarking utilities for performance testing
- Added cache management endpoints: /api/cache/stats, /api/cache/clear, /api/cache/benchmark
- Cache configuration via environment variables (CACHE_ENABLED, CACHE_L1_TTL, etc.)

Backend Services:
- Updated dashboard_service to use @cached decorator with request context
- Added cache support to invoice_service and treasury_service
- Integrated cache manager into main.py with lifespan events
- Added Request parameter to service methods for cache metadata

Frontend Enhancements:
- New CacheStatsView.vue for real-time cache monitoring dashboard
- Cache store (cacheStore.js) for state management
- Updated router to include /cache-stats route
- Navigation updates in DashboardHeader and HamburgerMenu
- Cache stats accessible from main navigation

Telegram Bot Improvements:
- Enhanced formatters with YTD comparison data
- Improved menu navigation and button layout
- Better error handling and user feedback
- Bot startup improvements with graceful shutdown

Auth & Middleware:
- Enhanced middleware with cache metadata injection
- Improved request state handling for cache source tracking

Development:
- Updated start-dev.sh with better error handling
- Added TELEGRAM_EMAIL_AUTH_PLAN.md documentation
- Updated requirements.txt with aiosqlite for async SQLite

Performance:
- L1 cache provides <1ms response for hot data
- L2 cache provides ~5ms response for warm data
- Database queries only for cold data or cache misses
- Cache hit rates tracked and displayed in real-time

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-07 22:42:00 +02:00

333 lines
13 KiB
Python

"""
ROA Reports API - FastAPI Backend
Aplicația principală pentru rapoarte facturi și încasări
"""
from fastapi import FastAPI, Depends
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
import sys
import os
from datetime import datetime
from dotenv import load_dotenv
# Încărcare environment variables din .env
load_dotenv()
# Configurare TNS_ADMIN pentru Oracle
tns_path = os.path.join(os.path.dirname(__file__), '../../../../app')
os.environ['TNS_ADMIN'] = tns_path
# Adăugare path pentru shared modules
sys.path.append(os.path.join(os.path.dirname(__file__), '../../../shared'))
from database.oracle_pool import oracle_pool
from auth.middleware import AuthenticationMiddleware
# from auth.routes import create_auth_router # Fixed inline
# Import routere locale
from app.routers import invoices, dashboard, treasury, companies, telegram, cache
# Auth endpoints pentru test
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from datetime import datetime, timedelta
import jwt
import logging
logger = logging.getLogger(__name__)
# JWT Setup
JWT_SECRET = os.getenv("JWT_SECRET_KEY", "test-secret-key")
JWT_ALGORITHM = "HS256"
JWT_EXPIRE_MINUTES = 30
class LoginRequest(BaseModel):
username: str
password: str
class LoginResponse(BaseModel):
access_token: str
refresh_token: str # Added refresh token
token_type: str
user: dict
def create_auth_router():
"""Create authentication router for testing"""
auth_router = APIRouter(tags=["authentication"])
@auth_router.post("/login", response_model=LoginResponse)
async def login(credentials: LoginRequest):
"""Autentificare utilizator prin Oracle database"""
try:
async with oracle_pool.get_connection() as connection:
with connection.cursor() as cursor:
# Call verificautilizator procedure using SELECT
cursor.execute("""
SELECT pack_drepturi.verificautilizator(:username, :password)
FROM DUAL
""", {
'username': credentials.username.upper(),
'password': credentials.password
})
result = cursor.fetchone()
verification_result = result[0] if result else -1
# Check if authentication was successful
if verification_result == -1:
raise HTTPException(status_code=401, detail="Invalid username or password")
# Get user companies - first get user ID from UTILIZATORI
cursor.execute("""
SELECT ID_UTIL, UTILIZATOR
FROM UTILIZATORI
WHERE UPPER(UTILIZATOR) = :username
""", {'username': credentials.username.upper()})
user_row = cursor.fetchone()
if not user_row:
raise HTTPException(status_code=401, detail="User not found in system")
user_id = user_row[0]
# Now get companies using the correct query structure
cursor.execute("""
SELECT A.ID_FIRMA, A.FIRMA
FROM V_NOM_FIRME A
WHERE A.ID_FIRMA IN (
SELECT ID_FIRMA
FROM VDEF_UTIL_FIRME
WHERE ID_PROGRAM = 2
AND ID_UTIL = :user_id
)
ORDER BY A.FIRMA
""", {'user_id': user_id})
companies_result = cursor.fetchall()
if not companies_result:
# Don't fail login if no companies - let frontend show message
companies = []
else:
companies = [str(row[0]) for row in companies_result]
# Create JWT token with all required fields
now = datetime.utcnow()
expire = now + timedelta(minutes=JWT_EXPIRE_MINUTES)
token_data = {
"username": credentials.username, # Changed from "sub" to "username"
"user_id": user_id, # Include user_id from database
"companies": companies,
"permissions": ["read", "reports"], # Default permissions
"exp": expire,
"iat": now, # Added issued at time
"type": "access" # Added token type
}
access_token = jwt.encode(token_data, JWT_SECRET, algorithm=JWT_ALGORITHM)
# Create refresh token
refresh_expire = now + timedelta(days=7)
refresh_token_data = {
"username": credentials.username,
"user_id": user_id,
"companies": companies,
"permissions": ["read", "reports"],
"exp": refresh_expire,
"iat": now,
"type": "refresh"
}
refresh_token = jwt.encode(refresh_token_data, JWT_SECRET, algorithm=JWT_ALGORITHM)
return LoginResponse(
access_token=access_token,
refresh_token=refresh_token, # Include refresh token
token_type="bearer",
user={
"username": credentials.username,
"user_id": user_id, # Include user_id
"companies": companies,
"permissions": ["read", "reports"] # Include permissions
}
)
except Exception as e:
logger.error(f"Login error: {str(e)}")
raise HTTPException(status_code=500, detail="Internal authentication error")
return auth_router
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Lifecycle events pentru aplicație"""
# Startup - Initialize Oracle connection pool
await oracle_pool.initialize()
logger.info("[ROA Reports API] Oracle pool initialized")
# Initialize cache system
from app.cache import init_cache, run_baseline_benchmarks, init_event_monitor, get_cache
from app.cache.config import CacheConfig
try:
cache_config = CacheConfig.from_env()
await init_cache(cache_config)
logger.info(f"[ROA Reports API] Cache initialized: type={cache_config.cache_type}, enabled={cache_config.enabled}")
# Run baseline benchmarks (optional, based on config)
if cache_config.benchmark_on_startup:
logger.info("[ROA Reports API] Running baseline performance benchmarks...")
benchmarks = await run_baseline_benchmarks()
logger.info(f"[ROA Reports API] Benchmarks completed: {len(benchmarks)} types measured")
# Initialize event monitor
cache = get_cache()
await init_event_monitor(cache, cache_config)
if cache_config.auto_invalidate_enabled:
logger.info("[ROA Reports API] Event-based auto-invalidation ENABLED")
else:
logger.info("[ROA Reports API] Event-based auto-invalidation DISABLED")
except Exception as e:
logger.error(f"[ROA Reports API] Cache initialization error: {e}", exc_info=True)
logger.warning("[ROA Reports API] Continuing without cache")
logger.info("[ROA Reports API] Started successfully")
yield
# Shutdown
from app.cache import close_cache, get_event_monitor
# Stop event monitor
try:
monitor = get_event_monitor()
if monitor:
await monitor.stop()
logger.info("[ROA Reports API] Event monitor stopped")
except Exception as e:
logger.error(f"[ROA Reports API] Event monitor shutdown error: {e}")
# Close cache
try:
await close_cache()
logger.info("[ROA Reports API] Cache closed")
except Exception as e:
logger.error(f"[ROA Reports API] Cache shutdown error: {e}")
await oracle_pool.close_pool()
logger.info("[ROA Reports API] Stopped")
app = FastAPI(
title="ROA Reports API",
description="API pentru rapoarte ERP - facturi, încasări și alte rapoarte financiare",
version="1.0.0",
# lifespan=lifespan # Using event handlers instead due to uvicorn compatibility issues
)
# STARTUP EVENT HANDLER (alternative to lifespan)
@app.on_event("startup")
async def startup_event():
"""Application startup - Initialize Oracle pool and cache"""
print("=" * 80, flush=True)
print("[STARTUP] Initializing Oracle pool...", flush=True)
logger.critical("=" * 80)
logger.critical("[STARTUP] Initializing Oracle pool...")
await oracle_pool.initialize()
print("[STARTUP] Oracle pool initialized", flush=True)
logger.critical("[STARTUP] Oracle pool initialized")
print("[STARTUP] Initializing cache system...", flush=True)
logger.critical("[STARTUP] Initializing cache system...")
from app.cache import init_cache, init_event_monitor, get_cache
from app.cache.config import CacheConfig
try:
cache_config = CacheConfig.from_env()
await init_cache(cache_config)
print(f"[STARTUP] Cache initialized: type={cache_config.cache_type}, enabled={cache_config.enabled}", flush=True)
logger.critical(f"[STARTUP] Cache initialized: type={cache_config.cache_type}, enabled={cache_config.enabled}")
# Initialize event monitor
cache = get_cache()
await init_event_monitor(cache, cache_config)
if cache_config.auto_invalidate_enabled:
logger.info("[STARTUP] Event-based auto-invalidation ENABLED")
else:
logger.info("[STARTUP] Event-based auto-invalidation DISABLED")
except Exception as e:
logger.error(f"[STARTUP] Cache initialization error: {e}", exc_info=True)
logger.warning("[STARTUP] Continuing without cache")
logger.info("[STARTUP] ROA Reports API started successfully")
logger.info("=" * 80)
# SHUTDOWN EVENT HANDLER
@app.on_event("shutdown")
async def shutdown_event():
"""Application shutdown - Cleanup resources"""
logger.info("[SHUTDOWN] Stopping event monitor...")
from app.cache import close_cache, get_event_monitor
try:
monitor = get_event_monitor()
if monitor:
await monitor.stop()
logger.info("[SHUTDOWN] Event monitor stopped")
except Exception as e:
logger.error(f"[SHUTDOWN] Event monitor error: {e}")
try:
await close_cache()
logger.info("[SHUTDOWN] Cache closed")
except Exception as e:
logger.error(f"[SHUTDOWN] Cache error: {e}")
await oracle_pool.close_pool()
logger.info("[SHUTDOWN] Oracle pool closed")
logger.info("[SHUTDOWN] ROA Reports API stopped")
# CORS pentru frontend Vue.js
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Allow all origins for production deployment
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Authentication middleware
app.add_middleware(
AuthenticationMiddleware,
excluded_paths=[
"/", "/docs", "/health", "/api/auth/login", "/redoc", "/openapi.json",
"/api/telegram/auth/verify-user", # Public endpoint for Telegram bot
"/api/telegram/auth/refresh-token", # Public endpoint for token refresh
"/api/telegram/health" # Health check for Telegram router
]
)
# Include routere with /api prefix
auth_router = create_auth_router()
app.include_router(auth_router, prefix="/api/auth", tags=["authentication"])
app.include_router(companies.router, prefix="/api/companies", tags=["companies"])
app.include_router(invoices.router, prefix="/api/invoices", tags=["invoices"])
app.include_router(dashboard.router, prefix="/api/dashboard", tags=["dashboard"])
app.include_router(treasury.router, prefix="/api/treasury", tags=["treasury"])
app.include_router(telegram.router, prefix="/api/telegram", tags=["telegram"])
app.include_router(cache.router, prefix="/api", tags=["cache"])
@app.get("/")
async def root():
print("[MAIN DEBUG] Root endpoint accessed")
return {"message": "ROA Reports API", "version": "1.0.0", "status": "running"}
@app.get("/health")
async def health_check():
# Test database connection
try:
async with oracle_pool.get_connection() as conn:
with conn.cursor() as cursor:
cursor.execute("SELECT 1 FROM DUAL")
return {"api": "healthy", "database": "connected", "timestamp": datetime.utcnow().isoformat()}
except Exception as e:
return {"api": "healthy", "database": f"error: {str(e)}", "timestamp": datetime.utcnow().isoformat()}