diff --git a/README.md b/README.md index 62026e5..dbbdd83 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,11 @@ and final replies in a local SQLite database. View them at `http://127.0.0.1:8765/conversations`; image bytes are excluded. Set `HOST_AGENT_CONVERSATION_LOG_PATH` to change the database path. +The authenticated `Devices` page has an on-demand `Get screenshot` button for +each connected device. Screenshots are captured only after the operator clicks +the button; the page does not auto-refresh or capture screenshots as part of +heartbeat synchronization. + In local mode, Appium supervision is enabled by default. Host Agent probes `/status`, adopts a healthy existing Appium instance, starts Appium when no listener exists, restarts only processes it started if they crash, and stops diff --git a/apps/device-host-agent/host_agent/web/app.py b/apps/device-host-agent/host_agent/web/app.py index 9c99baa..a787f8f 100644 --- a/apps/device-host-agent/host_agent/web/app.py +++ b/apps/device-host-agent/host_agent/web/app.py @@ -11,6 +11,7 @@ import jinja2 from fastapi import Depends, FastAPI, HTTPException, Request from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse, Response +from core.errors import DeviceNotFoundError, DeviceOfflineError, DeviceRuntimeError from device.manager import DeviceManager from host_agent.assignment import AssignmentExecutor from host_agent.client import ( @@ -488,6 +489,43 @@ def create_console_app( error=None, ) + @app.post("/api/devices/{device_id}/screenshot") + async def api_device_screenshot( + device_id: str, + session: SessionState = Depends(require_csrf), + ) -> Response: + """Capture one on-demand screenshot for a connected local device.""" + try: + screenshot = await asyncio.to_thread( + lambda: manager.active_driver(device_id).screenshot() + ) + except DeviceNotFoundError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + except DeviceOfflineError as exc: + raise HTTPException(status_code=503, detail=str(exc)) from exc + except DeviceRuntimeError as exc: + raise HTTPException(status_code=502, detail=str(exc)) from exc + except Exception as exc: + raise HTTPException( + status_code=502, + detail=str(exc) or "failed to capture device screenshot", + ) from exc + + if not isinstance(screenshot, bytes) or not screenshot: + raise HTTPException( + status_code=502, + detail="device returned an empty screenshot", + ) + return Response( + content=screenshot, + media_type="image/png", + headers={ + "Cache-Control": "no-store", + "Pragma": "no-cache", + "X-Content-Type-Options": "nosniff", + }, + ) + @app.post("/devices/save") async def devices_save( request: Request, diff --git a/apps/device-host-agent/host_agent/web/templates/devices.html b/apps/device-host-agent/host_agent/web/templates/devices.html index 10b8929..6f68e74 100644 --- a/apps/device-host-agent/host_agent/web/templates/devices.html +++ b/apps/device-host-agent/host_agent/web/templates/devices.html @@ -5,13 +5,20 @@
{{ error }}
{% endif %}| ID | Name | Driver | Cloud ID | ||
|---|---|---|---|---|---|
| ID | Name | Driver | Cloud ID | Screenshot | |
| {{ device["device_id"] }} | {{ device["name"] or "" }} | {{ device["driver_type"] }} | {{ device["cloud_device_id"] or "" }} | +
+
+
+
+
+ |
Edit + + {% endblock %} diff --git a/apps/device-host-agent/tests/host_agent/web/test_templates.py b/apps/device-host-agent/tests/host_agent/web/test_templates.py index a7f38d4..08a0cee 100644 --- a/apps/device-host-agent/tests/host_agent/web/test_templates.py +++ b/apps/device-host-agent/tests/host_agent/web/test_templates.py @@ -47,6 +47,7 @@ def test_devices_renders(env, sample_session) -> None: **make_devices_context(sample_session) ) assert ' |