Initial commit — cynfo RMM (clean history, no secrets)

This commit is contained in:
cynfo3000
2026-03-06 09:18:45 +01:00
parent d4b6ca86e5
commit 48ac353d28
35 changed files with 732 additions and 52 deletions
+8
View File
@@ -315,6 +315,14 @@ class ApiClient {
return this.del(`/api/v1/tasks/${id}`)
}
runTaskNow(id) {
return this.post(`/api/v1/tasks/${id}/run`)
}
updateTask(id, data) {
return this.put(`/api/v1/tasks/${id}`, data)
}
toggleTask(id) {
return this.put(`/api/v1/tasks/${id}/toggle`)
}
+179 -22
View File
@@ -6,7 +6,7 @@ import { useSettingsStore } from '../stores/settings'
import {
X, Cpu, MemoryStick, HardDrive, Clock, Network, Shield, Route,
Globe, Key, Download, Terminal, Wifi, Search, Plus, Trash2, Copy, Check, ExternalLink, Unplug, Plug, MonitorSmartphone, Ban,
FileKey, UserCheck, Database, CalendarClock, Cable, Settings, Server, GitBranch, Play, Power, ChevronDown, ChevronRight,
FileKey, UserCheck, Database, CalendarClock, Cable, Settings, Server, GitBranch, Play, Power, ChevronDown, ChevronRight, Pencil,
} from 'lucide-react'
// Two-level navigation structure (DATAZONE style)
@@ -1621,6 +1621,7 @@ function TasksTab({ agentId, agentName }) {
const [total, setTotal] = useState(0)
const [loading, setLoading] = useState(true)
const [showDialog, setShowDialog] = useState(false)
const [editTask, setEditTask] = useState(null)
const [creating, setCreating] = useState(false)
const [error, setError] = useState('')
const [showWebhook, setShowWebhook] = useState(false)
@@ -1631,6 +1632,45 @@ function TasksTab({ agentId, agentName }) {
reboot: false, health_check: false, health_check_timeout: 600,
webhook_url: '', active: true,
}
const jobTemplates = [
{
label: 'Wöchentliches Update',
icon: '🔄',
form: { name: 'Wöchentliches Update', action: 'update', schedule_type: 'weekly',
schedule_time: '02:00', schedule_weekday: 0, reboot: true, health_check: true,
health_check_timeout: 600, webhook_url: '', active: true },
},
{
label: 'Monatliches Update',
icon: '📅',
form: { name: 'Monatliches Update', action: 'update', schedule_type: 'monthly',
schedule_time: '02:00', schedule_monthday: 1, reboot: true, health_check: true,
health_check_timeout: 600, webhook_url: '', active: true },
},
{
label: 'Tägliches Backup',
icon: '💾',
form: { name: 'Tägliches Backup', action: 'backup', schedule_type: 'daily',
schedule_time: '03:00', reboot: false, health_check: false,
health_check_timeout: 600, webhook_url: '', active: true },
},
{
label: 'Monatliches Backup',
icon: '🗄️',
form: { name: 'Monatliches Backup', action: 'backup', schedule_type: 'monthly',
schedule_time: '03:00', schedule_monthday: 1, reboot: false, health_check: false,
health_check_timeout: 600, webhook_url: '', active: true },
},
{
label: 'Update + ERP-Webhook',
icon: '🔗',
form: { name: 'Monatliches Update + Servicebericht', action: 'update', schedule_type: 'monthly',
schedule_time: '02:00', schedule_monthday: 1, reboot: true, health_check: true,
health_check_timeout: 600, webhook_url: '', active: true },
showWebhook: true,
},
]
const [form, setForm] = useState(defaultForm)
const loadTasks = () => {
@@ -1669,9 +1709,47 @@ function TasksTab({ agentId, agentName }) {
if (!confirm('Job wirklich loeschen?')) return
try { await api.deleteTask(id); loadTasks() } catch (e) { setError(e.message) }
}
const handleEdit = (t) => {
setEditTask(t)
setForm({
name: t.name || '',
action: t.action,
schedule_type: t.schedule_type || 'once',
schedule_time: t.schedule_time || '02:00',
scheduled_at: '',
schedule_weekday: t.schedule_weekday ?? 1,
schedule_monthday: t.schedule_monthday ?? 1,
reboot: t.reboot || false,
health_check: t.health_check || false,
health_check_timeout: t.health_check_timeout || 600,
webhook_url: t.webhook_url || '',
active: t.active !== false,
})
setShowWebhook(!!t.webhook_url)
setShowDialog(true)
}
const handleSave = async () => {
setCreating(true); setError('')
try {
const data = {
name: form.name, schedule_type: form.schedule_type, schedule_time: form.schedule_time,
active: form.active, reboot: form.reboot, health_check: form.health_check,
health_check_timeout: parseInt(form.health_check_timeout) || 600,
webhook_url: form.webhook_url,
}
if (form.schedule_type === 'weekly') data.schedule_weekday = parseInt(form.schedule_weekday)
if (form.schedule_type === 'monthly') data.schedule_monthday = parseInt(form.schedule_monthday)
await api.updateTask(editTask.id, data)
setShowDialog(false); setEditTask(null); setForm(defaultForm); setShowWebhook(false); loadTasks()
} catch (e) { setError(e.message) } finally { setCreating(false) }
}
const handleRunNow = async (id) => {
// Trigger by creating a one-off copy? For now just toggle to trigger scheduler
// Simple approach: we don't have a "run now" endpoint, so we skip for now
try {
await api.runTaskNow(id)
setTimeout(loadTasks, 1500)
} catch (e) { setError(e.message) }
}
const weekdays = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa']
@@ -1702,9 +1780,24 @@ function TasksTab({ agentId, agentName }) {
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center" onClick={() => setShowDialog(false)}>
<div className="bg-gray-800 rounded-lg border border-gray-700 w-full max-w-lg mx-4 max-h-[90vh] overflow-y-auto" onClick={e => e.stopPropagation()}>
<div className="flex items-center justify-between px-5 py-4 border-b border-gray-700">
<h3 className="text-white font-semibold">Neuer Job{agentName ? ` — ${agentName}` : ''}</h3>
<button onClick={() => setShowDialog(false)} className="text-gray-400 hover:text-white"><X className="w-5 h-5" /></button>
<h3 className="text-white font-semibold">{editTask ? `Job bearbeiten — ${editTask.name || `#${editTask.id}`}` : `Neuer Job${agentName ? ` — ${agentName}` : ''}`}</h3>
<button onClick={() => { setShowDialog(false); setEditTask(null) }} className="text-gray-400 hover:text-white"><X className="w-5 h-5" /></button>
</div>
{!editTask && (
<div className="px-5 pt-4 pb-2 border-b border-gray-700/50">
<p className="text-xs text-gray-500 mb-2">Vorlage wählen</p>
<div className="flex flex-wrap gap-2">
{jobTemplates.map((tpl, i) => (
<button key={i} onClick={() => {
setForm({ ...defaultForm, ...tpl.form, scheduled_at: '', schedule_weekday: tpl.form.schedule_weekday ?? 1, schedule_monthday: tpl.form.schedule_monthday ?? 1 })
setShowWebhook(!!tpl.showWebhook)
}} className="px-3 py-1.5 bg-gray-700 hover:bg-gray-600 rounded text-xs text-gray-300 hover:text-white transition-colors">
{tpl.label}
</button>
))}
</div>
</div>
)}
<div className="px-5 py-4 space-y-4">
<div>
<label className={labelCls}>Name *</label>
@@ -1713,7 +1806,7 @@ function TasksTab({ agentId, agentName }) {
<div className="grid grid-cols-2 gap-3">
<div>
<label className={labelCls}>Intervall *</label>
<select value={form.schedule_type} onChange={e => setForm({...form, schedule_type: e.target.value})} className={inputCls}>
<select value={form.schedule_type} onChange={e => setForm({...form, schedule_type: e.target.value})} className={inputCls} disabled={!!editTask}>
<option value="once">Einmalig</option>
<option value="daily">Täglich</option>
<option value="weekly">Wöchentlich</option>
@@ -1745,17 +1838,19 @@ function TasksTab({ agentId, agentName }) {
<input type="number" min="1" max="28" value={form.schedule_monthday} onChange={e => setForm({...form, schedule_monthday: e.target.value})} className={inputCls} />
</div>
)}
<div>
<label className={labelCls}>Aktion *</label>
<div className="flex gap-2">
{['backup', 'update'].map(a => (
<button key={a} onClick={() => setForm({...form, action: a})}
className={`flex-1 py-2 rounded text-sm font-medium transition-colors ${form.action === a ? 'bg-orange-600 text-white' : 'bg-gray-700 text-gray-400 hover:bg-gray-600'}`}>
{a === 'backup' ? 'Backup' : 'Update'}
</button>
))}
{!editTask && (
<div>
<label className={labelCls}>Aktion *</label>
<div className="flex gap-2">
{['backup', 'update'].map(a => (
<button key={a} onClick={() => setForm({...form, action: a})}
className={`flex-1 py-2 rounded text-sm font-medium transition-colors ${form.action === a ? 'bg-orange-600 text-white' : 'bg-gray-700 text-gray-400 hover:bg-gray-600'}`}>
{a === 'backup' ? 'Backup' : 'Update'}
</button>
))}
</div>
</div>
</div>
)}
{form.action === 'update' && (
<div className="space-y-2 pl-1">
<label className="flex items-center gap-2 text-sm text-gray-300 cursor-pointer">
@@ -1779,7 +1874,10 @@ function TasksTab({ agentId, agentName }) {
{showWebhook ? <ChevronDown className="w-3.5 h-3.5" /> : <ChevronRight className="w-3.5 h-3.5" />} Webhook-URL (optional)
</button>
{showWebhook && (
<input type="text" value={form.webhook_url} onChange={e => setForm({...form, webhook_url: e.target.value})} placeholder="https://..." className={inputCls + ' mt-1'} />
<div>
<input type="text" value={form.webhook_url} onChange={e => setForm({...form, webhook_url: e.target.value})} placeholder="https://erp.example.com/api/service-reports" className={inputCls + ' mt-1'} />
<p className="text-xs text-gray-500 mt-1">Empfangt: customer_id, hostname, opnsense_version, reachable, update_status</p>
</div>
)}
</div>
<label className="flex items-center gap-2 text-sm text-gray-300 cursor-pointer">
@@ -1789,9 +1887,9 @@ function TasksTab({ agentId, agentName }) {
{error && <div className="text-sm text-red-400">{error}</div>}
</div>
<div className="flex justify-end gap-2 px-5 py-4 border-t border-gray-700">
<button onClick={() => { setShowDialog(false); setError('') }} className="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded text-sm text-gray-300 transition-colors">Abbrechen</button>
<button onClick={handleCreate} disabled={creating || !form.name} className="px-4 py-2 bg-orange-600 hover:bg-orange-700 disabled:bg-gray-700 disabled:text-gray-500 rounded text-sm text-white transition-colors">
{creating ? 'Erstelle...' : 'Erstellen'}
<button onClick={() => { setShowDialog(false); setEditTask(null); setError('') }} className="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded text-sm text-gray-300 transition-colors">Abbrechen</button>
<button onClick={editTask ? handleSave : handleCreate} disabled={creating || !form.name} className="px-4 py-2 bg-orange-600 hover:bg-orange-700 disabled:bg-gray-700 disabled:text-gray-500 rounded text-sm text-white transition-colors">
{creating ? (editTask ? 'Speichere...' : 'Erstelle...') : (editTask ? 'Speichern' : 'Erstellen')}
</button>
</div>
</div>
@@ -1814,6 +1912,7 @@ function TasksTab({ agentId, agentName }) {
<th className="px-3 py-2">Intervall</th>
<th className="px-3 py-2">Nächster Lauf</th>
<th className="px-3 py-2">Letzter Lauf</th>
<th className="px-3 py-2">Webhook</th>
<th className="px-3 py-2 text-right">Aktionen</th>
</tr>
</thead>
@@ -1834,8 +1933,23 @@ function TasksTab({ agentId, agentName }) {
<td className="px-3 py-2 text-gray-400 text-xs">{fmtInterval(t)}</td>
<td className="px-3 py-2 text-gray-400 text-xs">{fmtDate(t.next_run)}</td>
<td className="px-3 py-2 text-gray-400 text-xs">{fmtDate(t.last_run)}</td>
<td className="px-3 py-2 text-xs">
{t.webhook_url ? (
t.webhook_sent
? <span className="text-green-400" title={t.webhook_response || 'Gesendet'}>OK</span>
: t.last_run
? <span className="text-red-400" title={t.webhook_response || 'Fehlgeschlagen'}>Fehler</span>
: <span className="text-gray-500">—</span>
) : <span className="text-gray-600">—</span>}
</td>
<td className="px-3 py-2 text-right">
<div className="flex items-center justify-end gap-1">
<button onClick={() => handleEdit(t)} className="text-gray-400 hover:text-white p-1 rounded hover:bg-gray-700 transition-colors" title="Bearbeiten">
<Pencil className="w-4 h-4" />
</button>
<button onClick={() => handleRunNow(t.id)} className="text-blue-400 hover:text-blue-300 p-1 rounded hover:bg-blue-900/30 transition-colors" title="Jetzt ausführen" disabled={t.status === 'running'}>
<Play className="w-4 h-4" />
</button>
<button onClick={() => handleToggle(t.id)} className={`p-1 rounded transition-colors ${t.active ? 'text-green-400 hover:text-green-300 hover:bg-green-900/30' : 'text-gray-500 hover:text-gray-300 hover:bg-gray-700'}`} title={t.active ? 'Deaktivieren' : 'Aktivieren'}>
<Power className="w-4 h-4" />
</button>
@@ -2380,12 +2494,55 @@ function CaddyTab({ sys }) {
const caddy = sys?.caddy
if (!caddy) return <div className="text-gray-500">Keine Caddy-Daten</div>
const upstreams = caddy.upstreams || []
const healthyCount = upstreams.filter(u => u.healthy).length
const activeRequests = upstreams.reduce((s, u) => s + (u.num_requests || 0), 0)
const totalFails = upstreams.reduce((s, u) => s + (u.fails || 0), 0)
return (
<div className="space-y-4">
<h3 className="text-sm font-medium text-gray-300">Caddy Reverse Proxy</h3>
{caddy.version && (
<div className="text-xs text-gray-400">Version: <span className="text-white font-mono">{caddy.version}</span></div>
<div className="grid grid-cols-4 gap-3">
<StatusCard icon={Server} value={caddy.version || '—'} label="Version" />
<StatusCard icon={Network} value={`${healthyCount} / ${upstreams.length}`} label="Upstreams OK" />
<StatusCard icon={Globe} value={activeRequests} label="Aktive Requests" />
<StatusCard value={totalFails} label="Fehler gesamt" />
</div>
{upstreams.length > 0 && (
<>
<h3 className="text-sm font-medium text-gray-300">Upstreams</h3>
<div className="bg-gray-800/50 rounded-lg border border-gray-700/50 overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="text-left text-gray-500 text-xs border-b border-gray-700">
<th className="px-3 py-2">Adresse</th>
<th className="px-3 py-2">Status</th>
<th className="px-3 py-2">Aktive Req.</th>
<th className="px-3 py-2">Fehler</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-700/50">
{upstreams.map((u, i) => (
<tr key={i}>
<td className="px-3 py-2 text-white font-mono">{u.address}</td>
<td className="px-3 py-2">
<span className={`text-xs px-1.5 py-0.5 rounded ${
u.healthy ? 'bg-green-900/50 text-green-400' : 'bg-red-900/50 text-red-400'
}`}>{u.healthy ? 'Healthy' : 'Unhealthy'}</span>
</td>
<td className="px-3 py-2 text-gray-400">{u.num_requests}</td>
<td className="px-3 py-2 text-gray-400">{u.fails}</td>
</tr>
))}
</tbody>
</table>
</div>
</>
)}
<h3 className="text-sm font-medium text-gray-300">Konfigurierte Sites</h3>
<div className="bg-gray-800/50 rounded-lg border border-gray-700/50 overflow-hidden">
<table className="w-full text-sm">
<thead>
+4
View File
@@ -0,0 +1,4 @@
// Frontend-Konfiguration — bei Deployment anpassen
export const BACKEND_HOST = '192.168.85.13'
export const BACKEND_URL = `https://${BACKEND_HOST}:8443`
export const API_KEY = '01532e5a7c9e70bf2757df77a2f5b9b9'