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"; 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"> <div id="dashboard-stats">
@await Html.PartialAsync("_DashboardStats", Model) @await Html.PartialAsync("_DashboardStats", Model)
</div> </div>
<div class="row g-4 mt-4"> <div class="row g-4 mt-4">
<div class="col-12"> <div class="col-12">
<div class="card p-3"> <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"> <div class="bg-secondary rounded p-5 text-center text-muted">
<i class="bi bi-bar-chart-line" style="font-size: 2rem;"></i><br /> <i class="bi bi-bar-chart-line" style="font-size: 2rem;"></i><br />
(Diagramm folgt hier) (Diagramm folgt hier)
@@ -23,9 +26,17 @@
<div class="col-12"> <div class="col-12">
<div class="card p-3"> <div class="card p-3">
<h2 class="h5"><i class="bi bi-person-circle me-2"></i>Systeminfo</h2> <h2 class="h5">
<p><i class="bi bi-person-badge me-1"></i>Benutzer: <strong>@User.FindFirst("preferred_username")?.Value</strong></p> <i class="bi bi-person-circle me-2"></i>Systeminfo
<p><i class="bi bi-clock me-1"></i>Letzter Login: <strong>@Model.LastLogin.ToString("g")</strong></p> </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> </div>
</div> </div>

View File

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

View File

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

View File

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