Merge pull request 'Umbau auf Bootstrap 5 + Gerüst für Diagramme' (#41) from feature/data-visualisation into development

Reviewed-on: daniel-hbn/Watcher#41
This commit is contained in:
2025-06-24 09:37:59 +00:00
7 changed files with 227 additions and 67 deletions

View File

@@ -3,17 +3,20 @@
ViewData["Title"] = "Dashboard";
}
<h1 class="mb-4"><i class="bi bi-speedometer2 me-2"></i>Dashboard</h1>
<h1 class="mb-4">
<i class="bi bi-speedometer2 me-2"></i>Dashboard
</h1>
<div id="dashboard-stats">
@await Html.PartialAsync("_DashboardStats", Model)
</div>
<div class="row g-4 mt-4">
<div class="col-12">
<div class="card p-3">
<h2 class="h5"><i class="bi bi-graph-up me-2"></i>Uptime letzte 24h</h2>
<h2 class="h5">
<i class="bi bi-graph-up me-2"></i>Uptime letzte 24h
</h2>
<div class="bg-secondary rounded p-5 text-center text-muted">
<i class="bi bi-bar-chart-line" style="font-size: 2rem;"></i><br />
(Diagramm folgt hier)
@@ -23,9 +26,17 @@
<div class="col-12">
<div class="card p-3">
<h2 class="h5"><i class="bi bi-person-circle me-2"></i>Systeminfo</h2>
<p><i class="bi bi-person-badge me-1"></i>Benutzer: <strong>@User.FindFirst("preferred_username")?.Value</strong></p>
<p><i class="bi bi-clock me-1"></i>Letzter Login: <strong>@Model.LastLogin.ToString("g")</strong></p>
<h2 class="h5">
<i class="bi bi-person-circle me-2"></i>Systeminfo
</h2>
<p>
<i class="bi bi-person-badge me-1"></i>
Benutzer: <strong>@User.FindFirst("preferred_username")?.Value</strong>
</p>
<p>
<i class="bi bi-clock me-1"></i>
Letzter Login: <strong>@Model.LastLogin.ToString("g")</strong>
</p>
</div>
</div>
</div>

View File

@@ -1,17 +1,25 @@
@model Watcher.ViewModels.DashboardViewModel
<div class="grid grid-cols-2 gap-6">
<div class="bg-white shadow rounded-2xl p-4">
<h2 class="text-xl font-semibold mb-2">Server</h2>
<p>🟢 Online: <strong>@Model.ActiveServers</strong></p>
<p>🔴 Offline: <strong>@Model.OfflineServers</strong></p>
<a href="/Server/Overview" class="text-blue-500 hover:underline mt-2 inline-block">→ Zu den Servern</a>
<div class="row g-4">
<div class="col-12 col-md-6">
<div class="bg-white shadow rounded-3 p-4 h-100">
<h2 class="h5 fw-semibold mb-2">Server</h2>
<p>🟢 Online: <strong>@Model.ActiveServers</strong></p>
<p>🔴 Offline: <strong>@Model.OfflineServers</strong></p>
<a href="/Server/Overview" class="text-primary text-decoration-none mt-2 d-inline-block">
→ Zu den Servern
</a>
</div>
</div>
<div class="bg-white shadow rounded-2xl p-4">
<h2 class="text-xl font-semibold mb-2">Container</h2>
<p>🟢 Laufend: <strong>@Model.RunningContainers</strong></p>
<p>🔴 Fehlerhaft: <strong>@Model.FailedContainers</strong></p>
<a href="/Container/Overview" class="text-blue-500 hover:underline mt-2 inline-block">→ Zu den Containern</a>
<div class="col-12 col-md-6">
<div class="bg-white shadow rounded-3 p-4 h-100">
<h2 class="h5 fw-semibold mb-2">Container</h2>
<p>🟢 Laufend: <strong>@Model.RunningContainers</strong></p>
<p>🔴 Fehlerhaft: <strong>@Model.FailedContainers</strong></p>
<a href="/Container/Overview" class="text-primary text-decoration-none mt-2 d-inline-block">
→ Zu den Containern
</a>
</div>
</div>
</div>

View File

@@ -1,55 +1,69 @@
@model IEnumerable<Watcher.Models.Server>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@foreach (var s in Model)
{
<div class="bg-blue rounded-xl shadow p-5 border border-gray-200 flex flex-col gap-4">
<div class="flex justify-between items-center">
<h2 class="text-lg font-semibold text-gray-800">
<i class="bi bi-cpu me-1 text-gray-600"></i>(#@s.Id) @s.Name
</h2>
<span class="text-sm px-2 py-1 rounded font-medium
@(s.IsOnline ? "bg-green-100 text-green-700" : "bg-red-100 text-red-700")">
<i class="bi @(s.IsOnline ? "bi-check-circle" : "bi-x-circle") me-1"></i>
@(s.IsOnline ? "Online" : "Offline")
</span>
</div>
<div class="flex gap-6">
<!-- Linke Seite: Infos, nimmt ca. 40% Breite -->
<div class="flex-1 max-w-[40%] text-sm space-y-1 text-gray-700">
<div><i class="bi bi-globe me-1 text-gray-500"></i><strong>IP:</strong> @s.IPAddress</div>
<div><i class="bi bi-hdd me-1 text-gray-500"></i><strong>Typ:</strong> @s.Type</div>
<div><i class="bi bi-calendar-check me-1 text-gray-500"></i><strong>Erstellt:</strong>
@s.CreatedAt.ToLocalTime().ToString("dd.MM.yyyy HH:mm")</div>
<div><i class="bi bi-clock me-1 text-gray-500"></i><strong>Last-Seen:</strong>
@s.LastSeen.ToLocalTime().ToString("dd.MM.yyyy HH:mm")</div>
<div class="container py-4">
<div class="row g-4">
@foreach (var s in Model)
{
<div class="col-12 col-sm-6">
<div class="card h-100 border-secondary shadow-sm">
<div class="card-body d-flex flex-column gap-3">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="card-title text-dark mb-0">
<i class="bi bi-pc-display me-2 text-muted"></i>(#@s.Id) @s.Name
</h5>
<span class="badge
@(s.IsOnline ? "bg-success text-light" : "bg-danger text-light")">
<i class="bi @(s.IsOnline ? "bi-check-circle" : "bi-x-circle") me-1"></i>
@(s.IsOnline ? "Online" : "Offline")
</span>
</div>
<!-- Rechte Seite: Zwei Diagramme nebeneinander, je ca. 50% von 60% = 30% insgesamt -->
<div class="flex-1 flex gap-4 max-w-[60%]">
<div class="row mb-3">
<div class="col-md-5 text-muted small">
<div><i class="bi bi-globe me-1"></i><strong>IP:</strong> @s.IPAddress</div>
<div><i class="bi bi-pc-display me-1"></i><strong>Typ:</strong> @s.Type</div>
<div><i class="bi bi-calendar-check me-1"></i><strong>Erstellt:</strong> @s.CreatedAt.ToLocalTime().ToString("dd.MM.yyyy HH:mm")</div>
<div><i class="bi bi-clock me-1"></i><strong>Last-Seen:</strong> @s.LastSeen.ToLocalTime().ToString("dd.MM.yyyy HH:mm")</div>
<div><i class="bi bi-cpu me-1"></i><strong>CPU:</strong> @(s.CpuType ?? "not found") </div>
<div><i class="bi bi-cpu me-1"></i><strong>CPU-Kerne: </strong> @s.CpuCores </div>
<div><i class="bi bi-gpu-card me-1"></i><strong>GPU:</strong> @(s.GpuType ?? "not found") </div>
<div><i class="bi bi-memory me-1"></i><strong>RAM:</strong> @(s.RamSize) </div>
<div><i class="bi bi-hdd me-1"></i><strong>Disk Space:</strong> ... </div>
</div>
<div class="col-md-7">
<div class="card bg-secondary text-white p-3 h-100 d-flex flex-column justify-content-center align-items-center">
<h6 class="mb-3"><i class="bi bi-graph-up me-2"></i>Metrics letzte 24h</h6>
<div style="width: 100%; height: 250px;">
<canvas id="uptimeChart-@s.Id"></canvas>
</div>
</div>
</div>
</div>
</div>
<div class="flex justify-end gap-3">
<a asp-action="EditServer" asp-route-id="@s.Id" class="text-blue-600 hover:text-blue-800 font-medium">
<i class="bi bi-pencil-square me-1"></i>Bearbeiten
</a>
<div class="d-flex flex-wrap gap-2">
<a href="/Download/File/Linux/heartbeat" class="btn btn-success">
🖥️ Linux Agent
</a>
<form asp-action="Delete" asp-route-id="@s.Id" method="post"
onsubmit="return confirm('Diesen Server wirklich löschen?');">
<button type="submit" class="text-red-600 hover:text-red-800 font-medium">
<i class="bi bi-trash me-1"></i>Löschen
</button>
</form>
<a asp-action="#" asp-route-id="@s.Id" class="btn btn-outline-primary">
<i class="bi bi-pencil-square me-1"></i> Metrics
</a>
<a href="/Download/File/Linux/heartbeat" class="btn btn-success">
🖥️ Linux Tool herunterladen
</a>
<a asp-action="EditServer" asp-route-id="@s.Id" class="btn btn-outline-primary">
<i class="bi bi-pencil-square me-1"></i> Bearbeiten
</a>
<form asp-action="Delete" asp-route-id="@s.Id" method="post" onsubmit="return confirm('Diesen Server wirklich löschen?');" class="m-0">
<button type="submit" class="btn btn-outline-danger">
<i class="bi bi-trash me-1"></i> Löschen
</button>
</form>
</div>
</div>
</div>
}
</div>
</div>
}
</div>
</div>

View File

@@ -3,13 +3,13 @@
ViewData["Title"] = "Serverübersicht";
}
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold">
<i class="bi bi-hdd-network me-2 text-blue-700"></i>Serverübersicht
<div class="d-flex align-items-center justify-content-between mb-4">
<h1 class="h2 fw-bold mb-0">
<i class="bi bi-hdd-network me-2 text-primary"></i>Serverübersicht
</h1>
<a asp-controller="Server" asp-action="AddServer"
class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded transition">
<i class="bi bi-plus-circle me-1"></i>Server hinzufügen
class="btn btn-primary d-flex align-items-center gap-1">
<i class="bi bi-plus-circle"></i> Server hinzufügen
</a>
</div>
@@ -18,6 +18,9 @@
</div>
@section Scripts {
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="~/js/server_uptime.js"></script>
<script>
async function loadServerCards() {
try {

View File

@@ -0,0 +1,89 @@
CREATE TABLE "__EFMigrationsHistory" (
"MigrationId" TEXT NOT NULL CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY,
"ProductVersion" TEXT NOT NULL
);
CREATE TABLE "Tags" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_Tags" PRIMARY KEY AUTOINCREMENT,
"Name" TEXT NULL
);
CREATE TABLE sqlite_sequence(name,seq);
CREATE TABLE "Servers" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_Servers" PRIMARY KEY AUTOINCREMENT,
"CpuCores" INTEGER NOT NULL,
"CpuType" TEXT NULL,
"CreatedAt" TEXT NOT NULL,
"Description" TEXT NULL,
"GpuType" TEXT NULL,
"IPAddress" TEXT NOT NULL,
"IsOnline" INTEGER NOT NULL,
"LastSeen" TEXT NOT NULL,
"Name" TEXT NOT NULL,
"RamSize" REAL NOT NULL,
"TagId" INTEGER NULL,
"Type" TEXT NOT NULL, "IsVerified" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "FK_Servers_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id")
);
CREATE TABLE "LogEvents" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_LogEvents" PRIMARY KEY AUTOINCREMENT,
"ContainerId" INTEGER NULL,
"Level" TEXT NULL,
"Message" TEXT NULL,
"ServerId" INTEGER NULL,
"Timestamp" TEXT NOT NULL,
CONSTRAINT "FK_LogEvents_Containers_ContainerId" FOREIGN KEY ("ContainerId") REFERENCES "Containers" ("Id"),
CONSTRAINT "FK_LogEvents_Servers_ServerId" FOREIGN KEY ("ServerId") REFERENCES "Servers" ("Id")
);
CREATE TABLE "Images" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_Images" PRIMARY KEY AUTOINCREMENT,
"Name" TEXT NULL,
"Tag" TEXT NULL
);
CREATE TABLE "Containers" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_Containers" PRIMARY KEY AUTOINCREMENT,
"CreatedAt" TEXT NOT NULL,
"Hostname" TEXT NOT NULL,
"ImageId" INTEGER NULL,
"IsRunning" INTEGER NOT NULL,
"Name" TEXT NOT NULL,
"Status" TEXT NOT NULL,
"TagId" INTEGER NULL,
"Type" TEXT NOT NULL,
CONSTRAINT "FK_Containers_Images_ImageId" FOREIGN KEY ("ImageId") REFERENCES "Images" ("Id"),
CONSTRAINT "FK_Containers_Tags_TagId" FOREIGN KEY ("TagId") REFERENCES "Tags" ("Id")
);
CREATE TABLE "Metrics" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_Metrics" PRIMARY KEY AUTOINCREMENT,
"CPU_Load" REAL NOT NULL,
"CPU_Temp" REAL NOT NULL,
"DISK_Size" REAL NOT NULL,
"DISK_Temp" REAL NOT NULL,
"DISK_Usage" REAL NOT NULL,
"GPU_Load" REAL NOT NULL,
"GPU_Temp" REAL NOT NULL,
"GPU_Vram_Size" REAL NOT NULL,
"GPU_Vram_Usage" REAL NOT NULL,
"NET_In" REAL NOT NULL,
"NET_Out" REAL NOT NULL,
"RAM_Load" REAL NOT NULL,
"RAM_Size" REAL NOT NULL,
"ServerId" INTEGER NULL,
"Timestamp" TEXT NOT NULL
);
CREATE TABLE "Users" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_Users" PRIMARY KEY AUTOINCREMENT,
"Email" TEXT NULL,
"IdentityProvider" TEXT NOT NULL,
"LastLogin" TEXT NOT NULL,
"OIDC_Id" TEXT NULL,
"Password" TEXT NOT NULL,
"Username" TEXT NOT NULL
);
INSERT INTO __EFMigrationsHistory (MigrationId,ProductVersion) VALUES ('20250617153602_InitialMigration','8.0.6');
INSERT INTO __EFMigrationsHistory (MigrationId,ProductVersion) VALUES ('20250617165126_ServerPrimaryKey','8.0.6');
INSERT INTO __EFMigrationsHistory (MigrationId,ProductVersion) VALUES ('20250617174242_UserPasswordAdded','8.0.6');
INSERT INTO __EFMigrationsHistory (MigrationId,ProductVersion) VALUES ('20250621124832_DB-Update Issue#32','8.0.6');
INSERT INTO __EFMigrationsHistory (MigrationId,ProductVersion) VALUES ('20250621125157_DB-Update Issue#32 IsVerified-Servers','8.0.6');
INSERT INTO Servers (Id,CpuCores,CpuType,CreatedAt,Description,GpuType,IPAddress,IsOnline,LastSeen,Name,RamSize,TagId,Type,IsVerified) VALUES ('2','0','','2025-06-21 13:56:23.5504821','','','192.168.178.158','1','2025-06-23 14:30:23.9455939','Ubuntu Workstation','0',NULL,'Standalone','0');
INSERT INTO Servers (Id,CpuCores,CpuType,CreatedAt,Description,GpuType,IPAddress,IsOnline,LastSeen,Name,RamSize,TagId,Type,IsVerified) VALUES ('3','0','','2025-06-23 14:03:08.8875782','','','82.29.178.161','0','0001-01-01 00:00:00','hostinger-vps-1','0',NULL,'VPS','0');
INSERT INTO Servers (Id,CpuCores,CpuType,CreatedAt,Description,GpuType,IPAddress,IsOnline,LastSeen,Name,RamSize,TagId,Type,IsVerified) VALUES ('4','0','','2025-06-23 14:07:49.0219821','','','192.168.178.68','0','0001-01-01 00:00:00','Unraid NAS','0',NULL,'Standalone','0');
INSERT INTO Users (Id,Email,IdentityProvider,LastLogin,OIDC_Id,Password,Username) VALUES ('1','','local','2025-06-21 13:54:26.4374285','','$2a$11$iWF.vOrHuzgelKm3xWeJouVtmUd7LemK11yDxlw9t.YbLUZtJWRH6','admin');

View File

View File

@@ -0,0 +1,35 @@
document.addEventListener("DOMContentLoaded", () => {
const canvases = document.querySelectorAll("canvas[id^='uptimeChart-']");
canvases.forEach(canvas => {
const ctx = canvas.getContext('2d');
// Hier kannst du Daten dynamisch anpassen, ggf. mit data-* Attributen
new Chart(ctx, {
type: 'line',
data: {
labels: ['0h', '6h', '12h', '18h', '24h'],
datasets: [{
label: 'Uptime (%)',
data: [100, 90, 80, 100, 95], // Beispielwerte
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.3,
fill: true,
}]
},
options: {
responsive: true,
plugins: {
legend: { display: false }
},
scales: {
y: {
beginAtZero: true,
max: 100
}
}
}
});
});
});