diff --git a/docs/adr/0001-deck-as-panel-host-for-mystira-ops-tooling.md b/docs/adr/0001-deck-as-panel-host-for-mystira-ops-tooling.md new file mode 100644 index 0000000..7963f66 --- /dev/null +++ b/docs/adr/0001-deck-as-panel-host-for-mystira-ops-tooling.md @@ -0,0 +1,163 @@ +# ADR-0001: deck as panel-host for Mystira ops tooling in a generic shell + +## Status + +Proposed + +## Date + +2026-07-11 (proposed). Re-grounded against `dev` @ `bc0d1c5` on 2026-08-10 before commit — the +evidence in §Context was refreshed for PR #3 (Camera panel) and PR #5 (CI); the decision itself is +unchanged and still awaiting ratification. + +## Context + +deck was extracted from `mystira-workspace/apps/devhub` (March 2026) with the explicit goal of +becoming a **generic phoenixvc ops shell** — a VSCode-style Tauri desktop app with a Service +Manager, an Infrastructure panel, a Dashboard, and a .NET sidecar, reusable across the org. During +extraction the Mystira-specific panels (Cosmos Explorer, Migration Manager) were stripped so the +base shell would not carry Mystira coupling. + +R13 now wants deck to be the **operator cockpit over the Mystira AI-content pipeline** +(`sluice → story-generator → publisher`; `docket` meters spend), across three facets (see +`docs/specs/deck-ops-cockpit.md`): + +1. a Dashboard **cost/ops** section (sluice health + docket spend), +2. **re-porting** the Cosmos Explorer + Migration Manager panels, and +3. a Service-Manager **batch-run monitor** over the story-generator nightly Batch API. + +This reintroduces Mystira-specific surfaces into the very shell that was just made generic. Two +observations sharpen the decision: + +- The extraction was **incomplete**: the panel directories and their `VIEWS` entries are gone, but + the Rust `cosmos.rs` backend and the `.NET` `MigrationService` remained, `Dashboard.tsx` still + offers three Cosmos/Migration quick-actions that now silently land on the Service Manager, and + `StatisticsPanel.tsx` / `ExportPanel.tsx` still invoke `cosmos_stats` / `cosmos_export` while + nothing renders them. So Mystira coupling was never fully removed — it was left **half-wired**, + with live backends under no UI. (When first drafted this bullet also cited dangling + `AppContent.tsx` / `AppSidebar.tsx` imports that broke the build; PR #3 removed those, and CI is + green. The residue above is what survives, and the conclusion is unchanged.) +- Every R13 facet is fundamentally **Mystira-shaped** (Mystira Cosmos containers, the Mystira + story-gen Batch API, Mystira's spend in docket). None of it is generic ops. + +The question this ADR settles: **should deck host Mystira-specific ops panels, and if so, how does +it stay a reusable generic shell rather than collapsing back into "Mystira DevHub"?** + +Alternatives considered: + +1. **Keep deck strictly generic; build the Mystira cockpit elsewhere** (e.g. a new Mystira-only + desktop app, or back inside `mystira-workspace`). Rejected: it discards deck's entire reason for + existing (it *is* the extracted DevHub), duplicates the Tauri/Service-Manager/exec machinery, + and the ecosystem docs (README, ecosystem-provisioning design) already designate **deck** as the + operator window into sluice/docket/story-gen. +2. **Fold the Mystira panels into the generic shell as first-class, always-present features.** + Rejected: it re-Mystira-fies the shell — a second org adopting deck would inherit Cosmos/Migration/ + batch panels it does not want, and the "generic" claim becomes false. +3. **deck is a generic panel-host; Mystira panels are pluggable, clearly-bounded modules.** The + shell (VSCode layout, activity bar, Service Manager, Infrastructure panel, exec primitives, + .NET sidecar bridge) stays generic and org-agnostic; Mystira-specific surfaces are added through + the existing four-file panel recipe as **cohesive, self-contained modules** that consume + external Mystira/phoenixvc contracts and can be omitted without touching the shell. + +## Decision + +**deck is a generic panel-host, and Mystira ops tooling is hosted as bounded panel modules on top +of it (Alternative 3).** + +Concretely: + +- The **shell is the generic asset**: `VSCodeLayout`, the activity bar / `VIEWS` routing, the + Service Manager, the Infrastructure panel, the Rust exec primitives (`Command::new`, argv-only), + and the `cli.rs` ↔ .NET-sidecar JSON bridge carry **no Mystira semantics** and remain reusable by + any phoenixvc project. +- **Mystira-coupled surfaces are panels/modules**, added via deck's existing recipe (a `VIEWS` + entry + `ACTIVITY_BAR_ITEMS` + an `AppContent` route + an `AppSidebar` block) plus a dedicated + backend module (`cost.rs`, `story_gen.rs`, the re-ported `cosmos`/`migration` panels over the + existing `cosmos.rs` + `MigrationService`). Each module owns its Mystira coupling; the shell does + not. +- Panels **consume external contracts, they do not embed Mystira domain logic**: Facet 1 reads + sluice `/health`,`/metrics` and docket's MCP tools; Facet 3 reads the story-gen Batch operation + model (`BatchGenerationItem` / `BatchSubmission`, operation-id + status); Facet 2 talks to Mystira + Cosmos via the operator's `az` session. deck holds **no** Mystira business rules — it is a viewer + and a trigger. +- **Secrets never enter the repo.** The docket `MCP_SECRET`, Mystira-Cosmos connection strings, and + the story-gen API token are session/local-only (env or machine-local app-config), consistent with + deck's `security.md` and the org MCP-bearer rule. +- **The interrupted extraction is completed, not perpetuated.** Re-porting Facet 2 means finishing + the half-removal cleanly (re-add the two `VIEWS` entries + panels, restore the stripped + `cosmos.stats`/`cosmos.export` .NET CLI handlers, fix the DevHub→Deck naming/CLI-path residue) so + the tree is consistent and the coupling is explicit and bounded — not dangling. + +The full technical design is `docs/specs/deck-ops-cockpit.md`. + +## Consequences + +### Positive + +- **deck stays reusable.** A future adopter gets the generic shell without Mystira panels; the + Mystira modules are additive and omissible. +- **One cockpit, one payoff.** Health, spend, data tooling, and batch ops live in a single operator + window, and the modules can cross-link (e.g. a batch run → its docket spend slice) precisely + because they are co-located but individually bounded. +- **Clear ownership.** The shell is deck's; each panel owns its upstream contract; Mystira domain + logic stays in `mystira-workspace`. No repo crosses that line. +- **Honest dependency posture.** Because panels consume external contracts, deck degrades + gracefully when an upstream is only specced (docket MCP, story-gen Batch API) instead of pretending + data exists. + +### Negative / Costs + +- **Coupling creep risk.** "Generic shell + Mystira modules" is a discipline, not a mechanism — + nothing stops a future change from leaking Mystira assumptions into the shell. Mitigation: keep + Mystira strings/types inside the panel modules and their backend files; the shell's `VIEWS` + registry is the only shared touch-point. +- **Multi-tenant panel selection is unsolved.** Today all `VIEWS` are compiled in. If a second org + adopts deck, panel visibility should become config-driven (feature flags / a panel registry). + This ADR does not build that; it only keeps the coupling bounded so it is a later, tractable step. +- **Restores Mystira-specific maintenance to deck.** deck now tracks Mystira upstreams (sluice + endpoints, docket tool names, the Batch API contract) and must follow their changes. + +### Boundary summary + +| Layer | Generic (org-agnostic) | Mystira-coupled (panel modules) | +| --- | --- | --- | +| Shell / layout / routing | ✅ `VSCodeLayout`, activity bar, `VIEWS` mechanism | — | +| Exec + sidecar bridge | ✅ `Command::new` argv-only, `cli.rs`, .NET sidecar protocol | — | +| Service Manager / Infrastructure | ✅ generic panels | Batch-run **section** consumes story-gen | +| Dashboard | ✅ generic panel | Cost/Ops **sections** consume sluice + docket | +| Cosmos / Migration | — | ✅ Mystira Cosmos panels + `MigrationService` | + +## Notes on placement of this ADR + +Before this ADR, deck had **no** ADR directory. The only breadcrumb was a dangling reference in +`docs/guides/contracts-migration.md` (lines 20 and 267) to +`../adr/0020-package-consolidation-strategy.md` — an artifact inherited from mystira-workspace's +4-digit ADR numbering (that file does not exist in deck, and this ADR does not create it). deck is +**not** onboarded to AgentKit/Retort, so there is no generated `decisions/` registry to collide +with (unlike docket's ADR-09, which had to skip reserved numbers). + +Decision on location and number: + +- **Location:** `docs/adr/` — matches the single existing breadcrumb (`../adr/…`) and keeps ADRs + beside the existing `docs/guides/` and the proposed `docs/specs/`. (The phoenixvc/docket + convention `docs/architecture/decisions/NN-*.md` was considered; `docs/adr/` was chosen to honour + deck's own inherited breadcrumb and avoid inventing an `architecture/` tree deck doesn't have.) +- **Number:** **`0001`** — deck's first hand-authored ADR, 4-digit to match the inherited breadcrumb + style. +- **Format:** the org/mystira **narrative** ADR shape (Status / Context / Decision / Consequences / + placement note), not a weighted decision-matrix — this records a decision narrative, not a tool + bake-off. + +This establishes `docs/adr/NNNN-*.md` as deck's ADR home for future decisions. + +## References + +- `docs/specs/deck-ops-cockpit.md` — deck ops cockpit technical spec (this ADR's companion). +- `mystira-workspace .agents/roadmaps/video-generation/design-ecosystem-provisioning.md` — deck's + ecosystem role (Models panel + sluice hybrid routing; §1 fit-with-what-deck-already-is). +- `phoenixvc/sluice docs/planning/sluice-go-live-audit-2026-07.md` — sluice LIVE verification; + `/health`, `/metrics`, `/spend/logs` surfaces + the master-key caveat. +- `phoenixvc/docket docs/architecture/specs/07_docket_mcp_server.md` + `decisions/09-docket-mcp-domain-service.md` + — docket MCP tool surface, phasing, and the domain-service-MCP shape deck consumes. +- `mystira-workspace apps/story-generator/docs/specs/BATCH_API_GENERATION_ROUTING.md` — the batch + operation-id + status model deck's Facet 3 renders against. diff --git a/docs/specs/deck-ops-cockpit.md b/docs/specs/deck-ops-cockpit.md new file mode 100644 index 0000000..b280db5 --- /dev/null +++ b/docs/specs/deck-ops-cockpit.md @@ -0,0 +1,474 @@ +# Deck Ops Cockpit — Cost/Ops Panel + Re-Ported Mystira Ops Tooling (R13) + +- **Status:** Draft (design spec — no implementation) +- **Repo:** `phoenixvc/deck` (this repo). Companion doc lives in mystira-workspace for cross-repo review only. +- **Last updated:** 2026-08-10 — authored 2026-07-11; §1.1/§1.2/§1.4 re-grounded against `dev` + @ `bc0d1c5` before commit (PR #3 added the Camera panel and cleaned part of the extraction + residue; PR #5 added CI). The design and the three facets are unchanged. +- **Scope:** Turn deck from the current generic ops shell into the **operator cockpit over the + R13 Mystira AI-content pipeline** (`sluice → story-generator → publisher`; `docket` meters spend). + Three owner-selected facets: (1) a **cost/ops cockpit** in the Dashboard, (2) **re-port** the + Cosmos Explorer + Migration Manager panels stripped on the devhub→deck extraction, (3) + **Service-Manager batch-run ops** over the specced nightly story-gen Batch API. +- **Docs-convention note:** before this spec landed, deck had only `docs/guides/`. This spec + establishes `docs/specs/` as the home for deck design specs (mirrors story-generator's + `docs/specs/` and docket's `docs/architecture/specs/`). The ADR lands at `docs/adr/0001-*.md` + — see §9. +- **ADR:** `docs/adr/0001-deck-as-panel-host-for-mystira-ops-tooling.md` (this spec's companion + decision — deck-as-panel-host-in-a-generic-shell). + +--- + +## 0. One-line summary + +deck hosts three Mystira-facing surfaces inside its existing VSCode-style shell — a **Dashboard +cost/ops section** (sluice health + docket spend), a **re-ported Cosmos Explorer + Migration +Manager**, and a **Service-Manager batch-run monitor** — each added through deck's existing +panel-host recipe, each consuming a **real, specced upstream contract**, and each degrading +gracefully when its upstream is only specced (not yet built). + +--- + +## 1. Current-state grounding (deck as-is, 2026-08-10 — `dev` @ `bc0d1c5`) + +### 1.1 The shell and the panel-host recipe + +deck is a Tauri 2.0 + React 18/TS5/Vite5/Tailwind app (`app/`) over a Rust backend +(`app/src-tauri/`), a .NET 9 CLI sidecar (`Deck.CLI/` + `Deck.Services/`), and two Rust crates — +`crates/deck-contracts/` (shared Rust↔TS contracts) and `crates/deck-camera/` (a deck-owned local +RTSP transport, added in PR #3). Adding a panel is a fixed four-file recipe (confirmed in +`CLAUDE.md` §Architecture notes and the code): + +1. `app/src/types/constants.ts` — add a `VIEWS` entry. +2. `app/src/App.tsx` — add an `ACTIVITY_BAR_ITEMS` entry. +3. `app/src/components/app/AppContent.tsx` — add a route `case`. +4. `app/src/components/app/AppSidebar.tsx` — add a description + quick-actions block. + +The **Camera panel (PR #3) is the recipe's most recent worked example**, and a useful precedent for +this spec: a self-contained panel over its own backend crate (`crates/deck-camera/`), added by +*using* the four-file recipe rather than changing the routing mechanism it plugs into — which is +exactly the shape ADR-0001 prescribes for the Mystira panels. + +Backend commands are Rust `#[tauri::command]` functions registered in +`app/src-tauri/src/main.rs` (`invoke_handler![…]`). Heavy data work (Cosmos SDK, migrations) is +delegated to the .NET sidecar over JSON-stdin/stdout via `app/src-tauri/src/cli.rs` +(`execute_devhub_cli`), which the sidecar's `Deck.CLI/Program.cs` dispatches by `command` name. + +CI (PR #5, `.github/workflows/ci.yml`) gates every PR on `cargo fmt --check`, `cargo clippy +--workspace --all-targets`, `cargo test --workspace`, and — for `app/` — `tsc --noEmit`, `vitest +run`, and `npm run build`. Any work under this spec must keep all six green. + +### 1.2 Finding: the devhub→deck extraction is **incomplete** — half-wired, not merely stripped + +The README claims the Cosmos Explorer + Migration Manager panels "were removed during +extraction." The reality is a **half-removal**: the UI is gone, the backends are not, and a few +now-dead frontend callers survive. This is load-bearing for Facet 2: + +| Layer | State today | Evidence | +| --- | --- | --- | +| Frontend panels | **Removed** — `app/src/components/cosmos/` and `.../migration/` do not exist | directories absent | +| Frontend routing/sidebar | **Clean** — `AppContent.tsx` routes only `SERVICES`/`DASHBOARD`/`INFRASTRUCTURE`/`CAMERA`/`TEST`; `AppSidebar.tsx` has no Cosmos/Migration block | `AppContent.tsx:12-33`; `AppSidebar.tsx` (no matches) | +| Dashboard quick-actions | **Dead links** — three quick-actions still call `onNavigate("cosmos")` / `onNavigate("migration")`. Neither view exists, so `AppContent`'s `default:` silently renders `` — the buttons look live and go to the wrong panel | `Dashboard.tsx:66,74,82`; `AppContent.tsx:31-32` | +| Orphaned Cosmos callers | **Present but unrendered** — `StatisticsPanel.tsx` invokes `cosmos_stats` and `ExportPanel.tsx` invokes `cosmos_export`; nothing in the tree renders either component | `StatisticsPanel.tsx:38`; `ExportPanel.tsx:52` | +| `VIEWS` enum | **Missing** `COSMOS` / `MIGRATION` — `SERVICES, DASHBOARD, INFRASTRUCTURE, CAMERA, TEST` | `app/src/types/constants.ts:2-8` | +| Rust backend | **Present + registered** — `cosmos.rs` exposes `cosmos_export`, `cosmos_stats`, `migration_run`, `fetch_environment_connections`, `check_azure_cli_login`, all in `main.rs` `invoke_handler![]` | `app/src-tauri/src/cosmos.rs`; `main.rs:42-43,86-90` | +| .NET migration backend | **Present** — `Deck.Services/Migration/MigrationService.cs` + `Deck.CLI/Commands/MigrationCommands.cs` (14 known containers) | files exist | +| .NET Cosmos stats/export | **Removed** — `Deck.CLI/Program.cs` routes **only** `"migration.run"`; `cosmos.export` / `cosmos.stats` (invoked by `cosmos.rs`) return `"Unknown command"` | `Deck.CLI/Program.cs:126-127` | + +> **Consequence:** the tree **builds and is green** — CI (PR #5) runs `tsc --noEmit`, `vitest run` +> and `npm run build` on every PR. The residue is **behavioural, not compile-time**: three Dashboard +> quick-actions navigate to views that no longer exist and land on the Service Manager instead; two +> orphaned components call Cosmos commands whose .NET handlers were stripped; and the Rust + .NET +> halves of both features sit fully wired with no UI above them. Facet 2 is therefore not a +> from-scratch build — it is **finishing an interrupted extraction**: re-add the two panels, re-add +> the two `VIEWS` entries, re-point (or delete) the dead Dashboard quick-actions, adopt or remove +> the two orphaned components, and restore the two stripped `.NET` CLI handlers. See §4. +> +> **Drift note (2026-08-10).** As authored on 2026-07-11 this section recorded a stronger finding: +> that `AppContent.tsx` and `AppSidebar.tsx` still imported the removed `CosmosExplorer` / +> `MigrationManager` components and switched on undefined `VIEWS` members, so the frontend could not +> typecheck. **PR #3 removed that dangling wiring**, and PR #5's CI now proves the build is clean. +> The finding is retained in its corrected form because the *conclusion* is unchanged — the +> extraction is still unfinished, just less visibly so, and the surviving residue (dead nav, +> orphaned callers, headless backends) is now the evidence for it. + +### 1.3 Secret-handling posture (deck's existing conventions — normative) + +From `app/security.md` and the code, deck already has a coherent secret model this spec extends +rather than replaces: + +- **No token capture.** deck relies on the operator's already-authenticated `az` / `gh` CLI + sessions; it never stores auth tokens. `cosmos.rs::fetch_environment_connections` fetches Cosmos + and Storage connection strings **on demand** via `az cosmosdb keys list` / `az storage account + show-connection-string`, using the operator's `az login`. +- **Connection strings are session-only** — entered in the UI, held in component state, never + persisted to disk (`security.md` §Credential Management). Future: system keychain via + `tauri-plugin-keytar` (deferred). +- **argv-only host exec.** deck runs host commands via `Command::new` with an argument vector — no + `sh -c`, no shell interpolation (see the ecosystem-provisioning design doc §1.4 for the + normative statement). Every new backend command in this spec follows the same rule. +- **Minimal Tauri capabilities** — `app/src-tauri/capabilities/default.json` grants only + `core:default`, `shell:allow-open`, `opener:allow-open-url`, `dialog:default` and a scoped + `fs:default`. Any new *plugin* scope is an explicit capability addition. + > **Caveat — capabilities do not gate this spec's HTTP.** deck's outbound calls use **native + > `reqwest` from Rust**, not the Tauri HTTP plugin, so nothing in `capabilities/default.json` + > constrains them. Every new outbound command in §3.6 and §5.4 must therefore enforce its own + > allowlist **in Rust** — require `https`, and permit only the configured sluice / docket / + > story-gen hosts — rather than relying on the capability system. Related: the existing + > `services/status.rs:44` health client sets `danger_accept_invalid_certs(true)` for localhost + > self-signed certs; the cockpit's clients must **not** reuse that setting against remote + > endpoints. + +### 1.4 Naming residue (must be cleaned as part of any re-port) + +The extraction left DevHub identifiers that will break the re-ported paths: `main.rs:69` logs +`"Mystira DevHub starting…"`; `cli.rs:58,74,77` error text points the operator at +`tools/Mystira.DevHub.CLI`, and `helpers.rs:111-123` (`get_cli_executable_path()`) resolves the +sidecar binary by joining `Mystira.DevHub.CLI[.exe|.dll]` — the sidecar is now `Deck.CLI`. +`Deck.CLI/Program.cs` doc-comments say "DevHub" too. Facet 2 must reconcile the CLI-executable path +so `execute_devhub_cli` actually locates the `Deck.CLI` build output, or the re-ported +Cosmos/Migration commands will fail to spawn. + +--- + +## 2. Cockpit panel map (how the three facets land in the shell) + +| Facet | Lands in | Mechanism | New view? | +| --- | --- | --- | --- | +| **1 — Cost/Ops** | Existing **Dashboard** view | New "Pipeline Health" + "AI Spend" sections in `Dashboard.tsx`; new `costStore` (zustand, mirrors `connectionStore`); new Rust `cost.rs` Tauri module | No — extends Dashboard | +| **2 — Cosmos Explorer + Migration Manager** | New **Cosmos** + **Migration** views | Re-add `VIEWS.COSMOS`/`VIEWS.MIGRATION` + `ACTIVITY_BAR_ITEMS` + panels; backend already present (§1.2) | Yes — re-adds two views | +| **3 — Batch-run ops** | Existing **Services** view | New "Story-Gen Batch Runs" section in the Service Manager; new Rust `story_gen.rs` Tauri module | No — extends Service Manager | + +This keeps the cockpit inside deck's existing IA: **spend/health is a Dashboard concern, data +tooling is its own views, and long-running-job ops is a Service-Manager concern** — no new +navigation paradigm. + +--- + +## 3. Facet 1 — Dashboard cost/ops panel (sluice health + docket spend) + +### 3.1 What it shows + +Two new sections in `Dashboard.tsx`, above "Recent Operations": + +- **Pipeline Health** — sluice gateway liveness/readiness + throughput, and (later) story-gen + batch-queue depth. Green/amber/red tiles reusing the existing `getStatusColor` / `StatusBadge` + idiom. +- **AI Spend** — this-month burn, spend-by-service (Azure OpenAI et al.), a 30-day trend + sparkline, and budget-vs-actual against sluice's `max_budget`. Sourced from docket. + +### 3.2 Data sources and transport (the core design fork) + +| Signal | Source | Endpoint | Auth | Transport | +| --- | --- | --- | --- | --- | +| Gateway liveness | sluice | `GET https://sluice.phoenixvc.tech/health/liveliness` | **none** | Rust `cost.rs` (reqwest) | +| Gateway readiness (db, litellm version, callbacks) | sluice | `GET …/health/readiness` → JSON | **none** | Rust `cost.rs` | +| Throughput / request rollups | sluice | `GET …/metrics` (Prometheus text) | **none** (or scrape-scoped) | Rust `cost.rs` — parse Prometheus | +| Spend summary / by-service / burn / trend | **docket MCP** | `get_burn_total`, `get_spend_by_service`, `get_cost_trend`, `get_ai_spend` | `MCP_SECRET` Bearer | see §3.3 | +| Optimization insights | docket MCP | `get_optimization_insights` | `MCP_SECRET` Bearer | see §3.3 | +| Per-component LLM spend | docket MCP | `get_llm_usage_by_component` | `MCP_SECRET` Bearer | **gated** — §7 of the docket spec | + +**sluice is a green dependency** (verified live: `/health/readiness` → 200, +`litellm_version 1.83.7`, `db:"connected"`, Prometheus + OTel callbacks loaded — sluice go-live +audit §2.4). deck reaches it over the **stable custom domain** `https://sluice.phoenixvc.tech`, +never the raw ACA FQDN (which changes on CAE rebuild). The health/throughput half needs **no +secret**, so it is buildable immediately. + +> **Do NOT put the LiteLLM master key in deck.** sluice's row-level `/spend/logs` requires the +> LiteLLM master/admin key (`pvc-prod-sluice-kv/gateway-key`, sluice audit §4.3). That is a +> high-privilege, org-wide credential; a desktop app is the wrong place to hold it. deck reads +> **aggregated** spend through docket (which already ingests sluice spend as its domain job) and +> only touches sluice directly for **unauthenticated** health/metrics. If a future need forces +> direct `/spend/logs` access, it must be a deliberate, separately-reviewed KV-access decision — +> out of scope here. + +### 3.3 The MCP-from-a-desktop-app problem (must be resolved before Facet 1's spend half) + +docket's cost surface is specced as an **MCP server** (`07_docket_mcp_server.md`, ADR-09): +FastMCP mounted at `/mcp` on docket's FastAPI process, JSON-RPC over HTTP + `MCP_SECRET` Bearer, +deployed on Railway, discovered by **agents** via `claude mcp add --scope user`. That registration +model is agent-facing (Claude Code reads `~/.claude.json`). **deck is not an LLM agent** — it +cannot "discover and call MCP tools" the way Claude Code does. Two viable options; pick one before +building the spend half: + +- **Option A (recommended) — deck speaks MCP JSON-RPC directly.** A Rust `cost.rs` MCP client + performs the MCP `initialize` handshake against docket's `/mcp` and issues `tools/call` for + `get_burn_total` et al., sending `Authorization: Bearer `. Pro: consumes the exact + specced surface, no new docket work. Con: deck must implement a minimal MCP-over-HTTP client and + manage the bearer (see §3.4). +- **Option B — request docket expose a plain REST cost facade.** The docket MCP is mounted + *alongside* the existing FastAPI REST app; docket could expose the same six Phase-1 queries as + REST (`GET /cost/burn`, `/cost/by-service`, …) that deck calls with a simpler read token. Pro: + trivial for a desktop client. Con: **new docket work not in the current spec** — must be + requested of the docket team and is a net-new cross-repo dependency. + +**Recommendation:** Option A. It aligns with the ecosystem's "MCP over HTTP + Bearer is the wire +contract" stance (ADR-09) and needs nothing docket hasn't already specced. Treat the MCP client +as a small Rust module; the bearer is handled per §3.4. + +### 3.4 Auth / secret handling (never commit bearers) + +- The docket `MCP_SECRET` bearer is a per-developer secret. It must **never** enter the deck repo + (matches deck's `security.md` "never commit credentials" and the org rule that MCP bearers live + only in `~/.claude.json`). +- **Where deck reads it:** an env var (`DOCKET_MCP_TOKEN`) or deck's existing app-config store + (`config.rs` / `get_app_config`), which is machine-local and git-ignored — the same posture as + Cosmos connection strings (session/local only, never persisted to the repo). It is **not** put + in `~/.claude.json` (that file is Claude Code's, not deck's). +- **Endpoint config:** the docket `/mcp` base URL and the sluice base URL are non-secret and can + live in `Deck.CLI/appsettings.json` / deck app-config with env-override, defaulting to + `https://sluice.phoenixvc.tech` and the docket Railway `/mcp` URL. + +### 3.5 Graceful degradation (docket MCP is only specced, not built) + +The spend half **must** assume docket is absent and light up incrementally: + +- On mount, `costStore.probeCost()` first hits sluice health/metrics (works today), then attempts + the docket handshake. +- If docket is unreachable / unregistered / returns auth failure, the AI-Spend tiles render a + neutral **"docket cost API not yet available"** state (not an error, not a fabricated zero) — + mirroring how `get_llm_usage_by_component` must report `coverage: none` rather than a silent zero + (docket spec §7). The Pipeline-Health section stays fully live off sluice. +- A single `costStore` flag (`docketAvailable: boolean`) drives the whole spend section, so Facet 1 + ships **useful on day one** (health/throughput) and the spend tiles activate the moment docket's + MCP goes live and a bearer is configured — no deck code change required. + +### 3.6 Backend shape (new Rust module, mirrors `cosmos.rs`/`github.rs`) + +New `app/src-tauri/src/cost.rs`, registered in `main.rs`: + +```rust +#[tauri::command] pub async fn sluice_health() -> Result; // GET /health/readiness +#[tauri::command] pub async fn sluice_metrics() -> Result; // GET /metrics (Prometheus parse) +#[tauri::command] pub async fn docket_burn_total(since: String) -> Result; // MCP tools/call get_burn_total +#[tauri::command] pub async fn docket_spend_by_service(since: String, top: Option) -> Result; +#[tauri::command] pub async fn docket_cost_trend(since: String, granularity: String) -> Result; +``` + +The docket-facing commands reuse the crate's existing `CommandResponse` envelope +(`deck-contracts::devhub`) so the frontend contract is uniform with the rest of deck. A new +`crates/deck-contracts/src/cost.rs` (or a `docket` module) can hold the spend/health DTOs so the +Rust↔TS types are generated the same way as the existing contracts. + +--- + +## 4. Facet 2 — Cosmos Explorer + Migration Manager re-port + +### 4.1 What to bring back (and what is already here) + +Per the devhub audit lineage, these were Mystira-specific panels over Cosmos data + the +account-to-account migration flow. The **backend is mostly intact** (§1.2), so the re-port is +frontend-heavy: + +- **Migration Manager panel** — a React panel over the **already-present** `migration_run` Tauri + command → `Deck.CLI` `migration.run` → `Deck.Services.MigrationService.MigrateContainerAsync` / + `MigrateBlobStorageAsync`. The 14 known containers + partition keys already live in + `MigrationCommands.cs`; the dry-run, source/dest connection, and per-container selection UX is + documented in `docs/guides/cosmos-db-migration.md`. **No new backend needed** beyond the + naming-path fix (§1.4). +- **Cosmos Explorer panel** — a React panel for stats/export/browse. The `cosmos_stats` / + `cosmos_export` Tauri commands exist and are registered, **but** their `.NET` handlers + (`cosmos.stats`, `cosmos.export`) were stripped from `Deck.CLI/Program.cs` (only `migration.run` + survives). Re-port therefore also requires **restoring those two CLI handlers** in the sidecar + (a `CosmosCommands` class dispatched from `Program.cs`), or the Explorer's stats/export buttons + return `"Unknown command"`. + +### 4.2 Fit with deck's Tauri/React/.NET-9 shell + +- **Frontend:** re-create `app/src/components/cosmos/` (exporting `CosmosExplorer`) and + `app/src/components/migration/` (exporting `MigrationManager`), matching the existing panel + structure (`ServiceManager`, `InfrastructurePanel`, and the `CameraPanel` added in PR #3 — the + closest and most recent template). Re-add, following the §1.1 four-file recipe in full: + - `VIEWS.COSMOS = "cosmos"` and `VIEWS.MIGRATION = "migration"` in `constants.ts`; + - `ACTIVITY_BAR_ITEMS` entries (e.g. `🗄️ Cosmos`, `🔄 Migration`) in `App.tsx`; + - **new** `AppContent.tsx` route cases and `AppSidebar.tsx` description/quick-action blocks. PR #3 + deleted the originals, so these are written fresh rather than reactivated; + - re-point the three dead `Dashboard.tsx` quick-actions (§1.2), which currently fall through to + the Service Manager, and decide whether the orphaned `StatisticsPanel.tsx` / `ExportPanel.tsx` + become the Explorer's stats/export surfaces or are deleted as dead code. +- **Connection UX:** reuse `connectionStore` (already lists a "Cosmos DB" connection, `type: + "cosmos"`, `🗄️`) and `fetch_environment_connections(cosmosAccountName)` for auto-discovery of the + resource group + storage account from the Cosmos account name. +- **.NET-9 sidecar:** the migration path already flows through `execute_devhub_cli`; restore the + Cosmos stats/export handlers alongside it. Note `Program.cs` currently **requires an `AuthToken` + with the `admin` role on every command** (`AuthService.IsAuthorizedAsync`) — the re-ported + panels must supply this token exactly as the migration flow does. + +### 4.3 Mystira-Cosmos connection / secret model + +- deck **never stores** Cosmos connection strings in the repo or on disk (§1.3). The operator + either pastes a connection string into the panel (session-only) or lets + `fetch_environment_connections` mint one on demand from their `az login`. +- Canonical Mystira Cosmos accounts (from `cosmetics_pr_sequence` / prod-identity lineage and the + migration guide): destination `mys-{env}-core-cosmos` (the guide still shows the legacy + `-san`-suffixed names and the retired `prodwusappmystiracosmos` source — the re-ported panel's + presets should be updated to the ADR-0027 canonical names, no region suffix). +- **COPPA/PII caveat:** Cosmos containers hold child-facing data (`UserProfiles`, `Accounts`, + `GameSessions`). The Explorer is an operator tool behind the `admin` role; it must not export + PII to arbitrary local paths without the existing confirm-dialog + audit-log posture + (`security.md` §Audit and Logging). Export destinations stay operator-chosen and session-scoped. + +### 4.4 Re-port is independent of the AI pipeline + +Facet 2 has **no upstream on sluice/docket/story-gen**. Its only dependency is Mystira-Cosmos +access via the operator's own `az` session — which is already deck's model. It is the one facet +buildable end-to-end today (modulo the extraction-completion + naming-path fixes). + +--- + +## 5. Facet 3 — Service-Manager batch-run ops (story-gen nightly Batch API) + +### 5.1 What it does + +A "Story-Gen Batch Runs" section in the Service Manager view that lets an operator: + +1. **Trigger** a bulk/nightly generation run (enqueue a batch workload). +2. **List/monitor** batch operations by **operation id + status**. +3. **Surface failures** (errored / expired / re-queued items) and the kill-switch state. + +### 5.2 Design against the specced operation-id + status model + +From `BATCH_API_GENERATION_ROUTING.md` (R13, Draft — **not yet built**), the model deck renders +against: + +- **Enqueue → operation id.** A bulk request returns an **operation id immediately (202)**, exactly + like the continuity async pattern. `custom_id == BatchGenerationItem.Id`. +- **Item status ladder:** `Queued → Submitted → Succeeded | Errored | Expired | Failed` (with + `BatchId msgbatch_…`, `SubmittedAt`, `CompletedAt`, `Attempts`). +- **Batch status:** `BatchSubmission { BatchId, WindowId (batch-YYYYMMDD), ItemCount, + ProcessingStatus (in_progress|canceling|ended), Counts { processing, succeeded, errored, + canceled, expired } }`. +- **Two-phase cycle:** submit ~02:00 UTC, poll/retrieve every ~15 min. +- **Kill-switch:** `Ai:Sluice:Batch:Enabled` (default `false`). deck should **display** this flag's + state (read-only) so the operator knows whether batch is even active in that environment. + +### 5.3 Endpoints deck consumes (proposed by the story-gen spec — verify before building) + +| deck action | story-gen endpoint | Status | +| --- | --- | --- | +| Monitor one operation | `GET api/stories/batch/{operationId}` | **Proposed** (spec Appendix), not built | +| Enqueue a bulk workload | `POST api/stories/batch` | **Proposed**, not built | +| Trigger the submit phase **on demand** | *(none)* | **Does not exist** — nightly cron only | + +> **Two blockers to flag.** (a) The endpoints are a **proposed implementation checklist**, not +> shipped API. (b) There is **no on-demand "run the submit phase now" trigger** — the submit phase +> is a scheduled job (Container App Job or in-process `BackgroundService`, a deferred +> mystira-mason decision). So deck's "trigger a run" realistically means **enqueue items** via +> `POST api/stories/batch` and let the next nightly cycle submit them — deck **cannot** force an +> immediate LLM submission unless story-gen adds an ad-hoc trigger endpoint. deck should either +> (i) scope "trigger" to enqueue-only, or (ii) request an explicit on-demand submit endpoint from +> the story-gen team. + +### 5.4 Transport, auth, backend shape + +- New `app/src-tauri/src/story_gen.rs` Tauri module (mirrors `github.rs`'s workflow-dispatch/status + pattern), reqwest over HTTP to the story-gen App Service: + +```rust +#[tauri::command] pub async fn batch_enqueue(payload: BatchEnqueueRequest) -> Result; // POST api/stories/batch +#[tauri::command] pub async fn batch_status(operation_id: String) -> Result; // GET api/stories/batch/{id} +#[tauri::command] pub async fn batch_kill_switch() -> Result; // read Ai:Sluice:Batch:Enabled (via a story-gen status endpoint) +``` + +- **Contract placement (open):** `BatchOperation` above is a placeholder — it is *not* defined + anywhere yet, and it must not silently conflate `BatchGenerationItem` (one item's status ladder) + with `BatchSubmission` (a whole batch's rollup); §5.2 keeps them distinct and the DTOs should + too. deck already ships `crates/deck-contracts/src/story_generator.rs`, which is the natural home + for these types and should be reused/extended rather than duplicated in `story_gen.rs`. Settle + the exact request/response shapes — plus `POST` idempotency on `custom_id == + BatchGenerationItem.Id`, and whether the kill-switch is read-only or writable — with the + story-gen team when their endpoints move from proposed to built (§5.3). +- **Auth:** story-gen's batch endpoints sit behind `[Authorize]` + rate-limiting (spec §8.3). deck + needs a story-gen API credential/token for the target environment — **held session/local only**, + never in the repo (same posture as §3.4). The exact scheme (bearer, magic-link-derived, service + principal) is a story-gen decision to confirm. +- **Attribution visibility (nice-to-have):** batched requests are tagged `Workflow="nightly-batch"` + for docket attribution (story-gen spec §7). Once Facet 1's docket path is live, deck can + cross-link a batch run to its `get_llm_usage_by_component` slice — closing the loop between "I + ran this batch" and "it cost this much." This is the cockpit's payoff and the reason all three + facets belong in one tool. + +### 5.5 Batch ops is design-ready but non-functional until upstream ships + +deck can build the monitor UI now against the specced status model (it is stable and detailed), +but **nothing works until** story-gen ships `POST/GET api/stories/batch`, `Ai:Sluice:Batch:Enabled` +is flipped, and (upstream of *that*) sluice exposes the Anthropic `/v1/messages/batches*` passthrough +(story-gen spec §3c blocker, sluice task `5dc28591`). Build the UI behind a feature flag; keep it +dark in every environment until the endpoints return real data. + +--- + +## 6. Cross-cutting concerns + +- **Never commit secrets.** Three new credentials appear across the facets: docket `MCP_SECRET` + (Facet 1), Mystira-Cosmos connection strings (Facet 2), a story-gen API token (Facet 3). All are + session/local-only, sourced from env or machine-local app-config, and **must never enter the + deck repo** — consistent with `security.md` and the org's MCP-bearer rule. +- **argv-only + minimal capabilities.** New backend commands use `Command::new`/reqwest with no + shell interpolation; outbound HTTP to sluice/docket/story-gen is added as an explicit Tauri + capability, not a blanket network grant. +- **Naming residue.** The DevHub→Deck cleanup (§1.4) is a prerequisite for Facet 2 (CLI path + resolution) and good hygiene for the rest; do it in the same PR as the extraction-completion. +- **Sibling future panel (out of scope, noted for coherence).** The ecosystem-provisioning design + proposes a **deck Models panel** (local-model detect/install → report capability to sluice's + registry). It shares this cockpit's exec + Service-Manager machinery and the same argv-only/ + allowlist/consent posture, but is a separate proposal — not part of this spec. + +--- + +## 7. Per-facet readiness (build-now vs blocked-on-upstream) + +| Facet | Sub-capability | Readiness | Blocking upstream | +| --- | --- | --- | --- | +| **1 — Cost/Ops** | sluice **health + throughput** tiles | ✅ **Build now** | none — sluice LIVE (audit §2.4), unauthenticated | +| **1 — Cost/Ops** | docket **spend / budget / trend** tiles | ⛔ **Blocked** | docket MCP built + deployed + registered + per-dev bearer; **and** the MCP-consumption fork resolved (§3.3) | +| **2 — Cosmos Explorer + Migration Manager** | Migration Manager panel | ✅ **Build now** | none — backend present; needs frontend re-add + dead-nav re-point (§1.2) + naming-path fix + `az` login | +| **2 — Cosmos Explorer + Migration Manager** | Cosmos Explorer panel | ✅ **Build now** (small backend restore) | none upstream — but must **restore** the stripped `cosmos.stats`/`cosmos.export` .NET CLI handlers | +| **3 — Batch-run ops** | Monitor UI (against specced status model) | 🟡 **Design/build now, dark** | story-gen Batch API not built | +| **3 — Batch-run ops** | Functional trigger + live status | ⛔ **Blocked** | story-gen `POST/GET api/stories/batch` + `Ai:Sluice:Batch:Enabled`; on-demand trigger endpoint (absent); sluice batch passthrough (task `5dc28591`) | + +**Net:** Facet 2 is fully deliverable now (it is really *finishing the extraction*). Facet 1 is +**half deliverable now** (health), half blocked (spend). Facet 3 is **design-ready but +non-functional** until the story-gen Batch API ships. Recommended build order: **Facet 2 → Facet 1 +health half → Facet 1 spend half (when docket lands) → Facet 3 (when Batch API lands).** + +--- + +## 8. Cross-repo dependency list (what must become real for the cockpit to function) + +1. **docket MCP** (`phoenixvc/docket`) — Phase-1 tools built (`get_burn_total`, + `get_spend_by_service`, `get_cost_trend`, `get_ai_spend`, `get_spend_by_resource_group`, + `get_spend_by_region`), deployed on Railway behind `MCP_SECRET`, and **registered in the org + MCP** (`register_project`). Status: **specced, not built** (spec `07_docket_mcp_server.md`, + ADR-09; baton `91ca9c86-4057-49ad-8d2e-4744188daee1`). + - **Sub-dependency (deck-specific):** resolve how a **non-agent desktop app** consumes the MCP — + either deck ships an MCP JSON-RPC client (Option A, recommended) **or** docket exposes a REST + cost facade (Option B, net-new docket work). The current spec provides only the MCP surface. + - **Sub-dependency:** a **per-developer docket bearer** provisioned to deck via env/app-config + (never the repo, never `~/.claude.json`). +2. **sluice** (`phoenixvc/sluice`) — `https://sluice.phoenixvc.tech` `/health/*` + `/metrics` + reachable. Status: **LIVE ✓** (audit §2.4). *Do not* wire deck to `/spend/logs` (needs the + LiteLLM master key `pvc-prod-sluice-kv/gateway-key`) — route spend through docket instead. +3. **story-gen Batch API** (`mystira-workspace/apps/story-generator`) — endpoints + `POST api/stories/batch` (enqueue) + `GET api/stories/batch/{operationId}` (status) exposed with + an auth scheme deck can hold; the `Ai:Sluice:Batch:Enabled` flag flipped per environment; and — + if deck is to trigger runs on demand — an **on-demand submit trigger endpoint that does not yet + exist** (nightly cron only). Status: **specced, not built** (`BATCH_API_GENERATION_ROUTING.md`; + baton `944e357c-6491-47fc-be80-058e13ddcee3`). + - **Transitive blocker:** the Batch API itself is blocked on **sluice exposing the Anthropic + `/v1/messages/batches*` passthrough** with auth + metadata attribution (story-gen spec §3c; + sluice task `5dc28591`). Until that holds, Facet 3 cannot function even if deck + story-gen + are ready. +4. **deck internal (this repo)** — complete the interrupted devhub extraction (§1.2), fix the + DevHub→Deck naming/CLI-path residue (§1.4), and restore the stripped `cosmos.stats`/`cosmos.export` + .NET CLI handlers. No external dependency; prerequisite for Facet 2. + +--- + +## 9. ADR + +The generic-shell-vs-Mystira-coupling decision is captured in the companion ADR: +`docs/adr/0001-deck-as-panel-host-for-mystira-ops-tooling.md`. It records **why** deck hosts +Mystira-specific ops panels in a generic phoenixvc shell, the boundary that keeps the shell +reusable (panels are Mystira-coupled; the shell, exec primitives, and Service-Manager machinery +stay generic), and the placement/numbering choice for deck's first ADR.