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:
@@ -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)
|
||||||
|
|||||||
@@ -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" %}
|
||||||
|
|||||||
13
app/web/templates/fragments/account_status.html
Normal file
13
app/web/templates/fragments/account_status.html
Normal 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>
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user