feat(downloads): per-post body-capture diagnostics in the event UI (#842)
CI / lint (push) Successful in 2s
CI / frontend-build (push) Successful in 21s
CI / backend-lint-and-test (push) Successful in 28s
CI / integration (push) Successful in 3m32s

Operator can't (and shouldn't have to) hunt worker logs to see why a recapture
left a post body empty. Surface per-post handling ON THE EVENT, in the UI.

The feed already requests post_type (in _FIELDS_POST), so ingest_core builds a
per-post diagnostic {post_id, title, post_type, body_chars} with zero extra
fetching — a 0-char body next to its post_type explains an empty post at a
glance (e.g. polls/embeds whose body the API never returns).

- ingest_core: accumulate post_diagnostics; thread via DownloadResult
- download_service: write to DownloadEvent.metadata_['post_diagnostics']
- DownloadDetailModal: 'Post capture' section — totals + empty-body table
  (post_type + chars, flagged) + all-posts table; included in Copy-all
- tests: ingester diag (post_type + body_chars), download_service metadata

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-14 23:09:37 -04:00
parent 3df191e255
commit bcc7266021
6 changed files with 154 additions and 2 deletions
+22 -1
View File
@@ -60,7 +60,21 @@ class _FakeClient:
for page_cursor, posts in self._pages:
for post_id, media in posts:
self.consumed_posts += 1
yield {"id": post_id, "_media": media}, {}, page_cursor
# Carry feed attributes (title/post_type/content) like the real
# client — ingest_core reads these for the per-post diagnostic.
yield (
{
"id": post_id,
"_media": media,
"attributes": {
"title": f"Post {post_id}",
"post_type": "image_file",
"content": f"<p>body {post_id}</p>",
},
},
{},
page_cursor,
)
def extract_media(self, post, included_index):
return post["_media"]
@@ -640,6 +654,13 @@ async def test_backfill_skips_already_captured_post_but_recapture_forces_it(
# Diagnostic summary surfaces the post-record + relink counts (operator reads
# this off the event stdout to see what a recapture actually did).
assert "1 post-record(s), 1 relinked" in res_recap.stdout
# #842: per-post body diagnostic carries post_type + body length so the UI can
# show how each post was handled (and explain empties by post_type).
assert len(res_recap.post_diagnostics) == 1
diag = res_recap.post_diagnostics[0]
assert diag["post_id"] == "p1"
assert diag["post_type"] == "image_file"
assert diag["body_chars"] == len("<p>body p1</p>")
@pytest.mark.asyncio