diff --git a/app/web/routes.py b/app/web/routes.py index 1f7bd30..54aad43 100644 --- a/app/web/routes.py +++ b/app/web/routes.py @@ -15,6 +15,14 @@ router = APIRouter() templates = Jinja2Templates(directory=str(Path(__file__).resolve().parent / "templates")) +def _next_sync_tick(request: Request): + scheduler = getattr(request.app.state, "scheduler", None) + return getattr(scheduler, "next_tick", None) + + +templates.env.globals["next_sync_tick"] = _next_sync_tick + + def _get_user_or_404(repository: UserRepository, user_id: int) -> SyncUser: user = repository.get(user_id) if user is None: diff --git a/app/web/static/app.js b/app/web/static/app.js new file mode 100644 index 0000000..62b24cc --- /dev/null +++ b/app/web/static/app.js @@ -0,0 +1,10 @@ +document.addEventListener("DOMContentLoaded", () => { + document.querySelectorAll("time[data-utc]").forEach((el) => { + const date = new Date(el.dataset.utc); + if (Number.isNaN(date.getTime())) { + return; + } + el.textContent = date.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" }); + el.title = `${el.dataset.utc} (UTC)`; + }); +}); diff --git a/app/web/static/style.css b/app/web/static/style.css index 3119407..2d97e0d 100644 --- a/app/web/static/style.css +++ b/app/web/static/style.css @@ -64,12 +64,36 @@ a:hover { border-radius: 7px; } +.topbar-right { + display: flex; + align-items: center; + gap: 1.25rem; + flex-wrap: wrap; +} + .topbar nav { display: flex; gap: 1.25rem; font-size: 0.9rem; } +.sync-status { + display: inline-flex; + align-items: center; + gap: 0.35rem; + padding: 0.3rem 0.7rem; + border-radius: 999px; + background: var(--info-bg); + color: var(--info); + font-size: 0.78rem; + font-weight: 600; + white-space: nowrap; +} + +.sync-status time { + font-weight: 700; +} + .container { max-width: 960px; margin: 0 auto; diff --git a/app/web/templates/base.html b/app/web/templates/base.html index cbec323..3eb2b87 100644 --- a/app/web/templates/base.html +++ b/app/web/templates/base.html @@ -9,19 +9,26 @@ +
MyWhoosh → Garmin Sync - +
{% block content %}{% endblock %} diff --git a/tests/web/test_next_sync_display.py b/tests/web/test_next_sync_display.py new file mode 100644 index 0000000..211af29 --- /dev/null +++ b/tests/web/test_next_sync_display.py @@ -0,0 +1,36 @@ +from datetime import datetime, timezone + +from fastapi.testclient import TestClient + + +class _FakeScheduler: + def __init__(self, next_tick=None) -> None: + self.last_tick = None + self.next_tick = next_tick + + +def test_login_page_shows_next_sync_time(app, client: TestClient) -> None: + next_tick = datetime(2026, 8, 16, 14, 32, tzinfo=timezone.utc) + app.state.scheduler = _FakeScheduler(next_tick=next_tick) + + response = client.get("/login") + + assert response.status_code == 200 + assert 'data-utc="2026-08-16T14:32:00+00:00"' in response.text + + +def test_dashboard_shows_next_sync_time(app, authenticated_client) -> None: + next_tick = datetime(2026, 8, 16, 15, 0, tzinfo=timezone.utc) + app.state.scheduler = _FakeScheduler(next_tick=next_tick) + + response = authenticated_client.get("/") + + assert response.status_code == 200 + assert 'data-utc="2026-08-16T15:00:00+00:00"' in response.text + + +def test_login_page_renders_without_scheduler(client: TestClient) -> None: + response = client.get("/login") + + assert response.status_code == 200 + assert "data-utc" not in response.text