Stable twin of #1204. The capture-date backfill committed its result keyed on the row id alone. It snapshots every candidate up front, then walks them one at a time reading originals off S3 or a NAS mount — a pass that can run for many minutes. replacePhoto, reachable from the replace_by_name upload path, swaps a NEW file under an existing row and rewrites path/filename. A replacement landing inside that window carries no date of its own, so captured_at was still NULL, the whereNull guard passed, and the previous file's EXIF date was written onto the new photo. Silent: nothing errored, the run reported it as a success, and the gallery just sorted that photo to the wrong place. Fenced on path and filename as well as the id, so a replaced row matches zero rows and is skipped. The candidate query already selects both columns, so no query change. Knex renders a null value in the object form as `is null` on both the pg and sqlite3 clients, so a row with a NULL path still matches itself. Those skipped candidates are now counted rather than dropped. replacePhoto is not the only writer of path/filename — eventRenameService rewrites both on an event rename, which is not a content change — and another writer filling captured_at first lands in the same place. Without a counter they fell out of the run's arithmetic entirely: success + noExif + failed no longer added up to the count the operator was shown when they started the job. The card shows the count only when it is non-zero, and states what is known — changed by something else, not updated — rather than promising a retry: for the already-dated case there is nothing to retry, and the Missing Capture Date figure above is what says whether work is left. Locale coverage: en, de, fr, sl, with the defaultValue carrying the rest. Regression test: a replacement landing mid-run leaves captured_at NULL and is not counted as updated. Verified to fail against the unfenced code on this branch.
This commit is contained in:
@@ -689,6 +689,23 @@ export const StatusTab: React.FC<StatusTabProps> = ({
|
||||
failed: captureDateStatus.lastResult.failed,
|
||||
defaultValue: 'Last run: {{success}} updated, {{noExif}} with no date found, {{failed}} unreachable',
|
||||
})}
|
||||
{/* Only when it happened. Without it the three numbers above
|
||||
silently stop adding up to the count the run started with: a
|
||||
photo that was replaced, renamed or dated by someone else
|
||||
mid-run is read but not written.
|
||||
Deliberately says "not updated" and not "will be retried":
|
||||
one of the two ways to land here is another writer having
|
||||
filled captured_at, and that photo is finished, not backlog.
|
||||
The Missing Capture Date figure above is what says whether
|
||||
anything is actually left to do. */}
|
||||
{Number(captureDateStatus.lastResult.skipped) > 0 && (
|
||||
<span className="block text-amber-600 dark:text-amber-400 mt-1">
|
||||
{t('settings.captureDates.skipped', {
|
||||
count: captureDateStatus.lastResult.skipped,
|
||||
defaultValue: '{{count}} photo(s) were changed by something else while the run was reading them and were not updated.',
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user