v0.1.33 — Offboarding-Report und Update-Hinweis
- Offboarding: HTML-Report pro Geraet (New-OffboardHtmlReport) + reportUrl; Dialog-Ergebnis pro Geraet gruppiert + Report-Link - Update-Check: GET /api/update/check (Gitea releases/latest, Compare-SemVer); Kopfzeilen-Badge bei neuer Version, Link zur Release-Seite; optionaler read-only Token Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,17 @@ Versionierung folgt [Semantic Versioning](https://semver.org/lang/de/) — solan
|
||||
|
||||
---
|
||||
|
||||
## [0.1.33] - 2026-09-13
|
||||
|
||||
Offboarding-Report und Update-Hinweis.
|
||||
|
||||
### Neu
|
||||
|
||||
- **Offboarding-Report**: Nach dem Ausführen erzeugt das Offboarding einen **HTML-Report** (`/reports/offboard-report-<zeit>.html`) im Stil des Zuweisungs-Reports — mit KPIs (Geräte / Aktionen / Erfolg / Fehler) und **pro Gerät** einer Tabelle der ausgeführten Löschungen (Dienst · Aktion · Status · Meldung). Das Ergebnis im Dialog ist jetzt ebenfalls **pro Gerät gruppiert**, plus ein Button „HTML-Report öffnen".
|
||||
- **Update-Hinweis**: Beim Start prüft die App (nicht-blockierend) die neueste Release-Version über **Gitea** und zeigt bei einer neueren Version ein **Badge in der Kopfzeile** mit Link zur Release-Seite (dort MSI herunterladen → In-Place-Upgrade). Nur Lesezugriff; optionaler read-only Token via `settings.json → update.token`, falls das Repo privat ist. Neuer Endpoint `GET /api/update/check`.
|
||||
|
||||
---
|
||||
|
||||
## [0.1.32] - 2026-09-11
|
||||
|
||||
Bulk-Offboarding aus dem Geräte-Tab, Geräte-Ladefehler behoben, bessere Graph-Diagnose.
|
||||
|
||||
@@ -189,7 +189,7 @@ $script:State = [pscustomobject]@{
|
||||
Session = @() # geplante Zuweisungen
|
||||
}
|
||||
|
||||
$script:ToolVersion = "0.1.32"
|
||||
$script:ToolVersion = "0.1.33"
|
||||
$script:BuildStamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
||||
|
||||
Write-Host ""
|
||||
|
||||
@@ -12,7 +12,7 @@ if (Test-Path "$x64Dotnet\dotnet.exe") {
|
||||
$env:DOTNET_ROOT = "C:\Program Files\dotnet"
|
||||
}
|
||||
|
||||
$version = "0.1.32"
|
||||
$version = "0.1.33"
|
||||
$src = "\\Mac\Home\ClaudePRJ\Intune Manager"
|
||||
$stage = "$env:TEMP\IntuneManagerStage"
|
||||
$installerDir = "$src\installer"
|
||||
|
||||
+255
@@ -21,6 +21,7 @@ function Invoke-ApiHandler {
|
||||
"POST /api/settings/test" { return Test-SettingsEndpoint -Body $Body }
|
||||
"POST /api/settings/logo" { return Save-LogoEndpoint -Body $Body }
|
||||
"DELETE /api/settings/logo" { return Remove-LogoEndpoint }
|
||||
"GET /api/update/check" { return Get-UpdateCheckEndpoint }
|
||||
"POST /api/connect" { return Invoke-ConnectEndpoint }
|
||||
"POST /api/connect/token" { return Invoke-ConnectWithTokenEndpoint -Body $Body }
|
||||
"POST /api/connect/start" { return Start-DeviceCodeConnect }
|
||||
@@ -136,6 +137,65 @@ function Invoke-ApiHandler {
|
||||
return $null
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Update-Check (neueste Release-Version von Gitea vergleichen)
|
||||
# ============================================================
|
||||
|
||||
# Semver-Vergleich: 1 wenn A>B, -1 wenn A<B, 0 gleich. Fuehrendes 'v' und
|
||||
# Pre-Release/Build-Suffixe werden ignoriert; fehlende Teile zaehlen als 0.
|
||||
function Compare-SemVer {
|
||||
param([string]$A, [string]$B)
|
||||
$clean = {
|
||||
param($s)
|
||||
$s = ([string]$s).Trim().TrimStart('v','V')
|
||||
$s = ($s -split '[-+ ]')[0]
|
||||
@($s -split '\.' | ForEach-Object { [int]([regex]::Match($_, '\d+').Value -as [int]) })
|
||||
}
|
||||
$pa = & $clean $A
|
||||
$pb = & $clean $B
|
||||
$len = [Math]::Max($pa.Count, $pb.Count)
|
||||
for ($i = 0; $i -lt $len; $i++) {
|
||||
$x = if ($i -lt $pa.Count) { [int]$pa[$i] } else { 0 }
|
||||
$y = if ($i -lt $pb.Count) { [int]$pb[$i] } else { 0 }
|
||||
if ($x -gt $y) { return 1 }
|
||||
if ($x -lt $y) { return -1 }
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
# Prueft die neueste Release-Version. Quelle standardmaessig das Gitea-Repo;
|
||||
# ueber Settings.update.apiUrl / .token ueberschreibbar (falls Repo privat).
|
||||
# Braucht KEINE Graph-Verbindung. Fehler werden still zurueckgegeben (kein Banner).
|
||||
function Get-UpdateCheckEndpoint {
|
||||
$current = [string]$script:ToolVersion
|
||||
$out = @{ current = $current; latest = ''; updateAvailable = $false; releaseUrl = ''; error = '' }
|
||||
|
||||
$api = 'https://git.wende.it/api/v1/repos/marco/Intune-Manager/releases/latest'
|
||||
$token = ''
|
||||
try {
|
||||
if ($script:Settings.update) {
|
||||
if ($script:Settings.update.apiUrl) { $api = [string]$script:Settings.update.apiUrl }
|
||||
if ($script:Settings.update.token) { $token = [string]$script:Settings.update.token }
|
||||
}
|
||||
} catch {}
|
||||
|
||||
$headers = @{ 'User-Agent' = 'IntuneManager' }
|
||||
if ($token) { $headers['Authorization'] = "token $token" }
|
||||
|
||||
try { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } catch {}
|
||||
try {
|
||||
$rel = Invoke-RestMethod -Uri $api -Headers $headers -Method GET -TimeoutSec 8
|
||||
$latest = ([string]$rel.tag_name).TrimStart('v','V')
|
||||
$out.latest = $latest
|
||||
$out.releaseUrl = [string]$rel.html_url
|
||||
$out.updateAvailable = ((Compare-SemVer $latest $current) -gt 0)
|
||||
} catch {
|
||||
$out.error = $_.Exception.Message
|
||||
Write-Host "[UPDATE] Versions-Check fehlgeschlagen: $($_.Exception.Message)" -ForegroundColor DarkGray
|
||||
}
|
||||
return $out
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Status & Connection
|
||||
# ============================================================
|
||||
@@ -3605,3 +3665,198 @@ footer code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;bac
|
||||
[IO.File]::WriteAllText($path, $html, [System.Text.Encoding]::UTF8)
|
||||
return $path
|
||||
}
|
||||
|
||||
# HTML-Report fuers Geraete-Offboarding — pro Geraet die ausgefuehrten Loeschungen
|
||||
# (Entra/Intune/Autopilot) mit Status + Meldung. Liegt in Api.ps1 (BOM) wegen Umlauten.
|
||||
function New-OffboardHtmlReport {
|
||||
param($Devices, $Results, [int]$Success, [int]$Errors, [int]$Total, [string]$EntraAction, [bool]$DoIntune, [bool]$DoAutopilot)
|
||||
|
||||
Add-Type -AssemblyName System.Web
|
||||
function _e { param($s) if ($null -eq $s) { return "" } return [System.Web.HttpUtility]::HtmlEncode([string]$s) }
|
||||
|
||||
$ts = Get-Date -Format "yyyy-MM-dd_HH-mm-ss"
|
||||
$tsHuman = Get-Date -Format "dd.MM.yyyy HH:mm:ss"
|
||||
$name = "offboard-report-$ts.html"
|
||||
$path = Join-Path $script:Config.ReportDir $name
|
||||
$user = $env:USERNAME
|
||||
$machine = $env:COMPUTERNAME
|
||||
$tenant = if ($script:State.TenantId) { $script:State.TenantId } else { "" }
|
||||
$account = if ($script:State.Account) { $script:State.Account } else { "" }
|
||||
$devCount = @($Devices).Count
|
||||
|
||||
$bannerCls = if ($Errors -gt 0) { "banner-err" } else { "banner-ok" }
|
||||
$bannerTxt = if ($Errors -gt 0) { "$Errors von $Total Aktion(en) fehlgeschlagen - Details unten" } else { "Alle $Success Aktion(en) erfolgreich" }
|
||||
|
||||
# Dienst-Zusammenfassung (was angefordert wurde)
|
||||
$svcParts = @()
|
||||
if ($EntraAction -eq 'delete') { $svcParts += "Entra ID: geloescht" }
|
||||
elseif ($EntraAction -eq 'disable') { $svcParts += "Entra ID: deaktiviert" }
|
||||
if ($DoIntune) { $svcParts += "Intune: geloescht" }
|
||||
if ($DoAutopilot) { $svcParts += "Autopilot: geloescht" }
|
||||
$svcTxt = if ($svcParts.Count -gt 0) { ($svcParts -join " · ") } else { "keine" }
|
||||
|
||||
# Ergebnisse pro Geraet gruppieren (Reihenfolge = Geraeteliste)
|
||||
$byDev = [ordered]@{}
|
||||
foreach ($d in @($Devices)) { $dn = [string]$d.deviceName; if ($dn -and -not $byDev.Contains($dn)) { $byDev[$dn] = @() } }
|
||||
foreach ($r in @($Results)) {
|
||||
$dn = [string]$r.deviceName
|
||||
if (-not $byDev.Contains($dn)) { $byDev[$dn] = @() }
|
||||
$byDev[$dn] += $r
|
||||
}
|
||||
|
||||
$devBlocks = ""
|
||||
foreach ($dn in @($byDev.Keys)) {
|
||||
$rows = @($byDev[$dn])
|
||||
$okC = @($rows | Where-Object { $_.success }).Count
|
||||
$errC = @($rows | Where-Object { -not $_.success }).Count
|
||||
$rowsHtml = ""
|
||||
foreach ($r in $rows) {
|
||||
$cls = if ($r.success) { "ok" } else { "err" }
|
||||
$ico = if ($r.success) { "✓" } else { "!" }
|
||||
$lbl = if ($r.success) { "Erfolg" } else { "Fehler" }
|
||||
$msg = if ($r.error) { _e $r.error } else { "" }
|
||||
$rowsHtml += "<tr class='row-$cls'>" +
|
||||
"<td class='c-status'><span class='status-pill status-$cls'>$ico $lbl</span></td>" +
|
||||
"<td class='c-svc'>$(_e $r.service)</td>" +
|
||||
"<td class='c-act'>$(_e $r.action)</td>" +
|
||||
"<td class='c-msg'>$msg</td></tr>"
|
||||
}
|
||||
if (-not $rowsHtml) { $rowsHtml = "<tr><td colspan='4' class='c-msg'>Keine ausgefuehrten Aktionen.</td></tr>" }
|
||||
$stats = ""
|
||||
if ($okC -gt 0) { $stats += "<span class='hd-pill hd-ok'>$okC OK</span>" }
|
||||
if ($errC -gt 0) { $stats += "<span class='hd-pill hd-err'>$errC Fehler</span>" }
|
||||
$devBlocks += "<details class='dev-block' open><summary class='dev-head'>" +
|
||||
"<span class='dev-name'>$(_e $dn)</span><span class='dev-stats'>$stats</span></summary>" +
|
||||
"<div class='dev-body'><table class='res-table'><thead><tr>" +
|
||||
"<th class='c-status'>Status</th><th class='c-svc'>Dienst</th><th class='c-act'>Aktion</th><th class='c-msg'>Meldung</th>" +
|
||||
"</tr></thead><tbody>$rowsHtml</tbody></table></div></details>"
|
||||
}
|
||||
if (-not $devBlocks) { $devBlocks = "<div class='empty'>Keine Ergebnisse.</div>" }
|
||||
|
||||
$html = @"
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Intune Offboarding-Report - $tsHuman</title>
|
||||
<link rel="preconnect" href="https://rsms.me/">
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
|
||||
<style>
|
||||
:root{
|
||||
--black-100:#0A0E15;--black-80:#373F4E;--black-60:#667085;
|
||||
--white-100:#FFFFFF;--white-90:#F0F1F5;--white-80:#E0E4EB;--white-70:#D1D6E0;
|
||||
--blue-60:#3566B6;--blue-10:#E0ECFF;
|
||||
--success-100:#008072;--success-60:#A0E3D8;--success-10:#D0FBF5;
|
||||
--error-100:#B21919;--error-60:#EA9E9E;--error-10:#FBE0E0;
|
||||
--warning-100:#8F7200;--warning-60:#F8E081;--warning-10:#FFF3C2;
|
||||
--bg:#F7FAFD;--surface:#FFF;--surface-soft:#F4F7FB;--hairline:var(--white-70);--hairline-soft:var(--white-90);
|
||||
--text-primary:var(--black-100);--text-secondary:var(--black-80);--text-muted:var(--black-60);
|
||||
--r-sm:6px;--r-md:10px;--r-lg:14px;--shadow-soft:0 1px 2px rgba(23,64,130,.04),0 2px 6px rgba(23,64,130,.04);
|
||||
}
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
html,body{font-family:'Inter',ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif;background:var(--bg);color:var(--text-secondary);line-height:1.5;-webkit-font-smoothing:antialiased}
|
||||
body{padding:32px 16px 80px}
|
||||
.container{max-width:1100px;margin:0 auto}
|
||||
.head{background:var(--surface);border:1px solid var(--hairline);border-radius:var(--r-lg);padding:24px 28px;margin-bottom:20px;box-shadow:var(--shadow-soft)}
|
||||
.head-top{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:16px}
|
||||
.brand{display:flex;align-items:center;gap:12px}
|
||||
.logo{width:40px;height:40px;border-radius:9px;background:var(--blue-60);color:#fff;display:grid;place-items:center;font-size:20px;font-weight:700}
|
||||
.title{font-size:22px;font-weight:700;letter-spacing:-.015em;color:var(--text-primary)}
|
||||
.subtitle{font-size:13px;color:var(--text-muted);margin-top:2px}
|
||||
.head-meta{display:flex;flex-wrap:wrap;gap:6px}
|
||||
.meta-pill{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;background:var(--surface-soft);border:1px solid var(--hairline);padding:5px 10px;border-radius:999px;color:var(--text-secondary);font-weight:500}
|
||||
.meta-pill .lbl{color:var(--text-muted);text-transform:uppercase;font-size:10px;letter-spacing:.04em;font-weight:600}
|
||||
.meta-pill code{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:11px}
|
||||
.banner{margin-top:18px;padding:12px 16px;border-radius:var(--r-md);font-size:13.5px;font-weight:500;display:flex;align-items:center;gap:10px;border:1px solid transparent}
|
||||
.banner-ok{background:var(--success-10);border-color:var(--success-60);color:var(--success-100)}
|
||||
.banner-err{background:var(--error-10);border-color:var(--error-60);color:var(--error-100)}
|
||||
.banner-icon{width:22px;height:22px;border-radius:50%;display:inline-grid;place-items:center;font-size:13px;font-weight:700}
|
||||
.banner-ok .banner-icon{background:var(--success-60);color:var(--success-100)}
|
||||
.banner-err .banner-icon{background:var(--error-60);color:var(--error-100)}
|
||||
.kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-bottom:20px}
|
||||
.kpi{background:var(--surface);border:1px solid var(--hairline);border-radius:var(--r-md);padding:18px}
|
||||
.kpi-num{font-size:30px;font-weight:700;line-height:1;letter-spacing:-.02em;font-variant-numeric:tabular-nums;color:var(--text-primary)}
|
||||
.kpi-lbl{font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.04em;font-weight:600;margin-top:6px}
|
||||
.kpi.ok .kpi-num{color:var(--success-100)}
|
||||
.kpi.err .kpi-num{color:var(--error-100)}
|
||||
.section{background:var(--surface);border:1px solid var(--hairline);border-radius:var(--r-lg);padding:22px 24px;margin-bottom:18px;box-shadow:var(--shadow-soft)}
|
||||
.section-title{font-size:15px;font-weight:600;color:var(--text-primary);margin-bottom:4px}
|
||||
.section-sub{font-size:12px;color:var(--text-muted);margin-bottom:14px}
|
||||
.dev-block{border:1px solid var(--hairline);border-radius:var(--r-md);overflow:hidden;margin-bottom:10px;background:var(--surface)}
|
||||
.dev-head{cursor:pointer;padding:12px 16px;display:flex;align-items:center;justify-content:space-between;gap:12px;list-style:none}
|
||||
.dev-head::-webkit-details-marker{display:none}
|
||||
.dev-name{font-size:14px;font-weight:600;color:var(--text-primary)}
|
||||
.dev-stats{display:flex;gap:6px}
|
||||
.hd-pill{font-size:10.5px;font-weight:700;padding:2px 8px;border-radius:999px}
|
||||
.hd-ok{background:var(--success-10);color:var(--success-100)}
|
||||
.hd-err{background:var(--error-10);color:var(--error-100)}
|
||||
.dev-body{border-top:1px solid var(--hairline-soft)}
|
||||
.res-table{width:100%;border-collapse:collapse;font-size:13px}
|
||||
.res-table th{text-align:left;font-size:10.5px;text-transform:uppercase;letter-spacing:.04em;color:var(--text-muted);font-weight:600;padding:8px 16px;background:var(--surface-soft);border-bottom:1px solid var(--hairline-soft)}
|
||||
.res-table td{padding:9px 16px;border-bottom:1px solid var(--hairline-soft);vertical-align:top}
|
||||
.res-table tr:last-child td{border-bottom:none}
|
||||
.c-status{width:120px}.c-svc{width:120px;font-weight:600;color:var(--text-primary)}.c-act{width:130px}
|
||||
.c-msg{color:var(--error-100)}
|
||||
.status-pill{display:inline-flex;align-items:center;gap:4px;font-size:11px;font-weight:600;padding:3px 9px;border-radius:999px;border:1px solid transparent;white-space:nowrap}
|
||||
.status-ok{background:var(--success-10);color:var(--success-100);border-color:var(--success-60)}
|
||||
.status-err{background:var(--error-10);color:var(--error-100);border-color:var(--error-60)}
|
||||
.empty{padding:18px;text-align:center;color:var(--text-muted);font-size:13px;background:var(--surface-soft);border-radius:var(--r-md);border:1px dashed var(--hairline)}
|
||||
footer{text-align:center;color:var(--text-muted);font-size:11.5px;margin-top:24px}
|
||||
footer code{font-family:ui-monospace,Menlo,Consolas,monospace;background:var(--surface);padding:1px 6px;border-radius:4px;border:1px solid var(--hairline)}
|
||||
@media print{body{background:#fff;padding:0}.section,.head,.kpi,.dev-block{box-shadow:none;break-inside:avoid}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header class="head">
|
||||
<div class="head-top">
|
||||
<div class="brand">
|
||||
<div class="logo">I</div>
|
||||
<div>
|
||||
<div class="title">Intune Offboarding-Report</div>
|
||||
<div class="subtitle">$tsHuman · ausgefuehrt von $(_e $user)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="head-meta">
|
||||
"@
|
||||
if ($account) { $html += "<span class='meta-pill'><span class='lbl'>Account</span> $(_e $account)</span>" }
|
||||
if ($tenant) { $html += "<span class='meta-pill'><span class='lbl'>Tenant</span> <code>$(_e $tenant)</code></span>" }
|
||||
$html += "<span class='meta-pill'><span class='lbl'>Host</span> $(_e $machine)</span>"
|
||||
$html += @"
|
||||
</div>
|
||||
</div>
|
||||
<div class="banner $bannerCls">
|
||||
<span class="banner-icon">$(if ($Errors -gt 0) { '!' } else { '✓' })</span>
|
||||
<span>$bannerTxt</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="kpis">
|
||||
<div class="kpi tot"><div class="kpi-num">$devCount</div><div class="kpi-lbl">Geraete</div></div>
|
||||
<div class="kpi tot"><div class="kpi-num">$Total</div><div class="kpi-lbl">Aktionen</div></div>
|
||||
<div class="kpi ok"><div class="kpi-num">$Success</div><div class="kpi-lbl">Erfolgreich</div></div>
|
||||
<div class="kpi err"><div class="kpi-num">$Errors</div><div class="kpi-lbl">Fehler</div></div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">Angeforderte Dienste</div>
|
||||
<div class="section-sub">$svcTxt</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">Ergebnisse pro Geraet</div>
|
||||
<div class="section-sub">Jeder Block zeigt die ausgefuehrten Loeschungen/Aenderungen mit Status und Meldung.</div>
|
||||
$devBlocks
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
Intune Manager · Web Edition <code>v$(_e $script:ToolVersion)</code> · Offboarding-Report
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"@
|
||||
[IO.File]::WriteAllText($path, $html, [System.Text.Encoding]::UTF8)
|
||||
return $path
|
||||
}
|
||||
|
||||
+14
-2
@@ -281,7 +281,19 @@ function Invoke-OffboardExecuteEndpoint {
|
||||
success = $ok; error = $errMsg
|
||||
}
|
||||
}
|
||||
$okCount = @($results | Where-Object { $_.success }).Count
|
||||
$okCount = @($results | Where-Object { $_.success }).Count
|
||||
$errCount = $results.Count - $okCount
|
||||
Write-Host " -> $okCount/$($results.Count) erfolgreich" -ForegroundColor DarkGray
|
||||
return @{ ok = $true; successCount = $okCount; total = $results.Count; results = @($results) }
|
||||
|
||||
# HTML-Report erzeugen (pro Geraet die ausgefuehrten Loeschungen) und verlinken.
|
||||
$reportUrl = ""
|
||||
try {
|
||||
$reportFile = New-OffboardHtmlReport -Devices $devices -Results $results -Success $okCount -Errors $errCount -Total $results.Count -EntraAction $entraAction -DoIntune $doIntune -DoAutopilot $doAutopilot
|
||||
if ($reportFile) { $reportUrl = "/reports/" + (Split-Path $reportFile -Leaf) }
|
||||
Write-Host "[OFFBOARD] HTML-Report: $reportFile" -ForegroundColor DarkGray
|
||||
} catch {
|
||||
Write-Host "[OFFBOARD] Report-Erstellung fehlgeschlagen: $($_.Exception.Message)" -ForegroundColor Red
|
||||
}
|
||||
|
||||
return @{ ok = $true; successCount = $okCount; total = $results.Count; results = @($results); reportUrl = [string]$reportUrl }
|
||||
}
|
||||
|
||||
+42
-2
@@ -4784,6 +4784,24 @@ document.getElementById('btnRestoreColors')?.addEventListener('click', () => {
|
||||
// Init
|
||||
// =============================================================
|
||||
|
||||
// Prueft (nicht-blockierend) auf eine neuere Release-Version und zeigt bei Bedarf
|
||||
// das Update-Badge in der Kopfzeile mit Link zur Release-Seite.
|
||||
async function checkForUpdate() {
|
||||
try {
|
||||
const res = await api('/api/update/check');
|
||||
if (res && res.updateAvailable && res.releaseUrl) {
|
||||
const badge = document.getElementById('updateBadge');
|
||||
const txt = document.getElementById('updateBadgeText');
|
||||
if (badge && txt) {
|
||||
txt.textContent = `Update ${res.latest}`;
|
||||
badge.href = res.releaseUrl;
|
||||
badge.title = `Neue Version ${res.latest} verfügbar (aktuell ${res.current}) — Release-Seite öffnen`;
|
||||
badge.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
} catch (e) { /* still: Update-Check ist optional */ }
|
||||
}
|
||||
|
||||
(async function init() {
|
||||
renderConnection();
|
||||
renderTargets();
|
||||
@@ -4807,6 +4825,7 @@ document.getElementById('btnRestoreColors')?.addEventListener('click', () => {
|
||||
}
|
||||
await refreshStatus();
|
||||
refreshTenantSwitcher();
|
||||
checkForUpdate(); // nicht-blockierend
|
||||
if (State.connected) autoLoadAfterConnect();
|
||||
// First-Run: wenn kritische Felder leer sind, Settings-Modal automatisch oeffnen.
|
||||
if (setupNeeded) {
|
||||
@@ -7012,9 +7031,30 @@ async function offExecute() {
|
||||
try {
|
||||
const res = await api('/api/offboard/execute', { method: 'POST', body: { devices: payload, services }, timeoutMs: 180000 });
|
||||
const results = res.results || [];
|
||||
// Ergebnis pro Gerät gruppieren
|
||||
const byDev = new Map();
|
||||
for (const r of results) {
|
||||
if (!byDev.has(r.deviceName)) byDev.set(r.deviceName, []);
|
||||
byDev.get(r.deviceName).push(r);
|
||||
}
|
||||
const devHtml = [...byDev.entries()].map(([name, rows]) => {
|
||||
const errs = rows.filter(r => !r.success).length;
|
||||
const lines = rows.map(r =>
|
||||
`<div class="off-result ${r.success ? 'ok' : 'fail'}">${r.success ? '✓' : '✗'} ${escapeHtml(r.service)} ${escapeHtml(r.action)}${r.error ? ': ' + escapeHtml(r.error) : ''}</div>`
|
||||
).join('');
|
||||
return `<div class="off-result-dev">
|
||||
<div class="off-result-devname">${escapeHtml(name || '—')}${errs ? ` <span class="off-result-badge">${errs} Fehler</span>` : ''}</div>
|
||||
${lines}
|
||||
</div>`;
|
||||
}).join('');
|
||||
const reportLink = res.reportUrl
|
||||
? `<a href="${escapeHtml(res.reportUrl)}" target="_blank" rel="noopener" class="btn btn-secondary btn-sm" style="margin-top:12px;display:inline-flex;align-items:center;gap:6px;">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><line x1="10" y1="9" x2="8" y2="9"/></svg>
|
||||
HTML-Report öffnen
|
||||
</a>`
|
||||
: '';
|
||||
document.getElementById('offResults').innerHTML =
|
||||
`<div class="lbl">Ergebnis (${res.successCount}/${res.total})</div>` +
|
||||
results.map(r => `<div class="off-result ${r.success ? 'ok' : 'fail'}">${r.success ? '✓' : '✗'} ${escapeHtml(r.deviceName)} — ${escapeHtml(r.service)} ${escapeHtml(r.action)}${r.error ? ': ' + escapeHtml(r.error) : ''}</div>`).join('');
|
||||
`<div class="lbl">Ergebnis (${res.successCount}/${res.total})</div>${devHtml}${reportLink}`;
|
||||
toast(`${res.successCount}/${res.total} Aktionen erfolgreich.`, res.successCount === res.total ? 'ok' : 'warn', 'Offboarding');
|
||||
// Erfolgreich vollständig offgeboardete Geräte aus der Liste nehmen.
|
||||
const doneNames = new Set(results.filter(r => r.success).map(r => r.deviceName));
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
</nav>
|
||||
|
||||
<div class="header-actions">
|
||||
<a id="updateBadge" class="update-badge hidden" href="#" target="_blank" rel="noopener" title="Update verfügbar — Release-Seite öffnen">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-3-6.7"/><polyline points="21 3 21 9 15 9"/></svg>
|
||||
<span id="updateBadgeText">Update</span>
|
||||
</a>
|
||||
<button id="btnSettings" class="icon-btn icon-btn-header" title="Einstellungen" aria-label="Einstellungen öffnen">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .34 1.86l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.7 1.7 0 0 0-1.86-.34 1.7 1.7 0 0 0-1.04 1.57V21a2 2 0 0 1-4 0v-.09a1.7 1.7 0 0 0-1.1-1.57 1.7 1.7 0 0 0-1.86.34l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.7 1.7 0 0 0 .34-1.86 1.7 1.7 0 0 0-1.57-1.04H3a2 2 0 0 1 0-4h.09A1.7 1.7 0 0 0 4.66 9a1.7 1.7 0 0 0-.34-1.86l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.7 1.7 0 0 0 1.86.34H9a1.7 1.7 0 0 0 1.04-1.57V3a2 2 0 0 1 4 0v.09c0 .68.42 1.29 1.04 1.57.62.27 1.36.13 1.86-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.7 1.7 0 0 0-.34 1.86V9a1.7 1.7 0 0 0 1.57 1.04H21a2 2 0 0 1 0 4h-.09a1.7 1.7 0 0 0-1.51 1z"/></svg>
|
||||
</button>
|
||||
@@ -1070,6 +1074,9 @@
|
||||
.off-result { font-size:13px; padding:3px 0; overflow-wrap:anywhere; }
|
||||
.off-result.ok { color:var(--success); }
|
||||
.off-result.fail { color:var(--error); }
|
||||
.off-result-dev { border:1px solid var(--hairline-soft, var(--hairline)); border-radius:8px; padding:8px 12px; margin-bottom:8px; }
|
||||
.off-result-devname { font-weight:600; font-size:13px; margin-bottom:4px; display:flex; align-items:center; gap:8px; }
|
||||
.off-result-badge { font-size:10.5px; font-weight:700; padding:1px 8px; border-radius:999px; background:rgba(245,80,80,0.18); color:var(--error); }
|
||||
#offResults .lbl { display:block; font-size:12px; color:var(--body); margin:10px 0 4px; }
|
||||
</style>
|
||||
|
||||
|
||||
@@ -335,6 +335,24 @@ h1, h2, h3 {
|
||||
|
||||
.header-actions { display: flex; align-items: center; gap: 12px; }
|
||||
|
||||
/* Update-Hinweis-Badge (neue Version verfuegbar) */
|
||||
.update-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 11px;
|
||||
border-radius: 999px;
|
||||
background: var(--accent-soft);
|
||||
color: var(--brand-accent);
|
||||
border: 1px solid var(--brand-accent);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
transition: filter var(--tx, 0.15s ease-out);
|
||||
}
|
||||
.update-badge:hover { filter: brightness(0.96); }
|
||||
|
||||
/* Theme-Toggle — Slide-Switch */
|
||||
.theme-toggle {
|
||||
appearance: none;
|
||||
|
||||
Reference in New Issue
Block a user