fix: prune orphaned folder rows alongside photo rows

prune_missing_photos() previously only deleted Photo rows whose files
were gone, leaving every folder row from the old library in the DB —
which made the sidebar tree wildly out of sync with the on-disk
structure (still showing /photos/2024/, /photos/2026/03/, etc. that
no longer exist).

Now also drops Folder rows whose path doesn't resolve under a mounted
source root, with the same defensive "skip if source root unmounted"
guard. The Settings orphan card surfaces both counts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
root
2026-04-09 10:50:32 +02:00
parent 697343646a
commit d27ec1af2e
3 changed files with 121 additions and 67 deletions

View File

@@ -420,52 +420,59 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
)}
{/* Orphaned rows (files gone from disk) */}
{missingStats && (missingStats.would_delete ?? 0) > 0 && (
<div className="mt-3 rounded border border-star/40 bg-star/10 p-2">
<div className="flex items-center justify-between gap-2">
<div className="text-xs">
<div className="flex items-center gap-1.5 font-medium text-text">
<AlertTriangle className="h-3.5 w-3.5 text-star" />
{missingStats.would_delete} orphaned photo rows
{missingStats &&
((missingStats.would_delete ?? 0) > 0 ||
(missingStats.would_delete_folders ?? 0) > 0) && (
<div className="mt-3 rounded border border-star/40 bg-star/10 p-2">
<div className="flex items-center justify-between gap-2">
<div className="text-xs">
<div className="flex items-center gap-1.5 font-medium text-text">
<AlertTriangle className="h-3.5 w-3.5 text-star" />
Orphaned rows
</div>
<div className="mt-0.5 text-[10px] text-text-muted">
{missingStats.would_delete ?? 0} photos and{' '}
{missingStats.would_delete_folders ?? 0} folders
point at paths that no longer exist on disk under a
mounted source root. Usually means PHOTO_DIRS was
repointed at a different library.
{missingStats.skipped_unmounted > 0 && (
<>
{' '}
{missingStats.skipped_unmounted} more rows are
under unmounted roots and will not be touched.
</>
)}
</div>
</div>
<div className="mt-0.5 text-[10px] text-text-muted">
Files no longer exist on disk under a mounted source
root. Usually means PHOTO_DIRS was repointed at a
different library.
{missingStats.skipped_unmounted > 0 && (
<>
{' '}
{missingStats.skipped_unmounted} more rows are
under unmounted roots and will not be touched.
</>
)}
</div>
</div>
<ActionButton
loading={busy['prune-missing']}
destructive
onClick={() => {
if (
!confirm(
`Delete ${missingStats.would_delete} photo rows whose files are missing? ` +
'This cannot be undone.'
<ActionButton
loading={busy['prune-missing']}
destructive
onClick={() => {
const photos = missingStats.would_delete ?? 0
const folders = missingStats.would_delete_folders ?? 0
if (
!confirm(
`Delete ${photos} photo rows and ${folders} folder rows whose paths are missing? ` +
'This cannot be undone.'
)
)
)
return
runAction(
'prune-missing',
() => library.maintenance.pruneMissing(),
'Orphans pruned',
(r) => `${r.deleted ?? 0} rows deleted`
)
}}
>
<AlertTriangle className="h-4 w-4" />
Prune
</ActionButton>
return
runAction(
'prune-missing',
() => library.maintenance.pruneMissing(),
'Orphans pruned',
(r) =>
`${r.deleted ?? 0} photos + ${r.deleted_folders ?? 0} folders deleted`
)
}}
>
<AlertTriangle className="h-4 w-4" />
Prune
</ActionButton>
</div>
</div>
</div>
)}
)}
{/* Per-worker breakdown */}
{workerStatus && workerStatus.workers.length > 0 && (

View File

@@ -233,6 +233,8 @@ export interface RegenerateResult {
export interface MissingStats {
would_delete?: number
deleted?: number
would_delete_folders?: number
deleted_folders?: number
skipped_unmounted: number
dry_run: boolean
}