chore(openspec): add cloud-console proposal
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-07-13
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
`apps/cloud-api/cloud_api/app.py` composes exactly two routers today: `create_cloud_router` (the versioned `/v1` platform SDK, `packages/cloud-platform/cloud/sdk/api.py`) and `create_internal_router` (Host Agent-only heartbeat/claim/renew/result). Neither mounts any static assets or HTML — confirmed by reading `app.py` in full, it has no `StaticFiles`/template mount. There is also **no CORS middleware configured** on the Cloud API app, unlike the local Runtime's `api/rest.py`, which enables permissive CORS specifically so the existing `console/` SPA can call it cross-origin during `npm run dev`.
|
||||||
|
|
||||||
|
Auth on `/v1/...` is `ConfiguredBearerAuthProvider` (`cloud/auth.py`): a flat list of pre-shared opaque tokens, each mapped to a `Principal(id, scopes, host_id)`. There is no login flow, no session, no user directory — every integrator (Host Agent, `CloudClient`, and now this console) authenticates the same way, by presenting `Authorization: Bearer <token>` on each request.
|
||||||
|
|
||||||
|
The repository (`cloud/repository.py` Protocol, implemented in `sql_repository.py` for both SQLite and PostgreSQL via the same SQLAlchemy code path) exposes `list_queued_tasks()` (queued-only, used internally by `TaskScheduler.assign()`) and `get_task(task_id)` (single lookup, backs `GET /v1/tasks/{id}`), but nothing that lists tasks across all statuses, and nothing paginated. `list_task_attempts(task_id)` is fully implemented (`sql_repository.py:531`) but has no route anywhere — it exists only for internal/audit use today.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
- Give an operator read visibility into tasks (all statuses, including history and per-attempt outcomes), the device pool, the host registry, and the plugin registry, without scripting REST calls.
|
||||||
|
- Let an operator perform the two write actions that are already safe, scope-gated, and exposed today — submit an ad-hoc task and register a plugin manifest — from the same UI, instead of adding new write capabilities.
|
||||||
|
- Reuse the Cloud Control Plane's existing bearer-token auth model exactly as `CloudClient` does — zero new auth surface.
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
- Task cancellation or forced retry — no scheduler/repository operation for this exists (`assign`/`claim`/`renew`/`record_result` only); adding one is a scheduling-capability change out of this proposal's scope.
|
||||||
|
- Host Agent remote start/stop — the Host Agent only initiates outbound calls (heartbeat/claim/renew/result); the control plane has no channel to push commands to it.
|
||||||
|
- Plugin de-registration — `CloudRepository` has no delete/deregister method for plugins.
|
||||||
|
- A new login/session/RBAC/user-management system — the console is just another bearer-token holder, provisioned the same way as any `CLOUD_PUBLIC_CREDENTIALS_JSON` entry.
|
||||||
|
- Real-time push (WebSocket/SSE) — polling only.
|
||||||
|
- A shared component library or JS monorepo tooling spanning `console/` and the new `cloud-console/` — they stay two fully independent SPA projects, matching `console/README.md`'s own "Independent Vue 3 + Vite SPA" precedent.
|
||||||
|
- Multi-cluster/multi-control-plane aggregation — `docs/CLOUD_DEPLOYMENT.md` already limits deployment to one scheduler-enabled Cloud API process; the console targets one Cloud API base URL at a time.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
- **Extend `platform-sdk` rather than invent a parallel console-only API.** Add `GET /v1/tasks` and `GET /v1/tasks/{task_id}/attempts` to the existing `cloud/sdk/api.py` router, under the same `/v1` prefix, same `tasks:read` scope, same `AuthProvider` hook. Rejected alternative: a separate `console-api` router mirroring the local Runtime's `console-status-api`/`console-config-api` split — rejected because the Cloud Control Plane's `/v1` surface is already the one stable, versioned, scope-gated contract every integrator uses; a second parallel surface would duplicate auth wiring for no benefit and would fork `platform-sdk`'s existing "client mirrors REST API" requirement into two inconsistent contracts.
|
||||||
|
- **New bounded, filterable task listing at the repository layer.** Add `list_tasks(*, status: ScheduledTaskStatus | None, limit: int, offset: int) -> list[ScheduledTask]` and `count_tasks(status: ScheduledTaskStatus | None) -> int` to the `CloudRepository` Protocol, implemented via the same SQLAlchemy query builder `sql_repository.py` already uses for both SQLite and PostgreSQL (no dialect-specific SQL branch). `GET /v1/tasks` caps `limit` server-side (`Field(..., le=100)`, default 50), ordered most-recent-first, mirroring the bounded-`Field` pattern `internal_api/models.py` already uses for `ClaimRequest.timeout_seconds`. Rejected alternative: reusing/widening `list_queued_tasks()` — rejected because it is scheduler-internal (assign-loop candidate selection), has no status filter or pagination, and callers outside the scheduler have no business depending on its exact contract.
|
||||||
|
- **`GET /v1/tasks/{task_id}/attempts` is a thin pass-through** to the existing `list_task_attempts()`, no new repository work — it just needed a route and a response model.
|
||||||
|
- **Extend `CloudClient` in the same change.** `platform-sdk`'s existing requirement states the Python client mirrors every `/v1/...` route; adding routes without adding client methods would silently break that invariant. Add `list_tasks(...)` and `get_task_attempts(...)` to `cloud/sdk/client.py` with matching tests.
|
||||||
|
- **Bearer token pasted by the operator, held in `sessionStorage`, never in `localStorage`.** On load, the console shows a token-entry screen if no token is present; every subsequent request attaches `Authorization: Bearer <token>`, exactly like `CloudClient`. Rejected alternative: building any login/username-password flow — there is no user directory to authenticate against; the Cloud Control Plane's entire identity model is pre-issued scoped tokens, and inventing a session layer on top would be new auth infrastructure this proposal has no reason to add. Operators are expected to hold a token scoped at least to `tasks:read`+`pool:read`+`plugins:read` (add `tasks:submit`/`plugins:admin` only if the console's write actions are needed), provisioned the same way as any other `CLOUD_PUBLIC_CREDENTIALS_JSON` entry.
|
||||||
|
- **Add configurable, closed-by-default CORS to the Cloud API.** `apps/cloud-api/cloud_api/app.py` currently has zero CORS middleware. Add an env-driven allow-list (e.g. `CLOUD_CONSOLE_CORS_ORIGINS`, comma-separated, empty by default) wired through `CloudControlConfig`/`load_control_config()`, applied via FastAPI's `CORSMiddleware` only when non-empty. Rejected alternative: permissive CORS like the local Runtime's dev-only setup — rejected because the Cloud API, unlike the single-developer local Runtime, is meant for real deployment (PostgreSQL, production `CLOUD_ENVIRONMENT`) where the entire auth model is a bearer token; blanket `allow_origins=["*"]` would let any origin that tricks an operator's browser into sending that token succeed. Default-empty keeps every existing deployment's behavior unchanged until an operator opts in with their console's actual origin.
|
||||||
|
- **New top-level `cloud-console/` project, sibling to `console/`, not nested under `packages/cloud-platform` or `apps/cloud-api`.** Mirrors the existing `console/` precedent (independent Node/Vue toolchain kept out of the Python `uv` workspace) and keeps the two SPAs — and the two backends and auth models they talk to — visibly separate rather than implying a shared deployment unit.
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- [No CORS today on the Cloud API] → Mitigation: ship the allow-list closed by default; existing deployments see no behavior change until they configure their console's origin.
|
||||||
|
- [Bearer token lives in browser storage] → Mitigation: `sessionStorage` only (cleared on tab close), never logged, sent only to the configured Cloud API base URL; document least-privilege scoping and the existing token-rotation guidance from `docs/CLOUD_DEPLOYMENT.md`.
|
||||||
|
- [Task history is unbounded over time, unlike the depth-capped queue] → Mitigation: server-side hard cap on page size (`le=100`), default ordering most-recent-first; client paginates rather than fetching everything.
|
||||||
|
- [New repository method must behave identically on SQLite and PostgreSQL] → Mitigation: implement through the same SQLAlchemy query builder every other `sql_repository.py` method already uses; cover both backends in the existing dual-backend repository test suite.
|
||||||
|
- [Two independent frontends (`console/`, `cloud-console/`) can drift in look-and-feel] → Mitigation: accepted for this change's scope, matching `console/`'s own "independent" precedent; revisit a shared design system only if a third console appears.
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
1. Add `list_tasks`/`count_tasks` to the `CloudRepository` Protocol and `sql_repository.py`, with unit/integration tests against both SQLite and PostgreSQL.
|
||||||
|
2. Add `GET /v1/tasks` and `GET /v1/tasks/{task_id}/attempts` to `cloud/sdk/api.py`, with response models in `cloud/sdk/models.py`.
|
||||||
|
3. Add matching `CloudClient` methods and tests in `cloud/sdk/client.py`.
|
||||||
|
4. Add the closed-by-default CORS allow-list to `apps/cloud-api/cloud_api/app.py` / `cloud/control_config.py`.
|
||||||
|
5. Scaffold `cloud-console/` (Vue 3 + Vite): token-entry screen, an API client wrapper that attaches the bearer token, and routing shell.
|
||||||
|
6. Build the dashboard views: task list/detail/attempts, device pool, host registry, plugin registry + registration form.
|
||||||
|
7. Document console usage and operator token provisioning in `docs/CLOUD_DEPLOYMENT.md`.
|
||||||
|
8. Rollback: everything is additive — two new GET routes, two new repository methods, opt-in CORS middleware, a new frontend project. No schema migration and no changes to existing task/attempt table columns, so rollback is simply removing the new routes/middleware and not deploying the frontend.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
- Should live task status use SSE/WebSocket push instead of polling? Deferred — polling matches this change's scale; revisit only if operators report polling latency is actually a problem.
|
||||||
|
- Should credential provisioning offer a bundled "console" scope alias instead of operators requesting `tasks:read`+`pool:read`+`plugins:read` separately? Deferred — documenting the combination in `docs/CLOUD_DEPLOYMENT.md` is enough for now; scope aliasing is an auth-model change beyond this proposal's impact.
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
The Cloud Control Plane (`apps/cloud-api` + `packages/cloud-platform/cloud`) is reachable only through its versioned REST/SDK surface — there is no Web UI. An operator who wants to see queue backlog, device/host health, or plugin registrations today has to script calls against `/v1/*` or query the database directly. Now that `cloud-control-plane-integration` has landed and archived the remote scheduling/lease/Host Agent loop, the missing operational visibility layer is the clearest gap left before this control plane is easy to run day-to-day. The existing `console/` SPA does not cover this: it talks only to the local single-device Runtime API (port 8000), not the Cloud Control Plane (port 8001, scoped bearer auth).
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Add a **task listing endpoint** (`GET /v1/tasks`, `tasks:read` scope) returning tasks across all statuses with status filtering and bounded pagination — today only `POST /v1/tasks` (submit) and `GET /v1/tasks/{id}` (single lookup) exist; the repository itself only exposes `list_queued_tasks()` (queued-only, used internally by the scheduler).
|
||||||
|
- Add a **task attempt-history endpoint** (`GET /v1/tasks/{task_id}/attempts`, `tasks:read` scope) exposing the already-implemented `CloudRepository.list_task_attempts()`, which today has no route at all.
|
||||||
|
- Extend the Python `CloudClient` with corresponding methods for both new endpoints, preserving `platform-sdk`'s existing requirement that the client mirrors every `/v1/...` route.
|
||||||
|
- Add an **independent Web console frontend** (new `cloud-console/` Vue 3 + Vite SPA at the repo root, sibling to the existing `console/`) that authenticates with an operator-supplied bearer token (pasted in, held client-side) and renders:
|
||||||
|
- a task queue/history view (list + filter by status, detail with attempt history),
|
||||||
|
- a device pool and host registry view (including staleness/unreachable status),
|
||||||
|
- a plugin registry view with a form to submit a new plugin manifest (`POST /v1/plugins`).
|
||||||
|
- Explicitly out of scope: task cancellation or retry-from-UI (no such scheduler operation exists), Host Agent remote start/stop (Host Agent only makes outbound calls; the control plane cannot push commands to it), plugin de-registration (no repository method exists), and any login/session/RBAC system beyond pasting a pre-issued scoped bearer token (mirrors how `CloudClient` already authenticates — no new auth mechanism).
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
- `cloud-console-ui`: Independent Vue 3 + Vite SPA for the Cloud Control Plane, consuming the platform SDK's `/v1` REST surface (existing endpoints plus the two added by this change) to provide read dashboards for tasks/devices/hosts/plugins and a plugin-registration action, authenticated via an operator-supplied bearer token.
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
- `platform-sdk`: add task listing (`GET /v1/tasks`, filterable/paginated) and task attempt-history (`GET /v1/tasks/{task_id}/attempts`) requirements to the existing versioned `/v1` surface, both scope-gated by `tasks:read`; extend the `CloudClient` requirement so it continues to mirror every route.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- **New code**: routes and response models in `packages/cloud-platform/cloud/sdk/api.py` / `cloud/sdk/models.py`; a new bounded/filtered task-listing method on `CloudRepository` (`repository.py` Protocol) implemented in `sql_repository.py`; new `CloudClient` methods in `cloud/sdk/client.py`; a new top-level `cloud-console/` Vue 3 + Vite SPA project (own `package.json`/build, no Python dependency).
|
||||||
|
- **Modified code**: `cloud/sdk/api.py` router gains two GET routes; no change to existing submission, assignment, claim, renewal, or result-recording behavior.
|
||||||
|
- **Dependencies**: no new backend dependency (reuses FastAPI/Pydantic already in `packages/cloud-platform`); the new frontend brings its own Node/Vue 3/Vite toolchain, matching the existing `console/` project's pattern.
|
||||||
|
- **Docs**: `docs/CLOUD_DEPLOYMENT.md` gains a section on running the console and obtaining/rotating an operator bearer token for it.
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Operator authenticates with a bearer token
|
||||||
|
The console SHALL require an operator-supplied bearer token before calling any Cloud Control Plane endpoint, SHALL hold that token only in browser session storage, and SHALL attach it as an `Authorization: Bearer` header on every request.
|
||||||
|
|
||||||
|
#### Scenario: No token present
|
||||||
|
- **WHEN** an operator opens the console without a previously entered token
|
||||||
|
- **THEN** the console shows a token-entry screen instead of any dashboard view
|
||||||
|
|
||||||
|
#### Scenario: Token rejected by the Cloud API
|
||||||
|
- **WHEN** the Cloud Control Plane responds `401` or `403` to a request carrying the stored token
|
||||||
|
- **THEN** the console clears the stored token and returns to the token-entry screen with a clear message
|
||||||
|
|
||||||
|
#### Scenario: Tab closed
|
||||||
|
- **WHEN** an operator closes the browser tab running the console
|
||||||
|
- **THEN** the stored bearer token is discarded and is not available on the next visit
|
||||||
|
|
||||||
|
### Requirement: Task dashboard
|
||||||
|
The console SHALL render a task view listing tasks by status with pagination, and SHALL show a task's detail including its attempt history, using the platform SDK's task-listing and attempt-history endpoints.
|
||||||
|
|
||||||
|
#### Scenario: Browse the task queue
|
||||||
|
- **WHEN** an operator with a `tasks:read`-scoped token opens the task view
|
||||||
|
- **THEN** the console displays tasks with their status, goal or workflow reference, and assigned device/host, and lets the operator filter by status
|
||||||
|
|
||||||
|
#### Scenario: Inspect a task's attempt history
|
||||||
|
- **WHEN** an operator selects a task from the list
|
||||||
|
- **THEN** the console displays that task's recorded attempts in order, including each attempt's outcome
|
||||||
|
|
||||||
|
### Requirement: Device pool and host registry views
|
||||||
|
The console SHALL render the current device pool and host registry, including stale/unreachable device status, using the platform SDK's device- and host-listing endpoints.
|
||||||
|
|
||||||
|
#### Scenario: View devices across hosts
|
||||||
|
- **WHEN** an operator with a `pool:read`-scoped token opens the device view
|
||||||
|
- **THEN** the console displays every pooled device with its owning host, driver type, and current status, including `unreachable` for devices owned by a stale host
|
||||||
|
|
||||||
|
#### Scenario: View registered hosts
|
||||||
|
- **WHEN** an operator opens the host view
|
||||||
|
- **THEN** the console displays every registered host with its last-seen timestamp
|
||||||
|
|
||||||
|
### Requirement: Plugin registry view with registration
|
||||||
|
The console SHALL render the registered plugin list and SHALL let an operator submit a new plugin manifest for registration, using the platform SDK's plugin-listing and plugin-registration endpoints.
|
||||||
|
|
||||||
|
#### Scenario: View registered plugins
|
||||||
|
- **WHEN** an operator with a `plugins:read`-scoped token opens the plugin view
|
||||||
|
- **THEN** the console displays every registered plugin with its `entry_point_kind` and whether it is wired to an execution path
|
||||||
|
|
||||||
|
#### Scenario: Register a plugin from the console
|
||||||
|
- **WHEN** an operator with a `plugins:admin`-scoped token submits a valid plugin manifest through the console's registration form
|
||||||
|
- **THEN** the console calls the plugin-registration endpoint and displays the newly registered plugin on success, or the API's validation/conflict error on failure
|
||||||
|
|
||||||
|
#### Scenario: Registration attempted without admin scope
|
||||||
|
- **WHEN** an operator whose token lacks `plugins:admin` submits the registration form
|
||||||
|
- **THEN** the console surfaces the API's authorization error without retrying or silently discarding the submission
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Task listing via the SDK
|
||||||
|
The system SHALL allow an external integrator to list tasks known to the `task-scheduler` capability across all statuses, through the platform SDK's API, with optional status filtering and bounded pagination.
|
||||||
|
|
||||||
|
#### Scenario: List tasks without a filter
|
||||||
|
- **WHEN** an integrator with `tasks:read` calls the task-listing endpoint with no status filter
|
||||||
|
- **THEN** the API returns tasks across all statuses, most recently created first, bounded to the requested (or default) page size
|
||||||
|
|
||||||
|
#### Scenario: List tasks filtered by status
|
||||||
|
- **WHEN** an integrator calls the task-listing endpoint with a status filter (e.g. `failed`)
|
||||||
|
- **THEN** the API returns only tasks currently in that status
|
||||||
|
|
||||||
|
#### Scenario: Page size exceeds the maximum
|
||||||
|
- **WHEN** an integrator requests a page size above the API's configured maximum
|
||||||
|
- **THEN** the API rejects the request rather than returning an unbounded result set
|
||||||
|
|
||||||
|
### Requirement: Task attempt history via the SDK
|
||||||
|
The system SHALL allow an external integrator to retrieve the attempt history of a known task — each attempt's assignment, lease, and terminal outcome — through the platform SDK's API, backed by the `task-scheduler` capability's attempt records.
|
||||||
|
|
||||||
|
#### Scenario: Retrieve attempt history for a known task
|
||||||
|
- **WHEN** an integrator with `tasks:read` requests the attempt history for a task id that exists
|
||||||
|
- **THEN** the API returns every recorded attempt for that task in chronological order, including its outcome
|
||||||
|
|
||||||
|
#### Scenario: Retrieve attempt history for an unknown task
|
||||||
|
- **WHEN** an integrator requests attempt history for a task id that does not exist
|
||||||
|
- **THEN** the API returns a not-found response rather than an unhandled server error
|
||||||
|
|
||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Python SDK client mirrors the REST API
|
||||||
|
The system SHALL provide a Python client (`CloudClient`) exposing methods corresponding to each `/v1/...` route (submit task, get task status, list tasks, get task attempt history, list devices, list hosts, list plugins, register plugin), so integrators do not need to hand-construct HTTP requests.
|
||||||
|
|
||||||
|
#### Scenario: Client submits a task and retrieves status
|
||||||
|
- **WHEN** a caller uses `CloudClient` to submit a task and then fetch its status by the returned id
|
||||||
|
- **THEN** the client's methods produce the same result as calling the corresponding `/v1/...` endpoints directly over HTTP
|
||||||
|
|
||||||
|
#### Scenario: Client lists tasks and retrieves attempt history
|
||||||
|
- **WHEN** a caller uses `CloudClient` to list tasks with a status filter and then fetch attempt history for one returned task id
|
||||||
|
- **THEN** the client's methods produce the same result as calling the corresponding `/v1/...` endpoints directly over HTTP
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
## 1. Repository: bounded task listing
|
||||||
|
|
||||||
|
- [ ] 1.1 Add `list_tasks(*, status, limit, offset)` and `count_tasks(status)` to the `CloudRepository` Protocol in `repository.py`
|
||||||
|
- [ ] 1.2 Implement both methods in `sql_repository.py` using the existing SQLAlchemy query builder (no dialect-specific SQL), ordered most-recent-first
|
||||||
|
- [ ] 1.3 Add unit/integration tests covering status filtering, pagination bounds, and empty results against both SQLite and PostgreSQL
|
||||||
|
|
||||||
|
## 2. Platform SDK API: task listing & attempt history
|
||||||
|
|
||||||
|
- [ ] 2.1 Add response models (task summary list item, task attempt) to `cloud/sdk/models.py`
|
||||||
|
- [ ] 2.2 Implement `GET /v1/tasks` in `cloud/sdk/api.py`: `tasks:read` scope, optional `status` query param, `limit` (default 50, max 100) / `offset` query params, calling the new repository methods
|
||||||
|
- [ ] 2.3 Implement `GET /v1/tasks/{task_id}/attempts` in `cloud/sdk/api.py`: `tasks:read` scope, 404 on unknown task id, calling `list_task_attempts`
|
||||||
|
- [ ] 2.4 Add tests for both endpoints: filtered/unfiltered listing, page-size-exceeds-max rejection, attempts for known/unknown task id, and scope enforcement (401/403)
|
||||||
|
|
||||||
|
## 3. Python SDK client parity
|
||||||
|
|
||||||
|
- [ ] 3.1 Add `list_tasks(...)` and `get_task_attempts(task_id)` methods to `CloudClient` in `cloud/sdk/client.py`
|
||||||
|
- [ ] 3.2 Add client tests asserting parity with direct HTTP calls to the two new endpoints
|
||||||
|
|
||||||
|
## 4. Cloud API CORS configuration
|
||||||
|
|
||||||
|
- [ ] 4.1 Add a `cors_allowed_origins` field (env `CLOUD_CONSOLE_CORS_ORIGINS`, comma-separated, default empty) to `CloudControlConfig`/`load_control_config()`
|
||||||
|
- [ ] 4.2 Wire `CORSMiddleware` into `apps/cloud-api/cloud_api/app.py`'s `create_app()`, added only when the allow-list is non-empty
|
||||||
|
- [ ] 4.3 Add a config/app test confirming CORS headers are absent by default and present only for a configured origin
|
||||||
|
|
||||||
|
## 5. Cloud console frontend (independent SPA)
|
||||||
|
|
||||||
|
- [ ] 5.1 Scaffold an independent Vue 3 + Vite SPA project at `cloud-console/` (own `package.json`/build tooling, sibling to `console/`)
|
||||||
|
- [ ] 5.2 Implement the token-entry screen and an API client wrapper that stores the bearer token in `sessionStorage` and attaches it to every request, clearing it and returning to the entry screen on `401`/`403`
|
||||||
|
- [ ] 5.3 Implement the task view: filterable/paginated list against `GET /v1/tasks`, and a detail view with attempt history against `GET /v1/tasks/{id}/attempts`
|
||||||
|
- [ ] 5.4 Implement the device pool and host registry views against `GET /v1/devices` and `GET /v1/hosts`
|
||||||
|
- [ ] 5.5 Implement the plugin registry view (list) and registration form against `GET /v1/plugins` and `POST /v1/plugins`, surfacing validation/conflict/authorization errors from the API
|
||||||
|
- [ ] 5.6 Document how to run the frontend dev server against a Cloud API base URL (env config) and the CORS origin it needs configured
|
||||||
|
|
||||||
|
## 6. Documentation
|
||||||
|
|
||||||
|
- [ ] 6.1 Add a section to `docs/CLOUD_DEPLOYMENT.md` covering: running the console, provisioning an operator bearer token (least-privilege scopes), and configuring `CLOUD_CONSOLE_CORS_ORIGINS`
|
||||||
|
|
||||||
|
## 7. Verification
|
||||||
|
|
||||||
|
- [ ] 7.1 Run the full backend test suite (`uv run --all-packages pytest -m "not integration"`) and confirm no regressions
|
||||||
|
- [ ] 7.2 Run the PostgreSQL-backed repository/integration tests for the new listing methods
|
||||||
|
- [ ] 7.3 Manually verify end-to-end: submit a task via the existing SDK, confirm it appears in the console's task list, transitions status, and its attempt history renders; confirm device/host/plugin views render against a running Host Agent
|
||||||
Reference in New Issue
Block a user