Skip to content

feat(avatars): keep every profile photo each account saw and show earlier ones - #479

Merged
GeiserX merged 3 commits into
mainfrom
ai/avatar-history
Sep 24, 2026
Merged

GeiserX merged 3 commits into
mainfrom
ai/avatar-history

Conversation

@GeiserX

@GeiserX GeiserX commented Sep 24, 2026

Copy link
Copy Markdown
Owner

The archive kept every avatar file but only one pointer per account and chat (chats.avatar_photo_id), and the backup overwrote it on every change. So it forgot which photos an account had seen before, and when the pointer was empty the viewer could not tell "this account saw the photo removed" from "never recorded", and served another account's newest file for both.

Fix

  • Migration 031 adds avatar_history (account, chat, photo id or NULL, seen_at) with one index on (account_id, chat_id, seen_at). It is idempotent both ways, adds no stamping rung, and the AvatarHistory model matches it (schema parity is green on SQLite and on PostgreSQL 17).
  • upsert_chat appends a row, in the same transaction, whenever avatar_photo_id is in the payload and differs from the value stored for that account and chat. A missing chat row counts as stored None. There is no unique constraint: 1111, then 2222, then 1111 again is three rows, and a removal is a NULL row. The listener's partial upserts (no key) write nothing. The insert runs in a savepoint like _record_message_version, so a failure there never loses the chat upsert.
  • DatabaseAdapter.get_avatar_history(chat_id, account_id=) returns the rows newest first (seen_at, then id).
  • GET /api/chats/{ref}/avatars lists them as photo_id, seen_at, url (/media/avatar/{ref}?photo_id=N, None for a removal) and available (file on disk). It resolves through require_chat, so it has the same scoping as the other chat routes, and it never returns a chat id.
  • /media/avatar/{ref}?photo_id=N serves that exact file only when this account recorded N for this chat (the current id or a history row), and 404s otherwise, with no newest-file fallback. It does not read or write the 5-minute path cache, so it cannot change the default answer.
  • Without photo_id: if the recorded id is None and the newest history row is a removal, the route answers 404. With no history rows at all it keeps the newest-file fallback. The sender avatar route (/media/avatar/{ref}/{message_id}) applies the same removal rule, since it reads the same pointer.
  • The info panel shows a "Previous photos" row of round thumbnails when the history has at least one photo on disk other than the current one. Each photo shows once. Clicking a thumbnail opens it in the existing lightbox; the lightbox hides its download link for these, because the avatar route has no ?download=1.
  • CLAUDE.md: avatar_photo_id is no longer listed as known debt; the archive principle now names avatar_history.

What it deletes / overwrites / forgets

  • Deletes: nothing. The downgrade drops avatar_history, as every downgrade drops what its upgrade added.
  • Overwrites: nothing new. chats.avatar_photo_id is still overwritten in place, but every value it had is now kept in avatar_history first.
  • Forgets: sightings from before this migration beyond the one currently recorded. The upgrade seeds one row per chat that has a recorded photo and no history yet, dated at the chat row's updated_at, which is when that photo was last confirmed, not when it was first seen. A photo that changed before 029 or before this upgrade was never recorded and cannot be recovered.

Decisions to push back on

  • The seed in 031 is not in the original brief. Without it, the photo recorded before the upgrade would be missing from the history the first time it changes, so the viewer could not show or serve it. It only reads chats and skips chats that already have history, so re-runs and create_all() databases stay safe.
  • The sender route got the removal 404 too. It reads the same pointer, so leaving it out would keep the old answer (another account's newest file) for group message avatars. Drop that hunk if you want this PR limited to the chat route.
  • delete_chat_and_related_data does not remove avatar_history rows. Chat exclusion leaves avatar files on disk too, so it now leaves their history as well. If exclusion should purge this table, that is a one-line delete plus a test.
  • The stored value is read with SELECT ... FOR UPDATE before the upsert. On PostgreSQL this makes two writers changing the same chat's photo at once wait for each other. SQLite ignores it and serializes writers anyway. Duplicate rows would be harmless, since nothing is unique.

Tests: tests/test_avatar_history.py covers the migration, the write and read paths on both backends, the API, both avatar routes and the panel row (run under node). Each test was checked with a one-line mutant that turned it red and went green again once the line was restored.

…lier ones

chats.avatar_photo_id was one overwritten pointer, so the archive forgot every
earlier photo and could not tell a seen removal from never recorded.
Migration 031 adds the append-only avatar_history table; upsert_chat appends a
row whenever the recorded id changes (a removal is a NULL row). The viewer
lists the history at /api/chats/{ref}/avatars, serves an earlier photo with
?photo_id= only when this account recorded it, answers a seen removal with a
404 instead of another account's newest file, and shows a Previous photos row
in the info panel.
@coderabbitai

coderabbitai Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 14 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 2 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository: GeiserX/Telegram-Archive/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 307cf2d9-69d7-4e56-9ff4-366615353cec

📥 Commits

Reviewing files that changed from the base of the PR and between ae0fbc0 and 25690df.

⛔ Files ignored due to path filters (1)
  • alembic/versions/20260924_031_add_avatar_history.py is excluded by !alembic/versions/**
📒 Files selected for processing (6)
  • CLAUDE.md
  • src/db/adapter.py
  • src/db/models.py
  • src/web/main.py
  • src/web/templates/index.html
  • tests/test_avatar_history.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread src/web/main.py Dismissed
@github-actions

Copy link
Copy Markdown

🐳 Dev images published!

  • drumsergio/telegram-archive:dev
  • drumsergio/telegram-archive-viewer:dev

The dev/test instance will pick up these changes automatically (Portainer GitOps).

To test locally:

docker pull drumsergio/telegram-archive:dev
docker pull drumsergio/telegram-archive-viewer:dev

@codecov

codecov Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.04110% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.94%. Comparing base (ae0fbc0) to head (25690df).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
src/web/main.py 88.09% 5 Missing ⚠️
src/db/adapter.py 86.95% 3 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #479      +/-   ##
==========================================
- Coverage   94.96%   94.94%   -0.03%     
==========================================
  Files          29       29              
  Lines       12318    12396      +78     
==========================================
+ Hits        11698    11769      +71     
- Misses        620      627       +7     
Files with missing lines Coverage Δ
src/db/models.py 100.00% <100.00%> (ø)
src/db/adapter.py 96.11% <86.95%> (-0.11%) ⬇️
src/web/main.py 92.25% <88.09%> (-0.10%) ⬇️

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

Copy link
Copy Markdown

🐳 Dev images published!

  • drumsergio/telegram-archive:dev
  • drumsergio/telegram-archive-viewer:dev

The dev/test instance will pick up these changes automatically (Portainer GitOps).

To test locally:

docker pull drumsergio/telegram-archive:dev
docker pull drumsergio/telegram-archive-viewer:dev

@GeiserX
GeiserX merged commit b98994c into main Sep 24, 2026
11 checks passed
@GeiserX
GeiserX deleted the ai/avatar-history branch September 24, 2026 10:01
@GeiserX GeiserX mentioned this pull request Sep 24, 2026
PhenixStar pushed a commit to PhenixStar/Telegram-Archive that referenced this pull request Sep 25, 2026
…otos

Semantic port of upstream GeiserX#469/GeiserX#479/GeiserX#481 for one account per archive (no
account dimension; each account's stack has its own avatars).

- chats.avatar_photo_id (migration 018) records the photo currently seen.
  The viewer serves that file instead of whichever avatar file is newest,
  falls back to the newest file when nothing is recorded or the file has
  not landed yet (not cached, so it shows as soon as it downloads), and a
  recorded removal shows no avatar.
- avatar_history (018) is append-only: upsert_chat adds a row whenever the
  recorded photo changes, in a SAVEPOINT so a failure never aborts the chat
  update; a removal is a NULL photo id. "min" entities, which omit the
  photo, record nothing.
- GET /api/chats/{id}/avatars (scoped like the chat): the current photo
  and every previous one on disk, dated by history where sighted. The Chat
  Info panel shows the real photo and a "Previous photos" strip. The chat
  list resolves removals with one history query per page, and the single
  chat endpoint now carries avatar_url too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants