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:
Claude Agent
2026-01-26 22:39:06 +00:00
parent 5f99ee2fd0
commit b137e80b71
102 changed files with 9398 additions and 2787 deletions

View File

@@ -59,6 +59,7 @@ logger = logging.getLogger(__name__)
telegram_bot_task = None
ocr_job_worker_running = False
cleanup_task_running = False
email_cache_running = False
# ============================================================================
@@ -68,8 +69,33 @@ cleanup_task_running = False
async def init_oracle_pool():
"""Initialize Oracle connection pool (shared by all modules)."""
logger.info("[ORACLE] Initializing connection pool...")
await oracle_pool.initialize()
logger.info("[ORACLE] ✅ Pool initialized successfully")
# Get configured servers
servers = settings.get_oracle_servers()
if servers:
# Multi-server mode: register all servers for lazy pool creation
logger.info(f"[ORACLE] Registering {len(servers)} servers for lazy pool creation:")
for srv in servers:
oracle_pool.register_server(
server_id=srv.id,
host=srv.host,
port=srv.port,
user=srv.user,
password=srv.password,
sid=srv.sid,
service_name=srv.service_name,
)
logger.info(f"[ORACLE] - {srv.id}: {srv.name} @ {srv.host}:{srv.port}")
# Mark as initialized (pools will be created lazily on first connection)
await oracle_pool.initialize()
else:
# Legacy single-server mode: initialize with env vars
logger.info("[ORACLE] Using legacy single-server configuration")
await oracle_pool.initialize()
logger.info("[ORACLE] ✅ Pool manager initialized successfully")
async def init_reports_cache():
@@ -188,6 +214,44 @@ async def init_cleanup_task():
cleanup_task_running = False
async def init_email_server_cache():
"""Initialize the email-server cache for multi-Oracle auto-discovery (US-003).
Builds a cache mapping emails to server IDs by querying CONTAFIN_ORACLE.UTILIZATORI
on each configured Oracle server. Starts auto-refresh every 15 minutes.
"""
global email_cache_running
# Only initialize if multi-server mode is configured
servers = settings.get_oracle_servers()
if not servers or len(servers) <= 1:
logger.info("[EMAIL-CACHE] Single-server mode, skipping email cache initialization")
return
logger.info("[EMAIL-CACHE] Initializing email-server cache...")
try:
from shared.auth.email_server_cache import (
email_server_cache,
build_email_cache,
start_email_cache_refresh
)
# Build initial cache
await build_email_cache()
# Start auto-refresh
await start_email_cache_refresh()
email_cache_running = True
stats = email_server_cache.get_cache_stats()
logger.info(f"[EMAIL-CACHE] ✅ Cache initialized: {stats['total_emails']} emails")
except Exception as e:
logger.warning(f"[EMAIL-CACHE] ⚠️ Cache init failed: {e}")
logger.warning("[EMAIL-CACHE] Multi-server email lookup will not be available")
email_cache_running = False
async def run_telegram_bot():
"""Run Telegram bot as background task."""
logger.info("[TELEGRAM] Starting bot...")
@@ -301,7 +365,10 @@ async def startup_event():
# Step 4: Initialize cleanup task for expired failed receipts (US-008)
await init_cleanup_task()
# Step 5: Start Telegram bot as background task
# Step 5: Initialize email-server cache for multi-Oracle (US-003)
await init_email_server_cache()
# Step 6: Start Telegram bot as background task
if settings.telegram_bot_token:
telegram_bot_task = asyncio.create_task(run_telegram_bot())
logger.info("[STARTUP] ✅ Telegram bot task created")
@@ -321,13 +388,24 @@ async def startup_event():
@app.on_event("shutdown")
async def shutdown_event():
"""Application shutdown - Cleanup resources."""
global telegram_bot_task, ocr_job_worker_running, cleanup_task_running
global telegram_bot_task, ocr_job_worker_running, cleanup_task_running, email_cache_running
logger.info("=" * 80)
logger.info("[SHUTDOWN] Stopping ROA2WEB Unified Backend...")
logger.info("=" * 80)
try:
# Stop email cache auto-refresh (US-003)
if email_cache_running:
logger.info("[SHUTDOWN] Stopping email cache auto-refresh...")
try:
from shared.auth.email_server_cache import stop_email_cache_refresh
await stop_email_cache_refresh()
email_cache_running = False
logger.info("[SHUTDOWN] Email cache stopped")
except Exception as e:
logger.error(f"[SHUTDOWN] Email cache error: {e}")
# Stop cleanup task (US-008)
if cleanup_task_running:
logger.info("[SHUTDOWN] Stopping cleanup task...")
@@ -402,7 +480,9 @@ app.add_middleware(
AuthenticationMiddleware,
excluded_paths=[
"/", "/docs", "/health", "/redoc", "/openapi.json",
"/api/auth/login", "/api/auth/refresh",
"/api/auth/login", "/api/auth/refresh", "/api/auth/check-email",
"/api/auth/check-identity", # US-013: Dual login support (email + username)
"/api/system/auth-mode", # Public endpoint for login mode detection
"/api/telegram/auth/verify-user",
"/api/telegram/auth/verify-email",
"/api/telegram/auth/login-with-email",