perf: Fix all 7 code analysis issues

HIGH (Performance):
- Replace 8 sync file operations with aiofiles in async context (storage, mail,
  report_generator, dms_bridge, ai_assistant)
- Frontend bundle splitting: manualChunks for react-vendor, ui-components, tanstack,
  markdown, icons, utils, i18n (ui chunk 936K → ~19K)

MEDIUM (Architecture):
- Worker circular deps: Replace direct plugin imports with job_registry.py pattern
  (register_job/get_all_jobs, importlib-based lazy loading)
- App-wide ErrorBoundary: New ErrorBoundary.tsx component, wrapped in AppShell
  and all standalone routes

LOW (Code Quality):
- N+1 query fix: selectinload(Contact.contact_persons) in list_contacts()
- O(n²) dedup fix: SQL GROUP BY for email/phone duplicates, Dict-based name grouping
- Response format standardization: 7 routes converted from plain arrays to
  {items: [...], total: N} format
This commit is contained in:
Agent Zero
2026-07-25 09:19:32 +02:00
parent 224a71ba56
commit aaa7406929
26 changed files with 436 additions and 225 deletions
@@ -8,6 +8,8 @@ import json
import os
import uuid
import aiofiles
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.responses import StreamingResponse
from sqlalchemy import select
@@ -122,15 +124,15 @@ def _generate_json(rendered: str) -> dict:
return {"result": rendered}
def _save_report_file(
async def _save_report_file(
tenant_id: uuid.UUID, report_id: uuid.UUID, ext: str, data: bytes
) -> str:
"""Save report data to disk and return the path."""
dir_path = os.path.join(REPORT_STORAGE_BASE, str(tenant_id))
os.makedirs(dir_path, exist_ok=True)
file_path = os.path.join(dir_path, f"{report_id}.{ext}")
with open(file_path, "wb") as f:
f.write(data)
async with aiofiles.open(file_path, "wb") as f:
await f.write(data)
return file_path
@@ -142,7 +144,8 @@ async def list_presets(
current_user: dict = Depends(require_permission("reports:read")),
):
"""List all available preset report templates."""
return get_preset_list()
items = get_preset_list()
return {"items": items, "total": len(items)}
@router.post("/presets/generate")
@@ -468,10 +471,10 @@ async def download_report(
media_type = media_types.get(ext, "application/octet-stream")
filename = f"report_{report.id}.{ext}"
def _iter_file():
with open(report.output_path, "rb") as f: # type: ignore[arg-type]
async def _iter_file():
async with aiofiles.open(report.output_path, "rb") as f: # type: ignore[arg-type]
while True:
chunk = f.read(65536)
chunk = await f.read(65536)
if not chunk:
break
yield chunk