Skip to content

fix(viewer): show restricted viewers their own media and storage stats - #473

Merged
GeiserX merged 2 commits into
GeiserX:mainfrom
jordanfelle:fix/per-account-media-stats
Sep 23, 2026
Merged

GeiserX merged 2 commits into
GeiserX:mainfrom
jordanfelle:fix/per-account-media-stats

Conversation

@jordanfelle

@jordanfelle jordanfelle commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • A viewer restricted to some accounts or chats got media_files and total_size_mb removed from /api/stats, because the cached statistics only had archive-wide media totals. The stats popup then showed those rows as 0 files / 0 MiB.
  • The statistics job now also stores downloaded media count and bytes per (account, chat), under the same "<account>:<chat>" keys as the per-chat message counts. /api/stats sums them over the principal's visible chats, through the same fail-closed reader (_scoped_message_counts, renamed _scoped_chat_counts since it now serves three maps). The UI hides the two rows when the server omits them, instead of rendering a false zero.

Where

  • src/db/adapter.py: calculate_and_store_statistics runs one more grouped query (count, coalesce(sum(file_size), 0) over downloaded = 1, grouped by account_id, chat_id) and adds per_account_chat_media_counts / per_account_chat_media_bytes to the blob. Archive-wide totals are unchanged.
  • src/web/main.py: /api/stats pops both maps, like the message map, and for a restricted principal fills media_files / total_size_mb from them. /api/stats/refresh pops them too (their keys are chat ids).
  • src/web/templates/index.html: media_files / total_size_mb become ?? null rather than || 0, and each row has v-if="... != null". A real zero still shows.

Semantics worth reviewing

  • Restricted storage is logical, not on-disk. The archive-wide figure uses du on the backup path. Per-chat on-disk usage isn't defined once DEDUPLICATE_MEDIA symlinks rows into _shared/, so the restricted figure is SUM(media.file_size) of the principal's downloaded rows. A blob shared by two visible chats counts twice. This is the same value the archive-wide stat already falls back to when the path is unmounted.
  • Upgrade path fails closed. A blob written before this change has no media maps, so a restricted principal keeps today's behaviour (keys omitted), and the rows are now hidden rather than zero until the next daily calculation or POST /api/stats/refresh. The archive-wide figures never stand in for a restricted principal's own.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)

Database Changes

  • No database changes (the cached metadata.cached_stats JSON blob gains two keys)

Data Consistency Checklist

  • All chat_id values use marked format (read back from media.chat_id, already marked)
  • All datetime values pass through _strip_tz() before DB operations (no new datetimes)
  • INSERT and UPDATE operations handle the same fields identically (no writes to data tables)

Testing

  • tests/test_stats_account_scoped.py: 63 passed on SQLite and PostgreSQL 18 (real_adapter). New coverage: media maps grouped by (account, chat) with a never-downloaded row excluded; per-account and per-ref-grant media totals; empty grant reads zero; a pre-change blob omits the media keys; neither map reaches the /api/stats or /api/stats/refresh response.
  • tests/test_db_adapter.py::TestCalculateAndStoreStatistics updated for the extra execute call.
  • Full suite: 4263 passed, 218 skipped. The only failures are the two tests/test_frontend_bootstrap.py tests that need a node binary. They fail the same way on unmodified main in the same container.
  • ruff check . / ruff format --check . clean.
  • Manually tested in development environment. The bug was reproduced on a two-account deployment (an account-restricted viewer saw 0 media / 0 MiB), but the fix has not been deployed there yet. It is covered by the route tests above.

Security Checklist

  • No secrets or credentials committed
  • User input properly validated/sanitized (no new input; map values go through the existing int/bool guard)
  • Authentication/authorization properly checked (scoping uses the existing _visible_chat_pair_set; the new maps never leave the server)

Deployment Notes

  • Viewer + backup image rebuild. Restricted viewers see their media figures after the next statistics calculation.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HBKaAtgJ6LTpJMrLCk98WM

Summary by CodeRabbit

  • Bug Fixes
    • Media-file counts and storage totals in statistics now reflect only the chats available to restricted users.
    • The stats popup omits media and storage rows when those figures are unavailable, rather than displaying misleading zero values.

A viewer restricted to some accounts or chats had media_files and
total_size_mb removed from /api/stats, because the cached statistics only
held archive-wide media totals. The stats popup then rendered the missing
fields as 0 files / 0 MiB.

The statistics job now also stores downloaded media count and bytes per
(account, chat), under the same keys as the per-chat message counts, and
/api/stats sums them over the principal's visible chats. Bytes are the
logical DB file sizes, since on-disk usage cannot be split per chat once
media is deduplicated. A blob calculated before this change has no media
maps, so the figures stay omitted for restricted principals until the next
calculation, and the UI now hides those rows instead of showing a zero.
Both maps are stripped from /api/stats and /api/stats/refresh responses,
like the message map, since their keys are chat ids.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBKaAtgJ6LTpJMrLCk98WM
@coderabbitai

coderabbitai Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

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

Review profile: CHILL

Plan: Advanced

Run ID: 6464ad29-2f82-438e-89ad-2aa85bbf8296

📥 Commits

Reviewing files that changed from the base of the PR and between 3ad5efd and 3e33066.

📒 Files selected for processing (2)
  • tests/test_frontend_bootstrap.py
  • tests/test_stats_account_scoped.py

Included review availability: Your plan provides up to 2 included reviews per hour; 1 remains after this review.


📝 Walkthrough

Walkthrough

The statistics cache now stores downloaded-media counts and byte totals by account and chat. Restricted responses scope these values to entitled chats. The stats popup hides media and storage rows when their values are unavailable.

Changes

Account-scoped media statistics

Layer / File(s) Summary
Per-chat media statistics
src/db/adapter.py, tests/test_db_adapter.py, tests/test_stats_account_scoped.py
The statistics calculation stores downloaded-media counts and byte totals by account and chat. Tests cover the maps, downloaded-row filtering, and null file sizes.
Restricted statistics responses
src/web/main.py, tests/test_stats_account_scoped.py
Restricted responses sum media counts and bytes for entitled chats. If either map is unavailable or invalid, the response omits both media fields. The per-chat maps are removed from stats and refresh responses.
Popup handling of unavailable media statistics
src/web/templates/index.html, tests/test_frontend_bootstrap.py
The popup preserves null media and storage values and hides the corresponding rows when values are null. Tests cover missing and zero values.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to 3e330

The scoped media statistics and unavailable-value display are ready for normal merge checks; no actionable merge-blocking issue was established.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.06% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 31 functions across 5 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: restricted viewers can see their own media and storage statistics.
Description check ✅ Passed The description follows the required template and covers the summary, change type, database impact, consistency checklist, testing results, security, and deployment notes. It also clearly documents th…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR

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.

@codecov

codecov Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.95%. Comparing base (c537a2c) to head (3e33066).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #473      +/-   ##
==========================================
- Coverage   95.06%   94.95%   -0.11%     
==========================================
  Files          29       29              
  Lines       12252    12318      +66     
==========================================
+ Hits        11647    11697      +50     
- Misses        605      621      +16     
Files with missing lines Coverage Δ
src/db/adapter.py 96.22% <100.00%> (+0.07%) ⬆️
src/web/main.py 92.30% <100.00%> (-0.02%) ⬇️

... and 8 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.

…e account half of the key

Three gaps the review's mutants walked through: a template that renders
0 for an absent media figure (the bug this fixes) stayed green, a grouped
SUM without the coalesce aborted the statistics job on a NULL file_size
with no test noticing, and the test named for the colliding chat id
never held downloaded media on the second account's copy. Each new test
goes red on its mutant and green on the fix.
@GeiserX
GeiserX merged commit 214874f into GeiserX:main Sep 23, 2026
11 checks passed
@GeiserX

GeiserX commented Sep 23, 2026

Copy link
Copy Markdown
Owner

Thanks for this. The per-(account, chat) maps reuse the existing key format and the fail-closed reader, the new maps never reach the browser, and the pre-change blob case hides the rows instead of guessing. The suite ran on SQLite and PostgreSQL, and mutations of the scoping, the key builder and the response filtering were all caught by your tests.

Merged with three small test additions stacked on top: a node test on loadStats for the omitted-key and real-zero cases plus a static check of the two v-if guards, a NULL file_size row to pin the coalesce, and a downloaded row on account 2's copy of the colliding chat id so that test exercises the account half of the key. Two nits are tracked as follow-ups: totals under half a MiB render as "0 MiB" (pre-existing formatter), and the job now scans media three times where one grouped scan would do. It ships with the next release.

@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
…per-chat maps

Semantic port of upstream GeiserX#473 and GeiserX#476 for our one-account-per-archive
layout (maps keyed by chat id, not account:chat).

/api/stats returned the cached blob as is, so every login - including
share tokens and viewer accounts restricted to a few chats - received the
archive-wide totals and per_chat_message_counts, the id and message count
of every archived chat.

- The per-chat maps (messages, and now downloaded media count and bytes)
  are scoping input only: stripped from /api/stats and /api/stats/refresh.
- A restricted viewer's chats, messages, media and storage are summed over
  its visible chats (fail closed). A blob written before the media maps
  existed omits the two media figures, and the popup hides those rows
  rather than showing 0 or archive-wide numbers.
- The stats job reads downloaded media in one grouped scan instead of
  three; totals still count rows without a chat id.
- formatSize shows "<1 MiB" instead of "0 MiB" for tiny totals.

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