PlanTempusAdmin/Pages/Forgejo/Index.cshtml
Janus C. H. Knudsen 77d35ff965 Initial commit
2026-02-03 00:17:08 +01:00

475 lines
16 KiB
Text

@page
@model PlanTempusAdmin.Pages.Forgejo.IndexModel
@{
ViewData["Title"] = "Forgejo Oversigt";
}
<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>
}
<style>
.dashboard-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.dashboard-col {
display: flex;
flex-direction: column;
gap: 16px;
}
.status-detail {
font-size: 10px;
color: var(--muted-color);
margin-top: 4px;
}
.pulse {
animation: pulse 1.5s ease-in-out infinite;
color: var(--warning-color);
}
@@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.running-card {
border-color: var(--warning-color);
}
.running-card .card-header {
background: linear-gradient(90deg, rgba(240, 165, 0, 0.1), transparent);
}
.error-card {
border-color: var(--danger-color);
}
.error-card .card-header {
background: linear-gradient(90deg, rgba(220, 53, 69, 0.1), transparent);
}
.stat-bars {
display: flex;
flex-direction: column;
gap: 8px;
}
.stat-bar-item {
display: flex;
align-items: center;
gap: 8px;
}
.stat-bar-label {
width: 70px;
font-size: 11px;
}
.stat-bar {
flex: 1;
height: 8px;
background: var(--border-color);
border-radius: 4px;
overflow: hidden;
}
.stat-bar-fill {
height: 100%;
border-radius: 4px;
}
.stat-bar-value {
width: 30px;
text-align: right;
font-size: 11px;
font-weight: bold;
}
.ci-stats {
display: flex;
justify-content: space-around;
text-align: center;
}
.ci-stat-value {
font-size: 24px;
font-weight: bold;
}
.ci-stat-value.success { color: var(--success-color); }
.ci-stat-value.danger { color: var(--danger-color); }
.ci-stat-label {
font-size: 10px;
color: var(--muted-color);
text-transform: uppercase;
}
.ci-rate-bar {
height: 8px;
background: var(--danger-color);
border-radius: 4px;
overflow: hidden;
}
.ci-rate-success {
height: 100%;
background: var(--success-color);
}
.ci-rate-label {
text-align: center;
font-size: 10px;
color: var(--muted-color);
margin-top: 4px;
}
.compact-list {
padding: 8px 16px !important;
}
.list-item {
padding: 8px 0;
border-bottom: 1px solid var(--border-color);
}
.list-item:last-child {
border-bottom: none;
}
.item-main {
display: flex;
align-items: center;
gap: 8px;
}
.item-meta {
font-size: 10px;
color: var(--muted-color);
margin-top: 4px;
padding-left: 45px;
}
.success { color: var(--success-color); }
.danger { color: var(--danger-color); }
@@media (max-width: 1200px) {
.dashboard-grid {
grid-template-columns: 1fr;
}
}
</style>
@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);
}