// ─── Docker Page ───────────────────────────────────────────────────────────── const { useState: uDKS, useEffect: uDKE, useRef: uDKR } = React; function DockerPage() { const [containers, setContainers] = uDKS([]); const [filter, setFilter] = uDKS('all'); const [selectedId, setSelectedId] = uDKS(null); const [composeModal, setComposeModal] = uDKS(false); const [composeContent, setComposeContent] = uDKS(''); const [composeName, setComposeName] = uDKS(''); const [actionLog, setActionLog] = uDKS([]); const [toast, setToast] = uDKS(null); const [loading, setLoading] = uDKS(false); const log = (msg, color = '#00c8ff') => { const ts = new Date().toLocaleTimeString('de-DE'); setActionLog(prev => [{ ts, msg, color, id: Date.now() }, ...prev.slice(0, 19)]); }; const showToast = (msg, color = '#00ff88') => { setToast({ msg, color }); setTimeout(() => setToast(null), 2500); }; const fetchContainers = () => apiClient('/api/docker/containers').then(d => { if (d) setContainers(d); }).catch(() => {}); uDKE(() => { fetchContainers(); const id = setInterval(fetchContainers, 10000); return () => clearInterval(id); }, []); const doAction = async (id, action) => { const c = containers.find(x => x.id === id); if (!c) return; try { if (action === 'start' || action === 'stop' || action === 'restart') { await apiClient(`/api/docker/containers/${id}/${action}`, { method: 'POST' }); const colors = { start: '#00ff88', stop: '#ff2244', restart: '#ffd700' }; const icons = { start: '▶', stop: '■', restart: '↺' }; log(`${icons[action]} ${action.charAt(0).toUpperCase()+action.slice(1)}ed ${c.name}`, colors[action]); showToast(`${c.name} ${action}ed`, colors[action]); setTimeout(fetchContainers, 1500); } else if (action === 'backup') { const r = await apiClient(`/api/docker/containers/${c.name}/backup`, { method: 'POST' }); log(`💾 Backup started for ${c.name} (run: ${r?.run_id?.slice(0,8)})`, '#00c8ff'); showToast(`Backup started for ${c.name}`); } else if (action === 'update') { const r = await apiClient(`/api/docker/containers/${c.name}/update`, { method: 'POST' }); log(`⬆ Update started for ${c.name} (run: ${r?.run_id?.slice(0,8)})`, '#00c8ff'); showToast(`Update started for ${c.name}`); } } catch (err) { log(`✖ ${action} failed for ${c.name}: ${err.message}`, '#ff2244'); showToast(`${action} failed`, '#ff2244'); } }; const openCompose = async (c) => { setSelectedId(c.id); setComposeName(c.name); try { const data = await apiClient(`/api/docker/compose/${c.name}`); setComposeContent(data?.content || ''); } catch { setComposeContent('# Could not load compose file'); } setComposeModal(true); }; const saveCompose = async () => { try { await apiClient(`/api/docker/compose/${composeName}`, { method: 'PUT', body: JSON.stringify({ content: composeContent }), }); setComposeModal(false); showToast('docker-compose.yaml saved'); log(`Saved compose for ${composeName}`, '#00ff88'); } catch (err) { showToast(`Save failed: ${err.message}`, '#ff2244'); } }; const filtered = filter === 'all' ? containers : containers.filter(c => c.status === filter); const stats = { running: containers.filter(c => c.status === 'running').length, stopped: containers.filter(c => c.status === 'stopped').length, paused: containers.filter(c => c.status === 'paused').length, }; return (
{/* Top Stats */}
{[ ['TOTAL', containers.length, '#00c8ff'], ['RUNNING', stats.running, '#00ff88'], ['STOPPED', stats.stopped, '#ff2244'], ['PAUSED', stats.paused, '#ffd700'], ].map(([l, v, c]) => (
setFilter(l.toLowerCase() === 'total' ? 'all' : l.toLowerCase())} style={{ flex: 1, background: filter === (l.toLowerCase() === 'total' ? 'all' : l.toLowerCase()) ? `${c}15` : 'rgba(0,15,35,0.8)', border: `1px solid ${filter === (l.toLowerCase() === 'total' ? 'all' : l.toLowerCase()) ? c : c + '30'}`, padding: '8px 14px', cursor: 'pointer', display: 'flex', justifyContent: 'space-between', alignItems: 'center', transition: 'all 0.2s' }}> {l} {v}
))}
{/* Container List */}
{['NAME', 'IMAGE', 'STATUS', 'CPU %', 'MEM MB', 'PORTS', 'UPTIME', 'ACTIONS'].map(h => ( ))} {filtered.map(c => ( setSelectedId(c.id === selectedId ? null : c.id)} style={{ cursor: 'pointer', background: selectedId === c.id ? 'rgba(0,200,255,0.06)' : 'transparent', transition: 'background 0.15s' }}> ))} {filtered.length === 0 && ( )}
{h}
{c.name} {c.image} 1 ? '#ffd700' : '#00ff88', borderBottom: '1px solid rgba(0,200,255,0.05)' }}>{(c.cpu_pct||0).toFixed(1)} {(c.mem_mb||0).toFixed(0)} {(c.ports||[]).join(', ') || '—'} {c.uptime}
{c.status !== 'running' && { e.stopPropagation(); doAction(c.id, 'start'); }}>▶} {c.status === 'running' && { e.stopPropagation(); doAction(c.id, 'stop'); }}>■} { e.stopPropagation(); doAction(c.id, 'restart'); }}>↺ { e.stopPropagation(); doAction(c.id, 'backup'); }}>BAK { e.stopPropagation(); doAction(c.id, 'update'); }}>UPD { e.stopPropagation(); openCompose(c); }}>YAML
LOADING...
{/* Action Log */}
{actionLog.length === 0 && (
NO ACTIONS YET
)} {actionLog.map(e => (
{e.ts} {e.msg}
))}
{/* Compose Editor Modal */} setComposeModal(false)} title="DOCKER-COMPOSE EDITOR" width={700}>
/home/ingo/docker/{composeName}/docker-compose.yaml
setComposeModal(false)}>CANCEL SAVE
{toast && (
{toast.msg}
)}
); } Object.assign(window, { DockerPage });