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:
@@ -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 ? (
|
||||
|
||||
Reference in New Issue
Block a user