|
|
|
@@ -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 (
|
|
|
|
|
<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() {
|
|
|
|
|
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() {
|
|
|
|
|
<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">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 w-24"></th>
|
|
|
|
|
</tr>
|
|
|
|
@@ -156,6 +205,9 @@ export default function Customers() {
|
|
|
|
|
</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 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">
|
|
|
|
|
{new Date(c.created_at).toLocaleDateString('de-DE')}
|
|
|
|
|
</td>
|
|
|
|
@@ -179,7 +231,7 @@ export default function Customers() {
|
|
|
|
|
)}
|
|
|
|
|
{expandedId === 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} />
|
|
|
|
|
</td>
|
|
|
|
|
</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 ───────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
// 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 (
|
|
|
|
|
<div className="px-6 py-4 space-y-4">
|
|
|
|
|
|
|
|
|
|
{/* WAU Compliance Status */}
|
|
|
|
|
<WAUCompliancePanel compliance={compliance} loading={loadingComp} onRefresh={loadCompliance} />
|
|
|
|
|
|
|
|
|
|
{/* Installationsbefehl */}
|
|
|
|
|
<div className="bg-gray-900 rounded-lg border border-gray-700 p-3">
|
|
|
|
|
<div className="flex items-center justify-between mb-2">
|
|
|
|
|