fix(thumbnails): neither import job may tear down the shared directory

Found on a sandbox restore. `thumb_derived_import` ran first, imported
and deleted its own hash-named sidecars, then found `remove_dir` refused
because the `ext-*.jpg` previews were still there — those belong to
`thumb_attached_import`. The rename fallback fired, moving the tree to
`.thumbnails.migrated`; the attached job then looked in `.thumbnails/`,
found nothing, and reported zeros.

That stranded the user-uploaded previews, which are the one class of
file here with no render path to rebuild them. The rename exists for
files NEITHER job claims — a `.DS_Store` blocking removal forever — and
it fired for the sibling's work in progress instead. Inverting the job
order does not help: once the tree is renamed, both jobs look at
`.thumbnails/` and find nothing, whatever order they run in.

Teardown is now shared and refuses to act while anything remains that
either job would claim. Both jobs call it, so whichever finishes last
removes the tree in the same boot rather than leaving an empty
directory until the next one. The rename survives for its original
purpose, and now only fires when the remaining files are genuinely
nobody's.

Also drops the daily tick on both imports — they are on-demand now. The
boot run in repair mode IS the migration: nothing has written a sidecar
since step 10d2, so the tail cannot grow afterwards, and a tick could
not finish the job anyway because ticks never pass `repair`. Once
drained it was a `read_dir` returning nothing, every day, forever.

UX: the "at boot" badge moves from beside the job name into the cadence
column. It answers WHEN a job runs, which is what that column is for —
next to the name it read as a property of the job, and the row could
show "on-demand" beside a badge saying otherwise.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Edouard Vanbelle
2026-08-30 16:18:49 +02:00
parent 577ecb7cef
commit ce4354f497
3 changed files with 164 additions and 80 deletions
@@ -860,11 +860,17 @@
{mutatesLabel(job)}
</span>
{/if}
<!-- Configured to fire at boot. Called out because the
row is otherwise silent about the most consequential
fact on it: with repair on, this job deletes files
on every restart, not only when someone clicks Run.
Warn-coloured in that case, neutral otherwise. -->
</td>
<!-- "At boot" belongs in the cadence column: it answers
WHEN this job runs, which is the same question
`interval_ms` answers. Beside the name it read as a
property of the job rather than of its schedule, and
these two facts have to be read together — a job with
no interval that fires at boot is not on-demand, and
the row said "on-demand" next to a badge saying
otherwise. -->
<td class="jobs-panel__muted">
{cadenceLabel(job)}
{#if job.startup}
<span
class="jobs-panel__pill"
@@ -886,7 +892,6 @@
</span>
{/if}
</td>
<td class="jobs-panel__muted">{cadenceLabel(job)}</td>
<td class="jobs-panel__muted">{timeAgo(job.last_run_at)}</td>
<td>
<div class="jobs-panel__outcome-cell">