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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This project follows [Semantic Versioning](https://semver.org/). From **v1.0.0**

### Added

- **Web UI (`flightdeck serve`):** **`/#/settings`** for appearance (Light / Dark / System, **`flightdeck-theme`**); collapsible sidebar (**`flightdeck-sidebar-collapsed`**); **offline system font stack** (no remote font CSS); sidebar + favicon use **bundled** **`/assets/flightdeck-icon-*.png`** with stable **`GET /flightdeck-icon.png`** fallback; **`html[data-theme="dark"]`** tokens and Playwright **`web/e2e/`** (`smoke` icon checks, `theme.spec.ts`, `sidebar.spec.ts`).
- **Web UI (`flightdeck serve`):** **Theme** (Light / Dark / System icon radios, **`flightdeck-theme`**) in the **sidebar Settings** popover; legacy **`/#/settings`** redirects to **`#/`**; collapsible sidebar (**`flightdeck-sidebar-collapsed`**); **offline system font stack** (no remote font CSS); sidebar + favicon use **bundled** **`/assets/flightdeck-icon-*.png`** with stable **`GET /flightdeck-icon.png`** fallback; **`html[data-theme="dark"]`** tokens and Playwright **`web/e2e/`** (`smoke` icon checks, `theme.spec.ts`, `sidebar.spec.ts`).
- **`flightdeck pricing check`** — reports **`flightdeck-bundled-*`** snapshot age vs **`--max-age-days`** (default **90**); **`--fail`** for CI. **`release diff`** / **`POST /v1/diff`** append **`pricing.warnings`** when bundled snapshots exceed the same age threshold.
- **`flightdeck.integrations.telemetry.configure_otel_tracing()`** — optional OTLP HTTP **`TracerProvider`** wiring when the **`telemetry`** extra is installed (see **`docs/sdk-integrations.md`**).
- **SDK:** **`flightdeck.sdk.http_common`** shared serializers and retry policy; parity tests keep sync/async clients aligned. **`pytest-cov`** no longer omits **`sdk/client.py`**.
Expand All @@ -17,6 +17,7 @@ This project follows [Semantic Versioning](https://semver.org/). From **v1.0.0**

- **`[project.optional-dependencies] dev`:** **`ruff`** is **`>=0.15,<0.16`** (was an exact patch pin) so **`pip install`** / shared venvs can resolve alongside other tools; **`uv sync --frozen`** still follows **`uv.lock`**. **`docs/troubleshooting.md`** notes checking **`uv.lock`** for the resolved **`0.15.x`** wheel.
- **Docs / positioning:** README local-first and ICP copy; bundled pricing cadence, vendor pricing URLs in YAML comments, and **`docs/pricing-catalog.md`** / **ROADMAP** / **RELEASE_NOTES** staleness commitments.
- **Web UI (`flightdeck serve`):** Sidebar **Settings** popover shows a **Settings** heading and **Theme** as sun / moon / monitor icon radios (replacing text-only appearance controls in that surface).

## 1.2.0 - 2026-05-03

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions artifacts/flightdeck-demo-share/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Demo capture (UI marketing assets)

Generated with:

```bash
# from repo root, after: uv sync --frozen --extra dev
node web/scripts/capture-demo-artifacts.mjs
```

- **flightdeck-ui-demo.mp4** — short screen walkthrough (H.264, LinkedIn-friendly).
- **flightdeck-ui-demo.webm** — same session, WebM source.
- **01-overview-loopback-chips.png** — Overview with security strip (loopback).
- **02-overview-ledger-metrics.png** — Overview with ledger metrics expanded.
- **03-diff-compute.png** — Run diff page.
- **04-runs-query.png** — Run events page.
- **05-actions-promote.png** — Promote & rollback page.
- **06-settings-dark-theme.png** — Sidebar Settings popover → Theme icons (dark selected).

Safe to delete this directory before a squash if you do not want binaries in history.
Binary file not shown.
Binary file not shown.
12 changes: 6 additions & 6 deletions docs/web-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ The **[README product overview](../README.md#product-overview)** image is a **ma

| Art direction | Application in this repo |
|---------------|---------------------------|
| Dark navy / near-black shell | **`html[data-theme="dark"]`** in `web/src/index.css` mirrors semantic tokens; **Appearance** control in the sidebar defaults to **Light** (stored under **`localStorage`** key **`flightdeck-theme`**). |
| Dark navy / near-black shell | **`html[data-theme="dark"]`** in `web/src/index.css` mirrors semantic tokens; **Theme** (sun / moon / monitor icons) in the sidebar **Settings** popover defaults to **Light** (stored under **`localStorage`** key **`flightdeck-theme`**). |
| Cyan → purple gradient | CSS variables (for example `--fd-accent-gradient`) for **active nav**, **primary buttons**, and **focus-visible** accents—used sparingly so trust/safety UI stays calm. |
| High-contrast titles | Tune `--fd-type-*` and weights under dark mode; avoid shrinking body text for density. |
| “Neon” feel | Reserve for **interactive** states, not large background fills. |
Expand All @@ -30,7 +30,7 @@ The **[README product overview](../README.md#product-overview)** image is a **ma

1. **Token foundation** — Extend `:root` with any missing semantics (`--fd-surface-elevated`, gradient stops, optional `--fd-bg-subtle`). Replace scattered literals in `web/src/index.css` (for example warning callout backgrounds) with variables so dark mode does not require hunting hex values.
2. **`[data-theme="dark"]` block** — Mirror every semantic token used by `.fd-shell`, sidebar, cards, tables, `Badge`, drawers, and `JsonPanel`; set `color-scheme: dark` on `html` when active. Validate **WCAG AA** for body text and links.
3. **Preference UI** — **`/#/settings`** (and room for more prefs later): **Light** / **Dark** / **System**; listen to `prefers-color-scheme` when System is selected. Persist `localStorage` key **`flightdeck-theme`** (`light` \| `dark` \| `system`).
3. **Preference UI** — **Sidebar Settings** popover (and room for more prefs later): **Theme** as sun / moon / monitor icon radios for **Light** / **Dark** / **System**; listen to `prefers-color-scheme` when System is selected. Persist `localStorage` key **`flightdeck-theme`** (`light` \| `dark` \| `system`). Legacy **`/#/settings`** redirects to **`#/`**.
4. **Brand accents** — Apply the gradient token to **active** `.fd-nav__link--active` (left rail) and primary submit-style buttons; keep destructive actions on existing red semantics.
5. **Light theme polish** — Even before dark ships: align spacing rhythm and card shadows with the same tokens so both themes stay maintainable.
6. **Verification** — From `web/`: **`npm ci`**, **`npm run build`**, commit **`src/flightdeck/server/static/`**; **`npm run test:e2e`** (includes **`e2e/theme.spec.ts`**: default light, dark persistence, system / `prefers-color-scheme`, overview smoke in dark). Manually smoke **Diff** and **Actions** in both themes (policy panels, JSON drawer, rollback affordances).
Expand All @@ -57,7 +57,7 @@ The app uses **HashRouter** (`react-router-dom`) so all navigation stays within
| `#/` | `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 |
| `#/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. |
| `#/settings` | *(redirect)* | — | Redirects to **`#/`**; **Theme** (Light / Dark / System) lives in the **sidebar Settings** dialog (`SidebarSettingsMenu` + icon `ThemeToggle`). |
| `#/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 |
| `#/*` (any other) | — | Redirects to `#/` | |

Expand All @@ -81,17 +81,17 @@ 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 | DiffPage | RunsPage | ActionsPage
```

---

## `AppShell` (`web/src/components/AppShell.tsx`)

Renders a fixed-width **left sidebar** (`aside.fd-sidebar`) with brand (gradient **FlightDeck** wordmark, mark in a **raised tile**), a **collapse** control (SVG chevrons, `localStorage` **`flightdeck-sidebar-collapsed`**), a **primary** nav (inline SVG icons + labels; icon-only when collapsed), and a **footer** nav pinned to the bottom of the rail with **Settings** `#/settings`. Then a **`fd-shell__content`** column with `SecurityStatusBar` and
Renders a fixed-width **left sidebar** (`aside.fd-sidebar`) with brand (gradient **FlightDeck** wordmark, mark in a **raised tile**), a **collapse** control (SVG chevrons, `localStorage` **`flightdeck-sidebar-collapsed`**), a **primary** nav (inline SVG icons + labels; icon-only when collapsed), and a **footer** **Settings** control that opens a compact **Settings** popover (portal, **Theme** row with sun / moon / monitor icon radios). Legacy `#/settings` redirects to `#/`. Then a **`fd-shell__content`** column with `SecurityStatusBar` and
`<main>` wrapping an `<Outlet>` for the active page. On narrow viewports the sidebar stacks
above the content with a horizontal nav row; a **collapsed** rail is expanded back to full labels in that breakpoint. Wraps the subtree in `TimelineRefreshProvider`
so any descendant can access the refresh context. `ThemePreferenceProvider` (from `App.tsx`) wraps the router so `ThemeToggle` on **Settings** can read and update **`flightdeck-theme`**; `main.tsx` applies the effective theme before the first paint to avoid a flash of the wrong scheme.
so any descendant can access the refresh context. `ThemePreferenceProvider` (from `App.tsx`) wraps the router so `ThemeToggle` in the popover can read and update **`flightdeck-theme`**; `main.tsx` applies the effective theme before the first paint to avoid a flash of the wrong scheme.

A **Skip to main content** link (class `fd-skip-link`) appears first in the shell; it uses
`preventDefault` + `focus()` on `#main-content` so **HashRouter** hash URLs (`#/…`) are not
Expand Down
11 changes: 0 additions & 11 deletions src/flightdeck/server/static/assets/index-Cmx_W8JU.js

This file was deleted.

1 change: 1 addition & 0 deletions src/flightdeck/server/static/assets/index-DUVo1ca_.css

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion src/flightdeck/server/static/assets/index-DrCTr-qj.css

This file was deleted.

11 changes: 11 additions & 0 deletions src/flightdeck/server/static/assets/index-Fsxp868o.js

Large diffs are not rendered by default.

11 changes: 9 additions & 2 deletions src/flightdeck/server/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,16 @@
name="description"
content="FlightDeck web UI: release diffs, run evidence, policy gates, and promote or rollback actions against a local flightdeck serve instance."
/>
<meta name="theme-color" content="#f3f4f6" />
<meta property="og:title" content="FlightDeck" />
<meta
property="og:description"
content="Release safety ledger: compare releases, review runtime evidence, enforce policy gates, and promote with confidence."
/>
<meta property="og:type" content="website" />
<title>FlightDeck</title>
<script type="module" crossorigin src="/assets/index-Cmx_W8JU.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DrCTr-qj.css">
<script type="module" crossorigin src="/assets/index-Fsxp868o.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DUVo1ca_.css">
</head>
<body>
<div id="root"></div>
Expand Down
28 changes: 28 additions & 0 deletions web/e2e/security-strip.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { expect, test } from "@playwright/test";

test.describe("security strip", () => {
test("scoped strip shows loopback chips on real /health", async ({ page }) => {
await page.goto("/");
const strip = page.getByTestId("security-strip");
await expect(strip).toContainText("Loopback open");
await expect(strip).toContainText("Writes");
});

test("bearer /health shows chip copy and mismatch hint when UI token unset", async ({ page }) => {
await page.route("**/health", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
status: "ok",
mutation_auth: "bearer",
read_auth: "bearer",
}),
});
});
await page.goto("/");
const strip = page.getByTestId("security-strip");
await expect(strip).toContainText("Bearer required");
await expect(strip).toContainText("VITE_FLIGHTDECK_LOCAL_API_TOKEN");
});
});
23 changes: 20 additions & 3 deletions web/e2e/smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,31 @@ test("home loads FlightDeck shell and overview tables", async ({ page }) => {
await expect(page.getByText("No releases yet.")).toBeVisible();
});

test("hash routes reach diff, runs, settings, and promote pages", async ({ page }) => {
test("document title reflects current route", async ({ page }) => {
await page.goto("/");
await expect(page).toHaveTitle(/Overview · FlightDeck$/);
await page.goto("/#/diff");
await expect(page).toHaveTitle(/Run diff · FlightDeck$/);
await page.goto("/#/runs");
await expect(page).toHaveTitle(/Run events · FlightDeck$/);
await page.goto("/#/settings");
await expect(page).toHaveTitle(/Overview · FlightDeck$/);
await page.goto("/#/actions");
await expect(page).toHaveTitle(/Promote & rollback · FlightDeck$/);
});

test("hash routes reach diff, runs, settings redirect, and promote pages", async ({ page }) => {
await page.goto("/#/diff");
await expect(page.getByRole("heading", { name: "Run diff", level: 2 })).toBeVisible();
await expect(page.getByRole("region", { name: "Diff help" })).toBeVisible();
await page.goto("/#/runs");
await expect(page.getByRole("heading", { name: "Run events", level: 2 })).toBeVisible();
await page.goto("/#/settings");
await expect(page.getByRole("heading", { name: "Settings", level: 2 })).toBeVisible();
await expect(page).toHaveURL(/#\/?$/);
await expect(page.getByRole("heading", { name: "Overview", level: 2 })).toBeVisible();
await page.getByTestId("sidebar-settings-trigger").click();
await expect(page.getByRole("dialog", { name: "Settings" })).toBeVisible();
await page.keyboard.press("Escape");
await page.goto("/#/actions");
await expect(page.getByRole("heading", { name: "Promote & rollback", level: 2 })).toBeVisible();
});
Expand Down Expand Up @@ -99,5 +116,5 @@ test("stable root icon URL for favicon crawlers", async ({ request }) => {

test("security status reflects server loopback mode", async ({ page }) => {
await page.goto("/");
await expect(page.getByRole("status")).toContainText("loopback");
await expect(page.getByTestId("security-strip")).toContainText("Loopback open");
});
25 changes: 18 additions & 7 deletions web/e2e/theme.spec.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,41 @@
import { expect, test } from "@playwright/test";
import { expect, test, type Page } from "@playwright/test";

const settingsUrl = "/#/settings";
async function openSettingsMenu(page: Page) {
await page.goto("/");
await page.getByTestId("sidebar-settings-trigger").click();
await expect(page.getByRole("dialog", { name: "Settings" })).toBeVisible();
}

test.describe("appearance / theme", () => {
test.beforeEach(async ({ page }) => {
await page.goto(settingsUrl);
await page.goto("/");
await page.evaluate(() => localStorage.removeItem("flightdeck-theme"));
await page.reload();
});

test("defaults to light and shows Appearance controls on Settings", async ({ page }) => {
test("defaults to light and shows Theme icon controls in sidebar menu", async ({ page }) => {
await expect(page.locator("html")).toHaveAttribute("data-theme", "light");
await expect(page.getByRole("heading", { name: "Settings", level: 2 })).toBeVisible();
await expect(page.getByRole("group", { name: "Appearance" })).toBeVisible();
await expect(page.locator('meta[name="theme-color"]')).toHaveAttribute("content", "#f3f4f6");
await openSettingsMenu(page);
await expect(page.getByRole("radiogroup", { name: "Theme" })).toBeVisible();
await expect(page.getByRole("radio", { name: "Light" })).toBeChecked();
});

test("dark mode sets data-theme and persists", async ({ page }) => {
await openSettingsMenu(page);
await page.getByRole("radio", { name: "Dark" }).check();
await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
await expect(page.locator('meta[name="theme-color"]')).toHaveAttribute("content", "#0c0f14");
await expect(page.getByRole("radio", { name: "Dark" })).toBeChecked();

await page.reload();
await openSettingsMenu(page);
await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
await expect(page.getByRole("radio", { name: "Dark" })).toBeChecked();
});

test("system mode follows prefers-color-scheme", async ({ page }) => {
await openSettingsMenu(page);
await page.getByRole("radio", { name: "System" }).check();
await page.emulateMedia({ colorScheme: "dark" });
await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
Expand All @@ -36,9 +45,11 @@ test.describe("appearance / theme", () => {
});

test("dark theme keeps shell and overview readable", async ({ page }) => {
await openSettingsMenu(page);
await page.getByRole("radio", { name: "Dark" }).check();
await expect(page.getByRole("heading", { name: "FlightDeck", level: 1 })).toBeVisible();
await page.goto("/");
await page.keyboard.press("Escape");
await expect(page.getByRole("dialog", { name: "Settings" })).not.toBeVisible();
await expect(page.getByRole("heading", { name: "Overview", level: 2 })).toBeVisible();
await expect(page.getByTestId("ledger-metrics-toggle")).toBeVisible({ timeout: 30_000 });
await page.getByTestId("ledger-metrics-toggle").click();
Expand Down
7 changes: 7 additions & 0 deletions web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@
name="description"
content="FlightDeck web UI: release diffs, run evidence, policy gates, and promote or rollback actions against a local flightdeck serve instance."
/>
<meta name="theme-color" content="#f3f4f6" />
<meta property="og:title" content="FlightDeck" />
<meta
property="og:description"
content="Release safety ledger: compare releases, review runtime evidence, enforce policy gates, and promote with confidence."
/>
<meta property="og:type" content="website" />
<title>FlightDeck</title>
</head>
<body>
Expand Down
Loading
Loading