diff --git a/DEPLOY.md b/DEPLOY.md index 4c3e789..10154ef 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -119,3 +119,54 @@ python scripts/deploy.py --migrate-only ```bash python scripts/deploy.py --skip-build # startet Worker automatisch ``` + +## Backup & Restore + +### Backup (PostgreSQL) + +```bash +# Full DB backup (run on the host or via docker exec) +docker exec crm-postgres pg_dump -U crm_user -Fc crm_db > backup_$(date +%Y%m%d_%H%M%S).dump + +# Backup mit Custom-Format (komprimiert, parallel restore-fähig) +docker exec crm-postgres pg_dump -U crm_user -Fc -Z 9 crm_db > backup_$(date +%Y%m%d).dump +``` + +### Backup (Redis — Sessions/Queues) + +```bash +# Redis RDB Snapshot +docker exec crm-redis redis-cli -a "$REDIS_PASSWORD" SAVE +docker cp crm-redis:/data/dump.rdb redis_backup_$(date +%Y%m%d).rdb +``` + +### Backup (File Storage) + +```bash +# Local storage volume +docker run --rm -v leocrm-fix_storage:/data -v $(pwd):/backup alpine \ + tar czf /backup/storage_$(date +%Y%m%d).tar.gz /data +``` + +### Restore (PostgreSQL) + +```bash +# Stop app containers +docker compose stop crm-app crm-worker + +# Restore DB +docker exec -i crm-postgres pg_restore -U crm_user -d crm_db --clean < backup_20260726.dump + +# Restart app +docker compose start crm-app crm-worker +``` + +### Automatisierte Backups (Cron) + +```bash +# /etc/cron.d/leocrm-backup +0 2 * * * root docker exec crm-postgres pg_dump -U crm_user -Fc crm_db > /backups/leocrm_$(date +\%Y\%m\%d).dump +0 3 * * * root find /backups -name 'leocrm_*.dump' -mtime +30 -delete +``` + +**Empfehlung:** Tägliche DB-Backups, 30 Tage Aufbewahrung. Storage-Backup wöchentlich. diff --git a/app/plugins/builtins/ai_ui_control/websocket_manager.py b/app/plugins/builtins/ai_ui_control/websocket_manager.py index 9d85e52..efd1284 100644 --- a/app/plugins/builtins/ai_ui_control/websocket_manager.py +++ b/app/plugins/builtins/ai_ui_control/websocket_manager.py @@ -92,10 +92,20 @@ class AIUIControlWSManager: return None def store_feedback(self, feedback: dict[str, Any]) -> None: - """Store feedback from frontend after command execution.""" + """Store feedback from frontend after command execution. + + Maintains a maximum of 100 feedback entries to prevent memory exhaustion. + Oldest entries are removed when the limit is reached. + """ command_id = feedback.get("command_id") if command_id: self._feedback[command_id] = feedback + # Enforce max feedback entries (FIFO eviction) + MAX_FEEDBACK_ENTRIES = 100 + if len(self._feedback) > MAX_FEEDBACK_ENTRIES: + keys_to_remove = list(self._feedback.keys())[:-MAX_FEEDBACK_ENTRIES] + for key in keys_to_remove: + del self._feedback[key] logger.debug(f"AI UI Control: feedback stored for command {command_id}: {feedback.get('status')}") def get_feedback(self, command_id: str) -> dict[str, Any] | None: diff --git a/frontend/src/components/contacts/ContactDetail.tsx b/frontend/src/components/contacts/ContactDetail.tsx index f8e7700..43410a3 100644 --- a/frontend/src/components/contacts/ContactDetail.tsx +++ b/frontend/src/components/contacts/ContactDetail.tsx @@ -15,6 +15,8 @@ import { useAIUIControlStore } from '@/store/aiUIControlStore'; import { PluginPage } from '@/components/plugins/PluginLoader'; import { useAuthStore } from '@/store/authStore'; import { CustomFieldRenderer } from '@/components/contacts/CustomFieldRenderer'; +import { TagBadge } from '@/components/tags/TagBadge'; +import { EntityHistoryPanel } from '@/components/common/EntityHistoryPanel'; import { useCustomFields } from '@/api/customFields'; import { type UnifiedContact, @@ -427,7 +429,9 @@ export function ContactDetail({ contact, loading, onEdit, onDeleted }: ContactDe