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:
@@ -6,6 +6,7 @@ from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
import aiofiles
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile
|
||||
from fastapi.responses import FileResponse, StreamingResponse
|
||||
from sqlalchemy import select
|
||||
@@ -407,7 +408,8 @@ async def list_tools(
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
registry = get_tool_registry()
|
||||
return registry.list_for_api()
|
||||
items = registry.list_for_api()
|
||||
return {"items": items, "total": len(items)}
|
||||
|
||||
|
||||
# ─── Chat Sessions ───
|
||||
@@ -717,8 +719,8 @@ async def upload_attachment(
|
||||
file_id = str(uuid.uuid4())
|
||||
safe_filename = file.filename or "unnamed"
|
||||
storage_path = str(ATTACHMENT_DIR / f"{file_id}_{safe_filename}")
|
||||
with open(storage_path, "wb") as f:
|
||||
f.write(content)
|
||||
async with aiofiles.open(storage_path, "wb") as f:
|
||||
await f.write(content)
|
||||
|
||||
attachment = AIChatAttachment(
|
||||
session_id=session.id,
|
||||
|
||||
@@ -12,6 +12,8 @@ import logging
|
||||
import uuid
|
||||
from typing import Any, AsyncGenerator
|
||||
|
||||
import aiofiles
|
||||
|
||||
import litellm
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -380,8 +382,8 @@ async def _extract_attachment_content(
|
||||
parts: list[str] = []
|
||||
for att in attachments:
|
||||
try:
|
||||
with open(att.storage_path, "rb") as f:
|
||||
content = f.read()
|
||||
async with aiofiles.open(att.storage_path, "rb") as f:
|
||||
content = await f.read()
|
||||
|
||||
text_content = ""
|
||||
mime = att.mime_type.lower()
|
||||
|
||||
@@ -416,3 +416,9 @@ async def heartbeat(ctx: dict[str, Any], user_id: str, tenant_id: str) -> None:
|
||||
|
||||
except Exception:
|
||||
logger.exception("heartbeat: error posting status message")
|
||||
|
||||
|
||||
# Register all job functions with the job registry
|
||||
from app.core.job_registry import register_job
|
||||
|
||||
register_job("deep_analysis", deep_analysis)
|
||||
|
||||
@@ -268,4 +268,10 @@ async def run_agent(
|
||||
logger.exception("Failed to save AgentRun for %s", agent.id)
|
||||
result_data["save_error"] = str(e)
|
||||
|
||||
return result_data
|
||||
return result_data
|
||||
|
||||
|
||||
# Register all job functions with the job registry
|
||||
from app.core.job_registry import register_job
|
||||
|
||||
register_job("run_agent", run_agent)
|
||||
@@ -277,3 +277,9 @@ async def run_automation(
|
||||
result_data["save_error"] = str(e)
|
||||
|
||||
return result_data
|
||||
|
||||
|
||||
# Register all job functions with the job registry
|
||||
from app.core.job_registry import register_job
|
||||
|
||||
register_job("run_automation", run_automation)
|
||||
|
||||
@@ -181,7 +181,8 @@ async def list_miniapps(
|
||||
"""List custom MiniApps from plugin config."""
|
||||
from app.plugins.builtins.kommunikation.miniapp_registry import MiniAppRegistry
|
||||
registry = MiniAppRegistry()
|
||||
return registry.list_apps()
|
||||
items = registry.list_apps()
|
||||
return {"items": items, "total": len(items)}
|
||||
|
||||
|
||||
@router.post(
|
||||
|
||||
@@ -71,3 +71,9 @@ async def scheduler_tick(ctx: dict[str, Any]) -> None:
|
||||
|
||||
except Exception:
|
||||
logger.exception("Failed to process cron job %s", job.id)
|
||||
|
||||
|
||||
# Register all job functions with the job registry
|
||||
from app.core.job_registry import register_job
|
||||
|
||||
register_job("scheduler_tick", scheduler_tick)
|
||||
|
||||
@@ -80,3 +80,9 @@ async def check_workflow_timeouts(ctx: dict[str, Any]) -> None:
|
||||
|
||||
except Exception:
|
||||
logger.exception("Failed to process timeout for instance %s", inst.id)
|
||||
|
||||
|
||||
# Register all job functions with the job registry
|
||||
from app.core.job_registry import register_job
|
||||
|
||||
register_job("check_workflow_timeouts", check_workflow_timeouts)
|
||||
|
||||
@@ -998,7 +998,7 @@ async def shared_with_me(
|
||||
file_ids = {p.file_id for p in perms}
|
||||
|
||||
if not file_ids:
|
||||
return []
|
||||
return {"items": [], "total": 0}
|
||||
|
||||
file_result = await db.execute(
|
||||
select(DmsFile).where(
|
||||
|
||||
@@ -7,6 +7,8 @@ import os
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
import aiofiles
|
||||
|
||||
from fastapi import UploadFile
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -117,8 +119,8 @@ class DmsBridge:
|
||||
f"{file_id}{file_ext}",
|
||||
)
|
||||
os.makedirs(os.path.dirname(storage_path), exist_ok=True)
|
||||
with open(storage_path, "wb") as f:
|
||||
f.write(content)
|
||||
async with aiofiles.open(storage_path, "wb") as f:
|
||||
await f.write(content)
|
||||
|
||||
# Create DMS file record
|
||||
dms_file = DmsFile(
|
||||
|
||||
@@ -884,7 +884,8 @@ async def list_threads(
|
||||
threads[tid] = {"thread_id": tid, "subject": mail.subject, "mail_count": 0, "mails": []}
|
||||
threads[tid]["mail_count"] += 1
|
||||
threads[tid]["mails"].append(mail_to_response(mail))
|
||||
return list(threads.values())
|
||||
items = list(threads.values())
|
||||
return {"items": items, "total": len(items)}
|
||||
|
||||
|
||||
# ─── Templates (F-MAIL-06) ───
|
||||
|
||||
@@ -1531,8 +1531,8 @@ async def send_mail_via_smtp(
|
||||
mime_type = att_info.get("mime_type", "application/octet-stream")
|
||||
if not file_path or not os.path.exists(file_path):
|
||||
continue
|
||||
with open(file_path, "rb") as f: # noqa: ASYNC230
|
||||
content = f.read()
|
||||
async with aiofiles.open(file_path, "rb") as f:
|
||||
content = await f.read()
|
||||
# Determine maintype/subtype from mime_type
|
||||
if "/" in mime_type:
|
||||
maintype, subtype = mime_type.split("/", 1)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -59,3 +59,9 @@ async def tasks_due_reminder(ctx: dict) -> None:
|
||||
if total_notified > 0:
|
||||
logger.info(f"Tasks reminder: sent {total_notified} notifications")
|
||||
await db.commit()
|
||||
|
||||
|
||||
# Register all job functions with the job registry
|
||||
from app.core.job_registry import register_job
|
||||
|
||||
register_job("tasks_due_reminder", tasks_due_reminder)
|
||||
|
||||
@@ -249,3 +249,14 @@ async def embedding_batch(ctx: dict[str, Any]) -> None:
|
||||
pass
|
||||
|
||||
logger.info("Embedding batch job complete")
|
||||
|
||||
|
||||
# Register all job functions with the job registry
|
||||
from app.core.job_registry import register_job
|
||||
|
||||
register_job("index_mails", index_mails)
|
||||
register_job("index_file", index_file)
|
||||
register_job("index_contact", index_contact)
|
||||
register_job("index_event", index_event)
|
||||
register_job("reindex", reindex)
|
||||
register_job("embedding_batch", embedding_batch)
|
||||
|
||||
Reference in New Issue
Block a user