diff --git a/apps/device-host-agent/host_agent/web/app.py b/apps/device-host-agent/host_agent/web/app.py
index 38aa360..2749936 100644
--- a/apps/device-host-agent/host_agent/web/app.py
+++ b/apps/device-host-agent/host_agent/web/app.py
@@ -108,6 +108,17 @@ def _login_page(
return HTMLResponse(_chrome("Login", body, session=None))
+def _device_display_status(device: Any, *, busy_device_id: str | None) -> str:
+ """Connected-but-idle devices report "busy" at the DeviceManager layer
+ (an active Appium/WDA session is required to reuse it), which is not the
+ same as a task currently running on that device. Only the device actually
+ bound to the current assignment should read as "busy" here.
+ """
+ if device.status == "busy" and device.id != busy_device_id:
+ return "connected"
+ return device.status
+
+
def _dashboard_body(
*,
identity: HostIdentityState | None,
@@ -118,6 +129,7 @@ def _dashboard_body(
heartbeat = snapshot.get("last_heartbeat")
assignment = snapshot.get("current_assignment")
policy = snapshot.get("host_policy")
+ busy_device_id = assignment["device_id"] if assignment else None
heartbeat_text = (
f"{'ok' if heartbeat['ok'] else 'failed'} at {heartbeat['at']} "
f"({heartbeat['device_count']} devices)"
@@ -143,7 +155,8 @@ def _dashboard_body(
)
device_rows = "".join(
f"
| {escape(device.id)} | {escape(device.name or '')} | "
- f"{escape(device.driver_type)} | {escape(device.status)} |
"
+ f"{escape(device.driver_type)} | "
+ f"{escape(_device_display_status(device, busy_device_id=busy_device_id))} | "
for device in devices
)
return f"""
@@ -403,12 +416,14 @@ def create_console_app(
session: SessionState = Depends(require_session),
) -> JSONResponse:
snapshot = status_tracker.snapshot()
+ current_assignment = snapshot.get("current_assignment")
+ busy_device_id = current_assignment["device_id"] if current_assignment else None
devices = [
{
"id": device.id,
"name": device.name,
"driver_type": device.driver_type,
- "status": device.status,
+ "status": _device_display_status(device, busy_device_id=busy_device_id),
}
for device in manager.list_devices()
]
diff --git a/apps/device-host-agent/tests/test_web_app.py b/apps/device-host-agent/tests/test_web_app.py
index ae0af3d..36d3796 100644
--- a/apps/device-host-agent/tests/test_web_app.py
+++ b/apps/device-host-agent/tests/test_web_app.py
@@ -1,9 +1,11 @@
from __future__ import annotations
import re
+from datetime import UTC, datetime
from fastapi.testclient import TestClient
+from cloud.internal_api.models import AssignmentModel
from device.manager import DeviceManager
from host_agent.config import HostAgentConfig
from host_agent.history import ConsoleHistoryStore
@@ -53,6 +55,7 @@ def _build_client(tmp_path, *, create_account: bool = True) -> tuple[TestClient,
"local_account_store": local_account_store,
"history_store": history_store,
"session_manager": session_manager,
+ "status_tracker": status_tracker,
}
return client, context
@@ -119,6 +122,52 @@ def test_login_with_correct_password_sets_cookie_and_dashboard_succeeds(
assert "control.example" in response.text
+def test_connected_device_without_running_task_shows_connected_not_busy(
+ tmp_path,
+) -> None:
+ client, context = _build_client(tmp_path)
+ _login(client)
+ context["manager"].register_device(
+ "device-a",
+ lambda: object(), # type: ignore[arg-type,return-value]
+ status="busy",
+ )
+
+ dashboard_response = client.get("/")
+ api_response = client.get("/api/status")
+
+ assert "connected" in dashboard_response.text
+ assert "busy | " not in dashboard_response.text
+ assert api_response.json()["devices"][0]["status"] == "connected"
+
+
+def test_device_running_current_assignment_still_shows_busy(tmp_path) -> None:
+ client, context = _build_client(tmp_path)
+ _login(client)
+ context["manager"].register_device(
+ "device-a",
+ lambda: object(), # type: ignore[arg-type,return-value]
+ status="busy",
+ )
+ context["status_tracker"].mark_assignment_started(
+ AssignmentModel(
+ task_id="task-a",
+ attempt=1,
+ lease_id="lease-a",
+ lease_expires_at=datetime(2026, 7, 12, tzinfo=UTC),
+ host_id="host-a",
+ device_id="device-a",
+ goal="open settings",
+ )
+ )
+
+ dashboard_response = client.get("/")
+ api_response = client.get("/api/status")
+
+ assert "busy | " in dashboard_response.text
+ assert api_response.json()["devices"][0]["status"] == "busy"
+
+
def test_mutating_post_without_csrf_token_is_rejected_and_makes_no_change(
tmp_path,
) -> None: