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
100 changes: 100 additions & 0 deletions .github/workflows/web-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
name: Web E2E

on:
pull_request:
branches: [main]
paths:
- 'web/**'
- 'src/runtime/**'
- '.github/workflows/web-e2e.yml'
workflow_dispatch:

jobs:
playwright:
name: Build SPA + boot backend + Playwright
runs-on: ubuntu-latest
timeout-minutes: 25

env:
OLLAMA_API_KEY: ""
OPENROUTER_API_KEY: ""
AZURE_OPENAI_KEY: ""
AZURE_DEPLOYMENT: ""
AZURE_ENDPOINT: https://ci-dummy.example/
EXTERNAL_MCP_URL: https://ci-dummy.example/
EXT_TOKEN: ci-dummy

steps:
- name: Checkout
uses: actions/checkout@v6.0.2

- name: Set up Python 3.11
uses: actions/setup-python@v6.2.0
with:
python-version: "3.11"

- name: Set up uv
uses: astral-sh/setup-uv@v6.7.0
with:
version: "0.11.7"
enable-cache: true

- name: Install Python deps (locked)
run: uv sync --frozen --extra dev

- name: Set up Node 22
uses: actions/setup-node@v6.0.0
with:
node-version: "22"
cache: npm
cache-dependency-path: web/package-lock.json

- name: Install web deps (locked)
working-directory: web
run: npm ci

- name: Build SPA
working-directory: web
run: npm run build

- name: Install Playwright browsers
working-directory: web
run: npx playwright install --with-deps chromium

- name: Boot backend (serves SPA + API)
run: |
mkdir -p logs
nohup uv run uvicorn runtime.api:app --port 8000 > logs/uvicorn.log 2>&1 &
echo $! > .backend.pid
for i in $(seq 1 60); do
if curl -sf http://localhost:8000/api/v1/ui/hints >/dev/null; then
echo "backend ready after ${i}s"
exit 0
fi
sleep 1
done
echo "backend failed to start"
tail -200 logs/uvicorn.log
exit 1

- name: Run Playwright
working-directory: web
env:
E2E_BASE_URL: http://localhost:8000
run: npx playwright test --reporter=list

- name: Stop backend
if: always()
run: |
if [ -f .backend.pid ]; then kill "$(cat .backend.pid)" 2>/dev/null || true; fi

- name: Upload Playwright report (on failure)
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: |
web/playwright-report
web/test-results
logs/uvicorn.log
retention-days: 7
59 changes: 59 additions & 0 deletions .github/workflows/web.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Web CI

on:
push:
branches: [main]
paths:
- 'web/**'
- '.github/workflows/web.yml'
pull_request:
branches: [main]
paths:
- 'web/**'
- '.github/workflows/web.yml'

defaults:
run:
working-directory: web

jobs:
quality:
name: Lint / Type-check / Test / Build / Size
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v6.0.2

- name: Set up Node 22
uses: actions/setup-node@v6.0.0
with:
node-version: "22"
cache: npm
cache-dependency-path: web/package-lock.json

- name: Install (locked)
run: npm ci

- name: Lint
run: npm run lint

- name: Type-check
run: npm run typecheck

- name: Unit tests with coverage
run: npx vitest run --coverage

- name: Production build
run: npm run build

- name: Bundle-size budget
run: npm run check:size

- name: Upload bundle (PR builds only)
if: github.event_name == 'pull_request'
uses: actions/upload-artifact@v4
with:
name: web-dist
path: web/dist
retention-days: 7
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ htmlcov/
# --- Build artifacts -------------------------------------------------
# dist/app.py is the single-file ship target — tracked, regenerated by
# scripts/build_single_file.py.
# dist/airgap/ is the composed v2.0 air-gap deploy folder produced by
# scripts/package_airgap.py — local-only, never committed.
dist/airgap/

# --- Runtime data (user-specific, not source) ------------------------
# SQLite metadata DB and FAISS vector index live under /tmp by default
Expand Down Expand Up @@ -76,6 +79,8 @@ docs/*
!docs/09-build-deploy-release.md
!docs/10-known-risks-and-todos.md
!docs/11-agent-handoff.md
!docs/RELEASE.md
!docs/REACT_UI_PARITY.md
!docs/adr/
!docs/adr/*.md
REVIEW_*.md
Expand Down
26 changes: 21 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,17 @@ uv run pytest tests/ -x
# Run the incident-management app via the CLI entrypoint
uv run python -m runtime --config config/incident_management.yaml

# Streamlit UI
# Backend API (serves the React SPA at / when web/dist exists)
uv run uvicorn runtime.api:get_app --factory --port 8000

# React UI (Vite dev server; proxy /api/v1 -> :8000)
cd web && npm ci && npm run dev # http://localhost:5173

# Production: build once, then the backend serves it at http://localhost:8000
cd web && npm run build && cd ..
uv run uvicorn runtime.api:get_app --factory --port 8000

# Legacy Streamlit UI (deprecated in v2; banner inside)
ASR_LOG_LEVEL=INFO uv run streamlit run src/runtime/ui.py --server.port 37777
```

Expand All @@ -50,10 +60,16 @@ Set provider keys in `.env` (`OLLAMA_API_KEY`, `OPENROUTER_API_KEY`,
contributor loop: setup, regenerating `dist/`, adding a runtime
module.
- **[`docs/AIRGAP_INSTALL.md`](docs/AIRGAP_INSTALL.md)** —
air-gapped / internal-mirror install procedure.
air-gapped / internal-mirror install procedure (includes the v2
React UI air-gap layout).
- **[`docs/RELEASE.md`](docs/RELEASE.md)** — cutting a release
candidate; `npm run build` + `scripts/package_airgap.py` + git tag.

## Status

`main` carries v1.0 → v1.5. v2.0 (React UI replacing the Streamlit
prototype) is the next big move. See `docs/DESIGN.md` § 13 for the
milestone history and § 14 for the pending list.
`main` carries v1.0 → v1.5. **v2.0.0-rc1** ships the React UI
(Vite + React 19 + TS + Tailwind v4 + Radix primitives) in `web/`
which replaces the Streamlit prototype. The legacy Streamlit UI
remains shippable behind a deprecation banner until parity is
verified (`docs/REACT_UI_PARITY.md`). See `docs/DESIGN.md` § 13
for the milestone history.
35 changes: 35 additions & 0 deletions docs/AIRGAP_INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,38 @@ uv lock --check # exit 0 = in sync; non-zero = regenerate with `uv lock`
installs on any host (HARD-02 / CONCERNS C2).
- Ship vendored wheels as a separate tarball if your host has no mirror at
all; populate `~/.cache/uv` (or `UV_CACHE_DIR`) before running step 3.

## v2.0 — React UI in the air-gap payload

The React SPA in `web/` is built ahead of time and shipped as static
assets alongside the Python bundle. The backend serves it from `/`
via `runtime.api_static.mount_static_assets` so there is no separate
Node process on the deploy host.

```bash
# On a host with internet (one time, or each release):
cd web && npm ci && npm run build
cd ..
uv run python scripts/build_single_file.py # framework + app bundles
uv run python scripts/package_airgap.py # composes dist/airgap/

# Ship dist/airgap/ to the air-gap host. Layout:
# dist/airgap/
# app.py # flattened framework + incident-management
# ui.py # legacy Streamlit (deprecation banner)
# web/ # built React SPA (Vite output)
# README.txt
```

On the air-gap host:

```bash
ASR_WEB_DIST=./web \
python -m uvicorn app:get_app --factory --port 8000
# open http://<host>:8000/
```

`ASR_WEB_DIST` is read by `runtime.api_static.mount_static_assets`.
If unset, the backend falls back to `<bundle>/web/dist` (legacy dev
layout). When neither resolves, the SPA mount is skipped silently and
the API still serves on `/api/v1/*`.
63 changes: 63 additions & 0 deletions docs/RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Release process — v2.0

The React UI lives at `web/`. The Python framework lives at `src/runtime/`.
A v2.0.0-rc1 release ships **both** as a single tag.

## Cut a release candidate

1. Verify `web/package.json` version matches the tag you intend to cut.
```bash
jq -r .version web/package.json
```
For v2.0.0-rc1 the answer should be `2.0.0-rc1`.

2. Regenerate the framework bundles (HARD-08 requirement):
```bash
uv run python scripts/build_single_file.py
git diff --exit-code dist/ # must be clean
```

3. Build the React SPA:
```bash
cd web && npm ci && npm run build
```

4. Compose the air-gap deploy payload:
```bash
uv run python scripts/package_airgap.py --out dist/airgap
```
Output: `dist/airgap/{app.py, ui.py, web/, README.txt}` — local-only,
never committed. Drop it on the deploy host.

5. Sanity-check the bundle gates locally:
```bash
uv run pytest -x
uv run ruff check src/ tests/
uv run pyright src/runtime
(cd web && npm run lint && npm run typecheck && npm run test:unit && npm run build && npm run check:size)
```

6. Tag the release. Tagging always happens on `main` after the final PR
merge, never on a feature branch:
```bash
git switch main
git pull --ff-only
git tag v2.0.0-rc1 -m "v2.0.0-rc1: React UI"
git push origin v2.0.0-rc1
```

## Bump for the next RC

When cutting `v2.0.0-rc2` (or `v2.0.0`):

1. `web/package.json` — update the `version` field.
2. Repeat the build + tag steps above.

Avoid editing `package.json` mid-PR for an unrelated change; the version
field changes only in a release-prep PR.

## Streamlit prototype

`dist/ui.py` (legacy Streamlit) ships in the deploy folder until the
React UI hits 100% parity (see `docs/REACT_UI_PARITY.md`). Streamlit
displays a deprecation banner; the React UI is the supported path.
Loading