Files
ReifenSpaeth/Lagerbestand/Components/Pages/Status.razor
T

1545 lines
70 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@page "/status"
@implements IAsyncDisposable
@using Lagerbestand.Models
@using Lagerbestand.Components.Shared
@inject IConfiguration Configuration
@inject IAuthService AuthService
@inject IInventoryService InventoryService
@inject ICatalogService CatalogService
@inject ISiteSettingsService SiteSettingsService
@inject IFileStorageService FileStorageService
@inject ILexwareSyncService LexwareSyncService
@inject UserSessionService UserSession
@inject NavigationManager Navigation
@inject IJSRuntime JS
<PageTitle>System Status - Reifen Späth Lagerverwaltung</PageTitle>
<SeoMetaTags
Title="System Status - Reifen Späth Lagerverwaltung"
Description="System-Diagnose und Status-Informationen der Reifen Späth Lagerverwaltung."
Robots="noindex, nofollow" />
@if (!UserSession.IsAdmin)
{
<section class="status-container" role="alert" aria-labelledby="access-denied-title">
<div class="auth-required">
<div class="auth-icon" aria-hidden="true">??</div>
<h1 id="access-denied-title">Zugriff verweigert</h1>
<p>Sie haben keine Berechtigung für diese Seite.</p>
<a href="/tire-storage" class="btn btn-primary">Zur Lagerübersicht</a>
</div>
</section>
}
else
{
<main class="status-container" role="main">
<div class="status-header">
<div class="header-content">
<h1>System Status</h1>
<p class="header-subtitle">Entwickler-Informationen & Diagnose</p>
</div>
<button class="btn btn-outline-primary btn-sm" @onclick="RefreshStatus" disabled="@isLoading">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M23 4v6h-6"></path>
<path d="M1 20v-6h6"></path>
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>
</svg>
Aktualisieren
</button>
</div>
@if (isLoading)
{
<div class="loading-state">
<div class="spinner-border" role="status"></div>
<p>Lade Systemstatus...</p>
</div>
}
else
{
<div class="status-grid">
<!-- Application Info -->
<div class="status-card">
<div class="status-card-header">
<div class="card-title">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
<line x1="8" y1="21" x2="16" y2="21"></line>
<line x1="12" y1="17" x2="12" y2="21"></line>
</svg>
<h3>Anwendung</h3>
</div>
<span class="status-badge status-ok">
<span class="status-dot"></span>
Online
</span>
</div>
<div class="status-card-body">
<div class="status-item">
<span class="label">Name</span>
<span class="value">Lagerbestand</span>
</div>
<div class="status-item">
<span class="label">Version</span>
<span class="value">1.0.0</span>
</div>
<div class="status-item">
<span class="label">Framework</span>
<span class="value">.NET @Environment.Version</span>
</div>
<div class="status-item">
<span class="label">Umgebung</span>
<span class="value env-badge @(environment == "Development" ? "dev" : "prod")">@environment</span>
</div>
<div class="status-item">
<span class="label">Server-Zeit</span>
<span class="value">@DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss")</span>
</div>
<div class="status-item">
<span class="label">Uptime</span>
<span class="value highlight">@GetUptime()</span>
</div>
</div>
</div>
<!-- Database Info -->
<div class="status-card">
<div class="status-card-header">
<div class="card-title">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<ellipse cx="12" cy="5" rx="9" ry="3"></ellipse>
<path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"></path>
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path>
</svg>
<h3>Datenbank</h3>
</div>
<span class="status-badge @(dbConnected && (!storageSettings.IsEnabled || storageSettings.IsPathAccessible()) && (!lexwareSettings.IsEnabled || lexwareSettings.IsConfigured) ? "status-ok" : dbConnected ? "status-warning" : "status-error")">
<span class="status-dot"></span>
@if (dbConnected && (!storageSettings.IsEnabled || storageSettings.IsPathAccessible()) && (!lexwareSettings.IsEnabled || lexwareSettings.IsConfigured))
{
@:Verbunden
}
else if (!dbConnected)
{
@:Fehler
}
else if (storageSettings.IsEnabled && !storageSettings.IsPathAccessible())
{
@:NFS Fehler
}
else if (lexwareSettings.IsEnabled && !lexwareSettings.IsConfigured)
{
@:API Fehler
}
else
{
@:Teilweise
}
</span>
</div>
<div class="status-card-body">
<div class="status-item">
<span class="label">Server</span>
<span class="value mono">@dbServer</span>
</div>
<div class="status-item">
<span class="label">Datenbank</span>
<span class="value mono">@dbName</span>
</div>
<div class="status-item">
<span class="label">Status</span>
<span class="value @(dbConnected ? "text-success" : "text-danger")">
@(dbConnected ? "Verbunden" : dbError)
</span>
</div>
<div class="status-divider"></div>
<div class="status-item">
<span class="label">NFS/Dateispeicher</span>
<span class="value">
@if (storageSettings.IsEnabled)
{
<span class="text-success">Aktiviert</span>
}
else
{
<span class="text-muted">Deaktiviert</span>
}
</span>
</div>
@if (storageSettings.IsEnabled)
{
<div class="status-item">
<span class="label">NFS Pfad</span>
<span class="value mono small">@(string.IsNullOrWhiteSpace(storageSettings.BasePath) ? "–" : storageSettings.BasePath)</span>
</div>
<div class="status-item">
<span class="label">NFS Status</span>
<span class="value @(storageSettings.IsPathAccessible() ? "text-success" : "text-danger")">
@(storageSettings.IsPathAccessible() ? "Erreichbar" : "Nicht erreichbar")
</span>
</div>
<div class="status-item">
<span class="label">Max. Dateigröße</span>
<span class="value">@(storageSettings.MaxFileSizeBytes / (1024 * 1024)) MB</span>
</div>
}
<div class="status-divider"></div>
<div class="status-item">
<span class="label">Lexware API</span>
<span class="value">
@if (lexwareSettings.IsEnabled && lexwareSettings.IsConfigured)
{
<span class="text-success">Verbunden</span>
}
else if (lexwareSettings.IsEnabled && !lexwareSettings.IsConfigured)
{
<span class="text-danger">Nicht konfiguriert</span>
}
else
{
<span class="text-muted">Deaktiviert</span>
}
</span>
</div>
@if (lexwareSettings.IsEnabled)
{
<div class="status-item">
<span class="label">Letzte Synchronisierung</span>
<span class="value small">
@if (lexwareSettings.LastSyncAt.HasValue)
{
@lexwareSettings.LastSyncAt.Value.ToLocalTime().ToString("dd.MM.yyyy HH:mm")
}
else
{
<span class="text-muted">Noch nie</span>
}
</span>
</div>
@if (!string.IsNullOrEmpty(lexwareSettings.LastSyncResult))
{
<div class="status-item">
<span class="label">Letztes Ergebnis</span>
<span class="value small">@lexwareSettings.LastSyncResult</span>
</div>
}
}
</div>
</div>
<!-- Statistics -->
<div class="status-card">
<div class="status-card-header">
<div class="card-title">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="20" x2="18" y2="10"></line>
<line x1="12" y1="20" x2="12" y2="4"></line>
<line x1="6" y1="20" x2="6" y2="14"></line>
</svg>
<h3>Statistiken</h3>
</div>
</div>
<div class="status-card-body">
<div class="stats-grid">
<div class="stat-item">
<span class="stat-value">@userCount</span>
<span class="stat-label">Benutzer</span>
</div>
<div class="stat-item">
<span class="stat-value">@categoryCount</span>
<span class="stat-label">Kategorien</span>
</div>
<div class="stat-item">
<span class="stat-value">@inventoryCount</span>
<span class="stat-label">Artikel</span>
</div>
</div>
<div class="stats-detail">
<div class="stats-detail-item">
<span class="label">Aktive Benutzer</span>
<span class="value">@activeUserCount</span>
</div>
<div class="stats-detail-item">
<span class="label">Administratoren</span>
<span class="value">@adminCount</span>
</div>
</div>
</div>
</div>
<!-- Current User -->
<div class="status-card">
<div class="status-card-header">
<div class="card-title">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
<h3>Aktueller Benutzer</h3>
</div>
</div>
<div class="status-card-body">
<div class="status-item">
<span class="label">Benutzername</span>
<span class="value">@UserSession.Username</span>
</div>
<div class="status-item">
<span class="label">Anzeigename</span>
<span class="value">@UserSession.DisplayName</span>
</div>
<div class="status-item">
<span class="label">Rolle</span>
<span class="value role-badge">@UserSession.RoleName</span>
</div>
<div class="status-item">
<span class="label">User-ID</span>
<span class="value mono small">@UserSession.UserId</span>
</div>
<div class="status-item">
<span class="label">Letzter Login</span>
<span class="value">@(UserSession.CurrentUser?.LastLoginAt?.ToLocalTime().ToString("dd.MM.yyyy HH:mm") ?? "-")</span>
</div>
</div>
</div>
<!-- System Info -->
<div class="status-card">
<div class="status-card-header">
<div class="card-title">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect>
<rect x="9" y="9" width="6" height="6"></rect>
<line x1="9" y1="1" x2="9" y2="4"></line>
<line x1="15" y1="1" x2="15" y2="4"></line>
<line x1="9" y1="20" x2="9" y2="23"></line>
<line x1="15" y1="20" x2="15" y2="23"></line>
<line x1="20" y1="9" x2="23" y2="9"></line>
<line x1="20" y1="14" x2="23" y2="14"></line>
<line x1="1" y1="9" x2="4" y2="9"></line>
<line x1="1" y1="14" x2="4" y2="14"></line>
</svg>
<h3>System</h3>
</div>
</div>
<div class="status-card-body">
<div class="status-item">
<span class="label">Betriebssystem</span>
<span class="value">@Environment.OSVersion.Platform</span>
</div>
<div class="status-item">
<span class="label">OS Version</span>
<span class="value mono">@Environment.OSVersion.Version</span>
</div>
<div class="status-item">
<span class="label">Prozessoren</span>
<span class="value">@Environment.ProcessorCount Kerne</span>
</div>
<div class="status-item">
<span class="label">Architektur</span>
<span class="value">@(Environment.Is64BitProcess ? "64-Bit" : "32-Bit")</span>
</div>
<div class="status-item">
<span class="label">Speicher (App)</span>
<span class="value highlight">@GetMemoryUsage()</span>
</div>
<div class="status-item">
<span class="label">Working Set</span>
<span class="value mono">@GetWorkingSetMemory()</span>
</div>
</div>
</div>
<!-- Configuration -->
<div class="status-card">
<div class="status-card-header">
<div class="card-title">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
</svg>
<h3>Konfiguration</h3>
</div>
</div>
<div class="status-card-body">
<div class="status-item">
<span class="label">Admin-User</span>
<span class="value">@adminUsername</span>
</div>
<div class="status-item">
<span class="label">Base URL</span>
<span class="value mono small">@Navigation.BaseUri</span>
</div>
<div class="status-item">
<span class="label">Render Mode</span>
<span class="value">Interactive Server</span>
</div>
<div class="status-item">
<span class="label">Culture</span>
<span class="value mono">@System.Globalization.CultureInfo.CurrentCulture.Name</span>
</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="quick-actions">
<h3>Schnellaktionen</h3>
<div class="actions-grid">
<button class="action-btn" @onclick="ForceGarbageCollection" disabled="@isPerformingAction">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="1 4 1 10 7 10"></polyline>
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path>
</svg>
<span>Speicher bereinigen</span>
</button>
<a href="/users" class="action-btn">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
</svg>
<span>Benutzerverwaltung</span>
</a>
<button class="action-btn" @onclick="ToggleBannerSettings">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path>
</svg>
<span>Promo-Banner</span>
</button>
<button class="action-btn" @onclick="ToggleStorageSettings">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
</svg>
<span>Dateispeicher</span>
</button>
<button class="action-btn" @onclick="ToggleLexwareSettings">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
<line x1="17" y1="11" x2="23" y2="11"></line>
<line x1="20" y1="8" x2="20" y2="14"></line>
</svg>
<span>Lexware API</span>
</button>
<button class="action-btn" @onclick="ToggleTwoFactorSettings">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
</svg>
<span>2FA & SMTP</span>
</button>
<button class="action-btn" @onclick="ToggleSessionSettings">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
<span>Sitzung & Leerlauf</span>
</button>
</div>
</div>
@* Banner Settings Panel *@
@if (showBannerSettings)
{
<div class="banner-settings-panel">
<div class="panel-header">
<h3>Promo-Banner Einstellungen</h3>
<button class="btn-close" @onclick="ToggleBannerSettings">×</button>
</div>
<div class="panel-body">
<div class="form-group toggle-group">
<span class="toggle-label-text">Banner aktiviert</span>
<label class="toggle-container">
<input type="checkbox" @bind="bannerSettings.IsEnabled" />
<span class="toggle-switch"></span>
</label>
</div>
<div class="form-group">
<label>Titel</label>
<input type="text" class="form-control" @bind="bannerSettings.Title"
placeholder="z.B. REIFENWECHSELWOCHE" />
</div>
<div class="form-group">
<label>Nachricht</label>
<textarea class="form-control" rows="4" @bind="bannerSettings.Message"
placeholder="Nachricht für Ihre Kunden..."></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label>Start-Datum</label>
<input type="date" class="form-control"
@bind="bannerSettings.StartDate" />
</div>
<div class="form-group">
<label>End-Datum</label>
<input type="date" class="form-control"
@bind="bannerSettings.EndDate" />
</div>
</div>
<div class="form-group toggle-group">
<span class="toggle-label-text">Kontakt-Button anzeigen</span>
<label class="toggle-container">
<input type="checkbox" @bind="bannerSettings.ShowContactButton" />
<span class="toggle-switch"></span>
</label>
</div>
@* Status Info *@
<div class="banner-status-info">
<span class="status-indicator @(bannerSettings.ShouldShow() ? "active" : "inactive")">
@if (bannerSettings.ShouldShow())
{
<span class="status-icon active-icon"></span>
<span>Banner ist aktuell sichtbar</span>
}
else if (!bannerSettings.IsEnabled)
{
<span class="status-icon disabled-icon"></span>
<span>Banner ist deaktiviert</span>
}
else if (bannerSettings.StartDate.HasValue && DateTime.Now.Date < bannerSettings.StartDate.Value.Date)
{
<span class="status-icon scheduled-icon"></span>
<span>Startet am @bannerSettings.StartDate.Value.ToString("dd.MM.yyyy")</span>
}
else if (bannerSettings.EndDate.HasValue && DateTime.Now.Date > bannerSettings.EndDate.Value.Date)
{
<span class="status-icon expired-icon"></span>
<span>Abgelaufen am @bannerSettings.EndDate.Value.ToString("dd.MM.yyyy")</span>
}
else if (string.IsNullOrWhiteSpace(bannerSettings.Title) && string.IsNullOrWhiteSpace(bannerSettings.Message))
{
<span class="status-icon warning-icon"></span>
<span>Kein Inhalt vorhanden</span>
}
</span>
</div>
<div class="panel-actions">
<button class="btn btn-outline" @onclick="ToggleBannerSettings">Abbrechen</button>
<button class="btn btn-primary" @onclick="SaveBannerSettings" disabled="@isSavingBanner">
@if (isSavingBanner)
{
<span class="spinner-sm"></span>
}
Speichern
</button>
</div>
@if (!string.IsNullOrEmpty(bannerMessage))
{
<div class="banner-message @bannerMessageType">@bannerMessage</div>
}
</div>
</div>
}
@* File Storage Settings Panel *@
@if (showStorageSettings)
{
<div class="banner-settings-panel">
<div class="panel-header">
<h3>Dateispeicher (NFS/Disk)</h3>
<button class="btn-close" @onclick="ToggleStorageSettings">×</button>
</div>
<div class="panel-body">
<div class="form-group toggle-group">
<span class="toggle-label-text">Dateispeicher aktiviert</span>
<label class="toggle-container">
<input type="checkbox" @bind="storageSettings.IsEnabled" />
<span class="toggle-switch"></span>
</label>
</div>
<div class="form-group">
<label>Speicherpfad (NFS-Share oder lokaler Pfad)</label>
<input type="text" class="form-control" @bind="storageSettings.BasePath"
placeholder="z.B. /mnt/nfs/lagerbestand oder C:\Storage\Lagerbestand" />
<small class="form-hint">Für NFS: Zuerst den NFS-Share mounten, dann hier den Mount-Pfad eingeben.</small>
</div>
<div class="form-group">
<label>Max. Dateigröße (MB)</label>
<input type="number" class="form-control"
value="@(storageSettings.MaxFileSizeBytes / (1024 * 1024))"
@onchange="OnMaxFileSizeChanged"
min="1" max="100" />
</div>
<div class="form-group">
<button type="button" class="btn btn-outline btn-sm" @onclick="ValidateStoragePath" disabled="@isValidatingStorage">
@if (isValidatingStorage)
{
<span class="spinner-sm"></span>
}
Pfad testen
</button>
</div>
@* Status Info *@
<div class="banner-status-info">
<span class="status-indicator @(storageSettings.IsEnabled && storageSettings.IsPathAccessible() ? "active" : "inactive")">
@if (storageSettings.IsEnabled && storageSettings.IsPathAccessible())
{
<span class="status-icon active-icon"></span>
<span>Dateispeicher ist aktiv und erreichbar</span>
}
else if (!storageSettings.IsEnabled)
{
<span class="status-icon disabled-icon"></span>
<span>Dateispeicher ist deaktiviert (Dateien werden in der Datenbank gespeichert)</span>
}
else if (string.IsNullOrWhiteSpace(storageSettings.BasePath))
{
<span class="status-icon warning-icon"></span>
<span>Kein Pfad konfiguriert</span>
}
else
{
<span class="status-icon expired-icon"></span>
<span>Pfad ist nicht erreichbar: @storageSettings.BasePath</span>
}
</span>
</div>
<div class="panel-actions">
<button class="btn btn-outline" @onclick="ToggleStorageSettings">Abbrechen</button>
<button class="btn btn-primary" @onclick="SaveStorageSettings" disabled="@isSavingStorage">
@if (isSavingStorage)
{
<span class="spinner-sm"></span>
}
Speichern
</button>
</div>
@if (!string.IsNullOrEmpty(storageMessage))
{
<div class="banner-message @storageMessageType">@storageMessage</div>
}
</div>
</div>
}
@* Lexware API Settings Panel *@
@if (showLexwareSettings)
{
<div class="banner-settings-panel">
<div class="panel-header">
<h3>Lexware API — Kundensynchronisierung</h3>
<button class="btn-close" @onclick="ToggleLexwareSettings">×</button>
</div>
<div class="panel-body">
<div class="form-group toggle-group">
<span class="toggle-label-text">Lexware API aktiviert</span>
<label class="toggle-container">
<input type="checkbox" @bind="lexwareSettings.IsEnabled" />
<span class="toggle-switch"></span>
</label>
</div>
<div class="form-group">
<label>API Base-URL</label>
<input type="text" class="form-control" @bind="lexwareSettings.BaseUrl"
placeholder="https://api.lexoffice.io/v1" />
<small class="form-hint">Lexware/lexoffice API-Endpunkt (z.B. https://api.lexoffice.io/v1)</small>
</div>
<div class="form-group">
<label>API-Key</label>
<input type="password" class="form-control" @bind="lexwareSettings.ApiKey"
placeholder="Ihr Lexware API-Key..." />
</div>
<div class="form-row">
<div class="form-group toggle-group">
<span class="toggle-label-text">Tägliche Auto-Synchronisierung</span>
<label class="toggle-container">
<input type="checkbox" @bind="lexwareSettings.AutoSyncEnabled" />
<span class="toggle-switch"></span>
</label>
</div>
<div class="form-group">
<label>Sync-Uhrzeit</label>
<input type="text" class="form-control" @bind="lexwareSettings.AutoSyncTime"
placeholder="HH:mm (z.B. 02:00)" />
</div>
</div>
<div class="form-group">
<button type="button" class="btn btn-outline btn-sm" @onclick="TestLexwareConnection" disabled="@isTestingLexware">
@if (isTestingLexware)
{
<span class="spinner-sm"></span>
}
Verbindung testen
</button>
</div>
@* Status Info *@
<div class="banner-status-info">
<span class="status-indicator @(lexwareSettings.IsEnabled && lexwareSettings.IsConfigured ? "active" : "inactive")">
@if (lexwareSettings.IsEnabled && lexwareSettings.IsConfigured)
{
<span class="status-icon active-icon"></span>
<span>
API konfiguriert
@if (lexwareSettings.LastSyncAt.HasValue)
{
@:— Letzte Sync: @lexwareSettings.LastSyncAt.Value.ToLocalTime().ToString("dd.MM.yyyy HH:mm")
}
</span>
}
else if (!lexwareSettings.IsEnabled)
{
<span class="status-icon disabled-icon"></span>
<span>Lexware API ist deaktiviert</span>
}
else if (string.IsNullOrWhiteSpace(lexwareSettings.BaseUrl))
{
<span class="status-icon warning-icon"></span>
<span>Keine API-URL konfiguriert</span>
}
else if (string.IsNullOrWhiteSpace(lexwareSettings.ApiKey))
{
<span class="status-icon warning-icon"></span>
<span>Kein API-Key konfiguriert</span>
}
</span>
</div>
<div class="panel-actions">
<button class="btn btn-outline" @onclick="ToggleLexwareSettings">Abbrechen</button>
<button class="btn btn-primary" @onclick="SaveLexwareSettings" disabled="@isSavingLexware">
@if (isSavingLexware)
{
<span class="spinner-sm"></span>
}
Speichern
</button>
</div>
@if (!string.IsNullOrEmpty(lexwareMessage))
{
<div class="banner-message @lexwareMessageType">@lexwareMessage</div>
}
</div>
</div>
}
@* Two-Factor & SMTP Settings Panel *@
@if (showTwoFactorSettings)
{
<div class="banner-settings-panel">
<div class="panel-header">
<h3>Zwei-Faktor-Authentifizierung & SMTP</h3>
<button class="btn-close" @onclick="ToggleTwoFactorSettings">×</button>
</div>
<div class="panel-body">
<h4 class="panel-section-title">2FA-Einstellungen</h4>
<div class="form-group toggle-group">
<span class="toggle-label-text">2FA global aktiviert</span>
<label class="toggle-container">
<input type="checkbox" @bind="twoFactorSettings.IsEnabled" />
<span class="toggle-switch"></span>
</label>
</div>
<div class="form-group">
<label>Issuer-Name (in Authenticator-Apps)</label>
<input type="text" class="form-control" @bind="twoFactorSettings.Issuer"
placeholder="z.B. Reifen Späth Lagerverwaltung" />
<small class="form-hint">Wird in Google Authenticator, Authy etc. als Kontoname angezeigt.</small>
</div>
<div class="form-group toggle-group">
<span class="toggle-label-text">E-Mail-Fallback aktiviert</span>
<label class="toggle-container">
<input type="checkbox" @bind="twoFactorSettings.EmailFallbackEnabled" />
<span class="toggle-switch"></span>
</label>
</div>
<div class="banner-status-info">
<span class="status-indicator @(twoFactorSettings.IsEnabled ? "active" : "inactive")">
@if (twoFactorSettings.IsEnabled)
{
<span class="status-icon active-icon"></span>
<span>2FA ist global aktiviert – Benutzer können TOTP einrichten</span>
}
else
{
<span class="status-icon disabled-icon"></span>
<span>2FA ist global deaktiviert – kein Benutzer wird nach 2FA gefragt</span>
}
</span>
</div>
<div class="panel-actions">
<button class="btn btn-outline" @onclick="ToggleTwoFactorSettings">Abbrechen</button>
<button class="btn btn-primary" @onclick="SaveTwoFactorSettings" disabled="@isSavingTwoFactor">
@if (isSavingTwoFactor)
{
<span class="spinner-sm"></span>
}
2FA speichern
</button>
</div>
@if (!string.IsNullOrEmpty(twoFactorMessage))
{
<div class="banner-message @twoFactorMessageType">@twoFactorMessage</div>
}
<div class="status-divider" style="margin: 1.5rem 0;"></div>
<h4 class="panel-section-title">SMTP-Einstellungen (E-Mail-Versand)</h4>
<div class="form-group toggle-group">
<span class="toggle-label-text">SMTP aktiviert</span>
<label class="toggle-container">
<input type="checkbox" @bind="smtpSettings.IsEnabled" />
<span class="toggle-switch"></span>
</label>
</div>
<div class="form-row">
<div class="form-group">
<label>SMTP-Host</label>
<input type="text" class="form-control" @bind="smtpSettings.Host"
placeholder="z.B. smtp.gmail.com" />
</div>
<div class="form-group">
<label>Port</label>
<input type="number" class="form-control" @bind="smtpSettings.Port"
min="1" max="65535" />
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Benutzername</label>
<input type="text" class="form-control" @bind="smtpSettings.Username"
placeholder="SMTP-Benutzername" />
</div>
<div class="form-group">
<label>Passwort</label>
<input type="password" class="form-control" @bind="smtpSettings.Password"
placeholder="SMTP-Passwort" />
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Absender-E-Mail</label>
<input type="email" class="form-control" @bind="smtpSettings.FromEmail"
placeholder="noreply@reifenspaeth.de" />
</div>
<div class="form-group">
<label>Absender-Name</label>
<input type="text" class="form-control" @bind="smtpSettings.FromName"
placeholder="Reifen Späth Lagerverwaltung" />
</div>
</div>
<div class="form-group toggle-group">
<span class="toggle-label-text">SSL/TLS verwenden</span>
<label class="toggle-container">
<input type="checkbox" @bind="smtpSettings.UseSsl" />
<span class="toggle-switch"></span>
</label>
</div>
<div class="banner-status-info">
<span class="status-indicator @(smtpSettings.IsEnabled && smtpSettings.IsConfigured ? "active" : "inactive")">
@if (smtpSettings.IsEnabled && smtpSettings.IsConfigured)
{
<span class="status-icon active-icon"></span>
<span>SMTP konfiguriert – E-Mail-Versand bereit</span>
}
else if (!smtpSettings.IsEnabled)
{
<span class="status-icon disabled-icon"></span>
<span>SMTP deaktiviert</span>
}
else
{
<span class="status-icon warning-icon"></span>
<span>SMTP nicht vollständig konfiguriert</span>
}
</span>
</div>
<div class="panel-actions">
<button class="btn btn-outline" @onclick="ToggleTwoFactorSettings">Abbrechen</button>
<button class="btn btn-primary" @onclick="SaveSmtpSettings" disabled="@isSavingSmtp">
@if (isSavingSmtp)
{
<span class="spinner-sm"></span>
}
SMTP speichern
</button>
</div>
@if (!string.IsNullOrEmpty(smtpMessage))
{
<div class="banner-message @smtpMessageType">@smtpMessage</div>
}
</div>
</div>
}
@* Session Settings Panel *@
@if (showSessionSettings)
{
<div class="banner-settings-panel">
<div class="panel-header">
<h3>Sitzung &amp; Leerlauf</h3>
<button class="btn-close" @onclick="ToggleSessionSettings" aria-label="Schließen">×</button>
</div>
<div class="panel-body">
<p class="text-muted" style="margin-bottom: 1rem;">
Steuert, nach wie vielen Minuten Inaktivit&auml;t der Browser-Tab gesperrt wird
(Weiter-Klick erforderlich), und wie lange getrennte Server-Verbindungen
aufbewahrt werden, damit Eingaben nach WLAN-Unterbrechungen erhalten bleiben.
</p>
<div class="form-group">
<label for="idle-timeout" class="form-label">
Leerlauf-Sperre nach (Minuten)
</label>
<input type="number"
id="idle-timeout"
class="form-control"
min="@SessionSettings.MinIdleTimeoutMinutes"
max="@SessionSettings.MaxIdleTimeoutMinutes"
step="1"
@bind="sessionSettings.IdleTimeoutMinutes" />
<small class="text-muted">
Bereich: @SessionSettings.MinIdleTimeoutMinutes – @SessionSettings.MaxIdleTimeoutMinutes Minuten.
Standard: @SessionSettings.DefaultIdleTimeoutMinutes.
Gilt sofort f&uuml;r alle Benutzer nach Speichern und Seitenaufruf.
</small>
</div>
<div class="form-group" style="margin-top: 1rem;">
<label for="circuit-retention" class="form-label">
Verbindungs-Aufbewahrung bei Trennung (Stunden)
</label>
<input type="number"
id="circuit-retention"
class="form-control"
min="@SessionSettings.MinCircuitRetentionHours"
max="@SessionSettings.MaxCircuitRetentionHours"
step="0.5"
@bind="sessionSettings.CircuitRetentionHours" />
<small class="text-muted">
Bereich: @SessionSettings.MinCircuitRetentionHours – @SessionSettings.MaxCircuitRetentionHours Stunden.
Standard: @SessionSettings.DefaultCircuitRetentionHours.
<strong>Gilt erst nach einem Neustart der Anwendung.</strong>
</small>
</div>
<div class="panel-actions" style="margin-top: 1.5rem;">
<button class="btn btn-outline" @onclick="ToggleSessionSettings">Abbrechen</button>
<button class="btn btn-primary" @onclick="SaveSessionSettings" disabled="@isSavingSession">
@if (isSavingSession)
{
<span class="spinner-sm"></span>
}
Speichern
</button>
</div>
@if (!string.IsNullOrEmpty(sessionMessage))
{
<div class="banner-message @sessionMessageType">@sessionMessage</div>
}
</div>
</div>
}
@if (!string.IsNullOrEmpty(actionMessage))
{
<div class="action-message @(actionMessageType)">
@actionMessage
</div>
}
<div class="status-footer">
<p>Letzte Aktualisierung: @lastRefresh.ToString("HH:mm:ss")</p>
</div>
}
</main>
}
@code {
private static readonly DateTime StartTime = DateTime.Now;
private bool isLoading = true;
private bool isPerformingAction;
private string environment = "Unknown";
private string dbServer = "";
private string dbName = "";
private bool dbConnected;
private string? dbError;
private int userCount;
private int activeUserCount;
private int adminCount;
private int categoryCount;
private int inventoryCount;
private string adminUsername = "";
private DateTime lastRefresh = DateTime.Now;
private string? actionMessage;
private string actionMessageType = "success";
// Banner settings
private bool showBannerSettings;
private bool isSavingBanner;
private PromoBannerSettings bannerSettings = new();
private string? bannerMessage;
private string bannerMessageType = "success";
// File Storage settings
private bool showStorageSettings;
private bool isSavingStorage;
private bool isValidatingStorage;
private FileStorageSettings storageSettings = new();
private string? storageMessage;
private string storageMessageType = "success";
// Lexware API settings
private bool showLexwareSettings;
private bool isSavingLexware;
private bool isTestingLexware;
private LexwareApiSettings lexwareSettings = new();
private string? lexwareMessage;
private string lexwareMessageType = "success";
// Two-Factor settings
private bool showTwoFactorSettings;
private bool isSavingTwoFactor;
private TwoFactorSettings twoFactorSettings = new();
private string? twoFactorMessage;
private string twoFactorMessageType = "success";
// SMTP settings
private bool isSavingSmtp;
private SmtpSettings smtpSettings = new();
private string? smtpMessage;
private string smtpMessageType = "success";
// Session settings (idle lock + circuit retention)
private bool showSessionSettings;
private bool isSavingSession;
private SessionSettings sessionSettings = new();
private string? sessionMessage;
private string sessionMessageType = "success";
protected override async Task OnInitializedAsync()
{
if (UserSession.IsAdmin)
{
await LoadStatus();
await LoadBannerSettings();
await LoadStorageSettings();
await LoadLexwareSettings();
await LoadTwoFactorSettings();
await LoadSmtpSettings();
await LoadSessionSettings();
}
}
private DotNetObjectReference<Status>? _visibilityRef;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && UserSession.IsAdmin)
{
_visibilityRef = DotNetObjectReference.Create(this);
await VisibilityRefresh.RegisterAsync(JS, _visibilityRef, nameof(OnVisibilityReturn), 120);
}
}
[JSInvokable]
public async Task OnVisibilityReturn(int hiddenSeconds)
{
if (!UserSession.IsAdmin) return;
await LoadStatus();
await InvokeAsync(StateHasChanged);
}
public async ValueTask DisposeAsync()
{
await VisibilityRefresh.UnregisterAsync(JS);
_visibilityRef?.Dispose();
}
private async Task LoadSessionSettings()
{
try
{
sessionSettings = await SiteSettingsService.GetSessionSettingsAsync();
}
catch (Exception ex)
{
sessionMessage = $"Fehler beim Laden: {ex.Message}";
sessionMessageType = "error";
}
}
private void ToggleSessionSettings()
{
showSessionSettings = !showSessionSettings;
sessionMessage = null;
}
private async Task SaveSessionSettings()
{
isSavingSession = true;
sessionMessage = null;
StateHasChanged();
try
{
await SiteSettingsService.SaveSessionSettingsAsync(sessionSettings);
sessionMessage = "Sitzungs-Einstellungen gespeichert! Die Verbindungs-Aufbewahrung gilt erst nach einem Neustart der Anwendung.";
sessionMessageType = "success";
// Apply idle timeout immediately in the current tab.
try
{
await JS.InvokeVoidAsync("appInterop.startIdleWatcher", sessionSettings.GetSafeIdleTimeoutMinutes());
}
catch { /* non-fatal */ }
}
catch (Exception ex)
{
sessionMessage = $"Fehler: {ex.Message}";
sessionMessageType = "error";
}
finally
{
isSavingSession = false;
}
}
private async Task LoadBannerSettings()
{
try
{
bannerSettings = await SiteSettingsService.GetPromoBannerSettingsAsync();
}
catch (Exception ex)
{
bannerMessage = $"Fehler beim Laden: {ex.Message}";
bannerMessageType = "error";
}
}
private void ToggleBannerSettings()
{
showBannerSettings = !showBannerSettings;
bannerMessage = null;
}
private async Task LoadStorageSettings()
{
try
{
storageSettings = await FileStorageService.GetSettingsAsync();
}
catch (Exception ex)
{
storageMessage = $"Fehler beim Laden: {ex.Message}";
storageMessageType = "error";
}
}
private void ToggleStorageSettings()
{
showStorageSettings = !showStorageSettings;
storageMessage = null;
}
private void OnMaxFileSizeChanged(ChangeEventArgs e)
{
if (long.TryParse(e.Value?.ToString(), out var mb))
{
storageSettings.MaxFileSizeBytes = mb * 1024 * 1024;
}
}
private async Task ValidateStoragePath()
{
isValidatingStorage = true;
storageMessage = null;
StateHasChanged();
try
{
var (isValid, errorMessage) = await FileStorageService.ValidateStoragePathAsync(storageSettings.BasePath);
if (isValid)
{
storageMessage = "Pfad ist gültig und beschreibbar!";
storageMessageType = "success";
}
else
{
storageMessage = errorMessage ?? "Pfad ist ungültig.";
storageMessageType = "error";
}
}
catch (Exception ex)
{
storageMessage = $"Fehler: {ex.Message}";
storageMessageType = "error";
}
finally
{
isValidatingStorage = false;
}
}
private async Task SaveStorageSettings()
{
isSavingStorage = true;
storageMessage = null;
StateHasChanged();
try
{
await FileStorageService.SaveSettingsAsync(storageSettings);
storageMessage = "Dateispeicher-Einstellungen gespeichert!";
storageMessageType = "success";
}
catch (Exception ex)
{
storageMessage = $"Fehler: {ex.Message}";
storageMessageType = "error";
}
finally
{
isSavingStorage = false;
}
}
private async Task SaveBannerSettings()
{
isSavingBanner = true;
bannerMessage = null;
StateHasChanged();
try
{
await SiteSettingsService.SavePromoBannerSettingsAsync(bannerSettings);
bannerMessage = "Banner-Einstellungen gespeichert!";
bannerMessageType = "success";
}
catch (Exception ex)
{
bannerMessage = $"Fehler: {ex.Message}";
bannerMessageType = "error";
}
finally
{
isSavingBanner = false;
}
}
private async Task LoadLexwareSettings()
{
try
{
lexwareSettings = await SiteSettingsService.GetLexwareApiSettingsAsync();
}
catch (Exception ex)
{
lexwareMessage = $"Fehler beim Laden: {ex.Message}";
lexwareMessageType = "error";
}
}
private void ToggleLexwareSettings()
{
showLexwareSettings = !showLexwareSettings;
lexwareMessage = null;
}
private async Task TestLexwareConnection()
{
isTestingLexware = true;
lexwareMessage = null;
StateHasChanged();
try
{
var (success, message) = await LexwareSyncService.TestConnectionAsync(lexwareSettings);
lexwareMessage = message;
lexwareMessageType = success ? "success" : "error";
}
catch (Exception ex)
{
lexwareMessage = $"Fehler: {ex.Message}";
lexwareMessageType = "error";
}
finally
{
isTestingLexware = false;
}
}
private async Task SaveLexwareSettings()
{
isSavingLexware = true;
lexwareMessage = null;
StateHasChanged();
try
{
await SiteSettingsService.SaveLexwareApiSettingsAsync(lexwareSettings);
lexwareMessage = "Lexware API-Einstellungen gespeichert!";
lexwareMessageType = "success";
}
catch (Exception ex)
{
lexwareMessage = $"Fehler: {ex.Message}";
lexwareMessageType = "error";
}
finally
{
isSavingLexware = false;
}
}
private async Task LoadTwoFactorSettings()
{
try
{
twoFactorSettings = await SiteSettingsService.GetTwoFactorSettingsAsync();
}
catch (Exception ex)
{
twoFactorMessage = $"Fehler beim Laden: {ex.Message}";
twoFactorMessageType = "error";
}
}
private async Task LoadSmtpSettings()
{
try
{
smtpSettings = await SiteSettingsService.GetSmtpSettingsAsync();
}
catch (Exception ex)
{
smtpMessage = $"Fehler beim Laden: {ex.Message}";
smtpMessageType = "error";
}
}
private void ToggleTwoFactorSettings()
{
showTwoFactorSettings = !showTwoFactorSettings;
twoFactorMessage = null;
smtpMessage = null;
}
private async Task SaveTwoFactorSettings()
{
isSavingTwoFactor = true;
twoFactorMessage = null;
StateHasChanged();
try
{
await SiteSettingsService.SaveTwoFactorSettingsAsync(twoFactorSettings);
twoFactorMessage = "2FA-Einstellungen gespeichert!";
twoFactorMessageType = "success";
}
catch (Exception ex)
{
twoFactorMessage = $"Fehler: {ex.Message}";
twoFactorMessageType = "error";
}
finally
{
isSavingTwoFactor = false;
}
}
private async Task SaveSmtpSettings()
{
isSavingSmtp = true;
smtpMessage = null;
StateHasChanged();
try
{
await SiteSettingsService.SaveSmtpSettingsAsync(smtpSettings);
smtpMessage = "SMTP-Einstellungen gespeichert!";
smtpMessageType = "success";
}
catch (Exception ex)
{
smtpMessage = $"Fehler: {ex.Message}";
smtpMessageType = "error";
}
finally
{
isSavingSmtp = false;
}
}
private async Task RefreshStatus()
{
isLoading = true;
actionMessage = null;
StateHasChanged();
await Task.Delay(300);
await LoadStatus();
}
private async Task LoadStatus()
{
try
{
environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production";
var connectionString = Configuration.GetConnectionString("DefaultConnection") ?? "";
ParseConnectionString(connectionString);
adminUsername = Configuration["AdminUser:Username"] ?? "admin";
try
{
var users = await AuthService.GetAllUsersAsync();
userCount = users.Count;
activeUserCount = users.Count(u => u.IsActive);
adminCount = users.Count(u => u.Role == UserRole.Admin && u.IsActive);
var categories = await CatalogService.GetAllAsync();
categoryCount = categories.Count;
var items = await InventoryService.GetAllAsync();
inventoryCount = items.Count;
dbConnected = true;
}
catch (Exception ex)
{
dbConnected = false;
dbError = ex.Message;
}
lastRefresh = DateTime.Now;
}
catch (Exception ex)
{
dbError = ex.Message;
}
finally
{
isLoading = false;
}
}
private void ParseConnectionString(string connectionString)
{
foreach (var part in connectionString.Split(';'))
{
var kv = part.Split('=', 2);
if (kv.Length != 2) continue;
var key = kv[0].Trim().ToLowerInvariant();
var value = kv[1].Trim();
if (key == "server")
dbServer = value;
else if (key == "database")
dbName = value;
}
}
private async Task ForceGarbageCollection()
{
isPerformingAction = true;
actionMessage = null;
StateHasChanged();
try
{
var beforeMemory = GC.GetTotalMemory(false);
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
await Task.Delay(500);
var afterMemory = GC.GetTotalMemory(true);
var freedBytes = beforeMemory - afterMemory;
var freedMb = freedBytes / (1024.0 * 1024);
actionMessage = $"Speicherbereinigung abgeschlossen. {freedMb:F2} MB freigegeben.";
actionMessageType = "success";
}
catch (Exception ex)
{
actionMessage = $"Fehler: {ex.Message}";
actionMessageType = "error";
}
finally
{
isPerformingAction = false;
}
}
private static string GetUptime()
{
var uptime = DateTime.Now - StartTime;
if (uptime.TotalDays >= 1)
return $"{(int)uptime.TotalDays}d {uptime.Hours}h {uptime.Minutes}m";
if (uptime.TotalHours >= 1)
return $"{(int)uptime.TotalHours}h {uptime.Minutes}m";
return $"{uptime.Minutes}m {uptime.Seconds}s";
}
private static string GetMemoryUsage()
{
var bytes = GC.GetTotalMemory(false);
return FormatBytes(bytes);
}
private static string GetWorkingSetMemory()
{
try
{
var process = System.Diagnostics.Process.GetCurrentProcess();
return FormatBytes(process.WorkingSet64);
}
catch
{
return "N/A";
}
}
private static string FormatBytes(long bytes)
{
return bytes switch
{
>= 1024 * 1024 * 1024 => $"{bytes / (1024.0 * 1024 * 1024):F2} GB",
>= 1024 * 1024 => $"{bytes / (1024.0 * 1024):F2} MB",
>= 1024 => $"{bytes / 1024.0:F2} KB",
_ => $"{bytes} B"
};
}
}