feat(caddy): route edit, system log entries, fix routes load timing

Add inline edit for custom routes (Pencil icon → inline form with all fields).
Log route add/update/delete/import to the logs table (type: system) so
operations appear in the Logbook. Fix loadCaddyRoutes() called without await
after settings save, causing a race between the success message and route list.
This commit is contained in:
Brückner
2026-06-08 13:04:01 +02:00
parent 00cf5dd02d
commit f66b1ca456
3 changed files with 148 additions and 23 deletions

View File

@ -4,7 +4,7 @@ import { User } from '../types';
import {
Shield, Activity, Save, CheckCircle, AlertCircle, Eye, EyeOff,
Settings2, KeyRound, Globe, Clock, ChevronRight, Copy, Users, RefreshCw, Terminal,
Network, Trash2, Plus, HardDrive, Download, Upload, AlertTriangle, Plug, Server,
Network, Trash2, Plus, HardDrive, Download, Upload, AlertTriangle, Plug, Server, Pencil, X,
} from 'lucide-react';
const SECRET_SENTINEL = '__SET__';
@ -200,6 +200,13 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
const [newTls, setNewTls] = useState(true);
const [newCompress, setNewCompress] = useState(true);
const [editingRouteId, setEditingRouteId] = useState<number | null>(null);
const [editHostname, setEditHostname] = useState('');
const [editUpstream, setEditUpstream] = useState('');
const [editTls, setEditTls] = useState(true);
const [editCompress, setEditCompress] = useState(true);
const [savingRoute, setSavingRoute] = useState(false);
const [dbInfo, setDbInfo] = useState<DbInfo | null>(null);
const [backingUp, setBackingUp] = useState(false);
const [importFile, setImportFile] = useState<File | null>(null);
@ -293,7 +300,7 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
setAzureClientSecret('');
setCheckmkApiSecret('');
setSemaphoreApiToken('');
loadCaddyRoutes();
await loadCaddyRoutes();
setSuccessMsg('Settings saved successfully.');
setTimeout(() => setSuccessMsg(''), 4000);
} catch {
@ -406,6 +413,40 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
}
}
function handleEditStart(r: CaddyRoute) {
setEditingRouteId(r.id);
setEditHostname(r.hostname);
setEditUpstream(r.upstream);
setEditTls(r.tls === 1);
setEditCompress(r.compress === 1);
}
function handleEditCancel() {
setEditingRouteId(null);
}
async function handleEditSave(id: number) {
if (!editHostname.trim() || !editUpstream.trim()) return;
setSavingRoute(true);
try {
const res = await authFetch(`/api/caddy/routes/${id}`, {
method: 'PUT',
body: JSON.stringify({ hostname: editHostname.trim(), upstream: editUpstream.trim(), tls: editTls, compress: editCompress }),
});
if (!res.ok) {
const d = await res.json();
setError(d.error || 'Failed to update route.');
return;
}
setEditingRouteId(null);
await loadCaddyRoutes();
} catch {
setError('Network error updating route.');
} finally {
setSavingRoute(false);
}
}
async function loadDbInfo() {
try {
const res = await authFetch('/api/database/info');
@ -901,22 +942,61 @@ export default function Settings({ currentUser: _currentUser }: SettingsProps) {
{/* Custom routes */}
{caddyRoutes?.custom.map(r => (
<div key={r.id} className="flex items-center justify-between bg-slate-900/60 border border-slate-700/60 rounded-xl px-4 py-2.5">
<div className="flex items-center gap-3 min-w-0">
<span className="text-[11px] font-mono text-white truncate">{r.hostname}</span>
<span className="text-slate-600 text-[11px]">&gt;</span>
<span className="text-[11px] font-mono text-slate-400 truncate">{r.upstream}</span>
{r.tls ? <span className="text-[9px] font-semibold text-sky-500 font-mono">TLS</span> : null}
{r.compress ? <span className="text-[9px] font-semibold text-slate-500 font-mono">GZ</span> : null}
</div>
<button
type="button"
onClick={() => handleDeleteRoute(r.id)}
className="ml-3 shrink-0 p-1.5 rounded-lg text-slate-600 hover:text-red-400 hover:bg-red-950/30 transition-all"
title="Remove route"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
<div key={r.id} className="bg-slate-900/60 border border-slate-700/60 rounded-xl px-4 py-2.5">
{editingRouteId === r.id ? (
<div className="flex items-end gap-2">
<div className="flex-1 min-w-0">
<Label>Hostname</Label>
<Input value={editHostname} onChange={setEditHostname} placeholder="hostname" monospace />
</div>
<div className="flex-1 min-w-0">
<Label>Upstream</Label>
<Input value={editUpstream} onChange={setEditUpstream} placeholder="127.0.0.1:3000" monospace />
</div>
<div className="flex flex-col items-center gap-1 pb-0.5">
<span className="text-[9px] font-semibold text-slate-400 uppercase tracking-wide">TLS</span>
<button type="button" onClick={() => setEditTls(v => !v)}
className={`relative w-8 h-4 rounded-full transition-all duration-200 focus:outline-none ${editTls ? 'bg-sky-600' : 'bg-slate-800 border border-slate-700'}`}>
<span className={`absolute top-0.5 w-3 h-3 bg-white rounded-full shadow transition-all duration-200 ${editTls ? 'left-4' : 'left-0.5'}`} />
</button>
</div>
<div className="flex flex-col items-center gap-1 pb-0.5">
<span className="text-[9px] font-semibold text-slate-400 uppercase tracking-wide">GZ</span>
<button type="button" onClick={() => setEditCompress(v => !v)}
className={`relative w-8 h-4 rounded-full transition-all duration-200 focus:outline-none ${editCompress ? 'bg-sky-600' : 'bg-slate-800 border border-slate-700'}`}>
<span className={`absolute top-0.5 w-3 h-3 bg-white rounded-full shadow transition-all duration-200 ${editCompress ? 'left-4' : 'left-0.5'}`} />
</button>
</div>
<button type="button" onClick={() => handleEditSave(r.id)} disabled={savingRoute || !editHostname.trim() || !editUpstream.trim()}
className="flex items-center gap-1 bg-sky-950/60 hover:bg-sky-900/40 border border-sky-900/50 text-sky-400 text-xs font-semibold px-2.5 py-2 rounded-lg transition-all disabled:opacity-50 shrink-0">
<CheckCircle className="w-3.5 h-3.5" />
</button>
<button type="button" onClick={handleEditCancel}
className="p-2 rounded-lg text-slate-500 hover:text-slate-300 hover:bg-slate-800 transition-all shrink-0">
<X className="w-3.5 h-3.5" />
</button>
</div>
) : (
<div className="flex items-center justify-between">
<div className="flex items-center gap-3 min-w-0">
<span className="text-[11px] font-mono text-white truncate">{r.hostname}</span>
<span className="text-slate-600 text-[11px]">&gt;</span>
<span className="text-[11px] font-mono text-slate-400 truncate">{r.upstream}</span>
{r.tls ? <span className="text-[9px] font-semibold text-sky-500 font-mono">TLS</span> : null}
{r.compress ? <span className="text-[9px] font-semibold text-slate-500 font-mono">GZ</span> : null}
</div>
<div className="flex items-center gap-1 ml-3 shrink-0">
<button type="button" onClick={() => handleEditStart(r)}
className="p-1.5 rounded-lg text-slate-600 hover:text-sky-400 hover:bg-sky-950/30 transition-all" title="Edit route">
<Pencil className="w-3.5 h-3.5" />
</button>
<button type="button" onClick={() => handleDeleteRoute(r.id)}
className="p-1.5 rounded-lg text-slate-600 hover:text-red-400 hover:bg-red-950/30 transition-all" title="Remove route">
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</div>
)}
</div>
))}