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:
@@ -10,7 +10,7 @@ authentication can be added later without changing route signatures.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
|
||||
from cloud.auth import (
|
||||
PLUGINS_ADMIN_SCOPE,
|
||||
@@ -28,11 +28,14 @@ from cloud.sdk.models import (
|
||||
HostResponse,
|
||||
PluginRegistrationRequest,
|
||||
PluginResponse,
|
||||
TaskAttemptResponse,
|
||||
TaskListItem,
|
||||
TaskListResponse,
|
||||
TaskStatusResponse,
|
||||
TaskSubmissionRequest,
|
||||
TaskSubmissionResponse,
|
||||
)
|
||||
from fastapi import APIRouter, HTTPException, Request, status
|
||||
from fastapi import APIRouter, HTTPException, Query, Request, status
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cloud.plugins import PluginRegistry
|
||||
@@ -112,6 +115,76 @@ def create_cloud_router(
|
||||
failure_reason=task.failure_reason,
|
||||
)
|
||||
|
||||
@router.get("/tasks", response_model=TaskListResponse)
|
||||
def list_tasks(
|
||||
request: Request,
|
||||
status_filter: Literal[
|
||||
"queued", "assigned", "dispatched", "done", "failed"
|
||||
]
|
||||
| None = Query(default=None, alias="status"),
|
||||
limit: int = Query(default=50, ge=1, le=100),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
) -> TaskListResponse:
|
||||
_authorize(request, TASKS_READ_SCOPE)
|
||||
tasks = scheduler.store.list_tasks(
|
||||
status=status_filter,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
total = scheduler.store.count_tasks(status=status_filter)
|
||||
return TaskListResponse(
|
||||
items=[
|
||||
TaskListItem(
|
||||
id=task.id,
|
||||
status=task.status,
|
||||
goal=task.goal,
|
||||
workflow_definition_id=task.workflow_definition_id,
|
||||
assigned_device_id=task.assigned_device_id,
|
||||
assigned_host_id=task.assigned_host_id,
|
||||
attempt_count=task.attempt_count,
|
||||
failure_reason=task.failure_reason,
|
||||
created_at=task.created_at,
|
||||
)
|
||||
for task in tasks
|
||||
],
|
||||
total=total,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
@router.get(
|
||||
"/tasks/{task_id}/attempts",
|
||||
response_model=list[TaskAttemptResponse],
|
||||
)
|
||||
def list_task_attempts(
|
||||
task_id: str,
|
||||
request: Request,
|
||||
) -> list[TaskAttemptResponse]:
|
||||
_authorize(request, TASKS_READ_SCOPE)
|
||||
task = scheduler.store.get_task(task_id)
|
||||
if task is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"task {task_id!r} not found",
|
||||
)
|
||||
attempts = scheduler.store.list_task_attempts(task_id)
|
||||
return [
|
||||
TaskAttemptResponse(
|
||||
task_id=attempt.task_id,
|
||||
attempt=attempt.attempt,
|
||||
lease_id=attempt.lease_id,
|
||||
host_id=attempt.host_id,
|
||||
device_id=attempt.device_id,
|
||||
status=attempt.status,
|
||||
lease_expires_at=attempt.lease_expires_at,
|
||||
created_at=attempt.created_at,
|
||||
completed_at=attempt.completed_at,
|
||||
failure_reason=attempt.failure_reason,
|
||||
terminal_result=attempt.terminal_result,
|
||||
)
|
||||
for attempt in attempts
|
||||
]
|
||||
|
||||
@router.get("/devices", response_model=list[DeviceResponse])
|
||||
def list_devices(request: Request) -> list[DeviceResponse]:
|
||||
_authorize(request, POOL_READ_SCOPE)
|
||||
|
||||
Reference in New Issue
Block a user