Show next scheduled sync time on every page

Exposes the scheduler's next_tick in the topbar via a safe Jinja
helper (falls back to nothing if the scheduler isn't running yet),
and converts the server-rendered UTC timestamp to the visitor's local
time client-side so it reads correctly regardless of timezone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Bastian Wagner
2026-08-16 10:58:53 +02:00
parent 5d75aa328d
commit a74ab95f3f
5 changed files with 93 additions and 8 deletions

View File

@@ -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:

10
app/web/static/app.js Normal file
View File

@@ -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)`;
});
});

View File

@@ -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;

View File

@@ -9,10 +9,12 @@
<link rel="shortcut icon" href="/static/favicon.ico">
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
<link rel="stylesheet" href="/static/style.css">
<script src="/static/app.js" defer></script>
</head>
<body>
<header class="topbar">
<span class="brand"><img src="/static/logo.png" alt="" class="brand-logo" width="28" height="28">MyWhoosh &rarr; Garmin Sync</span>
<div class="topbar-right">
<nav>
{% if request.session.get('admin_authenticated') %}
<a href="/">Dashboard</a>
@@ -22,6 +24,11 @@
<a href="/account">My Account</a>
{% endif %}
</nav>
{% set next_tick = next_sync_tick(request) %}
{% if next_tick %}
<span class="sync-status">Next sync: <time class="next-sync" datetime="{{ next_tick.isoformat() }}" data-utc="{{ next_tick.isoformat() }}">{{ next_tick.strftime('%Y-%m-%d %H:%M UTC') }}</time></span>
{% endif %}
</div>
</header>
<main class="container">
{% block content %}{% endblock %}

View File

@@ -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