From 0a346a0d35e97e8df0bd607780751171178692bf Mon Sep 17 00:00:00 2001 From: BitWuehler <39520752+BitWuehler@users.noreply.github.com> Date: Wed, 10 Jun 2026 17:38:52 +0200 Subject: [PATCH 01/13] Leave a 4px gap for table edges on mobile --- static/style.css | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/static/style.css b/static/style.css index 777727f..6fa030c 100644 --- a/static/style.css +++ b/static/style.css @@ -721,11 +721,11 @@ th[data-sorted="true"][data-sorted-direction="descending"]::after { header { padding: 10px 10px !important; } .table-responsive { - margin-left: calc(-50vw + 50%) !important; - margin-right: calc(-50vw + 50%) !important; - width: 100vw !important; + margin-left: -4px !important; + margin-right: -4px !important; + width: calc(100% + 8px) !important; padding-right: 0 !important; - border-radius: 0; + border-radius: 8px !important; } .table-responsive th:last-child, From 301d1ebe78582bdd17d1b2e8373be5a8e37f101f Mon Sep 17 00:00:00 2001 From: BitWuehler <39520752+BitWuehler@users.noreply.github.com> Date: Wed, 10 Jun 2026 17:44:31 +0200 Subject: [PATCH 02/13] Fix CSS specificity so transparent actions-cell doesn't get 40px padding, which caused the gap --- static/style.css | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/static/style.css b/static/style.css index 6fa030c..442237b 100644 --- a/static/style.css +++ b/static/style.css @@ -728,14 +728,15 @@ th[data-sorted="true"][data-sorted-direction="descending"]::after { border-radius: 8px !important; } - .table-responsive th:last-child, - .table-responsive td:last-child, + .table-responsive th:not(.actions-cell):last-child, + .table-responsive td:not(.actions-cell):last-child, .table-responsive th:has(+ .actions-cell), .table-responsive td:has(+ .actions-cell) { padding-right: 40px !important; } - .table-responsive .actions-cell { + .table-responsive td.actions-cell, + .table-responsive th.actions-cell { padding-right: 0 !important; } From 4e152ee84378736494f878345cd7f70c9ca0278f Mon Sep 17 00:00:00 2001 From: BitWuehler <39520752+BitWuehler@users.noreply.github.com> Date: Wed, 10 Jun 2026 17:54:34 +0200 Subject: [PATCH 03/13] Center status column in daily report, and optimize slider width and flex alignment on client updates page --- static/style.css | 7 +++++-- templates/client_updates.html | 5 +++++ templates/daily_report.html | 4 ++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/static/style.css b/static/style.css index 442237b..dcf58b6 100644 --- a/static/style.css +++ b/static/style.css @@ -767,10 +767,13 @@ th[data-sorted="true"][data-sorted-direction="descending"]::after { background: rgb(30, 30, 50); /* GPU beschleunigte Animation statt width/padding, um Layout Thrashing zu vermeiden */ clip-path: inset(0 0 0 100%); - transition: clip-path 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease-in-out; display: flex; align-items: center; - justify-content: center; + justify-content: flex-end; + padding-right: 10px; + gap: 8px; + box-sizing: border-box; + transition: clip-path 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease-in-out; opacity: 0; pointer-events: none; border-left: 1px solid rgba(255,255,255,0.1); diff --git a/templates/client_updates.html b/templates/client_updates.html index 984f55a..d917424 100644 --- a/templates/client_updates.html +++ b/templates/client_updates.html @@ -79,6 +79,11 @@
| Scan Ergebnis | -Status | +Status | {{ item.status_text }} {% if laptop.last_scan_threats_found is true %} (Bedrohungen!) {% endif %} | From 559dd2e796bbf42786a14b1194d072b96940fb82 Mon Sep 17 00:00:00 2001 From: BitWuehler <39520752+BitWuehler@users.noreply.github.com> Date: Wed, 10 Jun 2026 18:01:45 +0200 Subject: [PATCH 04/13] Fix mobile table margins and slider handle alignment --- static/style.css | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/static/style.css b/static/style.css index dcf58b6..016ba51 100644 --- a/static/style.css +++ b/static/style.css @@ -698,8 +698,8 @@ th[data-sorted="true"][data-sorted-direction="descending"]::after { .mobile-bulk-fab:active { transform: scale(0.9) translateZ(0); } @media (max-width: 1000px) { - .row-actions-handle { display: flex; } - .mobile-bulk-fab { display: flex; } + .row-actions-handle { display: flex; right: 4px !important; } + .mobile-bulk-fab { display: flex; right: 4px !important; } /* Reduce paddings to fit more content on screen without scrolling */ th, td { @@ -721,9 +721,9 @@ th[data-sorted="true"][data-sorted-direction="descending"]::after { header { padding: 10px 10px !important; } .table-responsive { - margin-left: -4px !important; - margin-right: -4px !important; - width: calc(100% + 8px) !important; + margin-left: -11px !important; + margin-right: -11px !important; + width: calc(100% + 22px) !important; padding-right: 0 !important; border-radius: 8px !important; } From 12d2b543e98f5212c77b7c3a14d86a0bd7164714 Mon Sep 17 00:00:00 2001 From: BitWuehler <39520752+BitWuehler@users.noreply.github.com> Date: Wed, 10 Jun 2026 18:10:00 +0200 Subject: [PATCH 05/13] Revert previous workaround --- static/style.css | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/static/style.css b/static/style.css index 016ba51..dcf58b6 100644 --- a/static/style.css +++ b/static/style.css @@ -698,8 +698,8 @@ th[data-sorted="true"][data-sorted-direction="descending"]::after { .mobile-bulk-fab:active { transform: scale(0.9) translateZ(0); } @media (max-width: 1000px) { - .row-actions-handle { display: flex; right: 4px !important; } - .mobile-bulk-fab { display: flex; right: 4px !important; } + .row-actions-handle { display: flex; } + .mobile-bulk-fab { display: flex; } /* Reduce paddings to fit more content on screen without scrolling */ th, td { @@ -721,9 +721,9 @@ th[data-sorted="true"][data-sorted-direction="descending"]::after { header { padding: 10px 10px !important; } .table-responsive { - margin-left: -11px !important; - margin-right: -11px !important; - width: calc(100% + 22px) !important; + margin-left: -4px !important; + margin-right: -4px !important; + width: calc(100% + 8px) !important; padding-right: 0 !important; border-radius: 8px !important; } From 6d7ad1e3b02915fec37e1b51a112d102ec504b4c Mon Sep 17 00:00:00 2001 From: BitWuehler <39520752+BitWuehler@users.noreply.github.com> Date: Wed, 10 Jun 2026 18:14:11 +0200 Subject: [PATCH 06/13] Fix slider alignment properly by making container span full width and removing table overflow hidden --- static/style.css | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/static/style.css b/static/style.css index dcf58b6..8058312 100644 --- a/static/style.css +++ b/static/style.css @@ -721,12 +721,21 @@ th[data-sorted="true"][data-sorted-direction="descending"]::after { header { padding: 10px 10px !important; } .table-responsive { - margin-left: -4px !important; - margin-right: -4px !important; - width: calc(100% + 8px) !important; - padding-right: 0 !important; - border-radius: 8px !important; + margin-left: -15px !important; + margin-right: -15px !important; + padding-left: 11px !important; + padding-right: 11px !important; + width: auto !important; + border-radius: 0 !important; + } + + table { + overflow: visible !important; } + table th:first-child { border-top-left-radius: 12px; } + table th:last-child { border-top-right-radius: 12px; } + table tr:last-child td:first-child { border-bottom-left-radius: 12px; } + table tr:last-child td:last-child { border-bottom-right-radius: 12px; } .table-responsive th:not(.actions-cell):last-child, .table-responsive td:not(.actions-cell):last-child, @@ -758,9 +767,9 @@ th[data-sorted="true"][data-sorted-direction="descending"]::after { .actions-slider { position: absolute; - right: 0; + right: -11px !important; top: 0; - width: var(--slider-width, 150px); + width: calc(var(--slider-width, 150px) + 11px) !important; height: 100%; overflow: hidden; /* Performance Fix für Android Firefox: backdrop-filter entfernt, da es beim Scrollen extrem ruckelt */ @@ -770,15 +779,13 @@ th[data-sorted="true"][data-sorted-direction="descending"]::after { display: flex; align-items: center; justify-content: flex-end; - padding-right: 10px; + padding-right: 21px !important; gap: 8px; box-sizing: border-box; transition: clip-path 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease-in-out; opacity: 0; pointer-events: none; border-left: 1px solid rgba(255,255,255,0.1); - box-sizing: border-box; - padding: 0 10px; } /* Subtiler Slide-in Effekt für die Buttons ohne Layout-Berechnung */ @@ -802,7 +809,7 @@ th[data-sorted="true"][data-sorted-direction="descending"]::after { } body.show-mobile-actions .row-actions-handle { - transform: translate(calc(var(--slider-width, 150px) * -1), -50%) translateZ(0); + transform: translate(calc((var(--slider-width, 150px) + 11px) * -1), -50%) translateZ(0); } .row-actions-handle svg { From be002f1b2c4e1312a736eb99bacffe33e9fb185e Mon Sep 17 00:00:00 2001 From: BitWuehler <39520752+BitWuehler@users.noreply.github.com> Date: Wed, 10 Jun 2026 18:19:22 +0200 Subject: [PATCH 07/13] Fix slider math by using actual main padding of 8px --- static/style.css | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/static/style.css b/static/style.css index 8058312..0946e7d 100644 --- a/static/style.css +++ b/static/style.css @@ -721,10 +721,10 @@ th[data-sorted="true"][data-sorted-direction="descending"]::after { header { padding: 10px 10px !important; } .table-responsive { - margin-left: -15px !important; - margin-right: -15px !important; - padding-left: 11px !important; - padding-right: 11px !important; + margin-left: -8px !important; + margin-right: -8px !important; + padding-left: 4px !important; + padding-right: 4px !important; width: auto !important; border-radius: 0 !important; } @@ -767,9 +767,9 @@ th[data-sorted="true"][data-sorted-direction="descending"]::after { .actions-slider { position: absolute; - right: -11px !important; + right: -4px !important; top: 0; - width: calc(var(--slider-width, 150px) + 11px) !important; + width: calc(var(--slider-width, 150px) + 4px) !important; height: 100%; overflow: hidden; /* Performance Fix für Android Firefox: backdrop-filter entfernt, da es beim Scrollen extrem ruckelt */ @@ -779,7 +779,7 @@ th[data-sorted="true"][data-sorted-direction="descending"]::after { display: flex; align-items: center; justify-content: flex-end; - padding-right: 21px !important; + padding-right: 14px !important; gap: 8px; box-sizing: border-box; transition: clip-path 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease-in-out; @@ -809,7 +809,7 @@ th[data-sorted="true"][data-sorted-direction="descending"]::after { } body.show-mobile-actions .row-actions-handle { - transform: translate(calc((var(--slider-width, 150px) + 11px) * -1), -50%) translateZ(0); + transform: translate(calc((var(--slider-width, 150px) + 4px) * -1), -50%) translateZ(0); } .row-actions-handle svg { From 8e9d8a37b1dc6a0bedbad2c6cf321a5d512d9500 Mon Sep 17 00:00:00 2001 From: BitWuehler <39520752+BitWuehler@users.noreply.github.com> Date: Wed, 10 Jun 2026 18:26:31 +0200 Subject: [PATCH 08/13] Decouple slider from table scrolling by anchoring it to sticky actions-cell --- static/style.css | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/static/style.css b/static/style.css index 0946e7d..82483d5 100644 --- a/static/style.css +++ b/static/style.css @@ -750,12 +750,13 @@ th[data-sorted="true"][data-sorted-direction="descending"]::after { } tbody tr { - position: relative; + /* position: relative; Removed so actions-slider anchors to the sticky cell instead */ } /* Sticky Animated Column */ .actions-cell { position: sticky !important; + position: -webkit-sticky !important; right: 0 !important; padding: 0 !important; background: transparent !important; @@ -765,11 +766,18 @@ th[data-sorted="true"][data-sorted-direction="descending"]::after { width: 0 !important; } + /* On mobile, make actions-cell relative so slider anchors to it */ + @media (max-width: 1000px) { + .actions-cell { + position: relative; + } + } + .actions-slider { position: absolute; - right: -4px !important; + right: 0 !important; top: 0; - width: calc(var(--slider-width, 150px) + 4px) !important; + width: var(--slider-width, 150px) !important; height: 100%; overflow: hidden; /* Performance Fix für Android Firefox: backdrop-filter entfernt, da es beim Scrollen extrem ruckelt */ @@ -779,7 +787,7 @@ th[data-sorted="true"][data-sorted-direction="descending"]::after { display: flex; align-items: center; justify-content: flex-end; - padding-right: 14px !important; + padding-right: 10px !important; gap: 8px; box-sizing: border-box; transition: clip-path 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease-in-out; @@ -809,7 +817,7 @@ th[data-sorted="true"][data-sorted-direction="descending"]::after { } body.show-mobile-actions .row-actions-handle { - transform: translate(calc((var(--slider-width, 150px) + 4px) * -1), -50%) translateZ(0); + transform: translate(calc(var(--slider-width, 150px) * -1), -50%) translateZ(0); } .row-actions-handle svg { From 38af78fb4c1caeb67aea3162b85bb426c9b92f87 Mon Sep 17 00:00:00 2001 From: BitWuehler <39520752+BitWuehler@users.noreply.github.com> Date: Wed, 10 Jun 2026 22:39:38 +0200 Subject: [PATCH 09/13] more Details in report --- app/web_routes.py | 10 ++++++++-- templates/daily_report.html | 1 - 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/web_routes.py b/app/web_routes.py index 0e61fda..dcf5a34 100644 --- a/app/web_routes.py +++ b/app/web_routes.py @@ -165,7 +165,7 @@ async def export_daily_report_csv(request: Request, report_date_str: Optional[st if historical_report.threats_found is True: scan_result = "Fund!" - threats_str = "Ja" + threats_str = historical_report.threat_details if historical_report.threat_details else "Ja" else: scan_result = historical_report.scan_result_message or "Keine Meldung" if len(scan_result) > 50: @@ -216,19 +216,25 @@ async def web_daily_report(request: Request, report_date_str: Optional[str] = No laptop.last_scan_type = historical_report.scan_type laptop.last_scan_result_message = historical_report.scan_result_message laptop.last_scan_threats_found = historical_report.threats_found + laptop.last_scan_threat_details = historical_report.threat_details laptop.last_scan_duration_minutes = None # We don't have duration in historical reports right now else: laptop.last_scan_time = None laptop.last_scan_type = None laptop.last_scan_result_message = None laptop.last_scan_threats_found = None + laptop.last_scan_threat_details = None laptop.last_scan_duration_minutes = None status_text, color_class = "N/A", "status-white" if laptop.last_scan_time is not None: last_scan_time_aware = laptop.last_scan_time.replace(tzinfo=timezone.utc) if laptop.last_scan_threats_found is True: - status_text, color_class = "Bedrohung(en)!", "status-red" + if getattr(laptop, "last_scan_threat_details", None): + status_text = laptop.last_scan_threat_details + else: + status_text = "Bedrohung(en)!" + color_class = "status-red" elif (now_utc.date() == last_scan_time_aware.date()) and (now_utc - last_scan_time_aware) <= timedelta(days=1): status_text, color_class = "OK (Scan heute)", "status-green" elif (now_utc - last_scan_time_aware) <= timedelta(days=1): diff --git a/templates/daily_report.html b/templates/daily_report.html index c868aa6..5128920 100644 --- a/templates/daily_report.html +++ b/templates/daily_report.html @@ -143,7 +143,6 @@{{ item.status_text }} | From 24a36ae39c63aa090c6d6b5f4f56058bb443d0af Mon Sep 17 00:00:00 2001 From: BitWuehler <39520752+BitWuehler@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:06:28 +0200 Subject: [PATCH 11/13] Fix Defender Event handling, text formatting, and retroactively classify Event 1002 as error instead of threat --- app/web_routes.py | 26 ++++++++++++++++++++++++-- client/ScanOpClient.ps1 | 16 +++++++++------- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/app/web_routes.py b/app/web_routes.py index b5794ca..ffb04a6 100644 --- a/app/web_routes.py +++ b/app/web_routes.py @@ -163,7 +163,14 @@ async def export_daily_report_csv(request: Request, report_date_str: Optional[st scan_time_berlin = historical_report.client_scan_time.replace(tzinfo=timezone.utc).astimezone(berlin_tz) scan_time_str = scan_time_berlin.strftime('%d.%m.%Y %H:%M:%S') - if historical_report.threats_found is True: + is_real_threat = historical_report.threats_found + is_error = False + if historical_report.scan_result_message and ("Event 1002" in historical_report.scan_result_message or "FEHLER:" in historical_report.scan_result_message or "stopped" in historical_report.scan_result_message or "Fehler" in historical_report.scan_result_message): + if "Event 1002" in historical_report.scan_result_message: + is_real_threat = False + is_error = True + + if is_real_threat is True: scan_result = "Fund!" if historical_report.threat_details: threats_str = historical_report.threat_details @@ -171,6 +178,9 @@ async def export_daily_report_csv(request: Request, report_date_str: Optional[st threats_str = historical_report.scan_result_message else: threats_str = "Ja" + elif is_error: + scan_result = "Fehler" + threats_str = historical_report.scan_result_message or "Fehler aufgetreten" else: scan_result = historical_report.scan_result_message or "Keine Meldung" if len(scan_result) > 50: @@ -234,7 +244,16 @@ async def web_daily_report(request: Request, report_date_str: Optional[str] = No status_text, color_class = "N/A", "status-white" if laptop.last_scan_time is not None: last_scan_time_aware = laptop.last_scan_time.replace(tzinfo=timezone.utc) - if laptop.last_scan_threats_found is True: + + # Retroactively fix old DB entries where Event 1002 was marked as a threat + is_real_threat = laptop.last_scan_threats_found + is_error = False + if laptop.last_scan_result_message and ("Event 1002" in laptop.last_scan_result_message or "FEHLER:" in laptop.last_scan_result_message or "stopped" in laptop.last_scan_result_message or "Fehler" in laptop.last_scan_result_message): + if "Event 1002" in laptop.last_scan_result_message: + is_real_threat = False # 1002 is just a cancelled scan, not a threat + is_error = True + + if is_real_threat is True: if getattr(laptop, "last_scan_threat_details", None): status_text = laptop.last_scan_threat_details elif laptop.last_scan_result_message: @@ -242,6 +261,9 @@ async def web_daily_report(request: Request, report_date_str: Optional[str] = No else: status_text = "Bedrohung(en)!" color_class = "status-red" + elif is_error: + status_text = laptop.last_scan_result_message + color_class = "status-yellow" elif (now_utc.date() == last_scan_time_aware.date()) and (now_utc - last_scan_time_aware) <= timedelta(days=1): status_text, color_class = "OK (Scan heute)", "status-green" elif (now_utc - last_scan_time_aware) <= timedelta(days=1): diff --git a/client/ScanOpClient.ps1 b/client/ScanOpClient.ps1 index 8f38184..5ca664d 100644 --- a/client/ScanOpClient.ps1 +++ b/client/ScanOpClient.ps1 @@ -91,7 +91,11 @@ while ($true) { $CommandUrl = "$ServerBaseUrl/api/v1/clientcommands/$($AliasName)?version=$($ClientVersion)"; $ReportUrl = "$ServerBaseUrl/api/v1/scanreports/" # --- Hilfsfunktionen (unverändert) --- - function Send-ScanReport { param( [Parameter(Mandatory = $true)][string]$ScanTime, [Parameter(Mandatory = $true)][string]$ScanType, [Parameter(Mandatory = $true)][string]$ScanResultMessage, [Parameter(Mandatory = $true)][bool]$ThreatsFound, [string]$ThreatDetails = $null ); Write-Log -Message "Bereite Scan-Bericht ($ScanType) für Versand vor."; if ([string]::IsNullOrWhiteSpace($ScanTime)) { $ScanTime = (Get-Date "1970-01-01").ToUniversalTime().ToString("o") } ; if ([string]::IsNullOrWhiteSpace($ScanType)) { $ScanType = "Unbekannt" } ; if ([string]::IsNullOrWhiteSpace($ScanResultMessage)) { $ScanResultMessage = "Keine Meldung" } ; $CleanResultMessage = $ScanResultMessage -replace '[\x00-\x1F\x7F]', '' ; $CleanThreatDetails = if ($ThreatDetails) { $ThreatDetails -replace '[\x00-\x1F\x7F]', '' } else { $null } ; $payloadContent = @{ laptop_identifier = $AliasName; client_scan_time = $ScanTime; scan_type = $ScanType; scan_result_message = $CleanResultMessage; threats_found = $ThreatsFound }; if ($null -ne $CleanThreatDetails -and (-not [string]::IsNullOrWhiteSpace($CleanThreatDetails))) { $payloadContent.threat_details = $CleanThreatDetails } else { $payloadContent.threat_details = $null } ; $payloadBodyJson = $payloadContent | ConvertTo-Json -Depth 5 -Compress; $utf8Encoding = [System.Text.Encoding]::UTF8; $payloadBytes = $utf8Encoding.GetBytes($payloadBodyJson); $requestHeaders = @{ "Content-Type" = "application/json; charset=utf-8"; "X-API-Key" = $ApiKey }; Write-Log -Message "Sende Bericht... (Länge: $($payloadBytes.Length) bytes)"; $ErrorActionPreferenceBackup = $ErrorActionPreference; $ErrorActionPreference = "Stop"; try { Invoke-RestMethod -Uri $ReportUrl -Method Post -Body $payloadBytes -Headers $requestHeaders -TimeoutSec 120; Write-Log -Message "Scan-Bericht erfolgreich an Server gesendet."; $Global:LastSuccessfulReportTimeUTC = (Get-Date).ToUniversalTime(); try { ($Global:LastSuccessfulReportTimeUTC.ToString("o") | ConvertTo-Json -Compress) | Set-Content -Path $LastReportTimeFilePath -Force -Encoding UTF8; Write-Log -Message "Letzte erfolgreiche Report-Zeit aktualisiert: $($Global:LastSuccessfulReportTimeUTC.ToLocalTime())" } catch { Write-Log -Level WARN -Message "Fehler beim Speichern von '$LastReportTimeFilePath': $($_.Exception.Message)" }; return $true } catch { $CaughtException = $_; Write-Log -Level ERROR -Message "FEHLER bei Send-ScanReport: $($CaughtException.ToString())"; if ($CaughtException.Exception -is [System.Net.WebException] -and $null -ne $CaughtException.Exception.Response) { $webEx = $CaughtException.Exception; $httpResponse = $webEx.Response; $actualHttpStatusCode = [int]$httpResponse.StatusCode; Write-Log -Level ERROR -Message " HTTP Status: $actualHttpStatusCode"; try { $responseStream = $httpResponse.GetResponseStream(); $streamReader = New-Object System.IO.StreamReader($responseStream, [System.Text.Encoding]::UTF8); $errorBodyContent = $streamReader.ReadToEnd(); $streamReader.Close(); $responseStream.Close(); Write-Log -Level ERROR -Message " Fehler-Body vom Server: $errorBodyContent" } catch { Write-Log -Level ERROR -Message " Zusätzlicher Fehler beim Lesen des Fehler-Bodys: $($_.Exception.Message)" } }; return $false } finally { $ErrorActionPreference = $ErrorActionPreferenceBackup } }; function ConvertFrom-DefenderEvent { param( [Parameter(Mandatory = $true)] $Event ); $eventTimeUTC = $Event.TimeCreated.ToUniversalTime().ToString("o"); $simplified = @{ Message = "Event $($Event.Id): " + (($Event.Message -replace '[\x00-\x1F\x7F]', '').Trim() -split '\r?\n')[0]; ThreatsFound = $false; ThreatDetails = $null }; if ($simplified.Message -match "Bedrohung gefunden" -or $simplified.Message -match "Malware found") { $simplified.ThreatsFound = $true }; if ($Event.Id -in (1002, 1116, 1117, 1118)) { $simplified.ThreatsFound = $true }; if ($Event.Message -match "(?:Name|Threat Name):\s*(.*?)\s*(?:Pfad|Path|File):\s*(.*?)\s*(?:Aktion|Action):\s*(.*?)(?:\r?\n|$)") { $simplified.ThreatDetails = "Name: $($Matches[1].Trim()), Pfad: $($Matches[2].Trim()), Aktion: $($Matches[3].Trim())" } elseif ($Event.Message -match "(?:Name|Threat Name):\s*(.*?)\s*(?:Pfad|Path|File):\s*(.*?)(?:\r?\n|$)") { $simplified.ThreatDetails = "Name: $($Matches[1].Trim()), Pfad: $($Matches[2].Trim())" }; return [PSCustomObject]$simplified } + function Send-ScanReport { param( [Parameter(Mandatory = $true)][string]$ScanTime, [Parameter(Mandatory = $true)][string]$ScanType, [Parameter(Mandatory = $true)][string]$ScanResultMessage, [Parameter(Mandatory = $true)][bool]$ThreatsFound, [string]$ThreatDetails = $null ); Write-Log -Message "Bereite Scan-Bericht ($ScanType) für Versand vor."; if ([string]::IsNullOrWhiteSpace($ScanTime)) { $ScanTime = (Get-Date "1970-01-01").ToUniversalTime().ToString("o") } ; if ([string]::IsNullOrWhiteSpace($ScanType)) { $ScanType = "Unbekannt" } ; if ([string]::IsNullOrWhiteSpace($ScanResultMessage)) { $ScanResultMessage = "Keine Meldung" } ; $CleanResultMessage = $ScanResultMessage -replace '[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]', '' ; $CleanThreatDetails = if ($ThreatDetails) { $ThreatDetails -replace '[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]', '' } else { $null } ; $payloadContent = @{ laptop_identifier = $AliasName; client_scan_time = $ScanTime; scan_type = $ScanType; scan_result_message = $CleanResultMessage; threats_found = $ThreatsFound }; if ($null -ne $CleanThreatDetails -and (-not [string]::IsNullOrWhiteSpace($CleanThreatDetails))) { $payloadContent.threat_details = $CleanThreatDetails } else { $payloadContent.threat_details = $null } ; $payloadBodyJson = $payloadContent | ConvertTo-Json -Depth 5 -Compress; $utf8Encoding = [System.Text.Encoding]::UTF8; $payloadBytes = $utf8Encoding.GetBytes($payloadBodyJson); $requestHeaders = @{ "Content-Type" = "application/json; charset=utf-8"; "X-API-Key" = $ApiKey }; Write-Log -Message "Sende Bericht... (Länge: $($payloadBytes.Length) bytes)"; $ErrorActionPreferenceBackup = $ErrorActionPreference; $ErrorActionPreference = "Stop"; try { Invoke-RestMethod -Uri $ReportUrl -Method Post -Body $payloadBytes -Headers $requestHeaders -TimeoutSec 120; Write-Log -Message "Scan-Bericht erfolgreich an Server gesendet."; $Global:LastSuccessfulReportTimeUTC = (Get-Date).ToUniversalTime(); try { ($Global:LastSuccessfulReportTimeUTC.ToString("o") | ConvertTo-Json -Compress) | Set-Content -Path $LastReportTimeFilePath -Force -Encoding UTF8; Write-Log -Message "Letzte erfolgreiche Report-Zeit aktualisiert: $($Global:LastSuccessfulReportTimeUTC.ToLocalTime())" } catch { Write-Log -Level WARN -Message "Fehler beim Speichern von '$LastReportTimeFilePath': $($_.Exception.Message)" }; return $true } catch { $CaughtException = $_; Write-Log -Level ERROR -Message "FEHLER bei Send-ScanReport: $($CaughtException.ToString())"; if ($CaughtException.Exception -is [System.Net.WebException] -and $null -ne $CaughtException.Exception.Response) { $webEx = $CaughtException.Exception; $httpResponse = $webEx.Response; $actualHttpStatusCode = [int]$httpResponse.StatusCode; Write-Log -Level ERROR -Message " HTTP Status: $actualHttpStatusCode"; try { $responseStream = $httpResponse.GetResponseStream(); $streamReader = New-Object System.IO.StreamReader($responseStream, [System.Text.Encoding]::UTF8); $errorBodyContent = $streamReader.ReadToEnd(); $streamReader.Close(); $responseStream.Close(); Write-Log -Level ERROR -Message " Fehler-Body vom Server: $errorBodyContent" } catch { Write-Log -Level ERROR -Message " Zusätzlicher Fehler beim Lesen des Fehler-Bodys: $($_.Exception.Message)" } }; return $false } finally { $ErrorActionPreference = $ErrorActionPreferenceBackup } } + + function ConvertFrom-DefenderEvent { param( [Parameter(Mandatory = $true)] $Event ); $eventTimeUTC = $Event.TimeCreated.ToUniversalTime().ToString("o"); + $cleanMsg = $Event.Message -replace '[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]', '' -replace '%n', "`n" -replace '%t', " " -replace '%b', "" + $simplified = @{ Message = "Event $($Event.Id): " + $cleanMsg.Trim(); ThreatsFound = $false; ThreatDetails = $null }; if ($simplified.Message -match "Bedrohung gefunden" -or $simplified.Message -match "Malware found") { $simplified.ThreatsFound = $true }; if ($Event.Id -in (1116, 1117, 1118)) { $simplified.ThreatsFound = $true }; if ($Event.Message -match "(?:Name|Threat Name):\s*(.*?)\s*(?:Pfad|Path|File):\s*(.*?)\s*(?:Aktion|Action):\s*(.*?)(?:\r?\n|$)") { $simplified.ThreatDetails = "Name: $($Matches[1].Trim()), Pfad: $($Matches[2].Trim()), Aktion: $($Matches[3].Trim())" } elseif ($Event.Message -match "(?:Name|Threat Name):\s*(.*?)\s*(?:Pfad|Path|File):\s*(.*?)(?:\r?\n|$)") { $simplified.ThreatDetails = "Name: $($Matches[1].Trim()), Pfad: $($Matches[2].Trim())" }; return [PSCustomObject]$simplified } # --- HAUPT-POLLING-SCHLEIFE --- $currentRetryDelay = $InitialRetryDelaySeconds @@ -108,14 +112,12 @@ while ($true) { if ($elapsedMinutes -gt $timeoutLimitMinutes) { $isTimedOut = $true } if ($Script:ActiveScanJob.State -eq 'Running' -and (-not $isTimedOut)) { - try { - $completionEvent = Get-WinEvent -FilterHashtable @{ProviderName="Microsoft-Windows-Windows Defender"; ID=1001,1002,1005; StartTime=$Script:ScanInitiationTimeUTC.ToLocalTime()} -MaxEvents 1 -ErrorAction SilentlyContinue - } - catch { Write-Log -Level WARN -Message "Fehler bei proaktiver Event-Suche: $($_.Exception.Message)" } + # We NO LONGER check for completionEvent here to avoid preempting the job due to unrelated background scan events! + $completionEvent = $null } - if ($Script:ActiveScanJob.State -in @('Completed', 'Failed', 'Stopped') -or $completionEvent -or $isTimedOut) { - Write-Log -Message "Scan-Abschluss erkannt. Grund: Job-Status='$($Script:ActiveScanJob.State)', Event-Gefunden='$($completionEvent -ne $null)', Timeout='$isTimedOut'." + if ($Script:ActiveScanJob.State -in @('Completed', 'Failed', 'Stopped') -or $isTimedOut) { + Write-Log -Message "Scan-Abschluss erkannt. Grund: Job-Status='$($Script:ActiveScanJob.State)', Timeout='$isTimedOut'." if ($isTimedOut -and $Script:ActiveScanJob.State -notlike 'Stopped') { Write-Log -Level WARN -Message "Scan hat das Zeitlimit von $timeoutLimitMinutes Minuten überschritten. Breche Job ab." Stop-Job -Job $Script:ActiveScanJob -Force From 5cfd0b6a1dc24bd569eeca5c9e505fc8a050d0a5 Mon Sep 17 00:00:00 2001 From: BitWuehler <39520752+BitWuehler@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:11:37 +0200 Subject: [PATCH 12/13] Fix dashboard overview to also show Fehler retroactively, clean pseudo-localization tokens, and fix row height --- app/web_routes.py | 39 +++++++++++++++++++++++++++++---- templates/daily_report.html | 2 ++ templates/laptops_overview.html | 2 ++ 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/app/web_routes.py b/app/web_routes.py index ffb04a6..6dfd1c9 100644 --- a/app/web_routes.py +++ b/app/web_routes.py @@ -65,8 +65,21 @@ async def web_laptops_overview(request: Request, db: Session = Depends(get_db), hours_since = time_since_last_scan.total_seconds() / 3600.0 hours_rounded = round(hours_since) - if laptop_instance.last_scan_threats_found is True: + # Retroactively fix old DB entries where Event 1002 was marked as a threat + is_real_threat = laptop_instance.last_scan_threats_found + is_error = False + if laptop_instance.last_scan_result_message and ("Event 1002" in laptop_instance.last_scan_result_message or "FEHLER:" in laptop_instance.last_scan_result_message or "stopped" in laptop_instance.last_scan_result_message or "Fehler" in laptop_instance.last_scan_result_message): + if "Event 1002" in laptop_instance.last_scan_result_message: + is_real_threat = False # 1002 is just a cancelled scan, not a threat + is_error = True + + laptop_instance.last_scan_threats_found = is_real_threat + laptop_instance.is_error = is_error + + if is_real_threat is True: status_info = {"text": "Bedrohung(en) gefunden!", "color_class": "status-red", "style": ""} + elif is_error: + status_info = {"text": "Fehler / Abbruch", "color_class": "status-yellow", "style": ""} else: if hours_since <= 5: hue = 120 # Green @@ -90,11 +103,14 @@ async def web_laptops_overview(request: Request, db: Session = Depends(get_db), simplified_result_message = "N/A" if laptop_instance.last_scan_result_message: - msg = laptop_instance.last_scan_result_message - if "erfolgreich abgeschlossen" in msg: + # Clean up old pseudo-localization tokens from database + clean_msg = laptop_instance.last_scan_result_message.replace('%n', '\n').replace('%t', ' ').replace('%b', '') + laptop_instance.last_scan_result_message = clean_msg + + if "erfolgreich abgeschlossen" in clean_msg: simplified_result_message = "OK" else: - simplified_result_message = msg[:30] + ("..." if len(msg) > 30 else "") + simplified_result_message = clean_msg[:30] + ("..." if len(clean_msg) > 30 else "") has_error = False @@ -186,6 +202,10 @@ async def export_daily_report_csv(request: Request, report_date_str: Optional[st if len(scan_result) > 50: scan_result = scan_result[:50] + "..." threats_str = "Nein" + + # Clean up old pseudo-localization tokens from database + threats_str = threats_str.replace('%n', '\n').replace('%t', ' ').replace('%b', '') + scan_result = scan_result.replace('%n', '\n').replace('%t', ' ').replace('%b', '') writer.writerow([laptop.alias_name, laptop.hostname, scan_time_str, scan_result, threats_str]) @@ -253,6 +273,9 @@ async def web_daily_report(request: Request, report_date_str: Optional[str] = No is_real_threat = False # 1002 is just a cancelled scan, not a threat is_error = True + laptop.last_scan_threats_found = is_real_threat + laptop.is_error = is_error + if is_real_threat is True: if getattr(laptop, "last_scan_threat_details", None): status_text = laptop.last_scan_threat_details @@ -270,6 +293,14 @@ async def web_daily_report(request: Request, report_date_str: Optional[str] = No status_text, color_class = "OK (Scan <24h)", "status-green" else: status_text, color_class = "OK (Scan älter)", "status-yellow" + + # Clean up old pseudo-localization tokens from database + if status_text: + status_text = status_text.replace('%n', '\n').replace('%t', ' ').replace('%b', '') + + # Truncate very long texts if they aren't threats or errors to save space + if not is_real_threat and not is_error and len(status_text) > 100: + status_text = status_text[:100] + "..." else: status_text, color_class = "Kein Scan bisher", "status-white" diff --git a/templates/daily_report.html b/templates/daily_report.html index 84f35dc..46ef33b 100644 --- a/templates/daily_report.html +++ b/templates/daily_report.html @@ -131,6 +131,8 @@
|---|