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:
@@ -1,7 +1,7 @@
|
||||
"""Nomenclature API endpoints."""
|
||||
|
||||
from typing import Optional, List, Annotated
|
||||
from fastapi import APIRouter, Depends, HTTPException, Header
|
||||
from fastapi import APIRouter, Depends, HTTPException, Header, Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -190,14 +190,16 @@ async def get_cash_registers(
|
||||
|
||||
@router.post("/sync/suppliers", response_model=SyncResult)
|
||||
async def sync_suppliers(
|
||||
request: Request,
|
||||
company_id: Optional[int] = None,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
selected_company: SelectedCompany = None,
|
||||
):
|
||||
"""Manually trigger supplier sync from Oracle."""
|
||||
cid = company_id or selected_company
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
|
||||
synced, errors = await SyncService.sync_suppliers(session, cid)
|
||||
synced, errors = await SyncService.sync_suppliers(session, cid, server_id=server_id)
|
||||
|
||||
return SyncResult(
|
||||
synced=synced,
|
||||
@@ -208,14 +210,16 @@ async def sync_suppliers(
|
||||
|
||||
@router.post("/sync/cash-registers", response_model=SyncResult)
|
||||
async def sync_cash_registers(
|
||||
request: Request,
|
||||
company_id: Optional[int] = None,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
selected_company: SelectedCompany = None,
|
||||
):
|
||||
"""Manually trigger cash register sync from Oracle."""
|
||||
cid = company_id or selected_company
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
|
||||
synced, errors = await SyncService.sync_cash_registers(session, cid)
|
||||
synced, errors = await SyncService.sync_cash_registers(session, cid, server_id=server_id)
|
||||
|
||||
return SyncResult(
|
||||
synced=synced,
|
||||
@@ -226,18 +230,20 @@ async def sync_cash_registers(
|
||||
|
||||
@router.post("/sync/all", response_model=dict)
|
||||
async def sync_all_nomenclatures(
|
||||
request: Request,
|
||||
company_id: Optional[int] = None,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
selected_company: SelectedCompany = None,
|
||||
):
|
||||
"""Sync all nomenclatures (suppliers + cash registers) from Oracle."""
|
||||
cid = company_id or selected_company
|
||||
server_id = getattr(request.state, 'server_id', None)
|
||||
|
||||
# Sync suppliers
|
||||
suppliers_synced, suppliers_errors = await SyncService.sync_suppliers(session, cid)
|
||||
suppliers_synced, suppliers_errors = await SyncService.sync_suppliers(session, cid, server_id=server_id)
|
||||
|
||||
# Sync cash registers
|
||||
registers_synced, registers_errors = await SyncService.sync_cash_registers(session, cid)
|
||||
registers_synced, registers_errors = await SyncService.sync_cash_registers(session, cid, server_id=server_id)
|
||||
|
||||
return {
|
||||
"suppliers": {
|
||||
|
||||
Reference in New Issue
Block a user