Merge pull request #245 from christianlouis/copilot/fix-dashboard-status-metrics

Fix file status and metrics by tracking latest state per step
This commit is contained in:
Christian Krakau-Louis
2026-02-11 20:14:49 +01:00
committed by GitHub
3 changed files with 277 additions and 14 deletions
+12 -5
View File
@@ -76,13 +76,20 @@ def _compute_status_from_logs(logs: List[ProcessingLog]) -> Dict:
if not logs:
return {"status": "pending", "last_step": None, "has_errors": False, "total_steps": 0}
# Check for failures
has_errors = any(log.status == "failure" for log in logs)
# Get the latest status for each unique step
# Since logs are ordered by timestamp desc, the first occurrence is the latest
latest_by_step = {}
for log in logs:
if log.step_name not in latest_by_step:
latest_by_step[log.step_name] = log
# Check if any in progress
in_progress = any(log.status == "in_progress" for log in logs)
# Check for failures in latest statuses
has_errors = any(log.status == "failure" for log in latest_by_step.values())
# Get the latest log
# Check if any step is currently in progress (based on latest status per step)
in_progress = any(log.status == "in_progress" for log in latest_by_step.values())
# Get the overall latest log
latest_log = logs[0]
# Determine overall status
+14 -9
View File
@@ -347,8 +347,8 @@ def _compute_step_summary(logs):
main_counts = {"queued": 0, "in_progress": 0, "success": 0, "failure": 0}
upload_counts = {"queued": 0, "in_progress": 0, "success": 0, "failure": 0}
# Track which steps we've seen
main_steps_seen = set()
# Track latest status for each step (logs are ordered by timestamp desc)
main_steps_seen = {}
upload_tasks_seen = {}
for log in logs:
@@ -363,15 +363,20 @@ def _compute_step_summary(logs):
is_upload = any(step_name.startswith(prefix) for prefix in upload_prefixes)
if is_upload:
# Track latest status for each unique upload task
upload_tasks_seen[step_name] = status
# Track latest status for each unique upload task (first seen is latest)
if step_name not in upload_tasks_seen:
upload_tasks_seen[step_name] = status
elif step_name in main_steps:
# Track latest status for main steps
main_steps_seen.add(step_name)
if status in main_counts:
main_counts[status] += 1
# Track latest status for main steps (first seen is latest)
if step_name not in main_steps_seen:
main_steps_seen[step_name] = status
# Count upload task statuses
# Count main step statuses from latest status per step
for task_status in main_steps_seen.values():
if task_status in main_counts:
main_counts[task_status] += 1
# Count upload task statuses from latest status per task
for task_status in upload_tasks_seen.values():
if task_status in upload_counts:
upload_counts[task_status] += 1
+251
View File
@@ -0,0 +1,251 @@
"""
Tests for file status and metrics calculation bug fixes.
This test module verifies that:
1. Status calculation only considers the latest status per unique step
2. Metrics counting only uses the latest status per unique step
3. Files with completed steps show "completed" not "processing"
"""
import pytest
from datetime import datetime, timedelta
from app.utils.file_status import _compute_status_from_logs
from app.views.files import _compute_step_summary
@pytest.mark.unit
class TestFileStatusBugFixes:
"""Test fixes for status calculation bugs."""
def test_status_not_stuck_on_old_in_progress(self):
"""
Test that status doesn't show "processing" when old in_progress logs exist
but latest status for all steps is success.
This simulates the bug where a file shows "Processing" even though
all steps have completed successfully.
"""
class MockLog:
def __init__(self, step_name, status, timestamp):
self.step_name = step_name
self.status = status
self.timestamp = timestamp
now = datetime.now()
# Simulate logs ordered by timestamp desc (latest first)
logs = [
# Latest logs (all success)
MockLog("upload_to_dropbox", "success", now - timedelta(minutes=1)),
MockLog("extract_metadata_with_gpt", "success", now - timedelta(minutes=2)),
MockLog("check_text", "success", now - timedelta(minutes=3)),
# Older in_progress logs that should be ignored
MockLog("upload_to_dropbox", "in_progress", now - timedelta(minutes=5)),
MockLog("extract_metadata_with_gpt", "in_progress", now - timedelta(minutes=6)),
MockLog("check_text", "in_progress", now - timedelta(minutes=7)),
]
result = _compute_status_from_logs(logs)
# Should be completed, not processing
assert result["status"] == "completed"
assert result["has_errors"] is False
def test_status_shows_processing_for_active_tasks(self):
"""
Test that status correctly shows "processing" when there are
actually in-progress tasks (based on latest status).
"""
class MockLog:
def __init__(self, step_name, status, timestamp):
self.step_name = step_name
self.status = status
self.timestamp = timestamp
now = datetime.now()
logs = [
# One task actually in progress
MockLog("upload_to_dropbox", "in_progress", now - timedelta(minutes=1)),
# Other tasks completed
MockLog("extract_metadata_with_gpt", "success", now - timedelta(minutes=2)),
MockLog("check_text", "success", now - timedelta(minutes=3)),
]
result = _compute_status_from_logs(logs)
# Should be processing because one task is actually in progress
assert result["status"] == "processing"
assert result["has_errors"] is False
def test_status_shows_failed_when_latest_has_failure(self):
"""
Test that status shows "failed" when the latest status for any step is failure.
"""
class MockLog:
def __init__(self, step_name, status, timestamp):
self.step_name = step_name
self.status = status
self.timestamp = timestamp
now = datetime.now()
logs = [
# One task failed (latest status)
MockLog("upload_to_s3", "failure", now - timedelta(minutes=1)),
# Other tasks completed
MockLog("upload_to_dropbox", "success", now - timedelta(minutes=2)),
MockLog("extract_metadata_with_gpt", "success", now - timedelta(minutes=3)),
]
result = _compute_status_from_logs(logs)
assert result["status"] == "failed"
assert result["has_errors"] is True
@pytest.mark.unit
class TestMetricsCountingBugFixes:
"""Test fixes for metrics counting bugs."""
def test_main_steps_not_double_counted(self):
"""
Test that main processing steps are only counted once per step,
using the latest status, not counting all historical logs.
This simulates the bug where metrics show incorrect counts because
they count all logs instead of just the latest per step.
"""
from datetime import datetime, timedelta
class MockLog:
def __init__(self, step_name, status, timestamp):
self.step_name = step_name
self.status = status
self.timestamp = timestamp
now = datetime.now()
# Simulate logs ordered by timestamp desc (latest first)
logs = [
# Latest status for each step (all success)
MockLog("hash_file", "success", now - timedelta(minutes=1)),
MockLog("create_file_record", "success", now - timedelta(minutes=2)),
MockLog("extract_metadata_with_gpt", "success", now - timedelta(minutes=3)),
# Older in_progress logs that should be ignored
MockLog("hash_file", "in_progress", now - timedelta(minutes=5)),
MockLog("create_file_record", "in_progress", now - timedelta(minutes=6)),
MockLog("extract_metadata_with_gpt", "in_progress", now - timedelta(minutes=7)),
]
summary = _compute_step_summary(logs)
# Should count each main step only once
assert summary["total_main_steps"] == 3
assert summary["main"]["success"] == 3
assert summary["main"]["in_progress"] == 0 # No steps actually in progress
assert summary["main"]["failure"] == 0
def test_upload_tasks_not_double_counted(self):
"""
Test that upload tasks are only counted once per destination,
using the latest status.
"""
from datetime import datetime, timedelta
class MockLog:
def __init__(self, step_name, status, timestamp):
self.step_name = step_name
self.status = status
self.timestamp = timestamp
now = datetime.now()
# Logs ordered by timestamp desc (latest first)
logs = [
# Latest status for uploads
MockLog("upload_to_dropbox", "success", now - timedelta(minutes=1)),
MockLog("upload_to_s3", "success", now - timedelta(minutes=2)),
MockLog("upload_to_nextcloud", "success", now - timedelta(minutes=3)),
# Queue logs (older, should use upload_to_ as latest)
MockLog("queue_dropbox", "success", now - timedelta(minutes=4)),
MockLog("queue_s3", "in_progress", now - timedelta(minutes=5)),
MockLog("queue_nextcloud", "success", now - timedelta(minutes=6)),
# Even older in_progress logs
MockLog("upload_to_dropbox", "in_progress", now - timedelta(minutes=7)),
MockLog("upload_to_s3", "in_progress", now - timedelta(minutes=8)),
]
summary = _compute_step_summary(logs)
# Should count unique upload destinations
# Note: queue_X and upload_to_X are separate steps
assert summary["total_upload_tasks"] == 6 # 3 upload_to + 3 queue
assert summary["uploads"]["success"] == 5 # All uploads success, 2 queue success
assert summary["uploads"]["in_progress"] == 1 # 1 queue in_progress
def test_accurate_metrics_for_completed_file(self):
"""
Test the scenario from the issue: File with 6 actual uploads
should show 6, not 12.
"""
from datetime import datetime, timedelta
class MockLog:
def __init__(self, step_name, status, timestamp):
self.step_name = step_name
self.status = status
self.timestamp = timestamp
now = datetime.now()
# Simulate 6 successful uploads with their queue steps
logs = []
services = ["dropbox", "s3", "nextcloud", "google_drive", "onedrive", "webdav"]
# Add latest status (all success) - most recent
for i, service in enumerate(services):
logs.append(MockLog(f"upload_to_{service}", "success", now - timedelta(minutes=i * 2)))
logs.append(MockLog(f"queue_{service}", "success", now - timedelta(minutes=i * 2 + 1)))
# Add some older in_progress logs
base_offset = len(services) * 2
for i, service in enumerate(services):
logs.append(MockLog(f"upload_to_{service}", "in_progress", now - timedelta(minutes=base_offset + i * 2)))
logs.append(MockLog(f"queue_{service}", "in_progress", now - timedelta(minutes=base_offset + i * 2 + 1)))
summary = _compute_step_summary(logs)
# Should have 12 total upload tasks (6 upload_to + 6 queue)
assert summary["total_upload_tasks"] == 12
# All should be success (latest status)
assert summary["uploads"]["success"] == 12
assert summary["uploads"]["in_progress"] == 0
def test_mixed_upload_statuses(self):
"""
Test that upload metrics correctly reflect mixed statuses.
"""
from datetime import datetime, timedelta
class MockLog:
def __init__(self, step_name, status, timestamp):
self.step_name = step_name
self.status = status
self.timestamp = timestamp
now = datetime.now()
logs = [
# Latest statuses (ordered by timestamp desc)
MockLog("upload_to_dropbox", "success", now - timedelta(minutes=1)),
MockLog("upload_to_s3", "failure", now - timedelta(minutes=2)),
MockLog("upload_to_nextcloud", "in_progress", now - timedelta(minutes=3)),
MockLog("queue_dropbox", "success", now - timedelta(minutes=4)),
MockLog("queue_s3", "success", now - timedelta(minutes=5)),
MockLog("queue_nextcloud", "success", now - timedelta(minutes=6)),
]
summary = _compute_step_summary(logs)
assert summary["total_upload_tasks"] == 6
assert summary["uploads"]["success"] == 4 # 1 upload + 3 queue
assert summary["uploads"]["failure"] == 1 # 1 upload
assert summary["uploads"]["in_progress"] == 1 # 1 upload