Add factory reset, safe catalog rebuild, and DB hardening

- Factory reset endpoint clears computed tables (catalog, nightly_cache,
  tonight, weather_cache), VACUUMs the DB, then rebuilds in background.
  Preserves all user data (imaging_log, gallery, phd2_logs, horizon).
- Catalog rebuild now fetches data BEFORE touching the DB — network
  failures no longer leave the catalog empty. DELETE + INSERT wrapped
  in a single transaction via replace_catalog() so a mid-write failure
  rolls back and old data is preserved.
- Added nightly_cache indexes and bumped pool to 10 connections with
  30s acquire timeout to prevent exhaustion during rebuilds.
- Settings page: factory reset button with inline confirmation dialog.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-17 11:19:55 +02:00
parent b5a1f40f27
commit 01ccae5951
4 changed files with 183 additions and 33 deletions
+94
View File
@@ -285,6 +285,9 @@ export default function Settings() {
const [recomputeMsg, setRecomputeMsg] = useState('');
const [rebuilding, setRebuilding] = useState(false);
const [rebuildResult, setRebuildResult] = useState<any>(null);
const [showResetConfirm, setShowResetConfirm] = useState(false);
const [resetting, setResetting] = useState(false);
const [resetMsg, setResetMsg] = useState('');
const triggerRecompute = async () => {
setRecomputing(true);
@@ -302,6 +305,26 @@ export default function Settings() {
setRecomputing(false);
};
const triggerFactoryReset = async () => {
setResetting(true);
setResetMsg('');
setShowResetConfirm(false);
try {
const res = await fetch('/api/factory-reset', { method: 'POST' });
if (res.ok) {
setResetMsg('Reset started. Catalog is rebuilding (~60s). Reload the page when done.');
qc.invalidateQueries({ queryKey: ['health'] });
qc.invalidateQueries({ queryKey: ['targets'] });
} else {
const d = await res.json().catch(() => ({}));
setResetMsg((d as { error?: string }).error ?? 'Backend returned an error.');
}
} catch {
setResetMsg('Error reaching backend.');
}
setResetting(false);
};
const triggerRebuild = async () => {
setRebuilding(true);
setRebuildResult(null);
@@ -477,11 +500,82 @@ export default function Settings() {
{recomputing ? 'Starting…' : 'Recompute Tonight'}
</button>
</div>
{/* Factory Reset */}
<div style={{ marginTop: 18, borderTop: '1px solid var(--border)', paddingTop: 14 }}>
{!showResetConfirm ? (
<button
onClick={() => setShowResetConfirm(true)}
disabled={resetting}
style={{
padding: '6px 16px',
background: 'rgba(224,82,82,0.1)',
border: '1px solid rgba(224,82,82,0.4)',
color: 'var(--danger)',
borderRadius: 4,
fontFamily: 'var(--font-mono)',
fontSize: 12,
cursor: resetting ? 'default' : 'pointer',
opacity: resetting ? 0.6 : 1,
}}
>
{resetting ? 'Resetting…' : 'Factory Reset'}
</button>
) : (
<div style={{ background: 'rgba(224,82,82,0.08)', border: '1px solid rgba(224,82,82,0.3)', borderRadius: 4, padding: '12px 14px' }}>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--danger)', marginBottom: 8, fontWeight: 600 }}>
Factory Reset
</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text-mid)', marginBottom: 10, lineHeight: 1.5 }}>
Clears catalog, nightly cache, and weather data. Triggers a full rebuild (~60s).
<br />
<strong style={{ color: 'var(--text-hi)' }}>Imaging logs, gallery, PHD2 logs, and horizon are preserved.</strong>
</div>
<div style={{ display: 'flex', gap: 8 }}>
<button
onClick={triggerFactoryReset}
style={{
padding: '5px 14px',
background: 'var(--danger)',
color: '#fff',
border: 'none',
borderRadius: 3,
fontFamily: 'var(--font-mono)',
fontSize: 12,
cursor: 'pointer',
}}
>
Confirm Reset
</button>
<button
onClick={() => setShowResetConfirm(false)}
style={{
padding: '5px 14px',
background: 'var(--bg-deep)',
border: '1px solid var(--border)',
color: 'var(--text-mid)',
borderRadius: 3,
fontFamily: 'var(--font-mono)',
fontSize: 12,
cursor: 'pointer',
}}
>
Cancel
</button>
</div>
</div>
)}
</div>
{recomputeMsg && (
<div style={{ marginTop: 8, fontSize: 12, color: 'var(--text-mid)', fontFamily: 'var(--font-mono)' }}>
{recomputeMsg}
</div>
)}
{resetMsg && (
<div style={{ marginTop: 8, fontSize: 12, color: resetting ? 'var(--text-mid)' : 'var(--good)', fontFamily: 'var(--font-mono)' }}>
{resetMsg}
</div>
)}
{rebuildResult && (
<div style={{ marginTop: 12, fontSize: 11, color: 'var(--text-mid)', fontFamily: 'var(--font-mono)', background: 'var(--bg-row)', padding: '10px 12px', borderRadius: 4 }}>
{rebuildResult.error ? (