WAU Compliance: agent collector, backend endpoints, frontend badge + panel
- Agent: WAU registry config check (include/exclude URLs), pending updates count - Backend: /wau/compliance per customer, /wau/compliance/summary all customers - Frontend: compliance badge in customer list, compliance panel in WAU tab - config.js: no longer gitignored (secrets-free, reads from env/window.__RMM_CONFIG__) - vite.config.js: reads backend URL from .env instead of hardcoded value - .env.example added for clean setup
This commit is contained in:
parent
bf51ed8569
commit
54ddc0b902
4
.gitignore
vendored
4
.gitignore
vendored
@ -34,8 +34,8 @@ updater/rmm-updater
|
|||||||
frontend/node_modules/
|
frontend/node_modules/
|
||||||
frontend/dist/
|
frontend/dist/
|
||||||
|
|
||||||
# config.js hat echte IPs und API-Keys — nie einchecken
|
# .env hat echte IPs und API-Keys — nie einchecken
|
||||||
frontend/src/config.js
|
frontend/.env
|
||||||
!frontend/src/config.example.js
|
!frontend/src/config.example.js
|
||||||
|
|
||||||
# ========================
|
# ========================
|
||||||
|
|||||||
@ -31,6 +31,7 @@ type WindowsData struct {
|
|||||||
Disks []DiskInfo `json:"disks"`
|
Disks []DiskInfo `json:"disks"`
|
||||||
Services []SvcInfo `json:"services,omitempty"`
|
Services []SvcInfo `json:"services,omitempty"`
|
||||||
InstalledSoftware []SoftwareInfo `json:"installed_software,omitempty"`
|
InstalledSoftware []SoftwareInfo `json:"installed_software,omitempty"`
|
||||||
|
WAU *WAUInfo `json:"wau,omitempty"`
|
||||||
Domain string `json:"domain,omitempty"`
|
Domain string `json:"domain,omitempty"`
|
||||||
LastUpdate time.Time `json:"last_update"`
|
LastUpdate time.Time `json:"last_update"`
|
||||||
}
|
}
|
||||||
@ -123,6 +124,10 @@ func Collect() (*SystemData, error) {
|
|||||||
// Installierte Software (Registry)
|
// Installierte Software (Registry)
|
||||||
wd.InstalledSoftware = getInstalledSoftware()
|
wd.InstalledSoftware = getInstalledSoftware()
|
||||||
|
|
||||||
|
// WAU-Status (Installation + Konfiguration + Pending Updates)
|
||||||
|
wauInfo := getWAUInfo(wd.InstalledSoftware)
|
||||||
|
wd.WAU = &wauInfo
|
||||||
|
|
||||||
return &SystemData{Windows: wd}, nil
|
return &SystemData{Windows: wd}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
100
agent-windows/collector/wau.go
Normal file
100
agent-windows/collector/wau.go
Normal file
@ -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
|
||||||
|
}
|
||||||
@ -27,7 +27,7 @@ import (
|
|||||||
"github.com/cynfo/rmm-agent-windows/ws"
|
"github.com/cynfo/rmm-agent-windows/ws"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version = "1.1.0"
|
var Version = "1.2.0"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ServiceName = "RMMAgent"
|
ServiceName = "RMMAgent"
|
||||||
|
|||||||
@ -14,6 +14,10 @@ func (h *Handler) setupWAURoutes(mux *http.ServeMux) {
|
|||||||
mux.HandleFunc("POST /api/v1/customers/{id}/wau/rules", h.addWAURule)
|
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("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)
|
// 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)
|
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"))
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package db
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/cynfo/rmm-backend/models"
|
"github.com/cynfo/rmm-backend/models"
|
||||||
)
|
)
|
||||||
@ -133,3 +134,217 @@ func (d *Database) GetWAUSoftwareInventory(customerID int) ([]models.WAUSoftware
|
|||||||
}
|
}
|
||||||
return entries, rows.Err()
|
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()
|
||||||
|
}
|
||||||
|
|||||||
@ -17,3 +17,36 @@ type WAUSoftwareEntry struct {
|
|||||||
DeviceCount int `json:"device_count"`
|
DeviceCount int `json:"device_count"`
|
||||||
Versions []string `json:"versions"`
|
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"
|
||||||
|
}
|
||||||
|
|||||||
6
frontend/.env.example
Normal file
6
frontend/.env.example
Normal file
@ -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
|
||||||
@ -275,6 +275,14 @@ class ApiClient {
|
|||||||
return this.get(`/api/v1/customers/${customerId}/wau/inventory`)
|
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) {
|
createAPIKey(name, permissions = 'read', customerId = null) {
|
||||||
return this.post('/api/v1/apikeys', { name, permissions, customer_id: customerId })
|
return this.post('/api/v1/apikeys', { name, permissions, customer_id: customerId })
|
||||||
}
|
}
|
||||||
|
|||||||
8
frontend/src/config.js
Normal file
8
frontend/src/config.js
Normal file
@ -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 || ''
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import api from '../api/client'
|
import api from '../api/client'
|
||||||
import { copyToClipboard } from '../utils/clipboard'
|
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 = {
|
const PERM_LABELS = {
|
||||||
agent: { label: 'Agent', color: 'text-blue-400 bg-blue-900/30 border-blue-700/50' },
|
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' },
|
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 (
|
||||||
|
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded border text-[10px] font-medium ${cfg.cls} ${cfg.bg}`}>
|
||||||
|
<Icon className="w-3 h-3" />
|
||||||
|
{cfg.label}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
export default function Customers() {
|
||||||
const [customers, setCustomers] = useState([])
|
const [customers, setCustomers] = useState([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
@ -17,12 +56,21 @@ export default function Customers() {
|
|||||||
const [editId, setEditId] = useState(null)
|
const [editId, setEditId] = useState(null)
|
||||||
const [form, setForm] = useState({ number: '', name: '' })
|
const [form, setForm] = useState({ number: '', name: '' })
|
||||||
const [expandedId, setExpandedId] = useState(null)
|
const [expandedId, setExpandedId] = useState(null)
|
||||||
|
const [complianceSummaries, setComplianceSummaries] = useState({}) // customerID → summary
|
||||||
|
|
||||||
const reload = () => {
|
const reload = () => {
|
||||||
api.getCustomers().then((c) => setCustomers(c || [])).finally(() => setLoading(false))
|
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 () => {
|
const handleAdd = async () => {
|
||||||
if (!form.number || !form.name) return
|
if (!form.number || !form.name) return
|
||||||
@ -68,6 +116,7 @@ export default function Customers() {
|
|||||||
<th className="px-4 py-2 font-medium w-6"></th>
|
<th className="px-4 py-2 font-medium w-6"></th>
|
||||||
<th className="px-4 py-2 font-medium">Nummer</th>
|
<th className="px-4 py-2 font-medium">Nummer</th>
|
||||||
<th className="px-4 py-2 font-medium">Name</th>
|
<th className="px-4 py-2 font-medium">Name</th>
|
||||||
|
<th className="px-4 py-2 font-medium hidden lg:table-cell">WAU</th>
|
||||||
<th className="px-4 py-2 font-medium hidden md:table-cell">Erstellt</th>
|
<th className="px-4 py-2 font-medium hidden md:table-cell">Erstellt</th>
|
||||||
<th className="px-4 py-2 font-medium w-24"></th>
|
<th className="px-4 py-2 font-medium w-24"></th>
|
||||||
</tr>
|
</tr>
|
||||||
@ -156,6 +205,9 @@ export default function Customers() {
|
|||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2.5 text-orange-400 font-mono">{c.number}</td>
|
<td className="px-4 py-2.5 text-orange-400 font-mono">{c.number}</td>
|
||||||
<td className="px-4 py-2.5 text-white">{c.name}</td>
|
<td className="px-4 py-2.5 text-white">{c.name}</td>
|
||||||
|
<td className="px-4 py-2.5 hidden lg:table-cell">
|
||||||
|
<WAUComplianceBadge summary={complianceSummaries[c.id]} />
|
||||||
|
</td>
|
||||||
<td className="px-4 py-2.5 text-gray-500 text-xs hidden md:table-cell">
|
<td className="px-4 py-2.5 text-gray-500 text-xs hidden md:table-cell">
|
||||||
{new Date(c.created_at).toLocaleDateString('de-DE')}
|
{new Date(c.created_at).toLocaleDateString('de-DE')}
|
||||||
</td>
|
</td>
|
||||||
@ -179,7 +231,7 @@ export default function Customers() {
|
|||||||
)}
|
)}
|
||||||
{expandedId === c.id && (
|
{expandedId === c.id && (
|
||||||
<tr key={`expand-${c.id}`}>
|
<tr key={`expand-${c.id}`}>
|
||||||
<td colSpan={5} className="px-0 py-0">
|
<td colSpan={6} className="px-0 py-0">
|
||||||
<CustomerDetailPanel customerId={c.id} customerNumber={c.number} />
|
<CustomerDetailPanel customerId={c.id} customerNumber={c.number} />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -339,6 +391,165 @@ function APIKeysTab({ customerId }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── WAU Compliance Panel ──────────────────────────────────────────────────────
|
||||||
|
function WAUCompliancePanel({ compliance, loading, onRefresh }) {
|
||||||
|
const [expanded, setExpanded] = useState(false)
|
||||||
|
|
||||||
|
if (loading) return (
|
||||||
|
<div className="bg-gray-900 rounded-lg border border-gray-700 p-3 text-xs text-gray-500 flex items-center gap-2">
|
||||||
|
<RefreshCw className="w-3 h-3 animate-spin" /> Compliance wird geprueft...
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className={`rounded-lg border p-3 ${statusColors[overallStatus]}`}>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<StatusIcon className={`w-4 h-4 ${headerColors[overallStatus]}`} />
|
||||||
|
<span className={`text-xs font-medium ${headerColors[overallStatus]}`}>
|
||||||
|
WAU Compliance: {compliantCount}/{agents.length} Agents konform
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button onClick={onRefresh} className="text-gray-600 hover:text-gray-400 transition-colors">
|
||||||
|
<RefreshCw className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setExpanded(v => !v)}
|
||||||
|
className="text-xs text-gray-500 hover:text-gray-300 flex items-center gap-1"
|
||||||
|
>
|
||||||
|
{expanded ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />}
|
||||||
|
Details
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{expanded && (
|
||||||
|
<div className="mt-3 space-y-2">
|
||||||
|
{agents.map(agent => (
|
||||||
|
<AgentComplianceRow key={agent.agent_id} agent={agent} expectedIncludeUrl={compliance?.include_url} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="bg-gray-900/60 rounded border border-gray-700/50">
|
||||||
|
<button
|
||||||
|
className="w-full flex items-center gap-2 px-3 py-2 text-left hover:bg-gray-800/40 transition-colors"
|
||||||
|
onClick={() => setOpen(v => !v)}
|
||||||
|
>
|
||||||
|
<RowIcon className={`w-3.5 h-3.5 flex-shrink-0 ${rowColor}`} />
|
||||||
|
<span className="text-xs text-white font-mono flex-1">{agent.agent_name}</span>
|
||||||
|
<div className="flex gap-1 flex-wrap justify-end">
|
||||||
|
{!agent.wau_installed && (
|
||||||
|
<span className="text-[10px] px-1.5 py-0.5 rounded bg-red-900/40 border border-red-700/40 text-red-400">WAU fehlt</span>
|
||||||
|
)}
|
||||||
|
{agent.wau_installed && !agent.wau_config_ok && (
|
||||||
|
<span className="text-[10px] px-1.5 py-0.5 rounded bg-yellow-900/40 border border-yellow-700/40 text-yellow-400">Falsche URLs</span>
|
||||||
|
)}
|
||||||
|
{missingPkgs.length > 0 && (
|
||||||
|
<span className="text-[10px] px-1.5 py-0.5 rounded bg-red-900/40 border border-red-700/40 text-red-400">{missingPkgs.length} Paket(e) fehlen</span>
|
||||||
|
)}
|
||||||
|
{agent.pending_updates > 0 && (
|
||||||
|
<span className="text-[10px] px-1.5 py-0.5 rounded bg-yellow-900/40 border border-yellow-700/40 text-yellow-400">{agent.pending_updates} Updates</span>
|
||||||
|
)}
|
||||||
|
{agent.compliant && (
|
||||||
|
<span className="text-[10px] px-1.5 py-0.5 rounded bg-green-900/40 border border-green-700/40 text-green-400">OK</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{open ? <ChevronDown className="w-3 h-3 text-gray-600 flex-shrink-0" /> : <ChevronRight className="w-3 h-3 text-gray-600 flex-shrink-0" />}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{open && (
|
||||||
|
<div className="px-4 pb-3 pt-1 space-y-2 border-t border-gray-700/40">
|
||||||
|
{/* WAU Status */}
|
||||||
|
<div className="text-[10px] text-gray-400 space-y-1">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
{agent.wau_installed
|
||||||
|
? <CircleCheck className="w-3 h-3 text-green-400" />
|
||||||
|
: <CircleX className="w-3 h-3 text-red-400" />
|
||||||
|
}
|
||||||
|
<span>WAU {agent.wau_installed ? `installiert` : 'nicht installiert'}</span>
|
||||||
|
</div>
|
||||||
|
{agent.wau_installed && (
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
{agent.wau_config_ok
|
||||||
|
? <CircleCheck className="w-3 h-3 text-green-400" />
|
||||||
|
: <CircleX className="w-3 h-3 text-red-400" />
|
||||||
|
}
|
||||||
|
<span>
|
||||||
|
Konfiguration {agent.wau_config_ok ? 'korrekt' : 'falsch'}
|
||||||
|
{!agent.wau_config_ok && agent.include_url && (
|
||||||
|
<span className="text-gray-600 ml-1">({agent.include_url || 'keine URL'})</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{agent.pending_updates > 0 && (
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<CircleAlert className="w-3 h-3 text-yellow-400" />
|
||||||
|
<span>{agent.pending_updates} ausstehende Updates</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Paket-Status */}
|
||||||
|
{(agent.packages || []).length > 0 && (
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<div className="text-[10px] text-gray-600 font-medium uppercase tracking-wide mb-1">Pflichtpakete</div>
|
||||||
|
{(agent.packages || []).map(pkg => (
|
||||||
|
<div key={pkg.winget_id} className="flex items-center gap-1.5 text-[10px]">
|
||||||
|
{pkg.installed
|
||||||
|
? <CircleCheck className="w-3 h-3 text-green-400 flex-shrink-0" />
|
||||||
|
: <CircleX className="w-3 h-3 text-red-400 flex-shrink-0" />
|
||||||
|
}
|
||||||
|
<span className={pkg.installed ? 'text-gray-400' : 'text-red-300'}>{pkg.display_name}</span>
|
||||||
|
<span className="text-gray-700 font-mono">{pkg.winget_id}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// ── WAU-Tab ───────────────────────────────────────────────────────────────────
|
// ── WAU-Tab ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// Heuristik: Anzeigename → winget-ID-Vorschlag
|
// Heuristik: Anzeigename → winget-ID-Vorschlag
|
||||||
@ -364,6 +575,10 @@ function WauTab({ customerId, customerNumber }) {
|
|||||||
// Manuelles Formular (Fallback)
|
// Manuelles Formular (Fallback)
|
||||||
const [showManual, setShowManual] = useState(false)
|
const [showManual, setShowManual] = useState(false)
|
||||||
const [manualForm, setManualForm] = useState({ wingetId: '', displayName: '', ruleType: 'allow' })
|
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)
|
const reload = () => api.getWAURules(customerId).then(setRules)
|
||||||
|
|
||||||
@ -372,9 +587,15 @@ function WauTab({ customerId, customerNumber }) {
|
|||||||
api.getWAUInventory(customerId).then(setInventory).finally(() => setLoadingInv(false))
|
api.getWAUInventory(customerId).then(setInventory).finally(() => setLoadingInv(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadCompliance = () => {
|
||||||
|
setLoadingComp(true)
|
||||||
|
api.getWAUCompliance(customerId).then(setCompliance).finally(() => setLoadingComp(false))
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reload()
|
reload()
|
||||||
loadInventory()
|
loadInventory()
|
||||||
|
loadCompliance()
|
||||||
api.getSettings().then(s => {
|
api.getSettings().then(s => {
|
||||||
const url = s?.data?.backend_url_external || s?.backend_url_external
|
const url = s?.data?.backend_url_external || s?.backend_url_external
|
||||||
if (url) setExternalUrl(url.replace(/\/$/, ''))
|
if (url) setExternalUrl(url.replace(/\/$/, ''))
|
||||||
@ -441,6 +662,9 @@ function WauTab({ customerId, customerNumber }) {
|
|||||||
return (
|
return (
|
||||||
<div className="px-6 py-4 space-y-4">
|
<div className="px-6 py-4 space-y-4">
|
||||||
|
|
||||||
|
{/* WAU Compliance Status */}
|
||||||
|
<WAUCompliancePanel compliance={compliance} loading={loadingComp} onRefresh={loadCompliance} />
|
||||||
|
|
||||||
{/* Installationsbefehl */}
|
{/* Installationsbefehl */}
|
||||||
<div className="bg-gray-900 rounded-lg border border-gray-700 p-3">
|
<div className="bg-gray-900 rounded-lg border border-gray-700 p-3">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
|||||||
@ -1,16 +1,23 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig, loadEnv } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
|
||||||
export default defineConfig({
|
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()],
|
plugins: [react(), tailwindcss()],
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'https://your-backend:8443', // TODO: set your backend URL
|
target: backendUrl,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user