293 lines
10 KiB
Text
293 lines
10 KiB
Text
|
|
@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");
|
||
|
|
}
|
||
|
|
}
|