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.db.repositories import ActivityRepository, SyncRunRepository, UserRepository
from app.security.credentials import CredentialCipher from app.security.credentials import CredentialCipher
from app.sync.manager import SyncAlreadyRunning 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 from app.web.routes import templates
router = APIRouter() router = APIRouter()
@@ -92,11 +92,19 @@ async def account_sync(request: Request, csrf_token: str = Form(...)):
validate_csrf(request, csrf_token) validate_csrf(request, csrf_token)
try: try:
outcome = await request.app.state.sync_manager.sync_user(user_id) 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: except SyncAlreadyRunning:
return HTMLResponse("Sync already running for this user", status_code=409) with request.app.state.session_factory() as session:
return templates.TemplateResponse( user = UserRepository(session).get(user_id)
request, "fragments/sync_result.html", {"outcomes": [_normalize_outcome(outcome)]} 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) @router.get("/account/edit", response_class=HTMLResponse)

View File

@@ -6,7 +6,8 @@
<h1>{{ user.name }}</h1> <h1>{{ user.name }}</h1>
<div class="page-actions"> <div class="page-actions">
<a class="btn secondary" href="/account/edit">Edit</a> <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 }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit">Sync now</button> <button type="submit">Sync now</button>
</form> </form>
@@ -17,19 +18,7 @@
</div> </div>
<div class="card"> <div class="card">
<dl class="info-grid"> {% include "fragments/account_status.html" %}
<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>
</div> </div>
{% if user.action_reason == "mywhoosh_device_conflict" %} {% 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] 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) create_user_via_admin(client)
account_login(client, email="max@mywhoosh.example", password="mw-secret") account_login(client, email="max@mywhoosh.example", password="mw-secret")
app.state.sync_manager = fake_sync_manager 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) csrf = extract_csrf(page.text)
response = client.post("/account/sync", data={"csrf_token": csrf}) 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() 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: def test_account_sync_requires_login(client: TestClient) -> None:
response = client.post("/account/sync", data={"csrf_token": "whatever"}, follow_redirects=False) response = client.post("/account/sync", data={"csrf_token": "whatever"}, follow_redirects=False)
assert response.status_code == 303 assert response.status_code == 303