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
206 lines
8 KiB
Text
206 lines
8 KiB
Text
@page
|
|
@model PlanTempusAdmin.Pages.Forgejo.RepositoriesModel
|
|
@{
|
|
ViewData["Title"] = "Forgejo Repositories";
|
|
}
|
|
|
|
@section Styles {
|
|
<link rel="stylesheet" href="~/css/pages/forgejo.css" asp-append-version="true" />
|
|
}
|
|
|
|
<div class="page-header">
|
|
<h1 class="page-title">Repositories</h1>
|
|
<p class="page-subtitle">Alle repositories med backup 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 backedUp = Model.Repositories.Count(r => Model.BackupStatus.ContainsKey(r.FullName.ToLower()));
|
|
var notBackedUp = Model.Repositories.Count - backedUp;
|
|
|
|
<!-- Stats -->
|
|
<div class="status-grid">
|
|
<div class="status-item">
|
|
<div class="status-label">Total Repos</div>
|
|
<div class="status-value">@Model.Repositories.Count</div>
|
|
</div>
|
|
<div class="status-item">
|
|
<div class="status-label">Med Backup</div>
|
|
<div class="status-value success">@backedUp</div>
|
|
</div>
|
|
<div class="status-item">
|
|
<div class="status-label">Mangler Backup</div>
|
|
<div class="status-value @(notBackedUp > 0 ? "warning" : "")">@notBackedUp</div>
|
|
</div>
|
|
<div class="status-item">
|
|
<div class="status-label">Total Størrelse</div>
|
|
<div class="status-value">@FormatSize(Model.Repositories.Sum(r => r.Size) * 1024)</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filter Tabs -->
|
|
<div class="filter-tabs mt-2">
|
|
<button class="filter-tab active" onclick="filterRepos('all')">Alle (@Model.Repositories.Count)</button>
|
|
<button class="filter-tab" onclick="filterRepos('backed-up')">Med Backup (@backedUp)</button>
|
|
<button class="filter-tab" onclick="filterRepos('not-backed-up')">Mangler Backup (@notBackedUp)</button>
|
|
<button class="filter-tab" onclick="filterRepos('private')">Private (@Model.Repositories.Count(r => r.IsPrivate))</button>
|
|
</div>
|
|
|
|
<div class="card mt-2">
|
|
<table class="table" id="repos-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Repository</th>
|
|
<th>Type</th>
|
|
<th>Størrelse</th>
|
|
<th>Issues</th>
|
|
<th>PRs</th>
|
|
<th>Opdateret</th>
|
|
<th>Backup Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach (var repo in Model.Repositories)
|
|
{
|
|
var hasBackup = Model.BackupStatus.TryGetValue(repo.FullName.ToLower(), out var backupInfo);
|
|
var rowClass = hasBackup ? "backed-up" : "not-backed-up";
|
|
if (repo.IsPrivate) { rowClass += " private"; }
|
|
|
|
<tr class="repo-row @rowClass" data-name="@repo.FullName.ToLower()">
|
|
<td>
|
|
<code>@repo.FullName</code>
|
|
@if (repo.Description != null)
|
|
{
|
|
<div class="repo-desc">@TruncateText(repo.Description, 60)</div>
|
|
}
|
|
</td>
|
|
<td>
|
|
@if (repo.IsPrivate) { <span class="badge">privat</span> }
|
|
@if (repo.IsFork) { <span class="badge">fork</span> }
|
|
@if (repo.IsMirror) { <span class="badge">mirror</span> }
|
|
@if (repo.IsArchived) { <span class="badge badge-warning">arkiveret</span> }
|
|
@if (!repo.IsPrivate && !repo.IsFork && !repo.IsMirror && !repo.IsArchived) { <span class="badge badge-success">public</span> }
|
|
</td>
|
|
<td>@repo.SizeFormatted</td>
|
|
<td>
|
|
@if (repo.OpenIssues > 0)
|
|
{
|
|
<span class="text-warning">@repo.OpenIssues</span>
|
|
}
|
|
else
|
|
{
|
|
<span class="text-muted">0</span>
|
|
}
|
|
</td>
|
|
<td>
|
|
@if (repo.OpenPulls > 0)
|
|
{
|
|
<span class="text-warning">@repo.OpenPulls</span>
|
|
}
|
|
else
|
|
{
|
|
<span class="text-muted">0</span>
|
|
}
|
|
</td>
|
|
<td>@FormatTimeAgo(repo.UpdatedAt)</td>
|
|
<td>
|
|
@if (hasBackup)
|
|
{
|
|
<span class="badge badge-success">OK</span>
|
|
<span class="backup-detail">
|
|
@FormatTimeAgo(backupInfo.LastBackup) · @FormatSize(backupInfo.LastSize)
|
|
</span>
|
|
}
|
|
else
|
|
{
|
|
<span class="badge badge-warning">MANGLER</span>
|
|
}
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Missing Backups Alert -->
|
|
@if (notBackedUp > 0)
|
|
{
|
|
<div class="card mt-2 warning-card">
|
|
<div class="card-header">Repositories uden backup</div>
|
|
<div class="card-body">
|
|
<p style="margin-bottom: 12px;">Følgende repositories har ingen backup registreret:</p>
|
|
<div class="missing-list">
|
|
@foreach (var repo in Model.Repositories.Where(r => !Model.BackupStatus.ContainsKey(r.FullName)))
|
|
{
|
|
<code class="missing-repo">@repo.FullName</code>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
<script>
|
|
function filterRepos(filter) {
|
|
// Update active tab
|
|
document.querySelectorAll('.filter-tab').forEach(tab => tab.classList.remove('active'));
|
|
event.target.classList.add('active');
|
|
|
|
// Filter rows
|
|
document.querySelectorAll('.repo-row').forEach(row => {
|
|
let show = false;
|
|
switch (filter) {
|
|
case 'all':
|
|
show = true;
|
|
break;
|
|
case 'backed-up':
|
|
show = row.classList.contains('backed-up');
|
|
break;
|
|
case 'not-backed-up':
|
|
show = row.classList.contains('not-backed-up') && !row.classList.contains('backed-up');
|
|
break;
|
|
case 'private':
|
|
show = row.classList.contains('private');
|
|
break;
|
|
}
|
|
row.classList.toggle('hidden', !show);
|
|
});
|
|
}
|
|
</script>
|
|
|
|
@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 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);
|
|
|
|
string TruncateText(string text, int maxLength)
|
|
{
|
|
if (string.IsNullOrEmpty(text)) return "";
|
|
return text.Length <= maxLength ? text : text.Substring(0, maxLength) + "...";
|
|
}
|
|
}
|