Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +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`).
- **`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 Down
170 changes: 63 additions & 107 deletions README.md

Large diffs are not rendered by default.

11 changes: 9 additions & 2 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,14 @@ These map to **What is next** items **1**, **2**, and **5**; ship notes stay in

**Explicit UI deferrals**

Out of scope for the near-term web app: custom themes or theme marketplaces; embedded arbitrary log viewers; full observability or fleet consoles in the browser; multi-workspace UI (follows conditional **Fleet / cross-workspace** in **What is next**).
Out of scope for the near-term web app: arbitrary third-party themes or theme marketplaces; embedded arbitrary log viewers; full observability or fleet consoles in the browser; multi-workspace UI (follows conditional **Fleet / cross-workspace** in **What is next**). A **single built-in dark palette** (plus system preference) aligned with operator ergonomics and brand art is **not** a “custom theme product”—see **[docs/web-ui.md — Theming and brand alignment](docs/web-ui.md#theming-and-brand-alignment)** for the phased plan vs the marketing composite.

**Deferred until APIs or contracts exist (then revisit UX)**

- **Identity for HTTP and UI beyond shared-secret Bearer:** today **`FLIGHTDECK_LOCAL_API_TOKEN`** / **`VITE_FLIGHTDECK_LOCAL_API_TOKEN`** are an **operator-chosen static secret** for this server’s JSON API (and the bundled UI when configured). **OAuth2/OIDC**, per-user sessions, API key rotation, and enterprise SSO in front of **`flightdeck serve`** are **not** shipped in core; expect **reverse-proxy or gateway** patterns until a future design explicitly extends the trust model. Revisit when there is a concrete contract (token issuance, audience, rotation) that fits local-first operation.
- **Environment / promotion pipeline visualization** (for example DEV → STAGING → PROD lanes with per-stage policy state): today the ledger uses a single `environment` string and CLI/API fields—not a first-class multi-stage graph. Revisit when the server exposes enough structure to render without inventing state.
- **Dense “evidence-first release card”** on Diff (token deltas, tool lists, synthetic safety rows): ship only fields the diff and catalog payloads actually provide; expand the card when optional aggregates or provenance hooks land in the API.
- **README / social preview hero** (marketing composites, category positioning): tracked with docs and release comms, not as a substitute for honest in-app surfaces.

---

Expand All @@ -71,7 +78,7 @@ Gaps between “works locally” and “easy to use across production services.
| **Event pipeline** | Reliable `RunEvent` emission from app/agent runtimes. | Near term: reference integration examples; operator owns final runtime wiring. |
| **CI/GitOps flow** | Register → ingest → diff → gate → promote in pipelines. | Near term: maintained CI examples/templates. |
| **Deployment unit** | Repeatable `serve` packaging, health checks, process supervision. | Near term: container/compose guidance; still local-first by default. |
| **Identity and access** | Strong auth beyond loopback + optional bearer token. | Mid term: documented hardened patterns; first-class enterprise auth is a longer arc. |
| **Identity and access** | Strong auth beyond loopback + optional static Bearer (operator-chosen secret for HTTP API). | Mid term: documented proxy/gateway patterns; interactive OAuth/OIDC for the bundled UI is a **longer / conditional** arc (see **Deferred until APIs** above). |
| **Storage/availability** | Backup/restore, scaling, HA story. | Operator-owned today; improve docs and patterns. |
| **Observability integration** | Correlated telemetry export and operational visibility. | Mid term: OTLP-oriented integration paths (not an APM/dashboard product). |
| **Multi-workspace/fleet** | Cross-workspace views and policy coordination. | Long term and conditional; one workspace = one ledger today. |
Expand Down
6 changes: 6 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ See **[CONTRIBUTING.md](CONTRIBUTING.md)** for a pre-push checklist aligned with

## Local HTTP API (`flightdeck serve`)

### `FLIGHTDECK_LOCAL_API_TOKEN` — what it is (and is not)

- **You choose the value.** FlightDeck does **not** generate, mint, or rotate this string for you. It is a **shared secret** you set in the server environment (same idea as a static API key). A typical choice is a long random value (for example `openssl rand -hex 32` as in **[docs/http-api.md](docs/http-api.md)**).
- **What it gates:** access to this process’s **HTTP JSON API** (`GET /v1/*` when set, plus ledger writes and ingest) via the **`Authorization: Bearer …`** header. SDKs use **`api_token=`**; scripts and agents send the header explicitly; the **bundled React UI** uses **`VITE_FLIGHTDECK_LOCAL_API_TOKEN`** at build time (or **`web/.env.local`** under `npm run dev`) so the browser can send the **same** secret — see **[docs/web-ui.md](docs/web-ui.md)**.
- **What it is not:** end-user login, OAuth/OIDC, SSO, or per-person identity inside FlightDeck. Those are **not** part of today’s core product; the roadmap treats stronger identity as a longer arc (see **[ROADMAP.md](ROADMAP.md)**).

The bundled server is intended for **local development and demos**. **`POST /v1/promote`**, **`POST /v1/promote/request`**, **`POST /v1/promote/confirm`**, **`POST /v1/rollback`**, and **`POST /v1/events`** (run event ingest) share one **ledger-write** access model in server code: with no token configured, only **loopback** clients (`127.0.0.1`, `::1`, `localhost`, and the Starlette test client) may call them. If you set **`FLIGHTDECK_LOCAL_API_TOKEN`**, every such request must include **`Authorization: Bearer <that value>`**; use a strong random value and treat it like a local secret. Remote emitters (agents, sidecars) must use the Bearer path when the server listens beyond loopback.

**Human approval** (`promotion_requires_approval: true` in `flightdeck.yaml`) adds a **second actor step** before a promote is applied: **`POST /v1/promote/request`** creates a pending row; **`POST /v1/promote/confirm`** completes it. **Policy still runs on confirm** — approval is not a bypass; a request that fails policy remains blocked with the same HTTP **409** outcome as a direct promote.
Expand Down
2 changes: 2 additions & 0 deletions docs/http-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ first if it does not exist.

## Authentication and access control

**Credential model:** `FLIGHTDECK_LOCAL_API_TOKEN` is an **operator-chosen shared secret** for this server instance. FlightDeck does **not** issue it. It gates **HTTP access** to the local API (reads and/or writes per the table below). It is **not** OAuth, SSO, or per-user identity — see **[SECURITY.md](../SECURITY.md#local-http-api-flightdeck-serve)** and **[ROADMAP.md](../ROADMAP.md)** for scope and future directions.

Two access tiers:

| Route | No token configured | `FLIGHTDECK_LOCAL_API_TOKEN` set |
Expand Down
Binary file added docs/images/flightdeck-overview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
63 changes: 50 additions & 13 deletions docs/web-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,54 @@ For setup, dev workflow, build commands, and Playwright E2E instructions see

---

## Theming and brand alignment

### Desired state (disclaimer)

The **[README product overview](../README.md#product-overview)** image is a **marketing composite**: dark chrome, dense “dashboard” cards, and narrative labels that **do not** map one-to-one to shipped pages. The bundled **hex mark** (`web/public/flightdeck-icon.png`) matches that art direction (cyan–purple accent, dark ground). **This document and the operator UI** stay grounded in real **`/v1/*`** data—visual work should **not** invent panels (for example a synthetic “release blocked” hero) until the APIs and product decisions exist.

### What we can borrow from the art (incremental)

| 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`**). |
| 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. |
| Geometric sans | **Shipped:** offline **system UI stack** in `index.css` (`--fd-font`). Optional: install **Inter** locally if you want that face without bundling remote CSS. |

### Phased implementation plan

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`).
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).

### Explicit deferrals (still)

- **Multi-theme marketplaces**, per-user arbitrary color pickers, or third-party skin systems — off mission.
- **Infographic-only widgets** (staged DEV→STAGING→PROD pipeline strip, sparkline grids) — wait for real APIs and **[ROADMAP](ROADMAP.md)** operator outcomes, not decorative parity with the poster.

---

## Routing

The app uses **HashRouter** (`react-router-dom`) so all navigation stays within the single
`index.html` that FastAPI's static file mount serves. URLs look like
`http://127.0.0.1:8765/#/diff`. No server-side route matching is required.

**Static UI assets:** hashed bundles are mounted at **`/assets/`**. The sidebar mark and tab icons use the **bundled** URL from `web/src/assets/flightdeck-icon.png` (emitted as **`/assets/flightdeck-icon-<hash>.png`**; `main.tsx` sets `<link rel="icon">` at runtime). A **stable** duplicate remains at **`GET /flightdeck-icon.png`** (from `web/public/` at build time + FastAPI `FileResponse`) for bookmarks, probes, and **`web/e2e/smoke.spec.ts`**.

**Typography:** the UI uses an **offline-first system font stack** (no Google Fonts or other remote CSS). Install **Inter** locally if you want that face in dev tools without changing the bundle.

| 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 |
| `#/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 |
| `#/*` (any other) | — | Redirects to `#/` | |

Expand All @@ -36,25 +73,25 @@ promote/rollback capability should be unavailable regardless of network placemen
## Component tree

```
App (HashRouter)
└── AppShell (layout: left sidebar + main column)
└── TimelineRefreshProvider (context)
└── div.fd-shell
├── aside.fd-sidebar (brand + primary nav)
└── div.fd-shell__content
├── SecurityStatusBar
└── main#main-content → OverviewPage | DiffPage | RunsPage | ActionsPage
ThemePreferenceProvider (`App.tsx`)
└── HashRouter
└── Routes / AppShell layout route
└── TimelineRefreshProvider
└── div.fd-shell
├── aside.fd-sidebar (brand, collapse chevron, primary nav, footer nav → Settings)
└── div.fd-shell__content
├── SecurityStatusBar
└── main#main-content → OverviewPage | DiffPage | RunsPage | ActionsPage | SettingsPage
```

---

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

Renders a fixed-width **left sidebar** (`aside.fd-sidebar`) with brand and vertical primary
nav (Langfuse-style rail), 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** nav pinned to the bottom of the rail with **Settings** → `#/settings`. 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. Wraps the subtree in `TimelineRefreshProvider`
so any descendant can access the refresh context.
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.

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 Expand Up @@ -233,7 +270,7 @@ After a successful **promote** or **rollback** (or **confirm**):
2. Outcome card shows policy badge, pointer badge, metric grid, and reasons list when applicable.
3. `notifyTimelineMutated()` runs so `OverviewPage` refetches.

**Auth:** `VITE_FLIGHTDECK_LOCAL_API_TOKEN` is sent on every `fetchJson` call, including **`POST /v1/promote/request`** and **`POST /v1/promote/confirm`**. See [http-api.md § Authentication](http-api.md#authentication-and-access-control).
**Auth:** `VITE_FLIGHTDECK_LOCAL_API_TOKEN` is sent on every `fetchJson` call, including **`POST /v1/promote/request`** and **`POST /v1/promote/confirm`**. It must **match** `FLIGHTDECK_LOCAL_API_TOKEN` on the server when the server enforces Bearer (see [http-api.md § Authentication](http-api.md#authentication-and-access-control)). FlightDeck does **not** mint this value: the operator chooses a shared secret for **HTTP API** access. It is baked in at **build time** for the committed static bundle; local Bearer testing normally uses **`web/.env.local`** + **`npm run dev`**. It is **not** OAuth or end-user SSO — see [SECURITY.md](../SECURITY.md) (**Local HTTP API**).

**HTTP errors:** `fetchJson` formats FastAPI **`detail`** strings, validation arrays, and `{ message: … }` objects into a single `Error` message for the alert line.

Expand Down
10 changes: 9 additions & 1 deletion src/flightdeck/server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import os
from pathlib import Path

from fastapi import FastAPI, Request
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles

Expand Down Expand Up @@ -52,4 +52,12 @@ def health(request: Request) -> dict[str, str]:
def ui_index() -> FileResponse:
return FileResponse(static_dir / "index.html")

@app.get("/flightdeck-icon.png")
def ui_app_icon() -> FileResponse:
"""Shipped UI favicon / sidebar mark (copied from ``web/public`` at build time)."""
path = static_dir / "flightdeck-icon.png"
if not path.is_file():
raise HTTPException(status_code=404, detail="UI icon not found (rebuild web bundle)")
return FileResponse(path, media_type="image/png")

return app
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 0 additions & 11 deletions src/flightdeck/server/static/assets/index-BMbVZO_a.js

This file was deleted.

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

Large diffs are not rendered by default.

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

This file was deleted.

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

Large diffs are not rendered by default.

Binary file added src/flightdeck/server/static/flightdeck-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions src/flightdeck/server/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
content="FlightDeck web UI: release diffs, run evidence, policy gates, and promote or rollback actions against a local flightdeck serve instance."
/>
<title>FlightDeck</title>
<script type="module" crossorigin src="/assets/index-BMbVZO_a.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Cg8b3N1D.css">
<script type="module" crossorigin src="/assets/index-BPDMrxvX.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Dr1ovfXv.css">
</head>
<body>
<div id="root"></div>
Expand Down
7 changes: 7 additions & 0 deletions tests/test_server_health.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ def test_health_includes_mutation_auth_loopback_when_no_token(monkeypatch: pytes
assert r.json() == {"status": "ok", "mutation_auth": "loopback", "read_auth": "open"}


def test_ui_icon_png_served() -> None:
with TestClient(create_app()) as client:
r = client.get("/flightdeck-icon.png")
assert r.status_code == 200
assert r.headers.get("content-type", "").startswith("image/png")


def test_health_includes_mutation_auth_bearer_when_token_set(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("FLIGHTDECK_LOCAL_API_TOKEN", "test-secret-token")
with TestClient(create_app()) as client:
Expand Down
Loading
Loading