1
Fork 0
mirror of https://github.com/thegeneralist01/archivr synced 2026-07-21 18:55:36 +02:00

fix(core+frontend): exclude avatar blobs from tweet % cached display

tweet/tweet_thread entries have avatar artifacts that are always
deduplicated from the first capture of each author. Counting them in
the cached-bytes percentage makes it artificially high (or incorrect
when the real media content is new but avatars are cached).

database.rs:
- refresh_entry_cached_bytes: AND ea.artifact_role != 'avatar'
- cascade_cached_bytes_after_delete: same filter
- cascade_cached_bytes_after_subtree_delete: same filter
- Initial cached_bytes migration: same filter
- Re-migration (else branch): recomputes cached_bytes for existing
  entries that have avatar artifacts, scoped to only those entries

archive.rs:
- EntrySummary gains cacheable_bytes: i64 — non-avatar total bytes,
  computed inline in every SQL query as the denominator for % cached
- ENTRY_SELECT_COLS adds cacheable_bytes at index 13 (with children
  subquery, same as total_artifact_bytes)
- list_root_entries adds same expression
- All 6 row-mapping closures include cacheable_bytes: row.get(13)?

EntryRow.jsx:
- SIZE display stays: formatBytes(entry.total_artifact_bytes)
- % cached badge uses cacheable_bytes as denominator:
  cached_bytes / cacheable_bytes * 100
This commit is contained in:
TheGeneralist 2026-07-21 15:30:42 +02:00
parent 916146b4a8
commit b8cec96256
Signed by: thegeneralist01
SSH key fingerprint: SHA256:pp9qddbCNmVNoSjevdvQvM5z0DHN7LTa8qBMbcMq/R4
5 changed files with 49 additions and 7 deletions

View file

@ -32,6 +32,8 @@ pub struct EntrySummary {
pub cached_bytes: i64,
/// Number of direct child entries; 0 for non-container entries.
pub child_count: i64,
/// Total non-avatar artifact bytes (query-time; used as denominator for cache-hit %).
pub cacheable_bytes: i64,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
@ -218,7 +220,8 @@ pub fn list_root_entries(
NULL AS parent_entry_uid,
EXISTS(SELECT 1 FROM entry_artifacts fav WHERE fav.entry_id = e.id AND fav.artifact_role = 'favicon') AS has_favicon,
e.cached_bytes,
(SELECT COUNT(*) FROM archived_entries child WHERE child.parent_entry_id = e.id) AS child_count
(SELECT COUNT(*) FROM archived_entries child WHERE child.parent_entry_id = e.id) AS child_count,
COALESCE(SUM(CASE WHEN ea.artifact_role != 'avatar' THEN b.byte_size ELSE 0 END), 0) + COALESCE((SELECT SUM(b2.byte_size) FROM archived_entries c2 JOIN entry_artifacts ea2 ON ea2.entry_id = c2.id JOIN blobs b2 ON b2.id = ea2.blob_id WHERE c2.parent_entry_id = e.id), 0) AS cacheable_bytes
FROM archived_entries e
JOIN source_identities si ON si.id = e.source_identity_id
LEFT JOIN entry_artifacts ea ON ea.entry_id = e.id
@ -252,6 +255,7 @@ pub fn list_root_entries(
has_favicon: row.get::<_, i64>(10)? != 0,
cached_bytes: row.get(11)?,
child_count: row.get(12)?,
cacheable_bytes: row.get(13)?,
})
})?
.collect::<rusqlite::Result<Vec<_>>>()?;
@ -286,6 +290,7 @@ fn get_entry_summary(
has_favicon: row.get::<_, i64>(10)? != 0,
cached_bytes: row.get(11)?,
child_count: row.get(12)?,
cacheable_bytes: row.get(13)?,
})
})
.optional()?;
@ -474,6 +479,7 @@ pub fn list_entries_for_collection(
has_favicon: row.get::<_, i64>(10)? != 0,
cached_bytes: row.get(11)?,
child_count: row.get(12)?,
cacheable_bytes: row.get(13)?,
})
})?
.collect::<rusqlite::Result<Vec<_>>>()?;
@ -522,6 +528,7 @@ pub fn list_child_entries(
has_favicon: row.get::<_, i64>(10)? != 0,
cached_bytes: row.get(11)?,
child_count: row.get(12)?,
cacheable_bytes: row.get(13)?,
})
},
)?
@ -693,7 +700,8 @@ const ENTRY_SELECT_COLS: &str = "SELECT e.entry_uid, e.archived_at, e.source_kin
parent.entry_uid AS parent_entry_uid, \
EXISTS(SELECT 1 FROM entry_artifacts fav WHERE fav.entry_id = e.id AND fav.artifact_role = 'favicon') AS has_favicon, \
e.cached_bytes, \
(SELECT COUNT(*) FROM archived_entries child WHERE child.parent_entry_id = e.id) AS child_count";
(SELECT COUNT(*) FROM archived_entries child WHERE child.parent_entry_id = e.id) AS child_count, \
COALESCE(SUM(CASE WHEN ea.artifact_role != 'avatar' THEN b.byte_size ELSE 0 END), 0) + COALESCE((SELECT SUM(b2.byte_size) FROM archived_entries c2 JOIN entry_artifacts ea2 ON ea2.entry_id = c2.id JOIN blobs b2 ON b2.id = ea2.blob_id WHERE c2.parent_entry_id = e.id), 0) AS cacheable_bytes";
const ENTRY_FROM_JOINS: &str = "FROM archived_entries e \
JOIN source_identities si ON si.id = e.source_identity_id \
@ -802,6 +810,7 @@ pub fn search_entries(
has_favicon: row.get::<_, i64>(10)? != 0,
cached_bytes: row.get(11)?,
child_count: row.get(12)?,
cacheable_bytes: row.get(13)?,
})
})?
.collect::<rusqlite::Result<Vec<_>>>()?;
@ -1010,7 +1019,8 @@ pub fn entries_for_tag(
parent.entry_uid AS parent_entry_uid,
EXISTS(SELECT 1 FROM entry_artifacts fav WHERE fav.entry_id = e.id AND fav.artifact_role = 'favicon') AS has_favicon,
e.cached_bytes,
(SELECT COUNT(*) FROM archived_entries child WHERE child.parent_entry_id = e.id) AS child_count
(SELECT COUNT(*) FROM archived_entries child WHERE child.parent_entry_id = e.id) AS child_count,
COALESCE(SUM(CASE WHEN ea.artifact_role != 'avatar' THEN b.byte_size ELSE 0 END), 0) AS cacheable_bytes
FROM archived_entries e
JOIN source_identities si ON si.id = e.source_identity_id
LEFT JOIN entry_artifacts ea ON ea.entry_id = e.id
@ -1037,6 +1047,7 @@ pub fn entries_for_tag(
has_favicon: row.get::<_, i64>(10)? != 0,
cached_bytes: row.get(11)?,
child_count: row.get(12)?,
cacheable_bytes: row.get(13)?,
})
})?
.collect::<rusqlite::Result<Vec<_>>>()?;

View file

@ -390,6 +390,7 @@ pub fn initialize_schema(conn: &Connection) -> Result<()> {
JOIN blobs b ON b.id = ea.blob_id
WHERE ea.entry_id = archived_entries.id
AND ea.blob_id IS NOT NULL
AND ea.artifact_role != 'avatar'
AND EXISTS (
SELECT 1
FROM entry_artifacts ea2
@ -401,6 +402,33 @@ pub fn initialize_schema(conn: &Connection) -> Result<()> {
)
);",
)?;
} else {
// Re-migration: strip avatar blobs from cached_bytes on entries that
// already had the column populated before this filter was introduced.
// Scoped to entries with avatar artifacts only; fast and idempotent.
conn.execute_batch(
"UPDATE archived_entries
SET cached_bytes = (
SELECT COALESCE(SUM(b.byte_size), 0)
FROM entry_artifacts ea
JOIN blobs b ON b.id = ea.blob_id
WHERE ea.entry_id = archived_entries.id
AND ea.blob_id IS NOT NULL
AND ea.artifact_role != 'avatar'
AND EXISTS (
SELECT 1
FROM entry_artifacts ea2
JOIN archived_entries e2 ON e2.id = ea2.entry_id
WHERE ea2.blob_id = ea.blob_id
AND (e2.archived_at < archived_entries.archived_at
OR (e2.archived_at = archived_entries.archived_at
AND e2.id < archived_entries.id))
)
)
WHERE id IN (
SELECT DISTINCT entry_id FROM entry_artifacts WHERE artifact_role = 'avatar'
);",
)?;
}
// Migration: add notes_json column to existing capture_jobs tables.
@ -1482,6 +1510,7 @@ pub fn refresh_entry_cached_bytes(conn: &Connection, entry_id: i64) -> Result<()
JOIN archived_entries e ON e.id = ea.entry_id
WHERE ea.entry_id = ?1
AND ea.blob_id IS NOT NULL
AND ea.artifact_role != 'avatar'
AND EXISTS (
SELECT 1
FROM entry_artifacts ea2
@ -1518,6 +1547,7 @@ pub fn cascade_cached_bytes_after_delete(conn: &Connection, entry_id: i64) -> Re
JOIN blobs b ON b.id = ea.blob_id
WHERE ea.entry_id = archived_entries.id
AND ea.blob_id IS NOT NULL
AND ea.artifact_role != 'avatar'
AND EXISTS (
SELECT 1
FROM entry_artifacts ea3
@ -1570,6 +1600,7 @@ fn cascade_cached_bytes_after_subtree_delete(conn: &Connection, subtree_ids: &[i
JOIN blobs b ON b.id = ea.blob_id
WHERE ea.entry_id = archived_entries.id
AND ea.blob_id IS NOT NULL
AND ea.artifact_role != 'avatar'
AND EXISTS (
SELECT 1
FROM entry_artifacts ea3

View file

@ -4,7 +4,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Archivr</title>
<script type="module" crossorigin src="/assets/index-BlDAGZHO.js"></script>
<script type="module" crossorigin src="/assets/index-C9dla3pB.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D8ic-z4p.css">
</head>
<body>

View file

@ -143,9 +143,9 @@ export default function EntryRow({ entry, archiveId, rowIndex, isSelected, isMul
</div>
<div className="col-size">
<span className="size-total">{formatBytes(entry.total_artifact_bytes)}</span>
{entry.cached_bytes > 0 && entry.total_artifact_bytes > 0 && (
{entry.cached_bytes > 0 && entry.cacheable_bytes > 0 && (
<span className="size-cached-pct" title={`${formatBytes(entry.cached_bytes)} already on disk from an earlier entry`}>
{Math.round(entry.cached_bytes / entry.total_artifact_bytes * 100)}% cached
{Math.round(entry.cached_bytes / entry.cacheable_bytes * 100)}% cached
</span>
)}
</div>