Skip to content

Commit cd24cba

Browse files
committed
Add rolling-integrity coverage stats to API and dashboard (v2.6.64)
Budgeted integrity runs sweep the library in slices, so no single run report answers whether every file has been verified. /api/stats now returns an integrity block: files checked ever and in the last 30 days, coverage percent, never-checked count, oldest check date, and current bitrot-suspected count. The dashboard gains an Integrity Checked stat card with the full breakdown in its tooltip.
1 parent 77dc6b1 commit cd24cba

7 files changed

Lines changed: 104 additions & 3 deletions

File tree

CHANGELOG.MD

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0).
77

8+
## [2.6.64] - 2026-07-09
9+
10+
### Added
11+
12+
- **Rolling-integrity coverage is now visible.** Because budgeted integrity runs sweep the library in slices, no single run report answers "has every file been verified?". `/api/stats` now returns an `integrity` block - files checked ever and within the last 30 days, percent of library covered, never-checked count, the oldest check date (every checked file has been verified at least once since then), and the current bitrot-suspected count. The dashboard gains an "Integrity Checked" stat card showing the coverage percentage, with the full breakdown in its tooltip.
13+
814
## [2.6.63] - 2026-07-09
915

1016
### Fixed

pixelprobe/api/stats_routes.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ def get_stats():
5151
'marked_as_good': stats[7] or 0,
5252
'warning_files': stats[8] or 0
5353
}
54+
# Rolling-integrity coverage: answers "how much of the library has
55+
# been verified over time" - no single budgeted run report can
56+
result['integrity'] = StatsService().get_integrity_coverage()
5457

5558
return result
5659

@@ -104,7 +107,8 @@ def get_stats():
104107
'corrupted_files': corrupted_files,
105108
'healthy_files': healthy_files,
106109
'marked_as_good': marked_as_good,
107-
'warning_files': warning_files
110+
'warning_files': warning_files,
111+
'integrity': StatsService().get_integrity_coverage()
108112
}
109113
except Exception as e2:
110114
logger.error(f"Fallback stats query also failed: {str(e2)}")

pixelprobe/services/stats_service.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import os
66
import logging
77
from typing import Dict, List
8-
from datetime import datetime, timedelta
8+
from datetime import datetime, timedelta, timezone
99
from sqlalchemy import text, func
1010

1111
from pixelprobe.models import db, ScanResult, ScanReport
@@ -57,6 +57,49 @@ def get_file_statistics(self) -> Dict:
5757
# Fallback to individual queries
5858
return self._get_stats_fallback()
5959

60+
def get_integrity_coverage(self) -> Dict:
61+
"""Cumulative rolling-integrity coverage.
62+
63+
The integrity check sweeps the library stalest-first in budgeted
64+
slices, so no single run report answers "has every file been
65+
verified?". Coverage counts files by last_integrity_check_date:
66+
checked ever and within the last 30 days, plus the oldest check date
67+
(every checked file has been verified at least once since then) and
68+
how many files have never been checked.
69+
"""
70+
try:
71+
cutoff = datetime.now(timezone.utc) - timedelta(days=30)
72+
row = db.session.execute(
73+
text("""
74+
SELECT
75+
COUNT(*) as total_files,
76+
SUM(CASE WHEN last_integrity_check_date IS NOT NULL THEN 1 ELSE 0 END) as checked_files,
77+
SUM(CASE WHEN last_integrity_check_date >= :cutoff THEN 1 ELSE 0 END) as checked_last_30_days,
78+
MIN(last_integrity_check_date) as oldest_check,
79+
SUM(CASE WHEN bitrot_suspected = TRUE THEN 1 ELSE 0 END) as bitrot_suspected
80+
FROM scan_results
81+
"""),
82+
{'cutoff': cutoff}
83+
).fetchone()
84+
85+
total = row[0] or 0
86+
checked = row[1] or 0
87+
oldest = row[3]
88+
if oldest is not None and hasattr(oldest, 'isoformat'):
89+
oldest = oldest.isoformat()
90+
return {
91+
'total_files': total,
92+
'checked_files': checked,
93+
'checked_percent': round(checked / total * 100, 1) if total else 0.0,
94+
'checked_last_30_days': row[2] or 0,
95+
'never_checked': total - checked,
96+
'oldest_check_date': oldest,
97+
'bitrot_suspected': row[4] or 0,
98+
}
99+
except Exception as e:
100+
logger.error(f"Error getting integrity coverage: {e}")
101+
return {}
102+
60103
def get_system_info(self) -> Dict:
61104
"""Get comprehensive system information"""
62105
try:

pixelprobe/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# Default version - this is the single source of truth
55

66

7-
_DEFAULT_VERSION = '2.6.63'
7+
_DEFAULT_VERSION = '2.6.64'
88

99

1010
# Allow override via environment variable for CI/CD, but default to the hardcoded version

static/js/app.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,25 @@ class StatsDashboard {
360360
this.updateStatCard('warning-files', stats.warning_files || 0);
361361
this.updateStatCard('pending-files', stats.pending_files);
362362
this.updateStatCard('scanning-files', stats.scanning_files);
363+
this.renderIntegrityCoverage(stats.integrity);
364+
}
365+
366+
renderIntegrityCoverage(integrity) {
367+
const element = document.querySelector('#integrity-checked');
368+
if (!element || !integrity || !integrity.total_files) return;
369+
element.textContent = `${integrity.checked_percent}%`;
370+
let title = `${integrity.checked_files.toLocaleString()} of ` +
371+
`${integrity.total_files.toLocaleString()} files integrity-checked`;
372+
if (integrity.never_checked > 0) {
373+
title += `; ${integrity.never_checked.toLocaleString()} never checked`;
374+
}
375+
if (integrity.oldest_check_date) {
376+
title += `; every checked file verified since ${new Date(integrity.oldest_check_date).toLocaleString()}`;
377+
}
378+
if (integrity.bitrot_suspected > 0) {
379+
title += `; ${integrity.bitrot_suspected.toLocaleString()} bitrot suspected`;
380+
}
381+
element.title = title;
363382
}
364383

365384
updateStatCard(id, value) {

templates/index.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,10 @@ <h3 class="progress-title">Scan Progress</h3>
193193
<div class="stat-value" id="pending-files">0</div>
194194
<div class="stat-label">Pending Files</div>
195195
</div>
196+
<div class="stat-card" role="group" aria-label="Integrity coverage">
197+
<div class="stat-value" id="integrity-checked" aria-live="polite">-</div>
198+
<div class="stat-label">Integrity Checked</div>
199+
</div>
196200
</div>
197201

198202
<!-- Results Section -->

tests/integration/test_bitrot_endpoints.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,31 @@ def test_filter_false_excludes_flagged(self, authenticated_client, app, db):
5353
assert paths == ['/m/clean.mkv']
5454

5555

56+
class TestIntegrityCoverageStats:
57+
58+
def test_stats_include_rolling_coverage(self, authenticated_client, app, db):
59+
with app.app_context():
60+
now = datetime.now(timezone.utc).replace(tzinfo=None)
61+
seed(db, '/m/checked_recent.mkv', last_integrity_check_date=now)
62+
seed(db, '/m/checked_old.mkv',
63+
last_integrity_check_date=datetime(2025, 1, 1, 12, 0, 0))
64+
seed(db, '/m/never.mkv')
65+
seed(db, '/m/rotten.mkv', bitrot_suspected=True,
66+
last_integrity_check_date=now)
67+
68+
response = authenticated_client.get('/api/stats')
69+
assert response.status_code == 200
70+
integrity = response.get_json()['integrity']
71+
72+
assert integrity['total_files'] == 4
73+
assert integrity['checked_files'] == 3
74+
assert integrity['checked_percent'] == 75.0
75+
assert integrity['never_checked'] == 1
76+
assert integrity['checked_last_30_days'] == 2
77+
assert integrity['bitrot_suspected'] == 1
78+
assert integrity['oldest_check_date'].startswith('2025-01-01')
79+
80+
5681
class TestBitrotAccept:
5782

5883
def test_accept_adopts_candidate_and_clears_flag(self, authenticated_client, app, db):

0 commit comments

Comments
 (0)