PatreonClient.post_is_gated — gates only on an explicit current_user_can_view == False (missing/None → viewable, never over-filter).
ingest_corerun() + preview() skip gated posts entirely: no media download, no post-record stub. Skipped before the #862 body canary so they don't inflate its sample; surfaced as N gated-skipped.
A one-shot Settings → Maintenance action ("Clean up gated-post previews") to purge the blurred previews grabbed before the fix. The gated flag was never persisted, so it re-walks each enabled Patreon source (read-only) to re-derive the currently-served blurred filehashes, then matches by content hash against stored source_filehash — real content downloaded when access existed has a different hash and is provably spared. NULL hash → unverifiable, kept + reported. On apply: delete matched files, clear the seen/dead-letter ledger (so real media re-ingests if access returns), remove bare gated posts. Preview/apply share one predicate (rule 93).
Tier-gated Patreon posts (`current_user_can_view == False`) were having their blurred locked-preview media downloaded as if real.
## Ingester fix (#874)
- `PatreonClient.post_is_gated` — gates only on an explicit `current_user_can_view == False` (missing/None → viewable, never over-filter).
- `ingest_core` `run()` + `preview()` skip gated posts entirely: no media download, no post-record stub. Skipped before the #862 body canary so they don't inflate its sample; surfaced as `N gated-skipped`.
## Cleanup tool (#874 follow-up)
A one-shot Settings → Maintenance action ("Clean up gated-post previews") to purge the blurred previews grabbed before the fix. The gated flag was never persisted, so it re-walks each enabled Patreon source (read-only) to re-derive the currently-served blurred filehashes, then matches by **content hash** against stored `source_filehash` — real content downloaded when access existed has a different hash and is provably spared. NULL hash → unverifiable, kept + reported. On apply: delete matched files, clear the seen/dead-letter ledger (so real media re-ingests if access returns), remove bare gated posts. Preview/apply share one predicate (rule 93).
Verified green on dev (CI run 1170, `5401512`).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Patreon serves only blurred locked-preview thumbnails for posts the
authenticated account can't fully view; the native ingester was
downloading those as real media. current_user_can_view was already in
_FIELDS_POST but never read.
Add PatreonClient.post_is_gated (gate ONLY on explicit
current_user_can_view=False; missing/None → viewable, never over-filter)
and skip gated posts at the top of the ingest_core run() and preview()
loops — no media download AND no post-record stub (operator: 'no stub
for gated content'). Skipped before the post-record block so gated posts
never inflate the #862 body canary; surfaced as 'N gated-skipped' in the
run summary. Same gate in preview() for preview/apply parity (rule 93).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The prior commit's Edit orphaned the recapture test's relink/stdout
assertions into the new preview test (F821 res_recap/downloader2/m1) and
the gated-skip test's written_paths check matched 'gated' in the tmp dir
name. Restore the recapture assertions to their function and assert on
the media basename instead.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A one-shot Maintenance action to remove the blurred locked-preview images
the ingester downloaded from tier-gated Patreon posts before #874.
current_user_can_view was never persisted, so the cleanup re-walks each
enabled Patreon source (read-only) to re-derive which posts are gated now
and the blurred filehashes Patreon serves for them, then matches by
CONTENT HASH against stored source_filehash. Because the hash is
content-addressed, a real file downloaded when access existed has a
different hash and can never match — regained-then-lost-access content is
provably spared (operator's hard requirement). NULL source_filehash =>
unverifiable, kept + reported.
On apply: delete matched ImageRecords + files (provenance cascades),
clear seen/dead-letter ledger rows for those hashes so the real media
re-ingests if access returns, and delete gated posts left bare. Shares
one match predicate between preview and apply (rule 93).
- cleanup_service: collect_gated_previews + purge_gated_previews
- tasks.admin: purge_gated_previews_task (async re-walk bridge, timeboxed)
- api.admin: POST /maintenance/purge-gated-previews
- GatedPurgeCard.vue in Settings > Maintenance (preview -> confirm -> apply)
- tests: collect predicate, hash-match delete/spare/unverifiable, ledger
clear, bare-post removal, no-op
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Tier-gated Patreon posts (
current_user_can_view == False) were having their blurred locked-preview media downloaded as if real.Ingester fix (#874)
PatreonClient.post_is_gated— gates only on an explicitcurrent_user_can_view == False(missing/None → viewable, never over-filter).ingest_corerun()+preview()skip gated posts entirely: no media download, no post-record stub. Skipped before the #862 body canary so they don't inflate its sample; surfaced asN gated-skipped.Cleanup tool (#874 follow-up)
A one-shot Settings → Maintenance action ("Clean up gated-post previews") to purge the blurred previews grabbed before the fix. The gated flag was never persisted, so it re-walks each enabled Patreon source (read-only) to re-derive the currently-served blurred filehashes, then matches by content hash against stored
source_filehash— real content downloaded when access existed has a different hash and is provably spared. NULL hash → unverifiable, kept + reported. On apply: delete matched files, clear the seen/dead-letter ledger (so real media re-ingests if access returns), remove bare gated posts. Preview/apply share one predicate (rule 93).Verified green on dev (CI run 1170,
5401512).🤖 Generated with Claude Code