Extracts inline styles from Razor pages into modular CSS files Adds new CSS files for components and specific page styles Improves code organization and maintainability by separating styling concerns Updates layout to include new CSS files and optional style sections
322 lines
13 KiB
Text
322 lines
13 KiB
Text
@page
|
|
@model PlanTempusAdmin.Pages.Forgejo.IndexModel
|
|
@{
|
|
ViewData["Title"] = "Forgejo Oversigt";
|
|
}
|
|
|
|
@section Styles {
|
|
<link rel="stylesheet" href="~/css/pages/forgejo.css" asp-append-version="true" />
|
|
}
|
|
|
|
<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);
|
|
}
|