Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ This project follows [Semantic Versioning](https://semver.org/). From **v1.0.0**

- **Quickstart:** **`flightdeck-quickstart-verify`**, **`flightdeck.quickstart_smoke`**, **`scripts/quickstart_smoke.py`**; **CI** and **PyPI release** run **`uv run flightdeck-quickstart-verify`** (release: after schema drift check).
- **HTTP SDK:** **`FlightdeckClient`** / **`AsyncFlightdeckClient`** — optional **`api_token`**, **`health`**, **`list_releases`** / **`list_promoted`** / **`list_actions`**, **`post_diff`** / **`post_promote`** / **`post_rollback`**, plus ingest batching and retries.
- **Web + E2E:** React/Vite **`web/`**, committed **`src/flightdeck/server/static/`**, FastAPI **`/assets`**; Vite dev proxy **`/v1`** / **`/health`**, optional **`VITE_FLIGHTDECK_LOCAL_API_TOKEN`**; **`.gitattributes`** LF on **`static/`**; **`web/e2e/`**, **`web/playwright.config.ts`**, **`web/scripts/e2e-server.mjs`**, **`@playwright/test`**; **CI** / **PyPI release**: **`npm ci`**, **`npm run build`**, **`git diff --exit-code static/`**, **`npx playwright install chromium`**, **`npm run test:e2e`**.
- **Web + E2E:** React/Vite **`web/`**, committed **`src/flightdeck/server/static/`**, FastAPI **`/assets`**; Vite dev proxy **`/v1`** / **`/health`**, optional **`VITE_FLIGHTDECK_LOCAL_API_TOKEN`**; **`.gitattributes`** LF on **`static/`**; **`web/e2e/`**, **`web/playwright.config.ts`**, **`web/scripts/e2e-server.mjs`**, **`@playwright/test`**; **CI** / **PyPI release**: **`npm ci`**, **`npm run build`**, **`git diff --exit-code static/`**, **`npx playwright install chromium`**, **`npm run test:e2e`**. App shell uses **`HashRouter`** with three named pages: **Overview** (`/#/`), **Diff** (`/#/diff`), **Promote & rollback** (`/#/actions`). **`TimelineRefreshContext`** (`generation` + `notifyTimelineMutated`) propagates mutation signals from **`ActionsPage`** to **`OverviewPage`** for automatic table refresh without a full reload. Playwright smoke covers shell nav, hash routes, and `/health`.
- **Tests:** CLI contracts (**`release verify`**, **`diff`**, **`history`**, **`rollback`**); invalid JSON fixtures (**`PricingTable`**, **`RunEvent`**, **`ReleaseArtifact`**).
- **Tooling:** **`uv.lock`** / **`uv sync --frozen --extra dev`** / **`astral-sh/setup-uv`**; **`.github/workflows/release-pypi.yml`** (tag ↔ **`pyproject.toml`** / **`__init__.py`**, ruff, pytest, schemas, **`uv build`**, OIDC **PyPI**, GitHub Release); **`tests/test_version_consistency.py`**.

Expand Down
47 changes: 33 additions & 14 deletions web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ After any change under **`web/src/`** (or **`vite.config.ts`**, **`package.json`

**Vite** proxies **`/v1/*`** and **`/health`** to **`http://127.0.0.1:8765`** (override with **`VITE_DEV_PROXY_TARGET`** in **`.env.local`** or the environment). The React app calls relative **`/v1/...`** URLs so the browser talks to the Vite dev server only.

**Auth:** when the server has **`FLIGHTDECK_LOCAL_API_TOKEN`** set, set **`VITE_FLIGHTDECK_LOCAL_API_TOKEN`** in **`.env.local`** to the same value so promote/rollback requests include **`Authorization: Bearer `**.
**Auth:** when the server has **`FLIGHTDECK_LOCAL_API_TOKEN`** set, set **`VITE_FLIGHTDECK_LOCAL_API_TOKEN`** in **`.env.local`** to the same value so promote/rollback requests include **`Authorization: Bearer ...`**.

## Playwright E2E

Expand All @@ -46,27 +46,46 @@ npx playwright install chromium
npm run test:e2e
```

**`playwright.config.ts`** starts **`scripts/e2e-server.mjs`**: a fresh workspace under **`.tmp/playwright-fd-workspace/`**, then **`flightdeck serve`** on **`http://127.0.0.1:9876`**. On GitHub Actions the server uses **`uv run flightdeck `**; locally it uses **`python -m flightdeck.cli.main`** or **`py -3`**.
**`playwright.config.ts`** starts **`scripts/e2e-server.mjs`**: a fresh workspace under **`.tmp/playwright-fd-workspace/`**, then **`flightdeck serve`** on **`http://127.0.0.1:9876`**. On GitHub Actions the server uses **`uv run flightdeck ...`**; locally it uses **`python -m flightdeck.cli.main`** or **`py -3`**.

Run **`npm`** commands from this **`web/`** directory (repo root is one level up: **`cd web`**).

## PR split (subagent-friendly)
## App structure

**Already landed:** Vite + React + TS **`web/`**, committed **`static/`**, FastAPI **`/assets`** mount, CI **`npm run build`** + **`git diff --exit-code`** on **`static/`**, Playwright smoke, LF normalization via **`.gitattributes`** (stable **`git diff`** on Windows).
The UI is a React 19 + TypeScript single-page application using **`HashRouter`** from `react-router-dom`. All navigation uses hash-based URLs so the FastAPI static file handler only needs to serve `index.html` for every route.

**Suggested follow-ups:**
### Routing

1. **PR B — UI behavior**
Timeline UX (tables, loading states, `/v1/actions` query filters), mutation UX (inline errors, disable buttons while pending). Touch **`web/src/`** only, then **`npm run build`** and commit **`static/`**.
| URL hash | Component | Nav label |
|----------|-----------|-----------|
| `/#/` (default) | `OverviewPage` | Overview |
| `/#/diff` | `DiffPage` | Diff |
| `/#/actions` | `ActionsPage` | Promote |

*Subagent prompt:* “Improve **`web/src/App.tsx`** (and small new components under **`web/src/`**) for timeline and promote/rollback UX only; rebuild **`static/`**; do not change Python HTTP contracts.”
Any unrecognized hash redirects to `/#/`. **`AppShell`** (`web/src/components/AppShell.tsx`) renders the persistent header and `<nav>` links, wraps children in `TimelineRefreshProvider`, and provides the `<Outlet>` for nested routes.

2. **PR C — Optional**
React Router, richer diff visualization, shared design tokens. (**Playwright** smoke is under **`e2e/`**; see **Playwright E2E** above.)
### Pages

**Parallel subagents for PR B** (non-overlapping files if you split components first):
**`OverviewPage`** (`web/src/pages/OverviewPage.tsx`)
Read-only dashboard. On mount (and on every `generation` tick from `TimelineRefreshContext`) it calls `loadTimeline()` -- a parallel fetch of `/v1/releases`, `/v1/promoted`, and `/v1/actions` -- and renders three tables: Releases, Promoted, and Recent actions. A "Refresh" button triggers a manual reload. Errors surface as an inline alert; IDs are truncated to `first10...last6` with the full value in the `title` attribute.

- **Agent 1 — Read path:** `TimelinePanel` (or equivalent) + styles for releases/promoted/actions.
- **Agent 2 — Write path:** `DiffPanel` + `MutationPanel` + token-aware **`fetch`** helpers.
**`DiffPage`** (`web/src/pages/DiffPage.tsx`)
Form-driven diff computation. Submits `POST /v1/diff` with `baseline_release_id`, `candidate_release_id`, `window`, and `environment`. On success renders a metrics summary (cost per run, latency avg, error rate with delta vs baseline), a policy PASS/FAIL badge, sample counts, and a collapsible raw-JSON panel.

Rebase one branch onto the other, run **`npm run build` once**, fix any conflicts in **`static/`**, then push.
**`ActionsPage`** (`web/src/pages/ActionsPage.tsx`)
Promote and rollback form (nav label: **"Promote"**, page title: **"Promote & rollback"**, route: `/#/actions`). Submits `POST /v1/promote` or `POST /v1/rollback` with `release_id`, `environment`, `window`, `reason`, and `actor: "react-ui"`. Requires a non-empty reason; shows a browser confirmation dialog before sending. On success calls `notifyTimelineMutated()` so the Overview table refreshes. Buttons disable during in-flight requests.

### API helpers (`web/src/api.ts`)

- **`fetchJson<T>(path, init?)`** -- wrapper around `fetch` that injects `Authorization: Bearer <token>` when `VITE_FLIGHTDECK_LOCAL_API_TOKEN` is set, and throws a descriptive `Error` for non-2xx responses.
- **`loadTimeline()`** -- parallel-fetches `/v1/releases`, `/v1/promoted`, and `/v1/actions` and returns a combined `TimelinePayload`.
- Types: `ReleaseRow`, `PromotedRow`, `ActionRow`, `TimelinePayload`.

### Timeline refresh context (`web/src/context/TimelineRefreshContext.tsx`)

`TimelineRefreshProvider` holds a `generation` integer. Any component that mutates server state calls `notifyTimelineMutated()` to increment `generation`. `OverviewPage` includes `generation` as a `useEffect` dependency, so it automatically re-fetches when a mutation completes -- without a full page reload.

Flow:
1. Mutation succeeds in `ActionsPage` -- calls `notifyTimelineMutated()`
2. `generation` increments in `TimelineRefreshContext`
3. `OverviewPage`'s `useEffect([generation, refresh])` fires -- calls `loadTimeline()` -- tables update
Loading