using Dapper; using Npgsql; using PlanTempusAdmin.Models; namespace PlanTempusAdmin.Services; public class BackupService { private readonly string _connectionString; private readonly ILogger _logger; static BackupService() { DefaultTypeMap.MatchNamesWithUnderscores = true; } public BackupService(IConfiguration configuration, ILogger logger) { _connectionString = configuration.GetConnectionString("BackupDb") ?? throw new InvalidOperationException("BackupDb connection string not configured"); _logger = logger; } public async Task> GetLogsAsync(int limit = 100) { try { await using var connection = new NpgsqlConnection(_connectionString); var logs = await connection.QueryAsync(@" SELECT id, started_at, completed_at, duration_ms, backup_type, source_name, source_path, destination, remote_path, status, size_bytes, file_count, error_message, error_code, retry_count, hostname, script_version, checksum, created_at FROM backup_logs ORDER BY started_at DESC LIMIT @limit", new { limit }); return logs.ToList(); } catch (Exception ex) { _logger.LogError(ex, "Error fetching backup logs"); return new List(); } } public async Task GetSummaryAsync() { try { await using var connection = new NpgsqlConnection(_connectionString); var summary = await connection.QuerySingleOrDefaultAsync(@" SELECT COUNT(*)::int as total_backups, COUNT(*) FILTER (WHERE status = 'success')::int as successful_backups, COUNT(*) FILTER (WHERE status = 'failed')::int as failed_backups, MAX(started_at) as last_backup, MAX(started_at) FILTER (WHERE status = 'success') as last_successful_backup, COALESCE(SUM(size_bytes) FILTER (WHERE status = 'success'), 0) as total_size_bytes FROM backup_logs"); return summary ?? new BackupSummary(); } catch (Exception ex) { _logger.LogError(ex, "Error fetching backup summary"); return new BackupSummary(); } } public async Task> GetRepositorySummariesAsync() { try { await using var connection = new NpgsqlConnection(_connectionString); var summaries = await connection.QueryAsync(@" WITH ranked_backups AS ( SELECT source_name, backup_type, size_bytes, started_at, status, ROW_NUMBER() OVER (PARTITION BY source_name ORDER BY started_at DESC) as rn FROM backup_logs WHERE status = 'success' ) SELECT bl.source_name, bl.backup_type, COUNT(*)::int as total_backups, COUNT(*) FILTER (WHERE bl.status = 'success')::int as successful_backups, COUNT(*) FILTER (WHERE bl.status = 'failed')::int as failed_backups, MAX(bl.started_at) as last_backup, MAX(bl.started_at) FILTER (WHERE bl.status = 'success') as last_successful_backup, COALESCE(SUM(bl.size_bytes) FILTER (WHERE bl.status = 'success'), 0) as total_size_bytes, (SELECT rb.size_bytes FROM ranked_backups rb WHERE rb.source_name = bl.source_name AND rb.rn = 1) as last_backup_size_bytes, (SELECT rb.size_bytes FROM ranked_backups rb WHERE rb.source_name = bl.source_name AND rb.rn = 2) as previous_backup_size_bytes FROM backup_logs bl GROUP BY bl.source_name, bl.backup_type ORDER BY last_backup DESC NULLS LAST"); return summaries.ToList(); } catch (Exception ex) { _logger.LogError(ex, "Error fetching repository summaries"); return new List(); } } public async Task GetDashboardAsync() { var dashboard = new BackupDashboard(); try { await using var connection = new NpgsqlConnection(_connectionString); // Overall stats var stats = await connection.QuerySingleOrDefaultAsync(@" SELECT COUNT(*)::int as total_backups, COUNT(*) FILTER (WHERE status = 'success')::int as successful_backups, COUNT(*) FILTER (WHERE status = 'failed')::int as failed_backups, COUNT(*) FILTER (WHERE status = 'running')::int as running_backups, COALESCE(SUM(size_bytes) FILTER (WHERE status = 'success'), 0) as total_size_bytes, MAX(started_at) as last_backup, MAX(started_at) FILTER (WHERE status = 'success') as last_successful_backup, COUNT(*) FILTER (WHERE started_at > NOW() - INTERVAL '24 hours')::int as backups_last_24_hours, COUNT(*) FILTER (WHERE started_at > NOW() - INTERVAL '7 days')::int as backups_last_7_days, COALESCE(SUM(size_bytes) FILTER (WHERE status = 'success' AND started_at > NOW() - INTERVAL '24 hours'), 0) as size_last_24_hours, COALESCE(SUM(size_bytes) FILTER (WHERE status = 'success' AND started_at > NOW() - INTERVAL '7 days'), 0) as size_last_7_days FROM backup_logs"); if (stats != null) { dashboard.TotalBackups = (int)stats.total_backups; dashboard.SuccessfulBackups = (int)stats.successful_backups; dashboard.FailedBackups = (int)stats.failed_backups; dashboard.RunningBackups = (int)stats.running_backups; dashboard.TotalSizeBytes = (long)stats.total_size_bytes; dashboard.LastBackup = stats.last_backup; dashboard.LastSuccessfulBackup = stats.last_successful_backup; dashboard.BackupsLast24Hours = (int)stats.backups_last_24_hours; dashboard.BackupsLast7Days = (int)stats.backups_last_7_days; dashboard.SizeLast24Hours = (long)stats.size_last_24_hours; dashboard.SizeLast7Days = (long)stats.size_last_7_days; } // By backup type dashboard.ByType = (await connection.QueryAsync(@" SELECT backup_type, COUNT(*)::int as total, COUNT(*) FILTER (WHERE status = 'success')::int as successful, COUNT(*) FILTER (WHERE status = 'failed')::int as failed, COUNT(*) FILTER (WHERE status = 'running')::int as running, COALESCE(SUM(size_bytes) FILTER (WHERE status = 'success'), 0) as total_size, MAX(started_at) as last_backup FROM backup_logs GROUP BY backup_type ORDER BY total DESC")).ToList(); // By destination dashboard.ByDestination = (await connection.QueryAsync(@" SELECT destination, COUNT(*)::int as total, COUNT(*) FILTER (WHERE status = 'success')::int as successful, COUNT(*) FILTER (WHERE status = 'failed')::int as failed, COALESCE(SUM(size_bytes) FILTER (WHERE status = 'success'), 0) as total_size, MAX(started_at) as last_backup FROM backup_logs GROUP BY destination ORDER BY total DESC")).ToList(); // By host dashboard.ByHost = (await connection.QueryAsync(@" SELECT COALESCE(hostname, 'unknown') as hostname, COUNT(*)::int as total, COUNT(*) FILTER (WHERE status = 'success')::int as successful, COUNT(*) FILTER (WHERE status = 'failed')::int as failed, COUNT(*) FILTER (WHERE status = 'running')::int as running, MAX(started_at) as last_backup, (SELECT script_version FROM backup_logs b2 WHERE COALESCE(b2.hostname, 'unknown') = COALESCE(backup_logs.hostname, 'unknown') ORDER BY started_at DESC LIMIT 1) as script_version FROM backup_logs GROUP BY COALESCE(hostname, 'unknown') ORDER BY total DESC")).ToList(); // Top errors dashboard.TopErrors = (await connection.QueryAsync(@" SELECT COALESCE(error_code, 'UNKNOWN') as error_code, COUNT(*)::int as count, MAX(started_at) as last_occurrence, (SELECT error_message FROM backup_logs b2 WHERE COALESCE(b2.error_code, 'UNKNOWN') = COALESCE(backup_logs.error_code, 'UNKNOWN') AND b2.status = 'failed' ORDER BY started_at DESC LIMIT 1) as last_message FROM backup_logs WHERE status = 'failed' GROUP BY COALESCE(error_code, 'UNKNOWN') ORDER BY count DESC LIMIT 5")).ToList(); // Daily stats (last 14 days) dashboard.DailyStats = (await connection.QueryAsync(@" SELECT DATE(started_at) as date, COUNT(*)::int as total, COUNT(*) FILTER (WHERE status = 'success')::int as successful, COUNT(*) FILTER (WHERE status = 'failed')::int as failed, COALESCE(SUM(size_bytes) FILTER (WHERE status = 'success'), 0) as total_size FROM backup_logs WHERE started_at > NOW() - INTERVAL '14 days' GROUP BY DATE(started_at) ORDER BY date DESC")).ToList(); // Running now dashboard.RunningNow = (await connection.QueryAsync(@" SELECT * FROM backup_logs WHERE status = 'running' ORDER BY started_at DESC")).ToList(); // Recent successes dashboard.RecentSuccesses = (await connection.QueryAsync(@" SELECT * FROM backup_logs WHERE status = 'success' ORDER BY started_at DESC LIMIT 5")).ToList(); // Recent failures dashboard.RecentFailures = (await connection.QueryAsync(@" SELECT * FROM backup_logs WHERE status = 'failed' ORDER BY started_at DESC LIMIT 5")).ToList(); } catch (Exception ex) { _logger.LogError(ex, "Error fetching backup dashboard"); } return dashboard; } public async Task TestConnectionAsync() { try { await using var connection = new NpgsqlConnection(_connectionString); await connection.OpenAsync(); return true; } catch (Exception ex) { _logger.LogWarning(ex, "Could not connect to backup database"); return false; } } }