diff --git a/.gitignore b/.gitignore index 8dfaeda..500279c 100644 --- a/.gitignore +++ b/.gitignore @@ -34,8 +34,8 @@ updater/rmm-updater frontend/node_modules/ frontend/dist/ -# config.js hat echte IPs und API-Keys — nie einchecken -frontend/src/config.js +# .env hat echte IPs und API-Keys — nie einchecken +frontend/.env !frontend/src/config.example.js # ======================== diff --git a/agent-windows/collector/system.go b/agent-windows/collector/system.go index 170897f..42d66ea 100644 --- a/agent-windows/collector/system.go +++ b/agent-windows/collector/system.go @@ -31,6 +31,7 @@ type WindowsData struct { Disks []DiskInfo `json:"disks"` Services []SvcInfo `json:"services,omitempty"` InstalledSoftware []SoftwareInfo `json:"installed_software,omitempty"` + WAU *WAUInfo `json:"wau,omitempty"` Domain string `json:"domain,omitempty"` LastUpdate time.Time `json:"last_update"` } @@ -123,6 +124,10 @@ func Collect() (*SystemData, error) { // Installierte Software (Registry) wd.InstalledSoftware = getInstalledSoftware() + // WAU-Status (Installation + Konfiguration + Pending Updates) + wauInfo := getWAUInfo(wd.InstalledSoftware) + wd.WAU = &wauInfo + return &SystemData{Windows: wd}, nil } diff --git a/agent-windows/collector/wau.go b/agent-windows/collector/wau.go new file mode 100644 index 0000000..9fa0c3e --- /dev/null +++ b/agent-windows/collector/wau.go @@ -0,0 +1,100 @@ +//go:build windows + +package collector + +import ( + "os/exec" + "strings" + + "golang.org/x/sys/windows/registry" +) + +// WAUInfo enthält den Status von Winget-AutoUpdate auf diesem Gerät +type WAUInfo struct { + Installed bool `json:"installed"` + Version string `json:"version,omitempty"` + IncludeURL string `json:"include_url,omitempty"` + ExcludeURL string `json:"exclude_url,omitempty"` + PendingCount int `json:"pending_updates"` +} + +// getWAUInfo liest WAU-Installations- und Konfigurationsstatus +func getWAUInfo(software []SoftwareInfo) WAUInfo { + info := WAUInfo{} + + // WAU in installierter Software suchen + for _, s := range software { + lower := strings.ToLower(s.Name) + if strings.Contains(lower, "winget-autoupdate") || lower == "wau" { + info.Installed = true + info.Version = s.Version + break + } + } + + // WAU-Konfiguration aus Registry lesen + // WAU speichert seine Einstellungen unter HKLM\SOFTWARE\Winget-AutoUpdate + regPaths := []string{ + `SOFTWARE\Winget-AutoUpdate`, + `SOFTWARE\Policies\Microsoft\Windows\Winget-AutoUpdate`, + } + for _, path := range regPaths { + k, err := registry.OpenKey(registry.LOCAL_MACHINE, path, registry.QUERY_VALUE) + if err != nil { + continue + } + if url, _, err := k.GetStringValue("WAU_IncludeListURL"); err == nil && url != "" { + info.IncludeURL = url + } + if url, _, err := k.GetStringValue("WAU_ExcludeListURL"); err == nil && url != "" { + info.ExcludeURL = url + } + k.Close() + if info.IncludeURL != "" || info.ExcludeURL != "" { + info.Installed = true + break + } + } + + // Pending updates via winget upgrade --list zählen + wp := findWingetExe() + if wp != "" { + out, err := runPS(`& "` + wp + `" upgrade --list --accept-source-agreements 2>&1`) + if err == nil { + info.PendingCount = countWingetUpgradeLines(out) + } + } + + return info +} + +// findWingetExe sucht winget.exe im System +func findWingetExe() string { + if path, err := exec.LookPath("winget"); err == nil { + return path + } + script := `Get-ChildItem "C:\Program Files\WindowsApps" -Filter winget.exe -Recurse -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName` + out, err := runPS(script) + if err == nil && strings.TrimSpace(out) != "" { + return strings.TrimSpace(out) + } + return "" +} + +// countWingetUpgradeLines zählt Pakete mit Updates aus "winget upgrade --list" Output +func countWingetUpgradeLines(output string) int { + lines := strings.Split(output, "\n") + count := 0 + pastSeparator := false + for _, line := range lines { + trimmed := strings.TrimSpace(line) + if strings.Contains(trimmed, "---") { + pastSeparator = true + continue + } + if pastSeparator && trimmed != "" { + count++ + } + } + return count +} diff --git a/agent-windows/main.go b/agent-windows/main.go index 4eca756..9e161a6 100644 --- a/agent-windows/main.go +++ b/agent-windows/main.go @@ -27,7 +27,7 @@ import ( "github.com/cynfo/rmm-agent-windows/ws" ) -var Version = "1.1.0" +var Version = "1.2.0" const ( ServiceName = "RMMAgent" diff --git a/backend/api/wau.go b/backend/api/wau.go index 8c53039..e4b8dbd 100644 --- a/backend/api/wau.go +++ b/backend/api/wau.go @@ -10,10 +10,14 @@ import ( // setupWAURoutes registriert alle WAU-Endpunkte func (h *Handler) setupWAURoutes(mux *http.ServeMux) { // Authentifizierte Endpunkte (API-Key oder JWT) - mux.HandleFunc("GET /api/v1/customers/{id}/wau/rules", h.listWAURules) - mux.HandleFunc("POST /api/v1/customers/{id}/wau/rules", h.addWAURule) + mux.HandleFunc("GET /api/v1/customers/{id}/wau/rules", h.listWAURules) + mux.HandleFunc("POST /api/v1/customers/{id}/wau/rules", h.addWAURule) mux.HandleFunc("DELETE /api/v1/customers/{id}/wau/rules/{rid}", h.deleteWAURule) - mux.HandleFunc("GET /api/v1/customers/{id}/wau/inventory", h.getWAUInventory) + mux.HandleFunc("GET /api/v1/customers/{id}/wau/inventory", h.getWAUInventory) + mux.HandleFunc("GET /api/v1/customers/{id}/wau/compliance", h.getWAUCompliance) + + // Compliance-Summary für alle Kunden + mux.HandleFunc("GET /api/v1/wau/compliance/summary", h.getWAUComplianceSummary) // Plaintext-Endpunkte für WAU (kein Auth nötig — enthält nur App-IDs) mux.HandleFunc("GET /api/v1/customers/{id}/wau/included", h.wauIncludedList) @@ -149,3 +153,51 @@ func (h *Handler) serveWAUList(w http.ResponseWriter, r *http.Request, ruleType w.Write([]byte("\r\n")) } } + +// wauExpectedURLs leitet die erwarteten WAU-URLs aus dem Request-Host ab +func wauExpectedURLs(r *http.Request, customerID int) (includeURL, excludeURL string) { + scheme := "https" + if r.TLS == nil && (r.Header.Get("X-Forwarded-Proto") == "" || r.Header.Get("X-Forwarded-Proto") == "http") { + scheme = "http" + } + if fwd := r.Header.Get("X-Forwarded-Proto"); fwd != "" { + scheme = fwd + } + host := r.Host + base := scheme + "://" + host + includeURL = base + "/api/v1/customers/" + strconv.Itoa(customerID) + "/wau/included" + excludeURL = base + "/api/v1/customers/" + strconv.Itoa(customerID) + "/wau/excluded" + return +} + +// GET /api/v1/customers/{id}/wau/compliance +func (h *Handler) getWAUCompliance(w http.ResponseWriter, r *http.Request) { + customerID, err := strconv.Atoi(r.PathValue("id")) + if err != nil { + writeError(w, http.StatusBadRequest, "Ungültige Kunden-ID") + return + } + includeURL, excludeURL := wauExpectedURLs(r, customerID) + agents, err := h.db.GetWAUCompliance(customerID, includeURL, excludeURL) + if err != nil { + writeError(w, http.StatusInternalServerError, "Fehler beim Laden der Compliance-Daten") + return + } + writeJSON(w, http.StatusOK, map[string]interface{}{ + "agents": agents, + "include_url": includeURL, + "exclude_url": excludeURL, + }) +} + +// GET /api/v1/wau/compliance/summary — Zusammenfassung für alle Kunden +func (h *Handler) getWAUComplianceSummary(w http.ResponseWriter, r *http.Request) { + // Ohne customer_id können wir keine URLs ableiten — wir geben Summary ohne URL-Check zurück + // oder nutzen customer_id=0 als Wildcard (kein URL-Check) + summaries, err := h.db.GetAllCustomersWAUSummary("", "") + if err != nil { + writeError(w, http.StatusInternalServerError, "Fehler beim Laden der Compliance-Daten") + return + } + writeJSON(w, http.StatusOK, summaries) +} diff --git a/backend/db/wau.go b/backend/db/wau.go index b78880b..3ffefaf 100644 --- a/backend/db/wau.go +++ b/backend/db/wau.go @@ -3,6 +3,7 @@ package db import ( "database/sql" "encoding/json" + "strings" "github.com/cynfo/rmm-backend/models" ) @@ -133,3 +134,217 @@ func (d *Database) GetWAUSoftwareInventory(customerID int) ([]models.WAUSoftware } return entries, rows.Err() } + +// GetWAUCompliance berechnet den Compliance-Status aller Windows-Agents eines Kunden +// Prüft: WAU installiert, URLs korrekt, alle allow-Pakete installiert +func (d *Database) GetWAUCompliance(customerID int, expectedIncludeURL, expectedExcludeURL string) ([]models.WAUAgentCompliance, error) { + // Alle Windows-Agents des Kunden mit ihren system_data holen + rows, err := d.db.Query(` + SELECT a.id, a.hostname, sd.data_json + FROM agents a + JOIN system_data sd ON sd.agent_id = a.id + WHERE a.customer_id = $1 + AND sd.data_json ? 'windows' + ORDER BY lower(a.hostname) + `, customerID) + if err != nil { + return nil, err + } + defer rows.Close() + + // Allow-Pakete (= Required) für diesen Kunden laden + allowRules, err := d.GetWAUList(customerID, "allow") + if err != nil { + return nil, err + } + allowRulesFull, err := d.ListWAURules(customerID) + if err != nil { + return nil, err + } + // Map winget_id → display_name + displayNames := make(map[string]string) + for _, r := range allowRulesFull { + if r.RuleType == "allow" { + displayNames[r.WingetID] = r.DisplayName + } + } + + var result []models.WAUAgentCompliance + + for rows.Next() { + var agentID, hostname string + var dataJSON []byte + if err := rows.Scan(&agentID, &hostname, &dataJSON); err != nil { + continue + } + + // JSON parsen (nur was wir brauchen) + var data struct { + Windows struct { + InstalledSoftware []struct { + Name string `json:"name"` + Version string `json:"version"` + } `json:"installed_software"` + WAU *struct { + Installed bool `json:"installed"` + IncludeURL string `json:"include_url"` + ExcludeURL string `json:"exclude_url"` + PendingCount int `json:"pending_updates"` + } `json:"wau"` + } `json:"windows"` + } + if err := json.Unmarshal(dataJSON, &data); err != nil { + continue + } + + ac := models.WAUAgentCompliance{ + AgentID: agentID, + AgentName: hostname, + } + + // WAU-Status + if data.Windows.WAU != nil { + wau := data.Windows.WAU + ac.WAUInstalled = wau.Installed + ac.IncludeURL = wau.IncludeURL + ac.ExcludeURL = wau.ExcludeURL + ac.PendingCount = wau.PendingCount + + // URL-Check: beide URLs müssen gesetzt sein und passen + includeOK := expectedIncludeURL == "" || wau.IncludeURL == expectedIncludeURL + excludeOK := expectedExcludeURL == "" || wau.ExcludeURL == expectedExcludeURL + ac.WAUConfigOK = wau.Installed && includeOK && excludeOK + } + + // Installierte Software als lowercase-Set für schnelles Lookup + installedSet := make(map[string]bool) + for _, s := range data.Windows.InstalledSoftware { + installedSet[strings.ToLower(s.Name)] = true + } + + // Pro allow-Paket prüfen ob installiert + for _, wingetID := range allowRules { + displayName := displayNames[wingetID] + if displayName == "" { + displayName = wingetID + } + pkg := models.WAUPackageCompliance{ + WingetID: wingetID, + DisplayName: displayName, + } + // Fuzzy-Match: display_name (lowercase) muss in installierter Software enthalten sein + lowerDisplay := strings.ToLower(displayName) + for installedName := range installedSet { + if strings.Contains(installedName, lowerDisplay) || + strings.Contains(lowerDisplay, installedName) { + pkg.Installed = true + break + } + } + // Winget-ID-Teile als Fallback (z.B. "Mozilla.Firefox" → "firefox") + if !pkg.Installed { + parts := strings.Split(strings.ToLower(wingetID), ".") + if len(parts) > 1 { + appName := parts[len(parts)-1] + for installedName := range installedSet { + if strings.Contains(installedName, appName) { + pkg.Installed = true + break + } + } + } + } + ac.Packages = append(ac.Packages, pkg) + } + + // Gesamt-Compliance: WAU installiert + korrekt + alle Pakete da + keine Updates ausstehend + allInstalled := true + for _, p := range ac.Packages { + if !p.Installed { + allInstalled = false + break + } + } + ac.Compliant = ac.WAUInstalled && ac.WAUConfigOK && allInstalled && ac.PendingCount == 0 + + result = append(result, ac) + } + if result == nil { + result = []models.WAUAgentCompliance{} + } + return result, rows.Err() +} + +// GetWAUComplianceSummary gibt eine kompakte Zusammenfassung für die Kundenliste zurück +func (d *Database) GetWAUComplianceSummary(customerID int, expectedIncludeURL, expectedExcludeURL string) (*models.WAUComplianceSummary, error) { + agents, err := d.GetWAUCompliance(customerID, expectedIncludeURL, expectedExcludeURL) + if err != nil { + return nil, err + } + + summary := &models.WAUComplianceSummary{ + CustomerID: customerID, + TotalAgents: len(agents), + } + + for _, a := range agents { + if a.Compliant { + summary.CompliantAgents++ + } + if !a.WAUInstalled { + summary.WAUMissing++ + } else if !a.WAUConfigOK { + summary.WAUMisconfigured++ + } + if a.PendingCount > 0 { + summary.UpdatesPending++ + } + for _, p := range a.Packages { + if !p.Installed { + summary.PackagesMissing++ + break // pro Agent nur einmal zählen + } + } + } + + // Status ableiten + if summary.WAUMissing > 0 || summary.PackagesMissing > 0 { + summary.Status = "critical" + } else if summary.WAUMisconfigured > 0 || summary.UpdatesPending > 0 { + summary.Status = "warning" + } else if summary.TotalAgents == 0 { + summary.Status = "unknown" + } else { + summary.Status = "ok" + } + + return summary, nil +} + +// GetAllCustomersWAUSummary gibt Compliance-Summary für alle Kunden zurück +func (d *Database) GetAllCustomersWAUSummary(includeURL, excludeURL string) ([]models.WAUComplianceSummary, error) { + rows, err := d.db.Query(`SELECT id FROM customers ORDER BY name`) + if err != nil { + return nil, err + } + defer rows.Close() + + var summaries []models.WAUComplianceSummary + for rows.Next() { + var id int + if err := rows.Scan(&id); err != nil { + continue + } + s, err := d.GetWAUComplianceSummary(id, includeURL, excludeURL) + if err != nil { + continue + } + if s.TotalAgents > 0 { + summaries = append(summaries, *s) + } + } + if summaries == nil { + summaries = []models.WAUComplianceSummary{} + } + return summaries, rows.Err() +} diff --git a/backend/models/wau.go b/backend/models/wau.go index 5055bec..d700dd1 100644 --- a/backend/models/wau.go +++ b/backend/models/wau.go @@ -17,3 +17,36 @@ type WAUSoftwareEntry struct { DeviceCount int `json:"device_count"` Versions []string `json:"versions"` } + +// WAUPackageCompliance beschreibt den Compliance-Status eines einzelnen Pakets auf einem Gerät +type WAUPackageCompliance struct { + WingetID string `json:"winget_id"` + DisplayName string `json:"display_name"` + Installed bool `json:"installed"` + HasUpdates bool `json:"has_updates"` // true wenn PendingCount > 0 und dieses Paket betroffen +} + +// WAUAgentCompliance beschreibt den vollständigen WAU-Compliance-Status eines Agents +type WAUAgentCompliance struct { + AgentID string `json:"agent_id"` + AgentName string `json:"agent_name"` + WAUInstalled bool `json:"wau_installed"` + WAUConfigOK bool `json:"wau_config_ok"` // Include/Exclude-URLs korrekt gesetzt + IncludeURL string `json:"include_url"` // tatsächlich konfigurierte URL + ExcludeURL string `json:"exclude_url"` // tatsächlich konfigurierte URL + PendingCount int `json:"pending_updates"` + Packages []WAUPackageCompliance `json:"packages"` + Compliant bool `json:"compliant"` // true wenn alles OK +} + +// WAUComplianceSummary fasst den Compliance-Status eines Kunden zusammen (für die Kundenliste) +type WAUComplianceSummary struct { + CustomerID int `json:"customer_id"` + TotalAgents int `json:"total_agents"` + CompliantAgents int `json:"compliant_agents"` + WAUMissing int `json:"wau_missing"` // Agents ohne WAU + WAUMisconfigured int `json:"wau_misconfigured"` // WAU installiert, aber falsche URLs + PackagesMissing int `json:"packages_missing"` // Agents mit fehlenden Pflichtpaketen + UpdatesPending int `json:"updates_pending"` // Agents mit ausstehenden Updates + Status string `json:"status"` // "ok" | "warning" | "critical" +} diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..7522ead --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,6 @@ +# RMM Frontend Konfiguration +# Datei kopieren: cp .env.example .env (wird nicht eingecheckt) + +VITE_BACKEND_HOST=your-backend.example.com +VITE_BACKEND_PORT=8443 +VITE_API_KEY=your-api-key-here diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js index 4f11df1..2d5fd28 100644 --- a/frontend/src/api/client.js +++ b/frontend/src/api/client.js @@ -275,6 +275,14 @@ class ApiClient { return this.get(`/api/v1/customers/${customerId}/wau/inventory`) } + getWAUCompliance(customerId) { + return this.get(`/api/v1/customers/${customerId}/wau/compliance`) + } + + getWAUComplianceSummary() { + return this.get('/api/v1/wau/compliance/summary') + } + createAPIKey(name, permissions = 'read', customerId = null) { return this.post('/api/v1/apikeys', { name, permissions, customer_id: customerId }) } diff --git a/frontend/src/config.js b/frontend/src/config.js new file mode 100644 index 0000000..be21c1d --- /dev/null +++ b/frontend/src/config.js @@ -0,0 +1,8 @@ +// Frontend-Konfiguration +// Priorität: 1. window.__RMM_CONFIG__ (Docker Runtime), 2. Vite ENV (lokaler Build) +const rt = window.__RMM_CONFIG__ || {} + +export const BACKEND_HOST = rt.backendHost || import.meta.env.VITE_BACKEND_HOST || 'localhost' +export const BACKEND_PORT = rt.backendPort || import.meta.env.VITE_BACKEND_PORT || '8443' +export const BACKEND_URL = `https://${BACKEND_HOST}:${BACKEND_PORT}` +export const API_KEY = rt.apiKey || import.meta.env.VITE_API_KEY || '' diff --git a/frontend/src/pages/Customers.jsx b/frontend/src/pages/Customers.jsx index 633e1f8..2d48b8c 100644 --- a/frontend/src/pages/Customers.jsx +++ b/frontend/src/pages/Customers.jsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react' import api from '../api/client' import { copyToClipboard } from '../utils/clipboard' -import { Plus, Trash2, Edit2, X, Check, ChevronDown, ChevronRight, Copy, Key, Monitor, AlertTriangle, ShieldCheck, ShieldX, Package, Search, RefreshCw } from 'lucide-react' +import { Plus, Trash2, Edit2, X, Check, ChevronDown, ChevronRight, Copy, Key, Monitor, AlertTriangle, ShieldCheck, ShieldX, ShieldAlert, Package, Search, RefreshCw, CircleCheck, CircleX, CircleAlert, Info } from 'lucide-react' const PERM_LABELS = { agent: { label: 'Agent', color: 'text-blue-400 bg-blue-900/30 border-blue-700/50' }, @@ -10,6 +10,45 @@ const PERM_LABELS = { admin: { label: 'Admin', color: 'text-orange-400 bg-orange-900/30 border-orange-700/50' }, } +// ── WAU Compliance Badge ────────────────────────────────────────────────────── +function WAUComplianceBadge({ summary }) { + if (!summary) return null + if (summary.total_agents === 0) return null + + const { status, compliant_agents, total_agents, wau_missing, wau_misconfigured, packages_missing, updates_pending } = summary + + const configs = { + ok: { icon: CircleCheck, cls: 'text-green-400', bg: 'bg-green-900/20 border-green-700/40', label: `WAU OK (${compliant_agents}/${total_agents})` }, + warning: { icon: CircleAlert, cls: 'text-yellow-400', bg: 'bg-yellow-900/20 border-yellow-700/40', label: buildWarningLabel({ wau_misconfigured, updates_pending, compliant_agents, total_agents }) }, + critical: { icon: CircleX, cls: 'text-red-400', bg: 'bg-red-900/20 border-red-700/40', label: buildCriticalLabel({ wau_missing, packages_missing, compliant_agents, total_agents }) }, + unknown: { icon: Info, cls: 'text-gray-500', bg: 'bg-gray-800/40 border-gray-700/40', label: 'Keine Windows-Agents' }, + } + + const cfg = configs[status] || configs.unknown + const Icon = cfg.icon + + return ( + + + {cfg.label} + + ) +} + +function buildWarningLabel({ wau_misconfigured, updates_pending, compliant_agents, total_agents }) { + const parts = [] + if (wau_misconfigured > 0) parts.push(`${wau_misconfigured} fehlkonfiguriert`) + if (updates_pending > 0) parts.push(`${updates_pending} Updates`) + return parts.length ? parts.join(', ') : `${compliant_agents}/${total_agents} OK` +} + +function buildCriticalLabel({ wau_missing, packages_missing, compliant_agents, total_agents }) { + const parts = [] + if (wau_missing > 0) parts.push(`${wau_missing} ohne WAU`) + if (packages_missing > 0) parts.push(`${packages_missing} Pakete fehlen`) + return parts.length ? parts.join(', ') : `${compliant_agents}/${total_agents} OK` +} + export default function Customers() { const [customers, setCustomers] = useState([]) const [loading, setLoading] = useState(true) @@ -17,12 +56,21 @@ export default function Customers() { const [editId, setEditId] = useState(null) const [form, setForm] = useState({ number: '', name: '' }) const [expandedId, setExpandedId] = useState(null) + const [complianceSummaries, setComplianceSummaries] = useState({}) // customerID → summary const reload = () => { api.getCustomers().then((c) => setCustomers(c || [])).finally(() => setLoading(false)) } - useEffect(() => { reload() }, []) + const loadCompliance = () => { + api.getWAUComplianceSummary().then(data => { + const map = {} + ;(data || []).forEach(s => { map[s.customer_id] = s }) + setComplianceSummaries(map) + }).catch(() => {}) + } + + useEffect(() => { reload(); loadCompliance() }, []) const handleAdd = async () => { if (!form.number || !form.name) return @@ -68,6 +116,7 @@ export default function Customers() { Nummer Name + WAU Erstellt @@ -156,6 +205,9 @@ export default function Customers() { {c.number} {c.name} + + + {new Date(c.created_at).toLocaleDateString('de-DE')} @@ -179,7 +231,7 @@ export default function Customers() { )} {expandedId === c.id && ( - + @@ -339,6 +391,165 @@ function APIKeysTab({ customerId }) { ) } +// ── WAU Compliance Panel ────────────────────────────────────────────────────── +function WAUCompliancePanel({ compliance, loading, onRefresh }) { + const [expanded, setExpanded] = useState(false) + + if (loading) return ( +
+ Compliance wird geprueft... +
+ ) + + const agents = compliance?.agents || [] + if (agents.length === 0) return null + + const compliantCount = agents.filter(a => a.compliant).length + const allOk = compliantCount === agents.length + + const overallStatus = agents.some(a => !a.wau_installed || a.packages?.some(p => !p.installed)) + ? 'critical' + : agents.some(a => !a.wau_config_ok || a.pending_updates > 0) + ? 'warning' + : 'ok' + + const statusColors = { + ok: 'border-green-700/40 bg-green-900/10', + warning: 'border-yellow-700/40 bg-yellow-900/10', + critical: 'border-red-700/40 bg-red-900/10', + } + const headerColors = { + ok: 'text-green-400', + warning: 'text-yellow-400', + critical: 'text-red-400', + } + const StatusIcon = overallStatus === 'ok' ? CircleCheck : overallStatus === 'warning' ? CircleAlert : CircleX + + return ( +
+
+
+ + + WAU Compliance: {compliantCount}/{agents.length} Agents konform + +
+
+ + +
+
+ + {expanded && ( +
+ {agents.map(agent => ( + + ))} +
+ )} +
+ ) +} + +function AgentComplianceRow({ agent, expectedIncludeUrl }) { + const [open, setOpen] = useState(false) + const missingPkgs = (agent.packages || []).filter(p => !p.installed) + const hasIssues = !agent.wau_installed || !agent.wau_config_ok || missingPkgs.length > 0 || agent.pending_updates > 0 + + const rowColor = agent.compliant ? 'text-green-400' : hasIssues ? 'text-red-400' : 'text-yellow-400' + const RowIcon = agent.compliant ? CircleCheck : CircleX + + return ( +
+ + + {open && ( +
+ {/* WAU Status */} +
+
+ {agent.wau_installed + ? + : + } + WAU {agent.wau_installed ? `installiert` : 'nicht installiert'} +
+ {agent.wau_installed && ( +
+ {agent.wau_config_ok + ? + : + } + + Konfiguration {agent.wau_config_ok ? 'korrekt' : 'falsch'} + {!agent.wau_config_ok && agent.include_url && ( + ({agent.include_url || 'keine URL'}) + )} + +
+ )} + {agent.pending_updates > 0 && ( +
+ + {agent.pending_updates} ausstehende Updates +
+ )} +
+ + {/* Paket-Status */} + {(agent.packages || []).length > 0 && ( +
+
Pflichtpakete
+ {(agent.packages || []).map(pkg => ( +
+ {pkg.installed + ? + : + } + {pkg.display_name} + {pkg.winget_id} +
+ ))} +
+ )} +
+ )} +
+ ) +} + // ── WAU-Tab ─────────────────────────────────────────────────────────────────── // Heuristik: Anzeigename → winget-ID-Vorschlag @@ -364,6 +575,10 @@ function WauTab({ customerId, customerNumber }) { // Manuelles Formular (Fallback) const [showManual, setShowManual] = useState(false) const [manualForm, setManualForm] = useState({ wingetId: '', displayName: '', ruleType: 'allow' }) + // Compliance + const [compliance, setCompliance] = useState(null) + const [loadingComp, setLoadingComp] = useState(false) + const [showCompliance, setShowCompliance] = useState(false) const reload = () => api.getWAURules(customerId).then(setRules) @@ -372,9 +587,15 @@ function WauTab({ customerId, customerNumber }) { api.getWAUInventory(customerId).then(setInventory).finally(() => setLoadingInv(false)) } + const loadCompliance = () => { + setLoadingComp(true) + api.getWAUCompliance(customerId).then(setCompliance).finally(() => setLoadingComp(false)) + } + useEffect(() => { reload() loadInventory() + loadCompliance() api.getSettings().then(s => { const url = s?.data?.backend_url_external || s?.backend_url_external if (url) setExternalUrl(url.replace(/\/$/, '')) @@ -441,6 +662,9 @@ function WauTab({ customerId, customerNumber }) { return (
+ {/* WAU Compliance Status */} + + {/* Installationsbefehl */}
diff --git a/frontend/vite.config.js b/frontend/vite.config.js index d565fcf..c56ac37 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -1,16 +1,23 @@ -import { defineConfig } from 'vite' +import { defineConfig, loadEnv } from 'vite' import react from '@vitejs/plugin-react' import tailwindcss from '@tailwindcss/vite' -export default defineConfig({ - plugins: [react(), tailwindcss()], - server: { - proxy: { - '/api': { - target: 'https://your-backend:8443', // TODO: set your backend URL - changeOrigin: true, - secure: false, +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), '') + const backendHost = env.VITE_BACKEND_HOST || 'localhost' + const backendPort = env.VITE_BACKEND_PORT || '8443' + const backendUrl = `https://${backendHost}:${backendPort}` + + return { + plugins: [react(), tailwindcss()], + server: { + proxy: { + '/api': { + target: backendUrl, + changeOrigin: true, + secure: false, + }, }, }, - }, + } })