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

View file

@ -0,0 +1,292 @@
@page
@model PlanTempusAdmin.Pages.Forgejo.ActionsModel
@{
ViewData["Title"] = "Forgejo Actions";
}
<div class="page-header">
<h1 class="page-title">CI/CD Actions</h1>
<p class="page-subtitle">Workflow runs og statistik</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 running = Model.Runs.Count(r => r.Status == 2);
var successful = Model.Runs.Count(r => r.Status == 3);
var failed = Model.Runs.Count(r => r.Status == 4);
<!-- Stats -->
<div class="status-grid">
<div class="status-item">
<div class="status-label">Status</div>
<div class="status-value @(running > 0 ? "warning" : "success")">
@if (running > 0)
{
<span class="pulse">●</span> @running <text> KØRER</text>
}
else
{
<text>IDLE</text>
}
</div>
</div>
<div class="status-item">
<div class="status-label">Workflows</div>
<div class="status-value">@Model.Stats.Count</div>
</div>
<div class="status-item">
<div class="status-label">Success Rate</div>
<div class="status-value @(Model.Stats.Count > 0 ? (Model.Stats.Average(s => s.SuccessRate) >= 90 ? "success" : "warning") : "")">
@(Model.Stats.Count > 0 ? Model.Stats.Average(s => s.SuccessRate).ToString("0") : "0")%
</div>
</div>
<div class="status-item">
<div class="status-label">Viste Runs</div>
<div class="status-value">@Model.Runs.Count</div>
</div>
</div>
<!-- Running Now -->
@if (running > 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>Branch</th>
<th>Trigger</th>
<th>Startet</th>
<th>Varighed</th>
</tr>
</thead>
<tbody>
@foreach (var run in Model.Runs.Where(r => r.Status == 2))
{
<tr>
<td><code>@run.FullRepoName</code></td>
<td>@run.WorkflowId</td>
<td><code>@run.Ref.Replace("refs/heads/", "").Replace("refs/tags/", "tag:")</code></td>
<td>
<span class="badge">@run.Event</span>
@if (!string.IsNullOrEmpty(run.TriggerUser))
{
<span class="text-muted">by @run.TriggerUser</span>
}
</td>
<td>@run.Started?.ToString("HH:mm:ss")</td>
<td class="warning">@FormatDuration(run.Duration)</td>
</tr>
}
</tbody>
</table>
</div>
}
<div class="dashboard-grid mt-2">
<!-- Left Column: Workflow Stats -->
<div class="dashboard-col">
<div class="card">
<div class="card-header">Workflow Statistik</div>
<table class="table">
<thead>
<tr>
<th>Workflow</th>
<th>Repo</th>
<th>Runs</th>
<th>Rate</th>
<th>Avg. Tid</th>
<th>Sidst</th>
</tr>
</thead>
<tbody>
@foreach (var stat in Model.Stats.OrderByDescending(s => s.TotalRuns))
{
<tr>
<td><code>@stat.WorkflowId</code></td>
<td><code>@stat.RepoName</code></td>
<td>
<span class="success">@stat.Successful</span>
@if (stat.Failed > 0)
{
<span class="text-danger">/ @stat.Failed</span>
}
</td>
<td>
<div class="rate-bar">
<div class="rate-fill @(stat.SuccessRate >= 90 ? "good" : stat.SuccessRate >= 70 ? "warn" : "bad")"
style="width: @stat.SuccessRate.ToString("0")%"></div>
</div>
<span class="rate-text">@stat.SuccessRate.ToString("0")%</span>
</td>
<td>@(stat.AvgDurationSeconds.HasValue ? FormatDuration(TimeSpan.FromSeconds(stat.AvgDurationSeconds.Value)) : "-")</td>
<td>@FormatTimeAgo(stat.LastRun)</td>
</tr>
}
</tbody>
</table>
</div>
</div>
<!-- Right Column: Recent Runs -->
<div class="dashboard-col">
<div class="card">
<div class="card-header">Seneste Workflow Runs</div>
<table class="table">
<thead>
<tr>
<th>Status</th>
<th>Repository</th>
<th>Workflow</th>
<th>Event</th>
<th>Tid</th>
<th>Varighed</th>
</tr>
</thead>
<tbody>
@foreach (var run in Model.Runs.Take(50))
{
<tr class="@(run.Status == 4 ? "failed-row" : "")">
<td>
@switch (run.Status)
{
case 1:
<span class="badge">VENTER</span>
break;
case 2:
<span class="badge badge-warning">KØRER</span>
break;
case 3:
<span class="badge badge-success">OK</span>
break;
case 4:
<span class="badge badge-danger">FEJL</span>
break;
case 5:
<span class="badge">ANNULLERET</span>
break;
case 6:
<span class="badge">SKIPPED</span>
break;
default:
<span class="badge">@run.StatusText</span>
break;
}
</td>
<td><code>@run.FullRepoName</code></td>
<td>@run.WorkflowId</td>
<td>
<span class="badge">@run.Event</span>
</td>
<td>@FormatTimeAgo(run.Created)</td>
<td>@FormatDuration(run.Duration)</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
}
<style>
.dashboard-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.dashboard-col {
display: flex;
flex-direction: column;
gap: 16px;
}
.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);
}
.rate-bar {
display: inline-block;
width: 40px;
height: 6px;
background: var(--border-color);
border-radius: 3px;
overflow: hidden;
vertical-align: middle;
}
.rate-fill {
height: 100%;
}
.rate-fill.good { background: var(--success-color); }
.rate-fill.warn { background: var(--warning-color); }
.rate-fill.bad { background: var(--danger-color); }
.rate-text {
font-size: 10px;
margin-left: 4px;
}
.failed-row {
background: rgba(220, 53, 69, 0.05);
}
.success { color: var(--success-color); }
.warning { color: var(--warning-color); }
@@media (max-width: 1400px) {
.dashboard-grid {
grid-template-columns: 1fr;
}
}
</style>
@functions {
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 HH:mm");
}
}