PlanTempusAdmin/Pages/Forgejo/Index.cshtml

323 lines
13 KiB
Text
Raw Permalink Normal View History

2026-02-03 00:17:08 +01:00
@page
@model PlanTempusAdmin.Pages.Forgejo.IndexModel
@{
ViewData["Title"] = "Forgejo Oversigt";
}
@section Styles {
<link rel="stylesheet" href="~/css/pages/forgejo.css" asp-append-version="true" />
}
2026-02-03 00:17:08 +01:00
<div class="page-header">
<h1 class="page-title">Forgejo Oversigt</h1>
<p class="page-subtitle">Git repositories og CI/CD status</p>
</div>
@if (!Model.IsConnected)
{
<div class="card">
<div class="card-body">
<p class="text-danger">Kan ikke forbinde til Forgejo database</p>
</div>
</div>
}
else
{
var d = Model.Dashboard;
<!-- Hero Stats -->
<div class="status-grid">
<div class="status-item">
<div class="status-label">Repositories</div>
<div class="status-value">@d.TotalRepos</div>
<div class="status-detail">@d.PublicRepos public · @d.PrivateRepos private</div>
</div>
<div class="status-item">
<div class="status-label">Total Størrelse</div>
<div class="status-value">@FormatSize(d.TotalSize * 1024)</div>
<div class="status-detail">@d.TotalStars stars · @d.TotalForks forks</div>
</div>
<div class="status-item">
<div class="status-label">Åbne Issues/PRs</div>
<div class="status-value">@(d.TotalOpenIssues + d.TotalOpenPRs)</div>
<div class="status-detail">@d.TotalOpenIssues issues · @d.TotalOpenPRs PRs</div>
</div>
<div class="status-item">
<div class="status-label">CI Status</div>
<div class="status-value @(d.RunningNow > 0 ? "warning" : d.SuccessRate >= 90 ? "success" : "")">
@if (d.RunningNow > 0)
{
<span class="pulse">●</span> @d.RunningNow
}
else
{
<text>@d.SuccessRate.ToString("0")%</text>
}
</div>
<div class="status-detail">@d.RunsToday i dag · @d.RunsThisWeek denne uge</div>
</div>
</div>
<!-- Running Actions -->
@if (d.RunningRuns.Count > 0)
{
<div class="card mt-2 running-card">
<div class="card-header">
<span class="pulse">●</span> Kørende Workflows
</div>
<table class="table">
<thead>
<tr>
<th>Repository</th>
<th>Workflow</th>
<th>Event</th>
<th>Branch</th>
<th>Startet</th>
<th>Varighed</th>
</tr>
</thead>
<tbody>
@foreach (var run in d.RunningRuns)
{
<tr>
<td><code>@run.FullRepoName</code></td>
<td>@run.WorkflowId</td>
<td><span class="badge">@run.Event</span></td>
<td><code>@run.Ref.Replace("refs/heads/", "")</code></td>
<td>@run.Started?.ToString("HH:mm:ss")</td>
<td>@FormatDuration(run.Duration)</td>
</tr>
}
</tbody>
</table>
</div>
}
<div class="dashboard-grid mt-2">
<!-- Left Column -->
<div class="dashboard-col">
<!-- Repos Overview -->
<div class="card">
<div class="card-header">Repository Typer</div>
<div class="card-body">
<div class="stat-bars">
<div class="stat-bar-item">
<span class="stat-bar-label">Public</span>
<div class="stat-bar">
<div class="stat-bar-fill" style="width: @(d.TotalRepos > 0 ? d.PublicRepos * 100 / d.TotalRepos : 0)%; background: var(--success-color);"></div>
</div>
<span class="stat-bar-value">@d.PublicRepos</span>
</div>
<div class="stat-bar-item">
<span class="stat-bar-label">Private</span>
<div class="stat-bar">
<div class="stat-bar-fill" style="width: @(d.TotalRepos > 0 ? d.PrivateRepos * 100 / d.TotalRepos : 0)%; background: var(--accent-color);"></div>
</div>
<span class="stat-bar-value">@d.PrivateRepos</span>
</div>
<div class="stat-bar-item">
<span class="stat-bar-label">Forks</span>
<div class="stat-bar">
<div class="stat-bar-fill" style="width: @(d.TotalRepos > 0 ? d.ForkedRepos * 100 / d.TotalRepos : 0)%; background: var(--warning-color);"></div>
</div>
<span class="stat-bar-value">@d.ForkedRepos</span>
</div>
<div class="stat-bar-item">
<span class="stat-bar-label">Mirrors</span>
<div class="stat-bar">
<div class="stat-bar-fill" style="width: @(d.TotalRepos > 0 ? d.MirrorRepos * 100 / d.TotalRepos : 0)%; background: var(--muted-color);"></div>
</div>
<span class="stat-bar-value">@d.MirrorRepos</span>
</div>
<div class="stat-bar-item">
<span class="stat-bar-label">Archived</span>
<div class="stat-bar">
<div class="stat-bar-fill" style="width: @(d.TotalRepos > 0 ? d.ArchivedRepos * 100 / d.TotalRepos : 0)%; background: var(--danger-color);"></div>
</div>
<span class="stat-bar-value">@d.ArchivedRepos</span>
</div>
</div>
</div>
</div>
<!-- Largest Repos -->
<div class="card">
<div class="card-header">Største Repositories</div>
<table class="table">
<thead>
<tr>
<th>Repository</th>
<th>Størrelse</th>
<th>Issues</th>
<th>PRs</th>
</tr>
</thead>
<tbody>
@foreach (var repo in d.LargestRepos)
{
<tr>
<td>
<code>@repo.FullName</code>
@if (repo.IsPrivate) { <span class="badge">privat</span> }
</td>
<td>@repo.SizeFormatted</td>
<td>@repo.OpenIssues</td>
<td>@repo.OpenPulls</td>
</tr>
}
</tbody>
</table>
</div>
<!-- Recently Updated -->
<div class="card">
<div class="card-header">Senest Opdaterede</div>
<table class="table">
<thead>
<tr>
<th>Repository</th>
<th>Opdateret</th>
</tr>
</thead>
<tbody>
@foreach (var repo in d.RecentlyUpdated)
{
<tr>
<td>
<code>@repo.FullName</code>
@if (repo.IsPrivate) { <span class="badge">privat</span> }
</td>
<td>@FormatTimeAgo(repo.UpdatedAt)</td>
</tr>
}
</tbody>
</table>
</div>
</div>
<!-- Right Column -->
<div class="dashboard-col">
<!-- CI Stats -->
<div class="card">
<div class="card-header">CI/CD Statistik</div>
<div class="card-body">
<div class="ci-stats">
<div class="ci-stat">
<div class="ci-stat-value success">@d.SuccessfulRuns</div>
<div class="ci-stat-label">Success</div>
</div>
<div class="ci-stat">
<div class="ci-stat-value danger">@d.FailedRunsCount</div>
<div class="ci-stat-label">Failed</div>
</div>
<div class="ci-stat">
<div class="ci-stat-value">@d.TotalRuns</div>
<div class="ci-stat-label">Total</div>
</div>
</div>
<div class="ci-rate-bar mt-1">
<div class="ci-rate-success" style="width: @d.SuccessRate.ToString("0")%"></div>
</div>
<div class="ci-rate-label">@d.SuccessRate.ToString("0.0")% success rate</div>
</div>
</div>
<!-- Recent Runs -->
<div class="card">
<div class="card-header">Seneste Workflow Runs</div>
<div class="card-body compact-list">
@if (d.RecentRuns.Count == 0)
{
<p class="text-muted">Ingen workflow runs</p>
}
@foreach (var run in d.RecentRuns)
{
<div class="list-item">
<div class="item-main">
@if (run.Status == 3)
{
<span class="badge badge-success">OK</span>
}
else if (run.Status == 4)
{
<span class="badge badge-danger">FEJL</span>
}
else if (run.Status == 2)
{
<span class="badge badge-warning">KØRER</span>
}
else
{
<span class="badge">@run.StatusText</span>
}
<code>@run.FullRepoName</code>
</div>
<div class="item-meta">
@run.WorkflowId · @run.Event · @FormatTimeAgo(run.Created)
@if (run.Duration.HasValue)
{
<text>· @FormatDuration(run.Duration)</text>
}
</div>
</div>
}
</div>
</div>
<!-- Failed Runs -->
@if (d.FailedRuns.Count > 0)
{
<div class="card error-card">
<div class="card-header">Fejlede Workflows</div>
<div class="card-body compact-list">
@foreach (var run in d.FailedRuns)
{
<div class="list-item">
<div class="item-main">
<span class="badge badge-danger">FEJL</span>
<code>@run.FullRepoName</code>
</div>
<div class="item-meta">
@run.WorkflowId · @run.Event · @FormatTimeAgo(run.Created)
</div>
</div>
}
</div>
</div>
}
</div>
</div>
}
@functions {
string FormatSize(long bytes)
{
if (bytes == 0) return "0 B";
var sizes = new[] { "B", "KB", "MB", "GB", "TB" };
var i = (int)Math.Floor(Math.Log(bytes) / Math.Log(1024));
return $"{Math.Round(bytes / Math.Pow(1024, i), 1)} {sizes[i]}";
}
string FormatDuration(TimeSpan? duration)
{
if (!duration.HasValue) return "-";
var d = duration.Value;
if (d.TotalHours >= 1) return $"{(int)d.TotalHours}t {d.Minutes}m";
if (d.TotalMinutes >= 1) return $"{(int)d.TotalMinutes}m {d.Seconds}s";
return $"{d.Seconds}s";
}
string FormatTimeAgo(DateTime? time)
{
if (!time.HasValue) return "-";
var diff = DateTime.Now - time.Value;
if (diff.TotalMinutes < 1) return "lige nu";
if (diff.TotalMinutes < 60) return $"{(int)diff.TotalMinutes}m siden";
if (diff.TotalHours < 24) return $"{(int)diff.TotalHours}t siden";
if (diff.TotalDays < 7) return $"{(int)diff.TotalDays}d siden";
return time.Value.ToString("dd/MM");
}
string FormatTimeAgo(DateTime time) => FormatTimeAgo((DateTime?)time);
}