using Dapper; using Npgsql; using PlanTempusAdmin.Models; namespace PlanTempusAdmin.Services; public class ForgejoService { private readonly string _connectionString; private readonly ILogger _logger; static ForgejoService() { DefaultTypeMap.MatchNamesWithUnderscores = true; } public ForgejoService(IConfiguration configuration, ILogger logger) { _connectionString = configuration.GetConnectionString("ForgejoDb") ?? throw new InvalidOperationException("ForgejoDb connection string not configured"); _logger = logger; } 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 Forgejo database"); return false; } } public async Task GetDashboardAsync() { var dashboard = new ForgejoDashboard(); try { await using var connection = new NpgsqlConnection(_connectionString); // Repository stats var repoStats = await connection.QuerySingleOrDefaultAsync(@" SELECT COUNT(*)::int as total, COUNT(*) FILTER (WHERE NOT is_private)::int as public_repos, COUNT(*) FILTER (WHERE is_private)::int as private_repos, COUNT(*) FILTER (WHERE is_fork)::int as forked, COUNT(*) FILTER (WHERE is_archived)::int as archived, COUNT(*) FILTER (WHERE is_mirror)::int as mirrors, COALESCE(SUM(size), 0) as total_size, COALESCE(SUM(num_stars), 0)::int as total_stars, COALESCE(SUM(num_forks), 0)::int as total_forks, COALESCE(SUM(num_issues - num_closed_issues), 0)::int as open_issues, COALESCE(SUM(num_pulls - num_closed_pulls), 0)::int as open_prs FROM repository"); if (repoStats != null) { dashboard.TotalRepos = (int)repoStats.total; dashboard.PublicRepos = (int)repoStats.public_repos; dashboard.PrivateRepos = (int)repoStats.private_repos; dashboard.ForkedRepos = (int)repoStats.forked; dashboard.ArchivedRepos = (int)repoStats.archived; dashboard.MirrorRepos = (int)repoStats.mirrors; dashboard.TotalSize = (long)repoStats.total_size; dashboard.TotalStars = (int)repoStats.total_stars; dashboard.TotalForks = (int)repoStats.total_forks; dashboard.TotalOpenIssues = (int)repoStats.open_issues; dashboard.TotalOpenPRs = (int)repoStats.open_prs; } // Actions stats var actionStats = await connection.QuerySingleOrDefaultAsync(@" SELECT COUNT(*)::int as total, COUNT(*) FILTER (WHERE TO_TIMESTAMP(created) >= NOW() - INTERVAL '1 day')::int as today, COUNT(*) FILTER (WHERE TO_TIMESTAMP(created) >= NOW() - INTERVAL '7 days')::int as this_week, COUNT(*) FILTER (WHERE status = 3)::int as successful, COUNT(*) FILTER (WHERE status = 4)::int as failed, COUNT(*) FILTER (WHERE status = 2)::int as running FROM action_run"); if (actionStats != null) { dashboard.TotalRuns = (int)actionStats.total; dashboard.RunsToday = (int)actionStats.today; dashboard.RunsThisWeek = (int)actionStats.this_week; dashboard.SuccessfulRuns = (int)actionStats.successful; dashboard.FailedRunsCount = (int)actionStats.failed; dashboard.RunningNow = (int)actionStats.running; } // Recently updated repos dashboard.RecentlyUpdated = await GetRepositoriesAsync(connection, "ORDER BY r.updated_unix DESC LIMIT 5"); // Largest repos dashboard.LargestRepos = await GetRepositoriesAsync(connection, "ORDER BY r.size DESC LIMIT 5"); // Recent action runs dashboard.RecentRuns = await GetActionRunsAsync(connection, "ORDER BY ar.created DESC LIMIT 10"); // Failed runs dashboard.FailedRuns = await GetActionRunsAsync(connection, "WHERE ar.status = 4 ORDER BY ar.created DESC LIMIT 5"); // Running now dashboard.RunningRuns = await GetActionRunsAsync(connection, "WHERE ar.status = 2 ORDER BY ar.started LIMIT 10"); } catch (Exception ex) { _logger.LogError(ex, "Error fetching Forgejo dashboard"); } return dashboard; } public async Task> GetAllRepositoriesAsync() { try { await using var connection = new NpgsqlConnection(_connectionString); return await GetRepositoriesAsync(connection, "ORDER BY LOWER(u.name), LOWER(r.name)"); } catch (Exception ex) { _logger.LogError(ex, "Error fetching repositories"); return new List(); } } public async Task> GetAllActionRunsAsync(int limit = 100) { try { await using var connection = new NpgsqlConnection(_connectionString); return await GetActionRunsAsync(connection, $"ORDER BY ar.created DESC LIMIT {limit}"); } catch (Exception ex) { _logger.LogError(ex, "Error fetching action runs"); return new List(); } } public async Task> GetActionStatsAsync() { try { await using var connection = new NpgsqlConnection(_connectionString); var stats = await connection.QueryAsync(@" SELECT ar.workflow_id, r.name as repo_name, COUNT(*)::int as total_runs, COUNT(*) FILTER (WHERE ar.status = 3)::int as successful, COUNT(*) FILTER (WHERE ar.status = 4)::int as failed, TO_TIMESTAMP(MAX(ar.created)) as last_run, AVG(ar.stopped - ar.started) FILTER (WHERE ar.stopped > 0 AND ar.started > 0) as avg_duration_seconds FROM action_run ar JOIN repository r ON ar.repo_id = r.id GROUP BY ar.workflow_id, r.name ORDER BY total_runs DESC"); return stats.ToList(); } catch (Exception ex) { _logger.LogError(ex, "Error fetching action stats"); return new List(); } } private async Task> GetRepositoriesAsync(NpgsqlConnection connection, string orderClause) { var repos = await connection.QueryAsync($@" SELECT r.id, u.name as owner_name, r.name, r.description, r.is_private, r.is_fork, r.is_archived, r.is_mirror, r.num_stars, r.num_forks, r.num_watches, r.num_issues, r.num_closed_issues, r.num_pulls, r.num_closed_pulls, r.size, TO_TIMESTAMP(r.created_unix) as created_at, TO_TIMESTAMP(r.updated_unix) as updated_at FROM repository r JOIN ""user"" u ON r.owner_id = u.id {orderClause}"); return repos.ToList(); } private async Task> GetActionRunsAsync(NpgsqlConnection connection, string whereOrderClause) { var runs = await connection.QueryAsync($@" SELECT ar.id, ar.repo_id, r.name as repo_name, u.name as owner_name, ar.workflow_id, ar.""index"", COALESCE(tu.name, '') as trigger_user, ar.ref, ar.commit_sha, ar.event, ar.title, ar.status, CASE WHEN ar.started > 0 THEN TO_TIMESTAMP(ar.started) ELSE NULL END as started, CASE WHEN ar.stopped > 0 THEN TO_TIMESTAMP(ar.stopped) ELSE NULL END as stopped, TO_TIMESTAMP(ar.created) as created, TO_TIMESTAMP(ar.updated) as updated FROM action_run ar JOIN repository r ON ar.repo_id = r.id JOIN ""user"" u ON r.owner_id = u.id LEFT JOIN ""user"" tu ON ar.trigger_user_id = tu.id {whereOrderClause}"); return runs.ToList(); } }