Add dashboard summary tiles
Shows rider count, activities imported in the last 7 days, 7-day sync success rate, and how many riders currently need attention, right above the rider list where an admin looks first. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
from app.db.models import ActivityStatus, HealthState
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from app.db.models import ActivityStatus, HealthState, SyncRunStatus
|
||||
|
||||
|
||||
def test_create_two_independent_users(db_session, user_repository) -> None:
|
||||
@@ -129,3 +131,79 @@ def test_scheduler_settings_update_persists_all_fields(scheduler_settings_reposi
|
||||
assert reloaded.night_start_hour == 20
|
||||
assert reloaded.day_interval_minutes == 10
|
||||
assert reloaded.night_interval_minutes == 45
|
||||
|
||||
|
||||
def _make_user(repo, name, *, enabled=True, health_state=HealthState.HEALTHY):
|
||||
return repo.create(
|
||||
name=name,
|
||||
enabled=enabled,
|
||||
health_state=health_state,
|
||||
mywhoosh_email_enc=f"mw-{name}",
|
||||
mywhoosh_password_enc=f"mw-pw-{name}",
|
||||
garmin_email_enc=f"g-{name}",
|
||||
garmin_password_enc=f"g-pw-{name}",
|
||||
)
|
||||
|
||||
|
||||
def test_dashboard_summary_counts_riders_total_and_enabled(user_repository) -> None:
|
||||
_make_user(user_repository, "Alex", enabled=True)
|
||||
_make_user(user_repository, "Jamie", enabled=True)
|
||||
_make_user(user_repository, "Paused", enabled=False)
|
||||
|
||||
summary = user_repository.dashboard_summary(since=datetime.now(timezone.utc) - timedelta(days=7))
|
||||
|
||||
assert summary.rider_total == 3
|
||||
assert summary.rider_enabled == 2
|
||||
|
||||
|
||||
def test_dashboard_summary_counts_action_required_riders(user_repository) -> None:
|
||||
_make_user(user_repository, "Healthy", health_state=HealthState.HEALTHY)
|
||||
_make_user(user_repository, "Blocked", health_state=HealthState.ACTION_REQUIRED)
|
||||
_make_user(user_repository, "AlsoBlocked", health_state=HealthState.ACTION_REQUIRED)
|
||||
|
||||
summary = user_repository.dashboard_summary(since=datetime.now(timezone.utc) - timedelta(days=7))
|
||||
|
||||
assert summary.action_required_count == 2
|
||||
|
||||
|
||||
def test_dashboard_summary_sums_imported_count_within_window_only(user_repository, sync_run_repository) -> None:
|
||||
user = _make_user(user_repository, "Alex")
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
recent_run = sync_run_repository.start(user.id)
|
||||
recent_run.started_at = now - timedelta(days=1)
|
||||
sync_run_repository.finish(
|
||||
recent_run.id, status=SyncRunStatus.SUCCESS, discovered=5, imported=5, skipped=0, failed=0
|
||||
)
|
||||
|
||||
old_run = sync_run_repository.start(user.id)
|
||||
old_run.started_at = now - timedelta(days=30)
|
||||
sync_run_repository.finish(
|
||||
old_run.id, status=SyncRunStatus.SUCCESS, discovered=3, imported=3, skipped=0, failed=0
|
||||
)
|
||||
|
||||
summary = user_repository.dashboard_summary(since=now - timedelta(days=7))
|
||||
|
||||
assert summary.imported_recent == 5
|
||||
|
||||
|
||||
def test_dashboard_summary_computes_success_rate_from_finished_runs_in_window(
|
||||
user_repository, sync_run_repository
|
||||
) -> None:
|
||||
user = _make_user(user_repository, "Alex")
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
for status in (SyncRunStatus.SUCCESS, SyncRunStatus.PARTIAL, SyncRunStatus.FAILED, SyncRunStatus.FAILED):
|
||||
run = sync_run_repository.start(user.id)
|
||||
run.started_at = now - timedelta(hours=1)
|
||||
sync_run_repository.finish(run.id, status=status, discovered=1, imported=0, skipped=0, failed=1)
|
||||
|
||||
summary = user_repository.dashboard_summary(since=now - timedelta(days=7))
|
||||
|
||||
assert summary.success_rate_recent == 50.0
|
||||
|
||||
|
||||
def test_dashboard_summary_success_rate_is_none_without_finished_runs_in_window(user_repository) -> None:
|
||||
summary = user_repository.dashboard_summary(since=datetime.now(timezone.utc) - timedelta(days=7))
|
||||
|
||||
assert summary.success_rate_recent is None
|
||||
|
||||
43
tests/web/test_dashboard_summary.py
Normal file
43
tests/web/test_dashboard_summary.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from app.db.models import HealthState, SyncRunStatus
|
||||
from app.db.repositories import SyncRunRepository, UserRepository
|
||||
|
||||
|
||||
def _seed_user(session, name, *, enabled=True, health_state=HealthState.HEALTHY):
|
||||
return UserRepository(session).create(
|
||||
name=name,
|
||||
enabled=enabled,
|
||||
health_state=health_state,
|
||||
mywhoosh_email_enc=f"mw-{name}",
|
||||
mywhoosh_password_enc=f"mw-pw-{name}",
|
||||
garmin_email_enc=f"g-{name}",
|
||||
garmin_password_enc=f"g-pw-{name}",
|
||||
)
|
||||
|
||||
|
||||
def test_dashboard_shows_summary_tiles(app, authenticated_client) -> None:
|
||||
with app.state.session_factory() as session:
|
||||
alex = _seed_user(session, "Alex", enabled=True)
|
||||
_seed_user(session, "Jamie", enabled=False, health_state=HealthState.ACTION_REQUIRED)
|
||||
|
||||
run_repo = SyncRunRepository(session)
|
||||
run = run_repo.start(alex.id)
|
||||
run.started_at = datetime.now(timezone.utc) - timedelta(hours=2)
|
||||
run_repo.finish(run.id, status=SyncRunStatus.SUCCESS, discovered=3, imported=3, skipped=0, failed=0)
|
||||
|
||||
response = authenticated_client.get("/")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert '<span class="stat-tile-value">2</span>' in response.text
|
||||
assert "1 enabled" in response.text
|
||||
assert '<span class="stat-tile-value">3</span>' in response.text
|
||||
assert '<span class="stat-tile-value">100%</span>' in response.text
|
||||
assert '<span class="stat-tile-value">1</span>' in response.text
|
||||
|
||||
|
||||
def test_dashboard_shows_dash_for_success_rate_without_recent_runs(authenticated_client) -> None:
|
||||
response = authenticated_client.get("/")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert '<span class="stat-tile-value">–</span>' in response.text
|
||||
Reference in New Issue
Block a user