feat: add consolidated Übersicht (Overview) dashboard tab with real-time mini-cards
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 13s
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 13s
This commit is contained in:
43
admin.html
43
admin.html
@@ -6,7 +6,7 @@
|
||||
<title>MischLabs Admin</title>
|
||||
<meta name="description" content="MischLabs dashboard administration.">
|
||||
<meta name="theme-color" content="#0f172a">
|
||||
<link rel="stylesheet" href="/style.css?v=23">
|
||||
<link rel="stylesheet" href="/style.css?v=24">
|
||||
</head>
|
||||
<body>
|
||||
<header class="shell hero admin-hero">
|
||||
@@ -48,6 +48,41 @@
|
||||
</div>
|
||||
|
||||
<div id="adminUser" style="display: none;"></div>
|
||||
|
||||
<!-- Dashboard Overview Tab -->
|
||||
<div class="admin-tab-content" id="card-overview">
|
||||
<div class="overview-grid">
|
||||
<div class="overview-mini-card" data-target-tab="services">
|
||||
<div class="mini-card-head">
|
||||
<span class="mini-card-icon">🌐</span>
|
||||
<h4>Dienste-Status</h4>
|
||||
</div>
|
||||
<div class="mini-card-body" id="mini-services-status">Lade...</div>
|
||||
</div>
|
||||
<div class="overview-mini-card" data-target-tab="disks">
|
||||
<div class="mini-card-head">
|
||||
<span class="mini-card-icon">💾</span>
|
||||
<h4>NAS Speicher</h4>
|
||||
</div>
|
||||
<div class="mini-card-body" id="mini-disks-status">Lade...</div>
|
||||
</div>
|
||||
<div class="overview-mini-card" data-target-tab="watchtower">
|
||||
<div class="mini-card-head">
|
||||
<span class="mini-card-icon">🐳</span>
|
||||
<h4>Watchtower</h4>
|
||||
</div>
|
||||
<div class="mini-card-body" id="mini-watchtower-status">Lade...</div>
|
||||
</div>
|
||||
<div class="overview-mini-card" data-target-tab="containers">
|
||||
<div class="mini-card-head">
|
||||
<span class="mini-card-icon">📦</span>
|
||||
<h4>Container</h4>
|
||||
</div>
|
||||
<div class="mini-card-body" id="mini-containers-status">Lade...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ops-grid" id="opsGrid">
|
||||
<section class="ops-card ops-card-wide admin-tab-content" id="card-services">
|
||||
<div class="ops-card-head">
|
||||
@@ -109,6 +144,10 @@
|
||||
<button class="drawer-close-btn" id="drawerCloseBtn" aria-label="Menü schließen" type="button">×</button>
|
||||
</div>
|
||||
<nav class="drawer-nav">
|
||||
<button class="drawer-nav-item" data-tab="overview" type="button">
|
||||
<span class="drawer-nav-icon">📊</span>
|
||||
<span class="drawer-nav-text">Übersicht</span>
|
||||
</button>
|
||||
<button class="drawer-nav-item" data-tab="services" type="button">
|
||||
<span class="drawer-nav-icon">🌐</span>
|
||||
<span class="drawer-nav-text">Dienste-Status</span>
|
||||
@@ -143,6 +182,6 @@
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<script src="/admin.js?v=19" defer></script>
|
||||
<script src="/admin.js?v=20" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
83
admin.js
83
admin.js
@@ -394,6 +394,21 @@ function renderServicesStatus(services = []) {
|
||||
</a>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
const miniServices = document.getElementById('mini-services-status');
|
||||
if (miniServices) {
|
||||
let ledHtml = itemsToRender.map(item => {
|
||||
const ok = item.status.ok;
|
||||
return `<span class="mini-led ${ok ? 'mini-led-ok' : 'mini-led-down'}" title="${item.name}: ${ok ? 'Online' : 'Offline'}"></span>`;
|
||||
}).join('');
|
||||
miniServices.innerHTML = `
|
||||
<div class="mini-status-text">
|
||||
${offlineCount > 0 ? `<span style="color: var(--down); font-weight: 700;">${offlineCount} offline</span>` : `<span style="color: var(--ok); font-weight: 700;">Alle online</span>`}
|
||||
<span class="mini-status-total">(${totalCount} Dienste)</span>
|
||||
</div>
|
||||
<div class="mini-led-grid">${ledHtml}</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderDisks(disks = []) {
|
||||
@@ -416,6 +431,21 @@ function renderDisks(disks = []) {
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
const miniDisks = document.getElementById('mini-disks-status');
|
||||
if (miniDisks) {
|
||||
miniDisks.innerHTML = disks.map((disk) => {
|
||||
if (disk.error) return `<div class="mini-disk-row is-error"><strong>${disk.label}</strong>: Fehler</div>`;
|
||||
return `
|
||||
<div class="mini-disk-row">
|
||||
<div class="mini-disk-meta"><strong>${disk.label}</strong><span>${disk.usedPercent}%</span></div>
|
||||
<div class="usage-bar mini-bar" data-level="${disk.usedPercent >= 90 ? 'danger' : disk.usedPercent >= 80 ? 'warn' : 'ok'}">
|
||||
<span style="width: ${disk.usedPercent}%"></span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
}
|
||||
|
||||
function renderWatchtower(watchtower) {
|
||||
@@ -437,6 +467,23 @@ function renderWatchtower(watchtower) {
|
||||
<div class="ops-line"><strong>Status aktualisiert</strong><span>${statusGenerated}</span></div>
|
||||
`;
|
||||
|
||||
const miniWatchtower = document.getElementById('mini-watchtower-status');
|
||||
if (miniWatchtower) {
|
||||
let statusText = 'Keine Daten';
|
||||
let subText = '';
|
||||
if (lastManualWatchtowerRun) {
|
||||
statusText = lastManualWatchtowerRun.running ? 'Läuft manuell...' : 'Manuell beendet';
|
||||
subText = lastManualWatchtowerRun.startedAt ? new Date(lastManualWatchtowerRun.startedAt).toLocaleTimeString('de-DE') : '';
|
||||
} else if (officialSession) {
|
||||
statusText = 'Up-to-date';
|
||||
subText = `Lauf: ${officialSession.finishedAt ? new Date(officialSession.finishedAt).toLocaleDateString('de-DE') : '--'}`;
|
||||
}
|
||||
miniWatchtower.innerHTML = `
|
||||
<div class="mini-status-text"><span style="color: #67e8f9; font-weight: 700;">${statusText}</span></div>
|
||||
<div class="mini-status-sub">${subText}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (lastManualWatchtowerRun) {
|
||||
const run = lastManualWatchtowerRun;
|
||||
const session = run.session;
|
||||
@@ -498,6 +545,32 @@ function renderContainers(containers) {
|
||||
containerStatus.querySelectorAll('.restart-container').forEach((button) => {
|
||||
button.addEventListener('click', () => restartContainer(button.closest('.container-line')));
|
||||
});
|
||||
|
||||
const miniContainers = document.getElementById('mini-containers-status');
|
||||
if (miniContainers && Array.isArray(containers)) {
|
||||
const importantList = [
|
||||
'mischlabs',
|
||||
'watchtower',
|
||||
'gitea',
|
||||
'gitea-runner',
|
||||
'auth',
|
||||
'nextcloud_app',
|
||||
'vaultwarden',
|
||||
'photography-website',
|
||||
'michess',
|
||||
'calibre-web',
|
||||
'audiobookshelf'
|
||||
];
|
||||
const activeImportant = containers.filter(c => importantList.includes(c.name) && c.state === 'running').length;
|
||||
const totalImportant = containers.filter(c => importantList.includes(c.name)).length;
|
||||
miniContainers.innerHTML = `
|
||||
<div class="mini-status-text">
|
||||
<span style="color: var(--ok); font-weight: 700;">${activeImportant} / ${totalImportant}</span>
|
||||
<span class="mini-status-total">online</span>
|
||||
</div>
|
||||
<div class="mini-status-sub">${containers.length} Container insgesamt</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
async function restartContainer(row) {
|
||||
@@ -746,13 +819,14 @@ function initCollapsibleCards() {
|
||||
});
|
||||
}
|
||||
|
||||
let activeTab = localStorage.getItem('active_admin_tab') || 'services';
|
||||
let activeTab = localStorage.getItem('active_admin_tab') || 'overview';
|
||||
|
||||
function switchTab(tabId) {
|
||||
activeTab = tabId;
|
||||
localStorage.setItem('active_admin_tab', tabId);
|
||||
|
||||
const tabCards = {
|
||||
overview: document.getElementById('card-overview'),
|
||||
services: document.getElementById('card-services'),
|
||||
disks: document.getElementById('card-disks'),
|
||||
watchtower: document.getElementById('card-watchtower'),
|
||||
@@ -807,6 +881,13 @@ function initTabNavigation() {
|
||||
});
|
||||
});
|
||||
|
||||
// Setup click handlers for mini cards inside the Overview tab
|
||||
document.querySelectorAll('.overview-mini-card[data-target-tab]').forEach((card) => {
|
||||
card.addEventListener('click', () => {
|
||||
switchTab(card.dataset.targetTab);
|
||||
});
|
||||
});
|
||||
|
||||
switchTab(activeTab);
|
||||
}
|
||||
|
||||
|
||||
169
style.css
169
style.css
@@ -1268,3 +1268,172 @@ main {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Dashboard Overview Cards & Grid
|
||||
========================================================================== */
|
||||
.overview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 620px) {
|
||||
.overview-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.overview-mini-card {
|
||||
background: rgba(17, 22, 36, 0.45);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.overview-mini-card:hover {
|
||||
background: rgba(23, 29, 45, 0.65);
|
||||
border-color: rgba(103, 232, 249, 0.3);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.35), 0 0 15px rgba(103, 232, 249, 0.08);
|
||||
}
|
||||
|
||||
.mini-card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.mini-card-icon {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.mini-card-head h4 {
|
||||
margin: 0;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.mini-card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
font-size: 0.92rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.mini-status-text {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.mini-status-total {
|
||||
color: var(--dim);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.mini-status-sub {
|
||||
font-size: 0.8rem;
|
||||
color: var(--dim);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* LEDs for Services */
|
||||
.mini-led-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.mini-led {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.mini-led:hover {
|
||||
transform: scale(1.35);
|
||||
}
|
||||
|
||||
.mini-led-ok {
|
||||
background-color: var(--ok);
|
||||
box-shadow: 0 0 6px rgba(52, 211, 153, 0.4);
|
||||
}
|
||||
|
||||
.mini-led-down {
|
||||
background-color: var(--down);
|
||||
box-shadow: 0 0 6px rgba(251, 113, 133, 0.4);
|
||||
}
|
||||
|
||||
/* Disks Mini Rows */
|
||||
.mini-disk-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.mini-disk-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.mini-disk-row.is-error {
|
||||
color: var(--down);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.mini-disk-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.mini-disk-meta strong {
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Slim progress bars in Mini overview */
|
||||
.usage-bar.mini-bar {
|
||||
height: 5px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 99px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.usage-bar.mini-bar span {
|
||||
display: block;
|
||||
height: 100%;
|
||||
border-radius: 99px;
|
||||
transition: width 0.4s ease;
|
||||
}
|
||||
|
||||
.usage-bar.mini-bar[data-level="ok"] span {
|
||||
background-color: var(--ok);
|
||||
box-shadow: 0 0 4px rgba(52, 211, 153, 0.3);
|
||||
}
|
||||
|
||||
.usage-bar.mini-bar[data-level="warn"] span {
|
||||
background-color: var(--warn);
|
||||
box-shadow: 0 0 4px rgba(251, 191, 36, 0.3);
|
||||
}
|
||||
|
||||
.usage-bar.mini-bar[data-level="danger"] span {
|
||||
background-color: var(--down);
|
||||
box-shadow: 0 0 4px rgba(251, 113, 133, 0.3);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user