Live-update the account page status block after sync

Mirrors the dashboard's row update: "Sync now" on the account page
refreshes the status block in place and shows a toast, instead of
navigating to a separate result page.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Bastian Wagner
2026-08-16 13:18:23 +02:00
parent 8d73dea7dd
commit eb97374578
4 changed files with 45 additions and 20 deletions

View File

@@ -7,7 +7,7 @@ from app.auth.csrf import ensure_csrf_token, validate_csrf
from app.db.repositories import ActivityRepository, SyncRunRepository, UserRepository
from app.security.credentials import CredentialCipher
from app.sync.manager import SyncAlreadyRunning
from app.web.operations import _normalize_outcome
from app.web.operations import _outcome_toast, _toast_html
from app.web.routes import templates
router = APIRouter()
@@ -92,11 +92,19 @@ async def account_sync(request: Request, csrf_token: str = Form(...)):
validate_csrf(request, csrf_token)
try:
outcome = await request.app.state.sync_manager.sync_user(user_id)
with request.app.state.session_factory() as session:
user = UserRepository(session).get(user_id)
label = user.name if user is not None else "Account"
message, level = _outcome_toast(outcome, label)
except SyncAlreadyRunning:
return HTMLResponse("Sync already running for this user", status_code=409)
return templates.TemplateResponse(
request, "fragments/sync_result.html", {"outcomes": [_normalize_outcome(outcome)]}
with request.app.state.session_factory() as session:
user = UserRepository(session).get(user_id)
label = user.name if user is not None else "Account"
message, level = f"{label}: sync already running", "info"
status_html = (
templates.get_template("fragments/account_status.html").render(user=user) if user is not None else ""
)
return HTMLResponse(status_html + _toast_html(message, level))
@router.get("/account/edit", response_class=HTMLResponse)

View File

@@ -6,7 +6,8 @@
<h1>{{ user.name }}</h1>
<div class="page-actions">
<a class="btn secondary" href="/account/edit">Edit</a>
<form method="post" action="/account/sync" class="inline-form">
<form method="post" action="/account/sync" class="inline-form"
hx-post="/account/sync" hx-target="#account-status" hx-swap="outerHTML" hx-disabled-elt="find button">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit">Sync now</button>
</form>
@@ -17,19 +18,7 @@
</div>
<div class="card">
<dl class="info-grid">
<dt>Status</dt>
<dd><span class="badge badge-{{ user.health_state.value }}">{{ user.health_state.value.replace("_", " ") }}</span></dd>
<dt>MyWhoosh state</dt>
<dd>{{ user.mywhoosh_state }}</dd>
<dt>Garmin state</dt>
<dd>{{ user.garmin_state }}</dd>
<dt>Action reason</dt>
<dd>{{ user.action_reason or "-" }}</dd>
</dl>
{% include "fragments/account_status.html" %}
</div>
{% if user.action_reason == "mywhoosh_device_conflict" %}

View File

@@ -0,0 +1,13 @@
<dl class="info-grid" id="account-status">
<dt>Status</dt>
<dd><span class="badge badge-{{ user.health_state.value }}">{{ user.health_state.value.replace("_", " ") }}</span></dd>
<dt>MyWhoosh state</dt>
<dd>{{ user.mywhoosh_state }}</dd>
<dt>Garmin state</dt>
<dd>{{ user.garmin_state }}</dd>
<dt>Action reason</dt>
<dd>{{ user.action_reason or "-" }}</dd>
</dl>

View File

@@ -289,7 +289,7 @@ def test_account_sync_triggers_own_user_only(app, client: TestClient, fake_sync_
assert fake_sync_manager.user_calls == [user_id]
def test_account_sync_reports_already_running(app, client: TestClient, fake_sync_manager) -> None:
def test_account_sync_reports_already_running_as_toast(app, client: TestClient, fake_sync_manager) -> None:
create_user_via_admin(client)
account_login(client, email="max@mywhoosh.example", password="mw-secret")
app.state.sync_manager = fake_sync_manager
@@ -299,10 +299,25 @@ def test_account_sync_reports_already_running(app, client: TestClient, fake_sync
csrf = extract_csrf(page.text)
response = client.post("/account/sync", data={"csrf_token": csrf})
assert response.status_code == 409
assert response.status_code == 200
assert "already running" in response.text.lower()
def test_account_sync_updates_status_block_and_shows_toast(app, client: TestClient, fake_sync_manager) -> None:
create_user_via_admin(client)
account_login(client, email="max@mywhoosh.example", password="mw-secret")
app.state.sync_manager = fake_sync_manager
page = client.get("/account")
csrf = extract_csrf(page.text)
response = client.post("/account/sync", data={"csrf_token": csrf})
assert response.status_code == 200
assert 'id="account-status"' in response.text
assert 'hx-swap-oob="true"' in response.text
assert "0 imported, 0 failed" in response.text
def test_account_sync_requires_login(client: TestClient) -> None:
response = client.post("/account/sync", data={"csrf_token": "whatever"}, follow_redirects=False)
assert response.status_code == 303