fixes and ui improvements

This commit is contained in:
onysd 2026-09-03 09:37:55 +03:00
parent ec888d3a26
commit 9cf53449fd
11 changed files with 175 additions and 112 deletions

View file

@ -2574,8 +2574,15 @@ FROM photos p
// document/photo, but a purged file with no file_blobs rows left correctly
// contributes 0 to both, never a stale non-zero "ghost" size).
type StorageStatsRow struct {
PhysicalBytes int64 `json:"PhysicalBytes,string"`
LogicalBytes int64 `json:"LogicalBytes,string"`
PhysicalBytes int64 `json:"PhysicalBytes,string"`
LogicalBytes int64 `json:"LogicalBytes,string"`
// UnattributedBytes, DocumentCount, PhotoCount and AccountCount all count
// only items that still own real file_blobs bytes -- documents/photos
// rows themselves are kept forever after a hard-retention purge (so a
// message can still render "here was a file"), so counting rows instead
// of live bytes would keep growing even as the actual content becomes
// physically empty, diverging further and further from PhysicalBytes
// above.
UnattributedBytes int64 `json:"UnattributedBytes,string"`
DocumentCount int64 `json:"DocumentCount,string"`
PhotoCount int64 `json:"PhotoCount,string"`
@ -2597,14 +2604,31 @@ SELECT COALESCE(SUM(size), 0)::bigint FROM (`+perOwnerMediaSizeSQL+`) x`).Scan(&
SELECT COALESCE(SUM(size), 0)::bigint FROM (`+perOwnerMediaSizeSQL+`) x WHERE owner_user_id = 0`).Scan(&stats.UnattributedBytes); err != nil {
return StorageStatsRow{}, fmt.Errorf("sum unattributed media bytes: %w", err)
}
if err := s.pool.QueryRow(ctx, `SELECT count(*)::bigint FROM documents`).Scan(&stats.DocumentCount); err != nil {
// Documents/Photos/AccountCount all count only items that still own real
// file_blobs bytes -- documents/photos rows are deliberately kept forever
// after a hard-retention purge (so a message can still render "here was
// a file"), so a plain count(*) would keep growing even as everything it
// counts becomes physically empty, wildly diverging from PhysicalBytes
// above and making the overview page look broken/confusing rather than
// informative.
if err := s.pool.QueryRow(ctx, `
SELECT count(*)::bigint FROM documents d WHERE EXISTS (
SELECT 1 FROM file_blobs fb
WHERE fb.location_key = 'doc:' || d.id::text
OR fb.location_key LIKE 'doc:' || d.id::text || ':%'
)`).Scan(&stats.DocumentCount); err != nil {
return StorageStatsRow{}, fmt.Errorf("count documents: %w", err)
}
if err := s.pool.QueryRow(ctx, `SELECT count(*)::bigint FROM photos`).Scan(&stats.PhotoCount); err != nil {
if err := s.pool.QueryRow(ctx, `
SELECT count(*)::bigint FROM photos p WHERE EXISTS (
SELECT 1 FROM file_blobs fb
WHERE fb.location_key = 'photo:' || p.id::text
OR fb.location_key LIKE 'photo:' || p.id::text || ':%'
)`).Scan(&stats.PhotoCount); err != nil {
return StorageStatsRow{}, fmt.Errorf("count photos: %w", err)
}
if err := s.pool.QueryRow(ctx, `
SELECT count(DISTINCT owner_user_id)::bigint FROM (`+perOwnerMediaSizeSQL+`) x WHERE owner_user_id <> 0`).Scan(&stats.AccountCount); err != nil {
SELECT count(DISTINCT owner_user_id)::bigint FROM (`+perOwnerMediaSizeSQL+`) x WHERE owner_user_id <> 0 AND size > 0`).Scan(&stats.AccountCount); err != nil {
return StorageStatsRow{}, fmt.Errorf("count storage accounts: %w", err)
}
stats.BackendKind = strings.ToLower(strings.TrimSpace(os.Getenv("TELESRV_BLOB_BACKEND")))
@ -2680,9 +2704,14 @@ func (s *readStore) ListAccountStorageUsage(ctx context.Context, q string, sortB
args = append(args, offset, limit+1)
rows, err := s.pool.Query(ctx, `
WITH totals AS (
-- size > 0 excludes documents/photos whose file_blobs bytes have already
-- been purged -- their row is kept forever (see perOwnerMediaSizeSQL's
-- doc comment) so counting every row here would keep FileCount growing
-- long after Bytes has settled at (or near) 0, same mismatch this query
-- used to have before it was joined through file_blobs at all.
SELECT owner_user_id, SUM(size)::bigint AS bytes, COUNT(*)::bigint AS file_count
FROM (`+perOwnerMediaSizeSQL+`) x
WHERE owner_user_id <> 0
WHERE owner_user_id <> 0 AND size > 0
GROUP BY owner_user_id
)
SELECT t.owner_user_id, COALESCE(u.username, ''), COALESCE(u.first_name, ''), t.bytes, t.file_count

View file

@ -23,8 +23,8 @@
})();
</script>
<script type="module" crossorigin src="/assets/index-CMLbsGwc.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-B7hI8ol7.css">
<script type="module" crossorigin src="/assets/index-NtDKRYU1.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CU8XZPsy.css">
</head>
<body>
<div id="root"></div>

View file

@ -420,6 +420,9 @@ function CategoryRetentionModal({
<p className="env-field-desc">
{"Leave a category at 0 to inherit the shared Retention age. The mode switch still applies to all of them -- these only change how old that one category's media must be."}
</p>
<p className="modal-warn-note">
{"Danger zone: Avatar and GIF are shared buckets, not per-user ones. Avatar also covers the built-in system bots' own profile photos (BotFather, Stickers, ChatBot, VerifyBot, the official system account), and GIF also covers the server's bundled GIF catalog (@gif) -- a short age here purges those right along with ordinary user media."}
</p>
{CATEGORY_AGE_FIELDS.map((field) => (
<DurationField
key={field.key}
@ -488,6 +491,9 @@ function ManualPurgeStorageModal({ onClose }: { onClose: () => void }) {
<p className="env-field-desc">
{"Deletes the file bytes of every document/photo matching the categories below, right now -- independent of the retention mode/age configured above. The message/profile-photo itself is never deleted, only its file; a purged item starts showing as unavailable. Leave \"Created before\" empty to purge everything in the selected categories, regardless of age."}
</p>
<p className="modal-warn-note">
{"Danger zone: Avatar and GIF are shared buckets, not per-user ones. Selecting Avatar also purges the built-in system bots' own profile photos (BotFather, Stickers, ChatBot, VerifyBot, the official system account) -- these are bundled with the server and restore themselves automatically on the next restart. Selecting GIF also purges the server's bundled GIF catalog (@gif) entries, and those do NOT restore themselves: the catalog only re-imports a GIF whose source file is still sitting in TELESRV_GIF_SEED_DIR, so a purge is permanent for any catalog entry whose original file was since moved/deleted."}
</p>
<div className="attr-block">
<label className="checkline">
<input type="checkbox" checked={allSelected} onChange={toggleAll} />

View file

@ -461,6 +461,22 @@
border-radius: var(--radius);
}
/* Warns that an Avatar/GIF retention control also reaches built-in server
resources (system bot avatars, the bundled GIF catalog), not just
ordinary user media -- see StoragePage.tsx's CategoryRetentionModal and
ManualPurgeStorageModal. */
.modal-warn-note {
padding: 9px 12px;
background: var(--warn-tint);
border: 1px solid var(--warn-border);
border-radius: var(--radius);
color: var(--warn);
font-size: 12px;
font-weight: 700;
line-height: 1.4;
margin: 0 0 10px;
}
.secret-reveal-label {
display: flex;
align-items: center;