1545 lines
70 KiB
Plaintext
1545 lines
70 KiB
Plaintext
@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 & 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ä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ü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"
|
||
};
|
||
}
|
||
}
|