"""ARQ background jobs for the Automation plugin.""" from __future__ import annotations import logging from typing import Any from app.core.db import get_session_factory logger = logging.getLogger(__name__) async def backup_check(ctx: dict[str, Any]) -> None: """Check if backups are current and publish backup events. Runs daily at 2:00. Checks the last backup timestamp from system settings and publishes backup.completed or backup.failed events accordingly. """ from app.core.event_bus import get_event_bus from sqlalchemy import text event_bus = get_event_bus() factory = get_session_factory() async with factory() as db: try: # Check last backup timestamp from system settings result = await db.execute( text("SELECT value FROM system_settings WHERE key = 'last_backup_at' LIMIT 1") ) row = result.mappings().first() if row and row["value"]: logger.info("Backup check: last backup at %s", row["value"]) # Publish backup.completed event await event_bus.publish('backup.completed', { 'tenant_id': None, 'user_id': None, 'timestamp': row["value"], 'title': 'Backup erfolgreich', 'body': f'Letztes Backup: {row["value"]}', }) else: logger.warning("Backup check: no backup timestamp found") await event_bus.publish('backup.failed', { 'tenant_id': None, 'user_id': None, 'title': 'Backup fehlgeschlagen', 'body': 'Kein Backup-Zeitstempel gefunden', }) except Exception: logger.exception("Backup check failed") await event_bus.publish('backup.failed', { 'tenant_id': None, 'user_id': None, 'title': 'Backup fehlgeschlagen', 'body': 'Backup-Check fehlgeschlagen', }) logger.info("Backup check complete") async def search_index_check(ctx: dict[str, Any]) -> None: """Check if search index is current and log status. Runs daily at 3:00. Checks for entities without embeddings and logs the count. Publishes status via logging only. """ from sqlalchemy import text factory = get_session_factory() async with factory() as db: try: # Check for contacts without embeddings result = await db.execute( text("SELECT COUNT(*) as cnt FROM contacts WHERE deleted_at IS NULL AND embedding IS NULL") ) row = result.mappings().first() contacts_missing = row["cnt"] if row else 0 # Check for mails without embeddings result = await db.execute( text("SELECT COUNT(*) as cnt FROM mails WHERE embedding IS NULL") ) row = result.mappings().first() mails_missing = row["cnt"] if row else 0 # Check for files without embeddings result = await db.execute( text("SELECT COUNT(*) as cnt FROM files WHERE deleted_at IS NULL AND embedding IS NULL") ) row = result.mappings().first() files_missing = row["cnt"] if row else 0 total_missing = contacts_missing + mails_missing + files_missing if total_missing > 0: logger.warning( "Search index check: %d entities missing embeddings (contacts=%d, mails=%d, files=%d)", total_missing, contacts_missing, mails_missing, files_missing, ) else: logger.info("Search index check: all entities have embeddings") except Exception: logger.exception("Search index check failed") logger.info("Search index check complete")