External downloads import IN PLACE, so the post-attach dedup-skip unlink could
delete a file that IS an ImageRecord's backing file — orphaning the record and
404-ing on playback. Two sources of that:
- Two links on the same post (same film from mega + gdrive) emitted the same
filename into one external/<post_id>/ dir; the second overwrote the first.
Stage per-LINK now (external/<post_id>/<link_id>/) so each file keeps its path.
- The duplicate_hash/duplicate_phash branch unlinked `f` unconditionally. Make it
path-safe: only unlink when `f` is NOT the existing record's canonical file.
Plus an operator-triggered orphan-repair maintenance task
(prune_missing_file_records_task) to clean up records already orphaned by the
bug: scans ImageRecords, deletes those whose file is gone (cascade), with an
NFS-stall guard that aborts without deleting if a large sample is mostly missing.
Wired through POST /api/admin/maintenance/prune-missing-files and a
MissingFileRepairCard in the Maintenance panel.
Tests: refetch-same-link keeps the canonical file; orphan repair deletes only
real orphans and aborts on the mostly-missing guard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Text-only Patreon posts (WIP/announcement/poll — the bulk of a creator's feed)
rendered a big empty 'No images attached to this post' placeholder taking half
the card. Render the media column only when the post HAS images; image-less posts
let the title + body span the full width. Removes the now-dead PostEmptyThumbs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>