---
name: iterate-ops-webapp
description: Run the Airbyte Ops Webapp locally and drive it to produce visual evidence of a UI change. Use when iterating on the airbyte-ops-webapp UI and you need to verify or screenshot a change — especially state-gated UI hidden behind login or context resolution, where a naive screenshot shows nothing.
devin-enabled: true
icon: app-window
---

# Iterate on the Ops Webapp

Launch the `airbyte-ops-webapp` locally and drive it with a headless browser to
produce evidence that a UI change actually renders. This webapp is **not** a
plain server-rendered site, and a naive "screenshot the URL" approach will lie
to you — read the gotchas before driving.

## TL;DR

```bash
cd ~/repos/airbyte-ops-mcp
poe webapp-serve-mock        # safe: no creds, no production calls, demo data
# (real mode, loads creds from .env files, for OAuth/Config-API paths)
# poe webapp-serve
```

Serves at http://localhost:3000. Then drive it with the bundled
[`screenshot.uv`](screenshot.uv) (adapt per task — it is a starting point).

## How this app actually works (and why naive screenshots fail)

- **Pages are client-rendered by the FastMCP app bridge.** `curl` and a raw page
  load only return the host shell. You MUST drive a real browser and wait for the
  client to paint.
- **The app bridge holds an open websocket.** Playwright `wait_until="networkidle"`
  **never settles** and times out. Use `wait_until="domcontentloaded"` and then
  wait for a *specific element/text* you expect (`get_by_text(...).wait_for()`),
  not a blind `sleep`.
- **Most interesting UI is state-gated** — it only renders under runtime state:
  - The header **login status** (lock icon + email) only appears when
    `oauth_authenticated` — i.e. after a real OAuth login.
  - The **resolved-context** indicator only appears after a Context GUID resolves.
  - A cold page load shows neither. You must either *trigger the state* or use the
    marker check below.

## Two strategies for evidence

### 1. Marker check — deterministic, no login or flow needed (prefer this first)

Prefab ships **both** branches of every `If(...)` to the browser as a template,
so a state-gated component is present in the served payload even when it isn't
visible. To prove a change exists, scan the served component tree for a marker
(a lucide icon name like `lock`, an emoji like `✅`, or literal text):

```bash
uv run --with playwright .agents/skills/iterate-ops-webapp/screenshot.uv \
  --expect "lock" --expect "Log out" /home
uv run --with playwright .agents/skills/iterate-ops-webapp/screenshot.uv \
  --expect "✅" --expect "Resolved context" /connector_versions
```

This confirms your edit landed in the rendered UI without fighting auth or
multi-step flows. It does NOT prove pixels — pair with strategy 2 when the user
wants to *see* it.

### 2. Live screenshot — pixel proof by triggering the state

Run in **mock mode** so the flows work against demo data with no production
calls, then drive the flow and screenshot. See driving recipes below.

## Launch reference

| Task | Mode | Notes |
|---|---|---|
| `poe webapp-serve-mock` | mock | No creds loaded; apply is a no-op; demo data. Use for safe UI iteration. |
| `poe webapp-serve` | real | Loads root `.env` (Cloud creds) + `airbyte-ops-webapp/.env` (OAuth creds). |
| `poe webapp-env-check` | — | Prints whether creds loaded (set/unset, never values). |

- Port: `3000` (override with `PORT=...`). Architecture: a FastMCP backend
  subprocess (port 8000) + a uvicorn host on `PORT`.
- `serve.py` does **not** read any `.env` itself — the poe tasks inject the env.
- Routes: `/` → 307 `/home`; pages at `/home`, `/authorization`,
  `/connector_versions`, `/customer_billing`. `/login` 302-redirects to
  `/authorization` for backward compatibility.
- A benign `VIRTUAL_ENV does not match` warning prints on serve; uv ignores it.

## Driving recipes

### Surface the resolved-context ✅ (mock mode, no login)

1. Open `/connector_versions`.
2. In the combobox **"Search connector name, ID, or Docker repo"**, click it and
   pick **source-github** (or type `github` to filter).
3. A two-column layout appears: **Version List** (left) + **Connector Overview** (right).
4. Scroll down to the **Context** card, type any value in the **Context GUID** input
   (placeholder *"Context GUID: accepts Organization, Workspace, or Actor IDs"*).
   Mock resolutions: `actor_example` → "Actor", `workspace_example` → "Workspace",
   anything else → "Organization".
5. Click **Refresh context**.
6. The **Pin status → Resolved context** row renders the `✅` indicator.

### Open the Pin Modal (mock mode, no login)

1. After selecting a connector (steps 1-2 above), scroll to the bottom.
2. Click the **Set Pin** button.
3. The **Set Connector Pin** modal opens with:
   - Connector info (name + docker repo)
   - Scope section (Context GUID, Scope Type dropdown, Scope ID)
   - Justification section (Action dropdown, Version, reason textarea, reference URL, approval URL)
   - Options section (Admin user dropdown, Force checkbox)
   - **Apply Pin Change** button (no stage preview step)
4. The modal uses a `Dialog` component — first child is the trigger button, rest is the body.

### Authenticate via Okta push (real mode, requires human approval)

Login uses Keycloak → Okta. It cannot be faked in mock mode (mock loads no creds).
The service account `airbyte-support-bot@airbyte.io` is available for agent
testing — a human (currently AJ Steers) approves the Okta Verify push
notification on their device.

**Prerequisites:**

- Two OAuth secrets are needed for full real-mode testing. Retrieve each via the
  on-demand secret workflow using the `airbyte-ops-mcp` MCP server tools (**not**
  Devin's built-in `request_secret`):
  1. `list_devin_secrets` — confirm the vault item exists.
  2. `request_devin_secret` with `target_approver="aaronsteers"` — triggers
     Slack approval.
  3. After approval, call `request_devin_secret` again with the
     `approval_evidence_url` from the Slack notification.
  4. Open the resulting 1Password share link in Chrome, reveal/copy the
     `client_secret` field, and export before starting the server.

  | Vault item | Env var | Purpose |
  |---|---|---|
  | `AIRBYTE_OPS_WEBAPP_AIRBYTE_OAUTH_CLIENT` | `AIRBYTE_OPS_WEBAPP_OAUTH_CLIENT_SECRET` | Airbyte (Okta/Keycloak) login |
  | `AIRBYTE_OPS_WEBAPP_GOOGLE_OAUTH_CLIENT` | `AIRBYTE_OPS_WEBAPP_GOOGLE_CLIENT_SECRET` | Google OAuth (BigQuery access) |

  ```bash
  export AIRBYTE_OPS_WEBAPP_OAUTH_CLIENT_SECRET='<airbyte_secret>'
  export AIRBYTE_OPS_WEBAPP_GOOGLE_CLIENT_SECRET='<google_secret>'
  ```
- The webapp must be running in **real mode** with the secrets set:
  ```bash
  AIRBYTE_OPS_WEBAPP_OAUTH_CLIENT_SECRET="$AIRBYTE_OPS_WEBAPP_OAUTH_CLIENT_SECRET" \
  AIRBYTE_OPS_WEBAPP_GOOGLE_CLIENT_SECRET="$AIRBYTE_OPS_WEBAPP_GOOGLE_CLIENT_SECRET" \
  poe webapp-serve
  ```
- Use the **computer tool / GUI browser** (Chrome on the desktop), not a headless
  Playwright session — Okta FastPass needs "Local network access" which headless
  and embedded webviews cannot grant.

**Step-by-step flow:**

1. Open `http://localhost:3000/authorization` in Chrome on the desktop.
2. Click **"Log in with Airbyte"**. This redirects to Keycloak
   (`cloud.airbyte.com/auth/…`), which redirects to Okta.
3. On the Okta sign-in page, enter `airbyte-support-bot@airbyte.io` as the
   username and click **Next**.
4. Okta sends a **push notification** to the approver's device. Message the user
   (via `message_user`) that you need them to approve the Okta push now.
5. **Wait** for the human to approve. The browser will automatically redirect
   back through Keycloak → `localhost:3000/oauth/callback` → the app.
6. Verify login succeeded: the header should show a lock icon, the email
   `airbyte-support-bot@airbyte.io`, and a **Log out** button.
7. The OAuth bearer token is now hydrated in the session. All Config API calls
   (context resolution, pin operations) use this token automatically.

**Tips:**

- If Okta shows a password field instead of a push, the account may need
  re-enrollment — escalate to the approver.
- The OAuth session cookie persists across page navigations within the same
  browser profile. You do not need to re-authenticate for each page.
- If the callback fails (e.g. port mismatch), check that `PORT=3000` and no
  `AIRBYTE_OPS_WEBAPP_OAUTH_REDIRECT_URI` override is set.

### Authentication secrets reference

Both OAuth secrets are documented in `AGENTS.md` under "Ops Webapp
Authentication". See that section for the env var names, vault item names, and
retrieval workflow.

## The supporting script

[`screenshot.uv`](screenshot.uv) is a PEP-723 standalone driver — a **starting
point to adapt**, not a universal tool. It already handles the websocket gotcha
(`domcontentloaded` + `--wait-for TEXT`), captures console errors, and supports
`--expect MARKER` for the marker check. For a real interaction flow, copy it and
add the `fill`/`click`/`wait_for` steps for your path. Prereq once:

```bash
uvx --from playwright playwright install chromium
```

## Reference

- App entrypoint / routes: `airbyte-ops-webapp/airbyte_ops_webapp/serve.py`
- Header login status component: `airbyte-ops-webapp/airbyte_ops_webapp/pages/shared_components/auth.py`
- CVM page layout (orchestration only): `airbyte-ops-webapp/airbyte_ops_webapp/pages/connector_version_manager/page.py`
- CVM tool definitions: `airbyte-ops-webapp/airbyte_ops_webapp/pages/connector_version_manager/tools.py`
- CVM shared helpers: `airbyte-ops-webapp/airbyte_ops_webapp/pages/connector_version_manager/helpers.py`
- Connector selector tabs: `airbyte-ops-webapp/airbyte_ops_webapp/pages/connector_version_manager/connector_selector.py`
- Connector overview (right pane): `airbyte-ops-webapp/airbyte_ops_webapp/pages/connector_version_manager/connector_overview.py`
- Version list (left pane): `airbyte-ops-webapp/airbyte_ops_webapp/pages/connector_version_manager/version_list.py`
- Pin modal dialog: `airbyte-ops-webapp/airbyte_ops_webapp/pages/connector_version_manager/pin_modal.py`
- Mock/demo adapter: `airbyte-ops-webapp/airbyte_ops_webapp/services/connector_version_manager/demo_mode/mock_adapter.py`
- Poe tasks: `poe_tasks.toml` (`webapp-*`)
- Webapp README (env vars, run modes): `airbyte-ops-webapp/README.md`
