feat(cloud-console): task listing, attempt history, CORS, and console SPA

Implements the cloud-console OpenSpec change: adds GET /v1/tasks (filterable,
bounded pagination, tasks:read) and GET /v1/tasks/{id}/attempts (404 on unknown
task) to the platform SDK, with matching CloudClient methods and a closed-by-
default CLOUD_CONSOLE_CORS_ORIGINS allow-list wired through CloudControlConfig.
Ships an independent Vue 3 + Vite SPA at cloud-console/ that authenticates with
an operator-supplied bearer token held in sessionStorage, renders tasks with
attempt history, device pool, host registry, and the plugin registry with a
registration form.

Backend test suite: 438 passed (-m "not integration"); cloud-console typecheck
and production build both succeed. PostgreSQL-backed repository tests and
manual end-to-end verification remain pending external infrastructure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 14:00:23 +08:00
co-authored by Claude Opus 4.6
parent 62923b9285
commit 2169bb03d9
32 changed files with 3415 additions and 24 deletions
+11
View File
@@ -158,6 +158,17 @@ def create_app(
app = FastAPI(title="Device Cloud API", lifespan=lifespan)
if control_config.cors_allowed_origins:
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=list(control_config.cors_allowed_origins),
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.middleware("http")
async def correlation_logging(request: Request, call_next):
correlation_id = normalize_correlation_id(
+77
View File
@@ -608,3 +608,80 @@ def test_production_app_rejects_missing_credentials() -> None:
database_url="postgresql://db/cloud",
)
)
def test_cors_headers_are_absent_when_allow_list_is_empty() -> None:
app = create_app(config=CloudControlConfig(database_url="sqlite:///:memory:"))
with TestClient(app) as client:
response = client.options(
"/health/live",
headers={
"Origin": "http://console.example",
"Access-Control-Request-Method": "GET",
},
)
assert response.status_code >= 400
assert "access-control-allow-origin" not in {
key.lower() for key in response.headers
}
def test_cors_headers_reflect_configured_origin_only() -> None:
app = create_app(
config=CloudControlConfig(
database_url="sqlite:///:memory:",
cors_allowed_origins=("http://console.example",),
)
)
with TestClient(app) as client:
allowed = client.options(
"/health/live",
headers={
"Origin": "http://console.example",
"Access-Control-Request-Method": "GET",
},
)
blocked = client.options(
"/health/live",
headers={
"Origin": "http://attacker.example",
"Access-Control-Request-Method": "GET",
},
)
assert allowed.status_code in {200, 204}
assert allowed.headers["access-control-allow-origin"] == "http://console.example"
# An origin that is not on the allow-list must not be echoed back.
assert (
blocked.headers.get("access-control-allow-origin") != "http://attacker.example"
)
def test_load_control_config_parses_cors_allow_list() -> None:
from cloud.control_config import load_control_config
config = load_control_config(
env={
"CLOUD_ENVIRONMENT": "local",
"CLOUD_DATABASE_URL": "sqlite:///:memory:",
"CLOUD_CONSOLE_CORS_ORIGINS": (
"http://console.example, https://console.example"
),
}
)
assert config.cors_allowed_origins == (
"http://console.example",
"https://console.example",
)
def test_load_control_config_defaults_to_empty_cors_allow_list() -> None:
from cloud.control_config import load_control_config
config = load_control_config(
env={
"CLOUD_ENVIRONMENT": "local",
"CLOUD_DATABASE_URL": "sqlite:///:memory:",
}
)
assert config.cors_allowed_origins == ()