fix(scan): only resurrect discards when file changed; add Saved toast

The scan_folder resurrect path was unflagging every discarded photo on
every backend boot. start_initial_scan fires scan_all_source_roots on
container start, which fans out scan_folder for every source root,
which walked every file and silently set is_discarded=False on rows
whose file was still on disk -- so every deploy wiped the user's
discard decisions. Today's series of resurrect log lines for
admin/Photos came from that path, not from any actual user re-upload.

Gate the resurrect on os.path.getmtime(file) > discarded_at so the
WebDAV-DELETE-then-re-upload and trashbin-restore-via-PUT-overwrite
flows still trigger (those rewrite the file and bump mtime), but
routine sweeps respect the user's intent. Rows with discarded_at NULL
(legacy) fall through to skipped -- preserve intent over cleanup.

While there: add a Saved toast to the single-photo updateMutation.
The previous patch made cache writes synchronous, which removed the
visible save delay but also removed any signal that the change was
actually persisted. Toast picks a per-field label from the patched
keys (Title updated / Date updated / etc.) and falls back to a count
for multi-field saves.
This commit is contained in:
Claudio
2026-05-11 22:29:25 +02:00
parent abe5c1ec6b
commit 09c12ea35b
2 changed files with 75 additions and 14 deletions

View File

@@ -181,7 +181,10 @@ export function PhotoInfoPanel({ photoId, darkTheme = false }: PhotoInfoPanelPro
color_label?: string | null
taken_at?: string
}) => photosApi.update(photoId, data),
onSuccess: (updated: Partial<PhotoDetails> & { id: string }) => {
onSuccess: (
updated: Partial<PhotoDetails> & { id: string },
variables,
) => {
queryClient.setQueryData<PhotoDetails>(['photo', photoId], (prev) =>
prev ? { ...prev, ...updated } : (updated as PhotoDetails),
)
@@ -194,6 +197,35 @@ export function PhotoInfoPanel({ photoId, darkTheme = false }: PhotoInfoPanelPro
)
queryClient.invalidateQueries({ queryKey: ['photos'] })
queryClient.invalidateQueries({ queryKey: LIBRARY_STATS_QUERY_KEY })
// Confirmation toast. Cache updates are synchronous now, so without
// a toast a successful save is invisible (the field shows the new
// value but it's the same one the user just typed). Pick a label
// from the changed field; fall back to a count for multi-field
// patches and a generic message if neither applies.
const fieldLabels: Record<string, string> = {
filename: 'Filename updated',
rating: 'Rating updated',
color_label: 'Color updated',
user_title: 'Title updated',
user_notes: 'Notes updated',
taken_at: 'Date updated',
}
const fields = Object.keys(variables)
let detail: string
if (fields.length === 1) {
const k = fields[0]
if (k === 'is_discarded') {
detail = variables.is_discarded ? 'Discarded' : 'Restored'
} else {
detail = fieldLabels[k] ?? 'Updated'
}
} else if (fields.length > 1) {
detail = `${fields.length} fields updated`
} else {
detail = 'Updated'
}
toast.success('Saved', detail)
},
})