Phase 0 Complete: Tasks 0.7-0.20

- 0.7: UI-Design-Richtlinien (docs/ui-design-guidelines.md, 535 lines)
- 0.8: Theme-Customization Backend (4 theme fields, migration 0023)
- 0.9: Theme-Customization Frontend (SettingsTheme.tsx, themeStore.ts, live preview)
- 0.10: RBAC-Audit (4 plugins secured, 53 routes with require_permission)
- 0.11: LiteLLM-Cleanup (llm_client.py migrated from httpx to litellm)
- 0.12: KI-Agent-Framework docs (plugin-development-guide.md, agent_capabilities field)
- 0.13: Heartbeat configurable (ProactiveSettings, migration 0024, frontend UI)
- 0.14: Unified Search Field-Level RBAC (resolve_permissions + filter_fields_by_permission)
- 0.15: Undo/History-System (EntityHistory model, service, routes, migration 0025, HistoryViewer)
- 0.16: Storage Backend (LocalStorage + S3Storage, DMS/attachments/mail updated)
- 0.17: Import/Export unified Contact fields (firstname, surname, email_1, phone_1)
- 0.18: .gitignore & Config-Cleanup (webui→frontend, python-jose removed, .env untracked)
- 0.19: Mail-Salt Security-Fix (per-account random salt, migration 0026)
- 0.20: AGPL replaced (PyMuPDF→pypdf, OnlyOffice→Collabora, LICENSE + THIRD_PARTY_LICENSES.md)
This commit is contained in:
Agent Zero
2026-07-23 08:42:26 +02:00
parent 3d06cb2353
commit ec81940178
65 changed files with 3061 additions and 277 deletions
+53 -3
View File
@@ -12,6 +12,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.models.contact import Contact, ContactPerson
from app.services.entity_history_service import record_history
def _compute_displayname(data: dict) -> str:
@@ -239,7 +240,15 @@ async def create_contact(
q = select(Contact).options(selectinload(Contact.contact_persons)).where(Contact.id == contact.id)
result = await db.execute(q)
contact = result.scalar_one()
return _serialize_contact_detail(contact)
serialized = _serialize_contact_detail(contact)
# Record history
await record_history(
db, tenant_id, user_id, "contact", contact.id,
action="create", snapshot_after=serialized,
)
return serialized
async def update_contact(
@@ -260,6 +269,9 @@ async def update_contact(
if not contact:
raise ValueError("Contact not found")
# Capture snapshot before update
snapshot_before = _serialize_contact_detail(contact)
# Recompute displayname if name fields changed
if any(k in data for k in ("type", "name", "firstname", "surname", "surfix")):
merged = {**_serialize_contact(contact), **data}
@@ -271,10 +283,30 @@ async def update_contact(
contact.updated_by = user_id
await db.flush()
return _serialize_contact_detail(contact)
snapshot_after = _serialize_contact_detail(contact)
# Compute changes diff
changes: dict = {}
for key, new_val in snapshot_after.items():
old_val = snapshot_before.get(key)
if old_val != new_val:
changes[key] = {"old": old_val, "new": new_val}
# Record history
await record_history(
db, tenant_id, user_id, "contact", contact.id,
action="update",
snapshot_before=snapshot_before,
snapshot_after=snapshot_after,
changes=changes or None,
)
return snapshot_after
async def delete_contact(db: AsyncSession, tenant_id: uuid.UUID, contact_id: str) -> None:
async def delete_contact(
db: AsyncSession, tenant_id: uuid.UUID, contact_id: str, user_id: uuid.UUID | None = None
) -> None:
"""Soft-delete a contact."""
q = select(Contact).where(
Contact.id == uuid.UUID(contact_id),
@@ -285,10 +317,28 @@ async def delete_contact(db: AsyncSession, tenant_id: uuid.UUID, contact_id: str
contact = result.scalar_one_or_none()
if not contact:
raise ValueError("Contact not found")
# Capture snapshot before deletion
from sqlalchemy.orm import selectinload
q2 = (
select(Contact)
.options(selectinload(Contact.contact_persons))
.where(Contact.id == contact.id)
)
result2 = await db.execute(q2)
contact_full = result2.scalar_one()
snapshot_before = _serialize_contact_detail(contact_full)
from datetime import datetime, timezone
contact.deleted_at = datetime.now(timezone.utc)
await db.flush()
# Record history
await record_history(
db, tenant_id, user_id, "contact", contact.id,
action="delete", snapshot_before=snapshot_before,
)
async def hard_delete_contact(db: AsyncSession, tenant_id: uuid.UUID, contact_id: str) -> None:
"""GDPR hard-delete a contact."""