diff --git a/.specs/features/web-console/design.md b/.specs/features/web-console/design.md new file mode 100644 index 0000000..be6e0df --- /dev/null +++ b/.specs/features/web-console/design.md @@ -0,0 +1,304 @@ +# Web console design + +**Spec**: .specs/features/web-console/spec.md + +**Status**: Draft + +--- + +## Architecture overview + +Each web command starts the shared Node HTTP server in the CLI process. The server binds to 127.0.0.1, obtains its actual port after listen, creates the per-start security instance for that port, then opens or prints a token-bearing URL. The route table owns page registration and supplies its registered page links to the home renderer. Review keeps its existing root and /review aliases. + +Every request passes a Host check before route dispatch. POST routes also require the per-port session cookie and a matching HTTP Origin. A valid token query on an HTML GET bootstraps the cookie and redirects to the URL without the query token. The server closes with its owning command. + +Setup uses a pure planner that accepts a RunAgentConfig and selections, then returns a proposal and diff. Config reading, binding validation, and writing remain separate. The web apply path validates only bindings whose harness or model changed. Batch keeps validating only its winning --bind values. The terminal wizard continues accepting typed and off-catalog models without catalog validation. + +Usage shares one pure query parameter builder between the CLI and the web API. The page keeps its view-state, filter, and polling transitions in testable TypeScript functions and injects those function sources into its self-contained HTML string. + +~~~mermaid +flowchart LR + C[CLI commands] --> S[Shared loopback server] + S --> H[Host guard] + H --> O[Token and Origin guard on POST] + O --> R[Registered route table] + R --> P[Self-contained pages] + R --> A[HTTP API handlers] + A --> SP[Setup planner] + A --> V[Separate setup validation] + V --> W[Config writer] + A --> U[Usage query parameter builder] + U --> Q[fetchUsageQuery] + Q --> I[Daemon IPC] + Q --> D[Read-only SQLite fallback] +~~~ + +## Research notes + +The existing review command uses Node's built-in HTTP server and browser opener. The repository uses Node 24 or newer. The server and token can use node:http and node:crypto without another dependency. + +- Node.js v24 HTTP documentation: https://nodejs.org/download/release/latest-v24.x/docs/api/http.html +- Node.js v24 crypto documentation: https://nodejs.org/download/release/latest-v24.x/docs/api/crypto.html +- Context7 MCP was not available in this session. +- .specs/STATE.md is absent, so there are no active project decision entries to apply. + +## Code reuse analysis + +### Existing components to leverage + +| Component | Location | How to use | +| --- | --- | --- | +| Review port parsing, browser opening, and request behavior | src/cli/commands/review.ts:7-135 | Move server startup, port parsing, and browser opening into src/web/server.ts. Keep review data handling and both existing page routes. | +| Review page | src/web/review-page.ts:8-662 | Serve the current self-contained HTML string without adding assets. | +| Setup wizard selections | src/cli/commands/setup.ts:499-801 | Keep its harness, model, effort, orchestrator, sandbox, autocompact, skip, and profile choices. Pass completed selections through the planner before saving. | +| Picker free-text behavior | src/cli/picker-state.ts:113-141 | Preserve typed harness:model values, including models absent from the catalog. | +| Profile target resolution | src/cli/commands/setup.ts:831-853 | Use the current explicit and active profile rules for setup state and planning. | +| Setup batch validation | src/cli/commands/setup.ts:1402-1459 | Keep validation on winning --bind values in the batch caller and use the shared helpers from src/config/setup.ts. | +| Setup config reads | src/config/config.ts:391-405, 531-540 | Use readConfigForSetup for web state and apply so invalid JSON and read errors are not replaced by default config. Do not use loadConfig for apply. | +| Setup diff, target, envelope, and validation helpers | Current implementations are src/cli/commands/setup.ts:831-853, 1000-1036, 1229-1302; profile helpers are src/config/config.ts:48-76, 103-137, 237-257, 543-548 | Move resolveSetupTarget, SetupEnvelope, diffConfig, catalogContains, and validateBindings into src/config/setup.ts so both CLI and web callers import them without a config-to-CLI dependency. | +| Model catalog | src/core/models.ts:276-389 | Use getBatchModels for catalog reads and refresh. | +| Usage query and fallback | src/cli/commands/usage.ts:65-105 | Reuse fetchUsageQuery so CLI and web keep daemon usage.query and read-only SQLite fallback behavior. | +| Usage query and result types | src/daemon/protocol.ts:169-221 | Use UsageQueryParams and UsageQueryResult. Main already requires byOrigin at 80ec486 (#103); tolerate its absence only when an older daemon process answers over IPC. | +| Existing command and behavior tests | tests/review.test.ts, tests/review-command.test.ts, tests/setup-wizard.test.ts, tests/setup-cli-contract.test.ts, tests/usage-cli.test.ts | Preserve current contract assertions and add focused tests for new server, page logic, route handlers, and CLI options. | +| Setup README section | README.md:149-186 | Replace the picker-first description with web setup as the default and retain --tui and --refresh guidance. | + +### Integration points + +| System | Integration method | +| --- | --- | +| CLI command registry | Register codedeck ui while preserving the current review, setup, and usage command contracts. | +| Review data | Keep GET /api/review read-only and delegate to the current review loader. | +| Setup config | Read with readConfigForSetup, resolve the target, plan from RunAgentConfig, validate changed bindings separately, then save only a valid proposal. | +| Setup catalog | GET calls getBatchModels with allowNetwork:false. Protected refresh calls it with refresh:true, allowNetwork:true, and timeoutMs:12000. | +| Usage query | Convert URL filters to the options accepted by buildUsageQueryParams, then inject the result into fetchUsageQuery. | +| Daemon | No changes. HTTP stays in the CLI process; usage keeps its existing IPC and SQLite fallback. | + +## Components + +### Shared web server + +- **Purpose**: Start a loopback server, dispatch registered routes, open or print its URL, and close it with the CLI command. +- **Location**: src/web/server.ts +- **Interfaces**: + - createWebServer(options) creates a listener from a route table and injectable server dependencies. + - startWebServer(options) listens on 127.0.0.1 and returns its actual address, URL, and close operation. + - parseWebPort(value) accepts integer user ports from 1 through 65535. +- **Dependencies**: node:http, node:child_process, page and API route handlers, and the security guard. +- **Reuses**: Current listener, browser opener, and port behavior in src/cli/commands/review.ts. + +The injected server seam may listen on port 0 for tests. After listen, read server.address().port and pass that actual port to createWebSecurity before opening or printing the URL. The CLI parser still rejects port 0. Inject both server close and process exit functions so signal handling can be tested without process.exit. + +The browser opener and --no-open output use the initial route URL with ?t=. If opening fails, print that full URL and keep serving. SIGINT and SIGTERM close the server, then call the injected exit function. An occupied port reports the listen error and never reports a started URL. + +The route table identifies page routes separately from API routes and may attach a navigation label to a page. codedeck ui renders links only from its registered page routes. The review command selects the review page at both / and /review; codedeck ui selects the home page at /. + +### Request security + +- **Purpose**: Check Host on every request and protect each mutating route with a token cookie and Origin validation. +- **Location**: src/web/security.ts +- **Interfaces**: + - createWebSecurity(boundPort) creates one cryptographically random 32-byte token after listen. + - checkWebRequest(request, routePolicy) returns an allow result or HTTP 403. + - getTokenUrl(url, token) adds the out-of-band t query parameter to an initial HTML page URL. +- **Dependencies**: node:crypto and Node request headers. +- **Reuses**: Node's normalized IncomingMessage headers and the port resolved by the server. + +Accept only Host values 127.0.0.1: and localhost:, compared case-insensitively. Every POST requires an HTTP Origin whose host and port match that request Host and the bound port. A missing or mismatched Host, cookie, or Origin returns 403 before the route handler runs. + +Only an HTML GET with the valid t query token sets the host-only HttpOnly cookie named codedeck_ui_token_, with SameSite=Strict and Path=/. That response uses HTTP 303 and redirects to the same path without t. An HTML GET without a valid token does not set the cookie. The cookie value is checked against the current server token on every POST. Do not add CORS response headers. + +Every HTML response includes Content-Security-Policy: frame-ancestors 'none'. This applies to the home, review, setup, and usage pages. + +### Home page + +- **Purpose**: List links to pages registered by the active route table. +- **Location**: src/web/home-page.ts +- **Interfaces**: renderHomePage(pageRoutes) returns a self-contained HTML string for the passed route labels and paths. +- **Dependencies**: Registered page route metadata. +- **Reuses**: Inline HTML and CSS in src/web/review-page.ts. + +The home page does not hard-code future routes. The initial ui command links only the page routes it registers. Adding the setup and usage routes adds their links through the same route metadata. + +### Setup planner + +- **Purpose**: Apply setup selections to RunAgentConfig and return a proposed config and diff without I/O or catalog validation. +- **Location**: src/config/setup.ts +- **Interfaces**: + - buildSetupPlan(currentConfig, targetProfile, selections) returns proposedConfig and diff. + - resolveSetupTarget applies explicit and active profile rules. + - SetupSelection represents selected bindings, effort values, optional orchestrator mode and parameters, sandbox, autocompact, and per-role off-catalog confirmation. + - SetupEnvelope, diffConfig, catalogContains, and validateBindings are exported for the CLI and web routes. +- **Dependencies**: RunAgentConfig, Role, role binding, orchestrator, sandbox, and autocompact types; profile snapshot helpers; cached catalog result types. +- **Reuses**: Existing setup merge, target resolution, envelope, binding validation, and config diff behavior. + +Config reads happen before planning through readConfigForSetup. The shared config helper resolves explicit and active profiles using the current rules. An absent active profile keeps the existing SetupUsageError. A first-run target with all roles skipped keeps agents: {} as the empty sentinel. Turning autocompact off writes enabled=false only when the target already has an autocompact block. An omitted orchestrator selection preserves the target's current value or its absence, so an otherwise untouched selection has an empty diff. + +The planner does not produce binding validation results. Batch continues to validate only the winning --bind value for each role. Web apply compares each selected binding with the resolved target, then validates changed harness:model bindings against getBatchModels with allowNetwork:false. It does not validate effort-only changes. If a changed model is absent from that cached catalog, apply returns HTTP 422 unless the request includes offCatalogConfirmed[role]=true. The page sends that per-role confirmation only after an explicit user confirmation, matching the wizard's second Enter. An unchanged off-catalog binding does not block a sandbox or other unrelated change. + +### Setup page and API + +- **Purpose**: Show the current target and all wizard selections, produce a dry-run proposal, and save only a validated proposal. +- **Page location**: src/web/setup-page.ts +- **API location**: src/web/setup-routes.ts +- **Interfaces**: + - SETUP_PAGE is a self-contained HTML string with inline CSS and injected setup page behavior. + - createSetupRoutes(dependencies) returns the setup page, catalog, state, refresh, dry-run, and apply route definitions. + - buildSetupState(configRead, profileOption) returns the resolved target and current values needed for prefill. +- **Dependencies**: readConfigForSetup, resolveSetupTarget, getBatchModels, separate binding validation, buildSetupPlan, and the config writer. +- **Reuses**: Existing setup config, profile, diff, validation codes, and SetupEnvelope. + +GET /api/setup/state reads with readConfigForSetup and reports whether the resolved target is global or a named profile. It returns current bindings, per-role effort, orchestrator, sandbox, and autocompact values. Invalid config JSON returns code 14, and a config read error returns code 15. An active profile name without a snapshot returns the existing SetupUsageError with code 14 on apply. An explicit profile without a saved snapshot uses the current profile defaults. + +GET /api/setup/catalog calls getBatchModels with allowNetwork:false and returns its models, status, source, ageMs, cacheWriteFailed, and discoveryError when present. The protected POST /api/setup/catalog/refresh calls getBatchModels with refresh:true, allowNetwork:true, and timeoutMs:12000. If discovery is incomplete or a requested harness reports an error, getBatchModels returns its refresh fallback and discoveryError without partial network results. A refresh can return unavailable without previously fresh cache entries because getBatchModels drops fresh entries from its refresh fallback; the page keeps its already loaded catalog and shows discoveryError in that case. Concurrent refresh requests share one in-flight promise. + +Dry-run calls the planner and separate validator, then returns an object with the exact SetupEnvelope fields proposta, validacoes, mudancas, and resultado. It never writes config. Invalid JSON returns resultado.code=14 and a read error returns resultado.code=15. Apply uses the same proposal and validation steps, and writes only if the diff is non-empty and every changed binding validates. An empty diff returns unchanged without a write. + +The route reads config through readConfigForSetup, never loadConfig. Dry-run and apply return resultado.code=14 for invalid JSON and resultado.code=15 for a read error, without writing. GET /api/setup/state returns the matching code in its JSON error. Malformed or oversized requests return HTTP 400. Binding validation failures return HTTP 422 with the existing code and saved=false. Save failures return HTTP 500 with saved=false. The setup API has one route factory, createSetupRoutes. + +Setup selection, error rendering, and refresh state live in pure TypeScript page behavior functions. The HTML string injects those same function sources with Function.prototype.toString(). Node tests call the functions with fake fetch and timer adapters, then assert state changes and requests directly. Tests also extract the inline script from SETUP_PAGE and USAGE_PAGE, evaluate it in node:vm with a clean context plus stubbed fetch, timers, and document, then call the page functions from that context. This proves the injected code runs without module-scope dependencies. HTML substring checks may cover static markup but do not stand in for these behavior tests. A protected POST 403 changes setup page state to the reload/restart message from WEB-73. + +### Usage query parameter builder + +- **Purpose**: Produce identical UsageQueryParams for the CLI and the web API from the same options, working directory, and clock value. +- **Location**: src/core/usage-query.ts +- **Interfaces**: + - buildUsageQueryParams(opts, cwd, now) returns UsageQueryParams. +- **Dependencies**: UsageQueryParams and UsagePeriod from src/daemon/protocol.ts. +- **Reuses**: The aggregate filter logic currently in src/cli/commands/usage.ts:175-211. + +The CLI passes parsed options, process.cwd(), and the current date. The web handler converts its query string to the same option shape and passes its working directory and current date to the builder. The builder keeps the existing precedence: --all, --today, --days, then default today when since is absent. It maps 3, 7, and 30 to named periods, other positive day counts to a local-midnight since value, lets --current override --repo, and passes through since, until, model, and agent. + +### Usage page and API + +- **Purpose**: Return aggregate usage for browser filters and display results with all available breakdowns. +- **Page location**: src/web/usage-page.ts +- **API location**: src/web/usage-routes.ts +- **Interfaces**: + - USAGE_PAGE is a self-contained HTML string with inline CSS and injected usage page behavior. + - createUsageRoutes(dependencies) returns GET /usage and GET /api/usage handlers. + - parseUsageWebQuery(searchParams, cwd) maps the URL fields to the option shape for buildUsageQueryParams. +- **Dependencies**: buildUsageQueryParams and injected fetchUsageQuery. +- **Reuses**: UsageQueryParams, UsageQueryResult, UsageTotals, and UsageMetricBucket from src/daemon/protocol.ts. + +The endpoint returns the same UsageQueryResult as the aggregate CLI. Main already has the required byOrigin type at src/daemon/protocol.ts:220 from commit 80ec486 (#103). The page reads result.byOrigin ?? [] only because an older daemon process over IPC may omit the property. + +The page has controls for period, repo, model, agent, since, and until. A change to any control re-queries with the current filter set. The selected --by value is the initial highlighted breakdown; the page keeps every breakdown section accessible. It shows every UsageTotals field and the byDay, byRepository, byModel, byAgent, byRun, and when present byOrigin arrays. + +The page behavior functions own query state, filter changes, polling, render data, and error state. The HTML injects the same function source that Node tests import and call with fake fetch, timers, and render callbacks. Tests assert re-query-on-change, interval normalization, polling, origin-present and origin-absent results, and retention of the last good result after a failed query. + +Polling uses Math.max(1, Number(opts.interval) || 2). Commander supplies the default string "2", so the implementation cannot distinguish an omitted option from explicit --interval 2. Zero and NaN resolve to 2 seconds; negative values and positive values below 1 resolve to 1 second. For aggregate --web calls, the page refreshes itself; --watch does not select terminal rendering. A positional run ID or --run stays on usage.get and starts no server even with --web. --backfill also runs before web startup. --observe and --json retain their current single-run behavior. + +### CLI wiring + +- **Purpose**: Select the matching command path and preserve existing non-web contracts. +- **Locations**: src/cli/commands/ui.ts, src/cli/commands/review.ts, src/cli/commands/setup.ts, src/cli/commands/usage.ts, and src/cli/index.ts +- **Interfaces**: + - codedeck ui starts the home route table. + - codedeck review keeps /, /review, and GET /api/review. + - Interactive codedeck setup with a TTY opens /setup unless --tui selects the frozen wizard. Without both TTYs, no-batch setup keeps its current exit code 1 and terminal message. + - Setup batch flags continue to call runSetupBatch with their current output and status contracts. + - Aggregate codedeck usage opens /usage only with --web. + - When --backfill is absent, positional or --run usage IDs keep usage.get and do not start the server, including usage --web --json. --by origin remains aggregate-only. +- **Dependencies**: Shared server, route factories, page constants, query builder, and existing CLI parser. +- **Reuses**: Existing Commander registrations and branches. + +## Data models + +### Setup selection + +~~~typescript +interface SetupSelection { + agents: Partial> + orchestrator?: OrchestratorMode + sandbox?: RunAgentConfig["defaultSandbox"] + autocompact?: RunAgentConfig["autocompact"] + offCatalogConfirmed?: Partial> +} +~~~ + +Missing role entries mean the user skipped that role. An omitted orchestrator preserves its current value or absence. offCatalogConfirmed records explicit per-role confirmation for changed models absent from the cached catalog. A profile name is supplied by CLI target resolution and is never accepted from the browser request body. A first-run config with no selected roles retains the agents: {} sentinel. + +### Setup plan + +~~~typescript +interface SetupPlanResult { + proposedConfig: RunAgentConfig + diff: SetupDiff +} +~~~ + +The current config input is RunAgentConfig. Catalog validation and SetupConfigRead are not planner inputs. The CLI route adapts the result, separate validation result, and config status into the existing SetupEnvelope fields. + +### Usage result + +The route returns UsageQueryResult. The type in main requires byOrigin and contains UsageMetricBucket[]. At runtime, the page treats an absent field as an empty origin breakdown only for an older daemon process. + +## Error handling strategy + +| Error scenario | Handling | User impact | +| --- | --- | --- | +| Host, token, or Origin rejected | Return HTTP 403 before route handler invocation. | No protected action runs. | +| Stale session after server restart | Return HTTP 403; page shows the reload/restart message. | The user opens the new token URL if reload does not restore the session. | +| Invalid port or listen failure | Print the error and exit with code 1. | The command does not claim that a server is available. | +| Malformed or oversized setup body | Return HTTP 400 and do not write config. | The user can correct or retry the request. | +| Invalid setup config JSON | Return code 14 and do not write. | Existing invalid config remains available for repair. | +| Setup config read error | Return code 15 and do not write. | The user sees the original read error. | +| Changed binding validation fails | Return HTTP 422 with the existing validation code and saved=false. | The page can identify the changed binding that failed. | +| Config save fails | Return HTTP 500 with saved=false and the existing save error. | The server does not report that the proposal was saved. | +| Catalog discovery is incomplete or errors | Return getBatchModels fallback and discoveryError, without partial network results. | The page shows the actual cache or unavailable state. | +| Usage query fails | Return HTTP 500 with JSON error; preserve the last successful page result. | A later poll or filter change can recover. | + +## Risks & concerns + +| Concern | Location (file:line) | Impact | Mitigation | +| --- | --- | --- | --- | +| The review command combines server lifecycle, routing, port parsing, and browser opening. | src/cli/commands/review.ts:7-135 | A careless extraction can change the current review behavior. | Keep review route and command tests in the rewiring task. | +| Setup selection and persistence live in a long wizard command. | src/cli/commands/setup.ts:625-801 | Duplicated planning could change profiles, skips, or config merge behavior. | Extract one pure planner and test it against current setup behavior. | +| loadConfig swallows read and parse errors. | src/config/config.ts:531-540 | Web apply could treat invalid config as defaults and overwrite it. | Use readConfigForSetup and test codes 14 and 15 before any write. | +| The current review handler calls process.exit during shutdown. | src/cli/commands/review.ts:128-131 | Signal handling is hard to assert and can terminate a test runner. | Inject close and exit functions into the shared server. | +| Model discovery can return incomplete results after network errors. | src/core/models.ts:329-389 | A UI that displays partial provider data would imply a complete catalog. | Use getBatchModels only and render its fallback plus discoveryError. | +| UsageQueryResult in main requires byOrigin, but an older daemon process can answer IPC without the field. | src/daemon/protocol.ts:208-221; commit 80ec486 (#103) | Direct access can crash page rendering against an older process. | Keep the type required and use a runtime fallback only for that daemon response. | +| Page behavior currently has no DOM test dependency. | tests/review.test.ts, tests/setup-wizard.test.ts | Static markup checks cannot prove polling or state transitions. | Test exported pure page functions with fake fetch and timers in Node. | + +## Tech decisions + +| Decision | Choice | Rationale | +| --- | --- | --- | +| Home command | codedeck ui | It gives the route table a dedicated entry and keeps review at its current root. | +| Home links | Build from registered page routes. | P1 must not advertise a page that is not registered until its phase is complete. | +| Shared port | Default 3100; user values 1 through 65535; test seam may bind port 0. | This preserves review behavior while allowing ephemeral integration-test ports. | +| Bound port | Read server.address().port after listen, then create security with that value. | Host checks and cookie names must use the actual bound port. | +| Shutdown | Inject close and exit functions; close before exit. | Signal tests can assert both operations without terminating Vitest. | +| Token | 32 cryptographically random bytes, sent only in the initial URL query. | The page does not need to embed the token in HTML or JavaScript. | +| Token cookie | codedeck_ui_token_, host-only, HttpOnly, SameSite=Strict, Path=/. | Cookie storage does not isolate by port, so each server port gets a distinct name. | +| Token bootstrap | On valid token HTML GET, set the cookie and return HTTP 303 to the same route without t. | This removes the token from the visible URL after establishing the session. | +| Action security | Host-check every route; require cookie and matching HTTP Origin on every POST. | Read routes remain locally constrained and all mutating routes share one rule. | +| HTML framing | Add Content-Security-Policy: frame-ancestors 'none' to every HTML response. | Local pages should not be embedded by another origin. | +| Catalog source | Use getBatchModels for both GET and refresh. | One helper defines cache, discovery timeout, incomplete-result, and fallback behavior. | +| Setup apply catalog | Call getBatchModels with allowNetwork:false when validating changed bindings. | Apply checks the cache and does not start discovery. | +| Planner boundary | Accept RunAgentConfig and selections; return proposed config and diff only. | Validation depends on the selected catalog and belongs in separate callers. | +| Validation | Web validates changed harness:model pairs against the cached catalog with allowNetwork:false and requires per-role confirmation for off-catalog models; batch validates winning --bind entries; wizard adds no catalog validation. | This keeps web apply offline, preserves explicit user choice, and leaves existing wizard behavior intact. | +| Page tests | Export behavior functions from the page module and inject their source into HTML. | Node tests call the functions directly and run extracted inline scripts in node:vm without a DOM package. | +| Usage query params | Extract buildUsageQueryParams into src/core/usage-query.ts and call it from CLI and web. | A pure shared function makes filter mapping and parity testable. | +| Usage origin | Use the required byOrigin type already present in main at 80ec486 (#103); handle a missing runtime field only from an older daemon process. | Current callers use the complete type while IPC remains compatible with an earlier running daemon. | +| Aggregate usage --json | With --web, open /usage and print the token URL; retain JSON for aggregate calls without --web and for single-run calls. | The web option selects the aggregate interface while run-id callers retain their exact JSON path. | +| Usage --by | Use --by as the initially highlighted breakdown and keep all sections accessible. | The browser can show other metrics without discarding the CLI preference. | +| Usage polling | Use Math.max(1, Number(opts.interval) || 2); --watch does not select terminal output with --web. | This retains the current CLI normalization and default string behavior. | +| Setup --refresh | Open /setup and start a protected catalog refresh on page load. | It retains the existing flag without adding a discovery job API. | +| Setup --json with --port | Reject before starting a web server. | --json selects the batch interface and --port selects the web interface. | +| Setup batch flags with --port | Reject --dry-run and --non-interactive with --port before starting a server. | Both flags select the batch path. | +| Usage --web with --tui | Return a usage error before starting a web server. | One invocation cannot select the browser and terminal dashboard together. | +| Setup request size | Limit JSON bodies to 64 KiB. | The complete role selection is bounded and parseable before planning. | +| Setup HTTP status | Use 400 for malformed input, 422 for validation failures, and 500 for save failures. | Browser callers receive stable transport statuses while resultado.code retains CLI codes. | + +## Phase dependencies + +| Phase | Scope | Depends on | +| --- | --- | --- | +| P1 | Shared server, review compatibility, home page, and codedeck ui | None | +| P2 | Host, token, Origin, CSP, and server guard | P1 | +| P3 | Pure setup planner and wizard reuse | P2 | +| P4 | Setup page, routes, command wiring, and README update | P3 | +| P5 | Usage page, routes, command wiring, and query parity | P4 | + +These artifacts stop at planning. They do not authorize source, test, plugin, or README changes during this documentation correction. diff --git a/.specs/features/web-console/spec.md b/.specs/features/web-console/spec.md new file mode 100644 index 0000000..b6d0d72 --- /dev/null +++ b/.specs/features/web-console/spec.md @@ -0,0 +1,355 @@ +# Web console specification + +## Problem Statement + +Setup and usage analytics currently require the terminal, while review already serves a local page from the CLI. A browser console will make setup and usage easier to inspect while preserving the existing TUI and machine-readable command paths. + +## Goals + +- [ ] Serve Review, Usage, and Setup from one local HTTP server started by a CLI command. +- [ ] Keep the server bound to loopback and require token, Host, and Origin checks for actions that change state. +- [ ] Reuse a pure setup planner from the terminal wizard and the web setup flow without adding catalog validation to the planner. +- [ ] Preserve existing setup batch, usage JSON, single-run, backfill, observation, and statusline contracts. +- [ ] Render usage totals and all supported breakdowns in the browser, with filters and periodic refresh. +- [ ] Keep every page self-contained and test page behavior in Node without a DOM dependency. + +## Current state (verified) + +- Review owns its Node HTTP server, port parser, browser opener, and request handler in src/cli/commands/review.ts:1-135. It defaults to port 3100, accepts --port and --no-open, binds to 127.0.0.1, serves the same review page at GET / and GET /review, and serves read-only GET /api/review. Existing tests are in tests/review.test.ts:193-230 and tests/review-command.test.ts:5-27. +- src/web/review-page.ts:8-662 exports one self-contained HTML string with inline CSS and JavaScript. +- Usage option parsing and CLI branches are in src/cli/commands/usage.ts:108-130 and :131-261. fetchUsageQuery at :65-105 uses daemon IPC usage.query and falls back to read-only SQLite. The positional run-id path calls usage.get at :144-173. +- src/daemon/protocol.ts:208-221 defines UsageQueryResult with byOrigin. Usage origin support, --backfill, --observe, and --by origin are already in main as squash commit 80ec486 (#103), so P5 has no pending branch dependency. An older daemon process reached over IPC may omit byOrigin, and the page handles that runtime case with an empty origin breakdown. +- The setup command and wizard are in src/cli/commands/setup.ts:625-801 and :1560-1626. A non-batch invocation without both stdin and stdout TTYs exits with code 1 and prints “setup needs a terminal on both stdin and stdout”; it starts no discovery and writes no config. The behavior is covered by tests/setup-cli-contract.test.ts:315-334 and tests/setup-wizard.test.ts:1220-1243. +- The setup wizard allows typed harness:model values through src/cli/picker-state.ts:113-141 and keeps configured models that are absent from the catalog in src/cli/commands/setup.ts:499-518. It also preserves a role's binding and effort when their screens are skipped; first-run skips produce an empty agents object sentinel. Tests are in tests/setup-wizard.test.ts:579-592, :757-764, and :1179-1183. +- The setup command resolves explicit and active profiles in src/cli/commands/setup.ts:831-853. An active profile that does not exist raises SetupUsageError. An explicit profile without a saved snapshot starts from profile defaults with agents: {}. +- SetupEnvelope in src/cli/commands/setup.ts:1000-1036 uses top-level fields proposta, validacoes, mudancas, and resultado. runSetupBatch reads config without replacing invalid JSON and returns code 14 for invalid config or 15 for a read error at :1369-1387. It validates only winning --bind entries at :1404 and :1444-1459. loadConfig in src/config/config.ts:531-540 swallows read and parse errors, so web setup must use readConfigForSetup. +- getBatchModels in src/core/models.ts:276-389 supports allowNetwork:false for cached reads and refresh:true for network discovery, with a default 12,000 ms timeout. If discovery is incomplete or a requested harness reports an error, it discards partial network results and returns the cache fallback with status and discoveryError. +- Custom orchestrator parallelism accepts positive finite numbers in src/cli/commands/setup.ts:319-345 and :361-394. README.md:149-186 currently describes setup as a terminal picker. + +## Out of Scope + +| Feature | Reason | +| --- | --- | +| Removing or adding features to the setup TUI or usage TUI | Both remain frozen and reachable behind their flags. Their removal is a later feature. | +| Moving HTTP into the daemon | The server lives in the CLI process and only while its command runs. | +| Remote access or authentication beyond the per-start token | The server binds only to 127.0.0.1. | +| Profile management beyond the existing --profile target | The web setup edits only the target already selected by the CLI. | +| Changing setup batch, usage JSON, usage.get, backfill, observe, or statusline contracts | Existing command and agent callers keep their current interfaces and outputs. | +| New review page features | Review keeps its current page and GET API behavior. | +| Build tooling, a frontend framework, or frontend dependencies | Pages remain self-contained HTML strings with inline CSS and JavaScript. | + +--- + +## Assumptions & Open Questions + +| Assumption / decision | Chosen default | Rationale | Confirmed? | +| --- | --- | --- | --- | +| Home page entry command | codedeck ui | This is the requested default and leaves codedeck review at its current root page. | Yes | +| Setup TUI entry | codedeck setup --tui | This is the requested default for keeping the frozen wizard reachable. | Yes | +| Usage web entry | codedeck usage --web | This is the requested opt-in entry and leaves plain usage output unchanged. | Yes | +| Non-TTY setup | Without batch flags, lack of either stdin or stdout TTY keeps the current code 1 and message; no server starts. | This is the orchestrator's confirmed decision and preserves current tests. | Yes | +| Shared web port | 3100 by default; web commands accept --port values from 1 through 65535. | Review already uses this default and range. Port 0 is available only through the injected server test seam. | No | +| No-browser behavior | --no-open or a failed browser opener prints the full initial URL, including ?t=, and keeps serving until SIGINT or SIGTERM. | The printed link must bootstrap the same per-start browser session as openBrowser. | No | +| Host allowlist | Accept only 127.0.0.1: and localhost:, case-insensitively; reject absent or other Host values with HTTP 403. | These are the only accepted browser hostnames for the loopback server. | No | +| Token transport | Generate 32 cryptographically random bytes per server start; carry the token as the t query parameter in the opened and printed initial URL. | The page receives the token out of band and does not embed it in HTML or JavaScript. | No | +| Session cookie | Set a host-only HttpOnly; SameSite=Strict; Path=/ cookie named codedeck_ui_token_ only after a valid token GET. | Browser cookies do not isolate by port, so the name prevents one local CodeDeck port from replacing another port's token. | No | +| Token bootstrap redirect | A valid token GET to an HTML route sets the cookie and returns HTTP 303 to the same path without t. | The token is removed from the address bar after bootstrap. | No | +| Origin comparison | Require an HTTP Origin whose host and port match the accepted request Host and bound port on every POST; return HTTP 403 if absent or different. | This gives every action route one same-origin rule. | No | +| Home links | Render a link only when its page route is registered in the active route table. | P1 must not advertise Usage before P5 registers it. | No | +| Setup catalog source | GET calls getBatchModels with allowNetwork:false; protected refresh calls it with refresh:true, allowNetwork:true, and timeoutMs:12000. | This is the single catalog source and matches the batch helper's fallback behavior. | No | +| Concurrent catalog refresh | Requests during one discovery share one in-flight refresh promise. | Duplicate provider requests do not improve the displayed result. | No | +| Setup --refresh behavior | On an interactive setup web launch, open /setup and request protected catalog refresh on page load. | This preserves the current flag while keeping discovery behind the action route. | No | +| Setup request size | Accept JSON bodies up to 64 KiB; return HTTP 400 for larger or malformed bodies. | A full role selection is small and a fixed limit bounds request parsing. | No | +| Setup API status mapping | Malformed input returns 400; binding validation failures return 422; save failures return 500. The SetupEnvelope records the existing setup code and saved=false. | The page receives a stable HTTP result and the existing validation codes remain visible. | No | +| setup --json with --port | Reject the combination through the setup usage-error path before starting a server. | --json selects the existing batch contract while --port selects web startup. | No | +| Usage --by with --web | Use --by as the initially selected breakdown; keep all available breakdown sections on the page. | This preserves the CLI grouping preference without hiding the other web sections. | No | +| Usage interval | Use a floor of 1 second and a numeric-conversion fallback of 2 seconds for web polling. | Commander supplies the default string "2", so the implementation cannot distinguish an omitted option from explicit --interval 2. | No | +| Aggregate usage --web --json | Start the browser page and print the token URL; --json affects aggregate output only when --web is absent. | The browser is the selected aggregate interface, while the positional run-id JSON contract remains unchanged. | No | +| Catalog refresh method | Use protected POST /api/setup/catalog/refresh with refresh:true, allowNetwork:true, and timeoutMs:12000. | Discovery may update the model cache, so it uses the guarded action path and the helper's current timeout. | No | +| Web apply catalog validation | Validate changed harness:model bindings against the cached catalog with getBatchModels({allowNetwork:false}); reject an off-catalog model with HTTP 422 unless the request includes explicit confirmation for that role. | This keeps apply offline and mirrors the wizard's second-Enter confirmation for typed models absent from its catalog. | Yes | +| Page behavior tests | Put state, rendering decisions, and polling transitions in TypeScript functions exported from each page module, then inject their source into its HTML string. | Node tests can exercise the same functions without installing a DOM implementation. | No | +| Usage origin dependency | Use the required byOrigin type already present in main at 80ec486 (#103); tolerate an absent field only when an older daemon process answers over IPC. | The implementation branch has the usage changes already, while an older running daemon can still use the earlier protocol shape. | Yes | +| Usage query failure | Return HTTP 500 with a JSON error, show the error in the page, and retain the last successful result. | A later poll can recover without removing the visible result. | No | +| Usage run ID with --web | When --backfill is absent, preserve the usage.get single-run branch, including --observe and --json, and start no web server when a positional ID or --run is present; --by origin remains aggregate-only. | The branch is used by the statusline and is evaluated before aggregate web startup. | No | +| Usage --backfill with --web | Run the existing backfill branch and start no web server. | The CLI checks --backfill before single-run and aggregate handling. | No | +| --watch with --web | The browser polls; --watch does not select terminal rendering. The normalized --interval value controls browser polling. | The web page replaces terminal watch output for this invocation. | No | +| Active profile missing | Surface the existing SetupUsageError and do not silently switch to a global target. | This preserves resolveSetupTarget behavior. | No | + +**Open questions:** none. Defaults without a human decision are recorded above with their rationale. + +## User Stories + +### P1: Shared local server and home page + +**User Story**: As a CodeDeck user, I want one local browser entry point so that I can use registered Review, Usage, and Setup pages without changing their command-line data paths. + +**Why P1**: The shared server and route table are the base for every browser page. + +**Acceptance Criteria**: + + +1. WHEN a web entry command starts its server THEN the CLI SHALL bind only to 127.0.0.1. WEB-01 +2. WHILE a web command is running THEN SIGINT or SIGTERM SHALL close its server and invoke the injected exit function. WEB-02 +3. WHEN a web entry command starts without --port THEN the server SHALL listen on port 3100. WEB-03 +4. IF a user-supplied --port is not an integer from 1 through 65535 THEN the command SHALL report the port error, exit with code 1, and skip server startup. WEB-04 +5. WHEN codedeck review serves GET / THEN the server SHALL return REVIEW_PAGE with HTTP 200 and text/html content type. WEB-05 +6. WHEN codedeck review serves GET /review THEN the server SHALL return the same review page as GET /. WEB-06 +7. WHEN the browser sends GET /api/review THEN the handler SHALL pass ref (default HEAD) and optional file to the existing review loader and return its result. WEB-07 +8. WHEN the home page is rendered THEN it SHALL link every registered page route and no unregistered page route. WEB-08 +9. IF the requested port is already in use THEN the command SHALL print the listen error, exit with code 1, and not print a started-server URL. WEB-57 +10. WHEN the test seam listens on port 0 THEN the server SHALL use server.address().port when constructing its URL and security instance. WEB-75 +**Independent Test**: Start the server on an ephemeral test port, request each registered page and API route, and assert the address, status, content type, home links, and shutdown callbacks. + +### P2: Local request security + +**User Story**: As a local user, I want browser actions limited to the local server instance I started so that another site cannot submit setup changes. + +**Why P2**: Setup apply and catalog refresh must reject forged requests before state changes occur. + +**Acceptance Criteria**: + + +1. IF Host is absent or matches neither 127.0.0.1: nor localhost: THEN the server SHALL return HTTP 403 before route dispatch. WEB-10 +2. WHEN the server starts THEN it SHALL generate one token from 32 cryptographically random bytes for that process. WEB-11 +3. WHEN an action POST is dispatched THEN the server SHALL require the host-only cookie named codedeck_ui_token_ and return HTTP 403 when its value is missing, stale, or invalid. WEB-12 +4. WHEN an action POST is dispatched THEN the server SHALL require an HTTP Origin matching the request Host and bound port and return HTTP 403 when Origin is absent or different. WEB-13 +5. WHEN an HTML GET carries the valid t query token THEN the server SHALL set the port-specific cookie and redirect to the same path without t. WEB-71 +6. IF an HTML GET lacks a valid t query token THEN the server SHALL NOT set the session cookie. WEB-72 +7. IF a page receives HTTP 403 for a protected action THEN its page logic SHALL show “This CodeDeck session has expired. Reload the page. If it still fails, restart the command and open its new URL.” WEB-73 +8. WHEN the server returns an HTML page THEN the response SHALL include Content-Security-Policy: frame-ancestors 'none'. WEB-74 +9. IF --no-open is set or the browser opener fails THEN the command SHALL print the full initial URL, including ?t=, and keep serving. WEB-09 +**Independent Test**: Send accepted and rejected Host, token, and Origin combinations to protected and read routes. Confirm rejected action requests return 403 without invoking handlers, bootstrap redirects strip t, and all HTML responses include the framing policy. + +### P3: Shared setup planning + +**User Story**: As a user of either setup interface, I want the same selections to produce the same proposed config and diff so that the wizard and browser cannot drift in config assembly. + +**Why P3**: The browser can only match setup behavior after config planning is separated from terminal I/O. + +**Acceptance Criteria**: + + +1. WHEN the setup planner receives a current RunAgentConfig, resolved target, and complete selections THEN it SHALL return a proposed config and diff without reading or writing files, discovering models, or validating against a catalog. WEB-14 +2. WHEN the selection contains a role binding THEN the planner SHALL set that role's harness, model, and optional effort to the selected values. WEB-15 +3. WHEN a role is omitted from selections THEN the planner SHALL preserve that role's current binding. WEB-86 +4. WHEN the planner updates selected config fields THEN it SHALL preserve unrelated config keys. WEB-87 +5. WHEN the selection contains an orchestrator mode THEN the planner SHALL put its parameter values in the proposed config without persisting a preset label. WEB-16 +6. WHEN the selection contains a sandbox value THEN the planner SHALL set defaultSandbox to workspace-write or danger-full-access as selected. WEB-17 +7. WHEN the selection turns autocompact on THEN the planner SHALL set autocompact.enabled to true and preserve other autocompact fields. WEB-18 +8. WHEN setup receives an explicit --profile target THEN the planner SHALL update only that profile snapshot. WEB-19 +9. WHEN setup has no explicit --profile and an existing active profile THEN the planner SHALL use the resolved active profile snapshot. WEB-62 +10. WHEN the terminal wizard completes selections THEN it SHALL use the shared planner without adding catalog validation to the wizard path. WEB-22 +11. WHEN the selection turns autocompact off THEN the planner SHALL set enabled to false if the target config has an autocompact block and SHALL preserve the absent block otherwise. WEB-61 +**Independent Test**: Call the planner with global and profile RunAgentConfig values. Assert exact proposal and diff paths for each field, skipped role behavior, and absence of file, network, and catalog-validation calls. Run the existing setup wizard and CLI contract tests unchanged. + +### P4: Browser setup + +**User Story**: As a user configuring CodeDeck, I want to review the current setup and a complete proposal in a browser before it is saved. + +**Why P4**: This makes the browser the setup interface for interactive sessions while preserving the batch and TUI paths. + +**Acceptance Criteria**: + + +1. WHEN the browser requests GET /setup THEN the server SHALL return the self-contained setup page with HTTP 200. WEB-23 +2. WHEN the browser requests GET /api/setup/catalog THEN the handler SHALL call getBatchModels with allowNetwork:false and return models, status, source, ageMs, cacheWriteFailed, and discoveryError when present without starting discovery. WEB-24 +3. WHEN the browser requests GET /api/setup/state THEN the handler SHALL return whether the target is global or a named profile and the current bindings, per-role effort, orchestrator, sandbox, and autocompact values for page prefill. WEB-63 +4. IF the active profile does not exist THEN the state handler SHALL return the existing SetupUsageError message without selecting the global target. WEB-84 +5. WHEN the user requests catalog refresh THEN the setup page logic SHALL show “Discovering models...” until the refresh response completes. WEB-25 +6. WHILE catalog discovery is in progress THEN concurrent refresh requests SHALL share the same in-flight refresh promise. WEB-26 +7. WHEN the browser posts selections to /api/setup/dry-run THEN the handler SHALL return a JSON object with exact top-level fields proposta, validacoes, mudancas, and resultado. WEB-27 +8. WHEN the browser posts selections to /api/setup/dry-run THEN the handler SHALL leave the config file unchanged. WEB-28 +9. IF the proposal has a non-empty diff and all changed bindings validate THEN apply SHALL save it and return resultado.status=applied with saved=true. WEB-29 +10. IF the setup selection omits orchestrator and all other values match the resolved target THEN the planner SHALL preserve the target's orchestrator value or its absence and apply SHALL return resultado.status=unchanged with saved=false without writing config. WEB-30 +11. IF a setup POST body is malformed, larger than 64 KiB, or has an invalid shape THEN the handler SHALL return HTTP 400 without saving config. WEB-31 +12. IF changed binding validation fails THEN the handler SHALL return HTTP 422, include the existing validation code in resultado.code, and set saved=false. WEB-32 +13. IF saving the config fails THEN the handler SHALL return HTTP 500 with saved=false and the config error message. WEB-33 +14. IF the config contains invalid JSON THEN web apply SHALL return resultado.code=14 and SHALL NOT write the config. WEB-76 +15. IF reading the config file fails THEN web apply SHALL return resultado.code=15 and SHALL NOT write the config. WEB-77 +16. WHEN codedeck setup runs with a TTY, no batch flags, and without --tui THEN it SHALL start web setup at /setup. WEB-34 +17. WHEN codedeck setup runs with --tui and a TTY THEN it SHALL run the existing terminal wizard. WEB-35 +18. WHEN codedeck setup runs with existing batch flags THEN it SHALL keep the runSetupBatch JSON, dry-run, bind, exit-code, validation, and save contracts unchanged, with winning --bind validation outside the pure planner. WEB-36 +19. WHEN the setup page is rendered THEN it SHALL offer harness and model choices for every role in ROLES. WEB-37 +20. WHEN the user enters harness:model text absent from the displayed catalog THEN the page SHALL allow that value in the selection and dry-run proposal. WEB-64 +21. IF an existing binding's harness and model are unchanged from the resolved target THEN web apply SHALL preserve it without catalog validation, even when it is off-catalog. WEB-85 +22. WHEN a binding's harness or model differs from the resolved target THEN web apply SHALL validate that binding against getBatchModels with allowNetwork:false and SHALL NOT validate bindings changed only by effort. WEB-21 +23. WHEN a role is skipped THEN setup SHALL keep its target binding unchanged or leave it unset when no binding exists. WEB-65 +24. WHEN all first-run role screens are skipped and no bindings exist THEN apply SHALL write the empty agents object sentinel. WEB-66 +25. WHEN the user skips an effort screen for an unchanged harness and model THEN the page logic SHALL preserve that binding's current effort. WEB-67 +26. WHEN the selected orchestrator mode is custom and the user enters positive finite parallelism N THEN the planner SHALL store N as the numeric parallelism value. WEB-68 +27. WHEN a selected role supports a reasoning-effort screen THEN the setup page SHALL offer that role's effort values and SHALL omit the effort control for opencode. WEB-38 +28. WHEN the setup page is rendered THEN it SHALL offer the orchestrator presets and investigate, selfWork, tools, and parallelism parameters. WEB-39 +29. WHEN the setup page is rendered THEN it SHALL offer workspace-write and danger-full-access for sandbox. WEB-40 +30. WHEN the setup page is rendered THEN it SHALL offer autocompact on and off. WEB-41 +31. WHEN setup starts with --profile THEN the state response SHALL identify that profile and apply its proposal only to that profile. WEB-42 +32. IF model discovery is incomplete or any requested harness returns an error THEN the refresh response SHALL return getBatchModels cache fallback and discoveryError without partial network results. WEB-59 +33. IF codedeck setup runs without batch flags and either stdin or stdout is not a TTY THEN it SHALL exit with code 1, print `${getCliName()} setup needs a terminal on both stdin and stdout.`, and start no server. WEB-69 +34. IF codedeck setup receives both --json and --port THEN it SHALL report a setup usage error and start no server. WEB-70 +35. WHEN setup guidance is updated THEN README.md lines 154 and 186 SHALL describe browser setup as the default, --tui as the picker entry, and --refresh as the catalog refresh option. WEB-82 +36. IF a changed binding's model is absent from the cached catalog THEN web apply SHALL return HTTP 422 with saved=false unless the request includes offCatalogConfirmed[role]=true for that binding's role. WEB-88 +37. IF a changed binding's model is absent from the cached catalog and the request includes offCatalogConfirmed[role]=true THEN web apply SHALL save that binding and return resultado.status=applied with saved=true. WEB-89 +38. IF catalog refresh returns status=unavailable THEN setup page logic SHALL retain the previously loaded catalog and show the response's discoveryError. WEB-90 +39. IF dry-run encounters invalid config JSON or a config read error THEN the handler SHALL return an error SetupEnvelope with resultado.code=14 for invalid JSON or 15 for a read error, and saved=false. WEB-91 +40. IF GET /api/setup/state encounters invalid config JSON or a config read error THEN the handler SHALL return a JSON error with code=14 for invalid JSON or code=15 for a read error. WEB-92 +41. IF apply cannot resolve the active profile because its saved snapshot is missing THEN it SHALL return resultado.code=14 with saved=false and SHALL NOT write config. WEB-93 +42. IF codedeck setup receives --port with --dry-run or --non-interactive THEN it SHALL report a setup usage error and start no server. WEB-94 +43. WHEN a setup response is assembled THEN it SHALL expose the exact top-level fields proposta, validacoes, mudancas, and resultado from SetupEnvelope. WEB-20 +**Independent Test**: Request setup state and catalog, refresh, submit dry-run and apply, and assert exact envelope fields and writes. Cover off-catalog apply with and without per-role confirmation, refresh failure with a previously loaded catalog, profile targets, all role and effort skips, typed models, custom numeric parallelism, invalid config and read-error codes on state and dry-run, missing active-profile apply, and conflicting port flags. + +### P5: Browser usage analytics + +**User Story**: As a user reviewing agent costs, I want a browser dashboard with the CLI filters and available breakdowns so that I can inspect usage without the terminal. + +**Why P5**: Usage origin support is already in main at 80ec486 (#103); the page must retain compatibility with an older running daemon. + +**Acceptance Criteria**: + + +1. WHEN the browser requests GET /usage THEN the server SHALL return the self-contained usage page with HTTP 200. WEB-43 +2. WHEN the CLI or web handler calls buildUsageQueryParams(opts, cwd, now) THEN it SHALL apply the CLI precedence of all, today, days, then default today when since is absent; map 3, 7, and 30 to named periods and other positive day counts to local-midnight since; let current override repo; and pass through since, until, model, and agent so equal inputs produce equal UsageQueryParams. WEB-44 +3. WHEN a usage query succeeds THEN the page logic SHALL expose every UsageTotals field for rendering. WEB-45 +4. WHEN a usage query succeeds THEN the page logic SHALL expose byDay buckets for rendering. WEB-46 +5. WHEN a usage query succeeds THEN the page logic SHALL expose byRepository buckets for rendering. WEB-47 +6. WHEN a usage query succeeds THEN the page logic SHALL expose byModel buckets for rendering. WEB-48 +7. WHEN a usage query succeeds THEN the page logic SHALL expose byAgent buckets for rendering. WEB-49 +8. WHEN a usage query succeeds THEN the page logic SHALL expose byRun buckets for rendering. WEB-50 +9. WHEN a usage result contains byOrigin THEN the page logic SHALL expose its buckets for rendering. WEB-51 +10. IF a result from an older running daemon over IPC has no byOrigin field THEN the page logic SHALL render the other breakdowns without an error. WEB-52 +11. WHILE the usage page is open THEN its state logic SHALL poll the active query using the normalized interval value. WEB-53 +12. WHEN codedeck usage runs with --web and no run ID THEN it SHALL open /usage with aggregate filters and the selected --by breakdown. WEB-54 +13. WHEN codedeck usage runs without --web THEN it SHALL preserve the snapshot, TUI, watch, plain, JSON, --by origin, --observe, and --backfill contracts from the CLI. WEB-55 +14. IF --backfill is absent and codedeck usage receives a positional run ID or --run THEN it SHALL call usage.get, preserve valid --observe data and --json output, ignore aggregate-only --by origin, and start no web server even if --web is present. WEB-56 +15. IF codedeck usage receives --backfill together with --web THEN it SHALL run backfill and start no web server. WEB-83 +16. WHEN usage web polling normalizes --interval with Math.max(1, Number(opts.interval) || 2) THEN zero and non-numeric values SHALL resolve to 2 seconds and negative or positive values below 1 SHALL resolve to 1 second. WEB-81 +17. WHEN a usage page filter for period, repo, model, agent, since, or until changes THEN the page logic SHALL issue a new query with the updated filters. WEB-78 +18. IF a usage query fails after a successful result THEN the page logic SHALL show the error and retain the last successful result. WEB-60 +19. WHEN codedeck usage receives --by origin THEN the page logic SHALL select origin as the initial breakdown while leaving all available sections reachable. WEB-80 +20. IF codedeck usage receives --web and --tui THEN it SHALL report a usage error and start no web server. WEB-95 +**Independent Test**: Use fixed options, cwd, and time to assert CLI and /api/usage query parameter equality. Test the page logic directly in Node for filters, polling, query errors, and results with and without byOrigin. Test CLI runs with --web, --web --tui, --by origin, --backfill, --observe, --json, and a single-run ID without starting an unintended server. + +## Edge Cases + +- IF Host is absent or matches neither accepted loopback Host value THEN no route handler SHALL run. +- IF an HTML GET lacks a valid token query parameter THEN the response SHALL NOT set the session cookie. +- IF a protected POST returns 403 THEN the page SHALL show the session-expired reload/restart message. +- IF a user port is invalid or already in use THEN the command SHALL exit without claiming the server started. +- IF a model refresh returns partial network results THEN the page SHALL show only the getBatchModels result and its status, not a partial merged network catalog. +- IF a changed binding fails catalog validation THEN apply SHALL save none of the proposal. +- IF config reading returns invalid JSON or an I/O error THEN web apply SHALL return code 14 or 15 respectively and SHALL NOT write. +- IF an older daemon omits byOrigin THEN the page SHALL render totals and every other breakdown. +- IF codedeck setup lacks either TTY stream without batch flags THEN it SHALL preserve the current exit and message and SHALL NOT start the web server. + +## Requirement Traceability + +Each acceptance criterion has one requirement ID and maps to the task that implements and tests it. + +| Requirement ID | Story | Phase | Status | Task | +| --- | --- | --- | --- | --- | +| WEB-01 | P1: Shared local server and home page | P1 | In Tasks | T1 | +| WEB-02 | P1: Shared local server and home page | P1 | In Tasks | T1, T7 | +| WEB-03 | P1: Shared local server and home page | P1 | In Tasks | T1 | +| WEB-04 | P1: Shared local server and home page | P1 | In Tasks | T1 | +| WEB-05 | P1: Shared local server and home page | P1 | In Tasks | T3 | +| WEB-06 | P1: Shared local server and home page | P1 | In Tasks | T3 | +| WEB-07 | P1: Shared local server and home page | P1 | In Tasks | T3 | +| WEB-08 | P1: Shared local server and home page | P1 | In Tasks | T2, T4, T5, T13, T18 | +| WEB-09 | P2: Local request security | P2 | In Tasks | T7, T12, T17 | +| WEB-10 | P2: Local request security | P2 | In Tasks | T6, T7 | +| WEB-11 | P2: Local request security | P2 | In Tasks | T6 | +| WEB-12 | P2: Local request security | P2 | In Tasks | T6, T7 | +| WEB-13 | P2: Local request security | P2 | In Tasks | T6, T7 | +| WEB-14 | P3: Shared setup planning | P3 | In Tasks | T8 | +| WEB-15 | P3: Shared setup planning | P3 | In Tasks | T8 | +| WEB-16 | P3: Shared setup planning | P3 | In Tasks | T8 | +| WEB-17 | P3: Shared setup planning | P3 | In Tasks | T8 | +| WEB-18 | P3: Shared setup planning | P3 | In Tasks | T8 | +| WEB-19 | P3: Shared setup planning | P3 | In Tasks | T8 | +| WEB-20 | P4: Browser setup | P4 | In Tasks | T11 | +| WEB-21 | P4: Browser setup | P4 | In Tasks | T11 | +| WEB-22 | P3: Shared setup planning | P3 | In Tasks | T9 | +| WEB-23 | P4: Browser setup | P4 | In Tasks | T10 | +| WEB-24 | P4: Browser setup | P4 | In Tasks | T11 | +| WEB-25 | P4: Browser setup | P4 | In Tasks | T10 | +| WEB-26 | P4: Browser setup | P4 | In Tasks | T11 | +| WEB-27 | P4: Browser setup | P4 | In Tasks | T11 | +| WEB-28 | P4: Browser setup | P4 | In Tasks | T11 | +| WEB-29 | P4: Browser setup | P4 | In Tasks | T11 | +| WEB-30 | P4: Browser setup | P4 | In Tasks | T8, T11 | +| WEB-31 | P4: Browser setup | P4 | In Tasks | T11 | +| WEB-32 | P4: Browser setup | P4 | In Tasks | T11 | +| WEB-33 | P4: Browser setup | P4 | In Tasks | T11 | +| WEB-34 | P4: Browser setup | P4 | In Tasks | T12 | +| WEB-35 | P4: Browser setup | P4 | In Tasks | T12 | +| WEB-36 | P4: Browser setup | P4 | In Tasks | T9, T12 | +| WEB-37 | P4: Browser setup | P4 | In Tasks | T10 | +| WEB-38 | P4: Browser setup | P4 | In Tasks | T10 | +| WEB-39 | P4: Browser setup | P4 | In Tasks | T10 | +| WEB-40 | P4: Browser setup | P4 | In Tasks | T10 | +| WEB-41 | P4: Browser setup | P4 | In Tasks | T10 | +| WEB-42 | P4: Browser setup | P4 | In Tasks | T12, T13 | +| WEB-43 | P5: Browser usage analytics | P5 | In Tasks | T16, T18 | +| WEB-44 | P5: Browser usage analytics | P5 | In Tasks | T14, T15 | +| WEB-45 | P5: Browser usage analytics | P5 | In Tasks | T16 | +| WEB-46 | P5: Browser usage analytics | P5 | In Tasks | T16 | +| WEB-47 | P5: Browser usage analytics | P5 | In Tasks | T16 | +| WEB-48 | P5: Browser usage analytics | P5 | In Tasks | T16 | +| WEB-49 | P5: Browser usage analytics | P5 | In Tasks | T16 | +| WEB-50 | P5: Browser usage analytics | P5 | In Tasks | T16 | +| WEB-51 | P5: Browser usage analytics | P5 | In Tasks | T16 | +| WEB-52 | P5: Browser usage analytics | P5 | In Tasks | T16 | +| WEB-53 | P5: Browser usage analytics | P5 | In Tasks | T16 | +| WEB-54 | P5: Browser usage analytics | P5 | In Tasks | T17 | +| WEB-55 | P5: Browser usage analytics | P5 | In Tasks | T17 | +| WEB-56 | P5: Browser usage analytics | P5 | In Tasks | T17 | +| WEB-57 | P1: Shared local server and home page | P1 | In Tasks | T1 | +| WEB-59 | P4: Browser setup | P4 | In Tasks | T11 | +| WEB-60 | P5: Browser usage analytics | P5 | In Tasks | T16 | +| WEB-61 | P3: Shared setup planning | P3 | In Tasks | T8 | +| WEB-62 | P3: Shared setup planning | P3 | In Tasks | T8 | +| WEB-63 | P4: Browser setup | P4 | In Tasks | T10, T11 | +| WEB-64 | P4: Browser setup | P4 | In Tasks | T10 | +| WEB-65 | P4: Browser setup | P4 | In Tasks | T10, T11 | +| WEB-66 | P4: Browser setup | P4 | In Tasks | T8, T10, T11 | +| WEB-67 | P4: Browser setup | P4 | In Tasks | T10 | +| WEB-68 | P4: Browser setup | P4 | In Tasks | T8, T10 | +| WEB-69 | P4: Browser setup | P4 | In Tasks | T12 | +| WEB-70 | P4: Browser setup | P4 | In Tasks | T12 | +| WEB-71 | P2: Local request security | P2 | In Tasks | T6, T7 | +| WEB-72 | P2: Local request security | P2 | In Tasks | T6, T7 | +| WEB-73 | P2: Local request security | P2 | In Tasks | T10 | +| WEB-74 | P2: Local request security | P2 | In Tasks | T6, T7 | +| WEB-75 | P1: Shared local server and home page | P1 | In Tasks | T1, T7 | +| WEB-76 | P4: Browser setup | P4 | In Tasks | T11 | +| WEB-77 | P4: Browser setup | P4 | In Tasks | T11 | +| WEB-78 | P5: Browser usage analytics | P5 | In Tasks | T15, T16 | +| WEB-80 | P5: Browser usage analytics | P5 | In Tasks | T16, T17 | +| WEB-81 | P5: Browser usage analytics | P5 | In Tasks | T16, T17 | +| WEB-82 | P4: Browser setup | P4 | In Tasks | T19 | +| WEB-83 | P5: Browser usage analytics | P5 | In Tasks | T17 | +| WEB-84 | P4: Browser setup | P4 | In Tasks | T11 | +| WEB-85 | P4: Browser setup | P4 | In Tasks | T11 | +| WEB-86 | P3: Shared setup planning | P3 | In Tasks | T8 | +| WEB-87 | P3: Shared setup planning | P3 | In Tasks | T8 | +| WEB-88 | P4: Browser setup | P4 | In Tasks | T10, T11 | +| WEB-89 | P4: Browser setup | P4 | In Tasks | T10, T11 | +| WEB-90 | P4: Browser setup | P4 | In Tasks | T10, T11 | +| WEB-91 | P4: Browser setup | P4 | In Tasks | T11 | +| WEB-92 | P4: Browser setup | P4 | In Tasks | T11 | +| WEB-93 | P4: Browser setup | P4 | In Tasks | T11 | +| WEB-94 | P4: Browser setup | P4 | In Tasks | T12 | +| WEB-95 | P5: Browser usage analytics | P5 | In Tasks | T17 | + +**Coverage**: 93 total requirements, 93 mapped to tasks, 0 unmapped. +## External Dependencies + +None. The feature adds no external-system integration; catalog access reuses the repository's existing model-discovery code. + +## Success Criteria + +- [ ] codedeck ui lists only pages registered in its route table, and each linked route returns HTTP 200. +- [ ] Invalid Host, Origin, or token requests return HTTP 403 before an action handler runs. +- [ ] A valid token URL bootstraps a port-specific cookie and redirects to a URL without t. +- [ ] Setup state pre-fills the resolved profile or global target, and dry-run never writes config. +- [ ] Web apply validates only changed harness:model bindings and saves no invalid proposal. +- [ ] Existing setup wizard, setup CLI contract, review, usage CLI, JSON, and statusline tests pass unchanged. +- [ ] Usage query params match between CLI and /api/usage for equal inputs, and page logic passes Node tests without a DOM dependency. diff --git a/.specs/features/web-console/tasks.md b/.specs/features/web-console/tasks.md new file mode 100644 index 0000000..7847f5e --- /dev/null +++ b/.specs/features/web-console/tasks.md @@ -0,0 +1,522 @@ +# Web console tasks + +## Execution protocol + +Implement these tasks with the tlc-spec-driven skill. Keep tests in the task that changes the code they cover. Run only the scoped commands below. Do not change plugin files or add a frontend framework. Do not run the full Vitest suite. + +**Design**: .specs/features/web-console/design.md + +**Status**: Draft + +## Test Coverage Matrix + +> Generated from the feature requirements, repository test locations, and the project instruction to use scoped Vitest commands. The Node test environment has no DOM dependency. + +| Code layer | Required test type | Coverage expectation | Location pattern | Run command | +| --- | --- | --- | --- | --- | +| Web server/router | Integration | Loopback bind, actual port after listen, port parsing, route dispatch, review aliases, browser URL, listen failure, and injected shutdown | tests/web-server.test.ts, tests/review.test.ts, tests/review-command.test.ts | npx vitest run tests/web-server.test.ts tests/review.test.ts tests/review-command.test.ts | +| Security | Integration | Allowed and rejected Host, token bootstrap and cookie, Origin, POST rejection before dispatch, and HTML framing header | tests/web-security.test.ts, tests/web-server.test.ts | npx vitest run tests/web-security.test.ts tests/web-server.test.ts | +| Setup core | Unit and integration | Planner fields and preservation, optional orchestrator, profile targets, skip behavior, no catalog validation in planner, config-owned shared helpers, separate changed-binding validation, wizard and batch compatibility | tests/setup-plan.test.ts, tests/setup-wizard.test.ts, tests/setup-cli-contract.test.ts | npx vitest run tests/setup-plan.test.ts tests/setup-wizard.test.ts tests/setup-cli-contract.test.ts | +| Setup web handlers | Integration | State prefill and error codes, catalog cache and refresh fallback, dry-run no-write, apply, changed-only validation with per-role off-catalog confirmation, missing profile, malformed body, and save errors | tests/setup-web.test.ts | npx vitest run tests/setup-web.test.ts | +| Usage query builder | Unit and command contract | Existing CLI filter precedence and mappings, with fixed options, cwd, and clock | tests/usage-cli.test.ts | npx vitest run tests/usage-cli.test.ts | +| Usage web handler | Integration | Query mapping, every supported filter, CLI versus /api/usage parameter equality, query success, and query error response | tests/usage-web.test.ts | npx vitest run tests/usage-web.test.ts | +| HTML pages and page behavior | Unit | Direct Node tests of injected page functions plus node:vm execution of extracted SETUP_PAGE and USAGE_PAGE scripts with stubbed browser globals; setup state, free-text input, discovery errors, 403 message, filters, polling, rendering data, optional byOrigin, and retained result on error | tests/web-pages.test.ts | npx vitest run tests/web-pages.test.ts | +| CLI wiring | Command contract | ui, review, setup, and usage routes and flags; setup batch and non-TTY behavior; usage.get with --web --json without server startup | tests/web-cli.test.ts, tests/review-command.test.ts, tests/setup-cli-contract.test.ts, tests/usage-cli.test.ts, tests/usage-statusline-contract.test.ts | npx vitest run tests/web-cli.test.ts tests/review-command.test.ts tests/setup-cli-contract.test.ts tests/usage-cli.test.ts tests/usage-statusline-contract.test.ts | +| Documentation | none | Text-only README update; no test coverage required | None | git diff --check -- README.md | + +## Gate Check Commands + +| Gate level | When to use | Command | +| --- | --- | --- | +| Task | After each task | Run the scoped command in that task's Gate field. | +| P1 | After shared server and home wiring | npx vitest run tests/web-server.test.ts tests/web-pages.test.ts tests/review.test.ts tests/review-command.test.ts tests/web-cli.test.ts | +| P2 | After security integration | npx vitest run tests/web-security.test.ts tests/web-server.test.ts tests/web-pages.test.ts | +| P3 | After setup core extraction | npx vitest run tests/setup-plan.test.ts tests/setup-wizard.test.ts tests/setup-cli-contract.test.ts | +| P4 | After setup web wiring | npx vitest run tests/setup-web.test.ts tests/web-pages.test.ts tests/web-cli.test.ts tests/setup-cli-contract.test.ts | +| P5 | After P4 | npx vitest run tests/usage-cli.test.ts tests/usage-web.test.ts tests/web-pages.test.ts tests/web-cli.test.ts tests/usage-statusline-contract.test.ts | + +## Execution Plan + +Phases run in dependency order. Within each phase, start a task after its listed dependencies pass. P5 starts after P4. Usage origin support is already in main at 80ec486 (#103). + +### Phase 1: Shared server and home page + +```text +T1 -> T2 +T1 -> T3 +T2 -> T4 +T3 -> T4 +T4 -> T5 +``` + +### Phase 2: Request security + +```text +T1 -> T6 +T1 -> T7 +T6 -> T7 +``` + +### Phase 3: Shared setup planning + +```text +T7 -> T8 +T8 -> T9 +``` + +### Phase 4: Browser setup + +```text +T8 -> T10 +T7 -> T11 +T8 -> T11 +T9 -> T12 +T10 -> T12 +T11 -> T12 +T12 -> T13 +T12 -> T19 +``` + +### Phase 5: Browser usage + +```text +T14 -> T15 +T7 -> T15 +T15 -> T16 +T14 -> T17 +T15 -> T17 +T16 -> T17 +T13 -> T18 +T15 -> T18 +T16 -> T18 +``` + +## Task Breakdown + +### T1: Extract the shared server and router + +**What**: Move loopback server startup, route dispatch, port parsing, browser opening, and testable shutdown into the shared web server. +**Where**: src/web/server.ts +**Depends on**: None +**Reuses**: src/cli/commands/review.ts +**Requirement**: WEB-01, WEB-02, WEB-03, WEB-04, WEB-57, WEB-75 + +**Done when**: + +- The server binds to 127.0.0.1 and defaults to port 3100. +- The user-facing parser accepts only integer ports from 1 through 65535; an injected test seam can listen on port 0. +- After listen, the server reads server.address().port and uses that actual value for its returned address. +- Signal handling calls the injected close function and then the injected exit function for SIGINT and SIGTERM. +- A listen failure reports the error and exits with code 1 without printing a started URL. +- Server tests cover route dispatch, unknown routes, ephemeral bound port, listen failure, and both shutdown callbacks. + +**Tests**: Integration, tests/web-server.test.ts +**Gate**: npx vitest run tests/web-server.test.ts + +### T2: Render the home page from registered routes + +**What**: Add a self-contained home page renderer that receives the registered page routes and creates links only for those routes. +**Where**: src/web/home-page.ts +**Depends on**: T1 +**Reuses**: src/web/review-page.ts +**Requirement**: WEB-08 + +**Done when**: + +- The home renderer accepts route labels and paths instead of hard-coding future routes. +- Tests pass only /review as registered and assert there is no /usage or /setup link. +- The page uses inline CSS and has no external asset dependency. + +**Tests**: Unit, tests/web-pages.test.ts +**Gate**: npx vitest run tests/web-pages.test.ts + +### T3: Rewire the review command + +**What**: Start review with the shared server and preserve its page and API behavior. +**Where**: src/cli/commands/review.ts +**Depends on**: T1 +**Reuses**: src/web/review-page.ts and src/git/review.ts +**Requirement**: WEB-05, WEB-06, WEB-07 + +**Done when**: + +- GET / and GET /review return the current review page with HTTP 200. +- GET /api/review keeps ref=HEAD as its default and passes file when supplied. +- Existing review options and test seams remain available. +- Existing review assertions pass unchanged. + +**Tests**: Integration, tests/review.test.ts and tests/review-command.test.ts +**Gate**: npx vitest run tests/review.test.ts tests/review-command.test.ts + +### T4: Add the ui command + +**What**: Start the shared server with the home page at / and the registered review page. +**Where**: src/cli/commands/ui.ts +**Depends on**: T2, T3 +**Reuses**: Shared server, home renderer, and review handler +**Requirement**: WEB-08 + +**Done when**: + +- codedeck ui opens / by default. +- Its initial page route table registers the home page and /review. +- --port and --no-open use the shared server options. +- Command tests assert registered links, route status, and URL output. + +**Tests**: Command contract, tests/web-cli.test.ts +**Gate**: npx vitest run tests/web-cli.test.ts + +### T5: Register ui with the CLI + +**What**: Register codedeck ui with the root Commander program. +**Where**: src/cli/index.ts +**Depends on**: T4 +**Reuses**: Existing command registration +**Requirement**: WEB-08 + +**Done when**: + +- codedeck --help lists ui. +- A command test invokes the registered command and verifies the home route. + +**Tests**: Command contract, tests/web-cli.test.ts +**Gate**: npx vitest run tests/web-cli.test.ts + +### T6: Add request security + +**What**: Create the per-start token and route guard for Host, cookie, Origin, and HTML response policy. +**Where**: src/web/security.ts +**Depends on**: T1 +**Reuses**: Node request headers and the actual bound port from the server +**Requirement**: WEB-10, WEB-11, WEB-12, WEB-13, WEB-71, WEB-72, WEB-74 + +**Done when**: + +- Each security instance creates a token from 32 cryptographically random bytes. +- Host validation accepts only 127.0.0.1: and localhost:, case-insensitively. +- Every POST requires the codedeck_ui_token_ cookie and an HTTP Origin matching Host and the bound port. +- A valid t token on an HTML GET sets a host-only HttpOnly, SameSite=Strict, Path=/ cookie and returns HTTP 303 to the same path without t. +- An HTML GET without a valid token does not set the session cookie. +- HTML responses include Content-Security-Policy: frame-ancestors 'none'. +- Unit tests cover valid and invalid token bootstrap, Host, Origin, cookie name, redirect, and CSP behavior. + +**Tests**: Integration, tests/web-security.test.ts +**Gate**: npx vitest run tests/web-security.test.ts + +### T7: Apply security before route dispatch + +**What**: Integrate the guard into the shared server before any handler can run. +**Where**: src/web/server.ts +**Depends on**: T1, T6 +**Reuses**: Route policies from src/web/security.ts +**Requirement**: WEB-10, WEB-12, WEB-13, WEB-71, WEB-74, WEB-75 + +**Done when**: + +- The server obtains the actual port after listen and creates security with that port before opening or printing the URL. +- Every route gets a Host check before route dispatch. +- Invalid POST cookie or Origin requests return 403 without invoking the route handler. +- The opened and printed initial page URL includes ?t=; a browser-open failure prints the same full URL and keeps serving. +- Server tests prove rejected requests do not reach handlers and token URLs are passed to the browser opener and output. + +**Tests**: Integration, tests/web-security.test.ts and tests/web-server.test.ts +**Gate**: npx vitest run tests/web-security.test.ts tests/web-server.test.ts + +### T8: Extract shared setup logic into the config layer + +**What**: Move shared setup planning, target resolution, diffing, envelope types, and binding validation from the CLI command into the config layer. +**Where**: src/config/setup.ts +**Depends on**: T7 +**Reuses**: RunAgentConfig, profile helpers, role bindings, and existing catalog validation rules +**Requirement**: WEB-14, WEB-15, WEB-16, WEB-17, WEB-18, WEB-19, WEB-30, WEB-61, WEB-62, WEB-86, WEB-87 + +**Done when**: + +- buildSetupPlan accepts RunAgentConfig, a resolved target, and selections; it returns proposedConfig and diff only. +- resolveSetupTarget, diffConfig, SetupEnvelope, catalogContains, and validateBindings are exported from src/config/setup.ts for CLI and web callers. +- No src/config module imports from src/cli. +- The planner does not read or write files, discover models, or call catalog validation. +- Separate binding validation can be invoked by web apply and batch without being called by the planner; the wizard path still adds no catalog validation. +- Planner tests omit orchestrator and assert preservation of both an existing value and its absence. +- Planner tests cover selected values, skipped roles, unrelated config keys, profiles, and diff paths for each setup field. +- Tests cover off-catalog selections in the planner, profile resolution, diffing, and separate binding validation. + +**Tests**: Unit, tests/setup-plan.test.ts +**Gate**: npx vitest run tests/setup-plan.test.ts + +### T9: Reuse planning and separate validation in setup paths + +**What**: Route wizard config assembly through the pure planner and keep batch validation outside it. +**Where**: src/cli/commands/setup.ts +**Depends on**: T8 +**Reuses**: Existing wizard selection, runSetupBatch, and config save flow +**Requirement**: WEB-22, WEB-36 + +**Done when**: + +- The wizard passes its selections through buildSetupPlan before saving. +- The wizard keeps typed-model, role skip, effort skip, abort, discovery, save-failure, and profile behavior. +- runSetupBatch validates only the winning --bind value for each role outside the planner. +- Existing setup wizard and CLI contract cases pass unchanged. + +**Tests**: Integration, tests/setup-wizard.test.ts and tests/setup-cli-contract.test.ts +**Gate**: npx vitest run tests/setup-wizard.test.ts tests/setup-cli-contract.test.ts + +### T10: Add the setup page and tested behavior logic + +**What**: Add a self-contained setup HTML page whose exported TypeScript behavior functions also drive selection, refresh state, and 403 recovery in the browser. +**Where**: src/web/setup-page.ts +**Depends on**: T8 +**Reuses**: Wizard selection semantics and the route contracts in spec.md +**Requirement**: WEB-23, WEB-25, WEB-37, WEB-38, WEB-39, WEB-40, WEB-41, WEB-64, WEB-65, WEB-66, WEB-67, WEB-68, WEB-73, WEB-88, WEB-89, WEB-90 + +**Done when**: + +- The page has a free-text harness:model field for every role, with supported effort choices and no effort control for opencode. +- It has role skip controls, orchestrator presets and custom parameters, both sandbox values, and autocompact on/off. +- It asks for explicit per-role confirmation before sending an off-catalog model in offCatalogConfirmed. +- Its state logic preserves skipped roles and effort; a first-run all-role skip can produce agents: {}. +- Positive finite custom parallelism is kept as a number in the proposal. +- A pending refresh exposes “Discovering models...” until the response completes. +- An unavailable refresh keeps the previously loaded catalog and shows discoveryError. +- An untouched selection can omit orchestrator and preserves the current value or its absence. +- A protected POST response of 403 exposes the exact reload/restart message from WEB-73. +- The HTML injects its exported behavior function source; Node tests call those same functions with fake fetch and state callbacks without a DOM. +- Tests extract the inline script from SETUP_PAGE, evaluate it in node:vm with an empty context and stubbed fetch, timers, and document, then call the setup page functions from that context. + +**Tests**: Unit, tests/web-pages.test.ts +**Gate**: npx vitest run tests/web-pages.test.ts + +### T11: Add setup state, catalog, and mutation routes + +**What**: Add the setup route factory for state and catalog reads plus refresh, dry-run, and apply actions. +**Where**: src/web/setup-routes.ts +**Depends on**: T7, T8 +**Reuses**: readConfigForSetup, resolveSetupTarget, getBatchModels, buildSetupPlan, and separate binding validation +**Requirement**: WEB-20, WEB-21, WEB-24, WEB-26, WEB-27, WEB-28, WEB-29, WEB-30, WEB-31, WEB-32, WEB-33, WEB-59, WEB-63, WEB-76, WEB-77, WEB-84, WEB-85, WEB-88, WEB-89, WEB-90, WEB-91, WEB-92, WEB-93 + +**Done when**: + +- GET /api/setup/state returns the global or named profile target and current bindings, effort, optional orchestrator, sandbox, and autocompact values for prefill; invalid JSON returns code 14 and a read error returns code 15. +- An active profile without a snapshot returns the existing SetupUsageError instead of selecting global config. +- Apply returns code 14 and saved=false without a write when the active profile has no saved snapshot. +- GET /api/setup/catalog calls only getBatchModels with allowNetwork:false and returns its actual result fields. +- Protected refresh calls getBatchModels with refresh:true, allowNetwork:true, and timeoutMs:12000; incomplete discovery returns the helper's refresh fallback and discoveryError without partial network results. +- Concurrent refresh callers share the in-flight promise. +- Dry-run returns an exact SetupEnvelope with proposta, validacoes, mudancas, and resultado and does not write config. +- Apply uses readConfigForSetup, validates only changed harness:model pairs with getBatchModels allowNetwork:false, does not validate effort-only changes, and does not block unrelated changes for an unchanged off-catalog binding. +- An off-catalog changed model returns HTTP 422 unless its role has offCatalogConfirmed=true in the request; a confirmed off-catalog model saves. +- Dry-run returns resultado.code=14 for invalid JSON and resultado.code=15 for a config read error; neither path writes config. +- GET /api/setup/state returns JSON code 14 for invalid JSON and code 15 for a config read error. +- Empty diffs return unchanged and do not write; valid non-empty diffs save; malformed bodies return 400; validation failures return 422; save failures return 500. +- Tests cover state, cache and refresh results, concurrency, exact envelope keys, changed and unchanged off-catalog bindings with and without confirmation, invalid/read errors, missing active profiles, and every no-write path. + +**Tests**: Integration, tests/setup-web.test.ts +**Gate**: npx vitest run tests/setup-web.test.ts + +### T12: Make interactive setup open the web console + +**What**: Add setup web startup flags and preserve the confirmed TTY, TUI, and batch branches. +**Where**: src/cli/commands/setup.ts +**Depends on**: T9, T10, T11 +**Reuses**: Existing argument parser and runSetupBatch +**Requirement**: WEB-09, WEB-34, WEB-35, WEB-36, WEB-42, WEB-69, WEB-70, WEB-94 + +**Done when**: + +- With both TTYs, no batch flags, and no --tui, codedeck setup starts the server at /setup. +- With --tui and both TTYs, setup runs the existing terminal wizard. +- Without both TTYs and without batch flags, setup exits 1 with the existing terminal message and starts no server. +- Batch flags continue to call runSetupBatch without changing JSON, dry-run, bind, validation, save, or exit-code contracts. +- --profile targets the selected profile and --refresh starts the protected catalog refresh after /setup loads. +- --json with --port returns a usage error before server startup. +- --dry-run with --port and --non-interactive with --port return usage errors before server startup. +- --port, --no-open, open failure, and printed token URL are covered by web command tests. +- Tests assert --json, --dry-run, and --non-interactive each reject --port without starting a server. +- Existing setup CLI contract tests pass without changes. + +**Tests**: Command contract, tests/web-cli.test.ts and tests/setup-cli-contract.test.ts +**Gate**: npx vitest run tests/web-cli.test.ts tests/setup-cli-contract.test.ts + +### T13: Register setup routes with ui + +**What**: Add the setup page and API routes to the route table used by codedeck ui. +**Where**: src/cli/commands/ui.ts +**Depends on**: T12 +**Reuses**: The registered review routes and setup route factory +**Requirement**: WEB-08, WEB-23, WEB-42, WEB-63 + +**Done when**: + +- codedeck ui serves /setup, /api/setup/state, /api/setup/catalog, and the setup action routes. +- The home page includes /setup only after that page route is registered. +- Command tests assert setup status and route registration. + +**Tests**: Integration, tests/web-cli.test.ts +**Gate**: npx vitest run tests/web-cli.test.ts + +### T19: Update setup instructions in README + +**What**: Replace the picker-first setup description with browser setup as the interactive default and retain the --tui and --refresh options. +**Where**: README.md +**Depends on**: T12 +**Reuses**: Existing setup section at lines 149-186 +**Requirement**: WEB-82 + +**Done when**: + +- The example at line 154 describes codedeck setup opening the web console. +- The text at line 186 describes --refresh and points to --tui for the frozen terminal picker. +- No unrelated README sections change. + +**Tests**: none +**Gate**: git diff --check -- README.md + +### T14: Extract shared usage query parameter construction + +**What**: Extract buildUsageQueryParams(opts, cwd, now) from the aggregate CLI branch into the shared helper and use it from src/cli/commands/usage.ts. +**Where**: src/core/usage-query.ts +**Depends on**: None +**Reuses**: Existing aggregate filter precedence in src/cli/commands/usage.ts:175-211 +**Requirement**: WEB-44 + +**Done when**: + +- The pure function preserves all, today, days, default-today, current-over-repo, since, until, model, and agent behavior. +- src/cli/commands/usage.ts calls the shared function with parsed options, process.cwd(), and the current date. +- tests/usage-cli.test.ts covers the helper and existing CLI query contract. + +**Tests**: Unit and command contract, tests/usage-cli.test.ts +**Gate**: npx vitest run tests/usage-cli.test.ts + +### T15: Add the usage query route + +**What**: Map GET /api/usage filters through buildUsageQueryParams and call the injected usage query function. +**Where**: src/web/usage-routes.ts +**Depends on**: T7, T14 +**Reuses**: buildUsageQueryParams and fetchUsageQuery +**Requirement**: WEB-44 + +**Done when**: + +- Query parsing maps period, repo, model, agent, since, until, all, today, days, and current to the shared builder input. +- --current resolves to the CLI process working directory and overrides repo. +- For identical parsed CLI options, cwd, and clock, tests compare the CLI UsageQueryParams with the parameters passed by GET /api/usage. +- Successful results retain the UsageQueryResult shape. +- Query errors return an HTTP 500 JSON error. +- tests/usage-web.test.ts covers each filter, the current override, success, and error response. + +**Tests**: Integration, tests/usage-web.test.ts +**Gate**: npx vitest run tests/usage-web.test.ts + +### T16: Add usage page and tested behavior logic + +**What**: Add the self-contained usage page and its exported filter, render-state, and polling functions. +**Where**: src/web/usage-page.ts +**Depends on**: T15 +**Reuses**: UsageQueryResult, UsageTotals, and UsageMetricBucket +**Requirement**: WEB-43, WEB-45, WEB-46, WEB-47, WEB-48, WEB-49, WEB-50, WEB-51, WEB-52, WEB-53, WEB-60, WEB-78, WEB-80, WEB-81 + +**Done when**: + +- Page logic exposes every UsageTotals field and each of byDay, byRepository, byModel, byAgent, and byRun. +- It exposes byOrigin when present and handles its absence from an older running daemon process over IPC without throwing. +- Period, repo, model, agent, since, and until changes each issue a query with the updated filters. +- Polling refreshes the active filter set and normalizes the interval with Math.max(1, Number(opts.interval) || 2). +- A query error leaves the last successful result visible and stores the error state. +- --by origin selects origin initially while every available breakdown remains reachable. +- The HTML injects the exported behavior function source; tests directly call that function in Node with fake fetch, timer, and render adapters. +- Tests extract the inline script from USAGE_PAGE, evaluate it in node:vm with an empty context and stubbed fetch, timers, and document, then call the usage page functions from that context. + +**Tests**: Unit, tests/web-pages.test.ts +**Gate**: npx vitest run tests/web-pages.test.ts + +### T17: Add usage --web command wiring + +**What**: Add aggregate usage web startup while keeping the existing single-run and aggregate CLI branches in order. +**Where**: src/cli/commands/usage.ts +**Depends on**: T14, T15, T16 +**Reuses**: Shared server, usage route factory, and existing command options +**Requirement**: WEB-09, WEB-54, WEB-55, WEB-56, WEB-80, WEB-81, WEB-83, WEB-95 + +**Done when**: + +- Aggregate usage with --web starts /usage and passes the same date, repository, model, agent, and --by filters. +- --port and --no-open use the shared server; --interval uses the existing normalization rule. +- --web --json for aggregate usage starts the page and prints its token URL. +- When --backfill is absent, positional ID or --run takes the usage.get path before web startup, preserves --observe and --json, ignores aggregate-only --by origin, and starts no server. +- --backfill runs before web startup and starts no server even with --web. +- --web with --tui returns a usage error and starts no server. +- tests/web-cli.test.ts asserts --web --tui returns a usage error without starting a server. +- Without --web, snapshot, TUI, watch, plain, JSON, --by origin, --observe, and --backfill contracts remain unchanged. +- tests/web-cli.test.ts proves usage --web --json calls usage.get and starts no server. +- tests/usage-cli.test.ts covers --by origin, --observe, --backfill, and --interval contracts. + +**Tests**: Command contract, tests/web-cli.test.ts and tests/usage-cli.test.ts +**Gate**: npx vitest run tests/web-cli.test.ts tests/usage-cli.test.ts + +### T18: Register usage routes with ui + +**What**: Register /usage and /api/usage after their route factories are available. +**Where**: src/cli/commands/ui.ts +**Depends on**: T13, T15, T16 +**Reuses**: Existing ui route table and the home page renderer +**Requirement**: WEB-08, WEB-43 + +**Done when**: + +- codedeck ui serves /usage and /api/usage. +- The home page links to /usage only after its page route is registered. +- tests/web-cli.test.ts asserts both route status and the home link. + +**Tests**: Integration, tests/web-cli.test.ts +**Gate**: npx vitest run tests/web-cli.test.ts + + +## Diagram-Definition Cross-Check + +| Task | Depends on | Diagram edges | Status | +| --- | --- | --- | --- | +| T1 | None | None | Match | +| T2 | T1 | T1 -> T2 | Match | +| T3 | T1 | T1 -> T3 | Match | +| T4 | T2, T3 | T2 -> T4; T3 -> T4 | Match | +| T5 | T4 | T4 -> T5 | Match | +| T6 | T1 | T1 -> T6 | Match | +| T7 | T1, T6 | T1 -> T7; T6 -> T7 | Match | +| T8 | T7 | T7 -> T8 | Match | +| T9 | T8 | T8 -> T9 | Match | +| T10 | T8 | T8 -> T10 | Match | +| T11 | T7, T8 | T7 -> T11; T8 -> T11 | Match | +| T12 | T9, T10, T11 | T9 -> T12; T10 -> T12; T11 -> T12 | Match | +| T13 | T12 | T12 -> T13 | Match | +| T14 | None | None | Match | +| T15 | T7, T14 | T7 -> T15; T14 -> T15 | Match | +| T16 | T15 | T15 -> T16 | Match | +| T17 | T14, T15, T16 | T14 -> T17; T15 -> T17; T16 -> T17 | Match | +| T18 | T13, T15, T16 | T13 -> T18; T15 -> T18; T16 -> T18 | Match | +| T19 | T12 | T12 -> T19 | Match | + +## Test Co-location Validation + +| Task | Code layer | Matrix requires | Task tests | Status | +| --- | --- | --- | --- | --- | +| T1 | Web server/router | Integration | tests/web-server.test.ts | OK | +| T2 | HTML pages and page behavior | Unit | tests/web-pages.test.ts | OK | +| T3 | Web server/router | Integration | tests/review.test.ts, tests/review-command.test.ts | OK | +| T4 | CLI wiring | Command contract | tests/web-cli.test.ts | OK | +| T5 | CLI wiring | Command contract | tests/web-cli.test.ts | OK | +| T6 | Security | Integration | tests/web-security.test.ts | OK | +| T7 | Security | Integration | tests/web-security.test.ts, tests/web-server.test.ts | OK | +| T8 | Setup core | Unit | tests/setup-plan.test.ts | OK | +| T9 | Setup core | Integration | tests/setup-wizard.test.ts, tests/setup-cli-contract.test.ts | OK | +| T10 | HTML pages and page behavior | Unit | tests/web-pages.test.ts | OK | +| T11 | Setup web handlers | Integration | tests/setup-web.test.ts | OK | +| T12 | CLI wiring | Command contract | tests/web-cli.test.ts, tests/setup-cli-contract.test.ts | OK | +| T13 | CLI wiring | Command contract | tests/web-cli.test.ts | OK | +| T14 | Usage query builder | Unit and command contract | tests/usage-cli.test.ts | OK | +| T15 | Usage web handler | Integration | tests/usage-web.test.ts | OK | +| T16 | HTML pages and page behavior | Unit | tests/web-pages.test.ts | OK | +| T17 | CLI wiring | Command contract | tests/web-cli.test.ts, tests/usage-cli.test.ts | OK | +| T18 | CLI wiring | Command contract | tests/web-cli.test.ts | OK | +| T19 | Documentation | none | none | OK |