// ─── Settings Page ─────────────────────────────────────────────────────────── const { useState: uSTS, useEffect: uSTE } = React; function SettingsPage() { const [activeTab, setActiveTab] = uSTS('dashboard'); const [toast, setToast] = uSTS(null); const [settings, setSettings] = uSTS(null); const [newPath, setNewPath] = uSTS(''); const [editSvcIdx, setEditSvcIdx] = uSTS(null); const [newSvc, setNewSvc] = uSTS({ name: '', compose: '', backup: '', update: '' }); const showToast = (msg, color = '#00ff88') => { setToast({ msg, color }); setTimeout(() => setToast(null), 2500); }; uSTE(() => { apiClient('/api/settings').then(d => { if (d) setSettings(d); }).catch(() => {}); }, []); const upd = (patch) => setSettings(prev => ({ ...prev, ...patch })); const save = async () => { try { await apiClient('/api/settings', { method: 'PUT', body: JSON.stringify(settings) }); showToast('Settings saved'); } catch (err) { showToast(`Save failed: ${err.message}`, '#ff2244'); } }; const testNtfy = async () => { try { const r = await apiClient('/api/settings/ntfy/test', { method: 'POST' }); showToast(r?.ok ? 'Test notification sent!' : 'NTFY test failed', r?.ok ? '#00ff88' : '#ff2244'); } catch (err) { showToast(`Error: ${err.message}`, '#ff2244'); } }; const tabs = [ { id: 'dashboard', label: 'DASHBOARD' }, { id: 'docker', label: 'DOCKER' }, { id: 'scripts', label: 'SCRIPTS' }, { id: 'cron', label: 'CRON' }, { id: 'ntfy', label: 'NTFY' }, ]; if (!settings) return (
LOADING...
); const storagePaths = settings.storage_paths || []; const ntfy = { server: settings.ntfy_server, topic: settings.ntfy_topic, events: settings.ntfy_events || {} }; return (
{/* Sidebar tabs */}
CONFIGURATION
{tabs.map(t => (
setActiveTab(t.id)} style={{ padding: '13px 16px', cursor: 'pointer', fontSize: 14, letterSpacing: '0.1em', fontFamily: "'Share Tech Mono',monospace", color: activeTab === t.id ? '#00c8ff' : 'rgba(180,220,255,0.5)', background: activeTab === t.id ? 'rgba(0,200,255,0.1)' : 'transparent', borderLeft: activeTab === t.id ? '2px solid #00c8ff' : '2px solid transparent', transition: 'all 0.15s' }}> {t.label}
))}
{/* Content */} t.id===activeTab)?.label}`} headerRight={SAVE CHANGES}>
{/* ── Dashboard ── */} {activeTab === 'dashboard' && (
STORAGE MONITORING PATHS
{storagePaths.map((p, i) => (
{p} upd({ storage_paths: storagePaths.filter((_,j) => j !== i) })}>✕
))}
setNewPath(e.target.value)} placeholder="/path/to/monitor" style={{ flex: 1 }} /> { if (newPath) { upd({ storage_paths: [...storagePaths, newPath] }); setNewPath(''); } }}>ADD PATH
DISPLAY
)} {/* ── Docker ── */} {activeTab === 'docker' && (
DOCKER ROOT upd({ docker_root: v })} placeholder="/home/ingo/docker" />
)} {/* ── Scripts ── */} {activeTab === 'scripts' && (
PATHS upd({ scripts_path: v })} placeholder="/opt/scripts" /> upd({ python_path: v })} placeholder="/usr/bin/python3" />
)} {/* ── Cron ── */} {activeTab === 'cron' && (
CRON DAEMON
CRON USER: ingo
Managed via crontab -u ingo. Only scripts in /opt/scripts/ can be scheduled.
)} {/* ── NTFY ── */} {activeTab === 'ntfy' && (
CONNECTION upd({ ntfy_server: v })} placeholder="https://ntfy.sh" /> upd({ ntfy_topic: v })} placeholder="scp-alerts" />
⚠ NTFY token is set via SCP_NTFY_TOKEN env var — not editable here for security.
NOTIFY ON {[['onBackup','Backup completed'], ['onUpdate','Update completed'], ['onCron','Cron job finished'], ['onError','Any error'], ['onStart','Container start/stop']].map(([k, label]) => ( upd({ ntfy_events: { ...(settings.ntfy_events || {}), [k]: v } })} /> ))}
SEND TEST NOTIFICATION
)}
{toast && (
{toast.msg}
)}
); } function SectionHeader({ children, style }) { return (
{children}
); } Object.assign(window, { SettingsPage });