Initial commit

This commit is contained in:
Janus C. H. Knudsen 2026-02-03 00:17:08 +01:00
commit 77d35ff965
51 changed files with 5591 additions and 0 deletions

232
Services/ForgejoService.cs Normal file
View file

@ -0,0 +1,232 @@
using Dapper;
using Npgsql;
using PlanTempusAdmin.Models;
namespace PlanTempusAdmin.Services;
public class ForgejoService
{
private readonly string _connectionString;
private readonly ILogger<ForgejoService> _logger;
static ForgejoService()
{
DefaultTypeMap.MatchNamesWithUnderscores = true;
}
public ForgejoService(IConfiguration configuration, ILogger<ForgejoService> logger)
{
_connectionString = configuration.GetConnectionString("ForgejoDb")
?? throw new InvalidOperationException("ForgejoDb connection string not configured");
_logger = logger;
}
public async Task<bool> 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<ForgejoDashboard> GetDashboardAsync()
{
var dashboard = new ForgejoDashboard();
try
{
await using var connection = new NpgsqlConnection(_connectionString);
// Repository stats
var repoStats = await connection.QuerySingleOrDefaultAsync<dynamic>(@"
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<dynamic>(@"
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<List<ForgejoRepository>> 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<ForgejoRepository>();
}
}
public async Task<List<ForgejoActionRun>> 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<ForgejoActionRun>();
}
}
public async Task<List<ForgejoActionStats>> GetActionStatsAsync()
{
try
{
await using var connection = new NpgsqlConnection(_connectionString);
var stats = await connection.QueryAsync<ForgejoActionStats>(@"
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<ForgejoActionStats>();
}
}
private async Task<List<ForgejoRepository>> GetRepositoriesAsync(NpgsqlConnection connection, string orderClause)
{
var repos = await connection.QueryAsync<ForgejoRepository>($@"
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<List<ForgejoActionRun>> GetActionRunsAsync(NpgsqlConnection connection, string whereOrderClause)
{
var runs = await connection.QueryAsync<ForgejoActionRun>($@"
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();
}
}