from __future__ import annotations
import asyncio
import json
from html import escape as _escape
from typing import Any
from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse, Response
from device.manager import DeviceManager
from host_agent.client import HostAgentEnrollmentClient
from host_agent.config import HostAgentConfig
from host_agent.devices import register_local_device, unregister_local_device
from host_agent.history import ConsoleHistoryStore
from host_agent.identity import HostIdentityState, HostIdentityStore
from host_agent.local_account import LocalAccountState, LocalAccountStore
from host_agent.status import AgentStatusTracker
from host_agent.web.auth import (
SessionManager,
SessionState,
attempt_login,
change_password,
)
from storage.device_config import DeviceConfigStore
SESSION_COOKIE_NAME = "host_console_session"
CSRF_HEADER_NAME = "X-CSRF-Token"
CSRF_FORM_FIELD = "csrf_token"
_LOOPBACK_BIND_HOSTS = frozenset({"127.0.0.1", "localhost", "::1"})
_CSS = """
body { font-family: system-ui, sans-serif; margin: 0; background: #f5f5f5; color: #222; }
header { background: #20303f; color: #fff; padding: 0.75rem 1.5rem; }
header nav { display: inline; margin-left: 1.5rem; }
header nav a, header nav form { display: inline-block; margin-right: 1rem; }
header a { color: #fff; text-decoration: none; }
header button { background: none; border: none; color: #fff; text-decoration: underline; cursor: pointer; padding: 0; font: inherit; }
main { padding: 1.5rem; max-width: 960px; margin: 0 auto; }
table { border-collapse: collapse; width: 100%; margin-bottom: 1rem; background: #fff; }
th, td { border: 1px solid #ccc; padding: 0.4rem 0.6rem; text-align: left; }
form.inline { display: inline; margin: 0; }
.error { color: #b00020; }
.notice { color: #1b5e20; }
"""
def escape(value: object) -> str:
if value is None:
return ""
return _escape(str(value), quote=True)
def _chrome(title: str, body_html: str, *, session: SessionState | None) -> str:
nav = ""
if session is not None:
nav = f"""
"""
return f"""
{escape(title)}Host Agent Console
{nav}
{body_html}
"""
def _login_page(
*, account: LocalAccountState | None, error: str | None = None
) -> HTMLResponse:
if account is None:
body = """
Login
No local account exists yet. Run device-host-agent setup
on this machine to create one before logging in to the console.
"""
return HTMLResponse(_chrome("Login", body, session=None))
error_html = f'
{escape(error)}
' if error else ""
body = f"""
Login
{error_html}
"""
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,
snapshot: dict[str, Any],
devices: list[Any],
config: HostAgentConfig,
) -> str:
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)"
if heartbeat
else "never"
)
assignment_text = (
f"{assignment['task_id']} on {assignment['device_id']} "
f"(started {assignment['started_at']})"
if assignment
else "none"
)
policy_text = (
"revision {revision}; self-submission {self_submission}; "
"max active tasks {max_active}; daily token budget {daily_budget}".format(
revision=policy["revision"],
self_submission="enabled" if policy["self_submission_enabled"] else "disabled",
max_active=policy["max_active_tasks"] or "unlimited",
daily_budget=policy["daily_token_budget"] or "unmetered",
)
if policy
else "no Cloud policy cached"
)
device_rows = "".join(
f"