2026-02-03 00:17:08 +01:00
|
|
|
@page
|
|
|
|
|
@model PlanTempusAdmin.Pages.Forgejo.IndexModel
|
|
|
|
|
@{
|
|
|
|
|
ViewData["Title"] = "Forgejo Oversigt";
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 15:55:44 +01:00
|
|
|
@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);
|
|
|
|
|
}
|