feat: multi-Oracle server support with runtime switching
Complete implementation of multi-server Oracle database support: Backend: - Multi-pool Oracle with lazy loading per server - Email-to-server cache for automatic server discovery - JWT tokens include server_id claim - /auth/check-identity and /auth/check-email endpoints - /auth/my-servers endpoint for listing user's accessible servers - Server switch with password re-authentication Frontend: - New ServerSelector component for header dropdown - Multi-step login flow (identity → server → password) - Server switching from header with password modal - Mobile drawer menu with server selection - Dark mode support for all new components - URL bookmark support with ?server= query param Scripts: - Unified start.sh replacing start-prod.sh/start-test.sh - Unified ssh-tunnel.sh with multi-server support - Updated status.sh for new architecture Tests: - E2E tests for multi-server and single-server login flows - Backend unit tests for all new endpoints - Oracle multi-pool integration tests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -44,15 +44,15 @@ class DashboardService:
|
||||
return cte_sql, params
|
||||
|
||||
@staticmethod
|
||||
@cached(cache_type='schema', key_params=['company_id'])
|
||||
async def _get_schema(company_id: int) -> str:
|
||||
@cached(cache_type='schema', key_params=['company_id', 'server_id'])
|
||||
async def _get_schema(company_id: int, server_id: Optional[str] = None) -> str:
|
||||
"""
|
||||
Obține schema pentru company_id (CACHED PERMANENT)
|
||||
|
||||
CRITICAL: Acest query este cel mai frecvent - executat la FIECARE request API.
|
||||
Cache permanent reduce queries cu 99.99%.
|
||||
"""
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
schema_query = """
|
||||
SELECT schema
|
||||
@@ -68,8 +68,8 @@ class DashboardService:
|
||||
return schema_result[0]
|
||||
|
||||
@staticmethod
|
||||
@cached(cache_type='dashboard_summary', key_params=['company', 'username', 'luna', 'an'])
|
||||
async def get_complete_summary(company: str, username: str, luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None) -> DashboardSummary:
|
||||
@cached(cache_type='dashboard_summary', key_params=['company', 'username', 'luna', 'an', 'server_id'])
|
||||
async def get_complete_summary(company: str, username: str, luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None, server_id: Optional[str] = None) -> DashboardSummary:
|
||||
"""
|
||||
Obține toate datele pentru dashboard într-un singur apel (CACHED 30 min)
|
||||
Execută 2 query-uri separate: facturi și trezorerie
|
||||
@@ -80,14 +80,15 @@ class DashboardService:
|
||||
luna: Luna contabilă (1-12), opțional
|
||||
an: Anul contabil, opțional
|
||||
request: Request object pentru cache metadata
|
||||
server_id: ID-ul serverului Oracle (pentru multi-server)
|
||||
"""
|
||||
company_id = int(company)
|
||||
schema = await DashboardService._get_schema(company_id)
|
||||
schema = await DashboardService._get_schema(company_id, server_id)
|
||||
|
||||
# Construiește CTE pentru perioada curentă
|
||||
period_cte, period_params = DashboardService._build_period_cte(schema, luna, an)
|
||||
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
|
||||
# Query 1: Statistici facturi cu breakdown pe perioade - FIXED ORA-00937
|
||||
@@ -565,8 +566,8 @@ class DashboardService:
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@cached(cache_type='dashboard_trends', key_params=['company_id', 'period', 'luna', 'an'])
|
||||
async def get_trends(company_id: int, period: str = "12m", luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None) -> Dict[str, Any]:
|
||||
@cached(cache_type='dashboard_trends', key_params=['company_id', 'period', 'luna', 'an', 'server_id'])
|
||||
async def get_trends(company_id: int, period: str = "12m", luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None, server_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Get comprehensive trend analysis data for all dashboard indicators (CACHED 30 min)
|
||||
|
||||
Args:
|
||||
@@ -575,11 +576,12 @@ class DashboardService:
|
||||
luna: Luna contabilă (1-12), opțional - dacă nu e specificată, folosește MAX
|
||||
an: Anul contabil, opțional - dacă nu e specificat, folosește MAX
|
||||
request: Request object pentru cache metadata
|
||||
server_id: ID-ul serverului Oracle (pentru multi-server)
|
||||
"""
|
||||
try:
|
||||
schema = await DashboardService._get_schema(company_id)
|
||||
schema = await DashboardService._get_schema(company_id, server_id)
|
||||
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
|
||||
# Determine current period from params or database
|
||||
@@ -962,7 +964,7 @@ class DashboardService:
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
async def get_detailed_data(company: str, data_type: str, luna: Optional[int] = None, an: Optional[int] = None, page: int = 1, page_size: int = 25, search: str = ""):
|
||||
async def get_detailed_data(company: str, data_type: str, luna: Optional[int] = None, an: Optional[int] = None, page: int = 1, page_size: int = 25, search: str = "", server_id: Optional[str] = None):
|
||||
"""
|
||||
Obține date detaliate pentru tabelele din dashboard
|
||||
Fixed to use existing vireg_parteneri view instead of missing tables
|
||||
@@ -975,9 +977,10 @@ class DashboardService:
|
||||
page: Pagina curentă
|
||||
page_size: Mărimea paginii
|
||||
search: Termen de căutare
|
||||
server_id: ID-ul serverului Oracle (pentru multi-server)
|
||||
"""
|
||||
logger.info(f"get_detailed_data called: company={company}, data_type={data_type}, luna={luna}, an={an}, page={page}")
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
try:
|
||||
# Get schema for company
|
||||
@@ -1168,14 +1171,15 @@ class DashboardService:
|
||||
return {"error": f"Database error: {str(e)}", "data": [], "total": 0}
|
||||
|
||||
@staticmethod
|
||||
async def get_performance_data(company_id: int, period: str = "7d") -> Dict[str, Any]:
|
||||
async def get_performance_data(company_id: int, period: str = "7d", server_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Calculează performanța încasări/plăți pentru perioada selectată
|
||||
|
||||
|
||||
Args:
|
||||
company_id: ID-ul companiei
|
||||
period: Perioada ("7d", "1m", "3m", "6m", "ytd", "12m")
|
||||
|
||||
server_id: ID-ul serverului Oracle (pentru multi-server)
|
||||
|
||||
Returns:
|
||||
{
|
||||
labels: List[str] - etichete pentru perioadele de timp
|
||||
@@ -1190,7 +1194,7 @@ class DashboardService:
|
||||
}
|
||||
"""
|
||||
try:
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
# Get schema
|
||||
schema_query = "SELECT schema FROM CONTAFIN_ORACLE.v_nom_firme WHERE id_firma = :company_id"
|
||||
@@ -1262,14 +1266,15 @@ class DashboardService:
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
async def get_cashflow_forecast(company_id: int, period: str = "7d") -> Dict[str, Any]:
|
||||
async def get_cashflow_forecast(company_id: int, period: str = "7d", server_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Calculează previziunea cash flow bazată pe scadențe
|
||||
|
||||
|
||||
Args:
|
||||
company_id: ID-ul companiei
|
||||
period: Perioada ("7d", "1m", "3m", "6m")
|
||||
|
||||
server_id: ID-ul serverului Oracle (pentru multi-server)
|
||||
|
||||
Returns:
|
||||
{
|
||||
periods: List[str] - perioadele de timp
|
||||
@@ -1282,7 +1287,7 @@ class DashboardService:
|
||||
}
|
||||
"""
|
||||
try:
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
# Get schema
|
||||
schema_query = "SELECT schema FROM CONTAFIN_ORACLE.v_nom_firme WHERE id_firma = :company_id"
|
||||
@@ -1347,8 +1352,8 @@ class DashboardService:
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
@cached(cache_type='maturity_analysis', key_params=['company_id', 'period', 'luna', 'an'])
|
||||
async def get_maturity_analysis(company_id: int, period: str = "7d", luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None) -> Dict[str, Any]:
|
||||
@cached(cache_type='maturity_analysis', key_params=['company_id', 'period', 'luna', 'an', 'server_id'])
|
||||
async def get_maturity_analysis(company_id: int, period: str = "7d", luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None, server_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Analizează scadențele clienți vs furnizori cu date reale din Oracle (CACHED 30 min)
|
||||
|
||||
@@ -1357,6 +1362,7 @@ class DashboardService:
|
||||
period: Perioada ("7d", "1m", "3m", "6m", "12m", "over12m")
|
||||
luna: Luna contabilă (1-12), opțional
|
||||
an: Anul contabil, opțional
|
||||
server_id: ID-ul serverului Oracle (pentru multi-server)
|
||||
|
||||
Returns:
|
||||
{
|
||||
@@ -1367,7 +1373,7 @@ class DashboardService:
|
||||
}
|
||||
"""
|
||||
try:
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
# Get schema
|
||||
schema_query = "SELECT schema FROM CONTAFIN_ORACLE.v_nom_firme WHERE id_firma = :company_id"
|
||||
@@ -1546,8 +1552,8 @@ class DashboardService:
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
@cached(cache_type='monthly_flows', key_params=['company', 'luna', 'an'])
|
||||
async def get_monthly_flows(company: int, luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None) -> Dict[str, Any]:
|
||||
@cached(cache_type='monthly_flows', key_params=['company', 'luna', 'an', 'server_id'])
|
||||
async def get_monthly_flows(company: int, luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None, server_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Obține fluxurile lunare de intrare și ieșire pentru luna curentă (CACHED 30 min)
|
||||
|
||||
@@ -1556,9 +1562,10 @@ class DashboardService:
|
||||
luna: Luna contabilă (1-12), opțional
|
||||
an: Anul contabil, opțional
|
||||
request: Request object pentru cache metadata
|
||||
server_id: ID-ul serverului Oracle (pentru multi-server)
|
||||
"""
|
||||
try:
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
# Obține schema
|
||||
company_id = company
|
||||
@@ -1640,8 +1647,8 @@ class DashboardService:
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
@cached(cache_type='treasury_breakdown', key_params=['company', 'luna', 'an'])
|
||||
async def get_treasury_breakdown(company: int, luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None) -> Dict[str, Any]:
|
||||
@cached(cache_type='treasury_breakdown', key_params=['company', 'luna', 'an', 'server_id'])
|
||||
async def get_treasury_breakdown(company: int, luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None, server_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Obține breakdown-ul trezoreriei pe casă și bancă (CACHED 30 min)
|
||||
|
||||
@@ -1649,9 +1656,10 @@ class DashboardService:
|
||||
company: ID-ul firmei
|
||||
luna: Luna contabilă (1-12), opțional
|
||||
an: Anul contabil, opțional
|
||||
server_id: ID-ul serverului Oracle (pentru multi-server)
|
||||
"""
|
||||
try:
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
# Obține schema
|
||||
company_id = company
|
||||
@@ -1745,8 +1753,8 @@ class DashboardService:
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
@cached(cache_type='net_balance_breakdown', key_params=['company', 'luna', 'an'])
|
||||
async def get_net_balance_breakdown(company: int, luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None) -> Dict[str, Any]:
|
||||
@cached(cache_type='net_balance_breakdown', key_params=['company', 'luna', 'an', 'server_id'])
|
||||
async def get_net_balance_breakdown(company: int, luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None, server_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Obține breakdown-ul balanței nete pe clienți și furnizori cu detaliere pe perioade (CACHED 30 min)
|
||||
|
||||
@@ -1754,9 +1762,10 @@ class DashboardService:
|
||||
company: ID-ul firmei
|
||||
luna: Luna contabilă (1-12), opțional
|
||||
an: Anul contabil, opțional
|
||||
server_id: ID-ul serverului Oracle (pentru multi-server)
|
||||
"""
|
||||
try:
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
# Obține schema
|
||||
company_id = company
|
||||
@@ -1938,12 +1947,13 @@ class DashboardService:
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
async def get_current_period(company: int) -> Dict[str, Any]:
|
||||
async def get_current_period(company: int, server_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Obține perioada curentă (an și lună) din calendarul Oracle
|
||||
|
||||
Args:
|
||||
company: ID-ul companiei
|
||||
server_id: ID-ul serverului Oracle (pentru multi-server)
|
||||
|
||||
Returns:
|
||||
{
|
||||
@@ -1953,7 +1963,7 @@ class DashboardService:
|
||||
}
|
||||
"""
|
||||
try:
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
async with oracle_pool.get_connection(server_id) as connection:
|
||||
with connection.cursor() as cursor:
|
||||
# Obține schema
|
||||
company_id = company
|
||||
|
||||
Reference in New Issue
Block a user