From d452f3954aeb27a7375d543ba552330c3b0ccd95 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 11 May 2026 07:09:05 +0000 Subject: [PATCH] docs: align web-ui, cli, sdk-integrations, and ops-policy with shipped features - web-ui.md: document refactored DiffPage component tree (DiffVerdictStack, DiffReleaseTwin, DiffPolicyPanel, DiffChangeImpact/DiffPricingExpand, DiffDecisionCard, diffPayload.tsx), URL deep-linking for all pages, OverviewPage focused-release hero (?release= param), ReleaseLifecycleStrip, CopyTextButton, urlSearch.ts helpers, updated routing table, new CSS classes for DiffPage and OverviewPage additions - cli.md: add flightdeck pricing check subcommand (--max-age-days, --fail) with example output and CI usage pattern - pricing-catalog.md: link flightdeck pricing check reference to cli.md - operations-and-policy.md: add schema migration v4 (promotion_requests table); update storage schema table from 7 to 8 tables - sdk-integrations.md: add Module reference section documenting make_run_end_event, temporal_labels, and per-integration public APIs (openai_chat, anthropic_messages, openai_agents, langchain_callback, crewai_bridge) Co-authored-by: Gottam Sai Bharath --- docs/cli.md | 31 ++++++ docs/operations-and-policy.md | 4 +- docs/pricing-catalog.md | 2 +- docs/sdk-integrations.md | 94 ++++++++++++++++ docs/web-ui.md | 203 +++++++++++++++++++++++++++------- 5 files changed, 294 insertions(+), 40 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 5591085..efd4186 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -412,6 +412,37 @@ flightdeck pricing show --provider PROVIDER --version VERSION Both flags are required. If the table does not exist, exits 1 with an error message. +### `flightdeck pricing check` + +Check the age of **`flightdeck-bundled-*`** pricing tables in the ledger. Prints one line +per bundled snapshot with its anchor date and approximate age. Non-bundled tables are +ignored. + +```bash +flightdeck pricing check [--max-age-days N] [--fail] +``` + +| Option | Default | Description | +|--------|---------|-------------| +| `--max-age-days` | `90` | Threshold in days. Tables older than this print `STALE` to stderr (and count toward `--fail`). Tables at or under the limit print `OK`. | +| `--fail` | off | Exit 1 if any bundled table exceeds `--max-age-days`. Useful as a CI gate. | + +**Example output:** +``` +OK flightdeck-bundled-2026-05 (~11 days old; max 90) +``` + +If no `flightdeck-bundled-*` tables are in the ledger (e.g. after `flightdeck init --no-bundled-pricing`), +exits 0 and prints `No flightdeck-bundled-* pricing tables in the ledger.` + +Use in CI to surface stale bundled snapshots before they silently affect cost estimates: +```bash +flightdeck pricing check --max-age-days 90 --fail +``` + +See [pricing-catalog.md](pricing-catalog.md) for the bundled snapshot lifecycle and when +to replace with `flightdeck pricing import`. + --- ## `flightdeck policy` diff --git a/docs/operations-and-policy.md b/docs/operations-and-policy.md index 0e53a88..cec11e0 100644 --- a/docs/operations-and-policy.md +++ b/docs/operations-and-policy.md @@ -521,7 +521,7 @@ endpoints (`GET /v1/releases`, `GET /v1/promoted`, `GET /v1/actions`) and intern ## SQLite storage schema -The operations layer reads and writes seven tables (via `src/flightdeck/storage.py`): +The operations layer reads and writes eight tables (via `src/flightdeck/storage.py`): | Table | Purpose | |-------|---------| @@ -532,6 +532,7 @@ The operations layer reads and writes seven tables (via `src/flightdeck/storage. | `active_policy` | Single-row table holding the active `Policy` JSON | | `promoted_releases` | Current promoted pointer per `(agent_id, environment)` | | `release_actions` | Append-only audit ledger; `audit_seq` is monotonically increasing | +| `promotion_requests` | Pending / completed / cancelled approval requests (added in migration v4); used when `promotion_requires_approval: true` | `Storage.migrate()` runs forward-only numbered migrations. `flightdeck doctor` verifies that migrations are applied through `LATEST_SCHEMA_MIGRATION_VERSION` and that @@ -593,6 +594,7 @@ Migrations are numbered and forward-only; they are never reversed. | 1 | Initial schema (all base tables via `CREATE TABLE IF NOT EXISTS`) | | 2 | `CREATE INDEX … ON run_events(release_id, timestamp)` — speeds up diff/query | | 3 | `ALTER TABLE release_actions ADD COLUMN audit_seq INTEGER`; backfill existing rows; add unique index | +| 4 | `CREATE TABLE IF NOT EXISTS promotion_requests` — adds the approval request/confirm workflow (columns: `request_id`, `status`, `release_id`, `agent_id`, `environment`, `window`, `reason`, `actor`, `baseline_release_id`, `policy_result_json`, `created_at`, `resolved_at`, `completed_action_id`) | New migrations must increment `LATEST_SCHEMA_MIGRATION_VERSION` in `storage.py` and add a corresponding check in `test_schemas.py` (or `test_doctor.py`). diff --git a/docs/pricing-catalog.md b/docs/pricing-catalog.md index ce150c3..dc00b0c 100644 --- a/docs/pricing-catalog.md +++ b/docs/pricing-catalog.md @@ -24,7 +24,7 @@ your own YAML (and optionally **`--replace`** with **`--reason`**). Bundled table YAML in the wheel includes **comment links** to each provider’s official list-pricing page so you can spot-check rates between FlightDeck releases. -**Staleness guardrails:** list prices change often. Run **`flightdeck pricing check`** to see whether any **`flightdeck-bundled-*`** table in the ledger is older than **`--max-age-days`** (default **90**); pass **`--fail`** for CI. **`flightdeck release diff`** and **`POST /v1/diff`** add **`pricing.warnings`** when baseline or candidate **`pricing_version`** is a stale bundled snapshot so economics do not look authoritative after the snapshot has aged out. +**Staleness guardrails:** list prices change often. Run **`flightdeck pricing check`** to see whether any **`flightdeck-bundled-*`** table in the ledger is older than **`--max-age-days`** (default **90**); pass **`--fail`** for CI. **`flightdeck release diff`** and **`POST /v1/diff`** add entries to **`pricing.warnings`** when baseline or candidate **`pricing_version`** is a stale bundled snapshot so economics do not look authoritative after the snapshot has aged out. See [cli.md § flightdeck pricing check](cli.md#flightdeck-pricing-check) for the full option reference. **Maintainer cadence:** the bundled snapshot is **updated on each minor release** when vendor public list pricing changes materially (see **[ROADMAP.md](../ROADMAP.md)**). Operators in production should still treat **`flightdeck pricing import`** as the source of truth. diff --git a/docs/sdk-integrations.md b/docs/sdk-integrations.md index 7c0a329..b0dcfdb 100644 --- a/docs/sdk-integrations.md +++ b/docs/sdk-integrations.md @@ -52,6 +52,100 @@ batch processor. Set **`OTEL_EXPORTER_OTLP_ENDPOINT`** (for example FlightDeck does not auto-instrument **`httpx`** or the Python SDK; create spans in your app or attach upstream auto-instrumentation if you need request-level traces. +## Module reference + +Each submodule under `flightdeck.integrations` has a single responsibility: map +third-party SDK output into a `RunEvent`. Import only the submodule you need. + +### `flightdeck.integrations.common` (no extras required) + +Available as `from flightdeck.integrations import make_run_end_event, temporal_labels`. + +#### `make_run_end_event(**kwargs) -> RunEvent` + +Convenience constructor for a `type=run_end` `RunEvent`. All named parameters map +directly to fields on the v1 wire shape: + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `agent_id` | yes | Stable agent ID | +| `release_id` | yes | Release ID from `flightdeck release register` | +| `run_id` | yes | Unique identifier; duplicates are skipped at ingest | +| `tenant_id` | yes | Tenant scoping dimension | +| `task_id` | yes | Task type dimension | +| `environment` | yes | Deployment environment | +| `provider` | yes | LLM provider (e.g. `"openai"`) | +| `model` | yes | Model name (e.g. `"gpt-4o"`) | +| `input_tokens` | yes | Prompt token count | +| `output_tokens` | yes | Completion token count | +| `cached_input_tokens` | no | Cached-prompt token count (default `0`) | +| `latency_ms` | no | End-to-end latency in milliseconds | +| `success` | no | Whether the run succeeded (default `True`) | +| `error_type` | no | Optional error class string | +| `trace_id`, `session_id`, `span_id` | no | Tracing identifiers (stored in `request.*`) | +| `labels` | no | Arbitrary string labels dict | +| `timestamp` | no | Event timestamp (defaults to `datetime.now(UTC)`) | +| `workspace_id` | no | Workspace identifier (default `"ws_local"`) | + +#### `temporal_labels(*, workflow_id, workflow_run_id=None) -> dict[str, str]` + +Returns a `labels` dict with `temporal.workflow_id` (and optionally `temporal.run_id`) +for tagging run events emitted from Temporal workflows. Pass the result as the `labels=` +argument to `make_run_end_event`. + +### `flightdeck.integrations.openai_chat` (no extra needed; `openai` extra for the SDK itself) + +#### `run_event_from_openai_chat_completion(response, *, agent_id, release_id, run_id, tenant_id, task_id, environment, **kwargs) -> RunEvent` + +Constructs a `RunEvent` from an `openai.types.chat.ChatCompletion` response object. +Extracts `model`, `input_tokens`, `output_tokens`, and `cached_input_tokens` from +`response.usage`. Extra `kwargs` are passed to `make_run_end_event` (e.g. `latency_ms`, +`trace_id`). See `examples/integration/adoption/openai_chat/emit_run.py`. + +### `flightdeck.integrations.anthropic_messages` (no extra needed; `anthropic` extra for the SDK itself) + +#### `run_event_from_anthropic_message(message, *, agent_id, release_id, run_id, tenant_id, task_id, environment, **kwargs) -> RunEvent` + +Constructs a `RunEvent` from an `anthropic.types.Message` object. Extracts `model`, +`input_tokens`, `output_tokens`, and `cache_read_input_tokens` from `message.usage`. +See `examples/integration/adoption/anthropic_messages/emit_run.py`. + +### `flightdeck.integrations.openai_agents` (`integrations-openai-agents` extra) + +#### `run_event_from_openai_agents_result(result, *, agent_id, release_id, run_id, tenant_id, task_id, environment, **kwargs) -> RunEvent` + +Constructs a `RunEvent` from an OpenAI Agents SDK `RunResult` (or compatible object). +Aggregates token usage across all items in `result.raw_responses`. See +`examples/integration/adoption/openai_agents/emit_run.py`. + +### `flightdeck.integrations.langchain_callback` (`integrations-langchain` extra) + +#### `FlightDeckLangChainCallbackHandler` + +A `BaseCallbackHandler` subclass. Pass an instance to LangChain chains or agents as +`callbacks=[handler]`. On `on_llm_end`, extracts token usage from the LLM result and +appends a `RunEvent` to `handler.events` (a list). After the chain completes, call +`client.ingest_run_events(handler.events)`. Constructor parameters: + +| Parameter | Description | +|-----------|-------------| +| `agent_id` | Stable agent ID | +| `release_id` | Release ID | +| `run_id` | Unique run identifier (used for all events this handler captures) | +| `tenant_id`, `task_id`, `environment` | Standard scoping dimensions | + +See `examples/integration/adoption/langchain/emit_run.py`. + +### `flightdeck.integrations.crewai_bridge` (no extra; install `crewai` in your app env) + +#### `run_event_from_crew_token_totals(input_tokens, output_tokens, *, model, provider, agent_id, release_id, run_id, tenant_id, task_id, environment, **kwargs) -> RunEvent` + +Constructs a `RunEvent` from manually collected CrewAI token totals (no direct dependency +on CrewAI's internal classes). Collect totals from your crew's result callbacks and pass +them here. See `examples/integration/adoption/crewai/emit_totals.py`. + +--- + ## Trust boundaries Anyone who can reach **`POST /v1/events`** can append ledger rows. Keep **`flightdeck serve`** diff --git a/docs/web-ui.md b/docs/web-ui.md index 5dd4df4..73e494b 100644 --- a/docs/web-ui.md +++ b/docs/web-ui.md @@ -54,11 +54,11 @@ The app uses **HashRouter** (`react-router-dom`) so all navigation stays within | Hash path | Component | HTTP calls | Notes | |-----------|-----------|-----------|-------| -| `#/` | `OverviewPage` | `GET /v1/releases`, `GET /v1/promoted`, `GET /v1/actions`, `GET /v1/metrics` (parallel where applicable) | Ledger metrics (read-only); short per-counter hints; skeleton on first load; **auto-refresh** every 30s when the tab is visible + on timeline **`generation`** bump; links to Diff/Runs | -| `#/diff` | `DiffPage` | `POST /v1/diff` | Sections: policy gate (incl. `evaluated_at`), evidence window, pricing/catalog/hints (incl. provider/version skew callout when sides differ), per-1k prices when present, cost/quality rollups; raw JSON panel | +| `#/` | `OverviewPage` | `GET /v1/releases`, `GET /v1/promoted`, `GET /v1/actions`, `GET /v1/metrics` | `ReleaseLifecycleStrip` + optional `?release=` hero; promoted table first; releases table with filter row and copy/diff shortcuts; collapsible ledger metrics; **auto-refresh** every 30 s while tab is visible + on timeline **`generation`** bump | +| `#/diff` | `DiffPage` | `POST /v1/diff` | URL params prefill form (`baseline`, `candidate`, `window`, `environment`); result rendered through `DiffVerdictStack` → `DiffReleaseTwin` → `DiffPolicyPanel` → `DiffChangeImpact` (with collapsible `DiffPricingExpand`) → `DiffDecisionCard` + **Continue to promote** link → raw JSON panel | | `#/runs` | `RunsPage` | `GET /v1/releases` (for datalist), `GET /v1/runs`, `GET /v1/runs/export` | Forensics: filters, table (trace/status, trace band rows or **Group by trace_id**), **View** drawer (focus trap, session/span ids), typed **run-query error** card with **Retry**, empty/offset/truncation hints, NDJSON download | | `#/settings` | `SettingsPage` | *(none)* | **Color theme** (Light / Dark / System) via `ThemeToggle`; more preferences later. | -| `#/actions` | `ActionsPage` | `GET /v1/workspace`, `GET /v1/promotion-requests` (when `promotion_requires_approval`), `POST /v1/promote` **or** `POST /v1/promote/request` + `POST /v1/promote/confirm`, `POST /v1/rollback` | Workspace skeleton then strip; approval path: numbered steps, pending **Refresh list** / **Use for confirm**; **Rollback** danger-styled; see **ActionsPage** below | +| `#/actions` | `ActionsPage` | `GET /v1/workspace`, `GET /v1/promotion-requests` (when `promotion_requires_approval`), `POST /v1/promote` **or** `POST /v1/promote/request` + `POST /v1/promote/confirm`, `POST /v1/rollback` | URL params prefill form (`release_id`, `environment`, `window`); workspace skeleton then strip; approval path: numbered steps, pending **Refresh list** / **Use for confirm**; **Rollback** danger-styled | | `#/*` (any other) | — | Redirects to `#/` | | `App.tsx` declares the route tree. `AppShell` is the layout wrapper rendered for all routes. @@ -81,7 +81,20 @@ ThemePreferenceProvider (`App.tsx`) ├── aside.fd-sidebar (brand, collapse chevron, primary nav, footer nav → Settings) └── div.fd-shell__content ├── SecurityStatusBar - └── main#main-content → OverviewPage | DiffPage | RunsPage | ActionsPage | SettingsPage + └── main#main-content + ├── OverviewPage + │ ├── ReleaseLifecycleStrip + │ └── focused release hero (when ?release= is set) + ├── DiffPage + │ ├── DiffVerdictStack + │ ├── DiffReleaseTwin + │ ├── DiffPolicyPanel + │ ├── DiffChangeImpact → DiffPricingExpand + │ ├── DiffDecisionCard + │ └── JsonPanel + ├── RunsPage + ├── ActionsPage + └── SettingsPage ``` --- @@ -172,20 +185,50 @@ fail. This is a configuration hint only — the server enforces the actual gate. ## `OverviewPage` (`web/src/pages/OverviewPage.tsx`) -Read-only dashboard. Renders a **Ledger metrics** card from `fetchMetrics()` plus three tables from `loadTimeline()` output: +Read-only dashboard. Layout: -| Block | Source | Content | -|-------|--------|---------| -| Ledger metrics | `GET /v1/metrics` | Releases, pricing tables, run events, promoted pointers, and actions totals (plus `actions_by_action` breakdown), `schema_version`, `generated_at` | -| Releases | `GET /v1/releases` | Release ID, Agent, Version, Environment, Checksum, Created | -| Promoted | `GET /v1/promoted` | Agent, Environment, Active release | -| Recent actions | `GET /v1/actions` | When, Action, Policy (PASS/FAIL badge), Release, Environment, Reason | +1. **`ReleaseLifecycleStrip`** — horizontal workflow guide showing the four stages + (Register → Ingest → Diff & policy → Promote & rollback) as linked steps. Each step + links to the relevant page; the Promote step is static (no link) in read-only builds. + Includes a note that deep links prefill forms but do not auto-submit. + +2. **Focused release hero** — when `?release=` is present in the URL, a hero + section appears above the tables. It shows agent, version, environment, abbreviated + release ID (with **Copy ID** button), checksum, and the current promoted baseline for + that agent/environment pair (or a note that no pointer exists). Action buttons link to + Diff, Runs, and Promote with the release, environment, and a default `7d` window + pre-filled. A **Clear focus** button removes the `?release=` param. If the ID does not + match any registered release, a warning is shown instead. + +3. **Promoted releases table** — lists current `(agent_id, environment)` → `release_id` + pointers. Each row has a **View** link to `#/?release=` to focus that release. + +4. **Releases table** — lists all registered releases with Agent, Version, Environment, ID, + Checksum, and Created columns. A **Status** badge shows **Live** (the release matches the + current promoted pointer for that agent/environment) or **Registered**. A filter row + (agent substring, environment substring, and Live / Not live / All dropdown) reduces the + table without re-fetching. **Copy** buttons (via `CopyTextButton`) copy the release ID. + Each row has a **Diff** shortcut (links to `#/diff` with baseline = promoted pointer, + candidate = this release, environment and `7d` window pre-filled) and a **Focus** link. + +5. **Recent actions table** — promote/rollback audit rows: When, Action, Policy badge, + Release, Environment, Reason. + +6. **Ledger metrics** — collapsible panel (collapsed by default, toggle via button). Shows + raw counters from `GET /v1/metrics`: releases, pricing tables, run events, promoted + pointers, actions totals + breakdown, `schema_version`, `generated_at`. Long IDs are abbreviated with `shortId(id, keepStart, keepEnd)` and shown in full on hover via the HTML `title` attribute. +**URL params for OverviewPage:** + +| Param | Effect | +|-------|--------| +| `?release=` | Activates the focused release hero. The releases table filter and tables remain visible below. | + **Refresh:** while the document tab is visible, the page **auto-polls** metrics and the -timeline on an interval and uses **silent** fetches after the first load. The `generation` +timeline every 30 s and uses **silent** fetches after the first load. The `generation` counter from `TimelineRefreshContext` triggers an immediate refresh after mutations from `ActionsPage`. @@ -193,14 +236,18 @@ counter from `TimelineRefreshContext` triggers an immediate refresh after mutati ## `DiffPage` (`web/src/pages/DiffPage.tsx`) -Form-based interface for `POST /v1/diff`. Fields mirror the request body: +Form-based interface for `POST /v1/diff`. The page reads initial field values from URL +search params and writes them back on each submission, enabling **deep links** that +pre-fill the form: -| Field | Default | Maps to | -|-------|---------|---------| -| Baseline release ID | (empty) | `baseline_release_id` | -| Candidate release ID | (empty) | `candidate_release_id` | -| Window | `7d` | `window` | -| Environment | `local` | `environment` (sent as `null` when empty) | +| URL param | Form field | Default | +|-----------|-----------|---------| +| `baseline` | Baseline release ID | (empty) | +| `candidate` | Candidate release ID | (empty) | +| `window` | Time window | `7d` | +| `environment` | Environment | `local` | + +Example: `#/diff?baseline=rel_abc&candidate=rel_xyz&window=7d&environment=production` `tenant_id` and `task_id` are **not exposed** in the UI form. To run a diff narrowed to a specific tenant or task, use the CLI (`flightdeck release diff --tenant --task `) @@ -209,25 +256,25 @@ or call `POST /v1/diff` directly with the `tenant_id` and `task_id` fields. See [operations-and-policy.md § compute_diff vs. promote_release filter scope](operations-and-policy.md#compute_diff-vs-promote_release--rollback_release-filter-scope) for details on what those filters affect. -On submit, the raw diff response is parsed and rendered as: - -- **Summary card:** policy badge (PASS / FAIL), failure reasons list, sample counts and - confidence label (including `confidence_reason` when present). -- **Pricing table warnings:** when `pricing.warnings` is a non-empty string array, a - `fd-alert--warn` list is shown above the pricing/model-change banner (diagnostic only). -- **Catalog / hints:** when `pricing.catalog` or `pricing.hints` is present, the UI surfaces - catalog enabled state, lines, and hint strings (see [pricing-catalog.md](pricing-catalog.md)). -- **Pricing change warning:** when the diff response includes a `pricing` block with - `pricing_or_model_changed: true`, a `fd-alert--warn` banner is shown in the summary - card. It names the baseline and candidate provider/version/model so the user knows the - cost delta includes pricing assumption changes, not just usage changes. When the response - also includes a `pricing.prices` block with all four per-1k token rates present, the - banner additionally shows a **Per-1k token prices** line (baseline → candidate, input and - output separately) so the user can separate tariff moves from token volume changes in the - cost delta. Rates are rendered to six decimal places via `toFixed(6)`. -- **Metric cards:** cost/run (USD), latency avg (ms), error rate — each showing baseline, - candidate, and delta. -- **Raw diff JSON** panel (collapsed by default via `JsonPanel`). +On submit, the response is parsed via helpers in `diffPayload.tsx` and rendered through a +sequence of dedicated components: + +1. **`DiffVerdictStack`** — full-width strip at the top. Shows a **Blocked** banner with the + first policy reason when policy fails, then a **verdict strip** (green PASS / red FAIL + with a short narrative). If the diff response contains no `policy` block, a warning is + shown instead. +2. **`DiffReleaseTwin`** — side-by-side baseline vs candidate IDs, environment, window, and + resolved `provider/version model` lines from each side's pricing block. +3. **`DiffPolicyPanel`** — card showing the policy PASS/FAIL badge, `evaluated_at` + timestamp, and full reasons list. +4. **`DiffChangeImpact`** — card with three sub-sections: + - **Sample coverage** — baseline/candidate run counts and confidence label (with `confidence_reason` when present). + - **Cost and quality rollups** — `DiffMetric` cards for cost/run (USD), latency avg (ms), error rate, each with baseline → candidate and delta. + - **`DiffPricingExpand`** — collapsible pricing & model section (collapsed on each new diff result). Shows baseline vs candidate `provider/version model` inline. Expands to reveal: provider/version skew warning, `pricing.warnings` list, `pricing.hints` list, pricing catalog detail (when enabled), and per-1k token prices (input/output, baseline → candidate) when all four rates are present and pricing changed. +5. **`DiffDecisionCard`** — summarizes the gate outcome in plain English and, when policy + passes and the candidate release ID is known, shows a **Continue to promote** link to + `#/actions` with `release_id`, `environment`, and `window` pre-filled. +6. **Raw diff JSON** panel (`JsonPanel`, collapsed by default). The **Compute diff** button is disabled while the request is in flight (`busy` state). Errors from the API are shown as an inline `fd-alert--error` element. @@ -235,6 +282,23 @@ Errors from the API are shown as an inline `fd-alert--error` element. Note: `POST /v1/diff` is a **read-only computation** and does not require a mutation token. See [http-api.md](http-api.md) for the full response schema. +### Diff component subtree + +``` +DiffPage +├── DiffVerdictStack (full-width verdict/block strip) +├── DiffReleaseTwin (baseline vs candidate identity, env, pricing line) +├── DiffPolicyPanel (policy badge + reasons) +├── DiffChangeImpact (samples, metric rollups, expandable pricing) +│ └── DiffPricingExpand (collapsed; shows per-1k prices, warnings, catalog) +├── DiffDecisionCard (verdict copy + "Continue to promote" link) +└── JsonPanel (raw diff JSON, collapsed by default) +``` + +Shared data extraction: `web/src/components/diff/diffPayload.tsx` exports typed helpers +(`pickPolicy`, `pickPricing`, `pricingLine`, `DiffMetric`) that isolate JSON traversal from +rendering. + --- ## `ActionsPage` (`web/src/pages/ActionsPage.tsx`) @@ -276,6 +340,27 @@ After a successful **promote** or **rollback** (or **confirm**): --- +## `urlSearch.ts` (`web/src/urlSearch.ts`) + +Helpers for hash-router deep-linking. Both `DiffPage`, `OverviewPage`, `RunsPage`, and +`ActionsPage` use these to read from and write to `URLSearchParams`: + +| Export | Description | +|--------|-------------| +| `pickTrimmedSearch(searchParams, key)` | Returns `searchParams.get(key)?.trim() ?? ""`. Never returns `null`. | +| `searchParamsFromRecord(rec)` | Builds a `?key=value` string from a `Record`, omitting entries with empty values. Returns `""` when all values are empty. | + +**Deep-link examples:** + +| Page | URL | Effect | +|------|-----|--------| +| Overview | `#/?release=rel_abc123` | Activates focused release hero | +| Diff | `#/diff?baseline=rel_a&candidate=rel_b&window=7d&environment=production` | Pre-fills the diff form | +| Runs | `#/runs?release_id=rel_abc&window=24h&environment=staging` | Pre-fills release and filters | +| Actions | `#/actions?release_id=rel_abc&environment=production&window=7d` | Pre-fills promote/rollback form | + +--- + ## `api.ts` (`web/src/api.ts`) Typed client helpers shared across pages. @@ -371,6 +456,29 @@ Calls `GET /v1/promotion-requests` with optional query parameters. Used by `Acti ## Shared components +### `CopyTextButton` (`web/src/components/CopyTextButton.tsx`) + +Inline button that copies a string to the clipboard. Uses `navigator.clipboard.writeText` +with an `execCommand` fallback for headless or insecure contexts (so Playwright E2E tests +also work). Status cycles through `idle → "Copied" → idle` (2 s) or `idle → "Failed" → +idle` (2.5 s). Props: + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `label` | string | — | Accessible label prefix (e.g. `"Release ID"`) | +| `value` | string | — | String to copy | +| `buttonText` | string | `"Copy"` | Visible button text when idle | +| `className` | string | `"fd-btn fd-btn--ghost fd-copy-btn"` | CSS class | +| `testId` | string | — | Optional `data-testid` for E2E | + +### `ReleaseLifecycleStrip` (`web/src/components/ReleaseLifecycleStrip.tsx`) + +Horizontal `